---
title: "Wallet lifecycle & auth"
description: "The phases a Wavelength wallet moves through from creation to unlock, and how password-based and passkey-based authentication differ."
canonical: https://wavelength.lightning.engineering/concepts/wallet-lifecycle-and-auth/
---

> Docs index: https://wavelength.lightning.engineering/llms.txt

# 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](/guides/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'`.

> **Caution**
>
> Treat `'none'` and `'locked'` as non-spendable. Do not infer readiness from a successful unlock alone; wait for `'ready'` (or `walletReady === true`) before exposing payment actions.

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](/reference/wavelength-core/) 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).

```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.

```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.

> **Note**
>
> WebAuthn registration, PRF extension availability, and credential persistence are platform mechanics. See [Use a passkey](/guides/use-a-passkey/) for browser-specific steps; this page is the shared mental model.

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](/concepts/keys-backup-and-recovery/).
