Shield NFTs

Shield one or many ERC-721 tokens into RAILGUN private balances in a single transaction. ERC-1155 not supported at this time.

Imports

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

Gas Estimate

The erc721ShieldGasEstimate function calculates the estimated gas cost required for shielding a given amount of ERC-721 tokens. It uses the network and wallet information to derive a shield signature from the sender's wallet address, and the intended token recipients to compute the gas estimate.

export const erc721ShieldGasEstimate = async (
  network: NetworkName,
  wallet: Wallet | HDNodeWallet,
  erc721AmountRecipients: RailgunNFTAmountRecipient[]
) => {
  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,
    [],
    erc721AmountRecipients, // nftAmountRecipients
    fromWalletAddress
  );

  return gasEstimate;
};

Populate Transaction

The erc721PopulateShieldTransaction function constructs a transaction that authorizes and transfers the ERC-721 tokens to the RAILGUN shield. It checks if each token's transfer approval has been granted to the network's proxy contract and processes the transactions accordingly. It then calculates the gas details and populates the transaction with necessary privacy protocol data before returning the transaction information.

export const erc721PopulateShieldTransaction = async (
  network: NetworkName,
  wallet: Wallet | HDNodeWallet,
  erc721AmountRecipients: RailgunNFTAmountRecipient[],
  sendWithPublicWallet: boolean
) => {
  const spender = NETWORK_CONFIG[network].proxyContract;

  for (const amountRecipient of erc721AmountRecipients) {
    const contract = new Contract(
      amountRecipient.nftAddress,
      [
        "function getApproved(uint256 tokenId) view returns (address)",
        "function approve(address to, uint256 tokenId) external returns (bool)",
      ],
      wallet
    );
    const allowance = await contract.getApproved(amountRecipient.tokenSubID);
    if (allowance === ZeroAddress) {
      throw new Error("ERC721 Not minted.");
    }
    if (allowance.toLowerCase() == spender.toLowerCase()) {
      console.log("already have enough allowance");
      continue;
    }

    const tx = await contract.approve(spender, amountRecipient.tokenSubID);
    await tx.wait();
  }

  const gasEstimate = await erc721ShieldGasEstimate(
    network,
    wallet,
    erc721AmountRecipients
  );

  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,
    [],
    erc721AmountRecipients,
    gasDetails
  );

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

Example Usage

Finally, the TEST_shieldERC721 function demonstrates how to use these processes by shielding an example NFT to a specified RAILGUN wallet address. It showcases assembling the transaction, sending it with the wallet, and handling the transaction receipt.

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

  const nftAddress = TEST_NFT_ADDRESS;
  const tokenSubID = TEST_NFT_SUBID;
  const erc721AmountRecipients = [
    serializeERC721Transfer(nftAddress, tokenSubID, railgunWalletAddress),
  ];

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

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

Last updated