React
Provider (injected engine)
@lightninglabs/wavelength-react is transport-agnostic: it depends only on
@lightninglabs/wavelength-core and never imports a specific transport. The
provider takes a prebuilt WalletEngine; each transport ships its own
factory to build one (createWebWalletEngine from
@lightninglabs/wavelength-web for the web
transport, createNativeWalletEngine from
@lightninglabs/wavelength-react-native
for React Native).
Build the engine once, typically at module scope outside any component, and
pass it to WavelengthProvider. Set autoStart: true (with config) to boot
the runtime as soon as it is ready, instead of wiring a boot useEffect
yourself:
import { createWebWalletEngine, defaultConfig } from '@lightninglabs/wavelength-web';import { WavelengthProvider } from '@lightninglabs/wavelength-react';
const engine = createWebWalletEngine({ runtimeBaseUrl: 'https://your-host/wavewalletdk/', config: defaultConfig('signet'), autoStart: true,});
export function App() { return <WavelengthProvider engine={engine}>{/* your app */}</WavelengthProvider>;}import { createNativeWalletEngine, defaultConfig } from '@lightninglabs/wavelength-react-native';import { WavelengthProvider } from '@lightninglabs/wavelength-react';
const engine = createNativeWalletEngine({ config: defaultConfig('signet'), autoStart: true,});
export default function App() { return ( <WavelengthProvider engine={engine}> {/* your app */} </WavelengthProvider> );}The engine is built once outside the component tree, so WavelengthProvider
owns nothing beyond publishing it to useSyncExternalStore subscribers: it
never disposes the engine on unmount. Every lifecycle behavior (the sync-poll
while phase === 'syncing', activity-driven balance refresh, the restore
readiness poll, and an unsolicited 'stopped' transition on a runtime crash)
is owned by the WalletEngine itself and works the same whether or not React
is in the picture. If you would rather not use autoStart, call start()
from useWallet() yourself once
phase === 'runtimeReady'.
phase is a RuntimePhase, one of: 'loading', 'runtimeReady',
'starting', 'needsWallet', 'locked', 'syncing', 'restoring',
'ready', 'stopping', 'stopped', or 'error'. The loading/
runtimeReady/starting/stopping/stopped/error phases are owned by the
start/stop flow; needsWallet/locked/syncing/ready are derived from
wallet info; restoring is owned by the engine while a background restore
(recovery scan) is bringing a freshly restored wallet up. See
Handle phases and errors for the full
state machine and how to react to each phase.
The package re-exports every type and enum from core, including
WalletEngine and createWalletEngine, so a React app can import contracts
from one place. It does not re-export any transport’s engine factory;
keep the transport import separate so the same binding runs over any
transport, web or React Native.
Hooks
useWallet() is the application-shell hook: phase, error, start,
stop. It throws outside a provider. Read the rest of the state through the
granular hooks below.
State-reading hooks are plain selectors; each re-renders only when the slice it reads changes:
| Hook | Returns |
|---|---|
useWalletInfo() |
WalletInfo | null |
useWalletBalance() |
Balance | null |
useWalletActivity() |
Entry[] |
useWalletRecovery() |
{ recovery, acknowledge } |
useWalletLogs() |
{ logs, clear } |
Mutation hooks each expose an action plus verb-prefixed
{ <verb>Pending, <verb>Error, <verb>Data, reset<Verb> } fields built on one
shared convention, throw-and-capture: the action both settles its own promise
(resolve with the result, or reject with the failure) and mirrors that outcome
into the pending/error/data fields, so you can await/catch it imperatively
or read the hook’s fields declaratively, whichever fits. Error fields are
always an Error | null, never a string:
| Hook | Returns |
|---|---|
useWalletCreate() |
{ create, createPending, createError, createData, resetCreate } |
useWalletRestore() |
{ restore, restorePending, restoreError, restoreData, resetRestore } |
useWalletUnlock() |
{ unlock, unlockPending, unlockError, unlockData, resetUnlock } |
useWalletDeposit() |
{ deposit, depositPending, depositError, depositData, resetDeposit } |
useWalletReceive() |
{ receive, receivePending, receiveError, receiveData, resetReceive } |
useWalletPrepareSend() |
{ prepare, preparePending, prepareError, prepareData, resetPrepare } |
useWalletSend() |
{ send, sendPrepared, sendPending, sendError, sendData, resetSend } |
useWalletRefresh() |
{ refresh, refreshPending, refreshError, resetRefresh } (no data field: refresh() resolves void) |
Exit hooks cover cooperative and unilateral exits. useWalletExit,
useWalletExitPlan, useWalletSweep, and useWalletList are 1:1 wrappers
around one daemon verb and follow the same throw-and-capture convention as
the mutation hooks above. useWalletExitBatch is the orchestrator: it drives
a multi-outpoint exit through funding-contention guards and additionally
reports progress events. useWalletExitStatus and useWalletExits are read
hooks instead of mutations: they fetch on mount (and on relevant changes) and
expose a refresh* action rather than a reset* one:
| Hook | Returns |
|---|---|
useWalletExit() |
{ exit, exitPending, exitError, exitData, resetExit } |
useWalletExitPlan() |
{ plan, planPending, planError, planData, resetPlan } |
useWalletSweep() |
{ sweep, sweepPending, sweepError, sweepData, resetSweep } |
useWalletExitBatch() |
{ exitBatch, exitBatchPending, exitBatchError, exitBatchData, exitBatchEvents, resetExitBatch } |
useWalletExitStatus(outpoint, opts?) |
{ status, statusPending, statusError, refreshStatus } |
useWalletExits() |
{ summary, summaryPending, summaryError, refreshSummary } |
useWalletList() |
{ list, listPending, listError, listData, resetList } |
useWalletExitStatus defaults to a cheap, coarse phase-only fetch; pass
{ detailed: true } for recovery-tree progress, the CSV countdown, and fees.
Pass { pollMs } to poll while the hook is mounted: polling is opt-in and
runs with no visibility/focus gating, stopping only on unmount. See
Unilateral exit & status for end-to-end usage,
and the wavelength-react reference
for the full hazard notes on exitBatch.
For example, useWalletSend() returns a send(req) action alongside its own
sendPending/sendError state:
import { useWalletSend } from '@lightninglabs/wavelength-react';
function SendButton({ invoice }: { invoice: string }) { const { send, sendPending, sendError } = useWalletSend();
return ( <> <button disabled={sendPending} onClick={() => send({ invoice })}> Send payment </button> {sendError && <p role="alert">{sendError.message}</p>} </> );}See Send a payment, Receive a Lightning payment, Get a deposit address, and Show balance and activity for end-to-end usage of these hooks.
useWalletCreate() and useWalletRestore() are two separate hooks (mirroring
WalletEngine.createWallet and WalletEngine.restoreWallet) rather than one
combined bootstrap hook, so a component only subscribes to the flow it drives:
import { useWalletCreate } from '@lightninglabs/wavelength-react';
function Onboard() { const { create, createPending, createError } = useWalletCreate();
const handleCreate = async () => { const { mnemonic } = await create({ password: 'a-strong-password' }); // Back up `mnemonic` securely before the user sends funds. console.log('Wallet created'); };
return ( <> <button onClick={handleCreate} disabled={createPending}> Create wallet </button> {createError && <p role="alert">{createError.message}</p>} </> );}Once create resolves, phase from useWallet() automatically advances
(through 'syncing' when the daemon needs to catch up) to 'ready', so any
component watching phase updates without extra wiring.
Passkey
Passkeys are driven by useWalletPasskey(ceremony), where ceremony is a
PasskeyCeremony implementation injected from your transport. On web, pass
webPasskeyCeremony from @lightninglabs/wavelength-web; on React Native,
pass createNativePasskeyCeremony({ rpId }) from
@lightninglabs/wavelength-react-native (see
Passkey setup for the
relying-party domain association it requires). create and open track
separately, each with its own pending/error/reset:
import { useWalletPasskey } from '@lightninglabs/wavelength-react';import { webPasskeyCeremony } from '@lightninglabs/wavelength-web';
function PasskeyUnlock() { const { supported, create, createPending, createError, open, openPending, openError, } = useWalletPasskey(webPasskeyCeremony);
if (!supported) { return <p>Passkeys are not available on this device.</p>; }
return ( <> <button disabled={createPending} onClick={() => create('My App')}> Create with passkey </button> <button disabled={openPending} onClick={() => open(/* optional credential id */)}> Unlock with passkey </button> {createError && <p role="alert">{createError.message}</p>} {openError && <p role="alert">{openError.message}</p>} </> );}import { Button, Text } from 'react-native';import { useWalletPasskey } from '@lightninglabs/wavelength-react';import { createNativePasskeyCeremony } from '@lightninglabs/wavelength-react-native';
const ceremony = createNativePasskeyCeremony({ rpId: 'your-app-domain.example' });
function PasskeyUnlock() { const { supported, create, createPending, createError, open, openPending, openError, } = useWalletPasskey(ceremony);
if (!supported) { return <Text>Passkeys are not available on this device.</Text>; }
return ( <> <Button disabled={createPending} onPress={() => create('My App')} title="Create with passkey" /> <Button disabled={openPending} onPress={() => open(/* optional credential id */)} title="Unlock with passkey" /> {createError && <Text role="alert">{createError.message}</Text>} {openError && <Text role="alert">{openError.message}</Text>} </> );}The hook checks ceremony.supportsPasskeyPrf() on mount (platform
authenticator availability). create(appName) runs
registerPasskeyWallet, then opens the wallet from the derived PRF output,
then refreshes engine state. open(credentialId?) asserts an existing
passkey (scoped when an id is passed, discoverable otherwise) and opens the
wallet the same way. On success the engine’s phase advances automatically;
persist the returned credentialId if you want scoped unlocks later.
Unlike the rest of the hooks on this page, create/open are the one place
where a failure can be something other than a real error: dismissing the OS
passkey prompt rejects with a PasskeyCancelledError, which useWalletPasskey
rethrows without recording into createError/openError (a cancelled prompt
is not a failure worth displaying). Every other rejection behaves like the
mutation hooks above: await/catch the call, or read createError/
openError.
Both create and open resolve to a PasskeyWalletOutcome:
{ result, credentialId }. Capture credentialId from a successful result
and store it so a later open(credentialId) call can perform a scoped unlock
instead of a discoverable one:
const outcome = await create('My App');localStorage.setItem('passkeyCredentialId', outcome.credentialId);See Use a passkey for end-to-end onboarding UX.