Typescript Example
An API key is required to use the infrastructure to power gasless transactions. Visit https://app.rallyprotocol.com/ to generate both Amoy and Mainnet Polygon API keys.
Required payload:
For definitions, refer to: https://docs.opengsn.org/jsdoc/jsdoc-client.html
// 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 } }
);
Example from the SDK client:
https://github.com/rally-dfs/rly-network-mobile-sdk/blob/main/src/gsnClient/gsnClient.ts
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);
};
Last updated