Skip to content

hazbase/react

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@hazbase/react

npm version License

Overview

@hazbase/react is a React toolkit for two frontend patterns:

  • standard injected wallet apps with WalletProvider
  • passkey-native hazBase smart-wallet apps with PasskeyAccountProvider

The passkey flow is intentionally flow-oriented. Instead of wiring every backend step yourself, you create one client and then use helpers like ensurePasskey(), ensureAccount(), ensureSession(), ensureLiveSession(), sponsorAndSend(), and sponsorAndSendExecute().

Requirements

  • Node.js >= 18
  • React >= 18
  • Ethers v6
  • A hazBase-compatible backend for the passkey flow
  • WebAuthn support in the browser when you use passkeys

Installation

pnpm add @hazbase/react @hazbase/auth @hazbase/kit ethers react
# or
npm i @hazbase/react @hazbase/auth @hazbase/kit ethers react

Usage patterns

@hazbase/react supports two main frontend patterns:

  • WalletProvider: MetaMask / injected wallet apps
  • PasskeyAccountProvider: email OTP + passkey + account bootstrap + sponsored action apps
  • HazbaseX402Provider: token-agnostic x402 payment helpers for merchant pages

Use WalletProvider when your app should behave like a normal wallet-connected dApp. Use PasskeyAccountProvider when your app should guide users through a hazBase-managed smart-wallet flow.

Quick start: WalletProvider (MetaMask / injected wallet)

import {
  WalletProvider,
  useAddress,
  useNetwork,
  useSigner,
} from '@hazbase/react';

function WalletPanel() {
  const { signer, connectMetaMask, disconnect } = useSigner();
  const { address, isConnected } = useAddress();
  const network = useNetwork();

  return (
    <div>
      <button onClick={() => connectMetaMask()}>Connect MetaMask</button>
      <button onClick={() => disconnect()}>Disconnect</button>
      <pre>
        {JSON.stringify(
          {
            isConnected,
            address,
            chainId: network.chainId,
            hasSigner: Boolean(signer),
          },
          null,
          2,
        )}
      </pre>
    </div>
  );
}

export function App() {
  return (
    <WalletProvider autoConnect>
      <WalletPanel />
    </WalletProvider>
  );
}

Quick start: PasskeyAccountProvider

import {
  PasskeyAccountProvider,
  createHazbasePasskeyClient,
  usePasskeyAccount,
} from '@hazbase/react';

const client = createHazbasePasskeyClient();

function PasskeyPanel() {
  const {
    sendOtp,
    verifyOtp,
    ensurePasskey,
    ensureSession,
    ensureAccount,
    sponsorAndSendExecute,
  } = usePasskeyAccount();

  async function runFlow() {
    await sendOtp({ email: 'demo@example.com' });
    await verifyOtp({ email: 'demo@example.com', code: '123456' });
    await ensurePasskey({ deviceLabel: 'Chrome on MacBook' });
    const account = await ensureAccount({ chainId: 11155111, accountSalt: 'user-owned-account' });
    await ensureSession({ actionProfileKey: 'first_party_l2' });

    const sent = await sponsorAndSendExecute({
      mode: 'session',
      nonce: '0',
      target: '0x1111111111111111111111111111111111111111',
      data: '0x12345678',
      value: '0',
      callGasLimit: '150000',
      verificationGasLimit: '120000',
      preVerificationGas: '50000',
      maxFeePerGas: '1000000000',
      maxPriorityFeePerGas: '100000000',
    });

    console.log(account.smartAccountAddress, sent.userOpHash, sent.transactionHash);
  }

  return <button onClick={runFlow}>Run passkey account flow</button>;
}

export function App() {
  return (
    <PasskeyAccountProvider
      client={client}
      defaultChainId={11155111}
      defaultAccountSalt="user-owned-account"
      defaultActionProfileKey="first_party_l2"
    >
      <PasskeyPanel />
    </PasskeyAccountProvider>
  );
}

