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

# Stealth API

> OpaqueClient methods for meta-addresses, recipient resolution, stealth sends, scanning, sweeping, and anonymity utilities.

## Registry and identity

### `getMetaAddressHex()`

Returns the 98-byte stealth meta-address (V||S||S\_ed) derived from the wallet signature.

```ts theme={"dark"}
const meta = client.getMetaAddressHex();
```

***

### `resolveRecipient(input)`

Resolve ANY supported recipient identity to a validated meta-address. This is
the one entry point for "who am I paying": it accepts the same inputs you can pass to
`sendStealthPayment({ recipient })`.

```ts theme={"dark"}
const r = await client.resolveRecipient("alice.opq.eth");
// { metaAddressHex, source, input }
```

| Input                                                        | Resolution path                                                      | `source`                          |
| ------------------------------------------------------------ | -------------------------------------------------------------------- | --------------------------------- |
| meta-address, 66- or 98-byte (optionally `st:opq:`-prefixed) | Validated and passed through                                         | `"meta-address"`                  |
| `0x...` 20-byte EVM address                                  | ERC-6538 `StealthMetaAddressRegistry`                                | `"evm-registry"`                  |
| Solana base58 pubkey                                         | `stealth-registry` PDA (needs `solana` config)                       | `"solana-registry"`               |
| `ipfs://CID` or bare CID                                     | DID document fetched via gateways (configure `ipfs`)                 | `"ipfs-did"`                      |
| ONS name (`alice.opq.eth`; testnet parent `opqtest.eth`)     | Solana mirror PDA first, canonical `OpaqueNameRegistry` fallback     | `"ons-mirror"` / `"ons-registry"` |
| other `*.eth` name                                           | ENS `com.opaque.meta` text record (needs `ens` config)               | `"ens-text"`                      |
| `*.sol` name                                                 | SNS Records V2 TXT record (needs `solana` config or `sns.getRecord`) | `"sns-record"`                    |

Every path validates that both 33-byte halves of the result are valid compressed
secp256k1 points before returning. Unregistered, unset, or malformed identities throw
with a path-specific message.

