Confidential Transfers SDK · Playground

Send a payment nobody else can read.

Solana Token-2022 confidential transfers, run by @softseco/confidential-transfers inside this page. Nothing to install and no wallet extension: the page makes throwaway devnet keys and keeps them in this browser.

Solana devnet · test tokens with no valueRPC: api.devnet.solana.comOpen source · Apache-2.0
Loading the zero-knowledge module…
  1. 1

    Get devnet SOL

    Your throwaway wallet pays fees and rent. The whole run costs about 0.03 SOL. Devnet SOL is free and worth nothing.

    The call
    await rpc.requestAirdrop(you.address, lamports(1_000_000_000n)).send();
    // faucet refused? get devnet SOL at faucet.solana.com
  2. 2

    Create a confidential test dollar

    A Token-2022 mint with the confidential-transfer extension and a designated auditor key. You hold the mint authority, so you give yourself 1,000 public test dollars.

    The SDK call
    // the mint carries ConfidentialTransferMint { auditorElgamalPubkey }
    const auditorKeypair = await deriveAuditorElgamalKeypair(auditor);
    const auditorElgamalPubkey = getAuditorElgamalPubkey(auditorKeypair);
    
    // then your token account is configured for confidential transfers
    await configureAccount({ rpc, rpcSubscriptions, payer: you, owner: you, mint });
  3. 3

    Move 100 into your confidential balance

    A deposit moves public tokens into a pending confidential balance; applying it makes them spendable. From here on the chain holds only ciphertext for your confidential balance.

    The SDK call
    await deposit({ rpc, rpcSubscriptions, payer: you, owner: you, mint,
                    amount: 100_000_000n, decimals: 6 });
    await applyPendingBalance({ rpc, rpcSubscriptions, payer: you, owner: you, mint });
  4. 4

    Send a confidential transfer

    The amount is encrypted three times — to you, to Bob and to the auditor — and zero-knowledge proofs show it is valid without revealing it. Bob needs no SOL: you pay the fees.

    The SDK call
    const { signatures } = await transfer({
      rpc, rpcSubscriptions, payer: you, owner: you, mint,
      destinationOwner: bob.address,
      amount: 25_000_000n,
      auditorElgamalPubkey,
    });
  5. 5

    See who can read it

    Three keys can open the amount. Everyone else sees the addresses and a ciphertext.

    The SDK call
    await decryptBalance({ rpc, owner: you, mint });              // you
    await decryptBalance({ rpc, owner: bob, mint });              // Bob
    await decryptTransferAmountAsAuditor({ rpc, signature, auditorKeypair }); // the auditor
  6. 6

    Withdraw back to public

    Bob moves what he received from his confidential balance to his public balance, proving he has enough without showing how much he had.

    The SDK call
    await withdraw({ rpc, rpcSubscriptions, payer: you, owner: bob, mint,
                     amount: 25_000_000n, decimals: 6 });
  7. Part 2 · Rules

    Private amounts, rules still enforced

    The same transfer, on a token with Sentinel as its transfer hook. Token-2022 calls Sentinel on every transfer, confidential ones included, and Sentinel can refuse.

  8. 7

    Put a compliance rule on a new token

    A second confidential test dollar with the same auditor, plus a transfer hook. Its policy turns the blocklist on, and a new wallet, Mallory, goes on the list. You get 100 of it in your confidential balance.

    The calls
    // the mint: TransferHook { programId: SENTINEL } + ConfidentialTransferMint { auditor }
    
    // the rules, with @softseco/sentinel from Node
    await sentinel.initializeExtraAccountMetaList(mint);
    await sentinel.initializePolicy({ mint, allowlist: false, blocklist: true, maxTransferAmount: 0 });
    await sentinel.addToBlocklist(mint, mallory.address);
    // this page sends the same three instructions, built with @solana/kit
  9. 8

    Pay Mallory, then Bob

    Sentinel cannot see the amount, but it can see who gets paid. The payment to Mallory is refused by the chain. The same payment to Bob goes through, still encrypted, and the auditor can still read it.

    The SDK call
    // the same call as in step 4; the SDK finds the hook's accounts itself
    await transfer({ rpc, rpcSubscriptions, payer: you, owner: you, mint,
                     destinationOwner: mallory.address, amount: 25_000_000n,
                     auditorElgamalPubkey });   // refused: RecipientBlocked
    
    await transfer({ rpc, rpcSubscriptions, payer: you, owner: you, mint,
                     destinationOwner: bob.address, amount: 25_000_000n,
                     auditorElgamalPubkey });   // delivered