Quick start: usePasskeyOnboarding

import {
  PasskeyAccountProvider,
  createHazbasePasskeyClient,
  usePasskeyOnboarding,
} from '@hazbase/react';

const client = createHazbasePasskeyClient();

function OnboardingPanel() {
  const { sendOtp, completeOnboarding, isAccountReady, smartAccountAddress } = usePasskeyOnboarding();

  async function onboard() {
    await sendOtp({ email: 'demo@example.com' });
    await completeOnboarding({
      email: 'demo@example.com',
      code: '123456',
      deviceLabel: 'Chrome on MacBook',
      chainId: 11155111,
      accountSalt: 'user-owned-account',
    });
  }

  return (
    <div>
      <button onClick={onboard}>Bootstrap account</button>
      {isAccountReady ? <pre>{smartAccountAddress}</pre> : null}
    </div>
  );
}

export function App() {
  return (
    <PasskeyAccountProvider client={client} defaultChainId={11155111}>
      <OnboardingPanel />
    </PasskeyAccountProvider>
  );
}

Common operations

Standard wallet hooks

  • useSigner()
  • useAddress()
  • useNetwork()
  • useHazbaseWalletClient()
  • useTokenBalance()
  • useWalletActivity()

Passkey flow hooks

The main passkey hook is usePasskeyAccount().

High-level helpers:

  • sendOtp()
  • verifyOtp()
  • ensurePasskey()
  • ensureHighTrust()
  • ensureAccount()
  • ensureSession()
  • grantSession()
  • ensureLiveSession()
  • sponsorUserOp()
  • executeSessionDirect()
  • executeSessionDirectExecute()
  • executeSessionDirectExecuteBatch()
  • sponsorAndSend()
  • sponsorAndSendExecute()
  • sponsorAndSendExecuteBatch()
  • authorizeOwnerUserOp()
  • refreshAccount()
  • endSession()
  • signOut()

Onboarding-focused helper:

  • usePasskeyOnboarding()

Account security helper:

  • useAccountSecurity()

x402 hooks:

  • HazbaseX402Provider
  • useX402Client()
  • useX402ExtensionBridge()
  • useX402Requirement()
  • useX402WalletHandoff()
  • useX402Settlement()

Wallet address link helpers:

  • requestWalletAddress()
  • createWalletAddressUrl()
  • readWalletAddressFromUrl()
  • useWalletAddressLink()

Wallet API hooks:

  • useHazbaseWalletClient()
  • useTokenBalance()
  • useWalletActivity()

x402 components:

  • X402Paywall
  • X402RequirementScript

Execute helpers:

  • encodeSmartAccountExecute()
  • encodeSmartAccountExecuteBatch()
  • createExecuteUserOp()
  • createExecuteBatchUserOp()

x402 pure helpers:

  • createHazbaseX402Client()
  • createX402WalletUrl()
  • readX402PaymentFromUrl()
  • parseX402Payload()
  • serializeX402ScriptTag()
  • createX402BridgeRequest()
  • postX402BridgeRequest()
  • listenForX402BridgeMessages()
  • requestWalletAddress()
  • createWalletAddressUrl()
  • readWalletAddressFromUrl()
  • normalizeWalletAddress()
  • shortWalletAddress()

Design notes:

Advanced escape hatch:

  • raw: access to the low-level client for custom integrations

Main exports

  • WalletProvider
  • PasskeyAccountProvider
  • createHazbasePasskeyClient
  • useSigner
  • useAddress
  • useNetwork
  • usePasskeyAccount
  • usePasskeyOnboarding
  • useAccountSecurity
  • encodeSmartAccountExecute
  • encodeSmartAccountExecuteBatch
  • createExecuteUserOp
  • createExecuteBatchUserOp
  • createHazbaseX402Client
  • createX402WalletUrl
  • readX402PaymentFromUrl
  • parseX402Payload
  • serializeX402ScriptTag
  • createX402BridgeRequest
  • postX402BridgeRequest
  • listenForX402BridgeMessages
  • requestWalletAddress
  • createWalletAddressUrl
  • readWalletAddressFromUrl
  • useWalletAddressLink
  • X402Paywall
  • X402RequirementScript