**ONS names ([ONS.md](https://github.com/opaquecash/spec/blob/main/ONS.md)).** Subnames
of the Opaque Name Service parent resolve mirror-first: with `solana` configured, the
SDK derives the mirror PDA from `keccak256(name)` and reads **one Solana account, no
Ethereum RPC**. Without `solana`
(or when the mirror has no record yet), it falls back to the canonical
`OpaqueNameRegistry` wildcard resolver over your EVM RPC. The mirror lags the canonical
record by Wormhole latency (eventually consistent, canonical-chain-wins). Defaults
(parent name, registry, mirror program) ship in `@opaquecash/deployments`; override
with `ons: { parentName, registry, mirrorProgram }`.

```ts theme={"dark"}
// The named entry point if you only want the meta-address:
const meta = await client.resolveOpaqueMetaAddress("alice.opqtest.eth");
```

**`.sol` names.** With `solana` configured, the SDK reads the domain's Records V2 TXT
record via the bundled `@bonfida/spl-name-service` reader and validates it as a CSAP
2.9 value (`st:opq:`-prefixed or raw 132-hex). Inject `sns: { getRecord }` to mock or
to read from a custom source.

***

### ONS name management

Register, claim, and track ONS names for this wallet's meta-address (see the
[naming guide](/guides/ons-naming) for the consistency model):

```ts theme={"dark"}
// Ethereum (authoritative immediately; mirror follows after Wormhole relay):
const txHash = await client.registerOpaqueName("alice");

// Solana-only claim (PROVISIONAL until the canonical registry confirms):
const { signature, name } = await client.claimOpaqueName("bob");

// Track a claim through pending / confirmed / lost / expired:
const { state } = await client.getOpaqueNameStatus("bob.opqtest.eth");

// Close a finished claim and reclaim the provisional rent:
await client.reconcileOpaqueName("bob.opqtest.eth");
```

| Method                      | Chain / signer  | Notes                                                    |
| --------------------------- | --------------- | -------------------------------------------------------- |
| `registerOpaqueName(label)` | Ethereum signer | First-come-first-served on the canonical registry        |
| `claimOpaqueName(label)`    | `solanaWallet`  | Provisional; loses to a concurrent Ethereum registration |
| `getOpaqueNameStatus(name)` | Solana reads    | `none` / `pending` / `confirmed` / `lost` / `expired`    |
| `reconcileOpaqueName(name)` | `solanaWallet`  | Permissionless close; rent refunds to the claimer        |

**ENS setup.** The `*.eth` path reads the `com.opaque.meta` text record (CSAP 2.9).
Pass an ENS-capable viem `PublicClient` (mainnet or Sepolia; your scan RPC usually is
not one), or inject a custom reader:

```ts theme={"dark"}
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";

const client = await OpaqueClient.create({
  // ...
  ens: { client: createPublicClient({ chain: mainnet, transport: http(ensRpcUrl) }) },
});

// Or a custom reader (tests, alternative resolvers):
ens: { getText: async (name, key) => myResolver.text(name, key) }
```

**IPFS setup.** The `ipfs://` path fetches a DID document from public gateways
(`ipfs.io`, `cloudflare-ipfs.com` by default; tried in order) and extracts the
meta-address from either a `service` entry of type `OpaqueStealthMetaAddress` or a
top-level `com.opaque.meta` / `opaqueMetaAddress` field:

```ts theme={"dark"}
ipfs: {
  gateways: ["https://my-gateway.example"],   // optional override
  fetch: myFetch,                              // optional (e.g. a local node / Helia loader)
}
```

***

### `resolveRecipientMetaAddress(recipientAddress)`

Lower-level Ethereum-only registry lookup. Unlike `resolveRecipient`, it does not throw
for unregistered recipients, so it suits "is this person on Opaque yet?" UI checks.

```ts theme={"dark"}
const res = await client.resolveRecipientMetaAddress("0xRecipient...");
// { recipientAddress, registered, metaAddressHex? }
```

***

### `buildRegisterMetaAddressTransaction()`

Encode `registerKeys` calldata without submitting.

```ts theme={"dark"}
const req = client.buildRegisterMetaAddressTransaction();
// { to, data, chainId, metaAddressHex }
await walletClient.sendTransaction({ to: req.to, data: req.data });
```

***

### `registerMetaAddress(chain)`

Submit meta-address registration on `"ethereum"` or `"solana"`.

```ts theme={"dark"}
const { txHash, metaAddressHex, chain } = await client.registerMetaAddress("ethereum");
```

**Signers:** `ethereumWalletClient` / `ethereumProvider` (EVM) or `solanaWallet` (Solana).

***

### `isMetaAddressRegistered(chain)`

Check if this wallet's meta-address is registered.

```ts theme={"dark"}
const registered = await client.isMetaAddressRegistered("solana");
```

***

## Send

### `prepareStealthSend(recipientMetaAddressHex)`

Derive one-time stealth material for sending. Pure crypto, no chain call.

```ts theme={"dark"}
const send = client.prepareStealthSend(metaAddressHex);
/*
  schemeId, stealthAddress, viewTag,
  ephemeralPublicKey, ephemeralPrivateKey,
  metadata, stealthPubKey
*/
```

***

### `sendStealthPayment(params)`

High-level native send + announce (optional relay, optional delayed announce).

```ts theme={"dark"}
import { parseEther } from "viem";

const result = await client.sendStealthPayment({
  chain: "ethereum",
  recipient: "0x...",        // anything resolveRecipient accepts: EOA, meta-address,
                             // Solana pubkey, ipfs:// DID, or *.eth name
  amount: parseEther("0.01"),
  announce: true,            // default
  relay: false,              // set true for cross-chain UAB
  delayAnnouncement: 60_000, // optional ms; see anonymity utilities below
  token: undefined,          // ERC-20 address / SPL mint; omit for the native asset
  gasDrop: undefined,        // optional native top-up to the stealth address with a token send
  batchId: undefined,        // Solana Wormhole nonce when relay: true
});
```

**Returns:** `SendStealthPaymentResult`:
`{ chain, txHash, announceTxHash?, announcePromise?, stealthAddress, destination?, ephemeralPublicKey, metaAddressHex }`

Solana bundles transfer + announce in one transaction (unless `delayAnnouncement` is
set, which forces two). Ethereum submits the transfer first, then the announce.

<Note>
  `sendStealthPayment` accepts only `"ethereum"` and `"solana"` and throws for
  `"starknet"` (as does `registerMetaAddress`). On Starknet, send via
  `buildStarknetStealthSend` below; meta-address registration goes through the
  `@opaquecash/stealth-chain-starknet` adapter (SNIP-12 on-behalf) — see
  [Starknet](/concepts/starknet).
</Note>

***

### `buildStarknetStealthSend(params)`

Starknet send + announce (Starknet Sepolia, testnet-preview). Builds the unsigned
transfer and announce calls; the app's connected Starknet wallet broadcasts them (the
one meta-address and wallet already span all three chains).

