npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@umbra-privacy/widget

v0.1.3

Published

Embeddable Umbra privacy wallet — shield / private send / unshield / receive — in a single React component.

Readme

@umbra-privacy/widget

Embeddable Umbra privacy wallet in one React component — shield, private send, unshield, receive/claim, and a private-balance home, with one-time on-chain registration. Logic comes from @umbra-privacy/client (frontend-core), bundled in so the widget is self-contained; heavy ZK proving runs in a Web Worker.

Integrating with an AI agent? This README is structured for copy-paste correctness: every snippet is complete and runnable, deps are version-pinned, and known gotchas are called out inline. The single hardest part is the signer — read that section first. A condensed machine-readable index lives in llms.txt.


Table of contents


What it does

<UmbraWidget /> drops a complete private-wallet UI into any React app. The user shields public Solana tokens into a private balance, sends privately through a stealth UTXO pool, unshields back to public, and claims incoming private notes — all from one component you mount with a signer and an rpcUrl.

  • Self-contained@umbra-privacy/client + client-platform are bundled into dist. Consumers do not install them. No aliases to configure.
  • Two render modes — modal (pass a trigger or control open) or inline.
  • Themed at runtimeui prop maps to scoped CSS vars, no rebuild. Full-width customizable tabs; brand mark auto-tints black/white for contrast.
  • Flow feedback — submit buttons show an inline spinner while pending; on success/error a full-card overlay confirms the result with a 5s "Go Home".
  • Browser-only — uses Web Worker (ZK), WebCrypto, and IndexedDB.

Quickstart (60 seconds)

npm i @umbra-privacy/widget
import { UmbraWidget } from '@umbra-privacy/widget'
import '@umbra-privacy/widget/styles.css'

export function GoPrivate({ signer }) {
  return (
    <UmbraWidget
      signer={signer}                // a @solana/kit signer — see "Signer" below
      trigger={<button>Go Private</button>}
    />
  )
}

signer is the only required prop. Everything else (rpcUrl, network, mints, tabs, ui, storage, endpoints, padding) is optional — rpcUrl defaults to a Helius endpoint by network, the rest to Umbra production. Pass rpcUrl to use your own provider.

Two app-level requirements that the bundle can't satisfy for you — set them up once: Node globals polyfill and Web Worker support. See Bundler setup. If you skip them the widget fails to init.


Install

npm i @umbra-privacy/widget
# or: pnpm add / yarn add

Peer dependencies (the host app provides these — they are not bundled):

| Peer | Range | | ------------------------- | ---------------- | | react | >=18 | | react-dom | >=18 | | @solana/kit | >=6 | | @tanstack/react-query | >=5 | | @umbra-privacy/sdk | 5.0.0-rc.4 |

@umbra-privacy/client and @umbra-privacy/client-platform are bundled at build time — do not add them to your app.

Exports:

import {
  UmbraWidget,        // modal-or-inline component
  UmbraWidgetInline,  // always-inline component
  DEFAULT_MINTS       // the default token list
} from '@umbra-privacy/widget'
import '@umbra-privacy/widget/styles.css'   // required — ships the scoped styles

Public types: UmbraWidgetProps, WidgetSigner, WidgetMint, WidgetTab, WidgetUiConfig, WidgetStorage, WidgetNetwork, WidgetEndpoints.


Concepts

Flows (tabs). Each tab is one privacy operation:

| Tab | Direction | What happens | | ---------- | ------------------ | ----------------------------------------------------------------------- | | home | — | Private balances + USD value, live prices. | | shield | public → private | Deposit public tokens into your private balance. | | transfer | private → private | Send privately to another address via the stealth-pool UTXO. | | unshield | private → public | Withdraw private balance back to a public account. | | receive | inbound | Claim incoming private notes addressed to you. |

Registration gate. On mount the widget checks on-chain registration (checkRegistrationOnChain). Registered users land on Home; everyone else sees a one-time sign-to-register screen. After registering, the SDK client auto-initializes; until it's ready the widget shows only the app icon.

