Skip to main content
@opaquecash/react wraps the SDK client in idiomatic React hooks. It has a single peer dependency (react >= 18) and works with any wallet stack (wagmi, RainbowKit, Solana wallet-adapter, or your own).

Install

npm install @opaquecash/react @opaquecash/opaque

Provide the client

Build the client with OpaqueClient.fromWallet when the user connects, keep it in state, and hand it to OpaqueProvider. Pass null while the session is connecting; the hooks stay idle until a client arrives.
import { useState } from "react";
import { OpaqueClient } from "@opaquecash/opaque";
import { OpaqueProvider } from "@opaquecash/react";

function App({ children }) {
  const [client, setClient] = useState<OpaqueClient | null>(null);

  async function connect() {
    setClient(
      await OpaqueClient.fromWallet({
        wallets: { chain: "ethereum", address, provider: window.ethereum },
        chainId: 11155111,
        rpcUrl,
        wasmModuleSpecifier: "/pkg/cryptography.js",
        solana: { cluster: "devnet" },
      }),
    );
  }

  return <OpaqueProvider client={client}>{children}</OpaqueProvider>;
}
Rebuild the client (and call setClient) when the connected wallets change. Pass the cached walletSignature to fromWallet so the rebuild does not prompt for a new signature; the hooks re-run automatically because the context value changed.

useOpaqueClient() / useOpaqueClientOrNull()

import { useOpaqueClient, useOpaqueClientOrNull } from "@opaquecash/react";

function Account() {
  const client = useOpaqueClient();          // throws outside a connected provider
  return <code>{client.getMetaAddressHex()}</code>;
}

function Gate({ children }) {
  const client = useOpaqueClientOrNull();    // null while disconnected
  return client ? children : <ConnectButton />;
}

useScan(options?)

Scan the unified cross-chain inbox. Scans once on mount (and whenever the client or options change); set pollInterval to keep it fresh.
import { useScan } from "@opaquecash/react";

function Inbox() {
  const { outputs, loading, error, refresh } = useScan({
    chains: ["ethereum", "solana"],   // default: both
    pollInterval: 30_000,             // optional ms; omit for scan-once
    // fromBlock, toBlock, solanaLimit, includeCrossChain, paused
  });

  if (error) return <Retry onClick={refresh} message={error.message} />;
  return (
    <ul>
      {outputs.map((o) => (
        <li key={`${o.chain}:${o.stealthAddress}`}>
          [{o.chain}] {o.stealthAddress} ({o.source})
        </li>
      ))}
      {loading && <Spinner />}
    </ul>
  );
}
OptionDefaultDescription
chains["ethereum", "solana"]Chains to scan
pollIntervalscan onceRe-scan interval in ms
pausedfalseSkip scanning while true (e.g. tab hidden)
fromBlock / toBlock / solanaLimit / includeCrossChainadapter defaultsSame as client.scan
Returns { outputs, loading, error, refresh }.

useStealthBalance(outputs)

Resolve the native balance of each owned output (typically the outputs from useScan). Refetches when the output set or client changes.
import { useScan, useStealthBalance } from "@opaquecash/react";
import { formatEther } from "viem";

function Balances() {
  const { outputs } = useScan();
  const { balances, totals, loading } = useStealthBalance(outputs);

  return (
    <div>
      <p>Ethereum total: {formatEther(totals.ethereum)} ETH</p>
      <p>Solana total: {Number(totals.solana) / 1e9} SOL</p>
      {balances.map((b) => (
        <div key={b.address}>{b.address}: {b.nativeRaw.toString()}</div>
      ))}
      {loading && <Spinner />}
    </div>
  );
}
Returns { balances, totals: { ethereum, solana }, loading, error } with amounts in base units (wei / lamports).

Writes

The hooks cover the read side. For sends, sweeps, registration, and PSR writes, call the client directly from event handlers:
const client = useOpaqueClient();

async function onSend() {
  await client.sendStealthPayment({ chain: "ethereum", recipient, amount });
  refresh();  // from useScan
}