Skip to main content

Registry and identity

getMetaAddressHex()

Returns the 66-byte stealth meta-address derived from the wallet signature.
const meta = client.getMetaAddressHex();

resolveRecipient(input)

Resolve ANY supported recipient identity to a validated 66-byte meta-address. This is the one entry point for “who am I paying”: it accepts the same inputs you can pass to sendStealthPayment({ recipient }).
const r = await client.resolveRecipient("alice.opq.eth");
// { metaAddressHex, source, input }
InputResolution pathsource
66-byte meta-address (optionally st:opq:-prefixed)Validated and passed through"meta-address"
0x... 20-byte EVM addressERC-6538 StealthMetaAddressRegistry"evm-registry"
Solana base58 pubkeystealth-registry PDA (needs solana config)"solana-registry"
ipfs://CID or bare CIDDID 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 nameENS com.opaque.meta text record (needs ens config)"ens-text"
*.sol nameSNS 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). 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 }.
// 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 for the consistency model):
// 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");
MethodChain / signerNotes
registerOpaqueName(label)Ethereum signerFirst-come-first-served on the canonical registry
claimOpaqueName(label)solanaWalletProvisional; loses to a concurrent Ethereum registration
getOpaqueNameStatus(name)Solana readsnone / pending / confirmed / lost / expired
reconcileOpaqueName(name)solanaWalletPermissionless 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:
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:
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.
const res = await client.resolveRecipientMetaAddress("0xRecipient...");
// { recipientAddress, registered, metaAddressHex? }

buildRegisterMetaAddressTransaction()

Encode registerKeys calldata without submitting.
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".
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.
const registered = await client.isMetaAddressRegistered("solana");

Send

prepareStealthSend(recipientMetaAddressHex)

Derive one-time stealth material for sending. Pure crypto, no chain call.
const send = client.prepareStealthSend(metaAddressHex);
/*
  schemeId, stealthAddress, viewTag,
  ephemeralPublicKey, ephemeralPrivateKey,
  metadata, stealthPubKey
*/

sendStealthPayment(params)

High-level native send + announce (optional relay, optional delayed announce).
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.

prepareGhostReceive()

Derive stealth receive material for your own meta-address (no announce yet).
const ghost = client.prepareGhostReceive();
// Store ghost.ephemeralPrivateKey securely

buildAnnounceTransactionRequest(send)

Build StealthAddressAnnouncer.announce calldata.
const req = client.buildAnnounceTransactionRequest(send);
// { to, data, chainId, summary }

buildAnnounceTransactionRequestForGhost(ephemeralPrivateKey)

Rebuild announce calldata from a stored ephemeral private key only.
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.
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;
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.

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.
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.
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.
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.
const owned = await client.filterOwnedAnnouncements(indexerRows);
// OwnedStealthOutput[]

scan(opts)

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

Balances

getBalancesFromAnnouncements(rows)

Filter owned outputs and sum balances per tracked token (EVM RPC).
const totals = await client.getBalancesFromAnnouncements(rows);
// TokenBalanceSummary[]

getBalancesForOutputs(outputs)

Native balance per UnifiedOwnedOutput (multichain).
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.
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 a relayer (see Gasless token sweep).

getStealthSignerPrivateKey(output)

Reconstruct the 32-byte secp256k1 private key for an owned output.
const priv = client.getStealthSignerPrivateKey({ ephemeralPublicKey: output.ephemeralPublicKey });

getStealthSignerPrivateKeyFromEphemeralPrivateKey(ephemeralPrivateKey)

Same reconstruction from a ghost-stored ephemeral secret.
const priv = client.getStealthSignerPrivateKeyFromEphemeralPrivateKey(ghost.ephemeralPrivateKey);