Skip to main content
This guide walks you through connecting a Stellar/Soroban application to Arcane Compliance. By the end, your application will generate zero-knowledge proofs using PrivacyPoolSDK, submit transact calls to the deployed PrivacyPoolsContract, and have every transaction automatically indexed by the Arcane scanner so auditors can open disclosure cases in the Audit Portal.

Prerequisites

Before you begin, make sure you have the following in place:
  • A Stellar/Soroban network and RPC endpoint — Testnet or Mainnet, with a reachable Soroban-compatible RPC URL.
  • A deployed PrivacyPoolsContract — Either deploy your own or obtain the address of an existing deployment.
  • The contract’s verification key and decoding key — The verification key is set at deploy time; the decoding key is used by Arcane to interpret encrypted audit payloads.
  • @auditable/privacy-pool-zk-sdk installed in your application — Install with npm install @auditable/privacy-pool-zk-sdk.
  • A Stellar wallet integration — Your application needs to request message signing and transaction submission from a connected wallet.
  • An Arcane organization and application — Contact Arcane support to provision your organization, application record, and scanner configuration.

Integration flow

The following diagram shows how the pieces fit together from your application all the way to the Audit Portal: Your application calls the SDK to build proofs and the wallet to sign transactions. The contract verifies the proof, updates on-chain state, and emits an AuditEncodedDigest event. The Arcane scanner picks up that event via the Stellar RPC and makes the transaction available to auditors in the portal.

Contract setup

Deploy PrivacyPoolsContract with the following constructor arguments:
Constructor argumentPurpose
tree_depthDepth of the on-chain Merkle commitment tree
vk_bytesGroth16 verification key bytes matching your compiled circuit
public_n_inputsNumber of public withdrawal (input) slots in the circuit
public_n_outputsNumber of public deposit (output) slots in the circuit
adminStellar address granted admin privileges on the contract
The public_n_inputs and public_n_outputs values must match the circuit you compiled and the verification key you provide. Mismatches will cause proof verification to fail.

Application integration

1

Install and initialize the SDK

Install the SDK package:
npm install @auditable/privacy-pool-zk-sdk
Then initialize an SDK instance by loading the WASM binary, circuit WASM, and proving key. In a browser environment, fetch these files from your asset server:
import { PrivacyPoolSDK } from "@auditable/privacy-pool-zk-sdk";

const sdk = await PrivacyPoolSDK.init({
  wasmBinary: await fetch("/pkg/client_sdk_wasm_bg.wasm").then((r) => r.arrayBuffer()),
  circuitWasm: await fetch("/assets/main.wasm").then((r) => r.arrayBuffer()),
  zkey: await fetch("/assets/main_final.zkey").then((r) => r.arrayBuffer()),
});
In a Node.js environment (>=19.0.0), you can read the same files from disk using fs.readFile and pass the resulting Buffer objects.
2

Derive a user stealth address

Each user who wants to receive private funds needs a stealth address. The SDK derives one deterministically from a Stellar wallet signature — no extra key material is stored.
import { PrivacyPoolSDK } from "@auditable/privacy-pool-zk-sdk";

// Build the message the user must sign with their Stellar wallet
const message = PrivacyPoolSDK.buildStealthAddressSignMessage(stellarAddress);

// Request the signature from the connected wallet
const signature = await wallet.signMessage(message);

// Derive the stpl1 stealth address from the signature
const stealthAddress = await sdk.generateStealthAddressFromStellarSignature(signature);
// stealthAddress is a Bech32 string beginning with "stpl1"
The resulting stpl1 address is what recipients share with depositors.
3

Build a coin and read pool state

To spend or transfer a coin, you first need to construct it and build a Merkle witness against the current pool state. Read the commitment tree data from the contract, then use the SDK to build the witness:
// Generate a new private coin (for deposits)
const coin = await sdk.generateCoin();

// Or generate a coin tied to a recipient's stealth address via ECDH
const coinWithSecret = await sdk.generateCoinWithSharedSecret(recipientStealthAddress);

// Read the current pool state from the contract (commitments, roots, etc.),
// then build the Merkle witness for the coin you want to spend
const witness = await sdk.buildWithdrawMerkleWitness({
  coin: coinToSpend,
  commitments: poolCommitments,   // fetched from contract via get_commitments()
  treeDepth: poolTreeDepth,        // fetched from contract via get_merkle_depth()
});
4

Generate proof bytes and public signal bytes

With your coin and witness in hand, generate the Groth16 proof. Use proveWithdrawal for a withdrawal transaction or proveTransaction for the general-purpose proof path:
// Withdrawal proof
const proofResult = await sdk.proveWithdrawal({
  coin: coinToSpend,
  witness,
  publicWithdrawAddress: destinationStellarAddress,
  outputCoins: [], // private output coins if performing a transfer
});

