Private NFT Transfers

Private Transfers require you to "Generate Proof" and, optionally, make use of "Relayers": See UX for Private Transactions

RAILGUN users may transfer multiple ERC-721 tokens to other RAILGUN users in a single transaction, in a fully encrypted and privacy-preserving way.

ERC-1155 token transfers are not yet supported.

Gas Estimate

import {
  NetworkName,
  TransactionGasDetails,
  RailgunNFTAmountRecipient,
  EVMGasType,
  FeeTokenDetails,
  SelectedRelayer,
  NFTTokenType,
  getEVMGasTypeForTransaction
} from '@railgun-community/shared-models';
import {
  gasEstimateForUnprovenTransfer
} from '@railgun-community/wallet';

// RAILGUN wallet to transfer to.
const railgunAddress = '0zk...e7n';

// Database encryption key. Keep this very safe.
const encryptionKey = '...'; // See "Encryption Keys" in the Private Wallets section.

// Optional encrypted memo text only readable by the sender and receiver.
// May include text and emojis. See "Private Transfers" page for details.
const memoText = 'Thank you for dinner! 🍝😋';

// Formatted NFT amounts and recipients.
const nftAmountRecipients: RailgunNFTAmountRecipient[] = [
  {
    nftAddress: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', 
    tokenSubID: '135', // tokenID of NFT
    amount: 1n, // transfer amount - always 1n for ERC-721
    nftTokenType: NFTTokenType.ERC721,
    recipientAddress: railgunAddress, 
  },
];

const sendWithPublicWallet = false; // False if sending with Relayer. True if self-signing with public wallet.
const evmGasType: EVMGasType = getEVMGasTypeForTransaction(
  NetworkName.Ethereum,
  sendWithPublicWallet
);
const originalGasEstimate = 0n; // Always 0, we don't have this yet.

let originalGasDetails: TransactionGasDetails;
switch (evmGasType) {
  case EVMGasType.Type0:
  case EVMGasType.Type1:
    originalGasDetails = {
      evmGasType,
      originalGasEstimate,
      gasPrice: BigInt('0x100000'), // Proper calculation of network gasPrice is not covered in this guide
    }
    break;
  case EVMGasType.Type2:
    // Proper calculation of gas Max Fee and gas Max Priority Fee is not covered in this guide. See: https://docs.alchemy.com/docs/how-to-build-a-gas-fee-estimator-using-eip-1559
    const maxFeePerGas: BigInt('0x100000');
    const maxPriorityFeePerGas: BigInt('0x010000');

    originalGasDetails = {
      evmGasType,
      originalGasEstimate,
      maxFeePerGas,
      maxPriorityFeePerGas,
    }
    break;
}

// If using a Relayer. From their private balance, the user must select a token to pay the relayer fee
const selectedTokenFeeAddress = '0xabc...123';
const selectedRelayer: SelectedRelayer = ...; // See "Relayers" section to select a relayer

// Token Fee for selected Relayer.
const feeTokenDetails: FeeTokenDetails = {
  tokenAddress: selectedTokenFeeAddress,
  feePerUnitGas: selectedRelayer.feePerUnitGas,
}

// Whether to use a Relayer or self-signing wallet.
// true for self-signing, false for Relayer.
const sendWithPublicWallet = false;

const railgunWalletID = '...'; // Obtained after a previous call to `createRailgunWallet`

const { gasEstimate } = await gasEstimateForUnprovenTransfer(
  NetworkName.Ethereum,
  railgunWalletID,
  encryptionKey,
  memoText,
  [], // tokenAmountRecipients
  nftAmountRecipients,
  originalGasDetails,
  feeTokenDetails,
  sendWithPublicWallet,
);

const transactionGasDetails: TransactionGasDetails = {
  evmGasType,
  gasEstimate,
  gasPrice
}

Generate Proof

...
import {
  ...,
  calculateGasPrice,
} from '@railgun-community/shared-models';
import {
  ...,
  generateTransferProof,
} from '@railgun-community/wallet';

...

// See above for examples of other required fields.
// See above for previously generated fields
const generateProof = async (
  selectedTokenFeeAddress: string,
  selectedRelayer: SelectedRelayer,
  railgunWalletID: string,
  encryptionKey: string,
  memoText: Optional<string>,
  tokenAmountRecipients: RailgunERC20AmountRecipient[],
  sendWithPublicWallet: boolean,
  transactionGasDetails: TransactionGasDetails,
) => {
  // Token fee to pay Relayer.
  const relayerFeeERC20AmountRecipient: Optional<RailgunERC20AmountRecipient> = {
    tokenAddress: selectedTokenFeeAddress,
    // NOTE: Proper calculation of "amount" is based on transactionGasDetails and selectedRelayer.
    amount: BigInt('0x10000000'), // See "Relayers" > "Calculating the Relayer Fee" for more info.
    recipientAddress: selectedRelayer.railgunAddress,
  };

  // ONLY required for transactions that are using a Relayer. Can leave undefined if self-signing.
  const overallBatchMinGasPrice: Optional<bigint> = await calculateGasPrice(transactionGasDetails);

  const progressCallback = (progress: number) => {
    // Handle proof progress (show in UI). 
    // Proofs can take 20-30 seconds on slower devices.
  };
  
  const showSenderAddressToRecipient: boolean = true; // Allow recipient to see RAILGUN address of sender

  await generateTransferProof(
    NetworkName.Ethereum,
    railgunWalletID,
    encryptionKey,
    showSenderAddressToRecipient,
    memoText,
    tokenAmountRecipients,
    [], // nftAmountRecipients
    relayerFeeERC20AmountRecipient,
    sendWithPublicWallet,
    overallBatchMinGasPrice,
    progressCallback,
  );
}

// Call generateProof on interaction from user

Populate Transaction

...
import {
  ...,
  populateProvedTransfer,
} from '@railgun-community/wallet';

...

// NOTE: Must follow proof generation.
// Use the exact same parameters as proof or this will throw invalid error.

// See above for previously generated fields
const transact = (
  railgunWalletID: string,
  memoText: Optional<string>,
  nftAmountRecipients: RailgunNFTAmountRecipient[],
  sendWithPublicWallet: boolean,
  transactionGasDetails: TransactionGasDetails,
  showSenderAddressToRecipient: boolean,
  overallBatchMinGasPrice: Optional<BigInt>, // If sending with Relayer
  relayerFeeERC20AmountRecipient: Optional<RailgunERC20AmountRecipient> // If sending with Relayer
) => {
  const populateResponse = await populateProvedTransfer(
    NetworkName.Ethereum,
    railgunWalletID,
    showSenderAddressToRecipient,
    memoText,
    [], // erc20AmountRecipients
    nftAmountRecipients,
    relayerFeeERC20AmountRecipient,
    sendWithPublicWallet,
    overallBatchMinGasPrice,
    transactionGasDetails,
  ); 
  
  // Submit via Relayer with populateResponse output. See "Relayers" section
  // or self-sign with any wallet. See "UX: Private Transactions"
}

Last updated