Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@qubic.org/react

React hooks and context providers for the Qubic SDK, powered by TanStack Query.

Installation

bun add @qubic.org/react

Peer dependency: React 18+.

Security model

Seeds never appear in React state or component code:

  • VaultProvider stores wallets in a Ref — invisible to React DevTools, not serialisable, not part of the render cycle.
  • Components only receive identities: Identity[] (60-char strings).
  • useSendTransfer / useSendContractCall access signing internally — the component passes params but never touches a wallet.
  • Auto-lock on page hide (visibilitychange) and after a configurable idle timeout.

Session persistence (persistSession prop):

  • Seeds re-encrypted with a freshly generated AES-256-GCM key on every unlock.
  • The key is stored in IndexedDB as extractable: false — JS cannot read key bytes even under XSS.
  • The encrypted payload lives in sessionStorage (tab-scoped — cleared automatically when the tab closes).
  • Keys rotate on every restore: the previous IDB key is deleted before wallets are loaded (forward secrecy).
  • If decryption fails for any reason the session is silently cleared and the lock form is shown.

Setup

import { createLiveClient, createQueryClient } from '@qubic.org/rpc'
import { importVault } from '@qubic.org/wallet'
import { QubicProvider, VaultProvider } from '@qubic.org/react'

const live = createLiveClient()
const archive = createQueryClient()
const vault = importVault(localStorage.getItem('qubic-vault')!)

export function App() {
  return (
    <QubicProvider liveClient={live} archiveClient={archive}>
      <VaultProvider
        vaultData={vault}
        persistSession        // survives page refresh; expires on tab close
        autoLockMs={15 * 60_000}  // also auto-locks after 15 min idle
      >
        <YourApp />
      </VaultProvider>
    </QubicProvider>
  )
}
Prop Default Description
vaultData required VaultData from importVault / createVault
persistSession false Re-encrypt seeds into sessionStorage + IDB on unlock; restore on page refresh
sessionStorageKey 'qubic-session' Key used in sessionStorage — override for multiple vaults
autoLockMs none Auto-lock after N ms idle; also locks on visibilitychange

QubicProvider also acts as a TanStack QueryClientProvider. Pass your own queryClient to share the cache with the rest of your app.

Vault hooks

useVaultState()

Lock/unlock the vault and read the active identities.

import { useVaultState } from '@qubic.org/react'

function WalletPanel() {
  const { isUnlocked, identities, isUnlocking, error, unlock, lock } = useVaultState()

  if (!isUnlocked) {
    return (
      <form onSubmit={e => { e.preventDefault(); unlock(e.currentTarget.password.value) }}>
        <input name="password" type="password" />
        <button disabled={isUnlocking}>Unlock</button>
        {error && <p>Wrong password</p>}
      </form>
    )
  }

  return (
    <div>
      {identities.map(id => <p key={id}>{id}</p>)}
      <button onClick={lock}>Lock</button>
    </div>
  )
}

Send hooks

useSendTransfer()

Fetches the current tick, builds and signs a QU transfer, broadcasts. Zero wallet exposure.

import { useSendTransfer, useVaultState } from '@qubic.org/react'

function SendPanel() {
  const { identities } = useVaultState()
  const { mutate: send, isPending, data, error } = useSendTransfer()

  return (
    <button
      disabled={isPending || !identities[0]}
      onClick={() => send({
        from: identities[0]!,
        destination: 'BZBQFLLBNCXEMGLOBHUVFTLUPLVCPQUASSILFABOFFBCADQSSUPNWLZBQEXK',
        amount: 1_000_000n,
      })}
    >
      Send
    </button>
  )
}

useSendContractCall()

Signs and broadcasts a smart contract procedure call.

import { useSendContractCall, useVaultState } from '@qubic.org/react'
import { contractIndexToIdentity } from '@qubic.org/crypto'
import { qearn } from '@qubic.org/contracts'

function UnlockPanel() {
  const { identities } = useVaultState()
  const { mutate: send, isPending } = useSendContractCall()
  const call = qearn.buildUnlockInput({ amount: 10_000_000n, lockedEpoch: 212 })

  return (
    <button
      disabled={isPending}
      onClick={() => send({
        from: identities[0]!,
        inputType: call.inputType,
        payload: call.payload,
        destination: contractIndexToIdentity(qearn.contractIndex),
      })}
    >
      Unlock QU
    </button>
  )
}

Contract query hook

useContractQuery(fn, input)

Wraps any typed contract read function from @qubic.org/contracts as a TanStack Query. identityToPublicKey / publicKeyToIdentity are provided automatically.

import { useContractQuery } from '@qubic.org/react'
import { qearnGetStateOfRound } from '@qubic.org/contracts'

function RoundStats({ epoch }: { epoch: number }) {
  const { data, isLoading } = useContractQuery(qearnGetStateOfRound, { epoch })

  if (isLoading) return <span>Loading…</span>
  return <span>Locked: {data?.totalLockedAmount?.toString()}</span>
}

