Relayers

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.

Use the @railgun-community/waku-relayer-client package to find Relayers and submit transactions anonymously.

For web projects install:

npm install @railgun-community/waku-relayer-client-web@6.0.4

For mobile projects install:

npm install @railgun-community/waku-relayer-client-node@6.0.4

Initialize the Client

import {
  RailgunWakuRelayerClient,
  RelayerConnectionStatusCallback,
  RelayerDebugger,
  RelayerOptions,
} from '@railgun-community/waku-relayer-client';
import {
  Chain,
  ChainType,
  RelayerConnectionStatus,
} from '@railgun-community/shared-models';

const statusCallback: RelayerConnectionStatusCallback = (
  chain: Chain,
  status: RelayerConnectionStatus,
) => {
  // Handle connection status update
};

const relayerDebugger: RelayerDebugger = {
  log: (msg: string) => {
    // Log a debug message
  },
  error: (err: Error) => {
    // Log a debug error
  },
};

const initRelayerClient = async (
  statusCallback: RelayerConnectionStatusCallback,
  relayerDebugger: RelayerDebugger
) => {
  
  const chain: Chain = {
    type: ChainType.EVM;
    id: 1; // Chain number, 1 for Ethereum
  }
  
  const pubSubTopic = '/waku/2/railgun-relayer'; // The waku topic that Relayers publish to. This value is the default
  const additionalDirectPeers = []; // Optional direct connection to broadcasting Relayers
  const peerDiscoveryTimeout = 60000; // Timeout to discover Relayers

  const relayerOptions: RelayerOptions = {
    pubSubTopic,
    additionalDirectPeers,
    peerDiscoveryTimeout,
  };
  
  await RailgunWakuRelayerClient.start(
    chain,
    relayerOptions,
    statusCallback,
    relayerDebugger,
  );
}

// Call this somewhere on app launch
await initRelayerClient(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';

const chain: Chain = {
  type: ChainType.EVM;
  id: 1; // Chain number, 1 for Ethereum
}

const feeTokenAddress = '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.
const useRelayAdapt = false;

const relayer: 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';

...

const selectedRelayer = ...; // From the previous step
const tokenFeePerUnitGas = BigInt(selectedRelayer.tokenFee.feePerUnitGas);
const oneUnitGas = 10n ** 18n;

// See "Transactions" > "Private Transfers" > "Private ERC-20 Transfers"
// for an example of how to format transactionGasDetails
const transactionGasDetails: TransactionGasDetails = ...;

const maximumGas = calculateMaximumGas(transactionGasDetails);

const relayerFee = (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
const overallBatchMinGasPrice: bigint = ...; // See the examples in the "Transactions" section for getting this value

const selectedRelayer = ...; // Same as examples above
const chain: Chain = ...; // Same as examples above
const useRelayAdapt = ...; // Same as examples above
const nullifiers: string[] = populateResponse.nullifiers ?? [];

const relayerTransaction = await RelayerTransaction.create(
  populateResponse.transaction.to,
  populateResponse.transaction.data,
  selectedRelayer.railgunAddress,
  selectedRelayer.tokenFee.feesID,
  chain,
  nullifiers,
  overallBatchMinGasPrice,
  useRelayAdapt,
);
const txHash = await relayerTransaction.send();

Last updated