Skip to content
🎉 Welcome to the new Aptos Docs! Click here to submit an issue.
BuildSDKsTypeScript SDKConfidential Asset (CA)

Interact with Confidential Asset (CA) via the Typescript SDK

You can use confidentialCoin property of Aptos client to interact with CA

Initialization

Operations in CA require generating zk-proofs (ZKPs), and depending on your environment, you need to define a Range Proof calculation.

For the web, you could use confidential-asset-wasm-bindings/confidential-asset-wasm-bindings:

Let’s prepare range-proof generation and configure SDK to use it:

import initWasm, {
  batch_range_proof as batchRangeProof,
  batch_verify_proof as batchVerifyProof,
  range_proof as rangeProof,
  verify_proof as verifyProof,
} from '@aptos-labs/confidential-asset-wasm-bindings/range-proofs'
import {
  BatchRangeProofInputs,
  BatchVerifyRangeProofInputs,
  RangeProofInputs,
  VerifyRangeProofInputs,
} from '@lukachi/aptos-labs-ts-sdk'
 
const RANGE_PROOF_WASM_URL =
  'https://unpkg.com/@aptos-labs/confidential-asset-wasm-bindings@0.3.16/range-proofs/aptos_rp_wasm_bg.wasm'
 
export async function genBatchRangeZKP(
  opts: BatchRangeProofInputs,
): Promise<{ proof: Uint8Array; commitments: Uint8Array[] }> {
  await initWasm({ module_or_path: RANGE_PROOF_WASM_URL })
 
  const proof = batchRangeProof(
    new BigUint64Array(opts.v),
    opts.rs,
    opts.val_base,
    opts.rand_base,
    opts.num_bits,
  )
 
  return {
    proof: proof.proof(),
    commitments: proof.comms(),
  }
}
 
export async function verifyBatchRangeZKP(
  opts: BatchVerifyRangeProofInputs,
): Promise<boolean> {
  await initWasm({ module_or_path: RANGE_PROOF_WASM_URL })
 
  return batchVerifyProof(
    opts.proof,
    opts.comm,
    opts.val_base,
    opts.rand_base,
    opts.num_bits,
  )
}

And then, just place this at the very top of your app:

import { RangeProofExecutor } from '@aptos-labs/ts-sdk'
 
RangeProofExecutor.setGenBatchRangeZKP(genBatchRangeZKP);
RangeProofExecutor.setVerifyBatchRangeZKP(verifyBatchRangeZKP);
RangeProofExecutor.setGenerateRangeZKP(generateRangeZKP);
RangeProofExecutor.setVerifyRangeZKP(verifyRangeZKP);

For the native apps:

Generate android and ios bindings here and integrate in your app as you please.

And the last, but not the least important part:

To get a “numeric” value of the confidential balance, you also need to solve a Discrete Logarithm Problem (DLP). CA implements the Pollard’s Kangaroo method for solving DLPs on the Ristretto curve. Source

So we also need to initialize a decryption function for that:

// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0
 
import initWasm, {
  create_kangaroo,
  WASMKangaroo,
} from '@aptos-labs/confidential-asset-wasm-bindings/pollard-kangaroo'
import {
  ConfidentialAmount,
  TwistedEd25519PrivateKey,
  TwistedElGamal,
  TwistedElGamalCiphertext,
} from '@lukachi/aptos-labs-ts-sdk'
import { bytesToNumberLE } from '@noble/curves/abstract/utils'
 
const POLLARD_KANGAROO_WASM_URL =
  'https://unpkg.com/@aptos-labs/confidential-asset-wasm-bindings@0.3.15/pollard-kangaroo/aptos_pollard_kangaroo_wasm_bg.wasm'
 
export async function createKangaroo(secret_size: number) {
  await initWasm({ module_or_path: POLLARD_KANGAROO_WASM_URL })
 
  return create_kangaroo(secret_size)
}
 
export const preloadTables = async () => {
  const kangaroo16 = await createKangaroo(16)
  const kangaroo32 = await createKangaroo(32)
  const kangaroo48 = await createKangaroo(48)
 
  TwistedElGamal.setDecryptionFn(async pk => {
    if (bytesToNumberLE(pk) === 0n) return 0n
 
    let result = kangaroo16.solve_dlp(pk, 500n)
 
    if (!result) {
      result = kangaroo32.solve_dlp(pk, 1500n)
    }
 
    if (!result) {
      result = kangaroo48.solve_dlp(pk)
    }
 
    if (!result) throw new TypeError('Decryption failed')
 
    return result
  })
}

Now, place this at the top of your app:

const init = async () => {
  await preloadTables();
}

