Skip to content

Confidential Asset (CA)

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

This guide matches the 1.1.x SDK line (for example 1.1.1), which targets the current aptos_framework::confidential_asset entry functions such as register_raw, confidential_transfer_raw, and rotate_encryption_key_raw.

Terminal window
npm install @aptos-labs/confidential-asset@^1.1.1 @aptos-labs/ts-sdk

@aptos-labs/confidential-asset-bindings is a transitive dependency that ships the WebAssembly glue used for range proof generation/verification and discrete log solving. You don’t need to install it directly.

The .wasm binary is loaded lazily — it is not bundled with the SDK, so apps that never use confidential assets don’t pay the ~774 KiB binary cost. The first call to a function that needs WASM will trigger a one-time load (from node_modules in Node.js, or from a CDN in the browser). No explicit initializeWasm() call is required.

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


Create the ConfidentialAsset client:

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

Use confidentialAsset.<operation>(…) for submit-and-wait flows. The nested confidentialAsset.transaction builder returns unsigned SimpleTransaction payloads only; it does not chain helper steps (for example it will not insert a normalize_raw transaction before rollover_pending_balance the way rolloverPendingBalance does when you pass senderDecryptionKey).

The constructor also accepts:

  • confidentialAssetModuleAddress — override the framework address (defaults to 0x1).
  • withFeePayer — when true, every transaction submitted by this client is built with a fee-payer address.

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

Generate new:

import { TwistedEd25519PrivateKey } from "@aptos-labs/confidential-asset";
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);

Or derive a DK from a BIP-44 mnemonic + hardened derivation path (Ed25519 supports hardened derivation only):

const dk = TwistedEd25519PrivateKey.fromDerivationPath(
"m/44'/637'/0'/0'/0'",
"your twelve word mnemonic here ...",
);

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,
recipient: someAddress, // Optional — defaults to the signer's own confidential balance
});

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
withPauseIncoming: false, // Set to true to also pause incoming transfers (for key rotation flows)
});

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

Set withPauseIncoming: true to invoke the on-chain rollover_pending_balance_and_pause entry function, which atomically rolls over and pauses incoming transfers — the prerequisite for key rotation.


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
memo: new TextEncoder().encode("invoice-42"), // Optional, up to 256 bytes
});

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());

When building the low-level rotation transaction yourself via confidentialAsset.transaction.rotateEncryptionKey, optional flags mirror the on-chain entry function: unpause defaults to true (same as resume_incoming_transfers in rotate_encryption_key_raw), and checkPendingBalanceEmpty defaults to true.

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,
});

Read the auditor hint recorded against the user’s confidential store. Auditors compare its (isGlobal, epoch) against the current effective auditor config to detect stale ciphertexts:

const hint = await confidentialAsset.getEffectiveAuditorHint({
accountAddress: user.accountAddress,
tokenAddress: TOKEN_ADDRESS,
});
if (hint) {
console.log(`Encrypted under ${hint.isGlobal ? "global" : "asset-specific"} auditor at epoch ${hint.epoch}`);
}

Governance can pause all user operations on the protocol via set_emergency_paused. Check the current state before submitting transactions:

const paused = await confidentialAsset.isEmergencyPaused();

Query confidential asset activity for an account from the indexer. Each row is a discriminated union keyed on event_type ("Registered", "Deposited", "Withdrawn", "Transferred", "Normalized", "RolledOver", "KeyRotated", "IncomingTransfersPauseChanged", "AllowListingChanged", "ConfidentialityForAssetTypeChanged", "GlobalAuditorChanged", "AssetSpecificAuditorChanged"):

const activities = await confidentialAsset.getActivities({
where: { owner_address: { _eq: user.accountAddress.toStringLong() } },
orderBy: [{ transaction_version: "desc" }],
limit: 20,
});
for (const a of activities) {
if (a.event_type === "Transferred") {
// Narrowed to TransferredActivity — counterparty_address is non-null,
// event_data has amount_P / amount_R_* / memo / sender_auditor_hint.
console.log(a.counterparty_address, a.event_data.memo);
} else if (a.event_type === "Withdrawn") {
// Narrowed to WithdrawnActivity — `amount` is plaintext.
console.log(a.amount, a.event_data.auditor_hint);
}
}