Skip to main content
This quickstart walks you through the full client-side flow for submitting a private transaction with Arcane Compliance. By the end, you will have the SDK installed, a stealth address derived from a Stellar wallet signature, a Groth16 proof generated, and a transact call submitted to the Soroban PrivacyPoolsContract. The guide targets integrators — developers building end-user applications on top of the Arcane privacy layer.

Prerequisites

Before you begin, make sure you have the following:
  • Node.js >=19 or a browser environment with the Web Crypto API (crypto.subtle) available.
  • A Stellar wallet with a funded testnet account. You will need the account’s Stellar address and the ability to sign arbitrary messages and Soroban transactions.
  • A deployed PrivacyPoolsContract address on Stellar testnet. The contract must have been initialized with a Groth16 verification key, commitment tree depth, and public slot configuration that match your circuit assets.
  • Circuit assets — the WASM binary (privacy_pool_bg.wasm), circuit WASM (circuit.wasm), and proving key (circuit_final.zkey) that correspond to your deployed contract’s verification key.
If you do not have a deployed contract yet, follow the Integration Guide first. It covers contract deployment, Arcane registry setup, and scanner configuration.

Steps

1

Install the SDK

Install the Arcane client SDK from npm. The package ships the proof-generation WASM and TypeScript types together.
npm install @auditable/privacy-pool-zk-sdk
The SDK requires Node.js >=19.0.0 because it uses the native Web Crypto API. In a browser context, make sure your build toolchain does not polyfill crypto.subtle with a non-native implementation.
2

Initialize the SDK

Before calling any SDK method you must load the WASM binary and circuit assets. Fetch or import them as Uint8Array buffers and pass them to PrivacyPoolSDK.init.
import { PrivacyPoolSDK } from "@auditable/privacy-pool-zk-sdk";

async function initSDK(): Promise<PrivacyPoolSDK> {
  // Fetch your circuit assets — adjust paths to match your build output.
  const [wasmBinary, circuitWasm, zkey] = await Promise.all([
    fetch("/assets/privacy_pool_bg.wasm").then((r) => r.arrayBuffer()),
    fetch("/assets/circuit.wasm").then((r) => r.arrayBuffer()),
    fetch("/assets/circuit_final.zkey").then((r) => r.arrayBuffer()),
  ]);

  const sdk = await PrivacyPoolSDK.init({
    wasmBinary: new Uint8Array(wasmBinary),
    circuitWasm: new Uint8Array(circuitWasm),
    zkey: new Uint8Array(zkey),
  });

  console.log("PrivacyPoolSDK initialized");
  return sdk;
}
PrivacyPoolSDK.init is asynchronous — it compiles the WASM module before returning. Call it once at application startup and reuse the returned instance throughout the session.
3

Generate a stealth address

Stealth addresses decouple a user’s public Stellar identity from their on-chain shielded activity. To derive one, ask the user’s wallet to sign a deterministic message produced by the SDK, then pass the signature back to generate the address.
import type { StellarWallet } from "./your-wallet-adapter"; // your wallet integration

async function deriveStealthAddress(
  sdk: PrivacyPoolSDK,
  wallet: StellarWallet,
  stellarAddress: string
): Promise<string> {
  // 1. Build the canonical message the user must sign.
  const signMessage =
    PrivacyPoolSDK.buildStealthAddressSignMessage(stellarAddress);

  // 2. Ask the wallet to sign the message (exact API depends on your wallet adapter).
  const signature = await wallet.signMessage(signMessage);

  // 3. Derive the stealth address from the signature.
  const stealthAddress =
    await sdk.generateStealthAddressFromStellarSignature(signature);

  console.log("Stealth address:", stealthAddress);
  return stealthAddress;
}
The same Stellar address always produces the same stealth address from the same signature, so encourage users to keep their signing key safe. Losing the key means losing access to shielded funds at that stealth address.
4

Build a coin and generate a proof

