AvaCloud Gas Relayer Developer Guide

Prerequisite

Gasless Relayer has been set up by following this.

Introduction

Gas relayer service is a meta-transaction relayer following the EIP2771 standard. The specific implementation is based on Gas Station Network. This guide will show you how to use the service and how to implement smart contracts with support for meta-transactions.

Getting Started

After your gas relayer setup in AvaCloud is completed, you’ll have access to the following info in the dashboard:

  • Gas relayer RPC URL
  • Gas relayer wallet address
  • Forwarder contract address
  • Domain name
  • Domain version
  • Request type
  • Request suffix

Keep the dashboard open as you’ll need this info to interact with the gas relayer. Let’s start with deploying gasless counter contract example.

1. Install Rust and make sure the following works:

$rustc --version
>cargo --version

2. Install Foundry, and make sure the following works:

$forge --version
>cast --version

3. Clone and setup the repo

$git clone [email protected]:ava-labs/avalanche-evm-gasless-transaction.git
>cd avalanche-evm-gasless-transaction
>
>git submodule update --init --recursive
>forge update
>cp ./lib/gsn/packages/contracts/src/forwarder/Forwarder.sol src/Forwarder.sol
>cp ./lib/gsn/packages/contracts/src/forwarder/IForwarder.sol src/IForwarder.sol./scripts/build.release.sh

4. Deploy gasless counter contract

$forge create \
>--broadcast \
>--gas-price 700000000000 \
>--priority-gas-price 10000000000 \
>--private-key=<your_private_key> \
>--rpc-url=<subnet_public_rpc_url> \
>src/GaslessCounter.sol:GaslessCounter \
>--constructor-args <forwarder_contract_address>
>
># Sample output:
># Deployer: 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC
># Deployed to: 0x5DB9A7629912EBF95876228C24A848de0bfB43A9Transaction hash: ...

5. Send the gasless transaction. Make sure you use the values from the dashboard.

$./target/release/avalanche-evm-gasless-transaction \
>gasless-counter-increment \
>--gas-relayer-server-rpc-url "<gas_relayer_rpc_url>" \
>--chain-rpc-url "<subnet_public_rpc_url>" \
>--trusted-forwarder-contract-address "<forwarder_contract_address>" \
>--recipient-contract-address "<gasless_counter_contract_address>" \
>--domain-name "<domain_name>" \
>--domain-version "<domain_version>" \
>--type-name "<request_type>" \
>--type-suffix-data "<request_suffix>"

6. Verify that transaction was successfully processed

$cast call \
>--rpc-url=<subnet_public_rpc_url> \
><gasless_counter_contract_address> \
>"getNumber()" | sed -r '/^\s*$/d' | tail -1
># 0x0000000000000000000000000000000000000000000000000000000000000001

Congratulations! You’ve successfully deployed and interacted with a gasless contract through your relayer. Next, let’s see how to interact with gas relayer in code with ethers in javascript and web3py in python.

Usage

The steps to interact with the gas relayer are as follows, and both ethers and python scripts will follow these steps:

  1. Initializes the provider for your L1
  2. Gets the account nonce from the forwarder contract
  3. Estimates the gas for the destination contract function
  4. Creates the EIP712 message
  5. Signs the typed message
  6. Recover the signer address to verify the signature
  7. Sends the signed message to the gas relayer
  8. Fetch the receipt and verify transaction was successful

As we mentioned in the introduction, the gas relayer service is based on the GSN standard and submits all transactions to the forwarder contract for verification. The forwarder contract checks the signature and forwards the transaction to the target contract.

Step 1. To prepare the data for the request to gas relayer, you’ll need to fetch the nonce from the forwarder contract and estimate the gas for destination contract function. To do this, you’ll need to initialize the provider for your L1 with the public RPC URL.

Step 2. Full forwarder interface can be found here and implementation here.

Step 3. Since the forwarder message requires gas to be passed, we need to estimate the gas for the destination contract function. When implementing your own contract logic, make sure that you estimate the gas upfront and pass sufficient gas as a value, otherwise the transaction will fail.

Steps 4. and 5. create the EIP712 typed message and sign it. It is important that you use the correct values available in the dashboard.

Step 6. is optional, but can be used to verify that the signature is correct, before sending a request to the gas relayer.

Step 7. Gas relayer exposes POST endpoint for eth_sendRawTransaction method, which accepts the signed message in the JSON RPC format. The request should be sent to the gas relayer server URL.

