The Relayer network acts as an intermediary, forwarding private transactions on behalf of users and covering the associated gas costs. Users pay a small premium on top of the gas cost from their private balance to the Relayer's private balance in exchange for the service.
Relayers are only needed for transactions that originate from a 0zk address (i.e. not needed for Shield transactions). Through Relayers, private transactions within the RAILGUN privacy system remain unlinked to public addresses, ensuring anonymity even during interactions with smart contracts.
Relayers broadcast their fees and accept transactions via the Waku Network (as opposed to using HTTP requests) so that nobody can snoop on a user's IP address when submitting a transaction.
import { RailgunWakuRelayerClient, RelayerConnectionStatusCallback, RelayerDebugger, RelayerOptions,} from'@railgun-community/waku-relayer-client';import { Chain, ChainType, RelayerConnectionStatus,} from'@railgun-community/shared-models';conststatusCallback:RelayerConnectionStatusCallback= ( chain:Chain, status:RelayerConnectionStatus,) => {// Handle connection status update};constrelayerDebugger:RelayerDebugger= {log: (msg:string) => {// Log a debug message },error: (err:Error) => {// Log a debug error },};constinitRelayerClient=async ( statusCallback:RelayerConnectionStatusCallback, relayerDebugger:RelayerDebugger) => {constchain:Chain= { type:ChainType.EVM; id: 1; // Chain number, 1 for Ethereum }constpubSubTopic='/waku/2/railgun-relayer'; // The waku topic that Relayers publish to. This value is the defaultconstadditionalDirectPeers= []; // Optional direct connection to broadcasting RelayersconstpeerDiscoveryTimeout=60000; // Timeout to discover RelayersconstrelayerOptions:RelayerOptions= { pubSubTopic, additionalDirectPeers, peerDiscoveryTimeout, };awaitRailgunWakuRelayerClient.start( chain, relayerOptions, statusCallback, relayerDebugger, );}// Call this somewhere on app launchawaitinitRelayerClient(statusCallback, RelayerDebugger);
The initialized RailgunWakuRelayerClient will continue monitoring the connection to the Relayer network throughout the app session. It will call the statusCallback function with updates, and you must wait to use a Relayer until the status is Connected.
Finding a Relayer
When you are connected to the Relayer network and you are ready to submit a private transaction, you must first select a token from the user's private balance to pay the Relayer fee (likely from user input).
Next, we need to find the best Relayer based on the user's selected fee token and the fee amounts that the Relayers are broadcasting.
import { RailgunWakuRelayerClient,} from'@railgun-community/waku-relayer-client';import { Chain, ChainType, SelectedRelayer,} from'@railgun-community/shared-models';constchain:Chain= { type:ChainType.EVM; id: 1; // Chain number, 1 for Ethereum}constfeeTokenAddress='0x123...123'; // Address of token that the user will pay the Relayer fee in// Only set to true if you are making a cross-contract call.// For instance, if you are using a Recipe from the Cookbook. See "Cross-Contract Calls" in the "Transactions" section.constuseRelayAdapt=false;constrelayer:Optional<SelectedRelayer> =RailgunWakuRelayerClient.findBestRelayer( chain, feeTokenAddress, useRelayAdapt,);
If no Relayer is available that accepts fees in the selected token, then the findBestRelayer function will return undefined.
Calculating the Relayer Fee
Once we have a SelectedRelayer, we need to calculate the total Relayer fee for our transaction based on the current gas price, gas estimate of the transaction, and the fee that the Relayer charges.
import { ..., calculateMaximumGas, TransactionGasDetails,} from'@railgun-community/shared-models';...constselectedRelayer=...; // From the previous stepconsttokenFeePerUnitGas=BigInt(selectedRelayer.tokenFee.feePerUnitGas);constoneUnitGas=10n**18n;// See "Transactions" > "Private Transfers" > "Private ERC-20 Transfers"// for an example of how to format transactionGasDetailsconsttransactionGasDetails:TransactionGasDetails=...;constmaximumGas=calculateMaximumGas(transactionGasDetails);constrelayerFee= (tokenFeePerUnitGas * maximumGas) / oneUnitGas;
Submit via Relayer
import { RailgunPopulateTransactionResponse} from'@railgun-community/shared-models';const populateResponse: RailgunPopulateTransactionResponse = ...; // See the examples in the "Transactions" section for getting this value
constoverallBatchMinGasPrice:bigint=...; // See the examples in the "Transactions" section for getting this valueconstselectedRelayer=...; // Same as examples aboveconstchain:Chain=...; // Same as examples aboveconstuseRelayAdapt=...; // Same as examples aboveconstnullifiers:string[] =populateResponse.nullifiers ?? [];constrelayerTransaction=awaitRelayerTransaction.create(populateResponse.transaction.to,populateResponse.transaction.data,selectedRelayer.railgunAddress,selectedRelayer.tokenFee.feesID, chain, nullifiers, overallBatchMinGasPrice, useRelayAdapt,);consttxHash=awaitrelayerTransaction.send();