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 repogit clone git@github.com: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 contractforge create \
--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: 0x5DB9A7629912EBF95876228C24A848de0bfB43A9
Transaction 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 processedcast 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:
- Initializes the provider for your L1
- Gets the account nonce from the forwarder contract
- Estimates the gas for the destination contract function
- Creates the EIP712 message
- Signs the typed message
- Recover the signer address to verify the signature
- Sends the signed message to the gas relayer
- 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
mkdir gasless-ts
cd gasless-ts
yarn init
yarn add ethers@5.7.2 @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.GAS_RELAYER_RPC_URL=""
PUBLIC_SUBNET_RPC_URL=""
FORWARDER_ADDRESS=""
DOMAIN_NAME=""
DOMAIN_VERSION=""
REQUEST_TYPE=""
# request_suffix = {suffix_type} {suffix_name}) = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
# suffix_type = bytes32
# suffix_name = ABCDEFGHIJKLMNOPQRSTGSN
SUFFIX_TYPE=""
SUFFIX_NAME=""
# Address of the counter contract that you deployed in getting started section
COUNTER_CONTRACT_ADDRESS=""
# Private key of the account that will send the transaction
PRIVATE_KEY=""
3. Create a src
directory and new file increment.ts
mkdir src
cd src
touch increment.ts
4. Add the following code to increment.ts
import { BigNumber } from "@ethersproject/bignumber";
import "dotenv/config";
import axios from "axios";
import { ethers } from "ethers";
const SUBNET_RPC_URL = process.env.PUBLIC_SUBNET_RPC_URL || "";
const RELAYER_URL = process.env.GAS_RELAYER_RPC_URL || "";
const FORWARDER_ADDRESS = process.env.FORWARDER_ADDRESS || "";
const COUNTER_CONTRACT_ADDRESS = process.env.COUNTER_CONTRACT_ADDRESS || "";
const DOMAIN_NAME = process.env.DOMAIN_NAME || ""; // e.g. domain
const DOMAIN_VERSION = process.env.DOMAIN_VERSION || ""; // e.g. 1
const REQUEST_TYPE = process.env.REQUEST_TYPE || ""; // e.g. Message
// request_suffix = {suffix_type} {suffix_name}) = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
// suffix_type = bytes32
// suffix_name = ABCDEFGHIJKLMNOPQRSTGSN
// request_suffix = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
const SUFFIX_TYPE = process.env.SUFFIX_TYPE || "";
const SUFFIX_NAME = process.env.SUFFIX_NAME || "";
const REQUEST_SUFFIX = `${SUFFIX_TYPE} ${SUFFIX_NAME})`; // e.g. bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
const PRIVATE_KEY = process.env.PRIVATE_KEY || "";
interface MessageTypeProperty {
name: string;
type: string;
}
interface MessageTypes {
EIP712Domain: MessageTypeProperty[];
[additionalProperties: string]: MessageTypeProperty[];
}
function getEIP712Message(
domainName: string,
domainVersion: string,
chainId: number,
forwarderAddress: string,
data: string,
from: string,
to: string,
gas: BigNumber,
nonce: BigNumber
) {
const types: MessageTypes = {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
[REQUEST_TYPE]: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "gas", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "data", type: "bytes" },
{ name: "validUntilTime", type: "uint256" },
{ name: SUFFIX_NAME, type: SUFFIX_TYPE },
],
};
const message = {
from: from,
to: to,
value: String("0x0"),
gas: gas.toHexString(),
nonce: nonce.toHexString(),
data,
validUntilTime: String(
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
),
[SUFFIX_NAME]: Buffer.from(REQUEST_SUFFIX, "utf8"),
};
const result = {
domain: {
name: domainName,
version: domainVersion,
chainId: chainId,
verifyingContract: forwarderAddress,
},
types: types,
primaryType: REQUEST_TYPE,
message: message,
};
return result;
}
// ABIs for contracts
const FORWARDER_GET_NONCE_ABI = [
{
inputs: [
{
internalType: "address",
name: "from",
type: "address",
},
],
name: "getNonce",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
];
const COUNTER_CONTRACT_INCREMENT_ABI = [
{
inputs: [],
name: "increment",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
];
async function main() {
const provider = new ethers.providers.JsonRpcProvider(SUBNET_RPC_URL);
const account = new ethers.Wallet(PRIVATE_KEY, provider);
// get network info from node
const network = await provider.getNetwork();
// get forwarder contract
const forwarder = new ethers.Contract(
FORWARDER_ADDRESS,
FORWARDER_GET_NONCE_ABI,
provider
);
// get current nonce in forwarder contract
const forwarderNonce = await forwarder.getNonce(account.address);
// get counter contract
const gaslessCounter = new ethers.Contract(
COUNTER_CONTRACT_ADDRESS,
COUNTER_CONTRACT_INCREMENT_ABI,
account
);
// get function selector for gasless "increment" method
const fragment = gaslessCounter.interface.getFunction("increment");
const func = gaslessCounter.interface.getSighash(fragment);
const gas = await gaslessCounter.estimateGas.increment();
console.log("estimated gas usage for increment(): " + gas);
const eip712Message = getEIP712Message(
DOMAIN_NAME,
DOMAIN_VERSION,
network.chainId,
forwarder.address,
func,
account.address,
COUNTER_CONTRACT_ADDRESS,
BigNumber.from(gas),
forwarderNonce
);
const { EIP712Domain, ...types } = eip712Message.types;
const signature = await account._signTypedData(
eip712Message.domain,
types,
eip712Message.message
);
const verifiedAddress = ethers.utils.verifyTypedData(
eip712Message.domain,
types,
eip712Message.message,
signature
);
if (verifiedAddress != account.address) {
throw new Error("Fail sign and recover");
}
const tx = {
forwardRequest: eip712Message,
metadata: {
signature: signature.substring(2),
},
};
const rawTx = "0x" + Buffer.from(JSON.stringify(tx)).toString("hex");
// wrap relay tx with json rpc request format.
const requestBody = {
id: 1,
jsonrpc: "2.0",
method: "eth_sendRawTransaction",
params: [rawTx],
};
// send relay tx to relay server
try {
const result = await axios.post(RELAYER_URL, requestBody, {
headers: {
"Content-Type": "application/json",
},
});
const txHash = result.data.result;
console.log(`txHash : ${txHash}`);
const receipt = await provider.waitForTransaction(txHash);
console.log(`tx mined : ${JSON.stringify(receipt, null, 2)}`);
} catch (e: any) {
console.error("error occurred while sending transaction:", e.response.data);
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
5. Run the scriptyarn 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
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 infoGAS_RELAYER_RPC_URL=""
PUBLIC_SUBNET_RPC_URL=""
FORWARDER_ADDRESS=""
DOMAIN_NAME=""
DOMAIN_VERSION=""
REQUEST_TYPE=""
# request_suffix = {suffix_type} {suffix_name}) = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
# suffix_type = bytes32
# suffix_name = ABCDEFGHIJKLMNOPQRSTGSN
SUFFIX_TYPE=""
SUFFIX_NAME=""
# Address of the counter contract that you deployed in getting started section
COUNTER_CONTRACT_ADDRESS=""
# Private key of the account that will send the transaction
PRIVATE_KEY=""
3. Create a new file increment.py
and paste the following codeimport asyncio
import json
import os
from typing import Any
from aiohttp.client import ClientSession
from dotenv import load_dotenv
from eth_account import Account
from eth_account.messages import encode_typed_data
from web3 import AsyncHTTPProvider, AsyncWeb3
from web3.middleware import async_geth_poa_middleware
FORWARDER_ABI = '[{"inputs":[{"internalType":"address","name":"from","type":"address"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]'
COUNTER_ABI = '[{"inputs":[],"name":"increment","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
load_dotenv()
RELAYER_URL = os.getenv("GAS_RELAYER_RPC_URL")
SUBNET_RPC_URL = os.getenv("PUBLIC_SUBNET_RPC_URL")
FORWARDER_ADDRESS = os.getenv("FORWARDER_ADDRESS")
COUNTER_CONTRACT_ADDRESS = os.getenv("COUNTER_CONTRACT_ADDRESS")
DOMAIN_NAME = os.getenv("DOMAIN_NAME")
DOMAIN_VERSION = os.getenv("DOMAIN_VERSION")
REQUEST_TYPE = os.getenv("REQUEST_TYPE")
# request_suffix = {suffix_type} {suffix_name}) = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
# suffix_type = bytes32
# suffix_name = ABCDEFGHIJKLMNOPQRSTGSN
# request_suffix = bytes32 ABCDEFGHIJKLMNOPQRSTGSN)
SUFFIX_TYPE = os.getenv("SUFFIX_TYPE")
SUFFIX_NAME = os.getenv("SUFFIX_NAME")
REQUEST_SUFFIX = f"{SUFFIX_TYPE} {SUFFIX_NAME})"
PRIVATE_KEY = os.getenv("PRIVATE_KEY")
async def relay(
account_from: Account,
tx: dict[str, Any],
nonce: int,
w3: AsyncWeb3,
session: ClientSession,
) -> str:
chain_id = await w3.eth.chain_id
# Define domain and types for EIP712 message
domain = {
"name": DOMAIN_NAME,
"version": DOMAIN_VERSION,
"chainId": chain_id,
"verifyingContract": FORWARDER_ADDRESS,
}
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
REQUEST_TYPE: [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "gas", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "data", "type": "bytes"},
{"name": "validUntilTime", "type": "uint256"},
{"name": SUFFIX_NAME, "type": SUFFIX_TYPE},
],
}
# Populate data for EIP712 message
message = {
"from": tx["from"],
"to": tx["to"],
"value": hex(tx["value"]),
"gas": hex(tx["gas"]),
"nonce": hex(nonce),
"data": w3.to_bytes(hexstr=tx["data"]),
"validUntilTime": hex(
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
),
SUFFIX_NAME: REQUEST_SUFFIX.encode("utf-8"),
}
# Sign the typed data
eip712_message = {
"types": types,
"domain": domain,
"primaryType": REQUEST_TYPE,
"message": message,
}
signed_msg = w3.eth.account.sign_typed_data(
account_from.key, full_message=eip712_message
)
# Verify the signature
encoded_message = encode_typed_data(full_message=eip712_message)
recovered = w3.eth.account.recover_message(
encoded_message, signature=signed_msg.signature
)
if recovered != account_from.address:
raise Exception("Signing and recovering failed")
# Prepare payload for the relay server
relay_tx = {
"forwardRequest": eip712_message,
"metadata": {"signature": signed_msg.signature.hex()[2:]},
}
hex_raw_tx = w3.to_hex(w3.to_bytes(text=(w3.to_json(relay_tx))))
payload = json.dumps(
{
"jsonrpc": "2.0",
"method": "eth_sendRawTransaction",
"params": [hex_raw_tx],
"id": 1,
}
)
# Send the relay request
response = await session.post(
url=RELAYER_URL, data=payload, headers={"Content-Type": "application/json"}
)
data = await response.json(content_type=None)
return data["result"] # tx hash
async def main():
# Initialize provider
session = ClientSession()
w3 = AsyncWeb3(AsyncHTTPProvider(SUBNET_RPC_URL))
w3.middleware_onion.inject(async_geth_poa_middleware, layer=0)
await w3.provider.cache_async_session(session)
account_from = w3.eth.account.from_key(PRIVATE_KEY)
# Fetch forwarder nonce for the account
forwarder_contract = w3.eth.contract(
address=w3.to_checksum_address(FORWARDER_ADDRESS), abi=FORWARDER_ABI
)
nonce = await forwarder_contract.functions.getNonce(account_from.address).call()
# Estimate gas for the increment function
counter_contract = w3.eth.contract(
address=w3.to_checksum_address(COUNTER_CONTRACT_ADDRESS), abi=COUNTER_ABI
)
tx = await counter_contract.functions.increment().build_transaction(
{"from": account_from.address}
)
# Relay the transaction
tx_hash = await relay(account_from, tx, nonce, w3, session)
# Wait for the receipt and verify status
receipt = await w3.eth.wait_for_transaction_receipt(tx_hash)
print(receipt)
if receipt.status != 1:
raise Exception("Transaction failed")
if __name__ == "__main__":
asyncio.run(main())
4. Run the scriptpython 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 theERC2771Recipient
contract or implement the IERC2771Recipient
interface.import "@opengsn/contracts/src/ERC2771Recipient.sol";
contract 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.sender = _msgSender(); // not "msg.sender"
3. In the constructor of your contract, set the trusted forwarder address to the address of the forwarder contract.constructor(address _forwarder) {
_setTrustedForwarder(_forwarder);
}
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 asddresses 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.