Wallet address link helpers

Apps that only need to identify the user's hazBase wallet address can use the wallet address link helpers instead of building their own postMessage bridge. This is useful for game inventories, reward pages, gated dashboards, or other services that display off-chain or on-chain holdings for the connected wallet. Non-React pages can import these helpers from @hazbase/react/wallet to avoid pulling the React-facing surface into their app bundle.

import {
  requestWalletAddress,
  readWalletAddressFromUrl,
} from '@hazbase/react/wallet';

const returned = readWalletAddressFromUrl(window.location.href);
if (returned) {
  console.log('Linked wallet:', returned.address);
}

const result = await requestWalletAddress({
  walletUrl: 'https://wallet.example/pwa/',
  returnUrl: window.location.href,
  purpose: 'card_holdings',
  fallbackToPwa: true,
});

if (result.ok) {
  console.log('Linked wallet:', result.address);
}

requestWalletAddress() first asks a compatible extension through the generic hazbase:wallet:address-request bridge. If no extension answers, it can redirect to the configured PWA with walletAddressReturnUrl. The URL reader accepts generic params such as walletAddress and hazbaseWalletAddress.

React apps can use the stateful hook:

import { useWalletAddressLink } from '@hazbase/react';

function CardInventoryLink() {
  const wallet = useWalletAddressLink({
    walletUrl: 'https://wallet.example/pwa/',
    returnUrl: window.location.href,
    storageKey: 'my-app.wallet-address',
    purpose: 'card_holdings',
  });

  return wallet.isConnected ? (
    <button type="button" onClick={wallet.clear}>{wallet.address}</button>
  ) : (
    <button type="button" onClick={() => wallet.connect()}>Connect wallet</button>
  );
}

Wallet API hooks

Use these hooks when a React app needs hazBase-hosted wallet data without building its own fetch layer. They use https://api.hazbase.com by default; pass apiEndpoint only for local, staging, or self-hosted APIs.

import { useTokenBalance, useWalletActivity } from '@hazbase/react';

function WalletSummary({ account }: { account: string }) {
  const balance = useTokenBalance({
    chainId: 11155111,
    token: 'example-token',
    account,
  });

  const activity = useWalletActivity({
    chainId: 11155111,
    token: 'example-token',
    account,
    limit: 10,
  });

  return (
    <section>
      <p>{balance.balance?.formatted ?? '0'} {balance.balance?.symbol ?? ''}</p>
      <ul>
        {activity.activities.map((item) => (
          <li key={item.id}>{item.direction}: {item.amount.formatted}</li>
        ))}
      </ul>
    </section>
  );
}

x402 pure helpers

The x402 helpers are token-agnostic. Apps pass network, asset, priceAtomic, payoutMethod, and wallet URLs as configuration instead of using token-specific SDK methods. createHazbaseX402Client() also uses https://api.hazbase.com by default.

import {
  createHazbaseX402Client,
  createX402WalletUrl,
  readX402PaymentFromUrl,
  serializeX402ScriptTag,
} from '@hazbase/react';

const x402Client = createHazbaseX402Client();

const requirement = await x402Client.createRequirement({
  resourceId: 'member-page-v1',
  resourceUrl: 'https://example.com/member-page',
  description: 'Unlock member page',
  network: 'sepolia',
  asset: 'example-token',
  priceAtomic: '100000000000000000',
  payoutMethod: {
    kind: 'external_eoa',
    address: '0x1234567890AbcdEF1234567890aBcdef12345678',
  },
});

