Skip to main content
Scanning requires the cryptography WASM module. This guide covers unified inbox scanning, balance reads, and sweeping.

Setup

const client = await OpaqueClient.create({
  chainId: 11155111,
  rpcUrl,
  walletSignature,
  ethereumAddress: address,
  wasmModuleSpecifier: "/pkg/cryptography.js",
  solana: { cluster: "devnet" },
});

Unified scan

const inbox = await client.scan({
  chains: ["ethereum", "solana"],
  fromBlock: 5_000_000n,
  toBlock: undefined,           // chain tip
  solanaLimit: 1000,            // max signatures on Solana
  includeCrossChain: true,      // merge UAB (default when configured)
});

// UnifiedOwnedOutput:
// { chain, chainId, source: "native" | "uab", stealthAddress, ephemeralPublicKey, ... }

Filter custom announcement rows

If you have indexer JSON instead of adapters:
import type { IndexerAnnouncement } from "@opaquecash/opaque";

const owned = await client.filterOwnedAnnouncements(indexerRows as IndexerAnnouncement[]);

Cross-chain only

const rows = await client.fetchCrossChainAnnouncements({ fromBlock: 5_000_000n });
const crossOwned = await client.scanCrossChain();

Balances

Per-output native balances (multichain):
const balances = await client.getBalancesForOutputs(inbox);
// { chain, stealthAddress, address, nativeRaw }
Aggregated ERC-20 + native on Ethereum:
const totals = await client.getBalancesFromAnnouncements(indexerRows);
// { tokenAddress, symbol, decimals, totalRaw }[]
Per-output token balances across chains (for a token inbox). Ethereum defaults to the configured tracked ERC-20s; Solana SPL mints (base58) must be passed:
const tokenBalances = await client.getTokenBalancesForOutputs(inbox, {
  ethereum: ["0xUSDC..."],
  solana: ["EPjF...USDCmint"],
});
// { chain, stealthAddress, address, token, raw }[]  (zero balances omitted)

Sweep

const { tx } = await client.sweep({
  output: inbox[0],
  chain: inbox[0].chain,
  destination: "0xFreshAddress..." /* or Solana base58 */,
});

Sweep tokens

Pass token to sweep the full ERC-20 / SPL balance instead of the native asset. On Ethereum the stealth address must hold native gas (see gasDrop on the send, or use the gasless sweep below); on Solana set closeAccount to reclaim the token-account rent.
await client.sweep({
  output: inbox[0],
  chain: "ethereum",
  destination: "0xFreshAddress...",
  token: "0xUSDC...",
});

await client.sweep({
  output: solanaOutput,
  chain: "solana",
  destination: "FreshOwner...base58",
  token: "EPjF...USDCmint",
  closeAccount: true,
});

Gasless token sweep

When the stealth address holds a token but no native gas, build a relayer-submittable sweep. The stealth key authorizes it offline; a relayer pays the gas and takes fee in the token (spec/relayer-market.md §9).
const sweep = await client.buildGaslessTokenSweep({
  output: inbox[0],
  chain: "ethereum",
  token: "0xUSDC...",
  destination: "0xFreshAddress...",
  fee: 50_000n,                       // paid to the relayer in-token
  deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
  // forwarder defaults to the deployed StealthTokenSweep; value/tokenName auto-read
});

// Hand sweep.to + sweep.data to any relayer; it sends the tx and earns the fee.
On Solana, pass the relayer’s feePayer; the returned transaction is partially signed by the stealth key, and the relayer co-signs as fee payer:
const sweep = await client.buildGaslessTokenSweep({
  output: solanaOutput,
  chain: "solana",
  token: "EPjF...USDCmint",
  destination: "FreshOwner...base58",
  fee: 50_000n,
  deadline: 0n,                       // unused on Solana
  feePayer: "RelayerPubkey...base58",
});
// Relayer: submitSolanaGaslessSweep(connection, sweep.transactionBase64, relayerKeypair)

Manual key reconstruction

When you need custom signing logic instead of sweep:
const stealthPrivKey = client.getStealthSignerPrivateKey(inbox[0]);

// From ghost storage (ephemeral private key only):
const key = client.getStealthSignerPrivateKeyFromEphemeralPrivateKey(ephemeralPrivateKey);
Build a viem PrivateKeyAccount from the bytes and sign a transfer from output.stealthAddress.

PSR trait discovery

Fetch native announcement rows for the chain and scan V2 attestation markers:
const chain = "ethereum" as const;
const rows = await client.fetchAnnouncementRows(chain);
const traits = await client.discoverTraitsV2(rows, { chain });

for (const t of traits) {
  console.log(t.schemaName, t.schemaId, t.attestationUid, t.stealthAddress);
}
DiscoveredTrait includes the shared fields (attestationId, stealthAddress, txHash, blockNumber, discoveredAt, ephemeralPubkey) plus V2 fields (schemaId, schemaName, issuer, attestationUid, dataHex, nonce, merkleLeafPreimage, isValid, issuerAuthorized). getStealthSignerPrivateKeyForReputationTrait needs the ephemeralPubkey field returned by the scanner. Use discoverTraits(rows) or its alias getReputationTraitsFromAnnouncements(rows) only for legacy V1 0xA7 markers. New attestations created by issueAttestation use V2. Low-level witness JSON for custom prover flows:
const json = client.announcementsJsonForReputationWitness(rows);