SOL gate. Shield/transfer/unshield/register all pay on-chain fees, so the widget blocks them when public SOL is below the flow's minimum (button reads "Not enough SOL"). Claim is relayer-funded and exempt. Minimums in Defaults reference.

Flow feedback. While a flow is pending the submit button disables and shows an inline spinner beside its label. On settle, a full-card overlay covers the widget — a primary-coloured circle with a knocked-out tick (success) or cross (error) — and a "Go Home · Ns" button counts down from 5s, auto-returning to the Home tab at zero (or immediately on click).

Key consistency. The widget verifies that locally-derived keys match what's on-chain and exposes a restore mutation when they diverge.

Claimed-UTXO filtering. A burnt-nullifier indexer plus local hash derivation filters already-claimed UTXOs (cached in IndexedDB). The nullifier indexer needs CORS for browsers — see the note in Defaults reference.


Signer: the one thing to get right

signer must be a @solana/kit signer that can do both of these:

  1. Sign messages (MessagePartialSigner) — used once to derive the Umbra master seed.
  2. Sign a transaction and return it — either a TransactionPartialSigner (signTransactions, e.g. a KeyPairSigner) or a TransactionModifyingSigner (modifyAndSignTransactions, the wallet-standard solana:signTransaction adapter).

The TypeScript shape (src/types.ts):

type WidgetSigner = MessagePartialSigner
  & (TransactionModifyingSigner | TransactionPartialSigner)
  & Partial<TransactionSendingSigner>

⚠️ A sign-and-send-only wallet is NOT enough. A TransactionSendingSigner (signAndSendTransactions alone) broadcasts the tx and returns only a signature. The Umbra deposit/withdraw pipeline must get the signed transaction bytes back to submit them through its own relayer/MPC flow. signAndSendTransactions is accepted as an optional fast path when also present, but it cannot stand in for a returning signer.

The wallet-standard adapter recipe is in Recipes → Wallet Standard.


Recipes

Wallet Standard (recommended)

Adapt a connected Wallet Standard account into a WidgetSigner with @solana/react. This is the production path for browser wallets (Phantom, Solflare, …). Full reference: playground/wallet/use-widget-signer.ts.

import { useMemo } from 'react'
import {
  useWalletAccountMessageSigner,
  useWalletAccountTransactionSigner,
  useWalletAccountTransactionSendingSigner
} from '@solana/react'
import type { UiWalletAccount } from '@wallet-standard/react'
import type { WidgetSigner } from '@umbra-privacy/widget'

export function useWidgetSigner(
  account: UiWalletAccount,
  chain: `solana:${string}`   // e.g. 'solana:mainnet'
): WidgetSigner {
  const signTx = useWalletAccountTransactionSigner(account, chain)
  const sending = useWalletAccountTransactionSendingSigner(account, chain)
  const msg = useWalletAccountMessageSigner(account)

  return useMemo<WidgetSigner>(
    () => ({
      address: signTx.address,
      modifyAndSignTransactions: signTx.modifyAndSignTransactions,
      signAndSendTransactions: sending.signAndSendTransactions, // optional fast path
      signMessages: async (messages) => {
        const signed = await msg.modifyAndSignMessages(messages)
        return signed.map((m) => m.signatures)
      }
    }),
    [signTx, sending, msg]
  )
}

Then mount:

function Wallet({ account }: { account: UiWalletAccount }) {
  const signer = useWidgetSigner(account, 'solana:mainnet')
  return (
    <UmbraWidget
      signer={signer}
      rpcUrl={RPC_URL}
      network="mainnet"
      trigger={<button>Go Private</button>}
    />
  )
}

KeyPair signer (scripts / tests)