Step 8. Gas relayer will return the transaction hash in response, which can be used to fetch the transaction receipt. One should always verify the transaction receipt to ensure that the transaction was successfully executed on chain.

Usage with Typescript ethers library

Prerequisites:

  • Install node v18.16.1
  • Install yarn

1. Create new directory and initialize the project

$mkdir gasless-ts
>cd gasless-ts
>yarn init
>yarn add [email protected] @ethersproject/bignumber dotenv axios @types/node typescript
>yarn tsc --init --rootDir src --outDir ./bin --esModuleInterop --lib ES2019 --module commonjs --noImplicitAny true
>yarn add -D ts-node

2. Create .env from the template below and fill in the values using the AvaCloud dashboard info.

.env
1GAS_RELAYER_RPC_URL=""
2PUBLIC_SUBNET_RPC_URL=""
3FORWARDER_ADDRESS=""
4DOMAIN_NAME=""
5DOMAIN_VERSION=""
6REQUEST_TYPE=""
7
8# request_suffix = {suffix_type} {suffix_name}) = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
9# suffix_type = bytes32
10# suffix_name = ABCDEFGHIJKLMNOPQRSTGSN
11SUFFIX_TYPE=""
12SUFFIX_NAME=""
13
14# Address of the counter contract that you deployed in getting started section
15COUNTER_CONTRACT_ADDRESS=""
16
17# Private key of the account that will send the transaction
18PRIVATE_KEY=""

3. Create a src directory and new file increment.ts

$mkdir srccd srctouch increment.ts

4. Add the following code to increment.ts

