Skip to content

Confidential Asset (CA)

The @aptos-labs/confidential-assets package provides a high-level TypeScript API for interacting with the Confidential Asset protocol on Aptos.

Terminal window
npm install @aptos-labs/confidential-assets @aptos-labs/ts-sdk @aptos-labs/confidential-asset-wasm-bindings

Operations in CA require ZK-proof generation and discrete log solving, both powered by a unified WASM module. Initialize it once at the top of your app:

import { initializeWasm } from "@aptos-labs/confidential-assets";
await initializeWasm();

This initializes both the range proof system (for generating ZKPs) and the discrete log solver (for decrypting balances).

For native apps, you can generate Android and iOS bindings from the confidential-asset-wasm-bindings repository.


Now create the ConfidentialAsset client:

import { AptosConfig, Network } from "@aptos-labs/ts-sdk";
import { ConfidentialAsset } from "@aptos-labs/confidential-assets";
const config = new AptosConfig({ network: Network.TESTNET });
const confidentialAsset = new ConfidentialAsset({ config });

To interact with the confidential asset, create a unique key pair first.

Generate new:

import { TwistedEd25519PrivateKey } from "@aptos-labs/confidential-assets";
const dk = TwistedEd25519PrivateKey.generate();

Or import an existing one:

const dk = new TwistedEd25519PrivateKey("0x...");

You can also derive it from a signature (for testing purposes, don’t use in production):

import { Account } from "@aptos-labs/ts-sdk";
const user = Account.generate();
const signature = user.sign(TwistedEd25519PrivateKey.decryptionKeyDerivationMessage);
const dk = TwistedEd25519PrivateKey.fromSignature(signature);

Before using confidential balances, register an encryption key for the asset type:

const registerTxResponse = await confidentialAsset.registerBalance({
signer: user,
tokenAddress: TOKEN_ADDRESS,
decryptionKey: dk,
});

Check if a user has already registered for a specific asset type:

const isRegistered = await confidentialAsset.hasUserRegistered({
accountAddress: user.accountAddress,
tokenAddress: TOKEN_ADDRESS,
});

Deposit tokens from a non-confidential FA balance into a confidential balance:

const depositTxResponse = await confidentialAsset.deposit({
signer: user,
tokenAddress: TOKEN_ADDRESS,
amount: 100n,
});

Check the user’s balance after the deposit. The getBalance method returns the decrypted pending and available balances:

const balance = await confidentialAsset.getBalance({
accountAddress: user.accountAddress,
tokenAddress: TOKEN_ADDRESS,
decryptionKey: dk,
});
console.log("Available:", balance.availableBalance()); // bigint
console.log("Pending:", balance.pendingBalance()); // bigint

After depositing, the funds are in the pending balance. A user cannot spend pending balance directly — it must be rolled over to the available balance.

const rolloverTxResponses = await confidentialAsset.rolloverPendingBalance({
signer: user,
tokenAddress: TOKEN_ADDRESS,
senderDecryptionKey: dk, // Required if balance might not be normalized
});

This may return multiple transaction responses (a normalization transaction followed by a rollover transaction) if the available balance was not already normalized.


Usually you don’t need to call normalization explicitly — rolloverPendingBalance handles it automatically when given a senderDecryptionKey.

If you want to normalize manually:

const isNormalized = await confidentialAsset.isBalanceNormalized({
accountAddress: user.accountAddress,
tokenAddress: TOKEN_ADDRESS,
});
if (!isNormalized) {
const normalizeTxResponse = await confidentialAsset.normalizeBalance({
signer: user,
senderDecryptionKey: dk,
tokenAddress: TOKEN_ADDRESS,
});
}

Withdraw assets from a confidential balance back to a non-confidential FA balance:

const withdrawTxResponse = await confidentialAsset.withdraw({
signer: user,
senderDecryptionKey: dk,
tokenAddress: TOKEN_ADDRESS,
amount: 50n,
recipient: recipientAddress, // Optional — defaults to signer's address
});

If the available balance is insufficient but the total (available + pending) is enough, use withdrawWithTotalBalance which automatically rolls over the pending balance first:

const withdrawTxResponses = await confidentialAsset.withdrawWithTotalBalance({
signer: user,
senderDecryptionKey: dk,
tokenAddress: TOKEN_ADDRESS,
amount: 50n,
});

For a confidential transfer, you need the recipient’s account address. The SDK automatically fetches the recipient’s on-chain encryption key:

const transferTxResponse = await confidentialAsset.transfer({
signer: user,
recipient: recipientAddress,
tokenAddress: TOKEN_ADDRESS,
amount: 50n,
senderDecryptionKey: dk,
additionalAuditorEncryptionKeys: [], // Optional voluntary auditors
});

Like with withdrawals, you can use transferWithTotalBalance to automatically roll over the pending balance if the available balance is insufficient:

const transferTxResponses = await confidentialAsset.transferWithTotalBalance({
signer: user,
recipient: recipientAddress,
tokenAddress: TOKEN_ADDRESS,
amount: 50n,
senderDecryptionKey: dk,
});

You can also look up a user’s encryption key directly:

const recipientEK = await confidentialAsset.getEncryptionKey({
accountAddress: recipientAddress,
tokenAddress: TOKEN_ADDRESS,
});

To rotate the encryption key, provide the current and new decryption keys. The SDK handles pausing incoming transfers, rolling over pending balance, rotating the key, and resuming transfers:

const newDk = TwistedEd25519PrivateKey.generate();
const rotationTxResponses = await confidentialAsset.rotateEncryptionKey({
signer: user,
senderDecryptionKey: dk,
newSenderDecryptionKey: newDk,
tokenAddress: TOKEN_ADDRESS,
});
// Save the new decryption key — the old one is no longer valid
console.log("New DK:", newDk.toString());

Check if a user’s incoming transfers are paused:

const isPaused = await confidentialAsset.isIncomingTransfersPaused({
accountAddress: user.accountAddress,
tokenAddress: TOKEN_ADDRESS,
});

Query auditor configuration for an asset type:

// Get effective auditor encryption key (asset-specific if set, otherwise global)
const auditorEK = await confidentialAsset.getAssetAuditorEncryptionKey({
tokenAddress: TOKEN_ADDRESS,
});