# Get Started with CCIP (EVM)
Source: https://docs.chain.link/ccip/evm/getting-started
Last Updated: 2026-07-23

> For the complete documentation index, see [llms.txt](/llms.txt).

Get started with Chainlink CCIP 2.0, as a user and as a developer

In this guide, you will:

1. Bridge tokens from one chain to another as a user, via Chainlink Transporter.
2. Send and receive cross-chain messages as a developer, using Chainlink CCIP 2.0 infrastructure.

## Bridge Tokens Using Chainlink Transporter

The Transporter application is a user-friendly tool built by Chainlink Labs to allow users to bridge tokens from one chain to another seamlessly,
leveraging the power of CCIP 2.0's infrastructure.

- [Transporter UI (Testnet)](https://test.transporter.io/)
- [Transporter UI (Mainnet)](https://www.transporter.io/)

### 1. Connect Your Wallet <a id="connect-your-wallet"></a>

1. The Transporter UI supports most popular wallets, please make sure you have some funds on your desired chain in an account that is imported into one of these wallets.
2. Click the `Connect +` button on the top right of the Transporter UI to connect your wallet.

![Chainlink CCIP Transporter Connect Wallet](/images/ccip/tutorials/getting-started/Transporter-ui-1.png)

### 2. Select Lane and Approve Allowance <a id="select-lane-approve-allowance"></a>

1. Select the source and destination chains from the network dropdowns.
2. This tutorial uses the testnet version of the Transporter UI, your view may differ depending on the version of the UI you are using, and the types of lanes that are
   available in the moment.

![Chainlink CCIP Transporter Select Source and Destination Chains](/images/ccip/tutorials/getting-started/Transporter-ui-2.png)

3. The UI might ask you to `Approve` an allowance on the amount of tokens that you are trying to bridge. We highly recommend users to stick to a one-time
   allowance and not approve an unlimited allowance, unless the user explicitly intends to do so.

![Chainlink CCIP Transporter Approve Allowance](/images/ccip/tutorials/getting-started/Transporter-ui-3.png)

### 3. (Optional): Pay Gas Fee using LINK <a id="pay-gas-using-link"></a>

1. The UI allows you to pay for the transfer using `LINK` tokens, as shown in the screenshot attached.
2. If you select this option, the UI might ask for an additional approval of the `LINK` token, to pay for the gas fees.

![Chainlink CCIP Transporter Pay Gas Fee using LINK](/images/ccip/tutorials/getting-started/Transporter-ui-4.png)

### 4. Initiate Transfer <a id="initiate-transfer"></a>

1. After all the settings are configured, and allowances approved, click on the `Send` button to initiate the transfer.
2. Approve the transfer in the wallet popup.

![Chainlink CCIP Transporter Initiate Transfer](/images/ccip/tutorials/getting-started/Transporter-ui-5.png)

3. You can check out the status of the transfer in the `Activity` tab of the Transporter UI, or on the [CCIP Explorer](https://ccip.chain.link/).

![Chainlink CCIP Transporter Check Transfer Status](/images/ccip/tutorials/getting-started/Transporter-ui-6.png)

## Send and Receive Cross-Chain Messages Using CCIP

### Before you begin

You will need:

- Basic [Solidity](https://soliditylang.org/) and [smart contract deployment](/quickstarts/deploy-your-first-contract) experience

- One [wallet](https://metamask.io/) funded on two CCIP-supported EVM testnets: [Ethereum Sepolia and Arbitrum Sepolia](/ccip/directory/testnet). You will need `ETH` and `LINK` on *Ethereum Sepolia*, and `ETH` on *Arbitrum Sepolia*.

- Choose one of the following development environments:
  - **[Hardhat 3](https://hardhat.org/docs/getting-started)**
  - **[Foundry](https://book.getfoundry.sh/)**
  - **[Remix](https://remix-ide.readthedocs.io/en/latest/)**

### Examine the code

This section goes through the code for the `Sender` and `Receiver` contracts needed to complete the tutorial.
We will use the same contracts for all three development environments.

#### 1. Sender code <a id="sender-code"></a>

The sender contract interacts with CCIP to send data cross-chain. Key elements are explained below.

```sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {ExtraArgsCodec} from "@chainlink/contracts-ccip/contracts/libraries/ExtraArgsCodec.sol";
import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

/// @title - A simple contract for sending string data across chains.
contract Sender is OwnerIsCreator {
  error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);

  event MessageSent(
    bytes32 indexed messageId,
    uint64 indexed destinationChainSelector,
    address receiver,
    string text,
    address feeToken,
    uint256 fees
  );

  IRouterClient private s_router;
  LinkTokenInterface private s_linkToken;

  /// @notice Constructor initializes the contract with the router address.
  /// @param _router The address of the router contract.
  /// @param _link The address of the LINK token contract.
  constructor(
    address _router,
    address _link
  ) {
    s_router = IRouterClient(_router);
    s_linkToken = LinkTokenInterface(_link);
  }

  /// @notice Sends data to receiver on the destination chain.
  /// @dev Assumes your contract has sufficient LINK to cover fees.
  /// @param destinationChainSelector The identifier (aka selector) for the destination blockchain.
  /// @param receiver The address of the recipient on the destination blockchain.
  /// @param text The string text to be sent.
  /// @return messageId The ID of the message that was sent.
  function sendMessage(
    uint64 destinationChainSelector,
    address receiver,
    string calldata text
  ) external onlyOwner returns (bytes32 messageId) {
    // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
    Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
      receiver: abi.encode(receiver), // ABI-encoded receiver address
      data: abi.encode(text), // ABI-encoded string
      tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array — no tokens are being sent
      extraArgs: ExtraArgsCodec._getBasicEncodedExtraArgsV3(
        200_000, // Gas limit for the callback on the destination chain
        bytes4(0) // Default finality (wait for full finalization)
      ),
      feeToken: address(s_linkToken) // Pay CCIP fees in LINK
    });

    // Get the fee required to send the message
    uint256 fees = s_router.getFee(destinationChainSelector, evm2AnyMessage);

    if (fees > s_linkToken.balanceOf(address(this))) {
      revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
    }

    // Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
    s_linkToken.approve(address(s_router), fees);

    // Send the message through the router and store the returned message ID
    messageId = s_router.ccipSend(destinationChainSelector, evm2AnyMessage);

    // Emit an event with message details
    emit MessageSent(messageId, destinationChainSelector, receiver, text, address(s_linkToken), fees);

    // Return the message ID
    return messageId;
  }
}
```

##### Initializing the contract

When deploying the contract, you define the router address and the LINK contract address of the blockchain where you deploy. The router provides:

- The `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee) to estimate CCIP fees.
- The `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend) to send CCIP messages.

##### Sending data

The `sendMessage` function:

1. Constructs a CCIP message using the `EVM2AnyMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage):
   - `receiver`: ABI-encoded destination address.
   - `data`: ABI-encoded string payload.
   - `tokenAmounts`: Empty array (no tokens sent).
   - `extraArgs`: Encoded via `ExtraArgsCodec._getBasicEncodedExtraArgsV3` with a `gasLimit` of `200000` and default finality (`bytes4(0)`).
   - `feeToken`: The LINK token address, indicating fees are paid in LINK.

2. Computes the fees via the router's `getFee` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#getfee).

3. Verifies the contract's LINK balance covers the fees.

4. Approves the router to spend the required LINK.

5. Dispatches the message via the router's `ccipSend` [function](/ccip/evm/api-reference/v2.0.0/i-router-client#ccipsend).

> **CAUTION: Best Practices**
>
> This example is simplified for educational purposes. For production code, follow these best practices:
>
> - **Keep extraArgs mutable**: In production, build extraArgs off-chain and pass them into your contract calls, or store them in a storage variable that can be updated. Never hardcode them. This keeps your contract compatible with future extraArgs versions and lets one deployment serve both pre-2.0 and CCIP 2.0 lanes without redeploying. See [Best Practices: Using extraArgs](/ccip/evm/concepts/best-practices#using-extraargs).
>
> - **Validate the destination chain**: Always ensure the destination chain is valid and supported before sending messages. See [Verify destination chain](/ccip/evm/concepts/best-practices#verify-destination-chain).
>
> - **Use ExtraArgsV3 on CCIP 2.0 lanes**: New integrations should encode [ExtraArgsV3](/ccip/evm/api-reference/v2.0.0/extra-args-codec), which carries the gas limit, requested finality, Cross-Chain Verifiers (CCVs), and executor settings (*More on `ExtraArgsV3` below*)
>
> - Legacy [GenericExtraArgsV2](/ccip/evm/api-reference/v2.0.0/client#genericextraargsv2) arguments are still accepted, but only the gasLimit field is honored. allowOutOfOrderExecution is deprecated in v2.0.
>
> - **Understand CCIP Service Limits**: Review the [CCIP Service Limits](/ccip/evm/service-limits) for constraints on message data size, execution gas, and the number of tokens per transaction. If your requirements exceed these limits, you may need to [contact the Chainlink Labs Team](https://chain.link/ccip-contact).

#### 2. Receiver code <a id="receiver-code"></a>

The receiver contract interacts with CCIP to receive data on the destination chain.

```sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {CCIPReceiver} from "@chainlink/contracts-ccip/contracts/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {FinalityCodec} from "@chainlink/contracts-ccip/contracts/libraries/FinalityCodec.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

/// @title - A simple contract for receiving string data across chains.
contract Receiver is CCIPReceiver {
  event MessageReceived(bytes32 indexed messageId, uint64 indexed sourceChainSelector, address sender, string text);

  bytes32 private s_lastReceivedMessageId;
  string private s_lastReceivedText;

  /// @notice Constructor initializes the contract with the router address.
  /// @param router The address of the router contract.
  constructor(
    address router
  ) CCIPReceiver(router) {}

  /// @notice Handle a received message.
  function _ccipReceive(
    Client.Any2EVMMessage memory any2EvmMessage
  ) internal override {
    s_lastReceivedMessageId = any2EvmMessage.messageId;
    s_lastReceivedText = abi.decode(any2EvmMessage.data, (string));

    emit MessageReceived(
      any2EvmMessage.messageId,
      any2EvmMessage.sourceChainSelector,
      abi.decode(any2EvmMessage.sender, (address)),
      abi.decode(any2EvmMessage.data, (string))
    );
  }

  /// @notice Returns the CCVs and finality config for a given source chain.
  /// @dev Override to advertise receiver finality policy to the OffRamp.
  function getCCVsAndFinalityConfig(
    uint64,
    bytes calldata
  )
    external
    view
    override
    returns (
      address[] memory requiredCCVs,
      address[] memory optionalCCVs,
      uint8 optionalThreshold,
      bytes4 allowedFinalityConfig
    )
  {
    return (new address[](0), new address[](0), 0, FinalityCodec.WAIT_FOR_FINALITY_FLAG);
  }

  /// @notice Fetches the details of the last received message.
  /// @return messageId The ID of the last received message.
  /// @return text The last received text.
  function getLastReceivedMessageDetails() external view returns (bytes32 messageId, string memory text) {
    return (s_lastReceivedMessageId, s_lastReceivedText);
  }
}
```

###### Initializing the contract

When you deploy the contract, you define the router address. The receiver inherits from [CCIPReceiver](/ccip/evm/api-reference/v2.0.0/ccip-receiver), which uses the router address.

###### Receiving data

On the destination blockchain:

1. The CCIP Router invokes `ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#ccipreceive). This function is protected by the `onlyRouter` [modifier](/ccip/evm/api-reference/v2.0.0/ccip-receiver#onlyrouter-modifier), ensuring only the router can call it.

2. `ccipReceive` calls the internal `_ccipReceive` [function](/ccip/evm/api-reference/v2.0.0/ccip-receiver#_ccipreceive).

> **CAUTION: \_ccipReceive**
>
> - The receiver contract must implement this function. - The CCIP router uses this function to deliver messages. -
>   Messages will not be delivered if this function is not implemented correctly.

3. `_ccipReceive` receives an `Any2EVMMessage` [struct](/ccip/evm/api-reference/v2.0.0/client#any2evmmessage) containing:
   - The CCIP `messageId`.
   - The `sourceChainSelector`.
   - The `sender` address in bytes format, decoded via [abi.decode](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html).
   - The `data` in bytes format, decoded to a `string`.

> **CAUTION: Recommendations for receiver contracts**
>
> This example is simplified for learning purposes. For production code:
>
> - Validate the source chain.
> - Depending on your use case, validate the sender address.
>
> The receiver inherits from [CCIPReceiver](/ccip/evm/api-reference/v2.0.0/ccip-receiver), which uses the `onlyRouter` [modifier](/ccip/evm/api-reference/v2.0.0/ccip-receiver#onlyrouter-modifier) to ensure only the router can call `ccipReceive`.

### Send a cross-chain message using CCIP

Send and verify a cross-chain message using CCIP in under 10 minutes, with your favorite development framework.

#### Foundry

Best for **Solidity-native** workflows that prefer a modular, powerful scripting framework.

## 1. Bootstrap a new Foundry project <a id="foundry-bootstrap"></a>

1. Open a new terminal in a directory of your choice and run this command to initialize a new Foundry project:

```bash filename="Terminal"
forge init
```

2. Install the required dependencies:

```bash filename="Terminal"
forge install smartcontractkit/chainlink-ccip smartcontractkit/chainlink-evm
```

> **NOTE: Note**
>
> `cast wallet import` will throw an error if an `env` variable with the same name already exists. Use `cast wallet
>   list` to check previously set variables.

3. Use Foundry's `cast` command to create a new keystore for your `PRIVATE_KEY`:

```bash filename="Terminal"
cast wallet import --interactive PRIVATE_KEY
```

And use the `cast wallet list` command to verify:

![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png)

> **CAUTION: Note**
>
> While the Hardhat project pulls RPC URLs via encrypted keystore variables, Foundry supports keystore encryption only
> for private keys — not generic strings.

4. Configure the remappings so that your `foundry.toml` file looks like this:

```toml filename="foundry.toml"

[profile.default]
solc = "0.8.34"
src = "src"
out = "out"
libs = ["lib"]

remappings = [
  "forge-std/=lib/forge-std/src/",
  "@chainlink/contracts-ccip/contracts/=lib/chainlink-ccip/chains/evm/contracts/",
  "@chainlink/contracts/=lib/chainlink-evm/contracts/",
  "@openzeppelin/contracts@5.3.0/utils/introspection/=lib/forge-std/src/interfaces/"
]

[rpc_endpoints]
sepolia = "ENTER_YOUR_SEPOLIA_RPC_URL_HERE"
arbitrumSepolia = "ENTER_YOUR_ARBITRUM_SEPOLIA_RPC_URL_HERE"
```

## 2. Set up the contracts <a id="foundry-contracts"></a>

1. Create a new directory named `src` at the root of the project if it doesn't already exist.
2. Create a new file named `Sender.sol` in this directory and paste the [sender contract code](#sender-code) inside it.
3. Create a new file named `Receiver.sol` in the same directory and paste the [receiver contract code](#receiver-code) inside it.
4. Run the following command to compile the contracts:

```bash filename="Terminal"
forge build
```

## 3. Send a cross-chain message <a id="foundry-send"></a>

1. Create a new directory named `script` at the root of the project if it doesn't already exist.
2. Create a new file named `SendCrossChainMessage.s.sol` in this directory and paste the following code inside it:

```solidity filename="script/SendCrossChainMessage.s.sol"
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.34;

import {Script, console} from "forge-std/Script.sol";
import {Sender} from "../src/Sender.sol";
import {Receiver} from "../src/Receiver.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";

contract SendCrossChainMessage is Script {
    // Ethereum Sepolia configuration
    address constant SEPOLIA_ROUTER = 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59;
    address constant SEPOLIA_LINK = 0x779877A7B0D9E8603169DdbD7836e478b4624789;

    // Arbitrum Sepolia configuration
    address constant ARBITRUM_SEPOLIA_ROUTER = 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165;
    uint64 constant ARBITRUM_SEPOLIA_CHAIN_SELECTOR = 3478487238524512106;

    uint256 ONE_LINK = 1e18;

    function run() public {

        // Load RPC configs from foundry.toml
        uint256 sepoliaFork = vm.createFork(vm.rpcUrl("sepolia"));
        uint256 arbitrumSepoliaFork = vm.createFork(vm.rpcUrl("arbitrumSepolia"));

        // Step 1: Deploy Sender on Sepolia
        console.log("Connecting to Ethereum Sepolia...");
        vm.selectFork(sepoliaFork);
        vm.startBroadcast();

        console.log("\n[Step 1] Deploying Sender contract on Ethereum Sepolia...");
        Sender sender = new Sender(SEPOLIA_ROUTER, SEPOLIA_LINK);
        console.log("Sender deployed on Sepolia:", address(sender));
        console.log(
            string.concat(
                "View on Etherscan: https://sepolia.etherscan.io/address/",
                vm.toString(address(sender))
            )
        );

        // Step 2: Fund Sender with 1 LINK
        console.log("\n[Step 2] Funding Sender with 1 LINK...");
        LinkTokenInterface(SEPOLIA_LINK).transfer(address(sender), ONE_LINK);
        vm.stopBroadcast();
        console.log("Funded Sender with 1 LINK");

        // Step 3: Deploy Receiver on Arbitrum Sepolia
        console.log("\nConnecting to Arbitrum Sepolia...");
        vm.selectFork(arbitrumSepoliaFork);
        vm.startBroadcast();

        console.log("\n[Step 3] Deploying Receiver on Arbitrum Sepolia...");
        Receiver receiver = new Receiver(ARBITRUM_SEPOLIA_ROUTER);
        vm.stopBroadcast();
        console.log("Receiver deployed on Arbitrum Sepolia:", address(receiver));
        console.log(
            string.concat(
                "View on Arbiscan: https://sepolia.arbiscan.io/address/",
                vm.toString(address(receiver))
            )
        );
        console.log("\n .....Copy the receiver address for the verification script.....\n");
        console.log(address(receiver));

        // Step 4: Send cross-chain message (Sepolia -> Arbitrum Sepolia)
        vm.selectFork(sepoliaFork);
        vm.startBroadcast();

        console.log("\n[Step 4] Sending cross-chain message from Sepolia to Arbitrum Sepolia...");
        bytes32 messageId = sender.sendMessage(
            ARBITRUM_SEPOLIA_CHAIN_SELECTOR,
            address(receiver),
            "Hello World from Foundry script!"
        );
        vm.stopBroadcast();

        console.log("Message sent! Check for delivery after a few minutes...");
        console.log("CCIP messageId:");
        console.logBytes32(messageId);
        console.log("View transaction status on CCIP Explorer: https://ccip.chain.link");
    }
}
```

3. Run the following command to send the cross-chain message:

```bash filename="Terminal"
forge script script/SendCrossChainMessage.s.sol:SendCrossChainMessage --broadcast --multi --account PRIVATE_KEY
```

> **NOTE**
>
> - The `--broadcast` flag broadcasts transactions to the network.
> - The `-vvvv` verbosity flag gives detailed runtime logs.
> - The `--multi` flag sends transactions to multiple chains.

## 4. Verify message delivery <a id="foundry-verify"></a>

1. Create a new file named `VerifyCrossChainMessage.s.sol` in the `script` directory and paste the following code inside it:

```solidity filename="script/VerifyCrossChainMessage.s.sol"
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.34;

import {Script, console} from "forge-std/Script.sol";
import {Receiver} from "../src/Receiver.sol";

contract VerifyCrossChainMessage is Script {

    bytes32 constant ZERO_BYTES32 = bytes32(0);

    function run() public {

        address receiverAddress = PASTE_RECEIVER_ADDRESS_HERE;
        require(receiverAddress != address(0), "Set RECEIVER_ADDRESS");

        console.log("Connecting to Arbitrum Sepolia...");
        uint256 arbitrumSepoliaFork = vm.createFork(vm.rpcUrl("arbitrumSepolia"));
        vm.selectFork(arbitrumSepoliaFork);

        console.log("Checking for received message...\n");
        Receiver receiver = Receiver(receiverAddress);

        (bytes32 messageId, string memory text) = receiver
            .getLastReceivedMessageDetails();

        if (messageId == ZERO_BYTES32) {
            console.log("No message received yet.");
            console.log("Please wait a bit longer and try again.");
            revert("No message received yet");
        }

        console.log("Received Message ID:");
        console.logBytes32(messageId);
        console.log(string.concat('Received Text: "', text, '"'));
    }
}
```

2. Run the following command to verify the cross-chain message:

```bash filename="Terminal"
forge script script/VerifyCrossChainMessage.s.sol:VerifyCrossChainMessage
```

#### Hardhat 3

Best for a `TypeScript`-based scripting workflow where you deploy contracts, send a CCIP message, and verify delivery from the command line.

## 1. Bootstrap a new Hardhat project <a id="hardhat-bootstrap"></a>

1. Open a new terminal in a directory of your choice and run this command:

```bash filename="Terminal"
npx hardhat --init
```

Create a project with the following options:

- Hardhat Version: hardhat-3
- Initialize project: At root of the project
- Type of project: A minimal Hardhat project
- Install the necessary dependencies: Yes

2. Install the additional dependencies required by this tutorial:

```bash filename="Terminal"
npm install @chainlink/contracts-ccip @chainlink/contracts viem
npm install --save-dev @nomicfoundation/hardhat-viem @nomicfoundation/hardhat-keystore
```

> **CAUTION: WE HIGHLY RECOMMEND NOT USING PLAINTEXT PRIVATE KEYS**
>
> 1. It is not recommended to store your sensitive keys in plaintext in `.env` files.
> 2. In this tutorial we use in-built keystores for both Hardhat and Foundry for their respective projects.

3. Update `hardhat.config.ts` to use the `hardhat-viem` and `hardhat-keystore` plugins:

```typescript filename="hardhat.config.ts"
import { configVariable, defineConfig } from "hardhat/config"
import hardhatKeystore from "@nomicfoundation/hardhat-keystore"
import hardhatViem from "@nomicfoundation/hardhat-viem"

export default defineConfig({
  plugins: [hardhatViem, hardhatKeystore],
  solidity: {
    version: "0.8.34",
  },
  networks: {
    sepolia: {
      type: "http",
      url: configVariable("SEPOLIA_RPC_URL"),
      accounts: [configVariable("PRIVATE_KEY")],
    },
    arbitrumSepolia: {
      type: "http",
      url: configVariable("ARBITRUM_SEPOLIA_RPC_URL"),
      accounts: [configVariable("PRIVATE_KEY")],
    },
  },
})
```

4. Set the environment variables using `hardhat-keystore`. Run the following commands in succession — Hardhat will ask you to enter a password for the keystore for each variable:

```bash filename="Terminal"
npx hardhat keystore set SEPOLIA_RPC_URL
npx hardhat keystore set ARBITRUM_SEPOLIA_RPC_URL
npx hardhat keystore set PRIVATE_KEY
```

The output of `npx hardhat keystore list` should look like this:

![Hardhat keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-1.png)

## 2. Set up the contracts <a id="hardhat-contracts"></a>

1. Create a new directory named `contracts` for your smart contracts if it doesn't already exist.
2. Create a new file named `Sender.sol` in this directory and paste the [sender contract code](#sender-code) inside it.
3. Create a new file named `Receiver.sol` in the same directory and paste the [receiver contract code](#receiver-code) inside it.
4. Run the following command to compile the contracts:

```bash filename="Terminal"
npx hardhat build
```

## 3. Send a cross-chain message <a id="hardhat-send"></a>

1. Create a new directory named `scripts` at the root of the project if it doesn't already exist.
2. Create a new file named `send-cross-chain-message.ts` in this directory and paste the following code inside it:

```typescript filename="scripts/send-cross-chain-message.ts"
import { network } from "hardhat"
import { getContract, parseAbi, parseUnits } from "viem"

// Ethereum Sepolia configuration
const SEPOLIA_ROUTER = "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59"
const SEPOLIA_LINK = "0x779877A7B0D9E8603169DdbD7836e478b4624789"

// Arbitrum Sepolia configuration
const ARBITRUM_SEPOLIA_ROUTER = "0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165"
const ARBITRUM_SEPOLIA_CHAIN_SELECTOR = 3478487238524512106n

// Connect to Ethereum Sepolia
console.log("Connecting to Ethereum Sepolia...")
const sepoliaNetwork = await network.connect("sepolia")

// Connect to Arbitrum Sepolia
console.log("Connecting to Arbitrum Sepolia...")
const arbitrumSepoliaNetwork = await network.connect("arbitrumSepolia")

// Step 1: Deploy Sender on Sepolia
console.log("\n[Step 1] Deploying Sender contract on Ethereum Sepolia...")

const sender = await sepoliaNetwork.viem.deployContract("Sender", [SEPOLIA_ROUTER, SEPOLIA_LINK])
const sepoliaPublicClient = await sepoliaNetwork.viem.getPublicClient()

console.log(`Sender deployed on Sepolia: ${sender.address}`)
console.log(`View on Etherscan: https://sepolia.etherscan.io/address/${sender.address}`)

// Step 2: Fund Sender with LINK
console.log("\n[Step 2] Funding Sender with 1 LINK...")

const [sepoliaWalletClient] = await sepoliaNetwork.viem.getWalletClients()
if (!sepoliaWalletClient) {
  throw new Error("No wallet client available. Check PRIVATE_KEY + network config in hardhat.config.ts.")
}

const linkTokenInterfaceAbi = parseAbi(["function transfer(address to, uint256 value) returns (bool)"])

const link = getContract({
  address: SEPOLIA_LINK,
  abi: linkTokenInterfaceAbi,
  client: { public: sepoliaPublicClient, wallet: sepoliaWalletClient },
})

const transferLinkTx = await link.write.transfer([sender.address, parseUnits("1", 18)])

console.log("LINK token transfer in progress, awaiting confirmation...")
await sepoliaPublicClient.waitForTransactionReceipt({ hash: transferLinkTx, confirmations: 1 })
console.log("Funded Sender with 1 LINK")

// Step 3: Deploy Receiver on Arbitrum Sepolia
console.log("\n[Step 3] Deploying Receiver on Arbitrum Sepolia...")

const receiver = await arbitrumSepoliaNetwork.viem.deployContract("Receiver", [ARBITRUM_SEPOLIA_ROUTER])
const arbitrumSepoliaPublicClient = await arbitrumSepoliaNetwork.viem.getPublicClient()

console.log(`Receiver deployed on Arbitrum Sepolia: ${receiver.address}`)
console.log(`View on Arbiscan: https://sepolia.arbiscan.io/address/${receiver.address}`)
console.log(`\n📋 Copy the receiver address since it will be needed to run the verification script 📋\n`)

// Step 4: Send cross-chain message
console.log("\n[Step 4] Sending cross-chain message...")

const sendMessageTx = await sender.write.sendMessage([
  ARBITRUM_SEPOLIA_CHAIN_SELECTOR,
  receiver.address,
  "Hello World from Hardhat script!",
])

console.log("Cross-chain message sent, awaiting confirmation...")
console.log(`Message sent! ✅\nTx hash: ${sendMessageTx}`)
console.log(`View transaction status on CCIP Explorer: https://ccip.chain.link`)
console.log("Run the verification script after a few minutes to check if the message has been received.")
```

3. Run the following command to send the cross-chain message:

```bash filename="Terminal"
npx hardhat run scripts/send-cross-chain-message.ts
```

## 4. Verify message delivery <a id="hardhat-verify"></a>

1. Wait for a few minutes for the message to be delivered to the receiver contract.

2. Create a new file named `verify-cross-chain-message.ts` in the `scripts` directory and paste the following code inside it:

> **NOTE: Paste the Receiver contract address here**
>
> The verification script calls `getLastReceivedMessageDetails` on the receiver contract. It needs the correct address.

```typescript filename="scripts/verify-cross-chain-message.ts"
import { network } from "hardhat"

// Paste the Receiver contract address
const RECEIVER_ADDRESS = ""

console.log("Connecting to Arbitrum Sepolia...")
const arbitrumSepoliaNetwork = await network.connect("arbitrumSepolia")

console.log("Checking for received message...\n")
const receiver = await arbitrumSepoliaNetwork.viem.getContractAt("Receiver", RECEIVER_ADDRESS)

const [messageId, text] = await receiver.read.getLastReceivedMessageDetails()

const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"

if (messageId === ZERO_BYTES32) {
  console.log("No message received yet.")
  console.log("Please wait a bit longer and try again.")
  process.exit(1)
} else {
  console.log(`✅ Message ID: ${messageId}`)
  console.log(`Text: "${text}"`)
}
```

3. Run the following command to verify the cross-chain message:

```bash filename="Terminal"
npx hardhat run scripts/verify-cross-chain-message.ts
```

4. You should see the message ID and text of the last received message printed in the terminal.

#### Remix

Best for **Web3-native** workflows that prefer a browser-based IDE.

## 1. Deploy the sender contract <a id="remix-deploy-sender"></a>

Deploy the `Sender.sol` contract on *Ethereum Sepolia*. To see a detailed explanation of this contract, read the [Sender code](#sender-code) section.

1. Open the Sender.sol contract in Remix.

2. Compile the contract.

3. Deploy the sender contract on *Ethereum Sepolia*:
   1. Open MetaMask and select the *Ethereum Sepolia* network.

   2. In Remix under the **Deploy & Run Transactions** tab, select *Injected Provider - MetaMask* in the **Environment** list. Remix will use the MetaMask wallet to communicate with *Ethereum Sepolia*.

   3. Under the **Deploy** section, fill in the router address and the LINK token contract addresses. You can find both on the [CCIP Directory](/ccip/directory). For *Ethereum Sepolia*, the router address is 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59 and the LINK address is 0x779877A7B0D9E8603169DdbD7836e478b4624789.

      ![Chainlink CCIP deploy sender Ethereum Sepolia](/images/ccip/tutorials/deploy-sender-avalanche-fuji.webp)

   4. Click the **transact** button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to *Ethereum Sepolia*.

   5. After you confirm the transaction, the contract address appears in the **Deployed Contracts** list. Copy your contract address.

      ![Chainlink CCIP deployed sender Ethereum Sepolia](/images/ccip/tutorials/deployed-sender-avalanche-fuji.webp)

   6. Open MetaMask and send 1 LINK to the contract address that you copied. Your contract will pay CCIP fees in LINK.

## 2. Deploy the receiver contract <a id="remix-deploy-receiver"></a>

Deploy the receiver contract on *Arbitrum Sepolia*. You will use this contract to receive data from the sender on *Ethereum Sepolia*. To see a detailed explanation of this contract, read the [Receiver code](#receiver-code) section.

1. [Open the Receiver.sol](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/Receiver.sol) contract in Remix.

   [Open Receiver.sol in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/CCIP/Receiver.sol)

2. Compile the contract.

3. Deploy the receiver contract on *Arbitrum Sepolia*:
   1. Open MetaMask and select the *Arbitrum Sepolia* network.

   2. In Remix under the **Deploy & Run Transactions** tab, make sure the **Environment** is still set to *Injected Provider - MetaMask*.

   3. Under the **Deploy** section, fill in the router address field. For *Arbitrum Sepolia*, the Router address is 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165. You can find the addresses for each network on the [CCIP Directory](/ccip/directory).

      ![Chainlink CCIP Deploy receiver Arbitrum Sepolia](/images/ccip/tutorials/deploy-receiver-sepolia.webp)

   4. Click the **Deploy** button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to make sure you are deploying the contract to *Arbitrum Sepolia*.

   5. After you confirm the transaction, the contract address appears as the second item in the **Deployed Contracts** list. Copy this contract address.

      ![Chainlink CCIP deployed receiver Arbitrum Sepolia](/images/ccip/tutorials/deployed-receiver-sepolia.webp)

You now have one *sender* contract on *Ethereum Sepolia* and one *receiver* contract on *Arbitrum Sepolia*. You sent `1` LINK to the *sender* contract to pay the CCIP fees. Next, send data from the sender contract to the receiver contract.

## 3. Send data <a id="remix-send"></a>

Send a `Hello World!` string from your contract on *Ethereum Sepolia* to the contract you deployed on *Arbitrum Sepolia*:

1. Open MetaMask and select the *Ethereum Sepolia* network.

2. In Remix under the **Deploy & Run Transactions** tab, expand the first contract in the **Deployed Contracts** section.

3. Expand the **sendMessage** function and fill in the following arguments:

   | Argument                 | Description                                                                                                                         | Value (*Arbitrum Sepolia*)     |
   | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
   | destinationChainSelector | CCIP Chain identifier of the target blockchain. You can find each network's chain selector on the [CCIP Directory](/ccip/directory) | 3478487238524512106            |
   | receiver                 | The destination smart contract address                                                                                              | Your deployed contract address |
   | text                     | Any `string`                                                                                                                        | Hello World!                   |

   ![Chainlink CCIP Sepolia send message](/images/ccip/tutorials/fuji-sendmessage.webp)

4. Click the **transact** button to run the function. MetaMask prompts you to confirm the transaction.

> **NOTE: Gas price spikes**
>
> Under normal circumstances, transactions on the Ethereum Sepolia network require significantly fewer tokens to pay for gas. However, during exceptional periods of high gas price spikes, your transactions may fail if not sufficiently funded. In such cases, you may need to fund your contract with additional tokens. We recommend paying for your CCIP transactions in **LINK** tokens (rather than native tokens) as you can obtain extra LINK testnet tokens from [faucets.chain.link](https://faucets.chain.link/). If you encounter a transaction failure due to these gas price spikes, please add additional LINK tokens to your contract and try again.
> Alternatively, you can use a supported testnet other than Sepolia.

5. After the transaction is successful, note the transaction hash. Here is an [example](https://sepolia.etherscan.io/tx/0x113933ec9f1b2e795a1e2f564c9d452db92d3e9a150545712687eb546916e633) of a successful transaction on *Ethereum Sepolia*.

After the transaction is finalized on the source chain, it will take a few minutes for CCIP to deliver the data to *Arbitrum Sepolia* and call the `ccipReceive` function on your receiver contract. You can use the [CCIP explorer](https://ccip.chain.link/) to see the status of your CCIP transaction and then read data stored by your receiver contract.

6. Open the [CCIP explorer](https://ccip.chain.link/) and use the transaction hash that you copied to search for your cross-chain transaction. The explorer provides several details about your request.

   ![Chainlink CCIP Explorer transaction details](/images/ccip/tutorials/ccip-explorer-tx-details.webp)

7. When the status of the transaction is marked with a "Success" status, the CCIP transaction and the destination transaction are complete.

   ![Chainlink CCIP Explorer transaction details success](/images/ccip/tutorials/ccip-explorer-tx-details-success.webp)

## 4. Read data <a id="remix-read"></a>

Read data stored by the receiver contract on *Arbitrum Sepolia*:

1. Open MetaMask and select the *Arbitrum Sepolia* network.
2. In Remix under the **Deploy & Run Transactions** tab, expand the receiver contract deployed on *Arbitrum Sepolia*.
3. Click the **getLastReceivedMessageDetails** function button to read the stored data. In this example, it should be "Hello World!".

   ![Chainlink CCIP Arbitrum Sepolia message details](/images/ccip/tutorials/sepolia-getmessagedetails.webp)

Congratulations! You just sent your first cross-chain data using CCIP 2.0. Next, examine the example code to learn how this contract works.

Once you understand basic transfers, most applications move to [programmable token transfers (PTT)](/ccip/evm/tutorials/application-developers/programmable-token-transfers) to combine value and execution.