Stabletrust SDK Demo
The complete source code for this guide.
What You Are Building
Stabletrust uses Homomorphic Encryption (HE) to give each user a confidential token balance alongside their normal public balance. Once tokens enter the confidential layer, their amounts are encrypted onchain opaque to block explorers and other observers. The user flow:- Connect wallet via Privy.
- Initialize a confidential account, which generates the user’s HE keypair through a wallet signature.
- Deposit public tokens into the encrypted layer, transfer privately to another address, and withdraw back to a public balance when needed.
- Next.js 16 (App Router, TypeScript)
- Privy for embedded wallet and authentication
- ethers.js v6 for signer management and amount formatting
@fairblock/stabletrustfor confidential operations- viem for chain definitions
Prerequisites
- Node.js 18 or higher
- A Privy App ID created at dashboard.privy.io
- A wallet funded with test ETH on Base Sepolia for gas
Step 1: Create the Next.js Application
Step 2: Install Dependencies
Step 3: Environment Variables
Create.env.local in the root of your project:
NEXT_PUBLIC_PRIVY_APP_ID: from your Privy dashboardNEXT_PUBLIC_RPC_URL: the JSON-RPC endpoint for Base SepoliaNEXT_PUBLIC_TOKEN_ADDRESS: the ERC20 token for this demo (test USDC on Base Sepolia)NEXT_PUBLIC_CHAIN_ID:84532is Base Sepolia
Step 4: Add the Privy Provider
Privy must wrap the entire application so its authentication context is available everywhere. Createapp/Providers.tsx:
supportedChains and defaultChain lock the app to Base Sepolia so the wallet always targets the correct network.
You can see the complete Provider implementation here: app/Providers.tsx
Then wrap your layout in app/layout.tsx:
usePrivy() and useWallets().
You can see the complete Providers.tsx used in the demo here: app/Providers.tsx
Step 5: Building the Hook, Step by Step
Everything from this point forward lives inside a single React hook:app/hooks/useConfidentialClient.ts. This section builds it piece by piece so each operation is clear before the next one is added.
Start by creating the file with its imports and state:
5.1 Initialize the SDK Client
TheConfidentialTransferClient is the entry point to all SDK operations. It connects to the network and resolves the correct Stabletrust contract automatically from the chain ID.
Add this useEffect to your hook:
new ConfidentialTransferClient:
You only need one client instance for the lifetime of the app. Creating it once on mount is sufficient.
Custom deployments. If you are pointing at a custom Stabletrust deployment rather than one the SDK resolves automatically from the chain ID, pass the contract address explicitly as a middle argument:
5.2 Wrap the Privy Wallet into an Ethers Signer
The Stabletrust SDK expects a standardethers.Signer. Privy provides its own wallet abstraction, so you need to extract the raw EIP-1193 provider from it and wrap it with ethers.
Add this useEffect to your hook it runs whenever authentication state or the wallet list changes:
switchChain: forces the wallet onto the correct network before any transaction is signed. Without this, the user could accidentally sign on the wrong chain.getEthereumProvider(): returns the raw EIP-1193 provider Privy uses internally.BrowserProvider: the ethers.js v6 wrapper for browser-injected providers.getSigner(): returns aJsonRpcSignercapable of signing transactions and messages.
signer produced here is passed into every SDK operation below.
5.3 Initialize the Confidential Account
Before any confidential operation can happen, the user must have a confidential account registered onchain with their HE public key.ensureAccount handles the full flow in one call: it prompts a wallet signature, derives the HE keypair from that signature, and registers the public key onchain if it does not yet exist.
Add this function to your hook:
client.ensureAccount:
Returns:
{ publicKey: string, privateKey: string }
The privateKey returned here is a derived HE key, not the user’s wallet private key. It never leaves the browser and is re-derived on each session from the wallet signature. Store it in React state and pass it to getConfidentialBalance later.
Account initialization includes an onchain finalization step. Expect this to take approximately 45 seconds on the first call. The method waits for finalization before returning, so
loading will be true for the duration.5.4 Deposit: Move Tokens into the Confidential Layer
Deposit converts public ERC20 tokens into an encrypted confidential balance. The SDK handles the ERC20 approval and the deposit transaction in a single call you do not need to send a separateapprove transaction.
Add this function to your hook:
client.confidentialDeposit:
Returns: A transaction receipt once the deposit is confirmed and finalized onchain.
5.5 Withdraw: Move Tokens Back to Public
Withdraw removes tokens from the encrypted confidential balance and returns them to the user’s standard ERC20 balance. After withdrawal, the amount is visible onchain again. Add this function to your hook:client.withdraw:
Returns: A transaction receipt.
5.6 Transfer Send Tokens Privately
Confidential transfer sends tokens from the caller’s encrypted balance to another address’s encrypted balance. The amount is encrypted onchain: block explorers show that a transfer occurred and between which addresses, but not the value moved. The sender and recipient addresses stay visible this client hides amounts, not identities. If you also need to hide the sender’s address, use the Unlinkable Transfers flow. Add this function to your hook:client.confidentialTransfer:
Returns: A transaction receipt.
5.7 Fetch the Confidential Balance
The confidential balance is stored encrypted onchain. The SDK decrypts it client-side using the user’sprivateKey from ensureAccount. Call this after every operation to reflect the latest state.
Add this function to your hook:
client.getConfidentialBalance:
Return fields:
available is what can be transferred or withdrawn immediately. pending represents amounts that have been deposited but are still finalizing onchain after finalization, pending becomes available. For most display purposes, show amount (the total) and optionally break it down into available and pending.
5.8 Finish the Hook
Wire up balance polling and export everything. Add this to the bottom of the hook, just before the closing return:Step 6: Building the UI Step by Step
The UI is a single page component atapp/page.tsx. This section builds it piece by piece in the same order as the hook connect, initialize, deposit, withdraw, transfer.
Start with the skeleton:
6.1 Connect Wallet
The first thing the user sees is a connect button. Show it when unauthenticated, and swap it for a disconnect button and the user’s wallet address once connected. Add this block inside<main>:
login() opens its built-in modal. user?.wallet?.address is the connected address it only exists after authentication, so the optional chain prevents errors during the pre-auth render.
6.2 Initialize Confidential Account
Once connected, the user must initialize their confidential account before any other action is possible. Show this block only when authenticated butuserKeys is still null.
Add this block after the connect section:
ensureAccount comes directly from the hook. loading is set to true by the hook for the duration of the call, so the button disables automatically while the transaction is processing.
6.3 Balance Display
OnceuserKeys exists, show the user’s public and confidential balances at the top of the action area. These values are refreshed automatically by the hook’s 10-second polling interval and after each operation.
Add this block after the initialize section:
balances.public and balances.confidential are already formatted as human-readable strings by the hook (via ethers.formatUnits) no conversion needed here.
6.4 Deposit Form
The deposit form moves tokens from the public balance into the confidential layer. Add this state to the top of the component alongside the other state declarations:"10"). The hook’s confidentialDeposit function handles the ethers.parseUnits conversion internally, so you pass the raw string directly.
6.5 Withdraw Form
The withdraw form moves tokens from the confidential balance back to the public ERC20 balance. Add this state:6.6 Transfer Form
The transfer form sends tokens privately to another address. It requires a recipient address in addition to an amount. Add this state:recipient and transferAmount have values, preventing accidental empty submissions.
6.7 Error Display
Add a global error banner that surfaces errors from any hook operation. Place this at the bottom of the<main> content, after all the action blocks:
error string is set by the hook when any operation throws. It resets to null at the start of each new operation, so stale errors clear automatically when the user retries.
You can see the complete UI implementation here: app/page.tsx
Step 7: Run the Application
1
Connect Wallet
Click Connect Wallet - Privy opens a modal.
2
Initialize Confidential Account
Click Initialize Confidential Account - the wallet prompts a signature. Wait approximately 45 seconds for onchain finalization.
3
Account becomes active
The balance display and action forms appear once the account is active.
4
Deposit, Withdraw, Transfer
Deposit moves tokens from your public balance into the encrypted layer, Withdraw moves them back to your public ERC20 balance, and Transfer sends them privately to another address (recipient must have an initialized account).
Further Reading
Architecture
The cryptographic design and system layers behind confidential transfers.
Stabletrust
The browser-based interface for confidential stablecoins and transfers.
Confidential Transfers
How the core confidential operations work end to end.
Common Errors
Troubleshoot errors returned by the SDK.