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.

typescript
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.

typescript
// 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.

typescript
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).

typescript
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.

typescript
// 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.

typescript
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

ErrorResolution
Account does not existCall ensureAccount(wallet) before any deposit, transfer, or withdrawal
Recipient account does not existThe recipient must call ensureAccount() on their wallet first
Insufficient balanceCheck available balance with getConfidentialBalance(). Pending credits must be applied first
Account finalization timeoutWait ~45 s for on-chain finalization and retry
Proof generation failedVerify all parameters and ensure sufficient balance. Fetch a fresh balance and retry
contractAddress is required for chainIdPass the contract address explicitly as the second constructor argument