Futaba is still under development and its use may change before its official release.
We do not recommend using Futaba in a production environment at this time.
You can easily retrieve data from other chains as long as you implement send() and receiveQuery().
The structure of the data and what it actually does is described in the protocol section.
Here we will try to implement a contract that gets the balance of any token in any other chain from the src chain and mints a new token of that amount.
The sample code itself is in this repository and can be cloned.
Create new project
In this example we will use hardhat;
$npxhardhat88888888888888888888888888888888888888888888888888888888888b.888d888.d8888888888b.8888b.888888888888"88b 888P"d88" 888 888 "88b"88b 888888 888 .d888888 888 888 888 888 888 .d888888 888888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.888 888 "Y888888888"Y88888 888 888 "Y888888"Y888👷 Welcome to Hardhat v2.9.9 👷? What do you want to do? … Create a JavaScript project❯ Create a TypeScript project Create an empty hardhat.config.js Quithe
Choose a Typescript project. Choose y on all of the prompts.
First, install the OpenZeppelin package;
npminstall@openzeppelin/contracts
yarnadd@openzeppelin/contracts
Next, install the relay SDK for gelato;
npminstall@gelatonetwork/relay-sdk^4.0.0
yarnadd@gelatonetwork/relay-sdk^4.0.0
Install dotenv to protect your private key needed to deploy your contract;
npminstalldotenv
yarnadddotenv
At the root of your project, create a new .env file. Here you will store your private key used to deploy your contract.
Update .env with the following line:
PRIVATE_KEY = <YOUR-PRIVATE-KEY-HERE>
Define the interface
First, we need to define the structs involved in the query;
Define an IGateway.sol to execute the query() function;
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.9;import"./QueryType.sol";/** * @title Gateway interface * @notice This interfece is an endpoint for executing query */interface IGateway {/** * @notice This contract is an endpoint for executing query * @param queries query data * @param lightClient The light client contract address * @param callBack The callback contract address * @param message Data used when executing callback */functionquery(QueryType.QueryRequest[] memory queries,address lightClient,address callBack,bytescalldata message ) externalpayable;}
Also, create IReceiver.sol that defines the receiveQuery() function to receive the results of the query;
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.9;import"./QueryType.sol";/** * @title Receiver interface * @notice This interface is for the user to receive the results of the query */interface IReceiver {/** * @notice This function is used to receive the results of the query * @param results The results of the query * @param queries The query data * @param message Data to be used in the callback sent at the time of the request */functionreceiveQuery(bytes32 queryId,bytes[] memory results,QueryType.QueryRequest[] memory queries,bytesmemory message ) external;}
Send function
From here, the contract for the actual request is defined. First, define the function that will execute the query request.
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.9;import"@openzeppelin/contracts/access/Ownable.sol";import"@openzeppelin/contracts/token/ERC20/ERC20.sol";import"./QueryType.sol";import"./IReceiver.sol";import"./IGateway.sol";/** * @title BalanceQuery * @notice Example contract to execute and receive queries */contractBalanceQueryisOwnable, ERC20, IReceiver {// Gateway Contract endpointaddresspublic gateway;// Address of contract to verify storage proofaddresspublic ligthClient;constructor(address_gateway,address_lightClient)ERC20("Futaba Test Token", "FTB") { gateway = _gateway; ligthClient = _lightClient; }/** @notice Query execution via gateway contract * @param queries Information for doing a query, see QueryType.sol * @param decimals Decimals of tokens */functionsendQuery(QueryType.QueryRequest[] memory queries,uint256[] calldata decimals ) publicpayable {// Encode the decimal number of the token and the address to mint the tokenbytesmemory message = abi.encode(decimals, msg.sender);// Check to see if fee has been sentrequire(msg.value >0,"Insufficient fee");// Execute query from gateway contractIGateway(gateway).query{value: msg.value}( queries, ligthClient,address(this),// callback address message ); }}
Receive function
Create a function that receives the result of the query in the same Contract.
/** @notice Receive query results * @param queryId Unique id that can refer to query results, etc. * @param results Results of query in byte format * @param queries Information for doing a query, see QueryType.sol * @param message Encoded data for non-query use */functionreceiveQuery(bytes32 queryId,bytes[] memory results,QueryType.QueryRequest[] memory queries,bytesmemory message ) publiconlyGateway {/* Decode the data stored when requesting the query (in this case the decimal number of the token and the address to mint) */ (uint256[] memory decimals,address sender) = abi.decode( message, (uint256[],address) );// Mint the total token balance receiveduint256 amount;for (uint i =0; i < results.length; i++) {uint256 balance =uint256(bytes32(results[i]));uint256 decimal = decimals[i]; amount += balance * (10** (18- decimal)); }_mint(sender, amount); }/** @notice Allow data to be received only from gateway contract */modifieronlyGateway() {require(msg.sender == gateway,"Only gateway can call this function"); _;}
Now that you have finished writing the sample code, compile and deploy it.
Please check here for the target network and contract address.
Executing the Transaction
Here we create a script to execute the query from the client side.
import { ethers } from"hardhat";import { BigNumber } from"ethers";import { concat, hexZeroPad, keccak256 } from"ethers/lib/utils";import { GelatoRelay } from"@gelatonetwork/relay-sdk";import { QueryType } from"../typechain-types/contracts/BalanceQuery";// Initialize Gelatoconstrelay=newGelatoRelay();asyncfunctionmain() {constbalanceQuery=awaitethers.getContractAt("BalanceQuery",DEPLOYED_ADDRESS)// Calculate storage slot for a particular user's token balanceconstslot=concat([hexZeroPad(ANY_WALLET_ADDRESS,32),hexZeroPad(BigNumber.from(0).toHexString(),32), ]);constslot2=concat([hexZeroPad(ANY_WALLET_ADDRESS,32),hexZeroPad(BigNumber.from(0).toHexString(),32), ]);// USDC on Goerliconstsrc="0xA2025B15a1757311bfD68cb14eaeFCc237AF5b43"// Struct contains the id of the chain, the target contract, the height of the specific block, and the target storage slot.constqueries:QueryType.QueryRequestStruct[] = [ { dstChainId:5, to: src, height:8947355, slot:keccak256(slot) }, { dstChainId:5, to: src, height:8975344, slot:keccak256(slot2) }, ]try {// Estimated gas cost to pay for Gelato's relayerconstfee=awaitrelay.getEstimatedFee(80001,"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",BigNumber.from("1000000"),true)consttx=awaitbalanceQuery.sendQuery(queries, [6,6], { gasLimit:1000000, value:fee.mul(120).div(100) })awaittx.wait()console.log(tx) } catch (error) {console.error(error) }}main().catch((error) => {console.error(error);process.exitCode =1;});