For the native apps, you could generate android and ios bindings here to use instead of WASM.


Now we are ready to go. Let’s define Aptos client:

const APTOS_NETWORK: Network = NetworkToNetworkName[Network.TESTNET];
const config = new AptosConfig({ network: APTOS_NETWORK });
export const aptos = new Aptos(config);

Create Decryption Key (DK)

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

Generate new:

const dk = TwistedEd25519PrivateKey.generate();

Or import existed one:

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

Also, you could derive it using your signature (for testing purposes, don’t use at production):

const user = Account.generate()
 
const signature = user.sign(TwistedEd25519PrivateKey.decryptionKeyDerivationMessage);
 
const dk = TwistedEd25519PrivateKey.fromSignature(signature);

Or use pepper from Keyless Account

Register

Next, you need to register a previously generated encryption key (EK) in contracts:

export const registerConfidentialBalance = async (
  account: Account,
  publicKeyHex: string,
  tokenAddress = "0x...",
) => {
  const txBody = await aptos.confidentialAsset.deposit({
    sender: account.accountAddress,
    to: AccountAddress.from(to),
    tokenAddress: tokenAddress,
    amount: amount,
  })
 
  const txResponse = await aptos.signAndSubmitTransaction({ signer: user, transaction: userRegisterCBTxBody });
 
  const txReceipt = await aptos.waitForTransaction({ transactionHash: txResponse.hash });
 
  return txReceipt;
}

Check if a user has already registered a specific token:

export const getIsAccountRegisteredWithToken = async (
  account: Account,
  tokenAddress = "0x...",
) => {
  const isRegistered = await aptos.confidentialAsset.hasUserRegistered({
    accountAddress: account.accountAddress,
    tokenAddress: tokenAddress,
  })
 
  return isRegistered
}

Deposit

Let’s say you already have tokens.

This will deposit them to your confidential balance

export const depositConfidentialBalance = async (
  account: Account,
  amount: bigint,
  to: string,
  tokenAddress = "0x...",
) => {
  const txBody = await aptos.confidentialAsset.deposit({
    sender: account.accountAddress,
    to: AccountAddress.from(to),
    tokenAddress: tokenAddress,
    amount: amount,
  })
  // Sign and send transaction
}

Get user’s balance

Let’s check the user’s balance after the deposit.

const userConfidentialBalance = await aptos.confidentialAsset.getBalance({ accountAddress: user.accountAddress, tokenAddress: TOKEN_ADDRESS });

This method returns you the user’s pending and actual confidential balances, and to decrypt them, you can use ConfidentialAmount class

export const getConfidentialBalances = async (
  account: Account,
  decryptionKeyHex: string,
  tokenAddress = "0x...",
) => {
  const decryptionKey = new TwistedEd25519PrivateKey(decryptionKeyHex)
 
  const { pending, actual } = await aptos.confidentialAsset.getBalance({
    accountAddress: account.accountAddress,
    tokenAddress,
  })
 
  try {
    const [confidentialAmountPending, confidentialAmountActual] =
      await Promise.all([
        ConfidentialAmount.fromEncrypted(pending, decryptionKey),
        ConfidentialAmount.fromEncrypted(actual, decryptionKey),
      ])
 
    return {
      pending: confidentialAmountPending,
      actual: confidentialAmountActual,
    }
  } catch (error) {
    return {
      pending: ConfidentialAmount.fromAmount(0n),
      actual: ConfidentialAmount.fromAmount(0n),
    }
  }
}

Rollover

After you deposited to user’s confidential balance, you can see, that he has, for instance 5n at his pending balance, and 0n at his actual balance.

User can’t operate with pending balance, so you could rollover it to actual one.

And to do so - use aptos.confidentialAsset.rolloverPendingBalance.

⚠️

Important note, that user’s actual balance need to be normalized before rollover operation.

To cover normalization & rollover simultaneously, you could use aptos.confidentialAsset.safeRolloverPendingCB.

export const safelyRolloverConfidentialBalance = async (
  account: Account,
  decryptionKeyHex: string,
  tokenAddress = "0x...",
) => {
  const rolloverTxPayloads = await aptos.confidentialAsset.safeRolloverPendingCB({
    sender: account.accountAddress,
    tokenAddress,
    decryptionKey: new TwistedEd25519PrivateKey(decryptionKeyHex),
  })
 
  // Sign and send batch txs
}

Normalization

Usually you don’t need to explicitly call normalization

In case you want to:

⚠️

Firstly, check a confidential balance is normalized, because trying to normalize an already normalized balance will return you an exception