```ts theme={"dark"}
const starknetSend = await client.buildStarknetStealthSend({
  recipient,   // anything resolveRecipient accepts
  amount,      // STRK amount
});
// Unsigned transfer + announce calls for the connected Starknet account to broadcast.
```

Requires the `starknet: {}` client option and `@opaquecash/stealth-chain-starknet`. See
[Starknet](/concepts/starknet) for the full flow and caveats.

***

### `prepareGhostReceive()`

Derive stealth receive material for your own meta-address (no announce yet).

```ts theme={"dark"}
const ghost = client.prepareGhostReceive();
// Store ghost.ephemeralPrivateKey securely
```

***

### `buildAnnounceTransactionRequest(send)`

Build `StealthAddressAnnouncer.announce` calldata.

```ts theme={"dark"}
const req = client.buildAnnounceTransactionRequest(send);
// { to, data, chainId, summary }
```

***

### `buildAnnounceTransactionRequestForGhost(ephemeralPrivateKey)`

Rebuild announce calldata from a stored ephemeral private key only.

```ts theme={"dark"}
const req = client.buildAnnounceTransactionRequestForGhost(storedEphemeralPriv);
```

***

## Anonymity utilities

Two tools for growing the anonymity set around your payments (guide section 17).

### `delayAnnouncement` (send option)

Decouple send time from announce time: the value transfer is submitted immediately,
the announcement only after the delay, breaking the timing correlation between the two
on-chain events.

```ts theme={"dark"}
const result = await client.sendStealthPayment({
  chain: "ethereum",
  recipient,
  amount: parseEther("0.01"),
  delayAnnouncement: 5 * 60_000,  // announce 5 minutes after the transfer
});

console.log("transfer:", result.txHash);
// result.announceTxHash is undefined; the pending announce is a promise:
const announceTx = await result.announcePromise;
```

<Warning>
  Await (or attach a handler to) `announcePromise`, and keep the process alive until it
  resolves. If your process exits first, the announcement is never submitted and the
  recipient cannot discover the payment by scanning.
</Warning>

***

### `generateDummyAnnouncements(n)`

Mint `n` decoy announcements. Each is a fully valid DKSAP announcement to a freshly
generated THROWAWAY meta-address whose private keys are discarded: on-chain it is
indistinguishable from a real payment announcement (valid curve points, correctly
derived view tag), but nobody will ever match or spend it.

```ts theme={"dark"}
const dummies = client.generateDummyAnnouncements(5);
// DummyAnnouncement[]: PrepareStealthSendResult + the throwaway metaAddressHex
```

***

### `buildDummyAnnouncementTransactions(n)`

Convenience over `generateDummyAnnouncements`: ready-to-submit `announce` calldata.
Broadcast from any account, interleaved with real sends; announcements carry no value
and anyone may announce.

```ts theme={"dark"}
for (const tx of client.buildDummyAnnouncementTransactions(3)) {
  await walletClient.sendTransaction({ to: tx.to, data: tx.data });
}
```

***

## Scan and filter

### `fetchAnnouncementRows(chain, opts?)`

Fetch ALL native announcements on a chain as indexer-shaped rows, unfiltered, with full
on-chain `metadata`. Raw input for the metadata-aware scanners (`filterOwnedAnnouncements`,
`discoverTraitsV2`, and legacy `discoverTraits`); unlike `scan`, nothing is dropped.

```ts theme={"dark"}
const rows = await client.fetchAnnouncementRows("ethereum", {
  fromBlock: 5_000_000n, // EVM lower bound; omit to scan from the adapter default
  toBlock: undefined,    // EVM upper bound; omit for chain tip
  solanaLimit: 1000,     // max Solana signatures (when chain is "solana")
});
// IndexerAnnouncement[]
```

