Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Confidential Counter: React Frontend

Pre-Alpha Disclaimer: This is an early pre-alpha release for exploring the SDK and starting development only. There is no real encryption — all data is completely public and stored as plaintext on-chain. Do not submit any sensitive or real data. Encryption keys and the trust model are not final; do not rely on any encryption guarantees or key material until mainnet. All interfaces, APIs, and data formats are subject to change without notice. The Solana program and all on-chain data will be wiped periodically and everything will be deleted when we transition to Encrypt Alpha 1. This software is provided “as is” without warranty of any kind; use is entirely at your own risk and dWallet Labs assumes no liability for any damages arising from its use.

1. Project Setup

The frontend uses Vite + React + Solana wallet adapter.

cd chains/solana/examples/counter/react
bun install

Dependencies in package.json:

{
  "dependencies": {
    "@solana/wallet-adapter-base": "^0.9.23",
    "@solana/wallet-adapter-react": "^0.15.35",
    "@solana/wallet-adapter-react-ui": "^0.9.35",
    "@solana/wallet-adapter-wallets": "^0.19.32",
    "@solana/web3.js": "^1.95.3",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  }
}

Entry point (main.tsx) wraps the app with Solana providers:

const RPC_URL = "https://api.devnet.solana.com";

function Root() {
  const wallets = useMemo(() => [], []);
  return (
    <ConnectionProvider endpoint={RPC_URL}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>
          <App />
        </WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
}

2. Program IDs

const ENCRYPT_PROGRAM = new PublicKey(
  "Cq37zHSH1zB6xomYK2LjP6uXJvLR3uTehxA5W9wgHGvx"
);
const COUNTER_PROGRAM = new PublicKey(
  "CntR1111111111111111111111111111111111111111"
);

Update these to match your deployed program IDs.

3. PDA Derivation

All Encrypt infrastructure PDAs derive from known seeds:

function deriveEncryptPdas(payer: PublicKey) {
  const [configPda] = findPda([Buffer.from("encrypt_config")], ENCRYPT_PROGRAM);
  const [eventAuthority] = findPda([Buffer.from("__event_authority")], ENCRYPT_PROGRAM);
  const [depositPda, depositBump] = findPda(
    [Buffer.from("encrypt_deposit"), payer.toBuffer()], ENCRYPT_PROGRAM
  );
  const networkKey = Buffer.alloc(32, 0x55);
  const [networkKeyPda] = findPda(
    [Buffer.from("network_encryption_key"), networkKey], ENCRYPT_PROGRAM
  );
  const [cpiAuthority, cpiBump] = findPda(
    [Buffer.from("__encrypt_cpi_authority")], COUNTER_PROGRAM
  );
  return { configPda, eventAuthority, depositPda, depositBump, networkKeyPda, cpiAuthority, cpiBump };
}

The cpiAuthority is derived from the counter program (not the Encrypt program). Each program that CPIs into Encrypt has its own CPI authority PDA.

4. Encrypt CPI Account List

Every Encrypt CPI needs these accounts in order:

function encryptCpiAccounts(payer: PublicKey, enc: ReturnType<typeof deriveEncryptPdas>) {
  return [
    { pubkey: ENCRYPT_PROGRAM, isSigner: false, isWritable: false },
    { pubkey: enc.configPda, isSigner: false, isWritable: true },
    { pubkey: enc.depositPda, isSigner: false, isWritable: true },
    { pubkey: enc.cpiAuthority, isSigner: false, isWritable: false },
    { pubkey: COUNTER_PROGRAM, isSigner: false, isWritable: false },
    { pubkey: enc.networkKeyPda, isSigner: false, isWritable: false },
    { pubkey: payer, isSigner: true, isWritable: true },
    { pubkey: enc.eventAuthority, isSigner: false, isWritable: false },
    { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
  ];
}

5. Polling Pattern

After a CPI, the executor processes the FHE computation off-chain. The frontend polls until the ciphertext account is verified:

async function pollUntil(
  connection: any, account: PublicKey,
  check: (data: Buffer) => boolean,
  timeoutMs = 120_000, intervalMs = 1_000
): Promise<Buffer> {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    try {
      const info = await connection.getAccountInfo(account);
      if (info && check(info.data as Buffer)) return info.data as Buffer;
    } catch {}
    await new Promise((r) => setTimeout(r, intervalMs));
  }
  throw new Error("Timeout waiting for executor");
}

// Ciphertext is verified when status byte (offset 99) == 1
const isVerified = (d: Buffer) => d.length >= 100 && d[99] === 1;

// Decryption is complete when written_bytes == total_bytes and total > 0
const isDecrypted = (d: Buffer) => {
  if (d.length < 107) return false;
  const total = d.readUInt32LE(99);
  const written = d.readUInt32LE(103);
  return written === total && total > 0;
};

6. Create Counter Flow

const handleInitialize = useCallback(async () => {
  await ensureDeposit(); // create deposit account if needed
  const enc = getEnc();
  const id = Buffer.from(Keypair.generate().publicKey.toBytes());
  const [pda, bump] = findPda([Buffer.from("counter"), id], COUNTER_PROGRAM);
  const valueKeypair = Keypair.generate();

  await sendTx(
    [new TransactionInstruction({
      programId: COUNTER_PROGRAM,
      data: Buffer.concat([Buffer.from([0, bump, enc.cpiBump]), id]),
      keys: [
        { pubkey: pda, isSigner: false, isWritable: true },
        { pubkey: wallet.publicKey, isSigner: true, isWritable: false },
        { pubkey: valueKeypair.publicKey, isSigner: true, isWritable: true },
        ...encryptCpiAccounts(wallet.publicKey, enc),
      ],
    })],
    [valueKeypair]
  );

  setCounterPda(pda);
  setValueCt(valueKeypair.publicKey);
}, [/* deps */]);

The valueKeypair is a fresh keypair whose public key becomes the ciphertext account address. The Encrypt program creates this account during the CPI. The keypair must sign the transaction.

7. Increment / Decrement Flow

const handleOp = useCallback(async (opcode: 1 | 2, label: string) => {
  const enc = getEnc();
  await sendTx([new TransactionInstruction({
    programId: COUNTER_PROGRAM,
    data: Buffer.from([opcode, enc.cpiBump]),
    keys: [
      { pubkey: counterPda, isSigner: false, isWritable: true },
      { pubkey: valueCt, isSigner: false, isWritable: true },
      ...encryptCpiAccounts(wallet.publicKey, enc),
    ],
  })]);

  // Wait for executor to process the FHE computation
  await pollUntil(connection, valueCt, isVerified, 60_000);
}, [/* deps */]);

After sending the transaction, poll the ciphertext account until isVerified returns true. The executor typically processes within a few seconds on devnet.

8. Decrypt + Reveal Flow

Decryption is a two-step process:

const handleDecrypt = useCallback(async () => {
  const enc = getEnc();
  const reqKeypair = Keypair.generate();

  // Step 1: Request decryption
  await sendTx(
    [new TransactionInstruction({
      programId: COUNTER_PROGRAM,
      data: Buffer.from([3, enc.cpiBump]),
      keys: [
        { pubkey: counterPda, isSigner: false, isWritable: true },
        { pubkey: reqKeypair.publicKey, isSigner: true, isWritable: true },
        { pubkey: valueCt, isSigner: false, isWritable: false },
        ...encryptCpiAccounts(wallet.publicKey, enc),
      ],
    })],
    [reqKeypair]
  );

  // Step 2: Wait for decryptor
  await pollUntil(connection, reqKeypair.publicKey, isDecrypted);

  // Step 3: Reveal (copy plaintext into counter state)
  await sendTx([new TransactionInstruction({
    programId: COUNTER_PROGRAM,
    data: Buffer.from([4]),
    keys: [
      { pubkey: counterPda, isSigner: false, isWritable: true },
      { pubkey: reqKeypair.publicKey, isSigner: false, isWritable: false },
      { pubkey: wallet.publicKey, isSigner: true, isWritable: false },
    ],
  })]);

  // Read the revealed value from counter PDA
  const data = (await connection.getAccountInfo(counterPda))!.data as Buffer;
  const revealed = data.readBigUInt64LE(129);
  setDisplayValue(revealed.toString());
}, [/* deps */]);

The reqKeypair is a fresh keypair for the decryption request account. After the decryptor writes the result, reveal_value (opcode 4) copies the verified plaintext into counter.revealed_value.

9. Running on Devnet

The app connects to Solana devnet and the pre-alpha executor automatically. No local validator or executor setup is needed.

cd chains/solana/examples/counter/react
bun install
bun dev

Open http://localhost:5173, connect a wallet (e.g. Phantom set to devnet), airdrop SOL, and create a counter.