Live API hooks

useTickInfo(refetchIntervalMs?)

const { data } = useTickInfo(3_000)
// data.tick, data.epoch, data.duration

useBalance(identity)

const { data } = useBalance(identities[0])
// data.balance

useIssuedAssets(identity) / useOwnedAssets(identity) / usePossessedAssets(identity)

const { data: issued } = useIssuedAssets(identity)
const { data: owned } = useOwnedAssets(identity)
const { data: possessed } = usePossessedAssets(identity)

Archive API hooks (require archiveClient)

useTransaction(hash)

const { data } = useTransaction(txHash)

useEventLogs(request)

const { data } = useEventLogs({
  filters: { logType: '0' },
  pagination: { offset: 0, size: 20 },
})

useTransactionsForIdentity(request)

const { data } = useTransactionsForIdentity({
  addressId: identity,
  pagination: { offset: 0, size: 20 },
})

useQubic()

Direct access to liveClient and archiveClient for one-off queries.

const { liveClient, archiveClient } = useQubic()

Sharing your TanStack Query cache

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()

<QubicProvider liveClient={live} queryClient={queryClient}>
  <App />
</QubicProvider>

Wallet connectors (extension / MetaMask Snap)

Setup — WalletProvider

Register your wallet backends once and wrap your app. The primary runnable example below uses the browser extension and MetaMask Snap only. Components call useWallet() with no arguments — no connector plumbing in every component.

import { createLiveClient, createQueryClient } from '@qubic.org/rpc'
import {
  QubicProvider,
  WalletProvider,
  extensionConnector,
  createMetaMaskSnapConnector,
} from '@qubic.org/react'

// Create connectors outside the component tree (stable references)
const live = createLiveClient()
const archive = createQueryClient()

const connectors = [
  extensionConnector,
  createMetaMaskSnapConnector({
    broadcast: async (signedBase64) => {
      const result = await live.broadcastTransaction(signedBase64)
      if (!result.ok) throw result.error
      return { networkTxId: result.value.transactionId ?? '', broadcast: result.value }
    },
  }),
]

export function App() {
  return (
    <QubicProvider liveClient={live} archiveClient={archive}>
      <WalletProvider connectors={connectors}>
        <YourApp />
      </WalletProvider>
    </QubicProvider>
  )
}
Prop Default Description
connectors required List of WalletConnector instances
storageKey 'qubic-active-connector' localStorage key for persisting the active connector

useWallet()

Single hook for all wallet state. Works with any connector registered in WalletProvider.

import { useWallet } from '@qubic.org/react'

function ConnectModal() {
  const { connectors, isConnected, isConnecting, account, activeConnector, connect, disconnect, error } = useWallet()

  if (isConnected) {
    return (
      <div>
        <p>{account?.name ?? account?.identity}</p>
        <p>via {activeConnector?.id}</p>
        <button onClick={disconnect}>Disconnect</button>
      </div>
    )
  }

  return (
    <div>
      {connectors.filter(c => c.isAvailable()).map(c => (
        <button
          key={c.id}
          disabled={isConnecting}
          onClick={() => connect(c.id)}
        >
          {c.id}
        </button>
      ))}
      {error && <p>{error.message}</p>}
    </div>
  )
}

Signing from any component

function SendPanel() {
  const { isConnected, sendTransaction, signMessage } = useWallet()

  const handleSend = async () => {
    const result = await sendTransaction({
      destination: 'BZBQFLLBNCXEMGLOBHUVFTLUPLVCPQUASSILFABOFFBCADQSSUPNWLZBQEXK',
      amount: '1000',
    })
    console.log(result.networkTxId)
  }

  const handleSign = async () => {
    const result = await signMessage('hello qubic')
    console.log(result.signatureHex)
  }
}

Custom connector

Implement WalletConnector to add any other wallet backend:

import type { WalletConnector } from '@qubic.org/react'

export const myConnector: WalletConnector = {
  id: 'my-wallet',
  isAvailable: () => true,
  connect: async () => ({ identity: '…' }),
  getAccount: async () => null,
  disconnect: async () => {},
  sendTransaction: async (_tx) => { throw new Error('not implemented') },
  signTransaction: async (_tx) => { throw new Error('not implemented') },
  signMessage: async (_msg) => { throw new Error('not implemented') },
  on: (_event, _cb) => () => {},
}

Lower-level: useWalletConnector(connector) (no provider)

When you only need a single connector and don't want a provider:

import { extensionConnector, useWalletConnector } from '@qubic.org/react'

function ConnectButton() {
  const { isAvailable, isConnected, account, connect, disconnect } =
    useWalletConnector(extensionConnector)

  if (!isAvailable) return <p>Install the Qubic extension</p>
  if (!isConnected) return <button onClick={() => connect()}>Connect</button>
  return <button onClick={disconnect}>{account?.identity}</button>
}

See also