Skip to content

Confidential Asset (CA)

The @aptos-labs/confidential-asset package is a high-level TypeScript client for the Confidential Asset (CA) protocol. It handles proof generation, WASM initialization, balance decryption, and transaction building automatically.

Terminal window
npm i @aptos-labs/ts-sdk @aptos-labs/confidential-asset

The package depends on @aptos-labs/confidential-asset-bindings for cryptographic operations (Bulletproof range proofs and discrete-log decryption). The WASM binary is loaded on demand:

  • Browser: fetched from the unpkg CDN automatically.
  • Node.js: loaded from local node_modules (no network required).
import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";
import {
ConfidentialAsset,
TwistedEd25519PrivateKey,
initializeWasm,
isWasmInitialized,
} from "@aptos-labs/confidential-asset";
const config = new AptosConfig({ network: Network.TESTNET });
const aptos = new Aptos(config);
// The CA module is deployed at 0x1 (aptos_framework).
const ca = new ConfidentialAsset({ config });
// Pre-load WASM before the first user interaction to avoid latency.
await initializeWasm();
console.log("WASM ready:", isWasmInitialized());

ConfidentialAsset constructor options:

OptionTypeDefaultDescription
configAptosConfigrequiredNetwork configuration
confidentialAssetModuleAddressstring"0x1"Module address
withFeePayerbooleanfalseUse fee-payer transactions by default

CA operations use a dedicated keypair separate from the user's Aptos account signing key.

  • TwistedEd25519PrivateKey (decryption key, DK) — kept privately by the user; never leaves the client.
  • TwistedEd25519PublicKey (encryption key, EK) — stored on-chain; used by others to encrypt amounts for this user.
import { TwistedEd25519PrivateKey } from "@aptos-labs/confidential-asset";
// Generate a new decryption key
const dk = TwistedEd25519PrivateKey.generate();
// Derive the encryption key (public)
const ek = dk.publicKey();
// Serialize for secure storage
const dkHex = dk.toString();
// Restore from a saved hex string
const restoredDk = new TwistedEd25519PrivateKey(dkHex);
import { Aptos, AptosConfig, Network, Account } from "@aptos-labs/ts-sdk";
import { ConfidentialAsset, TwistedEd25519PrivateKey } from "@aptos-labs/confidential-asset";
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
const ca = new ConfidentialAsset({ config: aptos.config });
// APT Fungible Asset metadata address
const TOKEN = "0xa";
const alice = Account.generate();
const bob = Account.generate();
// Fund accounts via faucet before proceeding...
// 1. Generate decryption keys and register confidential balances
const aliceDk = TwistedEd25519PrivateKey.generate();
const bobDk = TwistedEd25519PrivateKey.generate();
await ca.registerBalance({ signer: alice, tokenAddress: TOKEN, decryptionKey: aliceDk });
await ca.registerBalance({ signer: bob, tokenAddress: TOKEN, decryptionKey: bobDk });
// 2. Deposit 1000 tokens from Alice's public balance → her pending balance
await ca.deposit({ signer: alice, tokenAddress: TOKEN, amount: 1000n });
// 3. Roll over pending → available
await ca.rolloverPendingBalance({
signer: alice,
tokenAddress: TOKEN,
senderDecryptionKey: aliceDk,
});
// 4. Decrypt and read Alice's balance
const balance = await ca.getBalance({
accountAddress: alice.accountAddress,
tokenAddress: TOKEN,
decryptionKey: aliceDk,
});
console.log("Available:", balance.availableBalance()); // bigint
console.log("Pending: ", balance.pendingBalance()); // bigint
// 5. Confidential transfer of 300 tokens to Bob (amount hidden on-chain)
await ca.transfer({
signer: alice,
tokenAddress: TOKEN,
recipient: bob.accountAddress,
amount: 300n,
senderDecryptionKey: aliceDk,
});
// 6. Withdraw 200 tokens back to Alice's public balance
await ca.withdraw({
signer: alice,
tokenAddress: TOKEN,
senderDecryptionKey: aliceDk,
amount: 200n,
});

Creates a confidential store for (signer, token) and publishes the EK on-chain.

await ca.registerBalance({
signer: alice,
tokenAddress: TOKEN,
decryptionKey: aliceDk,
});
// Check registration first
const registered = await ca.hasUserRegistered({
accountAddress: alice.accountAddress,
tokenAddress: TOKEN,
});

Transfers tokens from the signer's public FA balance into their pending_balance. The deposited amount is publicly visible. No ZKP required.

await ca.deposit({
signer: alice,
tokenAddress: TOKEN,
amount: 1000n,
recipient: alice.accountAddress, // optional; defaults to signer
});

Fetches encrypted balances from chain and decrypts them locally with the DK.

const balance = await ca.getBalance({
accountAddress: alice.accountAddress,
tokenAddress: TOKEN,
decryptionKey: aliceDk,
});
balance.availableBalance(); // bigint — spendable amount
balance.pendingBalance(); // bigint — pending (not yet spendable)

