Message Passing API

When transferring tokens using Rango cross-chain API, you could pass a random message from the source chain to the destination and call your contract on the destination. In order to do so, you need to pass your contracts on source & destination chains plus an arbitrary hex message. Here is a brief guide on what you need to do in terms of SDK usage and the smart contract side.

1. SDK Usage

You should specify sourceContract, destinationContract and imMessage arguments in both quote and swap methods if you want to pass a message from the source contract to the destination.

const quoteResponse = await rangoClient.quote({
  from: {
    "blockchain": "FANTOM",
    "symbol": "FTM",
    "address": null
  },
  to: {
    "blockchain": "BSC",
    "symbol": "BNB",
    "address": null
  },
  amount: "100000000000000000000",
  messagingProtocols: ['cbridge'],
  sourceContract: "<source contract address>",
  destinationContract: "<destination contract address>",
  imMessage: "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000007E8A8b130272430008eCa062419ACD8B423d339D" 
})

You could also limit messagingProtocols used to a custom list like ['cbridge']. (Please note that as the message is relayed alongside with token in a single transaction if you limit messaging protocols to cbridge, we use the same bridge for transferring tokens.

2. Smart Contract

Both dApp contracts on the source and destination chains should implement IRangoMessageReceiver interface. Rango will call handleRangoMessage function in case of SUCCESS, REFUND_IN_SOURCE or REFUND_IN_DESTINATION.

interface IRangoMessageReceiver {
    enum ProcessStatus { SUCCESS, REFUND_IN_SOURCE, REFUND_IN_DESTINATION }

    function handleRangoMessage(
        address _token,
        uint _amount,
        ProcessStatus _status,
        bytes memory _message
    ) external;
}

And here is a sample dApp contract for the demo purpose. (The demo is very simple and you should consider adding security considerations yourself)

CrosschainSampleApp.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";


interface IRangoMessageReceiver {
    enum ProcessStatus { SUCCESS, REFUND_IN_SOURCE, REFUND_IN_DESTINATION }

    function handleRangoMessage(
        address _token,
        uint _amount,
        ProcessStatus _status,
        bytes memory _message
    ) external;
}

contract CrosschainPurchaseApp is IRangoMessageReceiver {
    address payable constant NULL_ADDRESS = payable(0x0000000000000000000000000000000000000000);

    struct AppMessage { uint assetId; address buyer; }
    enum PurchaseType { BOUGHT, SOLD_OUT }
    event NFTPurchaseStatus(uint assetId, address buyer, PurchaseType purchaseType);

    address payable rangoContract;

    constructor(address payable _rangoContract) {
        rangoContract = _rangoContract;
    }

    receive() external payable { }


    // Source chain
    function buyNFTCrosschain(bytes calldata rangoData) external payable {
        // 1. Do your own logic here

        // 2. send the money via Rango
        (bool success, bytes memory retData) = rangoContract.call{value: msg.value}(rangoData);
        require(success, _getRevertMsg(retData));
    }

    // Destination chain
    function handleRangoMessage(
        address _token,
        uint _amount,
        ProcessStatus _status,
        bytes memory _message
    ) external {
        AppMessage memory m = abi.decode((_message), (AppMessage));

        if (m.assetId < 10) {
            emit NFTPurchaseStatus(m.assetId, m.buyer, PurchaseType.SOLD_OUT);

            // Give the money back to user
            if (_token == NULL_ADDRESS) {
                (bool sent, ) = m.buyer.call{value: _amount}("");
                require(sent, "failed to send native");
            } else {
                SafeERC20.safeTransfer(IERC20(_token), m.buyer, _amount);
            }
        } else {
            emit NFTPurchaseStatus(m.assetId, m.buyer, PurchaseType.BOUGHT);
            // give the purchased asset to user
        }
    }

    function refund(address _tokenAddress, uint256 _amount) external {
        IERC20 ercToken = IERC20(_tokenAddress);
        uint balance = ercToken.balanceOf(address(this));
        require(balance >= _amount, 'Insufficient balance');

        SafeERC20.safeTransfer(IERC20(_tokenAddress), msg.sender, _amount);
    }

    function refundNative(uint256 _amount) external {
        uint balance = address(this).balance;
        require(balance >= _amount, 'Insufficient balance');

        (bool sent, ) = msg.sender.call{value: _amount}("");
        require(sent, "failed to send native");
    }

    function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {
        // If the _res length is less than 68, then the transaction failed silently (without a revert message)
        if (_returnData.length < 68) return 'Transaction reverted silently';

        assembly {
        // Slice the sighash.
            _returnData := add(_returnData, 0x04)
        }
        return abi.decode(_returnData, (string)); // All that remains is the revert string
    }
}

You need to deploy this contract on every chains needed and ask us to white list them in Rango Contract.

As you can see in the code above, here is how dApp calls Rango in the source chain:

// Source chain
function buyNFTCrosschain(bytes calldata rangoData) external payable {
    // 1. Do your own logic here

    // 2. send the money via Rango
    (bool success, bytes memory retData) = rangoContract.call{value: msg.value}(rangoData);
    require(success, _getRevertMsg(retData));
}

And here is how dApp could handle Rango message:

function handleRangoMessage(
    address _token,
    uint _amount,
    ProcessStatus _status,
    bytes memory _message
) external {
    AppMessage memory m = abi.decode((_message), (AppMessage));

    // your logic here
}

Last updated