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