Skip to Content
ReferenceReact Hooks

React Hooks

Obscura’s UI never constructs a raw contract call. Every read, write, and decryption goes through a typed hook in apps/web/hooks. They are deliberately written as plain wagmi + viem + react-query so you can lift them into your own project with minimal surgery.

Dependencies: wagmi v3, viem v2, @tanstack/react-query v5, @zama-fhe/relayer-sdk, plus small lib modules the hooks import: lib/fhevm.ts (the SDK singleton), lib/unwrap-store.ts (persistent unwrap state), and lib/local-pairs-store.ts (pairs a visitor added themselves, stored per-browser).

Reads

useRegistryPairs(): use-registry.ts

Every registered pair, paged through the slice functions and enriched with symbols, names, decimals, rate, and TVS. Refetches every 60 s so new registrations and revocations appear without a redeploy. Metadata reads use Promise.allSettled, so one broken token degrades to placeholders instead of hiding the pair.

Sourcing is hybrid, three tiers deep: the onchain registry is the primary source of truth; pairs declared in config/custom-pairs.ts are merged in next with source: "custom"; pairs a visitor added themselves through the registry page’s Add a pair form (stored in their browser via lib/local-pairs-store.ts) are merged in last with source: "local". Each tier only fills in wrappers the tiers above it don’t already claim. See “Adding a new pair” in the repo README.

The registry exists on two networks, and the hook can read either. network defaults to "sepolia"; pass "mainnet" to browse the Ethereum mainnet registry (read-only in Obscura, and custom/local pairs are not merged there since they declare Sepolia addresses).

const { data: pairs, isLoading } = useRegistryPairs(); // pairs: EnrichedPair[], includes isValid, rate, tvs, wrapperDecimals... const { data: mainnetPairs } = useRegistryPairs({ network: "mainnet" }); // the production pairs: cUSDC, cUSDT, cWETH, ...

useUnderlyingBalance(tokenAddress): use-wrap.ts

Live ERC-20 balance of the connected account, 30 s refetch.

useConfidentialBalanceHandle(wrapper): use-decrypt.ts

The bytes32 balance handle for the connected account. Exposes the exported ZERO_HANDLE constant to detect never-funded accounts.

Writes

useWrapFlow(pair): use-wrap.ts

The full approve-then-wrap sequence behind one function, with per-step state for the UI (idle → approving → wrapping → success).

const { wrap, step, error, txHash, reset } = useWrapFlow(pair); await wrap(amount, roundedAmount); // checks allowance, approves if needed, wraps

The live allowance is checked at the start of every run, so a retry never re-prompts for an approval that already exists, and the approval covers the rounded amount only.

useUnwrapActions(): use-unwrap.ts

The two-step unwrap as a resumable state machine. Each transition persists to localStorage through lib/unwrap-store, so any step can fail and resume, even after a page reload.

const { startUnwrap, resumeUnwrap, busyKey } = useUnwrapActions(); await startUnwrap(pair, amountWrapperUnits); // encrypt → unwrap → decrypt → finalize await resumeUnwrap(record); // pick up a stored record wherever it stopped

resumeUnwrap derives the next action from which fields the record already carries (request id? cleartext + proof?), recovers the request id from the tx receipt if the tab closed mid-flight, and detects requests someone else already finalized via unwrapRequester.

useDecryptBalance(wrapper): use-decrypt.ts

User decryption behind a single call: throwaway keypair, EIP-712 signature, relayer round-trip. The cleartext lives in component state only.

const { decrypt, hide, step, error, decrypted } = useDecryptBalance(wrapper); await decrypt(handle); // step: idle → signing → decrypting

Aggregates

HookFileWhat it gives you
usePortfolio()use-portfolio.tsThe user’s holdings across every wrapper, decrypt-on-demand, pending unwraps
useActivity(...)use-activity.tsPaginated global activity feed from the REST API
useStats()use-stats.tsGlobal totals: pairs, valid/revoked, aggregate TVS
useFaucet(...)use-faucet.tsMints the mock underlying tokens on Sepolia

Cache invalidation is part of the contract. After a wrap, the hooks invalidate the erc20-balance, confidential-balance, and registry query keys; an unwrap burn invalidates confidential-balance immediately (the balance shrinks at the burn, not at finalization). If you lift a hook, keep its invalidations; they are what make the UI feel correct.

Lifting them

  1. Copy packages/shared (ABIs, addresses, types) or install the equivalents.
  2. Copy lib/fhevm.ts and, if you take the unwrap hook, lib/unwrap-store.ts.
  3. Copy the hook files you need; they import nothing from Obscura’s UI other than small formatting helpers you can inline.
  4. Serve your app with the COOP/COEP headers the Relayer SDK needs.
Last updated on