increment.ts
1import { BigNumber } from "@ethersproject/bignumber";
2import "dotenv/config";
3import axios from "axios";
4import { ethers } from "ethers";
5
6const SUBNET_RPC_URL = process.env.PUBLIC_SUBNET_RPC_URL || "";
7const RELAYER_URL = process.env.GAS_RELAYER_RPC_URL || "";
8const FORWARDER_ADDRESS = process.env.FORWARDER_ADDRESS || "";
9const COUNTER_CONTRACT_ADDRESS = process.env.COUNTER_CONTRACT_ADDRESS || "";
10const DOMAIN_NAME = process.env.DOMAIN_NAME || ""; // e.g. domain
11const DOMAIN_VERSION = process.env.DOMAIN_VERSION || ""; // e.g. 1
12const REQUEST_TYPE = process.env.REQUEST_TYPE || ""; // e.g. Message
13// request_suffix = {suffix_type} {suffix_name}) = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
14// suffix_type = bytes32
15// suffix_name = ABCDEFGHIJKLMNOPQRSTGSN
16// request_suffix = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
17const SUFFIX_TYPE = process.env.SUFFIX_TYPE || "";
18const SUFFIX_NAME = process.env.SUFFIX_NAME || "";
19const REQUEST_SUFFIX = `${SUFFIX_TYPE} ${SUFFIX_NAME})`; // e.g. bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
20
21const PRIVATE_KEY = process.env.PRIVATE_KEY || "";
22
23interface MessageTypeProperty {
24 name: string;
25 type: string;
26}
27
28interface MessageTypes {
29 EIP712Domain: MessageTypeProperty[];
30 [additionalProperties: string]: MessageTypeProperty[];
31}
32
33function getEIP712Message(
34 domainName: string,
35 domainVersion: string,
36 chainId: number,
37 forwarderAddress: string,
38 data: string,
39 from: string,
40 to: string,
41 gas: BigNumber,
42 nonce: BigNumber
43) {
44 const types: MessageTypes = {
45 EIP712Domain: [
46 { name: "name", type: "string" },
47 { name: "version", type: "string" },
48 { name: "chainId", type: "uint256" },
49 { name: "verifyingContract", type: "address" },
50 ],
51 [REQUEST_TYPE]: [
52 { name: "from", type: "address" },
53 { name: "to", type: "address" },
54 { name: "value", type: "uint256" },
55 { name: "gas", type: "uint256" },
56 { name: "nonce", type: "uint256" },
57 { name: "data", type: "bytes" },
58 { name: "validUntilTime", type: "uint256" },
59 { name: SUFFIX_NAME, type: SUFFIX_TYPE },
60 ],
61 };
62
63 const message = {
64 from: from,
65 to: to,
66 value: String("0x0"),
67 gas: gas.toHexString(),
68 nonce: nonce.toHexString(),
69 data,
70 validUntilTime: String("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
71 [SUFFIX_NAME]: Buffer.from(REQUEST_SUFFIX, "utf8"),
72 };
73
74 const result = {
75 domain: {
76 name: domainName,
77 version: domainVersion,
78 chainId: chainId,
79 verifyingContract: forwarderAddress,
80 },
81 types: types,
82 primaryType: REQUEST_TYPE,
83 message: message,
84 };
85
86 return result;
87}
88
89// ABIs for contracts
90const FORWARDER_GET_NONCE_ABI = [
91 {
92 inputs: [
93 {
94 internalType: "address",
95 name: "from",
96 type: "address",
97 },
98 ],
99 name: "getNonce",
100 outputs: [
101 {
102 internalType: "uint256",
103 name: "",
104 type: "uint256",
105 },
106 ],
107 stateMutability: "view",
108 type: "function",
109 },
110];
111
112const COUNTER_CONTRACT_INCREMENT_ABI = [
113 {
114 inputs: [],
115 name: "increment",
116 outputs: [],
117 stateMutability: "nonpayable",
118 type: "function",
119 },
120];
121
122async function main() {
123 const provider = new ethers.providers.JsonRpcProvider(SUBNET_RPC_URL);
124 const account = new ethers.Wallet(PRIVATE_KEY, provider);
125
126 // get network info from node
127 const network = await provider.getNetwork();
128
129 // get forwarder contract
130 const forwarder = new ethers.Contract(
131 FORWARDER_ADDRESS,
132 FORWARDER_GET_NONCE_ABI,
133 provider
134 );
135
136 // get current nonce in forwarder contract
137 const forwarderNonce = await forwarder.getNonce(account.address);
138
139 // get counter contract
140 const gaslessCounter = new ethers.Contract(
141 COUNTER_CONTRACT_ADDRESS,
142 COUNTER_CONTRACT_INCREMENT_ABI,
143 account
144 );
145
146 // get function selector for gasless "increment" method
147 const fragment = gaslessCounter.interface.getFunction("increment");
148 const func = gaslessCounter.interface.getSighash(fragment);
149
150 const gas = await gaslessCounter.estimateGas.increment();
151 console.log("estimated gas usage for increment(): " + gas);
152
153 const eip712Message = getEIP712Message(
154 DOMAIN_NAME,
155 DOMAIN_VERSION,
156 network.chainId,
157 forwarder.address,
158 func,
159 account.address,
160 COUNTER_CONTRACT_ADDRESS,
161 BigNumber.from(gas),
162 forwarderNonce
163 );
164
165 const { EIP712Domain, ...types } = eip712Message.types;
166 const signature = await account._signTypedData(
167 eip712Message.domain,
168 types,
169 eip712Message.message
170 );
171
172 const verifiedAddress = ethers.utils.verifyTypedData(
173 eip712Message.domain,
174 types,
175 eip712Message.message,
176 signature
177 );
178
179 if (verifiedAddress != account.address) {
180 throw new Error("Fail sign and recover");
181 }
182
183 const tx = {
184 forwardRequest: eip712Message,
185 metadata: {
186 signature: signature.substring(2),
187 },
188 };
189 const rawTx = "0x" + Buffer.from(JSON.stringify(tx)).toString("hex");
190
191 // wrap relay tx with json rpc request format.
192 const requestBody = {
193 id: 1,
194 jsonrpc: "2.0",
195 method: "eth_sendRawTransaction",
196 params: [rawTx],
197 };
198
199 // send relay tx to relay server
200 try {
201 const result = await axios.post(RELAYER_URL, requestBody, {
202 headers: {
203 "Content-Type": "application/json",
204 },
205 });
206 const txHash = result.data.result;
207 console.log(`txHash : ${txHash}`);
208 const receipt = await provider.waitForTransaction(txHash);
209 console.log(`tx mined : ${JSON.stringify(receipt, null, 2)}`);
210 } catch (e: any) {
211 console.error("error occurred while sending transaction:", e.response.data);
212 }
213}
214
215main().catch((error) => {
216 console.error(error);
217 process.exitCode = 1;
218});

5. Run the script

$yarn ts-node -r dotenv/config src/increment.ts

Beside the steps outlined in the general usage section, there are some things to point out.

For interacting with the forwarder contract, script uses minimal forwarder ABI and just utilizes the getNonce function call, which returns the forwarder contract nonce for the given address.

Request suffix that is available in the dashboard needs to be split into type and name. The type is the first part of the suffix, and the name is the rest of the suffix without the last character()). These values are then used in getEip712Message function to create the EIP712 message.