Cross-chain (UAB) announcements are not included: the 96-byte Wormhole payload only carries a
24-byte metadata tail, so relayed rows cannot hold attestation metadata. Fetch rows natively on
each chain instead (UAB outputs still surface in `scan` / `fetchCrossChainAnnouncements`).

***

### `filterOwnedAnnouncements(rows)`

WASM scan: filter indexer rows to outputs owned by this wallet. Malformed announcements
(for example an ephemeral key that is not a valid curve point) are skipped, never fatal;
anyone can announce.

```ts theme={"dark"}
const owned = await client.filterOwnedAnnouncements(indexerRows);
// OwnedStealthOutput[]
```

***

### `scan(opts)`

Unified multichain inbox across all three supported chains.

```ts theme={"dark"}
const inbox = await client.scan({
  chains: ["ethereum", "solana", "starknet"],
  fromBlock: 5_000_000n,
  toBlock: undefined,
  solanaLimit: 1000,
  includeCrossChain: true,
});
// UnifiedOwnedOutput[], tagged with chain, chainId, source
```

<Note>
  Include `"starknet"` (Starknet Sepolia, testnet-preview) only when the client was
  created with the `starknet: {}` option. Starknet participates as a native scan chain,
  not a Wormhole/UAB relay endpoint, so `includeCrossChain` adds no Starknet rows. See
  [Starknet](/concepts/starknet).
</Note>

***

## Balances

### `getBalancesFromAnnouncements(rows)`

Filter owned outputs and sum balances per tracked token (EVM RPC).

```ts theme={"dark"}
const totals = await client.getBalancesFromAnnouncements(rows);
// TokenBalanceSummary[]
```

***

### `getBalancesForOutputs(outputs)`

Native balance per `UnifiedOwnedOutput` (multichain).

```ts theme={"dark"}
const balances = await client.getBalancesForOutputs(inbox);
// OutputBalance[]
```

***

## Sweep and keys

### `sweep(params)`

Sweep the full native balance from a stealth output, or the full balance of `token`
(ERC-20 address / SPL mint) when set. `closeAccount` (Solana) reclaims the token-account rent.

```ts theme={"dark"}
const { chain, tx } = await client.sweep({
  output: inbox[0],
  chain: "ethereum",
  destination: "0xFresh...",
  token: "0xUSDC...",   // omit for native; ERC-20 sweep needs native gas at the stealth address
});
```

For a stealth address holding a token but no gas, use `buildGaslessTokenSweep` and submit
the result to a relayer node with `postGaslessSweep` from `@opaquecash/relayer-client`; the
relayer fronts the gas and is reimbursed `fee` in-token on both Ethereum and Solana
(see [Gasless token sweep](/guides/scan-and-sweep#gasless-token-sweep)). The gasless
forwarder is EVM/Solana only; Starknet sweeps self-fund instead (below).

***

### `buildStarknetSweep(params)`

Sweep a Starknet stealth output (Starknet Sepolia, testnet-preview). Returns the sweep
call for the app to broadcast via `starknet.js` (`EthSigner`, v10+). There is no
fee-in-token gasless forwarder on Starknet: the counterfactual stealth account
self-funds its `deploy_account` fee.

```ts theme={"dark"}
const starknetSweep = client.buildStarknetSweep({
  output,        // a Starknet UnifiedOwnedOutput
  destination,   // fresh Starknet address
  amount,
});
// Broadcast with the connected Starknet account (starknet.js EthSigner).
```

See [Starknet](/concepts/starknet).

***

### `getStealthSignerPrivateKey(output)`

Reconstruct the 32-byte secp256k1 private key for an owned output.

```ts theme={"dark"}
const priv = client.getStealthSignerPrivateKey({ ephemeralPublicKey: output.ephemeralPublicKey });
```

***

### `getStealthSignerPrivateKeyFromEphemeralPrivateKey(ephemeralPrivateKey)`

Same reconstruction from a ghost-stored ephemeral secret.

```ts theme={"dark"}
const priv = client.getStealthSignerPrivateKeyFromEphemeralPrivateKey(ghost.ephemeralPrivateKey);
```
