Skip to main content

Overview

Bridging with PrivacyCash is a three-step process:
  1. Quote: Request a bridge quote and a one-time Solana deposit address from the relayer.
  2. Private Withdrawal: Execute a PrivacyCash withdrawal where the recipient is the relayer’s deposit address.
  3. Finalize: Notify the relayer of the transaction hash so they can release the funds on the destination chain.

Prerequisites

Install the required dependencies:
npm install @solana/web3.js privacycash

Implementation

import { Connection, PublicKey, Keypair } from "@solana/web3.js";
import { 
    EncryptionService, 
    withdraw, 
    withdrawSPL, 
    tokens 
} from "privacycash";

// Configuration
const BRIDGE_URL = 'https://api3.privacycash.org/bridge';
const SOLANA_RPC = 'https://api.mainnet-beta.solana.com';

// Available output chain-tokens
const tokenMap: Record<string, string[]> = {
    "ethereum": ["eth", "usdc", "usdt", "dai"],
    "bnb": ["bnb", "usdc", "usdt"],
    "base": ["eth", "usdc"],
    "pol": ["pol", "usdc", "usdt"],
    "bitcoin": ["btc"],
};

/**
 * Calculates the net amount the recipient will receive after PrivacyCash 
 * withdrawal fees are subtracted.
 */
async function calculateWithdrawableAmount(amount: number, tokenName: string) {
    const configResp = await fetch('https://api3.privacycash.org/config');
    const config = await configResp.json();
    
    // Find token decimals and units
    const token = tokens.find(t => t.name === tokenName);
    if (!token) throw new Error("Token not supported");

    const feeRate = config.withdraw_fee_rate;
    const rentFeePerRecipient = config.rent_fees[tokenName] || 0;

    const withdrawUnites = amount * token.units_per_token;
    const withdrawRateFee = Math.floor(withdrawUnites * feeRate);
    const withdrawRentFee = Math.floor(token.units_per_token * rentFeePerRecipient);

    const totalFeeUnites = withdrawRateFee + withdrawRentFee;
    const totalFeeAmount = totalFeeUnites / token.units_per_token;

    return amount - totalFeeAmount;
}

async function runBridge() {
    // Setup
    const connection = new Connection(SOLANA_RPC);

    // user pubkey
    const publicKey = new PublicKey('UserPublicKeyHere');

    // bridge recipient address
    const recipientAddress = '0x...'
    
    // The message signature used to derive your PrivacyCash encryption key
    const userSignature = await getUserSignature(publicKey); 

    const encryptionService = new EncryptionService();
    encryptionService.deriveEncryptionKeyFromSignature(userSignature);

    const inputTokenName = 'usdc';
    const inputAmount = 100; // The total amount you want to withdraw from the private pool

    // Calculate how much the relayer will actually receive after PrivacyCash subtraction
    const withdrawableAmount = await calculateWithdrawableAmount(inputAmount, inputTokenName);

    // Bridge Parameters
    const params = {
        inputChain: 'solana',
        outputChain: 'ethereum',
        inputTokenName: inputTokenName,      // Supported: 'sol', 'usdc', 'usdt'
        outputTokenName: 'usdc',
        inputTokenAmount: withdrawableAmount.toString(), // Net amount for the relayer
        refundAddress: publicKey.toString(),
        recipientAddress: recipientAddress,   // Destination address on outputChain
        quoteWaitingTimeMs: 3000     // Recommended delay for best routing
    };

    try {
        // STEP 1: Get Quote and Deposit Address from Relayer
        console.log(`Fetching bridge quote for ${withdrawableAmount} ${inputTokenName}...`);
        const quoteResponse = await fetch(BRIDGE_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ ...params, step: 'quote' })
        });
        
        const quoteData = await quoteResponse.json();
        if (!quoteData.success) throw new Error(quoteData.error || "Failed to get quote");

        const depositAddress = quoteData.quote.depositAddress;
        console.log(`Quote received. Relayer Deposit Address: ${depositAddress}`);

        // STEP 2: Execute PrivacyCash Withdrawal
        console.log("Executing private withdrawal to relayer...");
        let txHash: string;

        // Use the original gross inputAmount for the withdrawal call
        if (params.inputTokenName === 'sol') {
            const amountInLamports = inputAmount * 1e9;
            const res = await withdraw({
                connection,
                encryptionService,
                publicKey: publicKey,
                recipient: new PublicKey(depositAddress),
                amount_in_lamports: amountInLamports,
                base_unites: amountInLamports,
                keyBasePath: './circuit2', 
                storage: localStorage,
                mintAddress:'So11111111111111111111111111111111111111112'
            });
            txHash = res.tx;
        } else {
            const token = tokens.find(t => t.name === params.inputTokenName);
            if (!token) throw new Error("Token not supported");

            const res = await withdrawSPL({
                connection,
                encryptionService,
                publicKey: publicKey,
                recipient: new PublicKey(depositAddress),
                mintAddress: token.pubkey.toString(),
                amount: inputAmount,
                amount_in_lamports: inputAmount, 
                base_unites: inputAmount,
                keyBasePath: './circuit2',
                storage: localStorage,
            });
            txHash = res.tx;
        }

        console.log(`Solana Transaction Confirmed: ${txHash}`);

        // STEP 3: Notify Relayer
        console.log("Notifying relayer to release funds on destination...");
        const notifyResponse = await fetch(BRIDGE_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ 
                txHash, 
                depositAddress, 
                step: 'send_tx' 
            })
        });

        const notifyData = await notifyResponse.json();
        if (notifyData.success) {
            console.log("Bridge successfully initiated!");
        } else {
            console.error("Relayer notification failed:", notifyData.error);
        }

    } catch (error) {
        console.error("Bridge operation failed:", error);
    }
}

async function getUserSignature(publicKey: PublicKey) {
    const encodedMessage = new TextEncoder().encode('Privacy Money account sign in')
    const cacheKey = `zkcash-signature-${publicKey.toBase58()}`

    // ask for sign
    let signature: Uint8Array
    try {
        signature = await walletProvider.signMessage(encodedMessage)
    } catch (err: any) {
        if (err instanceof Error && err.message?.toLowerCase().includes('user rejected')) {
            throw new Error('User rejected the signature request')
        }
        throw new Error('Failed to sign message: ' + err.message)
    }
    // If wallet.signMessage returned an object, extract `signature`
    // @ts-ignore
    if (signature.signature) {
        // @ts-ignore
        signature = signature.signature
    }
    return signature;
}

runBridge();

Key Considerations

Address Validation

Always validate the recipientAddress against the outputChain format before calling withdraw. The relayer uses the following logic:
  • EVMS (Ethereum, BNB, Base, Polygon): Starts with 0x followed by 40 hex characters.
  • Solana: Base58 string (32-44 characters).
  • Bitcoin: Bech32 (bc1...) or Legacy/P2SH formats.

Minimum Amounts

The bridge typically requires a minimum amount (equivalent to ~0.05 SOL) to cover cross-chain fees.

Fees

Three types of fees are applied:
  1. PrivacyCash Fee: A percentage of the withdrawal amount.
  2. Solana Network Fee: Rent and transaction costs for the withdrawal.
  3. Relay Fee: The cost of executing the transaction on the destination chain (dynamic).