Usage with Python

Prerequisites:

  • Python 3.11 installed

1. Create new directory, setup and activate, virtual environment and install the dependencies

$mkdir gasless-py
>cd gasless-py
>python -m venv venv
>source venv/bin/activate
>pip install web3 asyncio python-dotenv

2. Create .env from the template below and fill in the values using the AvaCloud dashboard info

.env
1GAS_RELAYER_RPC_URL=""
2PUBLIC_SUBNET_RPC_URL=""
3FORWARDER_ADDRESS=""
4DOMAIN_NAME=""
5DOMAIN_VERSION=""
6REQUEST_TYPE=""
7
8# request_suffix = {suffix_type} {suffix_name}) = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
9# suffix_type = bytes32
10# suffix_name = ABCDEFGHIJKLMNOPQRSTGSN
11SUFFIX_TYPE=""
12SUFFIX_NAME=""
13
14# Address of the counter contract that you deployed in getting started section
15COUNTER_CONTRACT_ADDRESS=""
16
17# Private key of the account that will send the transaction
18PRIVATE_KEY=""

3. Create a new file increment.py and paste the following code

increment.py
1import asyncio
2import json
3import os
4from typing import Any
5
6from aiohttp.client import ClientSession
7from dotenv import load_dotenv
8from eth_account import Account
9from eth_account.messages import encode_typed_data
10from web3 import AsyncHTTPProvider, AsyncWeb3
11from web3.middleware import async_geth_poa_middleware
12
13FORWARDER_ABI = '[{"inputs":[{"internalType":"address","name":"from","type":"address"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]'
14COUNTER_ABI = '[{"inputs":[],"name":"increment","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
15
16load_dotenv()
17RELAYER_URL = os.getenv("GAS_RELAYER_RPC_URL")
18SUBNET_RPC_URL = os.getenv("PUBLIC_SUBNET_RPC_URL")
19FORWARDER_ADDRESS = os.getenv("FORWARDER_ADDRESS")
20COUNTER_CONTRACT_ADDRESS = os.getenv("COUNTER_CONTRACT_ADDRESS")
21DOMAIN_NAME = os.getenv("DOMAIN_NAME")
22DOMAIN_VERSION = os.getenv("DOMAIN_VERSION")
23REQUEST_TYPE = os.getenv("REQUEST_TYPE")
24# request_suffix = {suffix_type} {suffix_name}) = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
25# suffix_type = bytes32# suffix_name = ABCDEFGHIJKLMNOPQRSTGSN
26# request_suffix = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
27SUFFIX_TYPE = os.getenv("SUFFIX_TYPE")
28SUFFIX_NAME = os.getenv("SUFFIX_NAME")
29REQUEST_SUFFIX = f"{SUFFIX_TYPE} {SUFFIX_NAME})"
30PRIVATE_KEY = os.getenv("PRIVATE_KEY")
31
32async def relay(
33 account_from: Account,
34 tx: dict[str, Any],
35 nonce: int,
36 w3: AsyncWeb3,
37 session: ClientSession,
38) -> str:
39 chain_id = await w3.eth.chain_id
40 # Define domain and types for EIP712 message
41 domain = {
42 "name": DOMAIN_NAME,
43 "version": DOMAIN_VERSION,
44 "chainId": chain_id,
45 "verifyingContract": FORWARDER_ADDRESS,
46 }
47 types = {
48 "EIP712Domain": [
49 {"name": "name", "type": "string"},
50 {"name": "version", "type": "string"},
51 {"name": "chainId", "type": "uint256"},
52 {"name": "verifyingContract", "type": "address"},
53 ],
54 REQUEST_TYPE: [
55 {"name": "from", "type": "address"},
56 {"name": "to", "type": "address"},
57 {"name": "value", "type": "uint256"},
58 {"name": "gas", "type": "uint256"},
59 {"name": "nonce", "type": "uint256"},
60 {"name": "data", "type": "bytes"},
61 {"name": "validUntilTime", "type": "uint256"},
62 {"name": SUFFIX_NAME, "type": SUFFIX_TYPE},
63 ],
64 }
65 # Populate data for EIP712 message
66 message = {
67 "from": tx["from"],
68 "to": tx["to"],
69 "value": hex(tx["value"]),
70 "gas": hex(tx["gas"]),
71 "nonce": hex(nonce),
72 "data": w3.to_bytes(hexstr=tx["data"]),
73 "validUntilTime": hex(
74 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
75 ),
76 SUFFIX_NAME: REQUEST_SUFFIX.encode("utf-8"),
77 }
78 # Sign the typed data
79 eip712_message = {
80 "types": types,
81 "domain": domain,
82 "primaryType": REQUEST_TYPE,
83 "message": message,
84 }
85 signed_msg = w3.eth.account.sign_typed_data(
86 account_from.key, full_message=eip712_message
87 )
88
89 # Verify the signature
90 encoded_message = encode_typed_data(full_message=eip712_message)
91 recovered = w3.eth.account.recover_message(
92 encoded_message, signature=signed_msg.signature
93 )
94 if recovered != account_from.address:
95 raise Exception("Signing and recovering failed")
96
97 # Prepare payload for the relay server
98 relay_tx = {
99 "forwardRequest": eip712_message,
100 "metadata": {"signature": signed_msg.signature.hex()[2:]},
101 }
102 hex_raw_tx = w3.to_hex(w3.to_bytes(text=(w3.to_json(relay_tx))))
103 payload = json.dumps(
104 {
105 "jsonrpc": "2.0",
106 "method": "eth_sendRawTransaction",
107 "params": [hex_raw_tx],
108 "id": 1,
109 }
110 )
111
112 # Send the relay request
113 response = await session.post(
114 url=RELAYER_URL, data=payload, headers={"Content-Type": "application/json"}
115 )
116 data = await response.json(content_type=None)
117 return data["result"] # tx hash
118
119async def main():
120 # Initialize provider
121 session = ClientSession()
122 w3 = AsyncWeb3(AsyncHTTPProvider(SUBNET_RPC_URL))
123 w3.middleware_onion.inject(async_geth_poa_middleware, layer=0)
124 await w3.provider.cache_async_session(session)
125
126 account_from = w3.eth.account.from_key(PRIVATE_KEY)
127
128 # Fetch forwarder nonce for the account
129 forwarder_contract = w3.eth.contract(
130 address=w3.to_checksum_address(FORWARDER_ADDRESS), abi=FORWARDER_ABI
131 )
132 nonce = await forwarder_contract.functions.getNonce(account_from.address).call()
133
134 # Estimate gas for the increment function
135 counter_contract = w3.eth.contract(
136 address=w3.to_checksum_address(COUNTER_CONTRACT_ADDRESS), abi=COUNTER_ABI
137 )
138 tx = await counter_contract.functions.increment().build_transaction(
139 {"from": account_from.address}
140 )
141
142 # Relay the transaction
143 tx_hash = await relay(account_from, tx, nonce, w3, session)
144
145 # Wait for the receipt and verify status
146 receipt = await w3.eth.wait_for_transaction_receipt(tx_hash)
147 print(receipt)
148 if receipt.status != 1:
149 raise Exception("Transaction failed")
150
151if __name__ == "__main__":
152 asyncio.run(main())

4. Run the script

$python increment.py

Implementing smart contracts

To add support for the transaction to be relayed to your smart contracts, you need to implement the following:

1. Your contract must be an instance of the ERC2771Recipient contract or implement the IERC2771Recipient interface.

1import "@opengsn/contracts/src/ERC2771Recipient.sol";
2
3contract GaslessCounter is ERC2771Recipient {

2. For any function that you want to call via the gas relayer, make sure that msg.sender is not used directly. Instead, use _msgSender() function from IERC2771Recipient interface.

1sender = _msgSender(); // not "msg.sender"

3. In the constructor of your contract, set the trusted forwarder address to the address of the forwarder contract.

1constructor(address _forwarder) {
2 _setTrustedForwarder(_forwarder);
3}

See GaslessCounter.sol for full code example.

Restricting access

Gas relayer supports restricting access to the service by using the allow/deny list. It is used to only allow/deny specific addresses to use the service. The allow/deny list is set in the dashboard and can be updated at any time.

Other examples

See other examples from the community:


For any additional questions, please view our other knowledge base articles or contact a support team member via the chat button. Examples are for illustrative purposes only.

Learn More About AvaCloud | Download Case Studies | Schedule an AvaCloud Demo