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

@oasisprotocol/privana-sdk

v0.9.1

Published

React SDK for Privana - manage deposits, withdrawals, locks, and transfers

Downloads

3,943

Readme

@oasisprotocol/privana-sdk

React SDK for Privana - manage deposits, withdrawals, locks, and transfers with ease.

Installation

npm install @oasisprotocol/privana-sdk
# or
bun add @oasisprotocol/privana-sdk

Peer Dependencies

This SDK requires the following peer dependencies:

npm install react react-dom wagmi viem @tanstack/react-query

Quick Start

1. Wrap your app with the PrivanaProvider

import { PrivanaProvider } from '@oasisprotocol/privana-sdk'
import { WagmiProvider } from 'wagmi'
import { QueryClientProvider } from '@tanstack/react-query'

function App() {
  return (
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        <PrivanaProvider
          networkConfig={{
            name: 'Sapphire Testnet',
            chainId: 23295,
            apiUrl: 'https://api.testnet.privana.finance',
            accountingContract: '0xYourContractAddress',
          }}
          hostedAuth={{
            clientId: 'honoroll-web',
            redirectUri: 'https://honoroll.example.com/auth/callback',
          }}
        >
          <YourApp />
        </PrivanaProvider>
      </QueryClientProvider>
    </WagmiProvider>
  )
}

accountingContract must be the deployed Privana accounting contract address for the same environment as apiUrl.

2. Or build custom UI with hooks

import {
  useBalance,
  useDeposit,
  useTotalLockedBalance,
  useWithdraw,
} from '@oasisprotocol/privana-sdk'

function CustomWallet() {
  const { balanceFormatted } = useBalance()
  const { totalLocked } = useTotalLockedBalance()
  const { deposit } = useDeposit()
  const { withdraw } = useWithdraw()

  return (
    <div>
      <p>Available: {balanceFormatted}</p>
      <p>Total locked: {totalLocked}</p>
      <button onClick={() => deposit({ amount: 1000000n, tokenId: '0xYourTokenId' })}>
        Deposit
      </button>
    </div>
  )
}

Private Reads

useBalance, useBatchBalances, useHistory, useLockedFunds, useExpiredLocks, and useTotalLockedBalance support two auth modes:

useHistory({ offset: -1, limit: 50 }) fetches one authenticated history page. Non-negative offsets count pages from the oldest entries, negative offsets count from the end, and each page is returned oldest-to-newest. limit must be between 0 and 100. History entries include lock-lifecycle kinds (modifyLock, unlockLock) and directional transfer kinds (transferFromLockOut/transferFromLockIn, transferBalanceOut/transferBalanceIn). On the outbound (Out) kinds counterparty is the recipient, and on the inbound (In) kinds it is the sender.

Direct SIWE private reads

Default mode for same-origin Privana browser integrations:

  • GET /v1/accounting/auth/domain
  • GET /v1/accounting/auth/nonce?address=0x...
  • POST /v1/accounting/auth/login

The hooks cache the returned X-SIWE-Token, dedupe concurrent auth so a group of mounted private-read hooks only triggers one sign prompt, and retry once on 401 by re-authenticating through the same shared in-flight auth request.

Direct in-app SIWE auth (siweAuth)

For same-origin apps that want an explicit authenticated session (and JWT-authenticated writes), enable siweAuth on PrivanaProvider. The connected wallet signs an EIP-4361 message in-app and useSiweAuth() exposes login, logout, session, and the raw tokens:

import { PrivanaProvider, useSiweAuth } from '@oasisprotocol/privana-sdk'
;<PrivanaProvider siweAuth={{ autoLogin: true }}>
  <AuthGate />
</PrivanaProvider>

siweAuth accepts true, or an object with autoLogin (default true) and persistJwt (default false). It is mutually exclusive with hostedAuth.

Persistent JWT sessions (persistJwt)

Set persistJwt: true to mirror the session in localStorage (key scoped by API URL and chain ID) so a page reload restores an active session without another signature prompt:

;<PrivanaProvider siweAuth={{ autoLogin: true, persistJwt: true }}>
  <App />
</PrivanaProvider>

Logout captures the refresh token, clears local and cross-tab state immediately, suppresses automatic re-login, and attempts server-side revocation.