Decryption uses BSGS (Baby-Step Giant-Step) over each 16-bit chunk via the WASM module.

Moves pending_balance into available_balance.

await ca.rolloverPendingBalance({
signer: alice,
tokenAddress: TOKEN,
senderDecryptionKey: aliceDk, // needed if normalization is required
withPauseIncoming: false, // set true before key rotation
});

Re-packs available_balance chunks to 16-bit bounds. Usually called automatically by rolloverPendingBalance.

const normalized = await ca.isBalanceNormalized({
accountAddress: alice.accountAddress,
tokenAddress: TOKEN,
});
if (!normalized) {
await ca.normalizeBalance({
signer: alice,
tokenAddress: TOKEN,
senderDecryptionKey: aliceDk,
});
}

Transfers a secret amount from sender's available_balance to recipient's pending_balance. The amount is never revealed on-chain.

await ca.transfer({
signer: alice,
tokenAddress: TOKEN,
recipient: bob.accountAddress,
amount: 300n,
senderDecryptionKey: aliceDk,
additionalAuditorEncryptionKeys: [], // optional voluntary auditors
memo: new TextEncoder().encode("invoice #42"), // optional, max 256 bytes
});

To automatically include a rollover before transferring if needed, use transferWithTotalBalance:

await ca.transferWithTotalBalance({
signer: alice,
tokenAddress: TOKEN,
recipient: bob.accountAddress,
amount: 300n,
senderDecryptionKey: aliceDk,
});

Moves tokens from available_balance back to a public FA store.

await ca.withdraw({
signer: alice,
tokenAddress: TOKEN,
senderDecryptionKey: aliceDk,
amount: 200n,
recipient: bob.accountAddress, // optional; defaults to signer
});

Use withdrawWithTotalBalance to automatically rollover before withdrawing if needed.

Replaces the on-chain EK and re-encrypts the available_balance under the new key.

const newDk = TwistedEd25519PrivateKey.generate();
// The SDK handles: rollover + pause → rotate → resume
await ca.rotateEncryptionKey({
signer: alice,
tokenAddress: TOKEN,
senderDecryptionKey: aliceDk, // old DK
newSenderDecryptionKey: newDk, // new DK
});
// Save newDk securely — the old DK is now invalid for this balance.

When a token has a configured auditor (global or asset-specific), the SDK automatically includes encrypted amount ciphertexts for that auditor in every spend operation.

// Query the asset-level auditor key (undefined if not set)
const auditorEk = await ca.getAssetAuditorEncryptionKey({ tokenAddress: TOKEN });
// Add voluntary (per-transfer) auditors
await ca.transfer({
// ...other params
additionalAuditorEncryptionKeys: [auditorEk],
});

All write methods support fee-payer transactions for gas sponsorship:

// Enable globally
const caFeePayer = new ConfidentialAsset({ config, withFeePayer: true });
// Or per-call
await ca.deposit({ signer: alice, tokenAddress: TOKEN, amount: 1000n, withFeePayer: true });
Error codeMeaningResolution
E_CONFIDENTIAL_STORE_NOT_REGISTERED (3)No store for this (user, token)Call registerBalance first
E_CONFIDENTIAL_STORE_ALREADY_REGISTERED (2)Already registeredCheck hasUserRegistered first
E_INCOMING_TRANSFERS_PAUSED (4)Recipient paused incoming transfersWait or ask recipient to resume
E_PENDING_BALANCE_MUST_BE_ROLLED_OVER (6)Pending count hit 65536Call rolloverPendingBalance
E_NORMALIZATION_REQUIRED (7)Normalize before rolloverPass senderDecryptionKey to rollover, or call normalizeBalance
E_ALREADY_NORMALIZED (8)Already normalizedCheck isBalanceNormalized first
E_PENDING_BALANCE_NOT_ZERO_BEFORE_KEY_ROTATION (5)Pending must be zero before rotationSDK handles this automatically in rotateEncryptionKey
E_SELF_TRANSFER (15)Sender = recipientUse a different recipient
E_ASSET_TYPE_DISALLOWED (9)Token not on allow-listThis token type is not yet enabled
E_EMERGENCY_PAUSED (20)Protocol paused by governanceCheck isEmergencyPaused() and wait
E_UNSAFE_DISPATCHABLE_FA (16)Dispatchable FA not supportedOnly non-dispatchable FA types are supported
E_MEMO_TOO_LONG (19)Memo > 256 bytesShorten the memo

Pre-warm WASM. Call initializeWasm() when the user opens the CA UI, before any operation, to avoid a noticeable delay on the first proof generation.

Check rollover need before spending. If pendingBalance() is non-zero and would make the total sufficient, use the *WithTotalBalance variants to combine rollover and spend automatically.

Verify recipient registration. Call hasUserRegistered before attempting a transfer.

Save the new key immediately after rotation. rotateEncryptionKey invalidates the old DK. Persist the new DK and verify it can decrypt the balance before discarding the old one.

HiddenNot hidden
Transfer amountsSender and recipient addresses
User balancesTotal tokens in confidential mode (get_total_confidential_supply)