// General transaction proof
const txProofResult = await sdk.proveTransaction({
  inputCoins: [coinToSpend],
  outputCoins: [newCoin],
  witness,
  publicDepositAmount: 0n,
  publicWithdrawAmount: withdrawAmount,
  publicWithdrawAddress: destinationStellarAddress,
});

// Serialize for the Soroban call
const proofBytes = sdk.proofToHex(proofResult.proof);
const pubSignalBytes = sdk.publicToHex(proofResult.publicSignals);
5

Prepare encrypted audit bytes

Every transact call must include an encrypted audit payload (encoded) that Arcane’s scanner will index and decrypt using the contract’s decoding key. Construct this payload according to your Arcane application configuration before submitting the transaction.
// The encoded bytes bundle the encrypted audit payload for the Arcane scanner.
// Construct this using the audit encoding utilities provided by your Arcane
// application setup — the exact format is defined by your decoding key configuration.
const encoded: Uint8Array = buildAuditEncodedPayload({
  proofResult,
  assetId,
  arcaneEncodingKey,
});
The encoded argument is emitted verbatim by the contract as the AuditEncodedDigest event. Arcane’s scanner picks it up and decrypts it server-side using the decoding_key registered for your contract.
6

Submit the transact call

Pass the serialized proof, public signals, and encoded audit payload to the contract through your Stellar wallet integration:
import * as StellarSdk from "@stellar/stellar-sdk";

// Build the Soroban contract call
const operation = contract.call(
  "transact",
  StellarSdk.nativeToScVal(stellarAddress, { type: "address" }), // from
  StellarSdk.nativeToScVal(Buffer.from(proofBytes, "hex")),       // proof_bytes
  StellarSdk.nativeToScVal(Buffer.from(pubSignalBytes, "hex")),   // pub_signals_bytes
  StellarSdk.nativeToScVal(Buffer.from(encoded)),                 // encoded
);

// Build, sign, and submit the transaction
const tx = new StellarSdk.TransactionBuilder(sourceAccount, { fee, networkPassphrase })
  .addOperation(operation)
  .setTimeout(30)
  .build();

const signedTx = await wallet.signTransaction(tx.toXDR());
const result = await server.sendTransaction(
  StellarSdk.TransactionBuilder.fromXDR(signedTx, networkPassphrase)
);
After a successful transaction, the contract emits AuditEncodedDigest, and the Arcane scanner will index it shortly after the ledger closes.

Soroban call shape

The transact entrypoint accepts four arguments:
ArgumentSource
fromWallet-authenticated Stellar address — the transaction signer
proof_bytesGroth16 proof serialized by sdk.proofToHex()
pub_signals_bytesPublic circuit signals serialized by sdk.publicToHex()
encodedEncrypted audit payload bytes for Arcane indexing
On success, the contract emits:
AuditEncodedDigest(message_name = "transact", digest = encoded)
The Arcane scanner maps this event to an audit row, which the interpretation worker decrypts and normalizes into a deposit, withdrawal, or transfer record.

Verification checklist

Use this checklist to confirm your integration is working end-to-end:
  • Contract is deployed with the correct tree_depth, vk_bytes, and public slot counts
  • PrivacyPoolSDK initializes without errors in your application
  • A stealth address (stpl1…) is successfully derived from a wallet signature
  • proveWithdrawal or proveTransaction produces a proof without errors
  • Wallet can sign and submit the transact call to the network
  • Contract emits an AuditEncodedDigest event after a successful transaction
  • Arcane scanner advances and writes audit rows for your contract
  • Audit rows are interpreted (deposit / withdrawal / transfer records appear)
  • An auditor can create a case request in the Audit Portal
  • An administrator can approve or close the request
  • Approved auditor can open the case and view scoped transaction fields

Troubleshooting

SymptomCheck
No audit rows appear in the portalConfirm the contract address is registered to the correct chain_id, the scanner chain name is configured, and the scanner checkpoint range covers the event ledger
Scanner runs but the event is ignoredVerify the event comes from a successful contract invocation and that the event topic maps to a supported audit type
Duplicate-looking scan resultsAudit row upserts are idempotent by (contract_id, soroban_event_id) — duplicates are safely overwritten
Public signals are missing or wrongCheck that the scanner’s calldata parser can read the transact invocation metadata
Proof verification fails on-chainConfirm vk_bytes, public_n_inputs, and public_n_outputs match the compiled circuit
Interpretation failsCheck contracts.decoding_key, that audit.cyphertext is populated, and that audit.public_signals_json is valid
Auditor cannot open a caseCheck case assignment, approval status, application scope, and the access window
UI shows workspace but not the application routeConfirm the required permission key is present in the application permission bucket