Shield ERC-20 tokens

Shield one or more ERC-20 tokens into RAILGUN private balances in a single transaction

Imports

import {
  NETWORK_CONFIG,
  NetworkName,
  TXIDVersion,
  type RailgunERC20AmountRecipient,
} from "@railgun-community/shared-models";
import {
  getGasDetailsForTransaction,
  getShieldSignature,
  serializeERC20Transfer,
} from "../util";
import {
  gasEstimateForShield,
  populateShield,
} from "@railgun-community/wallet";
import { Contract, type HDNodeWallet, type Wallet } from "ethers";
import { TEST_NETWORK, TEST_TOKEN } from "../../utils/constants";
import { getProviderWallet } from "../../utils/provider";

Gas Estimate

An asynchronous function erc20ShieldGasEstimate that calculates the estimated gas required to execute a shielding transaction for ERC-20 tokens in the RAILGUN system. It takes as parameters the network name, a wallet instance, and a list of ERC-20 amount recipients. The function first obtains a unique shield signature for the transaction using the provided wallet. It then derives the address from which the tokens are being shielded and calls gasEstimateForShield to perform the gas estimation based on transaction details like the recipient information and other transaction parameters. The estimated gas is then returned, providing insights into transaction costs before execution.

export const erc20ShieldGasEstimate = async (
  network: NetworkName,
  wallet: Wallet | HDNodeWallet,
  erc20AmountRecipients: RailgunERC20AmountRecipient[]
) => {
  const shieldPrivateKey = await getShieldSignature(wallet);

  // Address of public wallet we are shielding from
  const fromWalletAddress = wallet.address;

  const { gasEstimate } = await gasEstimateForShield(
    TXIDVersion.V2_PoseidonMerkle,
    network,
    shieldPrivateKey,
    erc20AmountRecipients,
    [], // nftAmountRecipients
    fromWalletAddress
  );

  return gasEstimate;
};

Populate Transaction

The code block defines an asynchronous function erc20PopulateShieldTransaction that facilitates the preparation of a transaction for shielding ERC20 tokens using the Railgun protocol. Initially, it approves the necessary token allowances for the specified erc20AmountRecipients by interacting with their token contracts. It checks the current allowance and approves additional tokens if needed. The function then estimates the gas required for the shielding transaction by calling erc20ShieldGasEstimate. With this estimate, it retrieves gas details tailored to whether the transaction is sent using a public or private wallet. Finally, the transaction is populated using populateShield, which returns the transaction details including gas estimation, nullifiers, and transaction object, suitable for execution on the Ethereum network.


export const erc20PopulateShieldTransaction = async (
  network: NetworkName,
  wallet: Wallet | HDNodeWallet,
  erc20AmountRecipients: RailgunERC20AmountRecipient[],
  sendWithPublicWallet: boolean
) => {
  // get gas estimate for tx,
  // populate tx with gas estimate
  // approve token to spender.

  // approve token to spender
  const spender = NETWORK_CONFIG[network].proxyContract;
  const tokens = erc20AmountRecipients.map((erc20AmountRecipient) => {
    return erc20AmountRecipient.tokenAddress;
  });

  for (const amountRecipient of erc20AmountRecipients) {
    const contract = new Contract(
      amountRecipient.tokenAddress,
      [
        "function allowance(address owner, address spender) view returns (uint256)",
        "function approve(address spender, uint256 amount) external returns (bool)",
      ],
      wallet
    );
    const allowance = await contract.allowance(wallet.address, spender);
    if (allowance >= amountRecipient.amount) {
      console.log("already have enough allowance");
      continue;
    }
    const tx = await contract.approve(spender, amountRecipient.amount);
    await tx.wait();
  }

  const gasEstimate = await erc20ShieldGasEstimate(
    network,
    wallet,
    erc20AmountRecipients
  );

  const shieldPrivateKey = await getShieldSignature(wallet);

  const gasDetails = await getGasDetailsForTransaction(
    network,
    gasEstimate,
    sendWithPublicWallet,
    wallet
  );

  const { transaction, nullifiers } = await populateShield(
    TXIDVersion.V2_PoseidonMerkle, // this is for V2 of the railgun protocol
    network,
    shieldPrivateKey,
    erc20AmountRecipients,
    [],
    gasDetails
  );

  return {
    gasEstimate,
    gasDetails,
    transaction,
    nullifiers,
  };
};

Example Usage

export const TEST_shieldERC20 = async (railgunWalletAddress: string) => {
  const { wallet } = getProviderWallet();

  const erc20AmountRecipients = [
    serializeERC20Transfer(
      TEST_TOKEN, // WETH
      1n,
      railgunWalletAddress
    ),
  ];

  const { gasEstimate, gasDetails, transaction, nullifiers } =
    await erc20PopulateShieldTransaction(
      TEST_NETWORK,
      wallet,
      erc20AmountRecipients,
      true
    );

  //   console.log("gasEstimate: ", gasEstimate);
  //   console.log("gasDetails: ", gasDetails);
  //   console.log("transaction: ", transaction);
  //   console.log("nullifiers: ", nullifiers);

  const tx = await wallet.sendTransaction(transaction);
  console.log("tx: ", tx);
  await tx.wait();
};

Last updated