SDK
Confidential Transfer Client
Encrypt token balances and transfer amounts on any EVM chain. Your wallet address is visible on-chain, but nobody can see how much you hold or send. No relay required — connects directly to any RPC endpoint.
Prerequisites
- Node.js 18 or later
- An ethers.js v6 compatible signer (MetaMask, Privy, server wallet)
- Test ETH on your target network for gas
- An ERC-20 token on a supported network. See Introduction for the full list.
Initialize
Pass an RPC URL and chain ID. The contract address resolves automatically.
import { ConfidentialTransferClient } from "@fairblock/stabletrust";
import { ethers } from "ethers";
// Pass RPC URL + chain ID. Contract address resolves automatically.
// See Introduction for all supported chains and addresses.
const client = new ConfidentialTransferClient(
"https://sepolia.base.org",
84532,
);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, client.provider);Create an account
Call once per wallet. Takes about 45 seconds to finalize on-chain. Returns the ElGamal keypair used for encrypting your balance.
// Creates the account if it does not exist and waits for on-chain finalization.
// Returns { publicKey, privateKey } — the ElGamal keypair derived from your wallet.
// Safe to call on every startup.
const keys = await client.ensureAccount(wallet);Deposit
Converts public ERC-20 into an encrypted on-chain balance. ERC-20 approval is handled automatically.
const tokenAddress = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const tokenDecimals = 6;
// ERC-20 approval is handled automatically.
const receipt = await client.confidentialDeposit(
wallet,
tokenAddress,
ethers.parseUnits("10", tokenDecimals),
);Check balance
Decryption happens entirely client-side. Your private key never leaves your device. Balances are returned in raw ERC-20 units (bigint).
const balance = await client.getConfidentialBalance(
wallet.address,
keys.privateKey,
tokenAddress,
);
// Decryption happens client-side. Your private key never leaves your device.
// Amounts are in raw ERC-20 units (bigint).
console.log("Available:", ethers.formatUnits(balance.available.amount, tokenDecimals));
console.log("Pending: ", ethers.formatUnits(balance.pending.amount, tokenDecimals));Transfer
Sends encrypted tokens to a recipient. Transfer amounts stay hidden on-chain. A small native ETH fee is collected automatically.
// Recipient must have a confidential account.
const receipt = await client.confidentialTransfer(
wallet,
"0xRecipientAddress",
tokenAddress,
ethers.parseUnits("3", tokenDecimals),
);The recipient must have an initialized confidential account. Have them call ensureAccount(wallet) first.
Withdraw
Converts encrypted tokens back to standard ERC-20 in your public balance. The ZK proof is generated client-side automatically.
const receipt = await client.withdraw(
wallet,
tokenAddress,
ethers.parseUnits("2", tokenDecimals),
);Method reference
ensureAccount(wallet, options?)Creates a confidential account if it does not exist, waits for on-chain finalization (~45 s), and returns { publicKey, privateKey }. Safe to call on every startup.
Must be called before any deposit, transfer, or withdrawal.
getAccountInfo(address)Returns on-chain account state: { exists, finalized, pendingAction, txId, elgamalPubkey, pendingCreditCounter }.
confidentialDeposit(wallet, tokenAddress, amount, options?)Converts public ERC-20 into an encrypted confidential balance. ERC-20 approval is automatic. Pass amount as bigint via ethers.parseUnits().
getConfidentialBalance(address, privateKey, tokenAddress)Decrypts and returns { amount, available, pending } with amounts in raw ERC-20 units (bigint). Decryption is client-side.
getPublicBalance(address, tokenAddress)Returns the plain ERC-20 token balance (bigint, raw units) for a given address.
confidentialTransfer(senderWallet, recipientAddress, tokenAddress, amount, options?)Sends encrypted tokens to another confidential account. Amount and balance stay hidden on-chain. A small native ETH fee is collected automatically.
The recipient must have an initialized confidential account.
withdraw(wallet, tokenAddress, amount, options?)Converts encrypted tokens back to standard ERC-20. A ZK withdrawal proof is generated client-side.
getFeeAmount()Returns the native ETH fee (bigint, wei) required per confidential transfer. The SDK passes this automatically — no manual handling needed.
Common errors
| Error | Resolution |
|---|---|
Account does not exist | Call ensureAccount(wallet) before any deposit, transfer, or withdrawal |
Recipient account does not exist | The recipient must call ensureAccount() on their wallet first |
Insufficient balance | Check available balance with getConfidentialBalance(). Pending credits must be applied first |
Account finalization timeout | Wait ~45 s for on-chain finalization and retry |
Proof generation failed | Verify all parameters and ensure sufficient balance. Fetch a fresh balance and retry |
contractAddress is required for chainId | Pass the contract address explicitly as the second constructor argument |