React Hooks
UPPAccountProvider, useUPPAccount, and the discrete operation hooks (useShield, usePoolTransfer, useWithdraw, useSwap) — the React layer that wraps the encoders, sync engine, and crypto into a familiar hook surface.
The React hooks compose the entire SDK — key derivation, sync engine, encoders, proof workers, circuit cache — into a familiar hook surface. They live under @permissionless-technologies/upp-sdk/react and are designed to coexist with wagmi: you bring the wallet, they bring the privacy layer.
Hook surface at a glance
The split matters: useUPPAccount is the state, the discrete hooks are the actions. Operation hooks read from the account context for keys and notes, then return a build() function that produces viem-compatible tx args — your component still calls writeContract itself. The build/submit separation keeps the SDK from owning your wallet client.
UPPAccountProvider
Wrap your app once near the wagmi provider:
import { UPPAccountProvider } from '@permissionless-technologies/upp-sdk/react'
import { useSignTypedData, usePublicClient, useAccount, useChainId } from 'wagmi'
function PrivacyLayer({ children }: { children: React.ReactNode }) {
const { address } = useAccount()
const chainId = useChainId()
const publicClient = usePublicClient()
const { signTypedDataAsync } = useSignTypedData()
return (
<UPPAccountProvider
ethAddress={address}
chainId={chainId}
publicClient={publicClient}
signTypedData={signTypedDataAsync}
activeProofSystem="snark"
indexerConfig={{
contractAddress: poolAddress,
swapModuleAddress,
fromBlock: deploymentBlock,
liveSyncIntervalMs: 60_000,
}}
enablePasskey
>
{children}
</UPPAccountProvider>
)
}Props summary:
| Prop | Purpose |
|---|---|
ethAddress | Wallet address (from wagmi useAccount). Drives the per-account storage prefix. |
chainId | Current chain ID. Used in the chain-fingerprint check so accounts can't leak across networks. |
publicClient | Viem PublicClient (from usePublicClient). Required for the indexer and chain-fingerprint guard. |
signTypedData | Wagmi's signTypedDataAsync. Used by connect() to derive master keys via EIP-712. |
indexerConfig | Pool/swap addresses, fromBlock, live-sync interval. Without this the indexer can't start. |
activeProofSystem | 'snark' or 'stark' — filters unspentNotes and balance to the rail you're showing in the UI. |
enablePasskey | Wires up the WebAuthn-PRF path so users can derive an account from a passkey instead of a wallet signature. |
The provider lazy-loads the SDK on mount and pre-warms the Poseidon31 WASM in the background so the first connect() doesn't pay the ~800 KB fetch on the critical path.
useUPPAccount
The account context. Use it for state and lifecycle; use the discrete hooks below for operations.
import { useUPPAccount } from '@permissionless-technologies/upp-sdk/react'
const {
// ─── Setup & lifecycle ─────────────────────────────────
isSetup, // boolean — master keys derived
cryptoReady, // boolean — SDK loaded
isConnecting, // boolean — connect() in flight
connect, // (password?: string) => Promise<void>
logout, disconnect, // clear keys + storage
passkeySupported, // boolean — WebAuthn PRF available
connectWithPasskey, // (options?) => Promise<void>
isConnectingPasskey,
isPasskeyAccount, // boolean — keys came from a passkey
// ─── Identity ──────────────────────────────────────────
masterKeys, // SNARK master keys, null if not set up
starkMasterKeys, // STARK master keys, null if not set up
ownerHash, // hex string — SNARK public identifier
stealthAddress, // 0zk1… SNARK stealth address
starkStealthAddress, // 0zks1… STARK stealth address
// ─── Notes ─────────────────────────────────────────────
notes, // ShieldedNote[] — all notes
unspentNotes, // ShieldedNote[] — filtered by activeProofSystem
balance, // bigint — sum of unspentNotes (filtered)
addNote, markNoteSpent, // manual edits (e.g. after a transfer)
createNoteForSelf, // for shield + change outputs
createNoteForRecipient, // for outbound transfers
// ─── Sync ──────────────────────────────────────────────
isSyncing, lastSyncedBlock,
syncNotes, // (contractAddress, client) => Promise<number>
startLiveSync, stopLiveSync, isLiveSyncing,
getStarkStateLeaves, // V3 ordered leaf stream
getStarkStateLeavesV4, // V4 ordered leaf stream
// ─── Selection ─────────────────────────────────────────
selectOptimalCircuit, // picks 1x2 / 2x2 for a given amount
} = useUPPAccount()Why no top-level shield/transfer/withdraw
Earlier drafts of this SDK exposed account.shield() / account.transfer() directly. They were removed because privacy ops need a build step (proof, encryption, contract args) that's distinct from the submit step — and the submit step belongs to your wallet client, not the SDK. The discrete hooks (useShield, usePoolTransfer, useWithdraw, useSwap) return a build() function; you compose that with viem's writeContract or wagmi's useWriteContract however your app already does writes.
Connecting
There are two derivation paths; both end with the same MasterKeys populated on the context.
// 1. Wallet signature (EIP-712)
const { connect, isConnecting } = useUPPAccount()
await connect() // signature with default domain
await connect('my-salt') // optional password salts the account
// 2. Passkey (WebAuthn PRF)
const { connectWithPasskey, passkeySupported } = useUPPAccount()
if (passkeySupported) {
await connectWithPasskey({ create: true }) // new account
await connectWithPasskey({ create: true, password: 'work' }) // sub-account
await connectWithPasskey() // unlock existing
}Same signature input → same keys, every time. The provider auto-loads previously created accounts from IndexedDB on mount.
Discrete operation hooks
Each operation hook is configured once with pool/token addresses, then returns a build() (or build*() for swaps) that takes operation-specific params and produces tx args. Your component is still in charge of calling writeContract, awaiting the receipt, and calling addNote / markNoteSpent on the account.
useShield
import { useShield } from '@permissionless-technologies/upp-sdk/react'
import { encodeShieldTxFromBuildData } from '@permissionless-technologies/upp-sdk'
const { build, isPending, stage, error } = useShield({
poolAddress,
tokenAddress: UPD_ADDRESS,
})
const handleShield = async () => {
const data = await build({ amount: 100n * 10n**18n, originAddress: address })
// ERC20 approve omitted for brevity
await writeContractAsync({
address: poolAddress,
...encodeShieldTxFromBuildData(data),
})
addNote(data.noteData.note)
}usePoolTransfer
The SNARK 1-in-2-out transfer with ASP membership proof. STARK transfer is a separate hook surface — see the V4 commit/flush page.
import { usePoolTransfer } from '@permissionless-technologies/upp-sdk/react'
import { encodeTransferTxFromBuildData } from '@permissionless-technologies/upp-sdk'
const { build, isPending, stage, provingProgress, error } = usePoolTransfer({
poolAddress,
tokenAddress: UPD_ADDRESS,
publicClient,
aspId: DEMO_ASP_ID,
})
const data = await build({ amount, recipient: '0zk1…' })
await writeContractAsync({
address: poolAddress,
...encodeTransferTxFromBuildData(data),
})
markNoteSpent(data.spentNote.commitment)
addNote(data.changeNoteData.note)stage walks through 'preparing' → 'generating_proof' → 'submitting_tx' → 'confirming' → 'done'. provingProgress is non-null only during the proof stage and is what you bind to a PLONK progress bar.
useWithdraw
import { useWithdraw } from '@permissionless-technologies/upp-sdk/react'
import { encodeWithdrawTxFromBuildData } from '@permissionless-technologies/upp-sdk'
const { build } = useWithdraw({ poolAddress, tokenAddress, publicClient })
const data = await build({
amount,
recipient,
aspId: 1n,
aspRoot,
aspPathElements,
aspPathIndices,
// OR isRagequit: true to bypass ASP (recipient must equal origin)
})
await writeContractAsync({ address: poolAddress, ...encodeWithdrawTxFromBuildData(data) })useSwap
The order-book hook is split: useSwapOrderBook reads order state, useSwap builds the order-side actions.
import { useSwap, useSwapOrderBook } from '@permissionless-technologies/upp-sdk/react'
const { orders, isLoading } = useSwapOrderBook({
swapModuleAddress, publicClient, sellToken, buyToken,
})
const { buildPlaceOrder, buildFillOrder, buildClaimOrder, buildCancelOrder, stage } = useSwap({
poolAddress, publicClient,
})Each build* returns the matching *BuildData shape — pass it to encodeSwapOrderTxFromBuildData / encodeSwapFillTxFromBuildData / etc. from the main SDK entry point.
Read-side hooks
usePrivateBalance
Per-token balances aggregated across both rails plus the swap module:
import { usePrivateBalance } from '@permissionless-technologies/upp-sdk/react'
const { balances, isSyncing, sync } = usePrivateBalance({
poolAddress,
publicClient,
walletAddress: address,
swapModuleAddress,
tokenList: [{ address: UPD_ADDRESS, symbol: 'UPD', decimals: 18 }],
})
balances.forEach(b => {
// b.snark.balance + b.stark.balance + b.unclaimedSnark.balance = b.total
})usePersonalASP
Reads the user's personal ASP registration state from the ASP Hub, used by the demo to drive the "approve yourself" flow:
import { usePersonalASP } from '@permissionless-technologies/upp-sdk/react'
const { aspInfo, isOriginApproved, getASPRootForOrigin } = usePersonalASP({
aspHubAddress, publicClient, userAddress: address,
})
if (aspInfo?.isApproved) {
// ready to transfer/withdraw
}useCircuitCache
Circuit proving keys are large (148–709 MB) and live in IndexedDB once downloaded. Proof hooks (usePoolTransfer, useWithdraw, useSwap) hit this cache automatically; this hook is for letting users see/manage what's cached.
import { useCircuitCache } from '@permissionless-technologies/upp-sdk/react'
const { isCached, preload, downloadProgress, status, evict, evictAll, version } = useCircuitCache()
<button disabled={isCached('transfer')} onClick={() => preload('transfer')}>
{isCached('transfer') ? 'Cached' : 'Pre-download Transfer (148 MB)'}
</button>
{downloadProgress && <p>{downloadProgress.circuitType}: {downloadProgress.percent}%</p>}useStarkVerifierType
Lists the STARK withdraw verifier types currently registered on the pool:
import { useStarkVerifierType } from '@permissionless-technologies/upp-sdk/react'
const { verifierTypes } = useStarkVerifierType(poolAddress)
// → [{ id: 0x…, name: 'SP1_GROTH16_BN254_V3' }, { id: 0x…, name: 'DIRECT_STWO_V1' }, …]Useful for a "which verifier do you want to prove against?" selector when both DIRECT_STWO_V1 (Solidity Stwo verifier) and SP1_GROTH16_BN254_V3 (SP1 zkVM) are live on the same pool. The additive-upgrade pattern means V1/V2/V3 stay deployed alongside each other.
useProofWorker
Off-main-thread PLONK proving via a Web Worker:
import { useProofWorker } from '@permissionless-technologies/upp-sdk/react'
const { workerManager } = useProofWorker()
// pass workerManager into usePoolTransfer / useWithdraw / useSwap configWhen provided, proof generation happens in the worker and the main thread stays responsive. Without it everything runs inline.
useUPPCrypto
Lazy-loaded crypto primitives (Poseidon, BabyJubJub) for any direct cryptographic work outside the hook surface. Most apps don't need this — use the encoders.
STARK accounts
Setting enablePasskey or calling connect() derives both SNARK and STARK keys from the same signature (or passkey PRF output). The two keysets are domain-separated so a leak on one rail doesn't compromise the other.
To make the React layer scan STARK notes alongside SNARK:
- The provider already passes
starkOwnerHashandstarkSecrettocreateSyncEngineoncestarkMasterKeysis set. - Flip
activeProofSystemto'stark'to filterunspentNotesandbalanceto STARK-rail spendable notes. usePrivateBalancealways returns both rails per token; no flag needed.
The getStarkStateLeaves() / getStarkStateLeavesV4() accessors on useUPPAccount return the global STARK leaf streams you need to build transfer/withdraw witnesses — see the V4 Octary State Tree page.
A complete shield+transfer component
import { useState } from 'react'
import { useAccount, usePublicClient, useWriteContract } from 'wagmi'
import { parseUnits } from 'viem'
import {
useUPPAccount,
useShield,
usePoolTransfer,
} from '@permissionless-technologies/upp-sdk/react'
import {
encodeShieldTxFromBuildData,
encodeTransferTxFromBuildData,
} from '@permissionless-technologies/upp-sdk'
function PrivacyPanel() {
const { address } = useAccount()
const publicClient = usePublicClient()
const { writeContractAsync } = useWriteContract()
const { isSetup, balance, addNote, markNoteSpent, connect } = useUPPAccount()
const shield = useShield({ poolAddress, tokenAddress: UPD_ADDRESS })
const xfer = usePoolTransfer({ poolAddress, tokenAddress: UPD_ADDRESS, publicClient })
const [recipient, setRecipient] = useState('')
if (!isSetup) return <button onClick={() => connect()}>Connect privacy layer</button>
return (
<div>
<p>Private balance: {balance.toString()} (wei)</p>
<button
disabled={shield.isPending}
onClick={async () => {
const d = await shield.build({ amount: parseUnits('100', 18), originAddress: address! })
// ERC20 approve omitted
await writeContractAsync({ address: poolAddress, ...encodeShieldTxFromBuildData(d) })
addNote(d.noteData.note)
}}
>
Shield 100 UPD
</button>
<input value={recipient} onChange={e => setRecipient(e.target.value)} placeholder="0zk1…" />
<button
disabled={xfer.isPending || !recipient}
onClick={async () => {
const d = await xfer.build({ amount: parseUnits('10', 18), recipient })
await writeContractAsync({ address: poolAddress, ...encodeTransferTxFromBuildData(d) })
markNoteSpent(d.spentNote.commitment)
addNote(d.changeNoteData.note)
}}
>
Transfer 10 UPD
</button>
{xfer.provingProgress && <p>{xfer.provingProgress.stage}: {xfer.provingProgress.message}</p>}
</div>
)
}See also
- Transaction Encoders —
encode*FromBuildDatahelpers used by the examples above - Indexer & Sync Engine — What the provider runs in the background
- Fee System —
feeAuthargument the encoders accept - V4 Commit/Flush Transfers — STARK V4 transfer lifecycle the hooks build against
Fee System
Pluggable per-operation fees collected by an external FeeManager contract — EIP-3009 self-pay, relayer transferFrom, configurable per FeeKind. Pre-audit, all live Sepolia amounts are zero.
Note Indexer & Sync Engine
Two integration paths — the high-level RPC indexer with built-in persistence, and the framework-agnostic sync engine that drives the React hooks. Plus the building blocks (decryption, nullifier verification, storage adapters) for custom indexers.