A coin is the SDK’s representation of a private UTXO. For a deposit you build a new output coin with generateCoin, then call proveTransaction (or proveWithdrawal for withdrawals) to produce the Groth16 proof and public signals. Refer to the TypeScript types exported by the SDK for the exact argument shapes of each method.
async function buildDepositProof(sdk: PrivacyPoolSDK) {
  // Build the output coin for the deposit.
  // See the SDK TypeScript types for the full argument shape of generateCoin.
  const coin = await sdk.generateCoin(/* coin parameters */);

  // Generate the proof.
  // Use proveWithdrawal() when spending an existing coin from the pool.
  // See the SDK TypeScript types for the full argument shape of proveTransaction.
  const { proof, publicSignals } = await sdk.proveTransaction(
    /* transaction witness */
  );

  // Serialize proof and public signals for the Soroban call.
  const proofBytes = sdk.proofToHex(proof);
  const pubSignalsBytes = sdk.publicToHex(publicSignals);

  return { coin, proofBytes, pubSignalsBytes };
}
For withdrawals, build the input coin from stored coin data and pass it to proveWithdrawal() together with the Merkle witness built with buildWithdrawMerkleWitness(). See the Integration Guide for the full withdrawal flow.
5

Submit the transact call

With the serialized proof and public signals ready, prepare the encrypted audit payload and ask the wallet to sign and submit PrivacyPoolsContract.transact.
import * as StellarSdk from "@stellar/stellar-sdk";

async function submitTransact(
  wallet: StellarWallet,
  contractAddress: string,
  stellarAddress: string,
  proofBytes: string,
  pubSignalsBytes: string,
  encodedAuditPayload: string // encrypted audit bytes from your SDK/application flow
): Promise<string> {
  const server = new StellarSdk.SorobanRpc.Server(
    "https://soroban-testnet.stellar.org"
  );
  const account = await server.getAccount(stellarAddress);

  // Build the Soroban transaction.
  // transact(from, proof_bytes, pub_signals_bytes, encoded)
  const contract = new StellarSdk.Contract(contractAddress);
  const tx = new StellarSdk.TransactionBuilder(account, {
    fee: "1000000",
    networkPassphrase: StellarSdk.Networks.TESTNET,
  })
    .addOperation(
      contract.call(
        "transact",
        StellarSdk.nativeToScVal(stellarAddress, { type: "address" }),
        StellarSdk.nativeToScVal(Buffer.from(proofBytes, "hex"),      { type: "bytes" }),
        StellarSdk.nativeToScVal(Buffer.from(pubSignalsBytes, "hex"), { type: "bytes" }),
        StellarSdk.nativeToScVal(Buffer.from(encodedAuditPayload, "hex"), { type: "bytes" })
      )
    )
    .setTimeout(30)
    .build();

  // Simulate, sign, and submit.
  const prepared = await server.prepareTransaction(tx);
  const signed = await wallet.signTransaction(prepared.toXDR());
  const result = await server.sendTransaction(
    StellarSdk.TransactionBuilder.fromXDR(signed, StellarSdk.Networks.TESTNET)
  );

  console.log("Transaction submitted:", result.hash);
  return result.hash;
}
After a successful call, PrivacyPoolsContract verifies the proof on-chain, updates the commitment tree and nullifier set, moves tokens, and emits an AuditEncodedDigest event. The Arcane scanner will pick up that event in the next scan cycle and make the interpreted transaction record available through the Auditing Portal.
The contract will reject the transaction if the proof is invalid, the nullifier has already been spent, or the Merkle root is not in the contract’s root history. Double-check your witness inputs if you receive a verification failure.

Next steps

You have submitted your first private transaction with Arcane. From here you can explore the full integration surface, understand the underlying cryptography, or set up compliance access for your team.

Integration Guide

Deploy the contract, configure the Arcane registry and scanner, and verify your end-to-end integration with the full checklist.

Cryptography Reference

Dive into the Groth16 circuit design, commitment and nullifier schemes, stealth address derivation, and ECDH audit encoding.

Auditor Guide

Learn how compliance teams request scoped disclosure cases, review transaction data, and generate audit reports through the Arcane portal.