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();