Security note: storing long-lived refresh credentials in localStorage makes them reachable from any JavaScript running on the page. An XSS vulnerability could exfiltrate them and forge a session, so only enable persistJwt on origins you fully control.

Hosted redirect auth for cross-domain apps

For widget or cross-domain frontends, configure hostedAuth on PrivanaProvider and use useHostedRedirectAuth() to start the hosted sign-in and complete it on your callback route:

import { PrivanaProvider, useBalance, useHostedRedirectAuth } from '@oasisprotocol/privana-sdk'

function HostedAuthButton() {
  const { login, logout, refresh, isAuthenticated, isLoading, error, session } =
    useHostedRedirectAuth()
  const { balanceFormatted } = useBalance()

  return (
    <div>
      <button onClick={() => void login()} disabled={isLoading || isAuthenticated}>
        Sign in with Privana
      </button>
      {isAuthenticated ? <button onClick={() => void refresh()}>Refresh Session</button> : null}
      {isAuthenticated ? <button onClick={() => void logout()}>Logout</button> : null}
      {session ? <p>Signed in as {session.address}</p> : null}
      {error ? <p>{error.message}</p> : null}
      <p>Balance: {balanceFormatted}</p>
    </div>
  )
}
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useHostedRedirectAuth } from '@oasisprotocol/privana-sdk'

function HostedAuthCallbackPage() {
  const router = useRouter()
  const { completeLogin } = useHostedRedirectAuth()
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    void completeLogin()
      .then((session) => {
        if (!session) {
          setError('No hosted authentication response was found.')
          return
        }
        router.replace('/')
      })
      .catch((err) => {
        setError(err instanceof Error ? err.message : 'Hosted authentication failed.')
      })
  }, [completeLogin, router])

  if (error) return <p>Error: {error}</p>
  return <p>Completing sign-in…</p>
}

In hosted-auth mode:

  • the SDK stores PKCE and state in sessionStorage, then redirects the browser to the hosted /auth/authorize page on the Privana auth origin
  • the hosted auth page signs on the current wallet chain if it is supported, otherwise it switches to the provider networkConfig.chainId
  • the hosted auth page redirects back to your registered callback URL with code / state or error
  • your callback route calls completeLogin() to exchange the code at /auth/token
  • private-read hooks use Authorization: Bearer <access_token> and refresh once through /auth/jwt/refresh on 401

Notes:

  • low-level PrivanaClient.getHostedAuthAuthorizeUrl() still mirrors backend authorize URL support and can build either response mode explicitly.
  • consumer apps must implement a callback route at the exact registered redirect_uri.
  • client_id and exact redirect_uri values must be registered in backend AUTH_CLIENTS.
  • staging end-to-end verification requires that registration on the staging deployment.
  • the standalone localhost popup page used during Firefox debugging was diagnostic only; it is not part of the supported SDK integration path.

Hooks

| Hook | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------ | | useHostedRedirectAuth | Hosted redirect auth for widget apps | | useBalance | Get token balance (available + locked) | | useBatchBalances | Get multiple token balances | | useDeposit | Deposit tokens. One flow per app, run by PrivanaProvider, so a deposit keeps crediting after unmount | | useDepositVerification | Run checkDeposit + status polling against an existing on-chain transfer (used by useDeposit and useFiatOnRamp) | | useFiatOnRamp | MoonPay on-ramp with provider-neutral recovery, verification, credit, and locking (from /on-ramp sub-export) | | useTransakOnRamp | Explicit Transak session launch and hardened iframe over the same shared recovery and credit core | | useWithdraw | Withdraw tokens | | useLockFunds | Lock funds for a recipient | | useUnlockFunds | Unlock expired locks | | useTransfer | Transfer tokens | | useLockedFunds | Get list of locked funds | | useTotalLockedBalance | Get total locked balance for one token | | useHistory | Get authenticated account activity | | usePendingWithdrawals | Get pending withdrawal requests | | useExpiredLocks | Get expired locks that can be claimed | | useTokenList | List all registered tokens | | useTokenInfo | Get info for a single token |

Reacting to credited deposits

A deposit keeps crediting after the deposit modal closes, so DepositModal's onDepositSuccess fires only for deposits credited while it is open. To react to every credit, subscribe once in a component that is always mounted:

useDeposit({
  // With an allowance, the deposit is done once the lock is accepted.
  onCredited: (_txHash, _response, lockPending) => {
    if (!lockPending) refreshBalances()
  },
  onLockSubmitted: () => refreshBalances(),
})

Fiat On-Ramp

The fiat on-ramp lets users buy tokens with a card and have them credited to their Privana balance in one flow. An internal provider-neutral core owns recovery, receipt verification, /deposits/check, credit, and optional post-credit locking. MoonPay and Transak launch and widget behavior live in thin provider-specific adapters.

The SDK product modals select one provider, token, and provider-side asset at the PrivanaProvider boundary:

<PrivanaProvider
  networkConfig={networkConfig}
  onRamp={{
    provider: 'transak',
    tokenId: '0x...',
    providerAssetCode: 'usdc',
  }}
>
  <App />
</PrivanaProvider>

An explicit configuration locks card purchases to that token and fails closed if the provider, token, or asset is invalid. When onRamp is omitted, the product modal preserves its existing MoonPay behavior using networkConfig.moonpayApiKey and the token's MoonPay mapping.

For Transak, the product modal requires a target at least 5% above Privana's on-chain minimum. This margin reduces the risk that provider estimate movement delivers a paid purchase below the amount that Privana can verify.

The entered crypto amount prefills Transak's editable quote. The final received amount may differ.

The provider delivers the purchased token directly to the server-derived Privana deposit address. Provider orders, amounts, events, and webhooks are correlation hints only. One unambiguous matching on-chain transfer, including its exact receipt log index, remains authoritative.

Exported from a separate entry point so consumers who don't use the on-ramp don't pay the bundle cost of @moonpay/moonpay-react:

import { FiatOnRampForm, useFiatOnRamp, useTransakOnRamp } from '@oasisprotocol/privana-sdk/on-ramp'

MoonPay setup

The SDK ships @moonpay/moonpay-react as a regular dependency, so it lands in your node_modules automatically. If you import <MoonPayProvider> directly (see below) you may also want to declare it in your own package.json to keep your dependency surface explicit:

npm install @moonpay/moonpay-react

Wrap your app in <MoonPayProvider> (only on routes that use the on-ramp, to keep MoonPay out of unrelated bundles):

import { MoonPayProvider } from '@moonpay/moonpay-react'
;<MoonPayProvider apiKey={import.meta.env.VITE_MOONPAY_API_KEY} debug={import.meta.env.DEV}>
  {/* on-ramp routes */}
</MoonPayProvider>

Quick start with <FiatOnRampForm>

import { FiatOnRampForm } from '@oasisprotocol/privana-sdk/on-ramp'
;<FiatOnRampForm
  tokenId="0x..." // Privana token id (e.g. USDC on Base)
  currencyCode="usdc_base" // MoonPay currency code; test/live is controlled by the apiKey (pk_test_* → testnet, pk_live_* → mainnet)
  baseCurrencyCode="usd" // optional, defaults to "usd"
  defaultBaseCurrencyAmount="100" // optional pre-fill (MoonPay still lets the user edit)
  onCredited={(txHash) => console.log('credited', txHash)}
  onError={(err) => console.error(err)}
/>

The form:

  • fetches the user's Privana deposit address and passes it to MoonPay as the destination,
  • sets externalCustomerId = address.toLowerCase() so the backend can bind the MoonPay transaction to the SIWE-authenticated user,
  • creates a backend on-ramp intent (POST /onramp/intent) and passes its id to MoonPay as externalTransactionId for exact authenticated recovery,
  • persists at most ten unresolved signed intents under the authenticated user and sends them as repeated externalTransactionId values on pending reads,
  • gates the "Buy" button on the configured token's minimum deposit (input-time check) and double-checks the delivered amount before triggering verification,
  • on MoonPay's transaction_created, fire-and-forget calls POST /onramp/{id} with the MoonPay transaction id so the backend can reconcile both ids during provider reads,
  • treats MoonPay events as wake-up hints, polls the authenticated pending read for the provider transaction and on-chain hash, derives the delivered amount and exact log index from one unambiguous matching receipt log, then triggers Privana verification (checkDeposit + status polling).