export const getIsBalanceNormalized = async (
  account: Account,
  tokenAddress = "0x...",
) => {
  const isNormalized = await aptos.confidentialAsset.isUserBalanceNormalized({
    accountAddress: account.accountAddress,
    tokenAddress: tokenAddress,
  })
 
  return isNormalized
}

Get your balance and finally call the aptos.confidentialAsset.normalizeUserBalance method:

export const normalizeConfidentialBalance = async (
  account: Account,
  decryptionKeyHex: string,
  encryptedPendingBalance: TwistedElGamalCiphertext[],
  amount: bigint,
  tokenAddress = "0x...",
) => {
  const normalizeTx = await aptos.confidentialAsset.normalizeUserBalance({
    tokenAddress,
    decryptionKey: new TwistedEd25519PrivateKey(decryptionKeyHex),
    unnormalizedEncryptedBalance: encryptedPendingBalance,
    balanceAmount: amount,
 
    sender: account.accountAddress,
  })
 
  // Sign and send transaction
}

Withdraw

To withdraw your assets out from confidential balance:

export const withdrawConfidentialBalance = async (
  account: Account,
  receiver: string,
  decryptionKeyHex: string,
  withdrawAmount: bigint,
  encryptedActualBalance: TwistedElGamalCiphertext[],
  tokenAddress = '0x...',
) => {
  const withdrawTx = await aptos.confidentialAsset.withdraw({
    sender: account.accountAddress,
    to: receiver,
    tokenAddress,
    decryptionKey: decryptionKey,
    encryptedActualBalance,
    amountToWithdraw: withdrawAmount,
  })
 
  // Sign and send transaction
}

Transfer

For transfer you need to know the recipient’s encryption key and aptos account address

Let’s say you have a recipient’s account address, let’s get their encryption key.

export const getEkByAddr = async (addrHex: string, tokenAddress: string) => {
  return aptos.confidentialAsset.getEncryptionByAddr({
    accountAddress: AccountAddress.from(addrHex),
    tokenAddress,
  })
}

Now, wrap it all together and transfer:

export const transferConfidentialCoin = async (
  account: Account,
  decryptionKeyHex: string,
  encryptedActualBalance: TwistedElGamalCiphertext[],
  amountToTransfer: bigint,
  recipientAddressHex: string,
  auditorsEncryptionKeyHexList: string[],
  tokenAddress = "0x...",
) => {
  const decryptionKey = new TwistedEd25519PrivateKey(decryptionKeyHex)
 
  const recipientEncryptionKeyHex = await getEkByAddr(
    recipientAddressHex,
    tokenAddress,
  )
 
  const transferTx = await aptos.confidentialAsset.transferCoin({
    senderDecryptionKey: decryptionKey,
    recipientEncryptionKey: new TwistedEd25519PublicKey(
      recipientEncryptionKeyHex,
    ),
    encryptedActualBalance: encryptedActualBalance,
    amountToTransfer,
    sender: account.accountAddress,
    tokenAddress,
    recipientAddress: recipientAddressHex,
    auditorEncryptionKeys: auditorsEncryptionKeyHexList.map(
      hex => new TwistedEd25519PublicKey(hex),
    ),
  })
 
  // Sign and send transaction
}

Key Rotation

To do key rotation, you need to create a new decryption key and use aptos.confidentialAsset.rotateCBKey

⚠️

But keep in mind, that key-rotation checks that pending balance equals 0. In that case, we could do a rollover with freeze option, to move assets from the pending balance to the actual one and lock our balance.

aptos.confidentialAsset.safeRolloverPendingCB({
  ...,
  withFreezeBalance: false,
})

Now let’s create a new decryption key and rotate our encryption key:

const balances = await getBalances(user.accountAddress.toString(), myDecryptionKey, TOKEN_ADDRESS);
 
const NEW_DECRYPTION_KEY = TwistedEd25519PrivateKey.generate();
const keyRotationAndUnfreezeTxResponse = await ConfidentialCoin.safeRotateCBKey(aptos, user, {
  sender: user.accountAddress,
 
  currDecryptionKey: currentDecryptionKey,
  newDecryptionKey: NEW_DECRYPTION_KEY,
 
  currEncryptedBalance: balances.actual.amountEncrypted,
 
  withUnfreezeBalance: true, // if you want to unfreeze balance after
  tokenAddress: TOKEN_ADDRESS,
});
 
// save: new decryption key
console.log(NEW_DECRYPTION_KEY.toString());
 
// check new balances
const newBalance = await getBalances(user.accountAddress.toString(), NEW_DECRYPTION_KEY, TOKEN_ADDRESS);
 
console.log(newBalance.pending.amount);
console.log(newBalance.actual.amount);