Sample Read Request Contract
This section explains how to create a simple cross-chain read request contract by integrating the Router Gateway contracts. The assumption made here is that a random contract already exists on the destination chain that retrieves a certain value from the source chain.
Step 1) Installing the dependencies
Install the evm-gateway
contracts with either of the following commands:
yarn add @routerprotocol/evm-gateway-contracts
npm install @routerprotocol/evm-gateway-contracts
Make sure you're using the latest version of the Gateway contracts.
Step 2) Instantiating the contract
Import IGateway.sol
from @routerprotocol/evm-gateway-contracts/contracts
:
//SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0 <0.9.0;
import "@routerprotocol/evm-gateway-contracts/contracts/IGateway.sol";
contract ReadCall {
}
Step 3) Defining the state variables and the constructor
IGateway public gatewayContract;
address public owner;
uint256 public value;
event ReceivedData(uint256 value);
constructor(
address payable gatewayAddress,
string memory feePayerAddress
) {
owner = msg.sender;
gatewayContract = IGateway(gatewayAddress);
gatewayContract.setDappMetadata(feePayerAddress);
}
- Create an instance of the
gatewayContract
of typeIGateway
. This will be the contract that will route the request to the destination chain. - Create a variable
owner
of typeaddress
which will be used for access control. - Create a variable
value
of typeuint256
which will be used for storing the value that we read from the destination chain. Note that one can read any kind of data from the destination chain contract. - Create an event
ReceivedData
with a parametervalue
of typeuint256
. It will be emitted when the data is received from the destination chain. - Create a constructor with the
gatewayAddress
and thefeePayer
address in string format and set these variables inside the constructor.
Step 4) Setting the fee payer address through the setDappMetadata
function
function setDappMetadata(
string memory FeePayer
) public {
require(msg.sender == owner, "Only owner can set the metadata");
gatewayContract.setDappMetadata(FeePayer);
}
To allow your dApp to send cross-chain transactions, you must specify the fee payer address on the Router chain. This can be done by calling the setDappMetadata
function on the Gateway contract, and passing the fee payer address as a parameter. Once the fee payer address is set, the fee payer must provide approval on the Router chain to confirm their willingness to pay fees for your dApp.
It's important to note that any fee refunds will be credited to the fee payer address specified in the setDappMetadata
function.
Step 5) Setting the Gateway address through the setGateway
function
function setGateway(address gateway) external {
require(msg.sender == owner, "only owner");
gatewayContract = IGateway(gateway);
}
This is an administrative function which sets the address of the Gateway contract. This function should be invoked whenever the Router's Gateway contract gets updated.
Step 6) Sending a read request to the destination chain
Lets say there is a Multiplication
contract on the destination chain with a function getResult()
that takes a uint256
number as parameter and multiplies it with 2 and returns the result.
contract Multiplication {
function getResult(uint256 num) external view returns (uint256) {
return num * 2;
}
}
To send a read request to the destination chain to get the multiplication of any number, create a getResult
function along with an interface.
Next, encode a function call using that interface in the following way:
interface IMultiplication {
function getResult(uint256 num) external view returns (uint256)
}
function sendReadRequest(
string calldata destChainId,
string calldata destinationContractAddress,
bytes calldata requestMetadata,
uint256 _value
) public payable {
bytes memory packet = abi.encodeCall(IMultiplication.getResult, (_value));
bytes memory requestPacket = abi.encode(destinationContractAddress, packet);
gatewayContract.iSend{ value: msg.value }(
1,
0,
string(""),
destChainId,
requestMetadata,
requestPacket
);
}
The parameters:
1) destChainId
- Chain ID of the destination chain in string format.
2) destinationContractAddress
- Address of the contract on the destination chain in string format.
3) requestMetadata
- The detailed documentation for the requestMetadata
function can be found here.
To create a read request payload packet, one can encode the function call using the abi.encodeCall
function in the way provided in the above snippet. Now, to create the final request packet, just abi.encode
the destination contract address along with the payload packet. Once this is done, we can generate a cross-chain read request by calling the iSend
function of the Gateway contract with the required parameters.
Step 7) Handling the Acknowledgment
Once the read request is executed on the destination chain, the requested data is sent along with an acknowledgment to the source chain. To handle the acknowledgment, the applications need to include an iAck()
function in the contract.
function iAck(
uint256 ,//requestIdentifier,
bool ,//execFlag,
bytes memory execData
) external {
value = abi.decode(execData, (uint256));
emit ReceivedData( value);
}
The function named
iAck
should be created with the same function signature as specified in the documentation. This function is called by the Gateway contract on the source chain and the function name and parameters should not be changed to avoid any failed requests. Further information about this function can be found here.The
requestIdentifier
parameter received in theiAck
function contains the nonce that was generated by the Gateway contract when the request was initiated on the source chain.The
execFlag
tells the execution status of the cross-chain request on the destination chain andexecData
consists of the abi-encoded value returned from theiReceive
function.If the execution is successful on the destination chain:
execFlag
-[true]
execData
-(abi.encode(<return_value>))
Since the return value is
uint256
, thisexecData
can be decoded using abi decoding in the following way:uint256 val = abi.decode(execData, (uint256));
If the execution fails on the destination chain:
execFlag
-[false]
execData
-[abi.encode(<error>)]
In this way, one can create a simple cross-chain read request contract using Router CrossTalk.
Full read request contract example (to be deployed on the intended source chain)
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "@routerprotocol/evm-gateway-contracts/contracts/IGateway.sol";
interface IMultiplication {
function getResult(uint256 num) external view returns (uint256);
}
contract ReadCall {
IGateway public gatewayContract;
address public owner;
uint256 public value;
event ReceivedData( uint256 value);
constructor(
address payable gatewayAddress,
string memory feePayerAddress
) {
owner = msg.sender;
gatewayContract = IGateway(gatewayAddress);
gatewayContract.setDappMetadata(feePayerAddress);
}
/// @notice function to set the fee payer address on Router Chain.
/// @param feePayerAddress address of the fee payer on Router Chain.
function setDappMetadata(string memory feePayerAddress) external {
require(msg.sender == owner, "only owner");
gatewayContract.setDappMetadata(feePayerAddress);
}
/// @notice function to set the Router Gateway Contract.
/// @param gateway address of the gateway contract.
function setGateway(address gateway) external {
require(msg.sender == owner, "only owner");
gatewayContract = IGateway(gateway);
}
/// @notice function to get the request metadata to be used while initiating cross-chain request
/// @return requestMetadata abi-encoded metadata according to source and destination chains
function getRequestMetadata(
uint64 destGasLimit,
uint64 destGasPrice,
uint64 ackGasLimit,
uint64 ackGasPrice,
uint128 relayerFees,
uint8 ackType,
bool isReadCall,
bytes memory asmAddress
) public pure returns (bytes memory) {
bytes memory requestMetadata = abi.encodePacked(
destGasLimit,
destGasPrice,
ackGasLimit,
ackGasPrice,
relayerFees,
ackType,
isReadCall,
asmAddress
);
return requestMetadata;
}
function sendReadRequest(
string calldata destChainId,
string calldata destinationContractAddress,
bytes calldata requestMetadata,
uint256 _value
) public payable {
bytes memory packet = abi.encodeCall(IMultiplication.getResult, (_value));
bytes memory requestPacket = abi.encode(destinationContractAddress, packet);
gatewayContract.iSend{ value: msg.value }(
1,
0,
string(""),
destChainId,
requestMetadata,
requestPacket
);
}
function iAck(
uint256 ,//requestIdentifier,
bool ,//execFlag,
bytes memory execData
) external {
value = abi.decode(execData, (uint256));
emit ReceivedData( value);
}
}
Multiplication contract example for reading contract state (to be deployed on the intended destination chain)
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "./IMultiplication.sol";
contract Multiplication {
function getResult(uint256 num) external view returns (uint256) {
return num * 2;
}
}