> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opaque.cash/llms.txt
> Use this file to discover all available pages before exploring further.

# Scan and sweep

> Discover owned stealth outputs across chains and move native or token balances.

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

## Setup

```ts theme={"dark"}
const client = await OpaqueClient.create({
  chainId: 11155111,
  rpcUrl,
  walletSignature,
  ethereumAddress: address,
  wasmModuleSpecifier: "/pkg/cryptography.js",
  solana: { cluster: "devnet" },
  starknet: {},                 // enable Starknet (Sepolia) scan + balances
});
```

## Unified scan

```ts theme={"dark"}
const inbox = await client.scan({
  chains: ["ethereum", "solana", "starknet"],
  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:

```ts theme={"dark"}
import type { IndexerAnnouncement } from "@opaquecash/opaque";

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

## Cross-chain only

```ts theme={"dark"}
const rows = await client.fetchCrossChainAnnouncements({ fromBlock: 5_000_000n });
const crossOwned = await client.scanCrossChain();
```

## Balances

Per-output native balances (multichain):

```ts theme={"dark"}
const balances = await client.getBalancesForOutputs(inbox);
// { chain, stealthAddress, address, nativeRaw }
```

Aggregated ERC-20 + native on Ethereum:

```ts theme={"dark"}
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:

```ts theme={"dark"}
const tokenBalances = await client.getTokenBalancesForOutputs(inbox, {
  ethereum: ["0xUSDC..."],
  solana: ["EPjF...USDCmint"],
});
// { chain, stealthAddress, address, token, raw }[]  (zero balances omitted)
```

## Sweep

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

### Sweep on Starknet

Starknet sweeps don't go through `sweep`. Build the calls with `buildStarknetSweep` and let
the app's Starknet wallet broadcast them — the counterfactual stealth account self-funds its
`deploy_account` fee, so there's no gas drop or relayer step here.

```ts theme={"dark"}
const starknetSweep = client.buildStarknetSweep({
  output: starknetOutput,
  destination: "0xFreshStarknetAddress...",
  amount: balance,              // STRK, raw
});
// The app broadcasts starknetSweep with starknet.js (EthSigner, v10+).
```

See [Starknet](/concepts/starknet) for the full flow and caveats.

### 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.

```ts theme={"dark"}
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).

```ts theme={"dark"}
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. The `fee` is split in the same
signed transaction: the destination receives `amount - fee` and the relayer's token
account receives `fee`, so the relayer is reimbursed in-token for the gas it fronts
(`sweep.fee` / `sweep.destinationAmount` report the split):

```ts theme={"dark"}
const sweep = await client.buildGaslessTokenSweep({
  output: solanaOutput,
  chain: "solana",
  token: "EPjF...USDCmint",
  destination: "FreshOwner...base58",
  fee: 50_000n,                       // transferred to the relayer's ATA
  deadline: 0n,                       // unused on Solana
  feePayer: "RelayerPubkey...base58", // read it from the relayer's /v1/sweep/info
});
```

### Submitting to a relayer node

An `opaque-relayer` node serves the sweep directly over HTTP — no job, bid, or payload
round. Discover its per-chain fee payer / forwarder with `getSweepInfo`, then POST the
built sweep with `postGaslessSweep` (both from `@opaquecash/relayer-client`):

```ts theme={"dark"}
import {
  getSweepInfo,
  postGaslessSweep,
  gaslessSweepSubmission,
} from "@opaquecash/relayer-client";

const gateway = { baseUrl: "http://relayer.example:8787" };
const info = await getSweepInfo(gateway);   // { chains: [{ chain, operator, forwarder? }] }

const { tx } = await postGaslessSweep(gateway, gaslessSweepSubmission(sweep));
```

The node fronts the gas, is reimbursed the in-token `fee`, and returns the confirmed
transaction id. If it rejects the sweep (wrong forwarder, wrong fee payer, or an
under-priced fee), the error message says why — rebuild with the values from
`/v1/sweep/info` and retry.

## Manual key reconstruction

When you need custom signing logic instead of `sweep`:

```ts theme={"dark"}
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:

```ts theme={"dark"}
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:

```ts theme={"dark"}
const json = client.announcementsJsonForReputationWitness(rows);
```
