UPP — Universal Private PoolSDK Reference

React Hooks

UPPAccountProvider and useUPPAccount — React integration for the UPP SDK.

React Hooks

# React hooks are included in the main package
npm install @permissionless-technologies/upp-sdk

UPPAccountProvider

Wrap your app with the provider to make UPP state available to all components:

import { UPPAccountProvider } from '@permissionless-technologies/upp-sdk/react'

function App() {
  return (
    <UPPAccountProvider
      ethAddress={address}
      chainId={chainId}
      signTypedData={signTypedDataAsync}
      enablePasskey  // Enable passkey authentication as an alternative
    >
      <YourApp />
    </UPPAccountProvider>
  )
}

The provider handles key derivation, note scanning, and client lifecycle. It supports two authentication methods:

  • Wallet signature (default) — requires a connected wagmi wallet
  • Passkey (WebAuthn PRF) — uses biometric/PIN, no wallet needed. Enable with enablePasskey prop.

useUPPAccount

import { useUPPAccount } from '@permissionless-technologies/upp-sdk/react'

function PrivateBalance() {
  const {
    notes,          // Note[] | undefined
    isScanning,     // boolean
    isReady,        // boolean — keys derived, client initialized
    shield,         // (params) => Promise<{commitment, txHash}>
    transfer,       // (params) => Promise<{...}>
    withdraw,       // (params) => Promise<{txHash}>
    merge,          // (params) => Promise<{commitment, txHash}>
    scan,           // () => Promise<Note[]>
    stealthAddress, // string | undefined — your 0zk1... address
  } = useUPPAccount()

  if (!isReady) return <div>Connecting...</div>
  if (isScanning) return <div>Scanning notes...</div>

  const balance = notes
    ?.filter(n => !n.spent)
    .reduce((sum, n) => sum + n.amount, 0n) ?? 0n

  return (
    <div>
      <p>Private balance: {balance.toString()}</p>
      <button onClick={() => shield({ token: UPD_ADDRESS, amount: 100n * 10n**18n })}>
        Shield 100 UPD
      </button>
    </div>
  )
}

Passkey Authentication

When enablePasskey is set on the provider, the context exposes passkey methods:

function PasskeyLogin() {
  const {
    passkeySupported,      // boolean — is WebAuthn PRF available?
    isPasskeyAccount,      // boolean — was this account derived from a passkey?
    connectWithPasskey,     // (options?) => Promise<void>
    isConnectingPasskey,   // boolean
  } = useUPPAccount()

  if (!passkeySupported) return null

  return (
    <div>
      <button onClick={() => connectWithPasskey({ create: true })}>
        Create Passkey
      </button>
      <button onClick={() => connectWithPasskey()}>
        Use Existing Passkey
      </button>
    </div>
  )
}

The optional password parameter creates different accounts from the same passkey:

await connectWithPasskey({ create: true, password: 'my-salt' })

Passkey accounts are persisted in IndexedDB and auto-loaded on return visits. The same keys are derived every time — deterministic via the WebAuthn PRF extension. Supported in Chrome 120+, Safari 18+, and Edge 120+.

Full Example

import { UPPAccountProvider, useUPPAccount } from '@permissionless-technologies/upp-sdk/react'
import { parseUnits, formatUnits } from 'viem'

const UPD_ADDRESS = '0x...'

function ShieldForm() {
  const { shield, isReady } = useUPPAccount()

  const handleShield = async () => {
    try {
      const { txHash } = await shield({
        token: UPD_ADDRESS,
        amount: parseUnits('100', 18),
      })
      console.log('Shielded:', txHash)
    } catch (e) {
      console.error(e)
    }
  }

  return (
    <button disabled={!isReady} onClick={handleShield}>
      Shield 100 UPD
    </button>
  )
}

function TransferForm() {
  const { transfer } = useUPPAccount()

  const handleTransfer = async (recipientAddress: string) => {
    const { txHash } = await transfer({
      amount: parseUnits('50', 18),
      to: recipientAddress,  // 0zk1... stealth meta-address
    })
    console.log('Transferred:', txHash)
  }

  return (
    <input
      placeholder="0zk1... recipient address"
      onKeyDown={e => e.key === 'Enter' && handleTransfer(e.currentTarget.value)}
    />
  )
}

export function App() {
  return (
    <UPPAccountProvider>
      <ShieldForm />
      <TransferForm />
    </UPPAccountProvider>
  )
}

useCircuitCache

Manage circuit artifact downloads and IndexedDB caching. Circuit proving keys are large (148–709 MB) and are downloaded lazily on first use, then cached persistently.

import { useCircuitCache } from '@permissionless-technologies/upp-sdk/react'

function CircuitManager() {
  const {
    isCached,           // (circuit) => boolean
    preload,            // (circuit) => Promise<void>
    downloadProgress,   // DownloadProgress | null
    isDownloading,      // boolean
    status,             // Map<UPPCircuitType, CircuitCacheStatus>
    evict,              // (circuit) => Promise<void>
    evictAll,           // () => Promise<void>
    version,            // string — current CIRCUIT_VERSION
  } = useCircuitCache()

  return (
    <div>
      <h3>Circuit Cache (v{version})</h3>

      <button
        onClick={() => preload('transfer')}
        disabled={isCached('transfer') || isDownloading}
      >
        {isCached('transfer') ? 'Transfer ✓' : 'Download Transfer (148 MB)'}
      </button>

      {downloadProgress && (
        <p>
          Downloading {downloadProgress.circuitType}.{downloadProgress.artifact}:
          {' '}{downloadProgress.percent}%
        </p>
      )}
    </div>
  )
}

Proof hooks (usePoolTransfer, useWithdraw, useSwap) automatically use the cache — if the circuit is already cached, proof generation starts instantly without re-downloading.

On this page