NEW

CCIP is now live for all developers. See what's new.

Using CCIP local simulator in RemixIDE

In this guide, you will test the Chainlink CCIP getting started guide locally in RemixIDE. You will deploy a CCIP sender contract and a CCIP receiver contract, and interact with the local simulator to send data from the sender contract to the receiver contract.

Prerequisites

RemixIDE is an online development environment that allows you to write, deploy, and test smart contracts. By default RemixIDE does not persist the files that you open from an external source. To save file you will need to manually create a workspace and copy the files into the workspace.

  1. Open the RemixIDE in your browser.

  2. Create a new workspace.

  3. Copy the content of the Test CCIP Local Simulator contract into a new file in the workspace.

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.19;
    
    // solhint-disable no-unused-import
    import {CCIPLocalSimulator} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol";
    
  4. Copy the content of the Sender contract into a new file in the workspace.

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.19;
    
    import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
    import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
    import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.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 {
        // Custom errors to provide more descriptive revert messages.
        error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance.
    
        // Event emitted when a message is sent to another chain.
        event MessageSent(
            bytes32 indexed messageId, // The unique ID of the CCIP message.
            uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
            address receiver, // The address of the receiver on the destination chain.
            string text, // The text being sent.
            address feeToken, // the token address used to pay CCIP fees.
            uint256 fees // The fees paid for sending the CCIP message.
        );
    
        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 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.
        /// @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 indicating no tokens are being sent
                extraArgs: Client._argsToBytes(
                    // Additional arguments, setting gas limit
                    Client.EVMExtraArgsV1({gasLimit: 200_000})
                ),
                // Set the feeToken  address, indicating LINK will be used for fees
                feeToken: address(s_linkToken)
            });
    
            // 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;
        }
    }
    
  5. Copy the content of the Receiver contract into a new file in the workspace.

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.19;
    
    import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
    import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.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 emitted when a message is received from another chain.
        event MessageReceived(
            bytes32 indexed messageId, // The unique ID of the message.
            uint64 indexed sourceChainSelector, // The chain selector of the source chain.
            address sender, // The address of the sender from the source chain.
            string text // The text that was received.
        );
    
        bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
        string private s_lastReceivedText; // Store the last received text.
    
        /// @notice Constructor initializes the contract with the router address.
        /// @param router The address of the router contract.
        constructor(address router) CCIPReceiver(router) {}
    
        /// handle a received message
        function _ccipReceive(
            Client.Any2EVMMessage memory any2EvmMessage
        ) internal override {
            s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
            s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text
    
            emit MessageReceived(
                any2EvmMessage.messageId,
                any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector)
                abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address,
                abi.decode(any2EvmMessage.data, (string))
            );
        }
    
        /// @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);
        }
    }
    

At this point, you should have three files in your workspace:

  • TestCCIPLocalSimulator.sol: The file imports the Chainlink CCIP local simulator contract.

  • Sender.sol: The file contains the Sender contract that interacts with CCIP to send data to the Receiver contract.

  • Receiver.sol: The file contains the Receiver contract that receives data from the Sender contract.

Deploy the contracts

  1. Compile the contracts.

  2. Under the Deploy & Run Transactions tab, make sure Remix VM in the Environment list is selected. Remix will use a sandbox blockchain in the browser to deploy the contracts.

  3. Deploy the CCIP local simulator:

    1. Select the TestCCIPLocalSimulator.sol file in the file explorer.

    2. In the Contract list, select CCIPLocalSimulator.

    3. Click the Deploy button.

    4. The CCIPLocalSimulator is shown in the Deployed Contracts section.

    5. In the list of functions, click on configuration function to retrieve the configuration details for the pre-deployed contracts and services needed for local CCIP simulations

      RemixIDE CCIP Local Simulator configuration
  4. You will interact with the LINK token contract to fund the sender contract with LINK tokens. The LINK token contract is pre-deployed in the local simulator configuration. Hence, load the LINK token contract instance:

    1. Select LinkToken in the Contract list.
    2. Fill in At Address with the address of the LINK token contract from the CCIPLocalSimulator configuration.
    3. Click on the At Address button.
    4. The LinkToken contract is shown in the Deployed Contracts section.
  5. Deploy the Sender.sol contract:

    1. Select the Sender.sol file in the file explorer.
    2. In the Contract list, select Sender.
    3. Under the Deploy section, fill in the constructor parameters:
      • _router: The address of the sourceRouter contract from the CCIPLocalSimulator configuration.
      • _link: The address of the LINK token contract from the CCIPLocalSimulator configuration.
    4. Click the Deploy button.
    5. The Sender contract is shown in the Deployed Contracts section.
  6. Deploy the Receiver.sol contract:

    1. Select the Receiver.sol file in the file explorer.
    2. In the Contract list, select Receiver.
    3. Under the Deploy section, fill in the constructor parameters:
      • _router: The address of the destinationRouter contract from the CCIPLocalSimulator configuration.
    4. Click the Deploy button.
    5. The Receiver contract is shown in the Deployed Contracts section.

Transfer data from the sender to the receiver

  1. Fund the sender contract with LINK tokens to pay for CCIP fees:

    1. Copy the address of the Sender contract from the Deployed Contracts section.
    2. In the CCIPLocalSimulator contract, fill in the requestLINKFromFaucet function with:
      • to: The address of the Sender contract.
      • amount: The amount of LINK tokens to transfer. For instance 1000000000000000000.
    3. Click the Transact button.
  2. Send data from the sender contract to the receiver contract:

    1. Copy the address of the Receiver contract from the Deployed Contracts section.

    2. In the Sender contract, fill in the sendMessage function with:

      • destinationChainSelector: The destination chain selector. You can find it in the CCIPLocalSimulator configuration.
      • receiver: The address of the Receiver contract.
      • text: The text to send. For instance Hello!.
    3. RemixIDE fails to estimate the gas properly for the sendMessage function. Hence, you need to manually set the gas limit to 3000000.

      RemixIDE CCIP Local sendMessage force gas limit
    4. Click the Transact button.

  3. Check the receiver contract to verify the data transfer:

    1. In the Receiver contract, click on the getLastReceivedMessageDetails function.

    2. The getLastReceivedMessageDetails function returns the text sent from the Sender contract.

      RemixIDE CCIP Local receivedMessage

Next steps

You have successfully tested the CCIP getting started guide within few minutes using RemixIDE. Testing locally is useful to debug your contracts and fix any issues before testing them on testnets, saving you time and resources. As an exercise, you can try any of the CCIP guides using the local simulator in RemixIDE.

Stay updated on the latest Chainlink news