Wallet lifecycle & auth

WalletState & phases

Before you enable send or receive, the wallet must exist, unlock, and finish syncing with the operator. The Wavelength SDK exposes that readiness at two related layers: a daemon-reported wallet state and a broader runtime phase your UI can render.

WalletState (on WalletInfo) is a lowercase string union the SDK normalizes from the daemon’s numeric wallet-state field:

Value Meaning
'none' No wallet exists yet, or the runtime has not loaded wallet metadata.
'locked' A wallet is present but encrypted; credentials are required.
'syncing' Unlocked and catching up with the operator, indexer, and chain view.
'ready' Unlocked, synced, and safe to spend.

The SDK maps the daemon’s numeric proto enum to these strings at the boundary (walletStateFromProto). The wallet-ready flag is also exposed and is true exactly when wallet state equals 'ready'.

RuntimePhase is the broader lifecycle a host UI renders. It merges runtime boot/shutdown with wallet state, plus one phase the engine owns on its own:

  • Runtime-owned phases: loading, runtimeReady, starting, stopping, stopped, error. The WalletEngine (from @lightninglabs/wavelength-web’s createWebWalletEngine, or the equivalent React Native factory) sets these while the runtime loads, start() runs, or stop() tears down. A failed start() or stop() lands directly on 'error'; there is no separate failed sub-phase to branch on. WavelengthProvider (from @lightninglabs/wavelength-react) owns nothing here: it just publishes whichever engine you hand it. If you are calling wavelength-core/wavelength-web directly without an engine, you own deriving this state machine yourself.
  • Wallet-owned phases (derived by phaseFromInfo() in core): needsWallet, locked, syncing, ready.
  • Engine-owned phase: restoring. Unlike the wallet-owned phases, this one is never derived from WalletInfo; the engine enters it itself whenever restoreWallet() (useWalletRestore()’s restore in React) is called, and leaves it once the restored wallet reports ready. When the call sets recoverState: true, the server-assisted recovery scan is additionally tracked through snapshot.recovery for the lifetime of the scan. See Restore a wallet for the full flow.

After createWallet() or a successful unlock, the daemon often lands in the syncing phase while it replays local state and reconciles with the operator. The WalletEngine automatically advances syncing to ready: while its phase is 'syncing', it polls refresh() every 2000ms until the phase leaves syncing, so you do not call a separate “finish sync” API. If you are integrating wavelength-core/wavelength-web directly without an engine, you must poll getInfo() or subscribe to activity events yourself to detect the syncing-to-ready transition. Show a spinner on 'syncing' (and 'restoring') and enable send/receive controls only on 'ready'.

Use getInfo() / useWalletInfo() for the current wallet state, and phaseFromInfo() when you want a single enum for gating UI across web and future native transports. See the wavelength-core reference for WalletEngine, WalletState, and RuntimePhase definitions.

Password vs passkey

Both paths protect the same underlying wallet seed, but they derive the encryption key differently. Choose one at setup time; after unlock, the lifecycle is the same.

Password unlock is the baseline flow:

  1. createWallet({ password }) generates (or imports) a mnemonic and creates the wallet database with its key material encrypted under the password.
  2. unlockWallet({ password }) opens the wallet database with that password on each session.

The password never leaves your app as plaintext on the wire; the SDK encodes it for the daemon’s unlock RPC. Users must remember the password (and back up the mnemonic if you expose it).

password-flow.ts
// After the runtime reaches 'runtimeReady' and start() has resolved:
await client.createWallet({ password });
// On a later session, unlock the existing wallet instead:
await client.unlockWallet({ password });
// Either call leaves the daemon syncing or ready; read the current phase from getInfo().
const info = await client.getInfo();
const phase = phaseFromInfo(info);
// Render UI from phase: 'syncing' shows a spinner, 'ready' unlocks send/receive.

In React, the same flow runs through useWalletCreate() and useWalletUnlock(), which wrap the calls above with verb-prefixed pending/error/data state (createPending/createError/createData and unlockPending/unlockError/unlockData) and refresh the engine’s snapshot afterward, so phase from useWallet() advances on its own.

Passkey unlock replaces the password with a WebAuthn PRF output:

  1. During setup, a platform PasskeyCeremony derives a deterministic secret (prfOutput) bound to the user’s passkey.
  2. openWalletFromPasskey({ prfOutput }) unlocks (or creates) the wallet using that secret instead of a typed password.
passkey-flow.tsx
import { webPasskeyCeremony } from '@lightninglabs/wavelength-web';
import { useWalletPasskey } from '@lightninglabs/wavelength-react';
function PasskeyOnboard() {
const { create, createPending } = useWalletPasskey(webPasskeyCeremony);
// First run: register a passkey and create the wallet from its PRF output.
const handleCreate = () => create('My Wallet App');
return (
<button onClick={handleCreate} disabled={createPending}>
Create wallet with passkey
</button>
);
}
function PasskeyUnlock({ credentialId }: { credentialId: string }) {
const { open, openPending } = useWalletPasskey(webPasskeyCeremony);
// Returning session: assert the existing passkey to unlock.
const unlock = () => open(credentialId);
return (
<button onClick={unlock} disabled={openPending}>
Unlock with passkey
</button>
);
}

Passkeys tie unlock to a device and biometric or PIN verification. They remove password re-entry but introduce platform constraints (browser support, secure context, cross-origin isolation on web). The Wavelength SDK folds passkey into the engine via an injected passkey ceremony; the core client only sees the PRF bytes.

From the wallet lifecycle perspective both paths converge: after unlock the daemon enters syncing, then ready. A restore (either path) instead passes through the engine-owned 'restoring' phase first; recovery tracking through snapshot.recovery only kicks in when the call sets recoverState: true. Choose password for the simplest cross-device story; choose passkey when you want passwordless return visits on a trusted device. For what the seed and passwords actually are, and how backup and recovery work in each mode, see Keys, backup & recovery.