Typescript Example
Last updated
Last updated
An API key is required to use the infrastructure to power gasless transactions. Visit to generate both Amoy and Mainnet Polygon API keys.
For definitions, refer to:
// This typescript type represents the full JSON payload required
// by our relay API endpoint.
type RlyTransactionPayload = {
relayRequest: {
request: {
from: string;
to: string;
value: string;
gas: string;
nonce: string;
data: string;
validUntilTime: string;
};
relayData: {
maxFeePerGas: string;
maxPriorityFeePerGas: string;
transactionCalldataGasUsed: string;
relayWorker: string;
paymaster: string;
forwarder: string;
paymasterData: string;
clientId: string;
};
};
metadata: {
maxAcceptanceBudget: string;
relayHubAddress: string;
signature: string;
approvalData: string;
relayMaxNonce: number;
relayLastKnownNonce: number;
domainSeparatorName: string;
relayRequestId: string;
};
};
// Usage example:
const examplePayload: RlyTransactionPayload = {
relayRequest: {
request: {
from: '0x1e7D51f413Dd60508352846fCa063f05cB423F8C',
to: '0x1C7312Cb60b40cF586e796FEdD60Cf243286c9E9',
value: '0',
gas: '73380',
nonce: '1',
data: '0x0c53c51c0000000000000000000000001e7d51f413dd60508352846fca063f05cb423f8c00000000000000000000000000000000000000000000000000000000000000a017fa37ac5967c642ba02b250f188da83d5613d0e5c0286241ff9aece0503662c2a6146626ce6d182f8b277723d3d638ec12c1fed1f15954ab344a5ecdfba5fd2000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000e75625f0c8659f18caf845eddae30f5c2a49cb000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000',
validUntilTime: '1691256746',
},
relayData: {
maxFeePerGas: '1650000000',
maxPriorityFeePerGas: '1650000000',
transactionCalldataGasUsed: '19296',
relayWorker: '0xb9950b71ec94cbb274aeb1be98e697678077a17f',
paymaster: '0x8b3a505413Ca3B0A17F077e507aF8E3b3ad4Ce4d',
forwarder: '0xB2b5841DBeF766d4b521221732F9B618fCf34A87',
paymasterData: '0x',
clientId: '1',
},
},
metadata: {
maxAcceptanceBudget: '285252',
relayHubAddress: '0xe213A20A9E6CBAfd8456f9669D8a0b9e41Cb2751',
signature:
'0xd3f7b35fc4985b2a52e4c1b47091b1f5ba947a2159d2da07c7c33b3e54fd5d5e3166847e5a6460fb9e69a90e6531399c5c258e332b6fe1c8157c8e06d527452e1b',
approvalData: '0x',
relayMaxNonce: 1424,
relayLastKnownNonce: 1421,
domainSeparatorName: 'GSN Relayed Transaction',
relayRequestId:
'0x000000001204aa7c5e04714775ca00cad9d6f298f8d43caa291b8f8d1b237e39',
},
};
const authHeader = `Bearer: ${yourApiToken}`;
const response = axios.post(
'https://api.rallyprotocol.com/relay',
examplePayload,
{ headers: { Authorization: authHeader } }
);
import type { GsnTransactionDetails, AccountKeypair } from './utils';
import type { RelayRequest } from './EIP712/RelayRequest';
import { handleGsnResponse } from './gsnTxHelpers';
import axios from 'axios';
import type { NetworkConfig } from 'src/network_config/network_config';
import {
estimateGasWithoutCallData,
estimateCalldataCostForRequest,
getSenderNonce,
signRequest,
getRelayRequestID,
//getClientId,
} from './gsnTxHelpers';
import { ethers, providers } from 'ethers';
interface GsnServerConfigPayload {
relayWorkerAddress: string;
relayManagerAddress: string;
relayHubAddress: string;
ownerAddress: string;
minMaxPriorityFeePerGas: string;
maxMaxFeePerGas: string;
minMaxFeePerGas: string;
maxAcceptanceBudget: string;
chainId: string;
networkId: string;
ready: boolean;
version: string;
}
const authHeader = (config: NetworkConfig) => {
return {
Authorization: `Bearer ${config.relayerApiKey || ''}`,
};
};
const updateConfig = async (
config: NetworkConfig,
transaction: GsnTransactionDetails
) => {
const response = await axios.get(`${config.gsn.relayUrl}/getaddr`, {
headers: authHeader(config),
});
const serverConfigUpdate = response.data as GsnServerConfigPayload;
config.gsn.relayWorkerAddress = serverConfigUpdate.relayWorkerAddress;
setGasFeesForTransaction(transaction, serverConfigUpdate);
return { config, transaction };
};
const setGasFeesForTransaction = (
transaction: GsnTransactionDetails,
serverConfigUpdate: GsnServerConfigPayload
) => {
const serverSuggestedMinPriorityFeePerGas = parseInt(
serverConfigUpdate.minMaxPriorityFeePerGas,
10
);
const paddedMaxPriority = Math.round(
serverSuggestedMinPriorityFeePerGas * 1.4
);
transaction.maxPriorityFeePerGas = paddedMaxPriority.toString();
//Special handling for Amoy because of quirk with gas estimate returned by GSN for Amoy
if (serverConfigUpdate.chainId === '80001') {
transaction.maxFeePerGas = paddedMaxPriority.toString();
} else {
transaction.maxFeePerGas = serverConfigUpdate.maxMaxFeePerGas;
}
};
const buildRelayRequest = async (
transaction: GsnTransactionDetails,
config: NetworkConfig,
account: AccountKeypair,
web3Provider: providers.JsonRpcProvider
) => {
//remove call data cost from gas estimate as tx will be called from contract
transaction.gas = estimateGasWithoutCallData(
transaction,
config.gsn.gtxDataNonZero,
config.gsn.gtxDataZero
);
const secondsNow = Math.round(Date.now() / 1000);
const validUntilTime = (
secondsNow + config.gsn.requestValidSeconds
).toString();
const senderNonce = await getSenderNonce(
account.address,
config.gsn.forwarderAddress,
web3Provider
);
const relayRequest: RelayRequest = {
request: {
from: transaction.from,
to: transaction.to,
value: transaction.value || '0',
gas: parseInt(transaction.gas, 16).toString(),
nonce: senderNonce,
data: transaction.data,
validUntilTime,
},
relayData: {
maxFeePerGas: transaction.maxFeePerGas,
maxPriorityFeePerGas: transaction.maxPriorityFeePerGas,
transactionCalldataGasUsed: '',
relayWorker: config.gsn.relayWorkerAddress,
paymaster: config.gsn.paymasterAddress,
forwarder: config.gsn.forwarderAddress,
paymasterData: transaction.paymasterData?.toString() || '0x',
clientId: '1',
},
};
const transactionCalldataGasUsed = await estimateCalldataCostForRequest(
relayRequest,
config.gsn
);
relayRequest.relayData.transactionCalldataGasUsed = parseInt(
transactionCalldataGasUsed,
16
).toString();
return relayRequest;
};
const buildRelayHttpRequest = async (
relayRequest: RelayRequest,
config: NetworkConfig,
account: AccountKeypair,
web3Provider: providers.JsonRpcProvider
) => {
const signature = await signRequest(
relayRequest,
config.gsn.domainSeparatorName,
config.gsn.chainId,
account
);
const approvalData = '0x';
const wallet = new ethers.VoidSigner(
relayRequest.relayData.relayWorker,
web3Provider
);
const relayLastKnownNonce = await wallet.getTransactionCount();
const relayMaxNonce = relayLastKnownNonce + config.gsn.maxRelayNonceGap;
const metadata = {
maxAcceptanceBudget: config.gsn.maxAcceptanceBudget,
relayHubAddress: config.gsn.relayHubAddress,
signature,
approvalData,
relayMaxNonce,
relayLastKnownNonce,
domainSeparatorName: config.gsn.domainSeparatorName,
relayRequestId: '',
};
const httpRequest = {
relayRequest,
metadata,
};
return httpRequest;
};
export const relayTransaction = async (
account: AccountKeypair,
config: NetworkConfig,
transaction: GsnTransactionDetails
) => {
const web3Provider = new ethers.providers.JsonRpcProvider(config.gsn.rpcUrl);
const updatedConfig = await updateConfig(config, transaction);
const relayRequest = await buildRelayRequest(
updatedConfig.transaction,
updatedConfig.config,
account,
web3Provider
);
const httpRequest = await buildRelayHttpRequest(
relayRequest,
updatedConfig.config,
account,
web3Provider
);
const relayRequestId = getRelayRequestID(
httpRequest.relayRequest,
httpRequest.metadata.signature
);
//update request metadata with relayrequestid
httpRequest.metadata.relayRequestId = relayRequestId;
const res = await axios.post(`${config.gsn.relayUrl}/relay`, httpRequest, {
headers: authHeader(config),
});
return handleGsnResponse(res, web3Provider);
};