useFiatOnRamp for custom UI

If you need a different shell around the MoonPay widget, use the hook directly and wire MoonPay's widget callbacks yourself:

import { MoonPayBuyWidget } from '@moonpay/moonpay-react'
import { useFiatOnRamp } from '@oasisprotocol/privana-sdk/on-ramp'

const {
  status, // 'idle' | 'awaiting-purchase' | 'awaiting-delivery' | 'verifying' | 'credited' | 'failed'
  activeIntentId, // pass to MoonPay as externalTransactionId
  pending, // recovery list (completed-but-unverified)
  error,
  depositAddress, // pass to MoonPay as walletAddress
  minDepositBaseUnits, // for input validation
  selectedToken, // resolved token config (decimals/symbol for the configured tokenId)
  prepareOnRampIntent, // call before opening the widget
  signUrl, // wire to onUrlSignatureRequested
  handleTransactionCreated, // wire to onTransactionCreated
  handleTransactionCompleted, // wire to onTransactionCompleted
  handleWidgetClosed, // call from onClose / onCloseOverlay
  finishPendingVerification, // call from the recovery CTA
  refreshPending, // manual refresh of the pending list
} = useFiatOnRamp({ tokenId, onCredited, onError })

useTransakOnRamp for a Transak checkout

Transak credentials and session creation remain in the backend. Call launch only from an explicit user action; it creates the signed intent and then requests one five-minute, single-use session. The hook keeps the opaque URL in memory and returns a hardened iframe as widget:

import { useTransakOnRamp } from '@oasisprotocol/privana-sdk/on-ramp'

function TransakCheckout({ tokenId }) {
  const {
    widget,
    isLaunching,
    isWidgetOpen,
    launch,
    recreateSession,
    closeWidget,
    pending,
    finishPendingVerification,
  } = useTransakOnRamp({ tokenId, onCredited, onError })

  return (
    <>
      <button
        disabled={isLaunching || isWidgetOpen}
        onClick={() => void launch({ providerAssetCode: 'usdc' }).catch(() => undefined)}
      >
        Buy with card
      </button>
      {widget}
    </>
  )
}

Before each session request, including an authenticated retry or explicit reopen, the SDK sends only the SHA-256 hash of the signed intent to the same-origin /__onramp-ip-attest Worker and passes its fresh opaque claim to the backend. The host must deploy that route on its Transak-approved origin.

recreateSession() retries the current unresolved intent only when its session failed or expired before the provider iframe became interactive. After the provider UI activates or order evidence exists, the adapter refuses to create a second provider order under that intent. Unmount the direct hook to abandon its local checkout UI; durable signed-intent recovery remains available. The adapter uses the backend URL byte-for-byte, preserves the browser Referer, and accepts messages only from the current iframe at the exact documented Transak origin. Widget messages are polling hints only. Authenticated /onramp/pending reads and the matching on-chain ERC-20 transfer remain the recovery and credit path.

Pending / recovery

If the user closes or reloads between launch and verification, the SDK reloads its bounded signed-intent set and combines exact provider lookups with the authenticated derived-wallet lookup. A corrupt local intent is isolated and removed without blocking valid recovery. Render pending with a "Finish verification" CTA that calls finishPendingVerification(record) — no wallet signature is required for deposit verification.

Required backend endpoints

The provider adapters call:

  • POST /v1/accounting/onramp/sign-url — HMAC-signs the MoonPay widget URL
  • POST /v1/accounting/onramp/session — creates a short-lived opaque Transak widget URL
  • POST /v1/accounting/onramp/intent — mints a signed provider/user/wallet/token/chain/asset intent
  • POST /v1/accounting/onramp/{transaction_id} — validates and echoes MoonPay compatibility metadata without becoming order state
  • GET /v1/accounting/onramp/pending — bounded provider reads for completed, strictly admitted transactions awaiting verification

And the existing deposit verification endpoints:

  • POST /v1/accounting/deposits/check
  • GET /v1/accounting/deposits/status/{id}

The MoonPay webhook (POST /v1/accounting/onramp/moonpay/webhook) is an optional verified observability signal. It does not populate order state and is not required for recovery or credit.

License

Apache-2.0