const walletUrl = createX402WalletUrl({
  walletUrl: 'https://wallet.example/pwa/',
  x402: requirement.x402,
  sourceUrl: 'https://example.com/member-page',
  title: 'Member page',
  completion: 'fragment',
});

const scriptTag = serializeX402ScriptTag(requirement.x402);
const returned = readX402PaymentFromUrl(window.location.href);
if (returned) {
  await x402Client.settlePayment({
    paymentRequestId: requirement.paymentRequestId,
    xPayment: returned.xPayment,
  });
}

x402 React hooks

Use HazbaseX402Provider when a React page needs shared x402 client config or a default wallet URL.

import {
  HazbaseX402Provider,
  useX402Settlement,
  useX402WalletHandoff,
} from '@hazbase/react';

function Paywall({ requirement }) {
  const handoff = useX402WalletHandoff({
    x402: requirement.x402,
    sourceUrl: window.location.href,
    title: document.title,
    completion: 'fragment',
  });

  const settlement = useX402Settlement({
    paymentRequestId: requirement.paymentRequestId,
    autoReadUrl: true,
    onSettled() {
      // App unlocks content or asks its backend to create an access grant.
    },
  });

  if (settlement.status === 'settled') return <article>Unlocked</article>;

  return (
    <a href={handoff.walletUrl ?? '#'} onClick={handoff.openWallet}>
      Pay to unlock
    </a>
  );
}

export function App({ requirement }) {
  return (
    <HazbaseX402Provider
      walletUrl="https://wallet.example/pwa/"
      completionParams={['xPayment', 'merchantXPayment']}
    >
      <Paywall requirement={requirement} />
    </HazbaseX402Provider>
  );
}

useX402Requirement() is available for demos and static-price pages, but production merchant pages should usually create requirements on their own backend and pass the returned payload into the paywall UI.

x402 render-prop component

X402Paywall wraps the handoff and settlement hooks while leaving the UI to the application.

import { HazbaseX402Provider, X402Paywall } from '@hazbase/react';

function ArticleGate({ requirement }) {
  return (
    <X402Paywall
      requirement={requirement}
      sourceUrl={window.location.href}
      title={document.title}
      completionParam="xPayment"
    >
      {({ status, walletUrl, openWallet, error }) => {
        if (status === 'settled') return <article>Unlocked</article>;
        return (
          <section>
            <a href={walletUrl ?? '#'} onClick={openWallet}>
              Pay to unlock
            </a>
            {error ? <p>{error.message}</p> : null}
          </section>
        );
      }}
    </X402Paywall>
  );
}

export function App({ requirement }) {
  return (
    <HazbaseX402Provider walletUrl="https://wallet.example/pwa/">
      <ArticleGate requirement={requirement} />
    </HazbaseX402Provider>
  );
}

By default, X402Paywall also renders an application/x-x402+json script tag for extension detection. Set renderRequirementScript={false} if the page renders its own script tag.

x402 extension bridge

Extension-enabled merchant pages can announce a payment request through a small postMessage bridge. Wallet extensions decide their own domain allowlist, side-panel behavior, and signing flow. Call announce() from a user action so browser extension surfaces such as Chrome Side Panel can open reliably.

import { useX402ExtensionBridge } from '@hazbase/react';

function ExtensionAwarePaywall({ requirement }) {
  const bridge = useX402ExtensionBridge({
    x402: requirement.x402,
    paymentRequestId: requirement.paymentRequestId,
    sourceUrl: window.location.href,
    title: document.title,
    completion: { mode: 'fragment', param: 'xPayment' },
    allowedOrigins: [window.location.origin],
    onPayment(message) {
      // Apps can settle message.xPayment, or keep URL fragment completion as
      // the primary flow and use this as an extension-only enhancement.
    },
  });

  return (
    <button type="button" onClick={bridge.announce}>
      Pay with wallet extension
    </button>
  );
}

The bridge is token-agnostic. It carries an x402 payload and generic wallet capabilities, but does not include private keys, passkey assertions, or token-specific policy.