Any KeyPairSigner from @solana/kit already satisfies WidgetSigner (it's a MessagePartialSigner + TransactionPartialSigner), so you can pass it directly.

Modal vs inline

// Modal — controlled
const [open, setOpen] = useState(false)
<UmbraWidget signer={signer} rpcUrl={RPC_URL} open={open} onOpenChange={setOpen} />

// Modal — uncontrolled, opened by its own trigger
<UmbraWidget signer={signer} rpcUrl={RPC_URL} trigger={<button>Go Private</button>} />

// Inline — no trigger and no `open` → renders in place
<UmbraWidgetInline signer={signer} rpcUrl={RPC_URL} className="w-full max-w-md" />

Rule: UmbraWidget renders as a modal when trigger or open is provided, otherwise it renders inline (identical to UmbraWidgetInline).

Theming

ui is partial — unset fields fall back to the built-in light theme. Each field maps to a scoped CSS variable, re-themable at runtime.

<UmbraWidget
  signer={signer}
  rpcUrl={RPC_URL}
  ui={{
    colors: {
      bg: '#0b0d12',
      surface: '#11141b',
      text: '#eef1f6',
      primary: '#3b9dff',
      primaryFg: '#ffffff',
      tabActive: '#1a1f2b'
    },
    font: { primary: 'Inter, system-ui, sans-serif' },
    rounding: '14px',  // single value sets sm/md/lg; or pass { sm, md, lg }

    // Tabs are full-width and split the row equally. Style the row + each tab:
    tabs: {
      rowBg: '#11141b',     // background behind the whole tab row
      rowPadding: '4px',    // padding inside the row (CSS length)
      bg: 'transparent',    // inactive tab background
      activeBg: '#1a1f2b',  // active tab background (alias of colors.tabActive)
      border: 'transparent',// per-tab border (color only)
      radius: '10px'        // rounding for the row and each tab
    }
  }}
/>

The brand mark auto-tints to pure black or white for contrast against the resolved colors.bg (luminance check) — no setting needed. Non-hex bg (rgb(), named, gradients) falls back to the black tint.

Full color/font/tabs/rounding fields: WidgetUiConfig.

Custom tokens, tabs, endpoints, storage

<UmbraWidget
  signer={signer}
  rpcUrl={RPC_URL}

  // Restrict the supported token set (symbol/decimals/icon auto-resolved from
  // Helius metadata unless you override them here).
  mints={[{ address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' }]}

  // Restrict & order the visible tabs.
  tabs={['home', 'shield', 'unshield']}

  // Point at a different deployment, or proxy the nullifier indexer (CORS).
  endpoints={{ nullifierIndexer: 'https://your-origin.example.com/null-proxy' }}

  // Bring your own persistence (defaults to IndexedDB).
  storage={window.localStorage}
/>

Bundler setup

The dist is pre-bundled — no aliases needed. Two browser-runtime requirements carry through to the host app:

1. Node globals. The SDK/crypto graph references Buffer, process, and global. Inject them once:

// Vite — vite.config.ts
import { nodePolyfills } from 'vite-plugin-node-polyfills'
export default {
  plugins: [nodePolyfills({ globals: { Buffer: true, process: true, global: true } })]
}

2. Web Workers. ZK proving runs in an ES-module Web Worker the widget spawns itself. Vite, webpack 5, and Next handle the emitted worker chunk with no config — just don't strip import.meta.url / worker support from your build.

No @noble/hashes alias is needed in the host; that's an internal build-time concern already resolved inside the shipped bundle.

Next.js (App Router)

The widget is browser-only — render it client-side, never on the server.

npm i buffer process   # polyfill sources for the webpack config
// next.config.js
const webpack = require('webpack')
module.exports = {
  webpack: (config) => {
    config.plugins.push(
      new webpack.ProvidePlugin({
        Buffer: ['buffer', 'Buffer'],
        process: 'process/browser'
      })
    )
    config.resolve.fallback = {
      ...config.resolve.fallback,
      buffer: require.resolve('buffer'),
      process: require.resolve('process/browser')
    }
    return config
  }
}
// app/wallet.tsx
'use client'
import dynamic from 'next/dynamic'
import '@umbra-privacy/widget/styles.css'

// ssr: false — the widget touches browser-only APIs at import time.
const UmbraWidget = dynamic(
  () => import('@umbra-privacy/widget').then((m) => m.UmbraWidget),
  { ssr: false }
)

export function Wallet({ signer, rpcUrl }) {
  return <UmbraWidget signer={signer} rpcUrl={rpcUrl} trigger={<button>Go Private</button>} />
}

On Turbopack (next dev --turbo), the webpack field is ignored — run dev on webpack, or provide the globals via a <script> shim until Turbopack polyfill config lands.


Props reference

All props live on UmbraWidgetProps. * = required.

| Prop | Type | Default | Notes | | --------------- | --------------------------------- | ------------------------ | --------------------------------------------------------------------- | | signer * | WidgetSigner | — | @solana/kit signer. See Signer. | | rpcUrl | string | Helius by network | Solana RPC HTTP endpoint. WS is derived (httpswss). Token metadata always uses Helius DAS regardless. | | network | 'mainnet' \| 'devnet' | inferred from rpcUrl | Override only if inference is wrong. | | mints | WidgetMint[] | DEFAULT_MINTS | Supported tokens; metadata auto-resolved unless overridden. | | tabs | WidgetTab[] | all five, in order | Subset & order of home/shield/transfer/unshield/receive. | | ui | WidgetUiConfig | light theme | Partial theme overrides → scoped CSS vars. Includes tabs chrome. | | storage | WidgetStorage | IndexedDB | KV store; sync (localStorage) returns accepted. | | endpoints | WidgetEndpoints | Umbra production | Per-field override; each falls back to default. | | padding | number \| string | 20 | Inline widget only. Global padding; number → px. | | walletAddress | Address \| string | signer.address | Override the resolved current address. | | open | boolean | — | Controlled modal open state. | | onOpenChange | (open: boolean) => void | — | Modal open-change callback. | | trigger | React.ReactNode | — | Element that opens the modal. Presence → modal mode. | | className | string (inline only) | standalone card chrome | UmbraWidgetInline only — size/position the inline card. |

Type shapes

interface WidgetMint {
  address: string        // base58 mint
  symbol?: string        // override auto-resolved metadata
  decimals?: number
  iconUrl?: string
}

type WidgetTab = 'home' | 'shield' | 'transfer' | 'unshield' | 'receive'

interface WidgetStorage {                       // IndexedDB-backed KV by default
  getItem(key: string): string | null | Promise<string | null>
  setItem(key: string, value: string): void | Promise<void>
  removeItem(key: string): void | Promise<void>
}

interface WidgetEndpoints {                      // each falls back to prod
  indexer?: string          // UTXO indexer (SDK indexerApiEndpoint)
  nullifierIndexer?: string // burnt-nullifier indexer (claimed-UTXO detection)
  relayer?: string          // gasless relayer
  das?: string              // DAS-capable RPC for token metadata (getAssetBatch);
                            //   defaults to Helius by network, independent of rpcUrl
  zkCdnUrl?: string         // ZK assets CDN base (no trailing slash)
  zkManifestUrl?: string    // defaults to `${zkCdnUrl}/v5/manifest.json`
}

Defaults reference

Default mints (src/constants/mints.ts) — mirrors PRIVATE_MODE_MINTS from @umbra-privacy/client:

EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v   (USDC)
PRVT6TB7uss3FrUd2D9xs2zqDBsa3GbMJMwCQsgmeta
So11111111111111111111111111111111111111112    (wSOL)
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB    (USDT)
CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH
zinc155BS4mSPk8GXQj4R5hkVDQXcW253pTYq5SGyfi

Default endpoints (src/constants/endpoints.ts):

| Endpoint | Default | | ------------------ | ------------------------------------------------ | | indexer | https://utxo-indexer.api.umbraprivacy.com | | nullifierIndexer | https://nullifier-indexer.api.umbraprivacy.com | | relayer | https://relayer.api.umbraprivacy.com | | das | Helius RPC by network (token metadata, DAS) | | zkCdnUrl | https://zk.api.umbraprivacy.com | | zkManifestUrl | ${zkCdnUrl}/v5/manifest.json |

CORS note: the nullifier indexer must allow your browser origin. If it doesn't, claimed UTXOs won't be filtered — proxy it through your own origin and set endpoints.nullifierIndexer to the proxy URL.

Minimum public SOL per flow (lamports, src/constants/sol-requirements.ts):

| Flow | Lamports | ≈ SOL | | -------- | ------------ | ------- | | shield | 13_000_000 | 0.013 | | unshield | 13_000_000 | 0.013 | | transfer | 20_000_000 | 0.020 | | register | 13_000_000 | 0.013 | | claim | — | relayer-funded (exempt) |


Troubleshooting

| Symptom | Cause | Fix | | -------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------ | | Buffer is not defined / process is not defined | Node globals not polyfilled in the host bundler | Add the polyfill — Bundler setup. | | Widget never leaves the app-icon screen | Client init blocked (worker failed, or RPC/registration error) | Verify worker support and a working rpcUrl; check console for the init error. | | Button reads "Not enough SOL" | Public SOL below the flow minimum | Fund the wallet — see SOL minimums. | | Type error: signer not assignable to WidgetSigner| Sign-and-send-only wallet | Provide a returning signer — Signer. | | Claimed UTXOs still shown as available | Nullifier indexer lacks CORS for your origin | Proxy it and set endpoints.nullifierIndexerCORS note. | | SSR crash / window is not defined (Next) | Rendered on the server | dynamic(..., { ssr: false }) + 'use client'Next.js. | | Styles missing / unstyled widget | Stylesheet not imported | import '@umbra-privacy/widget/styles.css'. |


Architecture

src/
  UmbraWidget.tsx        # public component (modal + inline)
  widget-body.tsx        # registration / client-init gate → tabs
  types.ts               # all public prop types
  providers/             # WidgetProvider (QueryClient + service graph + signer)
  client/                # services.ts (buildServices), signer, runtime-deps,
                         #   platform (zk/relayer), storage (IndexedDB), legacy seed
  features/<flow>/       # query.ts (RQ) → hooks/use-*.ts → components/*Tab.tsx
  workers/               # zk-proof worker (+ generic worker-rpc in client-platform)
  ui/                    # shadcn-style primitives (Dialog, Tabs, AmountField…)
  constants/             # mints, endpoints, sol-requirements

Data flow. Balances/prices come from @umbra-privacy/client/token (useTokens + aggregatePortfolio); privacy flows go through the client's shielding / utxo factories. Per-feature convention: query.ts (React Query) → orchestrator hooks/use-*.ts → thin presentational components.

Legacy seed. legacyMasterSeedScheme is registered so old-scheme accounts and their UTXOs stay viewable/claimable.


Develop

pnpm install
pnpm dev        # playground at http://localhost:5180
pnpm build      # dist/ (ESM + CJS + types + styles.css), client bundled in
pnpm typecheck

Build-config notes (vite.config.ts / vite.playground.config.ts)

The build configs are deliberately minimal. What remains and why:

  • @noble/hashes alias (the one resolve.alias entry) — @metaplex-foundation/* deps (pulled by client/solana) bare-import noble v1 subpaths, but pnpm hands them the hoisted v2, whose exports only expose *.js. The alias rewrites @noble/hashes/sha3…/sha3.js. Scoped to sha2|sha3|utils so it never touches v1-only specifiers (/crypto). Irreducible: client needs v2, mpl needs v1, and pnpm won't give mpl its own v1 for a peer dep.
  • nodePolyfills (playground only) — Buffer/process/global for the SDK graph. The lib build omits it on purpose; the host app provides them (see Bundler setup).
  • worker: { format: 'es' } — the ZK worker is an ES module (top-level import of snarkjs), so it must be emitted as ESM, not the legacy IIFE.
  • vite-tsconfig-paths — resolves the widget's own @/* imports from tsconfig, so no manual @ alias.

Client subpaths resolve with no alias because @umbra-privacy/client / client-platform ship explicit (non-wildcard) exports; the ledger transports load via dynamic import(), so no stub is needed.

Publishing

Prepped, not auto-published — see publishConfig (restricted). Bump version, pnpm build, npm pack --dry-run (ships dist/ only), then npm publish (set the registry via .npmrc). client / client-platform are bundled at build time (devDep file: tarballs), so consumers don't install them.