# FAQ
Source: https://privacycash.mintlify.app/documentation/sdk-reference/faq
1. **Who pays for the network fees?**
User pays Solana network fees on deposit. For withdrawals, relayers pay Solana network fees.\
\
After you integrate with the SDK, you don't need to pay any fees.
2. **What's the minimum withdrawal amount?**
Check minimum\_withdrawal field in [https://api3.privacycash.org/config](https://api3.privacycash.org/config)
3. **Is there any devnet support?**
Not for now. Please test on mainnet. It should be really straightforward to integrate.
4. **For frontend integration, does client/user ever need to pass private key or encryption key to relayer?**
No. Those keys belong to the user, and should never leave client.
5. **Can SDK integration devs sponsor the gas fee?**
Currently not.
6. **Can SDK integration devs charge fees?**
You can add a fee transfers instruction on deposit and charge some fees from your users. In fact, a handful of projects integrated our SDK already did so.
# Privacy Tips
Source: https://privacycash.mintlify.app/documentation/user-docs/privacy-tips
Common Tips to Improve Your Privacy When Using Privacy Pools
* **Privacy is money at rest.** Deposit more than you need right now and leave funds in the pool over time.
* **Avoid 1:1 timing and amounts.** Never withdraw the same amount immediately after depositing.
* **Stick to common denominations.** Use popular amounts (e.g. 10 SOL, 100 USDC) to blend into the crowd.
* **Add noise with swaps.** Swap a portion of your balance into other tokens before withdrawing.
* **Use pools with large anonymity sets.** A pool with just one transaction per day offers no real privacy. Just use Privacy Cash.
# Video Tutorial
Source: https://privacycash.mintlify.app/documentation/user-docs/video-tutorial
# Balance
Source: https://privacycash.mintlify.app/evmsdk/balance
Check your private EVM balance on Privacy Cash
## Get Private Balance
Retrieve your private ETH or ERC-20 balance from a Privacy Cash EVM pool.
```typescript theme={null}
import { getBalance } from 'privacycash-evm'
const result = await getBalance({
signature: string,
address: string,
token?: 'eth' | 'usdc' | 'usdt',
network?: NetworkConfig | number
})
```
### Parameters
| Parameter | Type | Required | Description |
| ----------- | --------------------------- | -------- | ------------------------------------------------------------ |
| `signature` | `string` | Yes | Wallet signature of the Privacy Cash sign-in message |
| `address` | `string` | Yes | Your EVM wallet address |
| `token` | `'eth' \| 'usdc' \| 'usdt'` | No | Token balance to scan. Defaults to `'eth'` |
| `network` | `NetworkConfig \| number` | No | EVM network. Defaults from `NEXT_PUBLIC_CHAIN_ID`, then Base |
### Returns
```typescript theme={null}
{
balance: string // token balance as a decimal string, e.g. "0.123456789"
}
```
### Example
```typescript theme={null}
import { ethers } from 'ethers'
import { BASE_NETWORK, ETH_NETWORK, getBalance } from 'privacycash-evm'
const network = ETH_NETWORK // or BASE_NETWORK
const SIGN_MESSAGE = 'Privacy Money account sign in'
const provider = new ethers.providers.JsonRpcProvider(network.rpcUrl, {
name: network.chainKey,
chainId: network.chainId,
})
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider)
const signature = await signer.signMessage(SIGN_MESSAGE)
const address = await signer.getAddress()
const result = await getBalance({ signature, address, token: 'eth', network })
console.log('Private ETH balance:', result.balance, 'ETH')
```
***
## How Balance Works
`getBalance()` scans all on-chain `NewCommitment` events emitted by the Privacy Cash contract and attempts to decrypt each one using your derived encryption key. UTXOs that decrypt successfully and have not been spent are summed to produce your total balance.
No private key or encryption key is ever sent off-device — all decryption happens locally.
***
## Supported Tokens
Privacy Cash EVM currently supports these token pools:
| Network | Tokens |
| -------- | --------- |
| Base | ETH, USDC |
| Ethereum | ETH, USDT |
# Balance
Source: https://privacycash.mintlify.app/evmsdk/balance-fe
# getBalance()
`getBalance()` is a function from `privacycash-evm` used to retrieve your private ETH or ERC-20 balance from Privacy Cash on an EVM network.
## Parameters
| Parameter | Type | Description |
| :---------- | :-------------------------- | :--------------------------------------------------------------------- |
| `signature` | `string` | Wallet signature of the Privacy Cash sign-in message. |
| `address` | `string` | Your EVM wallet address. |
| `token` | `'eth' \| 'usdc' \| 'usdt'` | Optional. Defaults to `'eth'`. |
| `network` | `NetworkConfig \| number` | Optional. Pass `BASE_NETWORK`, `ETH_NETWORK`, or a supported chain ID. |
## Return Value
| Property | Type | Description |
| :-------- | :------- | :--------------------------------------------------------- |
| `balance` | `string` | Total private balance as a decimal string (e.g. `"0.05"`). |
## Example Usage
```typescript theme={null}
'use client'
import { BASE_NETWORK, ETH_NETWORK, getBalance } from 'privacycash-evm'
import { useAccount, useChainId } from 'wagmi'
import { useEffect, useState } from 'react'
export function PrivateBalance() {
const { address } = useAccount()
const chainId = useChainId()
const [balance, setBalance] = useState('0')
useEffect(() => {
if (!address) return
const network = chainId === 1 ? ETH_NETWORK : chainId === 8453 ? BASE_NETWORK : undefined
if (!network) return
const signature = localStorage.getItem(`evm_sign_${address}`)
if (!signature) return
getBalance({ signature, address, token: 'eth', network }).then(res => {
setBalance(res.balance)
})
}, [address, chainId])
return
Private Balance: {balance} ETH
}
```
***
## Auto-Refresh Pattern
Re-fetch the balance after a deposit or withdrawal completes by tracking a change counter:
```typescript theme={null}
const [balanceChanged, setBalanceChanged] = useState(0)
const network = chainId === 1 ? ETH_NETWORK : chainId === 8453 ? BASE_NETWORK : undefined
const token = 'eth'
useEffect(() => {
if (!address || !network) return
const signature = localStorage.getItem(`evm_sign_${address}`)
if (!signature) return
getBalance({ signature, address, token, network }).then(res => setBalance(res.balance))
}, [balanceChanged, address, token, network])
// After a successful deposit or withdrawal:
setBalanceChanged(prev => prev + 1)
```
***
## How It Works
`getBalance()` scans all historic `NewCommitment` events from the Privacy Cash contract and attempts to decrypt each one locally using the key derived from your signature. UTXOs that decrypt successfully and have not been spent are summed to produce your total balance.
No private data leaves the client.
# Bridge
Source: https://privacycash.mintlify.app/evmsdk/bridge
Bridge from EVM Privacy Cash pools to other chains
## Overview
Bridging from an EVM Privacy Cash pool is a three-step process:
1. **Quote** — Request a bridge quote and a one-time deposit address from the relayer.
2. **Private Withdrawal** — Execute a Privacy Cash withdrawal where the recipient is the relayer's deposit address.
3. **Notify** — Tell the relayer your transaction hash so it can release funds on the destination chain.
The EVM bridge API endpoint is: `https://evm.privacycash.org/bridge`
***
## Supported Output Chains
| Output Chain | Tokens |
| ------------ | -------------------- |
| Solana | SOL, USDT, USDC |
| Ethereum | ETH, USDC, USDT, DAI |
| BNB Chain | BNB, USDC, USDT |
| Polygon | POL, USDC, USDT |
| Bitcoin | BTC |
Supported input pools are Base ETH/USDC and Ethereum ETH/USDT.
***
## Prerequisites
```bash theme={null}
npm install privacycash-evm ethers --save
```
***
## Implementation
The example below bridges ETH. For ERC-20 inputs, set `inputTokenName` and `token` to the supported token for the selected network: `usdc` on Base or `usdt` on Ethereum.
```typescript theme={null}
import { BASE_NETWORK, ETH_NETWORK, withdraw } from 'privacycash-evm'
const BRIDGE_URL = 'https://evm.privacycash.org/bridge'
const MINIMUM_BRIDGE_AMOUNT = 0.005 // ETH
const RENT_FEE = 0.00025
const FEE_RATE = 35 // basis points, 0.35%
/**
* Calculate how much ETH the relayer will actually receive after
* Privacy Cash withdrawal fees are deducted from the user's balance.
*/
function calculateWithdrawableAmount(totalAmount: number): number {
const flatFee = RENT_FEE // 0.00025 ETH
const rateFee = (totalAmount * FEE_RATE) / 10000 // 0.35%
return totalAmount - flatFee - rateFee
}
async function runBridge(address: string, signature: string) {
const network = ETH_NETWORK // or BASE_NETWORK
const inputChain = network.chainKey === 'eth' ? 'ethereum' : 'base'
const inputTokenName = 'eth' as const
const totalWithdrawAmount = 0.1 // ETH you will deduct from your private balance
const recipientAddress = '0xRECIPIENT_ADDRESS' // destination address on output chain
if (totalWithdrawAmount < MINIMUM_BRIDGE_AMOUNT) {
throw new Error(`Minimum bridge amount is ${MINIMUM_BRIDGE_AMOUNT} ETH`)
}
// Net amount the relayer receives (after Privacy Cash fees)
const withdrawableAmount = calculateWithdrawableAmount(totalWithdrawAmount)
const params = {
inputChain,
outputChain: 'ethereum', // see supported chains above
inputTokenName,
outputTokenName: 'eth', // token on the destination chain
inputTokenAmount: withdrawableAmount.toFixed(8),
refundAddress: address, // your EVM address for refunds
recipientAddress, // destination address
quoteWaitingTimeMs: 3000, // recommended delay for best routing
}
// STEP 1: Get quote and relayer deposit address
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: string = quoteData.quote.depositAddress
console.log('Relayer deposit address:', depositAddress)
console.log('Estimated output:', quoteData.quote.amountOutFormatted)
// STEP 2: Withdraw to the relayer's deposit address on the selected EVM network
const txHash = await withdraw({
withdrawAmountInput: totalWithdrawAmount,
recipient: depositAddress, // relayer's one-time EVM address
keyBasePath: '/circuit',
signature,
address,
token: inputTokenName,
network,
})
console.log('Withdrawal tx:', txHash)
// STEP 3: Notify the relayer of the transaction hash
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)
}
}
```
***
## Key Considerations
### Fee Calculation
Two types of fees are deducted from `withdrawAmountInput`:
1. **Privacy Cash flat fee** — `RENT_FEE` (0.00025 ETH)
2. **Privacy Cash rate fee** — `FEE_RATE` (0.35% of withdrawal amount)
Pass the *net* amount (`withdrawableAmount`) to the quote API so the relayer knows exactly how much input token it will receive.
### Address Validation
Validate the recipient address format before calling the quote API:
* **EVM chains** (Ethereum, BNB, Polygon): `0x` + 40 hex characters
* **Solana**: Base58 string (32–44 characters)
* **Bitcoin**: Bech32 (`bc1...`) or legacy format
### Minimum Amount
ETH input uses a **0.005 ETH** minimum. Stable-token inputs should use the relayer minimum returned by the quote flow.
### Quote Freshness
Quotes expire quickly due to market price movement. Re-fetch the quote if more than \~5 seconds have passed before submitting the withdrawal.
# Deposit
Source: https://privacycash.mintlify.app/evmsdk/deposit
Deposit ETH or ERC-20 tokens into Privacy Cash on EVM
## Deposit
Deposit ETH or a supported ERC-20 token into a Privacy Cash EVM privacy pool.
```typescript theme={null}
import { deposit } from 'privacycash-evm'
const tx = await deposit({
depositAmountInput: number,
keyBasePath: string,
signature: string,
address: string,
txSender: (unsignedTx: any) => Promise,
token?: 'eth' | 'usdc' | 'usdt',
network?: NetworkConfig | number
})
```
### Parameters
| Parameter | Type | Required | Description |
| -------------------- | --------------------------- | -------- | --------------------------------------------------------------------- |
| `depositAmountInput` | `number` | Yes | Amount to deposit in the selected token (e.g. `0.1` ETH or `10` USDC) |
| `keyBasePath` | `string` | Yes | Base path to the circuit zkey file (without extension) |
| `signature` | `string` | Yes | Wallet signature of the Privacy Cash sign-in message |
| `address` | `string` | Yes | The depositor's EVM wallet address |
| `txSender` | `Function` | Yes | Callback that signs and submits the raw transaction, returns tx hash |
| `token` | `'eth' \| 'usdc' \| 'usdt'` | No | Token to deposit. Defaults to `'eth'` |
| `network` | `NetworkConfig \| number` | No | EVM network. Defaults from `NEXT_PUBLIC_CHAIN_ID`, then Base |
### Returns
The transaction hash (`string`).
### Example
```typescript theme={null}
import { ethers } from 'ethers'
import { BASE_NETWORK, ETH_NETWORK, deposit } from 'privacycash-evm'
const network = ETH_NETWORK // or BASE_NETWORK
const SIGN_MESSAGE = 'Privacy Money account sign in'
const provider = new ethers.providers.JsonRpcProvider(network.rpcUrl, {
name: network.chainKey,
chainId: network.chainId,
})
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider)
const signature = await signer.signMessage(SIGN_MESSAGE)
const address = await signer.getAddress()
const txSender = async (unsignedTx: any) => {
const tx = await signer.sendTransaction(unsignedTx)
await tx.wait()
return tx.hash
}
const txHash = await deposit({
depositAmountInput: 0.01, // 0.01 ETH
keyBasePath: './circuits/transaction',
signature,
address,
txSender,
token: 'eth',
network,
})
console.log('Deposit tx:', txHash)
```
### Deposit Limits
The protocol enforces a maximum deposit amount. Attempting to exceed it will throw:
```
Please deposit less than X ETH
```
There is also a minimum deposit amount of **0.001 ETH**.
Depositing below the minimum will throw `Deposit amount must be at least 0.001 ETH`.
### Fees
| Fee Type | Amount |
| ------------ | ------------------------------------------ |
| Protocol fee | Free (0) |
| Network fee | Paid by the connected wallet in native ETH |
***
## How Deposits Work
The SDK derives your keypair from the signature, scans on-chain UTXOs, and generates a ZK proof consolidating any existing UTXOs with the new deposit amount.
Your wallet address is screened for malicious activity before the transaction is built.
An unsigned transaction is created and passed to your `txSender` callback for signing and submission.
An encrypted UTXO is written on-chain, decryptable only by you.
### Consolidation
New deposits are automatically consolidated with your existing private balance:
```typescript theme={null}
// First deposit: 0.05 ETH
await deposit({ depositAmountInput: 0.05, ...sharedParams })
// Second deposit: 0.03 ETH — merged into a single UTXO
await deposit({ depositAmountInput: 0.03, ...sharedParams })
// Private balance is now 0.08 ETH (minus network fees)
```
# Frontend Deposit
Source: https://privacycash.mintlify.app/evmsdk/deposit-fe
Frontend Deposit on EVM
# deposit()
`deposit()` is a function from `privacycash-evm` used to deposit ETH or a supported ERC-20 token into Privacy Cash on an EVM network.
## Parameters
| Parameter | Type | Description |
| :------------------- | :-------------------------- | :--------------------------------------------------------------------------------------- |
| `depositAmountInput` | `number` | Amount to deposit, denominated in the selected token (e.g. `0.05` ETH or `10` USDC). |
| `keyBasePath` | `string` | Base path for the circuit zkey (e.g. `'/circuit'`). |
| `signature` | `string` | Wallet signature of the Privacy Cash sign-in message. |
| `address` | `string` | The depositor's EVM wallet address. |
| `txSender` | `Function` | Callback to sign and submit the raw transaction: `(unsignedTx: any) => Promise`. |
| `token` | `'eth' \| 'usdc' \| 'usdt'` | Optional. Defaults to `'eth'`. |
| `network` | `NetworkConfig \| number` | Optional. Pass `BASE_NETWORK`, `ETH_NETWORK`, or a supported chain ID. |
***
## Example Usage
```typescript theme={null}
'use client'
import { ethers } from 'ethers'
import { BASE_NETWORK, ETH_NETWORK, deposit } from 'privacycash-evm'
import { useAccount, useChainId, useWalletClient } from 'wagmi'
export function DepositButton() {
const { address, connector } = useAccount()
const chainId = useChainId()
const { data: walletClient } = useWalletClient()
const handleDeposit = async () => {
if (!address) return
const network = chainId === 1 ? ETH_NETWORK : chainId === 8453 ? BASE_NETWORK : undefined
if (!network) {
alert('Switch to Base or Ethereum mainnet.')
return
}
// Retrieve the stored signature
const signature = localStorage.getItem(`evm_sign_${address}`)
if (!signature) {
alert('Please sign in first.')
return
}
// Build the txSender callback
const txSender = async (unsignedTx: any) => {
if (walletClient) {
return await walletClient.sendTransaction({
to: unsignedTx.to as `0x${string}`,
data: unsignedTx.data as `0x${string}`,
value: BigInt(unsignedTx.value?.toString() ?? '0'),
})
}
// Fallback via ethers BrowserProvider
const ethereum = await connector?.getProvider()
const provider = new ethers.BrowserProvider(ethereum as any)
const signer = await provider.getSigner(address)
const tx = await signer.sendTransaction(unsignedTx)
return tx.hash
}
const txHash = await deposit({
depositAmountInput: 0.01,
keyBasePath: '/circuit',
signature,
address,
txSender,
token: 'eth',
network,
})
console.log('Deposit tx:', txHash)
}
return
}
```
***
## Notes
* The `txSender` callback receives an unsigned transaction object. Your implementation must sign and submit it, then return the transaction hash as a `string`.
* Always check that the user is connected to a supported EVM mainnet before calling `deposit()`: Base (`8453`) or Ethereum (`1`).
* A small additional native token balance is needed for gas on top of the deposit amount. Validate that the wallet has enough ETH for network fees before proceeding.
* Current token support is Base ETH/USDC and Ethereum ETH/USDT.
# FAQ
Source: https://privacycash.mintlify.app/evmsdk/faq
1. **Who pays for the network fees?**
The user pays EVM network fees on deposit (included in the `txSender` callback). For withdrawals, the transaction is submitted directly and the user's wallet also covers gas.\
\
After you integrate with the SDK, no additional fee infrastructure is needed.
2. **What's the minimum deposit and withdrawal amount?**
Both are **0.001 ETH**. Attempting to deposit or withdraw below this threshold will throw an error.
3. **Is there testnet support?**
Not currently. Please test on the supported mainnet deployments: Base (`8453`) or Ethereum (`1`). Amounts as small as 0.001 ETH work fine for testing.
4. **Does the client ever send the private key or encryption key to the relayer?**
No. The signature derived from `signMessage` never leaves the client. All key derivation and UTXO decryption happens locally in the browser or Node.js process.
5. **What `keyBasePath` should I use?**
* **Next.js frontend**: Place `circuit2.zkey` in `public/` and pass `keyBasePath: '/circuit'`.
* **Node.js backend**: Pass the path relative to your working directory, e.g., `keyBasePath: './circuits/transaction'`.
6. **Why does `withdraw()` succeed but the recipient receives less than the requested amount?**
Fees (flat fee + 0.35% rate fee) are deducted from `withdrawAmountInput`. The recipient receives `withdrawAmountInput - fees`. See [Withdraw](/evmsdk/withdraw) for the exact fee formula.
7. **Why does `getBalance()` take a while?**
It scans all historic `NewCommitment` events from the contract and attempts to decrypt each one locally. Performance scales with the total number of deposits ever made to the pool. Caching is built into the SDK to speed up repeated calls.
8. **Can I use the EVM SDK in a Node.js script without a browser wallet?**
Yes. Create an `ethers.Wallet` from your private key, call `signer.signMessage('Privacy Money account sign in')` to get the signature, select `BASE_NETWORK` or `ETH_NETWORK`, and implement `txSender` using `signer.sendTransaction()`. See the [Backend Integration](/evmsdk/overview) page for a full example.
9. **Can I clear the local event cache?**
Yes. Call `clearCache()` exported from `privacycash-evm` to reset the cached UTXO event data.
```typescript theme={null}
import { BASE_NETWORK, clearCache } from 'privacycash-evm'
clearCache(address, 'eth', BASE_NETWORK)
```
# Frontend Integration
Source: https://privacycash.mintlify.app/evmsdk/frontend
## Demo Project
A complete example project demonstrating how to use the Privacy Cash EVM SDK in a Next.js frontend:
[https://github.com/Privacy-Cash/base-sdk-demo-interface](https://github.com/Privacy-Cash/base-sdk-demo-interface)
## Installation
```bash theme={null}
npm install privacycash-evm wagmi viem @rainbow-me/rainbowkit @tanstack/react-query --save
```
Requires **Node.js 20+**. The SDK is written in TypeScript and includes type definitions.
***
## Wallet Provider Setup
Wrap your app with Wagmi and RainbowKit providers configured for the supported EVM mainnets:
```typescript theme={null}
'use client'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { WagmiProvider, createConfig, http } from 'wagmi'
import { base, mainnet } from 'wagmi/chains'
import { walletConnect, coinbaseWallet } from 'wagmi/connectors'
const config = createConfig({
ssr: true,
chains: [base, mainnet],
connectors: [
coinbaseWallet({ appName: 'Your App' }),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID! }),
],
transports: {
[base.id]: http(process.env.NEXT_PUBLIC_BASE_RPC_URL),
[mainnet.id]: http(process.env.NEXT_PUBLIC_ETH_RPC_URL),
},
})
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
***
## Deriving Encryption Key
Before a user can interact with Privacy Cash, they must sign an off-chain message. This signature is used to derive their private encryption key and UTXO keypair — **neither key ever leaves the client**.
```typescript theme={null}
import { useSignMessage } from 'wagmi'
const SIGN_MESSAGE = 'Privacy Money account sign in'
const LS_KEY_PREFIX = 'evm_sign_'
function saveSignature(address: string, signature: string) {
localStorage.setItem(`${LS_KEY_PREFIX}${address}`, signature)
}
function getStoredSignature(address: string): string | null {
return localStorage.getItem(`${LS_KEY_PREFIX}${address}`)
}
// In your component:
const { signMessage } = useSignMessage()
const { address } = useAccount()
function handleSignIn() {
signMessage(
{ message: SIGN_MESSAGE },
{
onSuccess(signature) {
saveSignature(address!, signature)
},
}
)
}
```
Auto-prompt the user to sign when they connect their wallet:
```typescript theme={null}
useEffect(() => {
if (!isConnected || !address) return
const stored = getStoredSignature(address)
if (!stored) handleSignIn()
}, [isConnected, address])
```
***
## Circuit Key File
Place the `circuit2.zkey` file in your Next.js `public/` folder. Pass the base path (without extension) to each SDK function:
```text theme={null}
public/
circuit2.zkey ← download from the SDK repo
```
```typescript theme={null}
keyBasePath: '/circuit' // resolves to /circuit2.zkey at runtime
```
***
## Selecting a Network
Pass the active EVM network to SDK calls. Base is the default when `network` is omitted, but explicit selection keeps multi-chain apps predictable.
```typescript theme={null}
import { BASE_NETWORK, ETH_NETWORK, getBalance } from 'privacycash-evm'
import { useChainId } from 'wagmi'
function BalanceLoader({ signature, address }: { signature: string, address: string }) {
const chainId = useChainId()
const network = chainId === 1 ? ETH_NETWORK : chainId === 8453 ? BASE_NETWORK : undefined
async function refreshBalance() {
if (!network) throw new Error('Switch to Base or Ethereum mainnet.')
return getBalance({
signature,
address,
network,
})
}
return null // Render your balance UI here.
}
```
## Common Issues
1. **Signature re-prompt on every page refresh** — Store the signature in `localStorage` keyed by address as shown above.
2. **`walletClient` temporarily undefined** — Use `connector.getProvider()` as a fallback when `useWalletClient()` is unavailable during initial mount.
3. **Wrong chain data** — Pass `network: BASE_NETWORK` or `network: ETH_NETWORK` to each SDK call after checking the connected wallet chain.
## Warnings
Privacy Cash SDK requires consistent signature generation, otherwise the deposited tokens might be lost forever since the encrypted UTXO can't be decrypted. Please make sure deriveKeys() generates the same result for the same params passed in. Most wallets returns the same result, but some non major wallets might generate different result.
# Backend Integration
Source: https://privacycash.mintlify.app/evmsdk/overview
## Sample Project
[https://github.com/Privacy-Cash/privacy-cash-evm-sdk/blob/main/example/](https://github.com/Privacy-Cash/privacy-cash-evm-sdk/blob/main/example/)
## Installation
```bash theme={null}
npm install privacycash-evm --save
```
Requires **Node.js 20+**. The SDK is written in TypeScript and includes type definitions.
## Signing In
The SDK uses a wallet signature to derive your private encryption key and UTXO keypair. Ask the user to sign a fixed message:
```typescript theme={null}
import { ethers } from 'ethers'
import { BASE_NETWORK, ETH_NETWORK } from 'privacycash-evm'
const network = ETH_NETWORK // or BASE_NETWORK
const SIGN_MESSAGE = 'Privacy Money account sign in'
const provider = new ethers.providers.JsonRpcProvider(network.rpcUrl, {
name: network.chainKey,
chainId: network.chainId,
})
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider)
const signature = await signer.signMessage(SIGN_MESSAGE)
const address = await signer.getAddress()
```
The resulting `signature` and `address` strings are passed directly to each SDK function. Pass the selected `network` to read from and submit to the correct EVM deployment.
## Sending Transactions
`deposit()` requires a `txSender` callback that signs and submits the raw transaction:
```typescript theme={null}
const txSender = async (unsignedTx: any) => {
const tx = await signer.sendTransaction(unsignedTx)
await tx.wait()
return tx.hash
}
```
## Interacting with Privacy Cash
Once the above steps are done, you can check [balance](/evmsdk/balance), make [deposits](/evmsdk/deposit) and [withdrawals](/evmsdk/withdraw).
## Supported Networks
Privacy Cash EVM currently supports mainnet deployments only:
| Network | Chain ID | SDK constant | Supported tokens |
| -------- | -------- | -------------- | ---------------- |
| Base | `8453` | `BASE_NETWORK` | ETH, USDC |
| Ethereum | `1` | `ETH_NETWORK` | ETH, USDT |
```typescript theme={null}
import {
BASE_NETWORK,
ETH_NETWORK,
deposit,
withdraw,
getBalance,
} from 'privacycash-evm'
await getBalance({ signature, address, network: BASE_NETWORK })
await getBalance({ signature, address, network: ETH_NETWORK })
```
If `network` is omitted, the SDK reads `NEXT_PUBLIC_CHAIN_ID` and falls back to Base (`8453`).
## Key Path
All functions accept a `keyBasePath` parameter pointing to the circuit zkey file (without extension):
```typescript theme={null}
keyBasePath: './circuits/transaction' // loads ./circuits/transaction2.zkey
```
For Next.js projects, place the `.zkey` file in the `public/` folder:
```typescript theme={null}
keyBasePath: '/circuit' // loads /circuit2.zkey from public/
```
## Warnings
Privacy Cash SDK requires consistent signature generation, otherwise the deposited tokens might be lost forever since the encrypted UTXO can't be decrypted. Please make sure deriveKeys() generates the same result for the same params passed in. Most wallets returns the same result, but some non major wallets might generate different result.
# ERC-20 Tokens
Source: https://privacycash.mintlify.app/evmsdk/spl-tokens
Token support on Privacy Cash EVM
## Current Token Support
Privacy Cash EVM supports native ETH and selected ERC-20 pools per network.
| Network | Chain ID | Supported tokens |
| -------- | -------- | ---------------- |
| Base | `8453` | ETH, USDC |
| Ethereum | `1` | ETH, USDT |
***
## Working with Amounts
The SDK uses decimal token amounts throughout. Use ETH decimals for the native asset and token decimals for ERC-20 balances.
```typescript theme={null}
import { BASE_NETWORK, ETH_NETWORK, deposit, withdraw, getBalance } from 'privacycash-evm'
// Deposit 0.05 ETH on Ethereum
await deposit({
depositAmountInput: 0.05,
token: 'eth',
network: ETH_NETWORK,
keyBasePath,
signature,
address,
txSender,
})
// Withdraw 10 USDC on Base
await withdraw({
withdrawAmountInput: 10,
token: 'usdc',
network: BASE_NETWORK,
recipient,
keyBasePath,
signature,
address,
})
// Balance is returned as a string
const { balance } = await getBalance({
token: 'usdt',
network: ETH_NETWORK,
signature,
address,
})
console.log(balance) // "10.000000"
```
### Converting to Wei
If you need to work in base units for UI display or comparison:
```typescript theme={null}
import { ethers } from 'ethers'
const ethWei = ethers.utils.parseEther('0.05')
const usdcUnits = ethers.utils.parseUnits('10', 6)
console.log('ETH in wei:', ethWei.toString())
console.log('USDC in base units:', usdcUnits.toString())
```
***
Passing an unsupported token/network pair throws an SDK error. Use Base ETH/USDC or Ethereum ETH/USDT.
# Withdraw
Source: https://privacycash.mintlify.app/evmsdk/withdraw
Withdraw ETH or ERC-20 tokens from Privacy Cash on EVM
## Withdraw
Withdraw ETH or a supported ERC-20 token from your private balance to any EVM recipient address.
```typescript theme={null}
import { withdraw } from 'privacycash-evm'
const txHash = await withdraw({
withdrawAmountInput: number,
recipient: string,
keyBasePath: string,
signature: string,
address: string,
token?: 'eth' | 'usdc' | 'usdt',
network?: NetworkConfig | number
})
```
### Parameters
| Parameter | Type | Required | Description |
| --------------------- | --------------------------- | -------- | ---------------------------------------------------------------------------- |
| `withdrawAmountInput` | `number` | Yes | Total amount in the selected token to withdraw (fees are deducted from this) |
| `recipient` | `string` | Yes | EVM address to receive the funds |
| `keyBasePath` | `string` | Yes | Base path to the circuit zkey file (without extension) |
| `signature` | `string` | Yes | Wallet signature of the Privacy Cash sign-in message |
| `address` | `string` | Yes | The withdrawer's EVM wallet address |
| `token` | `'eth' \| 'usdc' \| 'usdt'` | No | Token to withdraw. Defaults to `'eth'` |
| `network` | `NetworkConfig \| number` | No | EVM network. Defaults from `NEXT_PUBLIC_CHAIN_ID`, then Base |
### Returns
The transaction hash (`string`).
### Example
```typescript theme={null}
import { ethers } from 'ethers'
import { BASE_NETWORK, ETH_NETWORK, withdraw } from 'privacycash-evm'
const network = ETH_NETWORK // or BASE_NETWORK
const SIGN_MESSAGE = 'Privacy Money account sign in'
const provider = new ethers.providers.JsonRpcProvider(network.rpcUrl, {
name: network.chainKey,
chainId: network.chainId,
})
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider)
const signature = await signer.signMessage(SIGN_MESSAGE)
const address = await signer.getAddress()
const txHash = await withdraw({
withdrawAmountInput: 0.05, // Total amount including fees
recipient: '0xRECIPIENT_ADDRESS', // Any EVM address
keyBasePath: './circuits/transaction',
signature,
address,
token: 'eth',
network,
})
console.log('Withdraw tx:', txHash)
```
***
## Withdrawal Fees
Fees are deducted from the `withdrawAmountInput`, so the recipient receives less than the requested amount.
| Component | Amount |
| ------------ | -------------------------- |
| Flat fee | 0.00025 ETH per withdrawal |
| Protocol fee | 0.35% of withdrawal amount |
### Fee Calculation Example
```typescript theme={null}
const withdrawAmount = 0.1 // ETH
// Flat fee: 0.00025 ETH
// Protocol fee: 0.1 × 0.35% = 0.00035 ETH
// Total fee: ~0.00060 ETH
// Recipient receives: ~0.09940 ETH
```
***
## How Withdrawals Work
The SDK scans on-chain events to find your unspent UTXOs (up to 2) to cover the withdrawal amount.
A zero-knowledge proof is generated proving you own the funds and authorizing the exact recipient and amount.
The transaction is submitted directly to the selected EVM smart contract. No relayer is needed for EVM withdrawals.
The recipient receives ETH with no on-chain link to your depositing wallet.
### Privacy Guarantee
The withdrawal transaction on-chain contains **no information** about the original depositor. The zero-knowledge proof ensures:
* The recipient address cannot be modified
* The amount cannot be modified
* Any tampering causes the transaction to revert
***
## Insufficient Balance
If your private balance is less than `withdrawAmountInput`, the SDK throws:
```
Insufficient balance. Have X ETH, need Y ETH.
```
Check your balance first with [`getBalance()`](/evmsdk/balance).
# Frontend Withdraw
Source: https://privacycash.mintlify.app/evmsdk/withdraw-fe
Frontend Withdraw on EVM
# withdraw()
`withdraw()` is a function from `privacycash-evm` used to withdraw ETH or a supported ERC-20 token from Privacy Cash to an EVM recipient address.
## Parameters
| Parameter | Type | Description |
| :-------------------- | :-------------------------- | :--------------------------------------------------------------------- |
| `withdrawAmountInput` | `number` | Total amount to withdraw. Fees are deducted from this value. |
| `recipient` | `string` | The EVM address that will receive the withdrawn funds. |
| `keyBasePath` | `string` | Base path for the circuit zkey (e.g. `'/circuit'`). |
| `signature` | `string` | Wallet signature of the Privacy Cash sign-in message. |
| `address` | `string` | The withdrawer's EVM wallet address. |
| `token` | `'eth' \| 'usdc' \| 'usdt'` | Optional. Defaults to `'eth'`. |
| `network` | `NetworkConfig \| number` | Optional. Pass `BASE_NETWORK`, `ETH_NETWORK`, or a supported chain ID. |
***
## Example Usage
```typescript theme={null}
'use client'
import { BASE_NETWORK, ETH_NETWORK, withdraw } from 'privacycash-evm'
import { useAccount, useChainId } from 'wagmi'
export function WithdrawButton() {
const { address } = useAccount()
const chainId = useChainId()
const handleWithdraw = async () => {
if (!address) return
const network = chainId === 1 ? ETH_NETWORK : chainId === 8453 ? BASE_NETWORK : undefined
if (!network) {
alert('Switch to Base or Ethereum mainnet.')
return
}
const signature = localStorage.getItem(`evm_sign_${address}`)
if (!signature) {
alert('Please sign in first.')
return
}
const recipient = '0xRECIPIENT_ADDRESS'
const txHash = await withdraw({
withdrawAmountInput: 0.05, // Total including fees
recipient,
keyBasePath: '/circuit',
signature,
address,
token: 'eth',
network,
})
console.log('Withdraw tx:', txHash)
}
return
}
```
***
## Fee Estimation
To display the estimated received amount before the user submits:
```typescript theme={null}
const RENT_FEE = 0.00025
const FEE_RATE = 35 // basis points, 0.35%
function estimateReceived(withdrawAmount: number): number {
const flatFee = RENT_FEE // 0.00025 ETH
const rateFee = (withdrawAmount * FEE_RATE) / 10000 // 0.35%
return withdrawAmount - flatFee - rateFee
}
console.log(estimateReceived(0.1)) // ~0.09940 ETH
```
***
## Notes
* `withdrawAmountInput` is the **total** amount deducted from your private balance; the recipient receives less after fees.
* Always validate that `withdrawAmountInput <= privateBalance` before calling `withdraw()`.
* The recipient must be a valid EVM address (`0x` + 40 hex characters).
* Current token support is Base ETH/USDC and Ethereum ETH/USDT.
# Privacy Cash User Docs
Source: https://privacycash.mintlify.app/index
Privacy Cash is a privacy protocol on Solana, Ethereum, Robinhood Chain, BNB Chain and Base that enables private transfers and swaps without linking your wallet addresses or transaction history. Built with zero-knowledge proofs, an append-only Merkle tree, and a relayer system, Privacy Cash breaks the on-chain link between deposits and withdrawals.
Privacy Cash has privately transferred and swapped [\$500M+](https://dune.com/privacy_cash_team/privacy-cash) in volume. The on-chain program, zero-knowledge circuits, and SDK are fully [open-sourced](https://github.com/orgs/Privacy-Cash/repositories), verified [on-chain](https://solscan.io/account/9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD), audited 20 times (with 14 on Solana & 6 on Base), and formally verified by [Veridise](https://veridise.com/). Privacy Cash is also backed by [AllianceDAO](https://alliance.xyz/).
## Private Transfers
Privacy Cash lets you transfer funds to a clean wallet without linking past addresses or transaction history.
To make a private transfer, you first deposit your tokens into Privacy Cash. Later, you can withdraw those tokens to a recipient address.
Privacy Cash breaks the link between deposits and withdrawals using zero-knowledge proofs, an append-only Merkle tree, and a relayer system. Your signed deposit transaction is sent to a relayer, which screens your wallet address through [CipherOwl](https://www.cipherowl.com/). If your address is flagged as malicious, the deposit is rejected.
The withdrawal process is similar, except the transaction is signed by the relayer. Your withdrawal in the onchain transaction (e.g. on SolScan or Orb or BaseScan) contains no information about the original depositor, breaking the link between deposits and withdrawals. A zero-knowledge proof generated on the client ensures that the relayer cannot modify the recipient address, amount, or other parameters. Any tampering will cause the on-chain transaction to fail.
Each deposit and withdrawal creates a UTXO that is encrypted and submitted to the Solana program on-chain. These encrypted UTXOs are indexed off-chain. Only the user who holds the encryption key can decrypt their UTXOs and spend the balance. The encryption key per user is deterministically derived on the client by the user signing a fixed message and is accessible only to the user, unless it is intentionally or unintentionally leaked (for example, through phishing). Don't blindly sign messages on phishing sites!
**Deposit Fees:** 0 **Withdrawal Fees:** Solana - 0.006 SOL (or SPL equivalent) \* number of recipients + 0.35% of withdrawal amount Base - 0.00025 ETH (or ERC20 equivalent) \* number of recipients + 0.35% of withdrawal amount Robinhood Chain - 0.00055 ETH (or ERC20 equivalent) \* number of recipients + 0.35% of withdrawal amount BNB Chain - 0.001 BNB (or ERC20 equivalent) \* number of recipients + 0.35% of withdrawal amount Ethereum - Dynamic network related fees + 0.35% of withdrawal amount
## Private Swaps (Solana Only)
Private Swaps is enabled by the same protocol (Solana program and ZK circuits) and SDK that passed 14 audits in total.
How it works:
1. Your input token is unshielded into an ephemeral wallet on the client
2. The ephemeral wallet executes a Jupiter swap
3. The output token is reshielded back to your main wallet
At no point is your main wallet address exposed on-chain during the swap. With private swaps, your private SOL can be swapped into private USDC/USDT/ORE/etc. in our privacy pool.
Private swaps will increase your anonymity, because it makes it harder for observers to do amount based analysis (you can partially swap your SOL deposits into USDC).
**Swap Fees:** 0.008 SOL (or SPL equivalent) + 0.35% of swap amount + Jupiter fees
## Private Bridging
Private Bridging is enabled by the same protocol (Solana/EVM program and ZK circuits) and SDK that passed 14 total audits in total on Solana and 5 audits on EVM.
How it works:
1. Your tokens are privately withdrawn to a NEAR Intents market maker address.
2. NEAR Intents then bridges the tokens to other chains.
Because the tokens are sent from the Privacy Cash protocol address to NEAR Intents, observers cannot determine who the original depositor was.
**Bridging Fees:** Solana - 0.006 SOL (or SPL equivalent) + 0.35% of bridging amount + 0.1% NEAR bridging fees Base - 0.00025 ETH (or ERC20 equivalent) + 0.35% of bridging amount + 0.1% NEAR bridging fees Robinhood Chain - 0.00055 ETH (or ERC20 equivalent) + 0.35% of bridging amount + variable Relay Network bridging fees BNB Chain - 0.001 BNB (or ERC20 equivalent) + 0.35% of bridging amount + 0.1% NEAR bridging fees Ethereum - Dynamic network related fees + 0.35% of bridging amount + 0.1% NEAR bridging fees
## Caveats
1. It's highly recommended that you withdraw to a clean new non-custodial wallet like Phantom, Solflare, Backpack, Coinbase Wallet, Rabby, or Metamask first. Then send from the new wallet to centralized apps (e.g. CEXs and Revolut might require manual processing, which may take days or longer).
2. Although Privacy Cash technically breaks the link between deposits and withdrawals, observers may still make educated guesses by analyzing on-chain activity (for example, via SolScan or other explorers) if a unique amount is deposited and the same amount is withdrawn shortly afterward. For maximum privacy, consider the following best practices in [https://privacycash.mintlify.app/documentation/user-docs/privacy-tips](https://privacycash.mintlify.app/documentation/user-docs/privacy-tips).
## Term of Use
[https://docs.google.com/document/d/1gPXLC2a7ehT\_T2bYOeZt0v2OCpyk4CYrGYhCuDOgXcw/](https://docs.google.com/document/d/1gPXLC2a7ehT_T2bYOeZt0v2OCpyk4CYrGYhCuDOgXcw/)
# Balance
Source: https://privacycash.mintlify.app/sdk/balance
Check your private balances in Privacy Cash
## Get SOL Balance
Retrieve your private SOL balance from Privacy Cash.
```typescript theme={null}
const balance = await client.getPrivateBalance(abortSignal?: AbortSignal)
```
### Parameters
| Parameter | Type | Required | Description |
| ------------- | ------------- | -------- | ------------------------------------ |
| `abortSignal` | `AbortSignal` | No | Optional signal to abort the request |
### Returns
```typescript theme={null}
{
lamports: number // Balance in lamports
}
```
### Example
```typescript theme={null}
import { PrivacyCash } from 'privacycash'
const client = new PrivacyCash({
RPC_url: process.env.SOLANA_RPC_URL!,
owner: process.env.PRIVATE_KEY!
})
const balance = await client.getPrivateBalance()
console.log('Private balance:', balance.lamports, 'lamports')
console.log('Private balance:', balance.lamports / 1_000_000_000, 'SOL')
```
***
## Get SPL Token Balance
Retrieve your private balance for any supported SPL token.
```typescript theme={null}
import { PublicKey } from '@solana/web3.js'
const balance = await client.getPrivateBalanceSpl(
mintAddress: PublicKey | string
)
```
### Parameters
| Parameter | Type | Required | Description |
| ------------- | --------------------- | -------- | -------------------------- |
| `mintAddress` | `PublicKey \| string` | Yes | The SPL token mint address |
### Returns
```typescript theme={null}
{
amount: number // Balance in base units
}
```
### Example
```typescript theme={null}
import { PublicKey } from '@solana/web3.js'
// USDC mint address
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')
const balance = await client.getPrivateBalanceSpl(USDC_MINT)
console.log('Private USDC:', balance.amount, 'base units')
console.log('Private USDC:', balance.amount / 1_000_000, 'USDC') // USDC has 6 decimals
```
***
## Get USDC Balance
Convenience method specifically for USDC.
```typescript theme={null}
const balance = await client.getPrivateBalanceUSDC()
```
### Returns
```typescript theme={null}
{
amount: number // Balance in base units (1 USDC = 1,000,000 base units)
}
```
### Example
```typescript theme={null}
const balance = await client.getPrivateBalanceUSDC()
console.log('Private USDC:', balance.amount / 1_000_000, 'USDC')
```
***
## Supported Tokens
| Token | Mint Address | Decimals | Units per Token |
| ----- | ---------------------------------------------- | -------- | --------------- |
| SOL | Native | 9 | 1,000,000,000 |
| USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | 6 | 1,000,000 |
| USDT | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` | 6 | 1,000,000 |
| ZEC | `A7bdiYdS5GjqGFtxf17ppRHtDKPkkRqbKtR27dxvQXaS` | 8 | 100,000,000 |
| ORE | `oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp` | 11 | 100,000,000,000 |
| STORE | `sTorERYB6xAZ1SSbwpK3zoK2EEwbBrc7TZAzg1uCGiH` | 11 | 100,000,000,000 |
***
## How Balance Checking Works
The SDK fetches all encrypted UTXOs from the relayer API
Using your encryption key, the SDK decrypts UTXOs that belong to you
Each UTXO is checked on-chain to see if it has been spent
The amounts of all unspent UTXOs are summed to get your balance
### Caching
The SDK caches decrypted UTXOs locally for faster subsequent balance checks:
```typescript theme={null}
// First call: fetches and decrypts all UTXOs (slower)
const balance1 = await client.getPrivateBalance()
// Second call: uses cached data + fetches only new UTXOs (faster)
const balance2 = await client.getPrivateBalance()
// Force refresh: clear cache and fetch everything
await client.clearCache()
const balance3 = await client.getPrivateBalance()
```
***
## Aborting Balance Requests
For long-running balance checks, you can abort the request:
```typescript theme={null}
const controller = new AbortController()
// Start balance check
const balancePromise = client.getPrivateBalance(controller.signal)
// Abort after 10 seconds
setTimeout(() => controller.abort(), 10000)
try {
const balance = await balancePromise
console.log('Balance:', balance.lamports)
} catch (error) {
if (error.message === 'aborted') {
console.log('Balance check was cancelled')
}
}
```
***
## Complete Example
```typescript theme={null}
import { PrivacyCash } from 'privacycash'
import { PublicKey } from '@solana/web3.js'
async function checkAllBalances() {
const client = new PrivacyCash({
RPC_url: process.env.SOLANA_RPC_URL!,
owner: process.env.PRIVATE_KEY!
})
// Check SOL balance
const solBalance = await client.getPrivateBalance()
console.log('SOL:', solBalance.lamports / 1e9)
// Check USDC balance
const usdcBalance = await client.getPrivateBalanceUSDC()
console.log('USDC:', usdcBalance.amount / 1e6)
// Check USDT balance
const USDT_MINT = new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB')
const usdtBalance = await client.getPrivateBalanceSpl(USDT_MINT)
console.log('USDT:', usdtBalance.amount / 1e6)
// Check ORE balance
const ORE_MINT = new PublicKey('oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp')
const oreBalance = await client.getPrivateBalanceSpl(ORE_MINT)
console.log('ORE:', oreBalance.amount / 1e11)
}
checkAllBalances()
```
# Balance
Source: https://privacycash.mintlify.app/sdk/balance-fe
# getBalanceFromUtxos()
`getBalanceFromUtxos()` is a utility function from `privacycash/utils` used to calculate the total balance from a set of SOL UTXOs (Unspent Transaction Outputs).
## Parameters
| Parameter | Type | Description |
| :-------- | :------- | :---------------------------------------------------------------- |
| `utxos` | `Utxo[]` | An array of UTXO objects, typically retrieved using `getUtxos()`. |
## Return Value
The function returns an object containing the balance information:
| Property | Type | Description |
| :--------- | :------- | :----------------------------- |
| `lamports` | `number` | The total balance in lamports. |
## Example Usage
```typescript theme={null}
import { getUtxos, getBalanceFromUtxos } from "privacycash/utils"
// 1. Fetch user UTXOs
const myValidUtxos = await getUtxos({
connection,
publicKey,
storage: localStorage,
encryptionService,
});
// 2. Calculate balance
const balanceInfo = getBalanceFromUtxos(myValidUtxos);
console.log("Total SOL Balance (lamports):", balanceInfo.lamports);
```
***
# getUtxos()
`getUtxos()` is used to fetch all valid private UTXOs for a given user.
## Parameters
| Parameter | Type | Description |
| :------------------ | :------------------ | :------------------------------------------- |
| `connection` | `Connection` | Solana web3 connection object. |
| `publicKey` | `PublicKey` | The user's Solana public key. |
| `storage` | `Storage` | A storage object (e.g., `localStorage`). |
| `encryptionService` | `EncryptionService` | The encryption service to decrypt UTXO data. |
| `offset` | `number` | (optional) utxo fetch offset |
## Example Usage
```typescript theme={null}
const myValidUtxos = await getUtxos({
connection,
publicKey,
storage: localStorage,
encryptionService,
});
```
***
# getBalanceFromUtxosSPL()
`getBalanceFromUtxosSPL()` calculates the total balance from a set of SPL token UTXOs.
## Parameters
| Parameter | Type | Description |
| :-------- | :------- | :--------------------------------------------------------- |
| `utxos` | `Utxo[]` | An array of UTXO objects, retrieved using `getUtxosSPL()`. |
## Return Value
| Property | Type | Description |
| :----------- | :------- | :------------------------------- |
| `base_units` | `number` | The total balance in base units. |
***
# getUtxosSPL()
`getUtxosSPL()` is used to fetch all valid private SPL token UTXOs for a given user and specific token mint.
## Parameters
| Parameter | Type | Description |
| :------------------ | :-------------------- | :--------------------------------------- |
| `connection` | `Connection` | Solana web3 connection object. |
| `publicKey` | `PublicKey` | The user's Solana public key. |
| `storage` | `Storage` | A storage object (e.g., `localStorage`). |
| `encryptionService` | `EncryptionService` | The encryption service. |
| `mintAddress` | `PublicKey or string` | The mint address of the SPL token. |
| `offset` | `number` | (optional) utxo fetch offset |
## Example Usage
```typescript theme={null}
import { getUtxosSPL, getBalanceFromUtxosSPL } from "privacycash/utils"
const myValidUtxos = await getUtxosSPL({
connection,
publicKey,
storage: localStorage,
encryptionService,
mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC
});
const balanceInfo = getBalanceFromUtxosSPL(myValidUtxos);
console.log("Total USDC Balance (base units):", balanceInfo.base_units);
```
# Bridge
Source: https://privacycash.mintlify.app/sdk/bridge
This guide explains how to build a front-end script to bridge assets from Solana to other chains using the PrivacyCash protocol and the Relayer API
## 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:
```bash theme={null}
npm install @solana/web3.js privacycash
```
***
## Implementation
```typescript theme={null}
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 = {
"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).
# Deposit
Source: https://privacycash.mintlify.app/sdk/deposit
Deposit SOL and SPL tokens into Privacy Cash
## Deposit SOL
Deposit SOL into the Privacy Cash privacy pool.
```typescript theme={null}
const result = await client.deposit({
lamports: number
})
```
### Parameters
| Parameter | Type | Required | Description |
| ---------- | -------- | -------- | --------------------------------------------------- |
| `lamports` | `number` | Yes | Amount in lamports (1 SOL = 1,000,000,000 lamports) |
### Returns
```typescript theme={null}
{
tx: string // Transaction signature
}
```
### Example
```typescript theme={null}
import { PrivacyCash } from 'privacycash'
const client = new PrivacyCash({
RPC_url: process.env.SOLANA_RPC_URL!,
owner: process.env.PRIVATE_KEY!
})
// Deposit 0.1 SOL
const result = await client.deposit({
lamports: 0.1 * 1_000_000_000 // 100,000,000 lamports
})
console.log('Transaction:', result.tx)
console.log('Explorer:', `https://explorer.solana.com/tx/${result.tx}`)
```
### Deposit Limits
The protocol enforces deposit limits to prevent abuse. Check the current limit before depositing large amounts.
If you exceed the deposit limit, the transaction will fail with `Don't deposit more than X SOL`.
### Fees
| Fee Type | Amount |
| ------------ | ------------------------------------ |
| Protocol fee | **Free** (0) |
| Network fee | \~0.002 SOL (Solana transaction fee) |
***
## How Deposits Work
The SDK creates a deposit transaction with your signed proof
Your wallet address is screened through CipherOwl for malicious activity
The relayer submits the transaction to Solana
An encrypted UTXO is created and stored on-chain, only decryptable by you
### Zero-Knowledge Proof
When you deposit, the SDK generates a ZK proof that:
1. Proves you own the funds being deposited
2. Creates an encrypted commitment that only you can decrypt
3. Ensures the relayer cannot modify any parameters
```typescript theme={null}
// The SDK handles all ZK proof generation automatically
const result = await client.deposit({
lamports: 50_000_000 // 0.05 SOL
})
// Behind the scenes:
// 1. ZK proof generated
// 2. Transaction signed
// 3. Sent to relayer
// 4. Confirmed on-chain
```
***
## Consolidation
If you already have a private balance, new deposits are automatically consolidated with existing UTXOs:
```typescript theme={null}
// First deposit: 0.1 SOL
await client.deposit({ lamports: 100_000_000 })
// Check balance
let balance = await client.getPrivateBalance()
console.log(balance.lamports) // 100,000,000
// Second deposit: 0.05 SOL (consolidates with first)
await client.deposit({ lamports: 50_000_000 })
// Balance is combined
balance = await client.getPrivateBalance()
console.log(balance.lamports) // 150,000,000
```
Consolidation happens automatically. You don't need to manage UTXOs manually.
***
## Best Practices
Deposit round, integer amounts (e.g., 1 SOL, 0.5 SOL) to avoid amount-based correlation
Use common deposit amounts that others also use to increase your anonymity set
### Example: Privacy-Optimized Deposit
```typescript theme={null}
// Good: Round amount
await client.deposit({ lamports: 1_000_000_000 }) // 1 SOL
// Avoid: Unique amount that's easy to trace
await client.deposit({ lamports: 1_234_567_890 }) // 1.23456789 SOL
```
***
## Error Handling
```typescript theme={null}
try {
const result = await client.deposit({
lamports: 100_000_000
})
console.log('Deposit successful:', result.tx)
} catch (error) {
if (error.message.includes('Insufficient balance')) {
console.error('Not enough SOL in wallet')
} else if (error.message.includes("Don't deposit more than")) {
console.error('Deposit exceeds protocol limit')
} else {
console.error('Deposit failed:', error.message)
}
}
```
### Common Errors
| Error | Solution |
| ------------------------------- | ------------------------------- |
| `Insufficient balance` | Add more SOL to your wallet |
| `Don't deposit more than X SOL` | Reduce deposit amount |
| `response not ok` | Check RPC connection, try again |
# Frontend Deposit
Source: https://privacycash.mintlify.app/sdk/deposit-fe
Frontend Deposit
# deposit()
`deposit()` is a function from `privacycash/utils` used to deposit SOL into the PrivacyCash protocol.
## Parameters
The `deposit()` function takes a single configuration object with the following properties:
| Parameter | Type | Description |
| :------------------- | :------------------ | :-------------------------------------------------------------------------------------------------------------------- |
| `lightWasm` | `any` | The Poseidon hasher instance (usually from `@lightprotocol/hasher.rs`). |
| `connection` | `Connection` | Solana web3 connection object. |
| `amount_in_lamports` | `number` | The amount of SOL to deposit, specified in lamports. |
| `keyBasePath` | `string` | The base path for loading circuit zkeys (e.g., `'/circuit2'`). |
| `publicKey` | `PublicKey` | The user's Solana public key. |
| `transactionSigner` | `Function` | A callback function to sign the generated transaction: `(tx: VersionedTransaction) => Promise`. |
| `storage` | `Storage` | A storage object that implements the Web Storage API (e.g., `localStorage`). |
| `encryptionService` | `EncryptionService` | An instance of the `EncryptionService` used for encrypting UTXO data. |
| `referrer` | `string` | (Optional) The Solana address of the referrer. |
***
## Example Usage
```typescript theme={null}
import { depositSPL } from "privacycash/utils"
import { PublicKey } from "@solana/web3.js"
const result = await depositSPL({
mintAddress: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), // USDC
base_units: 1_000_000, // 1 USDC (assuming 6 decimals)
lightWasm: hasher,
connection,
keyBasePath: '/circuit2',
publicKey: userPublicKey,
transactionSigner: async (tx: VersionedTransaction) => {
return await signTransaction(tx)
},
storage: localStorage,
encryptionService,
referrer: ''
});
```
# depositSPL()
`depositSPL()` is used to deposit SPL tokens (like USDC) into the PrivacyCash protocol.
## Parameters
The `depositSPL()` function takes a configuration object with the following properties:
| Parameter | Type | Description |
| :------------------ | :------------------ | :-------------------------------------------------------------------------------------- |
| `mintAddress` | `PublicKey` | The mint address of the SPL token to deposit. |
| `base_units` | `number` | The amount of tokens to deposit, specified in base units (e.g., amount \* 10^decimals). |
| `lightWasm` | `any` | The Poseidon hasher instance. |
| `connection` | `Connection` | Solana web3 connection object. |
| `keyBasePath` | `string` | The base path for loading circuit zkeys (e.g., `'/circuit2'`). |
| `publicKey` | `PublicKey` | The user's Solana public key. |
| `transactionSigner` | `Function` | A callback function to sign the generated transaction. |
| `storage` | `Storage` | A storage object (e.g., `localStorage`). |
| `encryptionService` | `EncryptionService` | An instance of the `EncryptionService`. |
| `referrer` | `string` | (Optional) The Solana address of the referrer. |
## Example Usage
```typescript theme={null}
import { deposit } from "privacycash/utils"
const result = await deposit({
lightWasm: hasher,
connection,
amount_in_lamports: 1_000_000_000, // 1 SOL
keyBasePath: '/circuit2',
publicKey: userPublicKey,
transactionSigner: async (tx: VersionedTransaction) => {
// let user sign the tx
return await signTransaction(tx)
},
storage: localStorage,
encryptionService,
referrer: '' // optional
});
```
# Frontend Integration
Source: https://privacycash.mintlify.app/sdk/frontend
## Demo Project
A complete example project demonstrating how to use PrivacyCash SDK:
[https://github.com/Privacy-Cash/solana-sdk-demo-interface](https://github.com/Privacy-Cash/solana-sdk-demo-interface)
## Installation
```bash theme={null}
npm install privacycash --save
```
Requires **Node.js 24+**. The SDK is written in TypeScript and includes type definitions.
## Deriving Encryption Key
Client need to ask user to sign an offchain message, before the user can interact with Privacy Cash.
```typescript theme={null}
async function getSignedSignature(signed: Signed) {
if (signed.signature) {
return
}
const encodedMessage = new TextEncoder().encode(`Privacy Money account sign in`)
const cacheKey = `zkcash-signature-${signed.publicKey.toBase58()}`
// ask for sign
let signature: Uint8Array
try {
signature = await signed.provider.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 (currentSigned.pubKeyStr != signed.publicKey.toString()) {
throw new Error(`Don't switch account when signing in. Refresh the page and try again.`)
}
// If wallet.signMessage returned an object, extract `signature`
// @ts-ignore
if (signature.signature) {
// @ts-ignore
signature = signature.signature
}
if (!(signature instanceof Uint8Array)) {
throw new Error('signature is not an Uint8Array type')
}
signed.signature = signature
}
export type Signed = {
publicKey: PublicKey,
signature?: Uint8Array,
provider: any
}
```
Once the user signed the message, pass the resulting signature to EncryptionService of the SDK:
```typescript theme={null}
export * from "privacycash/utils"
let encryptionService = new EncryptionService();
encryptionService.deriveEncryptionKeyFromSignature(signed.signature);
```
## Get hasher
```typescript theme={null}
const { WasmFactory } = await import('@lightprotocol/hasher.rs');
const hasher = await WasmFactory.getInstance();
```
## Common Issues
Next.js project needs to update the postinstall build scripts:
```text theme={null}
"scripts": {
"postinstall": "cp node_modules/@lightprotocol/hasher.rs/dist/hasher_wasm_simd_bg.wasm node_modules/@lightprotocol/hasher.rs/dist/browser-fat/es/ && cp node_modules/@lightprotocol/hasher.rs/dist/light_wasm_hasher_bg.wasm node_modules/@lightprotocol/hasher.rs/dist/browser-fat/es/",
},..}
```
## Warnings
Privacy Cash SDK requires consistent signature generation, otherwise the deposited tokens might be lost forever since the encrypted UTXO can't be decrypted. For frontend, please make sure deriveEncryptionKeyFromSignature() generates the same result for the same params passed in, and for backend, please make sure deriveEncryptionKeyFromWallet() generates the same result. Most wallets returns the same result, but some non major wallets might generate different result.
# Backend Integration
Source: https://privacycash.mintlify.app/sdk/overview
## Sample Project
[https://github.com/Privacy-Cash/privacy-cash-sdk/blob/main/example/](https://github.com/Privacy-Cash/privacy-cash-sdk/blob/main/example/)
## Installation
```bash theme={null}
npm install privacycash --save
```
Requires **Node.js 24+**. The SDK is written in TypeScript and includes type definitions.
## Initialization
```typescript theme={null}
import { PrivacyCash } from 'privacycash'
const client = new PrivacyCash({
RPC_url: 'YOUR_SOLANA_MAINNET_RPC_URL',
owner: 'YOUR_PRIVATE_KEY',
enableDebug: false // optional
})
```
### Supported Private Key Formats
```typescript theme={null}
// Base58 encoded string
const client1 = new PrivacyCash({
RPC_url: 'https://...',
owner: '5Jd7...' // Base58 string
})
// Byte array
const client2 = new PrivacyCash({
RPC_url: 'https://...',
owner: [1, 2, 3, ...] // number[]
})
// Uint8Array
const client3 = new PrivacyCash({
RPC_url: 'https://...',
owner: new Uint8Array([1, 2, 3, ...])
})
// Solana Keypair object
import { Keypair } from '@solana/web3.js'
const client4 = new PrivacyCash({
RPC_url: 'https://...',
owner: Keypair.generate()
})
```
## Interacting with Privacy Cash
Once above steps are done, client can query [balance](/sdk/balance), make [deposits](/sdk/deposit) and [withdrawals](/sdk/withdraw).
## Warnings
Privacy Cash SDK requires consistent signature generation, otherwise the deposited tokens might be lost forever since the encrypted UTXO can't be decrypted. For frontend, please make sure deriveEncryptionKeyFromSignature() generates the same result for the same params passed in, and for backend, please make sure deriveEncryptionKeyFromWallet() generates the same result. Most wallets returns the same result, but some non major wallets might generate different result.
# SPL Tokens
Source: https://privacycash.mintlify.app/sdk/spl-tokens
Deposit and withdraw USDC, USDT, and other SPL tokens
## Supported Tokens
Privacy Cash supports the following SPL tokens:
| Token | Mint Address | Decimals |
| ----- | ---------------------------------------------- | -------- |
| USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | 6 |
| USDT | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` | 6 |
| ZEC | `A7bdiYdS5GjqGFtxf17ppRHtDKPkkRqbKtR27dxvQXaS` | 8 |
| ORE | `oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp` | 11 |
| STORE | `sTorERYB6xAZ1SSbwpK3zoK2EEwbBrc7TZAzg1uCGiH` | 11 |
***
## Deposit SPL Tokens
Deposit any supported SPL token into Privacy Cash.
```typescript theme={null}
const result = await client.depositSPL({
mintAddress: PublicKey | string,
amount?: number, // Human-readable amount (e.g., 10 for 10 USDC)
base_units?: number // Raw amount in base units
})
```
### Parameters
| Parameter | Type | Required | Description |
| ------------- | --------------------- | ------------ | --------------------------- |
| `mintAddress` | `PublicKey \| string` | Yes | The SPL token mint address |
| `amount` | `number` | One of these | Human-readable token amount |
| `base_units` | `number` | One of these | Amount in base units |
Provide either `amount` OR `base_units`, not both. The `amount` parameter is converted to base units automatically.
### Returns
```typescript theme={null}
{
tx: string // Transaction signature
}
```
### Example: Deposit USDC
```typescript theme={null}
import { PublicKey } from '@solana/web3.js'
import { PrivacyCash } from 'privacycash'
const client = new PrivacyCash({
RPC_url: process.env.SOLANA_RPC_URL!,
owner: process.env.PRIVATE_KEY!
})
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')
// Deposit 10 USDC using human-readable amount
const result = await client.depositSPL({
mintAddress: USDC_MINT,
amount: 10 // 10 USDC
})
console.log('Deposit tx:', result.tx)
```
### Example: Deposit USDT
```typescript theme={null}
const USDT_MINT = new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB')
// Deposit 50 USDT
const result = await client.depositSPL({
mintAddress: USDT_MINT,
amount: 50
})
```
### Example: Using Base Units
```typescript theme={null}
// Deposit 2 USDC using base units (USDC has 6 decimals)
const result = await client.depositSPL({
mintAddress: USDC_MINT,
base_units: 2_000_000 // 2 USDC = 2,000,000 base units
})
```
***
## Deposit USDC (Convenience Method)
A convenience method specifically for USDC deposits:
```typescript theme={null}
const result = await client.depositUSDC({
base_units: number
})
```
### Example
```typescript theme={null}
// Deposit 5 USDC
const result = await client.depositUSDC({
base_units: 5_000_000 // 5 USDC
})
```
***
## Withdraw SPL Tokens
Withdraw any supported SPL token from your private balance.
```typescript theme={null}
const result = await client.withdrawSPL({
mintAddress: PublicKey | string,
amount?: number,
base_units?: number,
recipientAddress?: string,
referrer?: string
})
```
### Parameters
| Parameter | Type | Required | Description |
| ------------------ | --------------------- | ------------ | ----------------------------------------- |
| `mintAddress` | `PublicKey \| string` | Yes | The SPL token mint address |
| `amount` | `number` | One of these | Human-readable token amount |
| `base_units` | `number` | One of these | Amount in base units |
| `recipientAddress` | `string` | No | Recipient wallet. Defaults to your wallet |
| `referrer` | `string` | No | Optional referrer address |
### Returns
```typescript theme={null}
{
tx: string,
recipient: string,
base_units: number, // Amount received
fee_base_units: number, // Fee paid
isPartial: boolean
}
```
### Example: Withdraw USDC
```typescript theme={null}
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')
// Withdraw 10 USDC to a clean wallet
const result = await client.withdrawSPL({
mintAddress: USDC_MINT,
amount: 10,
recipientAddress: 'CLEAN_WALLET_ADDRESS'
})
console.log('Withdrew:', result.base_units / 1_000_000, 'USDC')
console.log('Fee:', result.fee_base_units / 1_000_000, 'USDC')
```
### Example: Withdraw USDT
```typescript theme={null}
const USDT_MINT = new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB')
const result = await client.withdrawSPL({
mintAddress: USDT_MINT,
amount: 25,
recipientAddress: 'RECIPIENT_ADDRESS'
})
```
***
## Withdraw USDC (Convenience Method)
```typescript theme={null}
const result = await client.withdrawUSDC({
base_units: number,
recipientAddress?: string,
referrer?: string
})
```
### Example
```typescript theme={null}
// Withdraw 100 USDC
const result = await client.withdrawUSDC({
base_units: 100_000_000, // 100 USDC
recipientAddress: 'CLEAN_WALLET'
})
```
***
## Get SPL Balance
Check your private balance for any supported SPL token.
```typescript theme={null}
const balance = await client.getPrivateBalanceSpl(mintAddress: PublicKey | string)
```
### Example
```typescript theme={null}
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')
const balance = await client.getPrivateBalanceSpl(USDC_MINT)
console.log('Private USDC:', balance.amount / 1_000_000)
```
***
## Get USDC Balance (Convenience Method)
```typescript theme={null}
const balance = await client.getPrivateBalanceUSDC()
console.log('Private USDC:', balance.amount / 1_000_000)
```
***
## Complete Example
Here's a complete example working with USDC and USDT:
```typescript theme={null}
import { PublicKey } from '@solana/web3.js'
import { PrivacyCash } from 'privacycash'
async function splExample() {
const client = new PrivacyCash({
RPC_url: process.env.SOLANA_RPC_URL!,
owner: process.env.PRIVATE_KEY!
})
const recipientAddress = process.env.RECIPIENT_ADDRESS!
// Token mint addresses
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')
const USDT_MINT = new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB')
// === USDC Operations ===
// Check initial USDC balance
let usdcBalance = await client.getPrivateBalanceSpl(USDC_MINT)
console.log('Initial USDC balance:', usdcBalance.amount / 1e6)
// Deposit 2 USDC
const depositUSDC = await client.depositSPL({
mintAddress: USDC_MINT,
amount: 2
})
console.log('USDC deposit tx:', depositUSDC.tx)
// Check balance after deposit
usdcBalance = await client.getPrivateBalanceSpl(USDC_MINT)
console.log('USDC after deposit:', usdcBalance.amount / 1e6)
// Withdraw 2 USDC
const withdrawUSDC = await client.withdrawSPL({
mintAddress: USDC_MINT,
amount: 2,
recipientAddress
})
console.log('USDC withdraw tx:', withdrawUSDC.tx)
// === USDT Operations ===
// Check initial USDT balance
let usdtBalance = await client.getPrivateBalanceSpl(USDT_MINT)
console.log('Initial USDT balance:', usdtBalance.amount / 1e6)
// Deposit 2 USDT
const depositUSDT = await client.depositSPL({
mintAddress: USDT_MINT,
amount: 2
})
console.log('USDT deposit tx:', depositUSDT.tx)
// Withdraw 2 USDT
const withdrawUSDT = await client.withdrawSPL({
mintAddress: USDT_MINT,
amount: 2,
recipientAddress
})
console.log('USDT withdraw tx:', withdrawUSDT.tx)
}
splExample().catch(console.error)
```
***
## SPL Token Fees
| Token | Deposit Fee | Withdrawal Fee |
| ------- | ----------- | -------------------------- |
| All SPL | Free | Rent fee + 0.35% of amount |
The rent fee varies by token and covers the Solana account rent for the recipient's token account if it doesn't exist.
***
## Requirements
Before depositing SPL tokens, ensure:
1. **Token account exists**: You must have a token account with the SPL token
2. **Sufficient balance**: Enough tokens to cover the deposit amount
3. **SOL for fees**: At least 0.002 SOL for Solana transaction fees
```typescript theme={null}
// The SDK will throw helpful errors if requirements aren't met:
// - "Insufficient balance. Need at least X USDC."
// - "Need at least 0.002 SOL for Solana fees."
// - "token not found: [address]" (unsupported token)
```
# Withdraw
Source: https://privacycash.mintlify.app/sdk/withdraw
Withdraw SOL from Privacy Cash to any wallet
## Withdraw SOL
Withdraw SOL from your private balance to any recipient address.
```typescript theme={null}
const result = await client.withdraw({
lamports: number,
recipientAddress?: string,
referrer?: string
})
```
### Parameters
| Parameter | Type | Required | Description |
| ------------------ | -------- | -------- | ----------------------------------------------------- |
| `lamports` | `number` | Yes | Amount in lamports to withdraw |
| `recipientAddress` | `string` | No | Recipient wallet address. Defaults to your own wallet |
| `referrer` | `string` | No | Optional referrer wallet address |
### Returns
```typescript theme={null}
{
tx: string, // Transaction signature
recipient: string, // Recipient address
amount_in_lamports: number, // Amount received (after fees)
fee_in_lamports: number, // Fee paid
isPartial: boolean // True if balance was insufficient for full amount
}
```
### Example
```typescript theme={null}
import { PrivacyCash } from 'privacycash'
const client = new PrivacyCash({
RPC_url: process.env.SOLANA_RPC_URL!,
owner: process.env.PRIVATE_KEY!
})
// Withdraw 0.1 SOL to a clean wallet
const result = await client.withdraw({
lamports: 0.1 * 1_000_000_000,
recipientAddress: 'CLEAN_WALLET_ADDRESS'
})
console.log('Transaction:', result.tx)
console.log('Amount received:', result.amount_in_lamports / 1_000_000_000, 'SOL')
console.log('Fee paid:', result.fee_in_lamports / 1_000_000_000, 'SOL')
```
***
## Withdrawal Fees
| Component | Amount |
| ------------ | -------------------------- |
| Base fee | 0.006 SOL per recipient |
| Protocol fee | 0.35% of withdrawal amount |
### Fee Calculation Example
```typescript theme={null}
// Withdrawing 1 SOL
const withdrawAmount = 1_000_000_000 // 1 SOL in lamports
// Fee calculation:
// Base: 0.006 SOL = 6,000,000 lamports
// Protocol: 1 SOL × 0.35% = 0.0035 SOL = 3,500,000 lamports
// Total fee: ~9,500,000 lamports (~0.0095 SOL)
const result = await client.withdraw({
lamports: withdrawAmount,
recipientAddress: 'RECIPIENT'
})
console.log('Requested:', withdrawAmount / 1e9, 'SOL')
console.log('Received:', result.amount_in_lamports / 1e9, 'SOL')
console.log('Fee:', result.fee_in_lamports / 1e9, 'SOL')
```
***
## How Withdrawals Work
The SDK selects your largest UTXOs to cover the withdrawal amount
A zero-knowledge proof is generated proving you own the funds
The proof is sent to the relayer, which signs and submits the transaction
The recipient receives funds with no on-chain link to your wallet
### Privacy Guarantee
The withdrawal transaction on-chain (visible on SolScan or other explorers) contains **no information** about the original depositor. The zero-knowledge proof ensures:
* The relayer cannot modify the recipient address
* The relayer cannot modify the amount
* Any tampering causes the transaction to fail
***
## Partial Withdrawals
If your private balance is less than the requested amount, the SDK performs a partial withdrawal:
```typescript theme={null}
// You have 0.05 SOL private balance
// Request to withdraw 0.1 SOL
const result = await client.withdraw({
lamports: 100_000_000, // 0.1 SOL
recipientAddress: 'RECIPIENT'
})
if (result.isPartial) {
console.log('Partial withdrawal - balance was insufficient')
console.log('Actually withdrew:', result.amount_in_lamports / 1e9, 'SOL')
}
```
***
## Withdraw to Self
If you omit `recipientAddress`, funds are withdrawn to your own wallet:
```typescript theme={null}
// Withdraw to your own wallet (unshield)
const result = await client.withdraw({
lamports: 50_000_000 // 0.05 SOL
})
console.log('Withdrawn to:', result.recipient) // Your wallet address
```
Withdrawing to your own wallet reduces privacy since it links your deposit and withdrawal addresses.
***
## Best Practices
Always withdraw to a fresh, never-used wallet address
Wait at least a day between deposit and withdrawal
Split large amounts into multiple smaller withdrawals over time
Don't withdraw the exact same amount you deposited
### Example: Privacy-Optimized Withdrawal
```typescript theme={null}
// Deposited 1 SOL yesterday
// Good: Withdraw different amounts over multiple days
await client.withdraw({
lamports: 400_000_000, // 0.4 SOL
recipientAddress: 'CLEAN_WALLET_1'
})
// Wait a day...
await client.withdraw({
lamports: 350_000_000, // 0.35 SOL
recipientAddress: 'CLEAN_WALLET_2'
})
// Wait another day...
await client.withdraw({
lamports: 200_000_000, // 0.2 SOL
recipientAddress: 'CLEAN_WALLET_3'
})
```
***
## Error Handling
```typescript theme={null}
try {
const result = await client.withdraw({
lamports: 100_000_000,
recipientAddress: 'RECIPIENT'
})
console.log('Withdrawal successful:', result.tx)
} catch (error) {
if (error.message.includes('no balance')) {
console.error('No private balance available')
} else if (error.message.includes('Need at least 1 unspent UTXO')) {
console.error('No UTXOs available for withdrawal')
} else {
console.error('Withdrawal failed:', error.message)
}
}
```
### Common Errors
| Error | Solution |
| ------------------------------ | ---------------------------------------- |
| `no balance` | Deposit funds first |
| `Need at least 1 unspent UTXO` | Wait for pending deposits to confirm |
| `withdraw amount too low` | Increase withdrawal amount to cover fees |
# Frontend Withdraw
Source: https://privacycash.mintlify.app/sdk/withdraw-fe
Frontend Withdraw
# withdraw()
`withdraw()` is a function from `privacycash/utils` used to withdraw SOL from the PrivacyCash protocol to a specified recipient address.
## Parameters
The `withdraw()` function takes a configuration object with the following properties:
| Parameter | Type | Description |
| :------------------- | :------------------ | :--------------------------------------------------------------------------- |
| `amount_in_lamports` | `number` | The amount of SOL to withdraw, specified in lamports. |
| `connection` | `Connection` | Solana web3 connection object. |
| `encryptionService` | `EncryptionService` | An instance of the `EncryptionService` used for decrypting UTXO data. |
| `keyBasePath` | `string` | The base path for loading circuit zkeys (e.g., `'/circuit2'`). |
| `publicKey` | `PublicKey` | The owner's Solana public key. |
| `storage` | `Storage` | A storage object that implements the Web Storage API (e.g., `localStorage`). |
| `recipient` | `PublicKey` | The Solana address that will receive the withdrawn funds. |
| `lightWasm` | `any` | The Poseidon hasher instance (usually from `@lightprotocol/hasher.rs`). |
***
## Example Usage
```typescript theme={null}
import { withdrawSPL } from "privacycash/utils"
import { PublicKey } from "@solana/web3.js"
const result = await withdrawSPL({
mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
base_units: 1_000_000, // 1 USDC
connection: connection,
encryptionService: encryptionService,
keyBasePath: '/circuit2',
publicKey: userPublicKey,
storage: localStorage,
recipient: new PublicKey('...'),
lightWasm: hasher,
});
```
# withdrawSPL()
`withdrawSPL()` is used to withdraw SPL tokens from the PrivacyCash protocol to a specified recipient address.
## Parameters
The `withdrawSPL()` function takes a configuration object with the following properties:
| Parameter | Type | Description |
| :------------------ | :------------------ | :--------------------------------------------------------- |
| `mintAddress` | `string` | The mint address of the SPL token. |
| `base_units` | `number` | The amount of tokens to withdraw, specified in base units. |
| `connection` | `Connection` | Solana web3 connection object. |
| `encryptionService` | `EncryptionService` | An instance of the `EncryptionService`. |
| `keyBasePath` | `string` | The base path for loading circuit zkeys. |
| `publicKey` | `PublicKey` | The owner's Solana public key. |
| `storage` | `Storage` | A storage object (e.g., `localStorage`). |
| `recipient` | `PublicKey` | The Solana address that will receive the withdrawn funds. |
| `lightWasm` | `any` | The Poseidon hasher instance. |
## Example Usage
```typescript theme={null}
import { withdraw } from "privacycash/utils"
import { PublicKey } from "@solana/web3.js"
const result = await withdraw({
amount_in_lamports: 1_000_000_000, // 1 SOL
connection: connection,
encryptionService: encryptionService,
keyBasePath: '/circuit2',
publicKey: userPublicKey,
storage: localStorage,
recipient: new PublicKey('...'), // Recipient address
lightWasm: hasher,
});
```