Notes

  • PasskeyAccountProvider assumes a first-party or allowlisted partner backend.
  • @hazbase/react stays backend-contract based: apps integrate against a backend URL, and the React surface does not depend on any specific infrastructure provider.
  • The same React integration should work with any hazBase-compatible backend as long as the backend API contract is preserved.
  • sendOtp() and verifyOtp() manage the application session, not wallet ownership by themselves.
  • ensureAccount() will reuse an existing bound smart account when possible and bootstrap only when needed.
  • New embedded sessions always require a fresh purpose=session passkey step-up. Existing active sessions are reused until revoked or expired.
  • Backends that expose the V2 bundler path return additive fields such as accountVariant, relayMode, and submittedUserOpHash; existing callers can ignore them safely.
  • Session mode is sponsor-required. The backend returns the final accountSignature for the sponsored payload, and the React layer forwards it to the bundler.
  • Embedded sessions use a snapshot of the action profile taken at issuance time. Later profile broadening does not widen already-issued sessions.
  • Profile deactivation still acts as a kill switch for active sessions.
  • raw is useful when you want to override one step without giving up the higher-level flow state.
  • useAccountSecurity() wraps device/session inventory plus reauth-gated revoke flows for first-party security settings screens.

Troubleshooting (FAQ)

MetaMask is not found

Make sure the browser has an injected wallet and that your app is running in a context where window.ethereum is available.

ensureSession() says that actionProfileKey is required

Pass an actionProfileKey directly, or set defaultActionProfileKey on PasskeyAccountProvider.

How do I build a device/session security screen?

Use useAccountSecurity() to list active devices and embedded sessions, then call revokeDevice() or revokeSession() when the user confirms a revoke. The hook triggers a fresh passkey reauth before destructive operations.

Why does passkey setup still need OTP?

The intended model is:

  • OTP starts the app session
  • passkey binds the device and handles step-up authentication
  • account bootstrap and sponsorship happen only after those steps succeed

Why does a new embedded session always ask for passkey step-up?

Session issuance is treated as a privileged action. A fresh purpose=session high-trust token is required whenever the provider needs to mint a new embedded session.

When should I use sponsorAndSendExecute() instead of sponsorAndSend()?

Use sponsorAndSendExecute() when you want the React layer to build SmartAccount.execute(...) callData for you. Use sponsorAndSend() when you already have a full userOp draft.

Low-level account security methods

PasskeyAccountProvider does not add first-class settings UI in this phase, but the underlying client exposed as raw includes backend-first account security methods:

  • raw.listPasskeyDevices()
  • raw.revokePasskeyDevice()
  • raw.listEmbeddedSessions()
  • raw.revokeEmbeddedSession()

Listing uses app-session auth. Revoke calls require a fresh purpose=reauth passkey step-up token. Device revoke cascades to active embedded sessions on that device.


Security: recommended overrides

Some transitive dependencies of this package carry known advisories with no upstream fix yet. Because npm ignores overrides declared inside a dependency, the only way to protect your own dependency tree is to add them to your application's package.json and reinstall:

{
  "overrides": {
    "ws": "^8.21.0",
    "bfj": "^9.1.3"
  }
}

(yarn: use resolutions; pnpm: use pnpm.overrides.)

  • ws ^8.21.0 — avoids a ws advisory in the version ethers currently pins.
  • bfj ^9.1.3 — removes the snarkjs → bfj → jsonpath chain (prototype-pollution / code-injection advisories) pulled in via @hazbase/auth / @hazbase/kit.

These are workarounds until ethers / snarkjs ship fixed transitive ranges upstream.


License

Apache-2.0

About

React wallet utilities built on ethers v6—supports MetaMask, WalletConnect, and guest keys with WalletProvider.

Resources

License

Contributing

Stars

1 star

Watchers

0 watching

Forks

Contributors