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:

main.tsx
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>;
}
App.tsx
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:

SendButton.tsx
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:

Onboard.tsx
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:

PasskeyUnlock.tsx
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>}
</>
);
}
PasskeyUnlock.tsx
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:

PasskeyUnlock.tsx
const outcome = await create('My App');
localStorage.setItem('passkeyCredentialId', outcome.credentialId);

See Use a passkey for end-to-end onboarding UX.