Source: https://wavelength.lightning.engineering/introduction/what-is-wavelength-sdk.md # What is the Wavelength SDK? ## What it is The Wavelength SDK is a TypeScript library for embedding a **self-custodial Bitcoin, Lightning, and Ark wallet** directly in your web or mobile app. Your users hold their own keys. You do not run a wallet server on their behalf. (Building a pure-native Kotlin or Swift app? The same embedded wallet ships as a [native mobile SDK](/native-ios-android/overview/) too.) The full wallet daemon ([wavelength](https://github.com/lightninglabs/wavelength)) is compiled to run **inside your app**: to WebAssembly in the browser on web, and into the app binary on mobile. There is no separate process, no local socket, and no backend you must operate to host the wallet logic. Your app boots the daemon and talks to it through a typed client. The Wavelength SDK ships as these packages: Package `@lightninglabs/wavelength-core` Role Shared types, the WavelengthClient interface, errors, and enums. No DOM, no transport. Package `@lightninglabs/wavelength-web` Role The browser WASM transport. Framework-agnostic: use it from vanilla JS, Vue, Svelte, or React. Re-exports core. Package `@lightninglabs/wavelength-react-native` Role The React Native transport. The daemon is compiled into the app binary. Re-exports core. Package `@lightninglabs/wavelength-react` Role React provider and hooks. Depends on core only and takes an injected client, so the same binding can run over the web or React Native transport. **React is the primary developer experience**, but React is not required. On web, if you are not using React, install `@lightninglabs/wavelength-web`, call `createWebClient()`, and use the `WavelengthClient` methods directly. The React package is a thin binding over the same interface. Native iOS and Android apps are covered as well: the [wavelength-mobile](https://github.com/lightninglabs/wavelength-mobile) wrappers embed the same daemon behind idiomatic Kotlin and Swift APIs, documented in the [Native iOS & Android](/native-ios-android/overview/) section. ## Mental model Think of the Wavelength SDK as **your app talking to a local wallet daemon**, not your app talking to a remote wallet API. Your app WavelengthClienttyped API Embedded daemonin your app GatewaysArk · Swap · Esplora 1. **Your app** calls methods like `balance()`, `send()`, and `deposit()` on a `WavelengthClient`. 2. **The client** forwards those calls to the embedded daemon running in your app (on web, in a Web Worker by default, or the main thread if configured). 3. **The daemon** holds keys, tracks VTXOs, coordinates Ark rounds, and reaches out to public infrastructure when it needs chain data, mailbox relay, or Lightning swaps. The daemon connects to three backend services configured in `RuntimeConfig`: - **Ark operator + mailbox** (`arkServerAddress`) for rounds and VTXO/mailbox relay - **Swap server** (`swapServerAddress`) for Lightning↔Ark atomic swaps - **Esplora** (`walletEsploraUrl`) for chain and UTXO queries On web, the Ark and swap addresses are REST URLs. On React Native, they are `host:port` gRPC addresses. `walletEsploraUrl` is an HTTP Esplora endpoint on both platforms. Your app never calls those gateways directly. It only talks to the local daemon. **User keys never leave the device.** Seed generation, signing, and wallet state live on the device (OPFS in the browser; the app data directory on React Native). The gateways see protocol traffic, not your users’ private keys. A minimal boot sequence looks like this: ```ts import { createWebClient, defaultConfig } from '@lightninglabs/wavelength-web'; const client = createWebClient({ runtimeBaseUrl: '/wavewalletdk/' }); await client.ready(); // load WASM runtime await client.start(defaultConfig('signet')); // boot daemon, connect gateways const { mnemonic } = await client.createWallet({ password: '…' }); // show mnemonic to the user for backup ``` ```ts import { createNativeClient, defaultConfig } from '@lightninglabs/wavelength-react-native'; const client = createNativeClient(); await client.ready(); // ready the native runtime await client.start(defaultConfig('signet')); // boot daemon, connect gateways const { mnemonic } = await client.createWallet({ password: '…' }); // show mnemonic to the user for backup ``` From here, use the same client for deposits, Lightning receive, sends, and activity. The [System architecture](/introduction/system-architecture/) page explains how those operations map to Ark, swaps, and on-chain exits. --- Source: https://wavelength.lightning.engineering/introduction/the-wavelength-system.md # The Wavelength system ## What Wavelength is Wavelength adds self-custodial Bitcoin and Lightning payments to any app with a handful of API calls. Your users get a real wallet that can send and receive over the Lightning Network, and you never run a Lightning node, open channels, or source liquidity to make it work. If you can call an API, you can accept Bitcoin. (Stablecoin support is on the way through Taproot Assets, over the same wallet surface.) The value is in what you do **not** have to run. Accepting Lightning payments has traditionally meant operating a node, balancing channels, sourcing liquidity, and keeping all of that healthy around the clock. Wavelength runs that machinery and gives you a small, friendly wallet surface in its place, so the capability (instant, global, low-fee payments) is available without the operational burden. > **This does not replace running your own node** > > Lightning has always offered a fully self-sovereign path where you run your own node and channels, and that path is not going anywhere. Wavelength is for everyone who would rather not run infrastructure at all, without giving up control of their funds. ## What you run vs. what is managed You run **Wavelength**, the self-custodial client (compiled from [wavelength](https://github.com/lightninglabs/wavelength)). It holds the user’s keys, tracks their balance, and builds and signs their payments. Everything the wallet talks to is managed for you. Your app wallet API Wavelengththe client you run · holds keys, signs paymentswebnativeserverMCP exit BOLT 11 Bitcoin on-chainyours anytime, no permission Wavelength Operatormanaged for you · coordination + settlementdeep liquidity via Loop Lightning Network | Piece | Who runs it | What it does | | ----------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Wavelength** (the client) | You | Holds keys, tracks balances, builds and signs payments. Runs as an embedded SDK in a web or mobile app, as a standalone server, or as an MCP server for agents. | | **Coordination + settlement** | Wavelength Operator | An Ark-like layer that batches off-chain transfers instantly and cheaply. It settles between users but never takes unilateral control of anyone’s funds. | | **Liquidity via Loop** | Wavelength Operator | Deep Lightning liquidity through Loop, so payments route reliably without you sourcing, funding, or managing channels. | | **The Lightning Network** | Open network | Everything speaks BOLT 11, so you can pay and be paid by any wallet, exchange, or app already on Lightning. | ## Self-custodial, exit anytime Users hold their own keys. The coordination service settles transfers but never has unilateral control of anyone’s money, and a user can always move their balance back to the Bitcoin blockchain on their own, at any time, without anyone’s cooperation. The wallet exposes this directly through an `exit` command. You get the convenience of a managed payments experience with the trust guarantees of holding your own Bitcoin. See [Leaving Ark](/concepts/leaving-ark/) for how exits work in practice. ## One invoice for everything Every payment you send and receive is a BOLT 11 invoice, the standard Lightning format. There is a single payment format to learn, and your wallet works with the entire Lightning Network the moment you integrate it. A normal Bitcoin address only shows up at the edges, when funding a wallet or moving funds back to the chain. See [Lightning payments are swaps](/concepts/lightning-payments-are-swaps/) for how a Lightning payment maps to an Ark swap under the hood. ## Ways to integrate Wavelength meets you where you build: - **Embedded SDK** - pull the SDK into your app to put a wallet directly inside it. It runs on web (compiled to WebAssembly, in a browser tab) and on mobile (compiled into the app binary), on React Native or on native Kotlin and Swift. Start with the [Web quickstart](/web/get-started/quickstart/), the [React Native quickstart](/react-native/get-started/quickstart/), or the [Native iOS & Android quickstart](/native-ios-android/quickstart/). - **As a standalone client** - a single self-contained process driven over a gRPC and REST API. - **For agents** - run the MCP server so an AI agent can drive the wallet as typed tool calls. Wallet creation and unlock stay off the agent channel, so seeds and passwords are never exposed to a model. These docs focus on the SDK (web, React Native, and native iOS and Android). The other surfaces share the same wallet commands and are documented separately. ## Networks Wavelength runs on signet and testnet by default, and both are open to everyone. These test networks let you build and exercise the full payment flow with coins that have no real value. Mainnet access is gated to an approved allowlist and requires an explicit opt-in. See [Networks and config](/concepts/networks-and-config/) for the details. ## Where to go next - [What is Wavelength?](/introduction/what-is-wavelength-sdk/) - the client you run, and the packages it ships as. - [Web quickstart](/web/get-started/quickstart/), [React Native quickstart](/react-native/get-started/quickstart/), or [Native iOS & Android quickstart](/native-ios-android/quickstart/) - a running wallet in a few minutes. - [System architecture](/introduction/system-architecture/) - how the embedded daemon connects to the backend gateways. --- Source: https://wavelength.lightning.engineering/introduction/system-architecture.md # System architecture ## The embedded daemon The Wavelength SDK embeds [wavelength](https://github.com/lightninglabs/wavelength), the same wallet daemon on every platform, and runs it wherever your app runs. On the web it is compiled to WebAssembly and runs inside the browser; you boot it with `createWebClient()` and `client.start()`. On React Native it is compiled into your app binary and runs on device; you boot it with `createNativeClient()` and `client.start()`. Either way it stays running for the session, and you drive it through one typed `WavelengthClient`. **`WavelengthClient` is the app-facing API.** It is defined in `@lightninglabs/wavelength-core` and implemented by each transport (`@lightninglabs/wavelength-web` on the web, `@lightninglabs/wavelength-react-native` on React Native). Your code never imports Go or protobuf types. You call typed methods (`deposit`, `receive`, `send`, `balance`, and so on) and receive plain JavaScript objects. The wire protocol between the client and the embedded daemon differs by transport, and from your app’s perspective it is an implementation detail. The React Native transport uses the daemon’s native gRPC. Browsers cannot open arbitrary gRPC connections the way a native host can, so the web transport uses **REST** against the embedded daemon’s HTTP facade instead. You still hold one `WavelengthClient` and call the same methods on either. The daemon also owns local persistence (wallet database, swap state) and signing. Your app configures *where* the daemon connects on the network, not *how* it stores keys. ## The three backend gateways Once started, the daemon reaches three external backend services. Configure them in `RuntimeConfig` (or use your transport package’s `defaultConfig(network)` for the public signet and testnet presets): ```ts import { createWebClient, defaultConfig } from '@lightninglabs/wavelength-web'; const client = createWebClient(); await client.ready(); await client.start(defaultConfig('signet')); // arkServerAddress, walletEsploraUrl, swapServerAddress pre-filled for signet ``` ```ts import { createNativeClient, defaultConfig } from '@lightninglabs/wavelength-react-native'; const client = createNativeClient(); await client.ready(); await client.start(defaultConfig('signet')); // arkServerAddress, walletEsploraUrl, swapServerAddress pre-filled for signet ``` | Field | Service | What the daemon uses it for | | ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `arkServerAddress` | Ark operator and mailbox edge | Ark **rounds**, VTXO lifecycle, and **mailbox relay** for out-of-round transfers. A REST URL on web; a `host:port` gRPC address on React Native. | | `swapServerAddress` | Swap server | **Lightning↔Ark atomic swaps** (Lightning receive and send). A REST URL on web; a `host:port` gRPC address on React Native. | | `walletEsploraUrl` | Esplora-compatible indexer | **Chain and UTXO queries**; an HTTP endpoint on both platforms that implements the Esplora `/address/:addr/utxo` API | **Mailbox and operator share one edge.** VTXO mailbox traffic goes through the same Ark server URL. There is no separate `mailboxUrl` in `RuntimeConfig`; the redundant mailbox fields were removed so you configure a single Ark gateway. Lightning is not a fourth gateway. Lightning send and receive are **swap operations** routed through `swapServerAddress`. On-chain visibility (boarding deposits, cooperative leaves, unilateral exits) goes through Esplora and the Ark operator as appropriate. Set `disableSwaps: true` if you only need Ark and on-chain flows without Lightning. It suppresses the preset and any override swap fields. ## How boarding, swaps, and exits connect Wavelength unifies three payment rails behind one balance and one activity stream. Here is how each rail uses the gateways above. ### On-chain in: boarding 1. Your app calls `deposit()` and gets a **boarding address** (a standard Bitcoin receive address). 2. The user sends on-chain BTC to that address. 3. Esplora lets the daemon see the UTXO arrive. 4. At the **next Ark round** (interval is operator-configured, often well under a minute), the deposit **boards** into Ark and becomes a **VTXO** you can spend instantly off-chain. Until boarding completes, funds show as pending inbound balance. ### Lightning: swaps Wavelength does not open Lightning channels in the browser. Lightning payments are **atomic swaps** between Lightning and Ark: - **Receive Lightning** (`receive()`) starts a **receive swap**: the swap server holds a Lightning invoice; when it is paid, you receive Ark balance (a VTXO). - **Send Lightning** (`send({ invoice })`) runs a **send swap**: you spend Ark balance; the swap server pays the BOLT-11 invoice on Lightning. For a quote-then-confirm flow instead of a single call, see `prepareSend()`/`sendPrepared()`. Both directions go through `swapServerAddress`. Swaps can sit in a pending state while liquidity or routing resolves; the daemon tracks them in your activity stream and can refund if a swap expires. ### On-chain out: leave and exit Spending to a normal Bitcoin address uses a **cooperative leave**: the Ark operator helps convert your VTXO into an on-chain transaction. This is the default path for `send({ onchainAddress })`. It is faster and cheaper than exiting alone. **Unilateral exit** is the emergency fallback. If the operator is unavailable or you need to recover funds without cooperation, the daemon can execute a unilateral exit from a VTXO. It is slower, more expensive, and intended for edge cases. Use `exit()` and `exitStatus()` on `WavelengthClient` for this path; see [Unilateral exit](/guides/unilateral-exit/) for integration details. Call `getExitPlan()` first to preview readiness and backing-wallet funding requirements before triggering `exit()`. On-chain in Deposit Boarding Round VTXO Lightning receive Receive swap Swap server VTXO Lightning send Send swap Swap server Invoice paid On-chain send Cooperative leave Operator + Esplora Emergency Unilateral exit On-chain For deeper detail on each rail, see [Balances & VTXOs](/concepts/balances-and-vtxos/), [Lightning payments = swaps](/concepts/lightning-payments-are-swaps/), and [Leaving Ark](/concepts/leaving-ark/). --- Source: https://wavelength.lightning.engineering/concepts/balances-and-vtxos.md # Balances & VTXOs ## VTXOs Wavelength tracks spendable funds the way Ark does: as off-chain outputs bundled into the operator’s periodic batch transactions, not as a single on-chain UTXO in your app. The sections below walk through that model, how boarding turns into live balance, and why confirmed and pending numbers can differ. In Ark, your spendable balance is not a single on-chain UTXO in your wallet. It is a set of **Virtual Transaction Outputs (VTXOs)**: off-chain outputs created inside the operator’s periodic **batch transactions**. Each batch builds a Virtual Transaction Tree (VTXT) whose leaves are individual VTXOs assigned to participants. A VTXO behaves like a UTXO you can spend, but it lives in Ark’s shared batch structure until you exit, refresh, or the operator sweeps expired batches. Wavelength tracks every live VTXO the daemon knows about and uses them as inputs when you send, swap, or leave. Each VTXO carries two spend paths: - **Collaborative path**: you and the Ark operator co-sign, so the output can move instantly inside Ark (for example through a cooperative leave or an out-of-round transfer). - **Unilateral exit path**: you alone can broadcast after a relative timelock (CSV delay) if the operator stops cooperating. VTXOs also have a **sweep delay** tied to their batch. Before the operator becomes eligible to sweep the batch, the VTXO should either be refreshed through a **batch swap** (getting a fresh VTXO in a newer batch) or moved to on-chain Bitcoin. This refresh is daemon-driven: Wavelength detects the approaching expiry and submits the batch swap for you, so there is no method to call yourself. Wavelength surfaces expiry pressure through balance and activity updates rather than asking you to manage raw outpoints manually. > **Note** > > `balance()` and `list({ view: 'vtxos' })` read from the embedded daemon’s view of live VTXOs. The operator’s indexer is authoritative on the network side; the daemon reconciles that view as rounds complete and swaps settle. ## Confirmed vs pending When you show balance in a UI, users usually want one “available” number. Wavelength splits the picture into confirmed and pending fields so you can explain why spendable funds lag behind what they see on-chain or in the activity feed. `balance()` returns three satoshi fields that answer different questions: - **`confirmedSat`**: funds you can treat as settled inside the wallet. These VTXOs are live, unencumbered, and available for send, swap, or leave. - **`pendingInSat`**: inbound value still moving through boarding, a Lightning swap, or a round that has not finished yet. A deposit may be visible on-chain before it becomes a VTXO; a Lightning receive stays pending until the swap server funds and you claim the **virtual HTLC (vHTLC)** (see [Lightning payments are swaps](/concepts/lightning-payments-are-swaps/)). - **`pendingOutSat`**: outbound value reserved by in-flight sends, swap funding, or leave requests. The wallet holds these amounts aside so you do not double-spend the same VTXOs while an operation is still settling. Confirmed satoshis are what you show as “available.” Pending fields explain why the total on-screen balance can differ from what a user can spend right now, and why a payment can appear in the activity feed before it affects confirmed balance. Pair `balance()` with the activity stream when you need per-operation detail: each **Entry** carries its own status (`pending`, `complete`, or `failed`) and a coarse phase (for example `waiting_for_payment`, `settling`, or `confirmed`) that describes where that specific operation sits in its pipeline. See the [wavelength-core reference](/reference/wavelength-core/) for the full **Balance** and **Entry** shapes. ## Boarding → rounds → VTXO lifecycle On-chain Bitcoin enters Ark through **boarding**: 1. `deposit()` returns a **boarding address** (and an initial deposit activity entry). Send on-chain BTC to that address like any other receive address. 2. The operator watches the chain via Esplora. Once the deposit confirms and meets the operator’s policy, the wallet submits a **boarding request** for the next **round**. 3. Rounds advance on a fixed cadence (often around one minute on public test networks; roughly 90 seconds end-to-end is a reasonable UX expectation). During request collection the operator aggregates boarding inputs, VTXO creations, leaves, and batch swaps into one batch transaction. 4. When the round completes, your boarded sats become one or more **VTXOs** in the new batch. The deposit entry moves from pending toward confirmed, and confirmed balance increases. Every VTXO you receive this way inherits the batch’s **CSV expiry** (sweep delay). As that horizon approaches, the VTXO is **refreshed** with a **batch swap**: forfeiting the old VTXO in a round in exchange for a fresh one in a newer batch, which resets the expiry clock. Wavelength handles the round interaction; you do not pick batch transaction IDs yourself. Operators can advertise a **free refresh window**: a late-lifetime span of blocks in which a pure refresh gets its fee waived. When that waiver can be reached without weakening exit safety, the daemon delays the automatic refresh into the window; the advertised width surfaces as `serverInfo.freeRefreshWindowBlocks` on [`getInfo()`](/reference/wavelength-core/#getInfo). See [Leaving Ark](/concepts/leaving-ark/) for the `exit()` / `getExitPlan()` / `sweepWallet()` APIs that apply if you choose to move funds on-chain instead of refreshing. > **Tip** > > Boarding is not instant. Show pending inbound balance and the deposit entry’s phase (`waiting_for_confirmation`, then `settling`) so users know their on-chain payment is recognized but not yet spendable inside Ark. Lightning receives and sends also end in VTXOs (via vHTLC outputs), but on-chain boarding is the path most integrators hit first: address → wait for round → live VTXO → refresh before expiry. --- Source: https://wavelength.lightning.engineering/concepts/activity-and-events.md # Activity & events ## The activity stream Every deposit, payment, and exit shows up in one ordered history. Wavelength keeps that history as a stream you can snapshot once or subscribe to for live updates as operations settle. Wallet history in Wavelength is an **activity stream**: a sequence of **Entry** objects, one per user-visible wallet operation. Deposits, Lightning receives, sends, and cooperative exits each create an entry when they start and update in place as they settle. You can read history in two ways: - `list({ view: 'activity' })` for a paginated snapshot. Filter with `pendingOnly` or `kinds`, and page with `limit` and the string list cursor: ```ts const result = await client.list({ view: 'activity', kinds: ['receive', 'deposit'], }); const { activity } = result; // activity.entries, activity.hasMore, activity.nextCursor ``` - `startActivity()` for a live stream that pushes updates until you call `stopActivity()`. For UI work, prefer the live stream. Call `subscribe()` first to register a listener, then open the stream with the entry kinds you need. Persist the last numeric `Entry.cursor` you processed and pass it back to resume after that position: ```ts await client.startActivity({ includeExisting: true, kinds: ['send', 'receive'], }); await client.startActivity({ kinds: ['send', 'receive'], cursor: lastCursor, }); ``` Each update arrives as a **WavelengthEvent** with `type: 'activity'` and the changed entry as payload. Pending entries mutate (status, phase, txid, confirmation height) until they reach `complete` or `failed`. > **Note** > > `stopActivity()` closes the stream but leaves your `subscribe()` listener registered. You can call `startActivity()` again later without re-subscribing. Direct clients also receive a terminal `activityStream` event when a stream ends or fails. The client does not retry automatically, so a direct consumer chooses its own retry policy. The current bridges do not provide structured gap cursor or reason details. `WalletEngine` manages recovery for UI consumers. It reconciles the snapshot with `list()`, retries from its last safe numeric stream cursor with bounded backoff, and resets that cursor with the runtime lifecycle. Replayed activity events trigger idempotent snapshot refreshes instead of direct row appends. This provides eventual snapshot consistency after reconnects without duplicating entries. The React `useWalletActivity` hook reads that engine snapshot, ordered newest-first, and re-renders as reconciliation completes. ## Event model Beyond activity rows, the client emits runtime and log events on the same subscription. Narrow on the event type before reading its payload. **WavelengthEvent** is a discriminated union. Narrow on `type` before reading `payload`: All five event types arrive through the same `subscribe(listener)` callback registered once. | `type` | Payload | Meaning | | ------------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------- | | `'activity'` | `Entry` | A wallet operation was created or updated. | | `'activityStream'` | `{ state: 'ended' }` or `{ state: 'failed', message }` | The activity stream ended or failed without a consumer-initiated close. | | `'log'` | `{ level, message }` | Daemon log line forwarded for debugging (`debug`, `info`, `warn`, `error`). | | `'runtimeReady'` | none | The embedded runtime finished loading and can accept `start()`. | | `'runtimeStopped'` | none | The runtime stopped, including an unsolicited stop surfaced by the transport. | Activity events carry the full entry shape: - **kind**: high-level category (see below). - **status**: collapsed outcome: `pending`, `complete`, or `failed`. - **amountSat** / **feeSat**: principal and fees in satoshis (plain int64 magnitudes; use the entry’s **kind** to determine direction). - **cursor**: the monotonic stream position of this update. It is zero for entries returned outside the subscription path. - **progress**: optional lifecycle metadata (phase, payment hash, txid, confirmation height, VTXO outpoint, and Lightning preimage). - **request**: optional echo of what the user initiated (Lightning invoice, on-chain address, etc.). - **failureReason** / **failureCode**: populated when status is `failed`. **failureCode** is one of: - **`timed_out`**: the operation exceeded its deadline before completing. - **`expired`**: the underlying invoice, address, or quote expired before it was used. - **`refunded`**: a swap or payment failed and funds were returned to the wallet. - **`needs_intervention`**: the operation stalled in a state that requires manual follow-up. - **`failed`**: a generic failure not covered by the other codes. When known, `EntryProgress.preimage` is the hex-encoded Lightning payment preimage. For a completed Lightning-backed send, it is proof of payment for the invoice. **Entry kinds** group operations for filtering and display: - **`deposit`**: on-chain boarding (`deposit()` address flow). - **`receive`**: inbound Lightning receive settled through an atomic swap into Ark. - **`send`**: outbound payment (Lightning send swap, cooperative on-chain leave, or in-Ark send). - **`exit`**: cooperative leave of VTXOs to a plain on-chain address. For a `send` entry, `entry.request?.type` (`'lightning'`, `'onchain'`, or `'ark'`) tells you which rail the payment actually used; when preparing a send, `PrepareSendResult.rail` reports the same thing ahead of time. See the [wavelength-core reference](/reference/wavelength-core/) for the full field list. **Entry phases** sit one level below status and describe *where* a pending operation is in its pipeline. They do not replace status; use both. Common phases include: - **`request_created`**: the wallet recorded the intent but nothing external has happened yet. - **`waiting_for_payment`**: waiting for a counterparty (Lightning payer, swap funder, or boarding confirmation). - **`waiting_for_confirmation`**: an on-chain tx is seen but not yet deep enough in the chain. - **`payment_detected`** / **`settling`**: value is in motion through Ark, Lightning, or swap machinery. - **`confirmed`**: terminal success from the backing subsystem’s perspective. - **`refunding`** / **`refunded`**: swap timeout or cancellation path returning funds. - **`failed`**: terminal failure. You can switch on the progress phase for structured UI states, or render the phase label directly when you want the daemon’s short label without maintaining a mapping table. Field definitions live in the [wavelength-core reference](/reference/wavelength-core/). For the React hook signatures built on this stream, see the [wavelength-react reference](/reference/wavelength-react/). For how activity and events relate to the client’s broader startup and connection state, see [Wallet lifecycle & auth](/concepts/wallet-lifecycle-and-auth/). --- Source: https://wavelength.lightning.engineering/concepts/wallet-lifecycle-and-auth.md # 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 ( ); } function PasskeyUnlock({ credentialId }: { credentialId: string }) { const { open, openPending } = useWalletPasskey(webPasskeyCeremony); // Returning session: assert the existing passkey to unlock. const unlock = () => open(credentialId); return ( ); } ``` 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/). --- Source: https://wavelength.lightning.engineering/concepts/keys-backup-and-recovery.md # Keys, backup & recovery Every Wavelength wallet is a hierarchical-deterministic (HD) wallet rooted in a single seed. The seed’s origin depends on the auth mode (typed password or passkey), and everything else follows from it: what sits on disk, what counts as a backup, and what a user needs to get funds back on a new device. For the lifecycle states and UI phases around unlock, see [Wallet lifecycle & auth](/concepts/wallet-lifecycle-and-auth/). ## One seed, two ways to derive it A **password wallet** gets its seed from a freshly generated (or imported) 24-word mnemonic; the password protects the wallet’s key material at rest. A **passkey wallet** inverts this: the seed itself is *derived* from the passkey, so the passkey is simultaneously the unlock credential and the root secret. ```mermaid %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, sans-serif","fontSize":"16px","primaryColor":"#2c2c33","primaryTextColor":"#f5f5f7","primaryBorderColor":"#15e0c2","secondaryColor":"#24242a","secondaryTextColor":"#f5f5f7","secondaryBorderColor":"#a78bfa","tertiaryColor":"#1c1c21","tertiaryTextColor":"#b6b6c0","tertiaryBorderColor":"#56c7f2","lineColor":"#8c8c96","textColor":"#f5f5f7","mainBkg":"#2c2c33","nodeBorder":"#303037","clusterBkg":"#1c1c21","clusterBorder":"#303037","titleColor":"#b6b6c0","edgeLabelBackground":"#24242a","edgeLabelTextColor":"#f5f5f7","nodeTextColor":"#f5f5f7","rectBorderRadius":"10px","clusterBorderRadius":"12px"},"flowchart":{"htmlLabels":false,"curve":"basis","padding":20,"nodeSpacing":55,"rankSpacing":60,"diagramPadding":12,"useMaxWidth":false}}}%% flowchart TD PK["Passkey assertion
(WebAuthn PRF output)"] --> HKDF["HKDF-SHA256"] HKDF -->|"info: wavewalletdk:seed:v1"| ENT["16-byte entropy"] HKDF -->|"info: wavewalletdk:dbpw:v1"| DBP["wallet DB password
(64-char hex)"] ENT --> AZ["aezeed mnemonic
(24 words)"] AZ --> SEED["HD wallet seed"] PW["User password"] --> DBP2["wallet DB password"] MN["Generated / imported
mnemonic"] --> SEED SEED --> DB[("wallet database
(OPFS / app sandbox)")] DBP --> DB DBP2 --> DB ``` Both paths converge on the same storage: the seed lives only inside the wallet database, encrypted under the wallet DB password and stored device-locally. On the web that is [OPFS](/web/runtime/data-and-persistence/), the browser’s origin-private file system. On React Native it is a data directory inside the app sandbox: `RuntimeConfig.dataDir`, defaulting to the platform path reported by `getDefaultDataDir()`. Either way the database never leaves the device, and no key material ever reaches the operator or any server. ## The passkey ceremony Passkey wallets are built on the WebAuthn **PRF extension**: an authenticator holds a hidden pseudo-random function per credential, and evaluating it over a fixed input yields a stable 32-byte secret. The SDK always evaluates the PRF over the same input, `SHA-256("wavewalletdk-passkey:v1")` (exported from core as `PASSKEY_PRF_SALT_HEX`), so the same passkey produces the same secret on every device and every session. That determinism is the whole design: the PRF output *is* the wallet’s root secret. On the web, `registerPasskeyWallet(appName)` creates the credential with `authenticatorAttachment: 'platform'`, `residentKey: 'required'`, and `userVerification: 'required'`, meaning a discoverable, biometric-gated passkey in the platform authenticator (Face ID, Touch ID, Windows Hello, or a synced password manager). Some browsers do not return a PRF result from the creation ceremony itself, so registration falls back to an immediate assertion scoped to the just-created credential to read the PRF value reliably. `assertPasskeyPrf(credentialId?)` runs the returning-session ceremony. With a stored `credentialId` the assertion is scoped to that one credential and the OS unlocks it directly; without one, the assertion is *discoverable*, so a passkey synced from another device can be offered even though this device has never seen the wallet. The `credentialId` is not a secret; persisting it in `localStorage` just skips the credential chooser on the next unlock. On React Native, `createNativePasskeyCeremony({ rpId })` supplies the same ceremony through the platform credential APIs: Credential Manager on Android and AuthenticationServices on iOS (iOS support is experimental and needs iOS 18 or newer). Because every platform evaluates the PRF over the same fixed input, the same passkey derives the same wallet everywhere: a wallet created in the browser opens in the app, and vice versa, provided both use the same relying-party domain. > **Caution** > > Passkeys are bound to the **relying-party ID**. On the web the SDK sets it to the page’s hostname; on React Native you pass `rpId` explicitly and must associate that domain with your app (see [Passkey setup](/react-native/get-started/passkey-setup/)). If the app later moves to a different domain, existing passkeys cannot be asserted there and passkey-based recovery breaks for every user. Plan the relying-party domain as a long-term commitment, share it between your web and mobile apps so one passkey opens both, and treat the mnemonic as the domain-independent escape hatch. ## Seed derivation The wallet runtime (the Go SDK, compiled to WASM on the web and bundled as a native library on React Native) expands the PRF output with **HKDF-SHA256** into two domain-separated secrets: | HKDF `info` | Output | Role | | ---------------------- | --------------------- | ----------------------------------- | | `wavewalletdk:seed:v1` | 16 bytes | aezeed entropy (the HD wallet seed) | | `wavewalletdk:dbpw:v1` | 32 bytes, hex-encoded | wallet DB password | The entropy is wrapped into an [aezeed](https://github.com/lightningnetwork/lnd/tree/master/aezeed) cipher seed with a pinned version and a birthday pinned to the Bitcoin genesis date, and an **empty seed passphrase**. Pinning all three means the wallet is a pure function of the PRF output: nothing else needs to be stored or remembered for the keys to be reproducible. Two invariants protect this contract: - **The PRF input never changes.** The evaluation input is fixed per namespace version. If a caller ever evaluated the PRF over a different input, the same passkey would silently derive a different seed (a different wallet), and the original funds would be unreachable until the correct input is used again. - **Short PRF outputs are rejected.** The SDK refuses PRF outputs under 32 bytes, so a platform bug or empty input cannot collapse the derivation into a low-entropy, attacker-reproducible seed. The derived DB password plays the same role a typed password plays in a password wallet: it is the private passphrase that encrypts the wallet database’s key material at rest. Because it comes out of HKDF, it carries the full 32 bytes of entropy, far stronger than any human password. And because the derivation is deterministic, every device derives the same password without storing or syncing it, which is why a passkey wallet never shows a password prompt. For a **password wallet**, the user’s typed password is used directly as the wallet database passphrase, and the seed comes from a mnemonic generated by the daemon (`createWallet({ password })`) or imported by the user (`createWallet({ password, mnemonic })`). An optional seed passphrase (`seedPassphrase`) can be layered on an imported mnemonic; passkey wallets always use an empty one. ## What is stored where | Artifact | Where | Protected by | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | HD seed and derived keys | Wallet database in [OPFS](/web/runtime/data-and-persistence/) (web) or the app-sandbox data directory (React Native) | Encrypted under the wallet DB password (typed password, or HKDF-derived for passkey wallets) | | Swap and daemon bookkeeping | SQLite databases alongside the wallet database | Origin or app-sandbox isolation (no key material) | | Passkey private key | Platform authenticator / passkey provider | Device biometrics or PIN; provider sync encryption | | `credentialId`, wallet-kind markers | App-level storage (`localStorage` on web, by convention) | Nothing (not secret) | | Mnemonic | Wherever the user wrote it down | The user | Nothing above leaves the device except the passkey itself, which the platform’s passkey provider (iCloud Keychain, Google Password Manager, 1Password, and similar) may sync end-to-end encrypted across the user’s devices. The operator sees signed protocol messages, never keys. ## Backup A passkey wallet has two independent backup layers: 1. **The synced passkey.** If the user’s passkey provider syncs credentials, the wallet is already recoverable on any device signed into that provider: the PRF travels with the credential, and the seed is derived from it on demand. This is the primary, zero-effort layer. 2. **The mnemonic.** When a passkey wallet is created (or first imported on a device), the result includes a 24-word aezeed mnemonic. It decodes to the same entropy the passkey derives, so it recovers the same wallet *without* the passkey, on any domain, in any aezeed-compatible flow. Offer it to the user once, encourage writing it down, and do not persist it anywhere. > **Note** > > Two exports of the same passkey wallet show **different 24-word phrases**. aezeed encrypts the entropy under a fresh random salt each time it renders a mnemonic, so the words differ while deciphering to the same entropy, and therefore the same wallet. Both phrases are valid backups; a re-export that differs from the recorded phrase is expected, not a bug. A password wallet has exactly one backup layer: the mnemonic. The password cannot be recovered or reset from anywhere, and without the mnemonic a forgotten password means the funds are gone once the local database is lost. ## Recovery paths | Scenario | Path | What comes back | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | Same device, returning session | Passkey assertion (scoped to the stored `credentialId`) + `openWalletFromPasskey()` / `unlockWallet({ password })` | Everything; the local database is intact | | New device, synced passkey | Discoverable passkey assertion + `openWalletFromPasskey()`. The wallet state is `'none'`, so the SDK derives the seed and imports it | Keys immediately; the view of funds converges in the background | | Lost passkey, has mnemonic | `createWallet({ password, mnemonic, recoverState: true })`; the wallet continues as a password wallet | Keys immediately; funds and history via server-assisted recovery | | Lost passkey and mnemonic | None | Nothing; there is no custodial reset | Recovery restores the *seed* instantly; restoring the *view* of funds takes a scan. A mnemonic restore can opt into **server-assisted recovery** with `recoverState: true`: the daemon walks the seed’s addresses and queries the operator’s indexer to rebuild boarding outputs, VTXOs, and Lightning receive history, tracked in the background through the engine’s `'restoring'` phase and `snapshot.recovery`. See [Restore a wallet](/guides/restore-a-wallet/) for the full flow. The passkey import path does not run that scan today, so an app that supports passkey recovery on a fresh device should treat the imported wallet’s view of funds as converging rather than immediate. > **Caution** > > Recovery reproduces keys, not liveness obligations. VTXOs have expiries and unilateral-exit windows; a wallet that has been offline for a long time should be brought back online with enough margin to refresh or exit its VTXOs. See [Leaving Ark](/concepts/leaving-ark/). ## Security model - **Spending requires the device.** A passkey assertion demands user verification (biometric or PIN) on an unlocked device. Anyone who can pass that check can derive the seed; there is no second factor beyond the platform’s. - **The database at rest is only as strong as its passphrase.** For passkey wallets that is 32 bytes of HKDF output, effectively unbreakable offline. For password wallets it is the user’s password; encourage real passphrases. - **The app is part of the trust boundary.** Script running on the wallet’s web origin, or code running inside the mobile app’s process, can drive a ceremony (still gated by a user-verification prompt) and can read the PRF output once a legitimate ceremony completes. XSS on the wallet origin or a compromised dependency in the app bundle is therefore equivalent to key compromise; treat content-security policy and dependency hygiene as wallet security, not routine hygiene. - **The mnemonic is the root of everything.** Anyone holding the 24 words holds the wallet, independent of passkeys, devices, or domains. --- Source: https://wavelength.lightning.engineering/concepts/lightning-payments-are-swaps.md # Lightning payments = swaps ## Receiving over Lightning Receiving Lightning does not mean opening a channel in your app. Wavelength routes inbound payments through an atomic swap that moves value **from Lightning into Ark**. You do **not** open or manage Lightning channels. Instead, `receive()` asks the **swap server** to act as a bridge. The flow, at a high level: 1. The wallet builds a BOLT-11 **invoice** whose payment hash locks a **virtual HTLC (vHTLC)** on Ark. 2. A Lightning payer routes to the swap server (via a route hint embedded in the invoice). 3. The swap server **holds** the incoming Lightning HTLC and **funds a matching vHTLC** on Ark, locked to the same payment hash. 4. The wallet observes the funded vHTLC (via the operator’s indexer), **claims** it with the preimage, and the swap server uses that preimage to **settle** the Lightning side. The vHTLC is a taproot output with the same hashlock/timelock structure as a Lightning HTLC, but settled inside Ark. Until the claim completes, the receive shows up as pending inbound balance and a receive activity entry in phases like `waiting_for_payment` and `settling`. > **Note** > > Wavelength Lightning receive is swap-backed. There is no inbound channel capacity to pre-fund, but swap server liquidity and operator liveness still matter for how quickly funding appears. ## Sending over Lightning Sending to a Lightning invoice works the same way in reverse: Ark value funds a vHTLC, and the swap server pays the invoice on Lightning once the Ark side is claimable. The swap moves value **from Ark out to Lightning**. Again, this is not a channel payment. The flow: 1. `prepareSend({ invoice })` (or `send()` in one step) selects VTXOs and constructs a swap session. 2. The wallet **funds a vHTLC** on Ark locked to the invoice’s payment hash. 3. The swap server **claims** that vHTLC with cooperation from the operator, learns the preimage, and **pays** the BOLT-11 invoice on Lightning. 4. When the Lightning payment succeeds, the Ark side is settled and the send entry’s progress phase (`entry.progress.phase`) reaches `confirmed`, and `entry.status` becomes `complete`. Outbound Lightning therefore consumes confirmed balance (and shows pending outbound balance while the swap runs). The **SendRail** on a prepared send is `'lightning'` when the invoice path is selected. On-chain sends (`onchainAddress`) and cooperative leaves are separate rails; only invoice payments use the swap server. ## Pending & refunds Swap legs can take seconds or minutes, and either side can time out. Your UI should treat activity entries as live until they reach a terminal status or refund phase. Swap operations are **long-lived and asynchronous**. An activity entry stays pending while the swap server, operator, or Lightning network has not reached a terminal state. The progress phase tells you which leg is active: waiting for a payer, detected funding, settling through Ark, and so on. Every vHTLC encodes **refund paths** with CLTV and CSV timelocks. If the swap server never claims (receive side) or never pays the invoice (send side), the wallet can unwind cooperatively first (operator-co-signed OOR refunds) and, if needed, fall back to on-chain unilateral leaves after delays elapse. Wavelength **arms automatic refund recovery** on pay sessions so stuck sends do not strand funds indefinitely. Entries may pass through `refunding` and `refunded` phases when a timeout fires; `failed` covers hard errors with failure reason text for display. When an entry fails, `entry.failureReason` holds a human-readable message and `entry.failureCode` holds one of `timed_out | expired | refunded | needs_intervention | failed` for programmatic handling. > **Caution** > > A pending Lightning send is often waiting on swap server liquidity or routing, not a bug in your integration. Surface the entry phase to users and expect auto-refund if the invoice expires or the swap times out. See [Troubleshooting](/web/support/troubleshooting/) for the “stuck send” pattern. You do not implement the refund ladder yourself for normal flows; the daemon drives cooperative refunds and escalates when deadlines pass. Your job is to show pending state clearly and avoid treating `prepareSend` quotes as final until the entry completes. --- Source: https://wavelength.lightning.engineering/concepts/networks-and-config.md # Networks & config ## Backend endpoints Starting the embedded daemon requires backend endpoints that serve the same Bitcoin network. Your app never calls them directly. Misaligned endpoints are a common cause of stuck deposits or swaps that never fund. `RuntimeConfig` uses one flat, camelCase shape on both platforms. The endpoint values differ by transport: 1. **`arkServerAddress`** is the Ark operator and mailbox edge. It handles rounds, boarding, VTXO relay, cooperative leave, and mailbox traffic. Use a REST URL on web and a `host:port` gRPC address on React Native. There is no separate mailbox field. 2. **`swapServerAddress`** is the swap server for Lightning to Ark atomic swaps. Use a REST URL on web and a `host:port` gRPC address on React Native. 3. **`walletEsploraUrl`** is an HTTP Esplora endpoint on both platforms for chain queries and UTXO lookups. It must implement the Esplora `/address/:addr/utxo` API (electrs or mempool.space compatible). All configured services must point at the same **`network`**. A mismatched Esplora endpoint or swap server is a common source of “stuck deposit” or “swap never funds” bugs. ## defaultConfig presets For public test networks, use your transport package’s `defaultConfig(network)` instead of hand-typing URLs. It returns a ready **RuntimeConfig** with canonical endpoints merged with any overrides you pass: ```ts import { createWebClient, defaultConfig } from '@lightninglabs/wavelength-web'; const client = createWebClient(); const config = defaultConfig('signet', { dataDir: 'my-app-wallet' }); await client.start(config); ``` ```ts import { createNativeClient, defaultConfig } from '@lightninglabs/wavelength-react-native'; const client = createNativeClient(); const config = defaultConfig('signet', { dataDir: 'my-app-wallet' }); await client.start(config); ``` Presets ship for: | Network | Typical use | | ----------- | ----------------------------------------------------------------------------------------- | | `'signet'` | Default for docs live embeds and the demo app; public Lightning Labs test infrastructure. | | `'testnet'` | Bitcoin testnet3 with hosted Ark/swap gateways. | Hosted test-network gateways use TLS. The web preset uses REST URLs, while the React Native preset uses matching `host:port` gRPC addresses. `defaultConfig` accepts only these preset networks. `'mainnet'` and `'regtest'` are excluded: mainnet has no public deployment yet, and regtest’s local ports vary per development environment. Build a `RuntimeConfig` for either by hand: mainnet with your own endpoints and `allowMainnet` (see below), regtest with local endpoints and the insecure-transport flags. Pass overrides as the second argument to change data directory, point at your own operator, or toggle advanced flags without losing the rest of the preset. ## allowMainnet Mainnet is deliberately gated. Set **`allowMainnet: true`** together with **`network: 'mainnet'`** and your own **`arkServerAddress`**, **`swapServerAddress`**, and **`walletEsploraUrl`**. Without **`allowMainnet`**, the SDK rejects the configuration before startup. > **Caution** > > `allowMainnet` is an explicit safety rail. Do not enable it in example code or demos that users might copy against production networks by accident. Test networks ignore **`allowMainnet`**; it exists only for mainnet runs. ## Mainnet access Signet and testnet are open to anyone. Mainnet is different: the mainnet server accepts only clients on an approved allowlist, so `allowMainnet` on its own is not enough to connect. Access is keyed to your client’s identity, which is its mailbox ID. To request access, initialize and unlock your wallet, run `wavecli getinfo`, and copy the `identity_pubkey` value (a 66-character hex string). Submit that value through the [mainnet access request form](https://docs.google.com/forms/d/e/1FAIpQLScX-AwTYPCRqD9WI2LOtalgL25PSOJ__I6Gf4D8xW04WyWCpA/viewform). Once approved, your client can register and connect. If you re-initialize with a new seed, the identity changes and you have to request access again for the new `identity_pubkey`. ## Complete configuration Most apps only need `defaultConfig(network)` plus an optional data directory. Do not copy this advanced example into a quickstart. It shows the flat fields available for self-hosting and specialized deployments: ```ts const config: RuntimeConfig = { network: 'signet', walletType: 'btcwallet', walletFeeUrl: 'https://fees.example.com', walletBlockHeadersSource: 'neutrino', walletFilterHeadersSource: 'neutrino', walletRecoveryWindow: 250, maxOperatorFeeSat: 100, signingWorkers: 4, bufferSize: 64, }; ``` ### Common fields - `network`, `allowMainnet`, `dataDir`, and `debugLevel` select the network, mainnet safety gate, storage root, and daemon log verbosity. - `arkServerAddress`, `arkServerTlsCertPath`, and `arkServerInsecure` configure the Ark endpoint. On web, `arkServerAddress` is a REST URL and filesystem certificate paths for `arkServerTlsCertPath` are rejected. On React Native, it is a `host:port` gRPC address. - `swapServerAddress`, `swapServerTlsCertPath`, `swapServerInsecure`, and `swapDatabaseFileName` configure the swap service and its state. The web transport accepts `swapServerTlsCertPath` only when `disableSwaps: true`, which suppresses every swap field. - `disableSwaps: true` disables Lightning swaps and suppresses both preset and override swap fields. - `maxOperatorFeeSat`, `signingWorkers`, and `bufferSize` are numeric runtime limits. They must be nonnegative safe integers. ### Embedded wallet backend fields - `walletEsploraUrl`, `walletPasswordFile`, and `walletPollIntervalSeconds` apply only to `lwwallet`. - `walletFeeUrl`, `walletBlockHeadersSource`, and `walletFilterHeadersSource` apply only to `btcwallet`. - `walletRecoveryWindow` applies to both supported embedded backends. `lwwallet` is the default backend. The SDK rejects incompatible backend fields and invalid numeric values before it starts the daemon. LND and eager-round-join disable are intentionally unavailable through this mobile start contract. On regtest, set both insecure flags yourself because local web gateways speak plain HTTP. On hosted networks, leave them unset so connections stay TLS-verified. If you self-host Ark or swap infrastructure, replace the preset endpoints while keeping the network aligned with your Esplora instance. Full field documentation is in the [wavelength-core reference](/reference/wavelength-core/). ## Next steps Once your client is configured and started, see [Handle phases and errors](/guides/handle-phases-and-errors/) for surfacing startup and runtime failures, or [Installation](/web/get-started/installation/) if you still need to add the SDK packages to your app. --- Source: https://wavelength.lightning.engineering/concepts/leaving-ark.md # Leaving Ark ## Cooperative leave Most withdrawals should use the cooperative path: you forfeit VTXOs in an operator round and receive an on-chain payout in roughly one round cadence. Unilateral exit exists for emergencies when the operator cannot or will not co-sign. A **cooperative leave** is the normal way to move Ark balance to a plain on-chain Bitcoin address. You forfeit selected VTXOs in an operator **round**; the batch includes a **leave output** that pays your destination address on-chain. The operator co-signs, so the exit settles in roughly one round cadence instead of waiting for CSV timelocks. In Wavelength this is the default when you send to an on-chain address (`prepareSend` / `send` with `onchainAddress`) or call `exit({ outpoint, destination })` with a destination set. Activity shows up as kind **`exit`** (or a **`send`** on the on-chain rail). The path is **`cooperative`** in the exit result. Cooperative leave requires a **live, honest operator**. It is cheaper and faster than unilateral exit: one batch transaction (plus normal mining confirmation) rather than a chain of checkpoint and connector transactions you broadcast yourself. Round completion is asynchronous; the entry may stay pending through `settling` until the leave output confirms. > **Tip** > > For everyday withdrawals, cooperative leave is what you want. Direct users to a normal Bitcoin address send, not the unilateral exit APIs. ## Unilateral exit When the operator is unreachable or dishonest, you can broadcast pre-signed tree branches yourself. That path is slower, costs more in fees, and should never be the default “Send” action. A **unilateral exit** (also called unilateral unroll) is the **trustless emergency** path. You broadcast the pre-signed Virtual Transaction Tree branch for your VTXO on-chain without operator cooperation, then walk checkpoint and connector transactions until your funds sit in plain UTXOs you control. It works even if the operator disappears, but it is **slow and expensive**: - CSV delays on VTXO scripts must mature before each step is valid. - Multiple on-chain transactions may be required (preview with `getExitPlan({ outpoints })`). - You may need to fund a backing wallet for fees before the exit can proceed unilaterally. Wavelength exposes this through the acknowledged `exit()` branch: ```ts import { FORCE_UNROLL_ACK } from '@lightninglabs/wavelength-core'; await client.exit({ outpoint, forceUnrollAck: FORCE_UNROLL_ACK, }); ``` Import `FORCE_UNROLL_ACK` from any Wavelength package. It carries the exact acknowledgement string the daemon requires and cannot be combined with a cooperative `destination`. Cooperative leave failures reject; they never start unilateral unroll. Once a unilateral job exists, follow-up `exitStatus({ outpoint })` and `sweepWallet({ destinationAddress, broadcast: false })` calls drive the unroll to completion. Call the sweep with `broadcast: false` first to preview. This path is not the same API as sending to an address cooperatively. Unilateral exit does not instantaneously credit a user-chosen address. It creates claimable on-chain outputs over time; `sweepWallet()` consolidates them once timelocks expire. Track progress with `exitStatus({ outpoint })`, which returns an `ExitJobStatus` that advances through `pending` (job queued), `materializing` (checkpoint and connector transactions being broadcast), `csv_pending` (waiting on CSV timelocks to mature), `sweeping` (final consolidation broadcast), and `completed` (funds are plain UTXOs). A job can also land in `failed`, surfaced via `ExitStatusResult.lastError`; `exitStatus()` returns `found: false` rather than an error when no job exists yet for that outpoint. Before the job can start, `getExitPlan({ outpoints })` reports whether it is ready to go: each `ExitPlanEntry` includes a per-outpoint `fundingAddress` and `fundingShortfallSat` for topping up the backing wallet’s fees, plus a `canStart` flag (and an overall `canStart` on the `GetExitPlanResult`). Fund the reported address until the shortfall clears and `canStart` becomes `true` before calling `exit()`. ## When to use which | Situation | Prefer | | ----------------------------------------------------------------- | ----------------------------------------------------------------- | | User taps “Withdraw to my exchange address” | **Cooperative leave** (on-chain send / `exit` with `destination`) | | Operator online, routine payout | **Cooperative leave** | | Operator unresponsive or dishonest | **Unilateral exit** | | User needs funds soon and network is healthy | **Cooperative leave** | | User accepts higher fees and multi-block delays for trustlessness | **Unilateral exit** | Use cooperative leave whenever the Ark service is reachable. Reserve unilateral exit for custody emergencies, disputed operator behavior, or infrastructure outages that block rounds. > **Caution** > > Unilateral exit is irreversible once broadcast and can cost many times the fee of a cooperative leave. Preview `getExitPlan({ outpoints })`, explain delays, and never trigger it silently from a primary “Send” button. If cooperative leave fails, Wavelength surfaces the error directly; it does not silently fall back to unilateral unroll. The unilateral path requires an explicit `forceUnrollAck` on `exit()`. Read `ExitResult.path` to see which branch ran so the UI can treat unilateral exit as a deliberate, informed event rather than a silent substitution. > **Note** > > See [Unilateral exit](/guides/unilateral-exit/) for a walkthrough of building the flow end to end, or [Handle phases and errors](/guides/handle-phases-and-errors/) for surfacing `WavelengthErrorCode` failures along the way. --- Source: https://wavelength.lightning.engineering/web/get-started/quickstart.md # Quickstart (React) ## Install Install the two packages you need: `wavelength-web` for the browser wallet runtime and `wavelength-react` for the React bindings. ```sh npm install @lightninglabs/wavelength-web @lightninglabs/wavelength-react ``` Requires Node 20 or later to run locally. The runtime itself runs in Chrome 100+, Firefox 110+, and Safari 16+. No special build config is needed for Vite or Next.js. ## Wire the provider Create a [`WalletEngine`](/reference/wavelength-core/#WalletEngine) with [`createWebWalletEngine()`](/reference/wavelength-web/#createWebWalletEngine), and wrap your app with [`WavelengthProvider`](/reference/wavelength-react/#WavelengthProvider). Pass `config` and `autoStart: true` to boot the embedded daemon as soon as the runtime is ready, no boot effect needed. Every component below the provider gains access to the wallet through hooks. ```tsx import { createWebWalletEngine, defaultConfig } from '@lightninglabs/wavelength-web'; import { WavelengthProvider, useWallet } from '@lightninglabs/wavelength-react'; const engine = createWebWalletEngine({ runtimeBaseUrl: '/wavewalletdk/', config: defaultConfig('signet'), autoStart: true, }); export function App() { return ( ); } function Balance() { const { phase } = useWallet(); return

Wallet phase: {phase}

; } ``` `defaultConfig(network, overrides?)` returns a `RuntimeConfig` preloaded with public REST gateway endpoints for that network. Overrides are a shallow merge, so pass only top-level fields such as `dataDir`. Keep this quickstart on the default config path; see [Networks & config](/concepts/networks-and-config/) for advanced self-hosting fields. mainnet has no preset: build its `RuntimeConfig` by hand with `allowMainnet: true`, and only after you have key-backup UX in place. Mainnet access is also gated to an approved allowlist; see [Mainnet access](/concepts/networks-and-config/#mainnet-access). ## Create a wallet Call `create` from [`useWalletCreate`](/reference/wavelength-react/#useWalletCreate) to generate a new HD seed in the browser. The seed is encrypted locally and never transmitted. ```tsx import { useWalletCreate } from '@lightninglabs/wavelength-react'; function Onboard() { const { create, createPending } = 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 ( ); } ``` 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. ## Try it Call `send` from [`useWalletSend`](/reference/wavelength-react/#useWalletSend) with a Lightning invoice or on-chain address. Wavelength routes Lightning payments through the swap server and settles on-chain sends through a cooperative leave with the Ark operator. ```tsx import { useWalletSend } from '@lightninglabs/wavelength-react'; function SendButton() { const { send, sendPending } = useWalletSend(); const handleSend = async () => { const result = await send({ invoice: 'lnbc1…', // or onchainAddress: 'bc1q…' }); console.log('Sent!', result.paymentHash ?? result); }; return ( ); } ``` You are all set. Explore the full guides to go deeper on individual topics. ### [Try the live demo](https://wavelength.lightning.engineering/demo/) [Run a signet wallet in your browser, no install required.](https://wavelength.lightning.engineering/demo/) [Open the demo →](https://wavelength.lightning.engineering/demo/) ### [Create a wallet](/guides/create-a-wallet/) [Full walkthrough of wallet creation, key backup, and passkey registration.](/guides/create-a-wallet/) [Read the guide →](/guides/create-a-wallet/) ### [Send a payment](/guides/send-a-payment/) [Fee estimation, prepare/confirm flow, and on-chain cooperative leave.](/guides/send-a-payment/) [Read the guide →](/guides/send-a-payment/) ### [Show balance and activity](/guides/show-balance-and-activity/) [Subscribe to balance changes and render a live activity feed.](/guides/show-balance-and-activity/) [Read the guide →](/guides/show-balance-and-activity/) --- Source: https://wavelength.lightning.engineering/web/get-started/run-the-demo-app.md # Run the demo app > **Try it live** > > A hosted signet demo runs in your browser at . No clone or local setup required. ## Clone & install The reference integration lives in the Wavelength monorepo at `apps/web-wallet-demo`. Clone the repository, install dependencies with pnpm, and build the workspace packages: ```bash git clone https://github.com/lightninglabs/wavelength-sdk.git cd wavelength-sdk pnpm install pnpm build ``` The demo depends on the workspace packages `@lightninglabs/wavelength-web` and `@lightninglabs/wavelength-react`. The `apps/web-wallet-demo/public/` folder is gitignored and ephemeral, so before running the demo you need to populate it with the runtime WASM binaries: ```bash pnpm --filter web-wallet-demo run wasm:local ``` See [Hosting runtime assets](/web/get-started/hosting-runtime-assets/) for details on what this stages and how to point the demo at a different asset source. ## Run Start the Vite dev server from the monorepo root: ```bash pnpm --filter web-wallet-demo dev ``` Open the URL Vite prints (typically `http://localhost:5173`). The demo boots a **signet** wallet out of the box: choose signet on the connect screen and the app boots with its own signet preset (`signetDefaults` in `src/lib/runtime-config.ts`), which mirrors the same public gateway URLs `defaultConfig('signet')` would produce. No local backend is required. Cross-origin isolation headers are configured in the demo’s Vite config so OPFS persistence works in every supported browser. See [Cross-origin isolation](/web/get-started/cross-origin-isolation/) for why these headers are required and how to configure them outside of Vite: ```ts const crossOriginIsolation = { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', 'Cross-Origin-Resource-Policy': 'same-origin', }; export default defineConfig({ server: { headers: crossOriginIsolation }, preview: { headers: crossOriginIsolation }, }); ``` Use `pnpm --filter web-wallet-demo preview` to serve the production build with the same headers. ## Optional (wasm:local, regtest, testnet) **Rebuild runtime assets locally.** When you are developing against a custom wavelength build, stage fresh WASM files into the demo’s `public/` folder: ```bash pnpm --filter web-wallet-demo run wasm:local ``` This runs `make wasm-wallet` in wavelength and copies the full `RUNTIME_ASSET_FILES` set into `public/runtime//`, the versioned path the demo’s `runtimeBaseUrl` points at. **Regtest with arktest.** To exercise deposits, swaps, and exits against a local chain, run [arktest](https://github.com/lightninglabs/wavelength) in a separate terminal: ```bash make build ./arktest-debug start --swapserver ``` Keep arktest running. In another terminal, print the connection URLs: ```bash ./arktest-debug env ``` In the demo connect screen, switch the network to **regtest**. The defaults match arktest’s usual ports (`7071` for Ark, `8501` for Esplora, `10032` for the swap server). Adjust the gateway fields if your arktest instance binds different ports. At a high level the full local workflow is: 1. Terminal 1: `./arktest-debug start --swapserver` 2. Terminal 2: `pnpm --filter web-wallet-demo dev` (optionally after `wasm:local` if you changed the daemon) 3. Browser: select regtest, start the runtime, create or unlock a wallet **Testnet.** Select testnet on the connect screen to boot with its own testnet preset (`testnetDefaults` in `src/lib/runtime-config.ts`), which mirrors the same public gateway URLs `defaultConfig('testnet')` would produce. No arktest instance is needed. **Troubleshooting OPFS.** If wallet persistence fails in the browser, confirm COOP/COEP headers are present: ```bash curl -sI http://localhost:5173 | grep -i cross-origin ``` Both `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` must appear in the response. See [Troubleshooting / FAQ](/web/support/troubleshooting/) for more fixes, and the [web guides](/guides/create-a-wallet/) for end-to-end wallet flows built on top of this demo. --- Source: https://wavelength.lightning.engineering/web/support/demo-app.md # Demo app > **Try it live** > > A hosted signet demo runs in your browser at . No clone or local setup required. ## The reference integration The **`web-wallet-demo`** app in the Wavelength monorepo is a full React integration you can read, run, and copy patterns from. It is not a separate product; it exercises the same `@lightninglabs/wavelength-web` and `@lightninglabs/wavelength-react` packages your app would use. What it demonstrates: - **`WavelengthProvider`** with an injected `createWebWalletEngine()` engine (self-hosted runtime assets, worker mode, optional RPC debug logging). - **Phase-based routing** from `useWallet()`: connect, create/unlock, backup, syncing, ready, stopped, and error screens driven by `phase` and `useWalletInfo()`. - **Password and passkey onboarding** via `useWalletCreate` / `useWalletUnlock` and `useWalletPasskey(webPasskeyCeremony)`, including credential id persistence in localStorage for scoped unlock (see `src/lib/walletKind.ts`). - **Core wallet flows**: deposit address, Lightning receive, send (prepare and confirm), balance and activity, settings, and runtime configuration forms. - **Network presets** for signet, testnet, and regtest (`runtime-config.ts` mirrors public gateway URLs and local arktest ports). - **Production-like hosting concerns**: COOP/COEP headers in `vite.config.ts`, self-hosted WASM binaries under `public/runtime//`, persistent storage request, and a local wipe path that clears OPFS and app markers (`src/lib/wipeLocalData.ts`). Treat the demo as the source of truth for wiring details that docs summarize. Key entry points: `src/main.tsx` (engine bootstrap), `src/App.tsx` (orchestration), and `src/lib/runtime-config.ts` (gateway defaults). See also: [`createWebWalletEngine`](/reference/wavelength-web/#createWebWalletEngine), [`useWallet`](/reference/wavelength-react/#useWallet), and [`useWalletPasskey`](/reference/wavelength-react/#useWalletPasskey) in the API reference for details behind each of these bullets. ## How to run it Clone the monorepo, install dependencies, and start the demo dev server. Step-by-step commands (including `wasm:local` for a fresh WASM build and regtest/testnet overrides) live on the dedicated run page: **[Run the demo app](/web/get-started/run-the-demo-app/)** Out of the box the demo boots a **signet** wallet against Lightning Labs public gateways. No local backend is required for signet. For regtest, start [arktest](https://github.com/lightninglabs/wavelength) locally and point the runtime form at the ports it prints. Runtime assets are staged (never committed) under `public/runtime//`. Before the first `pnpm dev`, and after upgrading the SDK or rebuilding WASM from `wavelength`, run `pnpm --filter web-wallet-demo run wasm:local` to stage fresh binaries. React Native readers: see [Run the demo app](/react-native/get-started/run-the-demo-app/) for the mobile counterpart, which mirrors this demo screen for screen. --- Source: https://wavelength.lightning.engineering/web/get-started/installation.md # Installation ## The packages The Wavelength SDK ships as three npm packages. You install one or two of them depending on your stack: | Package | What it is | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `@lightninglabs/wavelength-core` | Types, the `WavelengthClient` interface, errors, and enums. Pulled in transitively; you rarely install it directly. | | `@lightninglabs/wavelength-web` | The browser WASM transport. Framework-agnostic: use it from vanilla JS, Vue, Svelte, or React. Re-exports everything from `core`. | | `@lightninglabs/wavelength-react` | React provider and hooks. Transport-agnostic: it takes an injected engine and depends only on `core`. | **React apps** need the web transport and the React bindings: ```bash npm install @lightninglabs/wavelength-web @lightninglabs/wavelength-react ``` **Vanilla JS, Vue, Svelte, or other non-React frameworks** need only the web transport: ```bash npm install @lightninglabs/wavelength-web ``` Before wiring up the code samples below, make sure the browser environment you’re deploying to meets the [requirements](/web/get-started/requirements/) and serves your app with the headers described in [Cross-origin isolation](/web/get-started/cross-origin-isolation/). The Wavelength SDK’s WebAssembly runtime will not start without them. ## Peer deps `@lightninglabs/wavelength-react` declares `react` as a peer dependency, with an accepted range of `^18.0.0 || ^19.0.0`. Your app must also provide `react-dom` (or the equivalent for your React renderer). Either major version, 18 or 19, works: ```bash npm install react react-dom ``` Build the wallet engine once near the root of your app. `createWebWalletEngine` accepts `WebWalletEngineOptions` (for example `runtimeBaseUrl` pointing at hosted WASM assets, see [Hosting runtime assets](/web/get-started/hosting-runtime-assets/)), plus `config` and `autoStart` to boot the embedded daemon as soon as the runtime is ready, no boot effect needed. ```tsx import { WavelengthProvider, useWallet, useWalletBalance } from '@lightninglabs/wavelength-react'; import { createWebWalletEngine, defaultConfig } from '@lightninglabs/wavelength-web'; // Configure transport options once. runtimeBaseUrl points at the hosted WASM // asset folder. const engine = createWebWalletEngine({ runtimeBaseUrl: 'https://your-host/wavewalletdk/', config: defaultConfig('signet'), autoStart: true, }); export function App() { return ( ); } function Wallet() { const { phase, error } = useWallet(); const balance = useWalletBalance(); if (phase !== 'ready') { return

Loading… ({phase})

; } return

Spendable: {balance?.confirmedSat ?? 0} sats

; } ``` `error` from `useWallet` holds the last fatal runtime-level error: set when the initial runtime load fails or when `start`/`stop` fails. Render it instead of dropping the failure on the floor. For a non-React app, skip the provider and call `start` on the client directly after `ready()` resolves: ```ts import { createWebClient, defaultConfig } from '@lightninglabs/wavelength-web'; const client = createWebClient({ runtimeBaseUrl: 'https://your-host/wavewalletdk/', }); await client.ready(); await client.start(defaultConfig('signet')); ``` ## Next steps - [Requirements](/web/get-started/requirements/): confirm the browser support matrix and backend services the Wavelength SDK needs. - [Hosting runtime assets](/web/get-started/hosting-runtime-assets/): obtain the WebAssembly runtime asset set and serve it from your own host. - [Quickstart](/web/get-started/quickstart/): wire up the provider, create a wallet, and send your first payment. --- Source: https://wavelength.lightning.engineering/web/get-started/requirements.md # Requirements ## Browser support Wavelength runs the full wallet daemon in the browser as WebAssembly. Target browsers with mature WASM, Web Worker, and storage APIs: | Browser | Minimum version | | ------------------------------------ | --------------- | | Chrome (and Chromium-based browsers) | 100+ | | Firefox | 110+ | | Safari (macOS and iOS) | 16+ | Node.js 20 or later is recommended for local development (Vite, Next.js, and similar bundlers). No special build plugins are required beyond serving the WASM runtime assets and, for worker mode, setting cross-origin isolation headers. Passkey support (WebAuthn with PRF) is optional and depends on the browser and OS. Password-based wallets work everywhere in the matrix above. ## Cross-origin isolation The default transport runs the daemon in a dedicated Web Worker and persists wallet state with OPFS-backed SQLite. Both paths rely on `SharedArrayBuffer`, which browsers only expose on **cross-origin isolated** pages. Your app must send `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` response headers on every route that loads the wallet. See [Cross-origin isolation](/web/get-started/cross-origin-isolation/) for the exact values and a quick verification command. If you cannot isolate the page (for example, because third-party embeds block `require-corp`), you can run the runtime on the main thread with `createWebClient({ runtimeThread: 'main' })`, but OPFS persistence will not be available and the UI thread will block while the daemon is busy. See [Data & persistence](/web/runtime/data-and-persistence/) for how storage behavior differs between main-thread and worker mode. ## Backends Wavelength is self-custodial: the seed never leaves the browser. The embedded daemon still needs reachable **Ark**, **Esplora**, and (for Lightning swaps) **swap server** endpoints at runtime. **Signet (recommended for development)** works out of the box with the public preset. Pass `defaultConfig('signet')` to `start()` and the client connects to Lightning Labs hosted gateways: ```ts import { createWebClient, defaultConfig } from '@lightninglabs/wavelength-web'; const client = createWebClient(); await client.start(defaultConfig('signet')); ``` Presets also exist for `testnet`. No local backend is required for any hosted network. **Regtest (local development)** targets services you run yourself, so there is no `defaultConfig` preset for it; local ports vary per machine, and the URLs are the whole point of a preset. Start [arktest](https://github.com/lightninglabs/wavelength), note the ports it prints, and build the config by hand, including the insecure-transport flags for plain-HTTP local gateways: ```ts import type { RuntimeConfig } from '@lightninglabs/wavelength-web'; const config: RuntimeConfig = { network: 'regtest', arkServerAddress: 'http://127.0.0.1:7071', walletEsploraUrl: 'http://127.0.0.1:8501', swapServerAddress: 'http://127.0.0.1:10032', arkServerInsecure: true, swapServerInsecure: true, }; await client.start(config); ``` **Mainnet** has no public preset yet, so like regtest there is no `defaultConfig` for it. Build the config by hand with your own gateway URLs and set `allowMainnet: true` before going live: ```ts await client.start({ network: 'mainnet', arkServerAddress: 'https://your-ark-server.example.com', walletEsploraUrl: 'https://your-esplora.example.com/api', swapServerAddress: 'https://your-swap-server.example.com', allowMainnet: true, }); ``` On web, `arkServerAddress` and `swapServerAddress` must be REST URLs. `walletEsploraUrl` is an HTTP Esplora endpoint. The web transport always rejects `arkServerTlsCertPath`. It accepts `swapServerTlsCertPath` only when `disableSwaps: true`, which suppresses every swap field. Mainnet access is gated to an approved allowlist. See [Mainnet access](/concepts/networks-and-config/#mainnet-access) for how to request it with your client’s `identity_pubkey`. --- Source: https://wavelength.lightning.engineering/web/get-started/hosting-runtime-assets.md # Hosting runtime assets The web transport loads its WebAssembly runtime from a set of files that you host yourself and point `runtimeBaseUrl` at. You always provide the assets. The worker entry (`wavewalletdk-worker.js`) ships inside `@lightninglabs/wavelength-web` and is emitted by your bundler; only the daemon binaries below need to be hosted. ```ts import { createWebClient } from '@lightninglabs/wavelength-web'; const client = createWebClient({ runtimeBaseUrl: 'https://your-host/wavewalletdk//', }); ``` If `runtimeBaseUrl` is unset, assets resolve relative to the page URL in both worker and main-thread mode, so you can also serve the runtime files alongside your app. ## The asset set The runtime is a fixed set of files built from [wavelength](https://github.com/lightninglabs/wavelength). Import `RUNTIME_ASSET_FILES` to see every filename that must be served together at one base URL. `RUNTIME_MANIFEST_VERSION` identifies the daemon build the SDK is paired with: ```ts import { RUNTIME_ASSET_FILES, RUNTIME_MANIFEST_VERSION } from '@lightninglabs/wavelength-web'; console.log(RUNTIME_ASSET_FILES); // wavewalletdk.wasm, wavewalletdk.wasm.gz, wasm_exec.js, sqlite-bridge.js, // sqlite-worker.js, sqlite3.js, sqlite3.wasm, sqlite3-opfs-async-proxy.js ``` ## Get the asset set There are two ways to obtain the files, both producing the set for the pinned `RUNTIME_MANIFEST_VERSION`: **Download from the wavelength release.** The matching asset set is attached to the paired [wavelength release](https://github.com/lightninglabs/wavelength/releases). Download the files for the pinned version. **Build from a wavelength checkout.** From a checkout of `wavelength`, build the WASM wallet target and copy the output into your app’s static folder: ```bash make -C /path/to/wavelength wasm-wallet mkdir -p "./public/wavewalletdk/" for f in wavewalletdk.wasm wavewalletdk.wasm.gz wasm_exec.js sqlite-bridge.js \ sqlite-worker.js sqlite3.js sqlite3.wasm sqlite3-opfs-async-proxy.js; do cp "/path/to/wavelength/bin/wasm/$f" "./public/wavewalletdk//" done ``` The monorepo demo wraps the same build in `pnpm --filter web-wallet-demo run wasm:local`, which builds from a sibling `wavelength` checkout and stages files into `apps/web-wallet-demo/public/runtime//`, the versioned path the demo’s `runtimeBaseUrl` points at. After `vite build`, those files land in `dist/` and are served from the app origin. ## Host the assets Host all of them under a single path and set `runtimeBaseUrl` to that directory. Prefer a path that includes `RUNTIME_MANIFEST_VERSION` (for example `/wavewalletdk//`): every asset set then gets a unique URL, so browsers pick up new assets on an SDK upgrade instead of serving stale cached copies, and you can cache the files aggressively. Trailing slashes are optional; the client normalizes the base URL. **Version lock.** Runtime assets are version-locked to the embedded daemon inside `@lightninglabs/wavelength-web`. When you upgrade the SDK, obtain and redeploy the matching WASM bundle. Mismatched versions can fail at load time or produce subtle runtime errors. **Compression.** The client prefers `wavewalletdk.wasm.gz` when the browser supports `DecompressionStream` and falls back to the uncompressed `wavewalletdk.wasm`. Host both files so every supported browser can load the runtime. --- Source: https://wavelength.lightning.engineering/web/get-started/cross-origin-isolation.md # Cross-origin isolation ## Why The Wavelength SDK’s default transport runs the wallet daemon in a Web Worker and stores encrypted wallet data in OPFS-backed SQLite. Both the worker’s WASM module and the SQLite stack use `SharedArrayBuffer` for efficient memory sharing. Browsers gate `SharedArrayBuffer` behind **cross-origin isolation**: the page must be served with headers that prevent other origins from reading its memory. Without isolation, `SharedArrayBuffer` is unavailable, OPFS persistence fails, and the worker transport cannot start. Cross-origin isolation is a web-only concern. It applies to every route in your app that loads the Wavelength SDK, including embedded demo or playground pages on the docs site. ## COOP/COEP headers These are response headers on your own HTML document, so only the server that serves your app can set them. The Wavelength SDK cannot set them for you: they are not something the SDK, the worker, or the runtime assets can emit on their own. Send them on wallet routes (and on your dev server while testing locally): ```http Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` `Cross-Origin-Opener-Policy: same-origin` keeps other browsing contexts from holding a reference to your page’s global object. `Cross-Origin-Embedder-Policy: require-corp` requires every subresource (script, stylesheet, image, font, WASM) to either be same-origin or explicitly opt in with `Cross-Origin-Resource-Policy: cross-origin` (or be loaded with `crossorigin` where applicable). Same-origin assets need no extra attribute. **Scope headers to embed routes.** If only part of your site runs the wallet (for example `/web/playground/*` on the docs site), apply COOP/COEP on those paths rather than the entire domain. Hosts like Netlify and Cloudflare Pages read a `_headers` file: ```text /web/playground/* Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` For Vite during local development, set the same headers on the dev and preview servers: ```ts const crossOriginIsolation = { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', 'Cross-Origin-Resource-Policy': 'same-origin', }; export default defineConfig({ server: { headers: crossOriginIsolation }, preview: { headers: crossOriginIsolation }, }); ``` **Self-host third-party assets.** Cross-origin Google Fonts, analytics scripts, and similar embeds are blocked under `require-corp`. Bundle fonts locally (for example with `@fontsource`) or serve them from the same origin as your app. > **Note** > > If you set `runtimeBaseUrl` to point the daemon runtime binaries (`wavewalletdk.wasm.gz`, `wasm_exec.js`, `sqlite-*.js`) at a different origin, that origin must also send `Cross-Origin-Resource-Policy: cross-origin` on those asset responses. Otherwise `require-corp` blocks them from loading. See [Hosting runtime assets](/web/get-started/hosting-runtime-assets/) for details. **Optional: `credentialless` COEP.** Some teams use `Cross-Origin-Embedder-Policy: credentialless` as a lighter alternative. It is not supported uniformly across browsers (Safari in particular). The Wavelength SDK targets `require-corp` because every major engine supports it today. The key difference: `credentialless` drops credentials (cookies, client certificates) on cross-origin requests instead of requiring the target origin to opt in with `Cross-Origin-Resource-Policy`. **Verify with curl.** After deploying, confirm the headers are present: ```bash curl -sI https://your-app.example/web/playground/ | grep -i cross-origin ``` You should see both `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` (or `credentialless` if you chose that mode). In the browser, open DevTools and check that `crossOriginIsolated` is `true`: ```js console.log(crossOriginIsolated); // true ``` If isolation is missing, the wallet will fail to initialize the OPFS database. See [Troubleshooting](/web/support/troubleshooting/) for common fixes. ## See also - [Requirements](/web/get-started/requirements/) - [Hosting runtime assets](/web/get-started/hosting-runtime-assets/) --- Source: https://wavelength.lightning.engineering/web/runtime/data-and-persistence.md # Data & persistence ## OPFS vs localStorage The embedded daemon persists two kinds of browser-local data: 1. **Encrypted seed and wallet metadata** (the wallet database). 2. **SQLite state** for swap tracking and other daemon bookkeeping (via an OPFS-backed VFS when available). Set **`swapDatabaseFileName`** in `RuntimeConfig` to name this SQLite file explicitly; set **`disableSwaps`** to turn off the Lightning swap subsystem (and its storage) entirely. When swaps are disabled, Wavelength suppresses preset and override swap fields. **OPFS (Origin Private File System)** is the production path. Files live under the page origin in a private directory the page cannot enumerate from outside the API. The shipped WASM daemon stores the encrypted seed and SQLite databases in OPFS. Set **`dataDir`** in `RuntimeConfig` to choose a subdirectory (for example `'/my-wallet'`); when unset the daemon uses its default. **localStorage** is a window-only key/value store. It is **not available inside Web Workers**, so the default worker transport cannot use it for daemon storage. Main-thread mode can fall back to storage patterns that do not require OPFS, but you lose durable SQLite persistence and the UI thread blocks while the daemon runs. Your app may still use **localStorage for app-level markers** (for example recording whether a wallet was created with a passkey or storing a passkey credential id for scoped unlock). The reference demo does this; daemon state remains in OPFS. OPFS-backed SQLite requires **`SharedArrayBuffer`**, which browsers only expose on [cross-origin isolated](/web/get-started/cross-origin-isolation/) pages. Without isolation, OPFS persistence for the daemon will not come up cleanly. Request **persistent storage** (`navigator.storage.persist()`) if you want the browser to deprioritize eviction of origin data. It is best-effort and does not replace backing up the recovery phrase. ```ts // Ask the browser to treat this origin's storage as persistent. const granted = await navigator.storage.persist(); ``` Whether the browser grants the request depends on its own engagement heuristics (installed as a PWA, bookmarked, frequently visited, and similar signals). Do not rely on it being granted; treat it as best-effort. ## Main thread vs worker `createWebClient()` accepts **`runtimeThread`**: `'worker'` (default) or `'main'`. ### 'worker' (default) **Thread:** Dedicated Web Worker. **Storage:** OPFS. Production web apps. Keeps the UI responsive while the daemon runs. ### 'main' **Thread:** Page main thread. **Storage:** No OPFS SQLite path. Escape hatch when Workers are unavailable or you cannot set COOP/COEP headers. Expect UI jank. Worker mode spawns the bundled `wavewalletdk-worker.js` (override with `workerURL`). The worker receives **`runtimeBaseUrl`** on init so it can fetch `wavewalletdk.wasm.gz`, `wasm_exec.js`, and the SQLite bridge from your hosted asset folder. Main-thread mode loads those scripts on the page directly. ```ts import { createWebClient, defaultConfig } from '@lightninglabs/wavelength-web'; // Default: worker + OPFS (requires cross-origin isolation). const workerClient = createWebClient({ runtimeBaseUrl: 'https://your-host/wavewalletdk/', }); // Escape hatch: main thread, no OPFS persistence. const mainClient = createWebClient({ runtimeThread: 'main', runtimeBaseUrl: 'https://your-host/wavewalletdk/', }); // Choose a dataDir subdirectory for the daemon's on-disk tree. const config = defaultConfig('signet', { dataDir: '/my-wallet' }); await workerClient.ready(); await workerClient.start(config); ``` **Persistence requirements** for the default worker path: 1. Serve **COOP/COEP headers** so `crossOriginIsolated` is true and `SharedArrayBuffer` is available. 2. Host the **runtime asset set** (`RUNTIME_ASSET_FILES`) at `runtimeBaseUrl`. 3. Use a **secure context** (HTTPS or localhost) for WebAuthn passkeys. 4. Pick a stable **`dataDir`** per wallet profile; changing it starts a fresh on-disk tree. Wiping local wallet data means clearing both OPFS entries under your origin and any app-level localStorage keys you wrote. See the demo’s wipe helper (`apps/web-wallet-demo/src/lib/wipeLocalData.ts`) for a reference pattern, and the [wavelength-web reference](/reference/wavelength-web/) for the client start/stop lifecycle. --- Source: https://wavelength.lightning.engineering/web/support/browser-support.md # Browser support ## Matrix The Wavelength SDK targets evergreen desktop and mobile browsers with mature WebAssembly, Worker, and storage APIs. Minimum versions below are **floors for the WASM runtime**; passkeys and OPFS persistence have stricter requirements noted in their columns. Chrome / Edge - Min version `100` - WebAuthn passkeys (PRF) `114+ (platform authenticator + PRF extension)` - SharedArrayBuffer `Yes, with COOP/COEP` - OPFS persistence `Yes, with COOP/COEP` Firefox - Min version `110` - WebAuthn passkeys (PRF) `Limited PRF support; prefer password fallback` - SharedArrayBuffer `Yes, with COOP/COEP` - OPFS persistence `Yes, with COOP/COEP` Safari (macOS) - Min version `16` - WebAuthn passkeys (PRF) `17.4+ for PRF` - SharedArrayBuffer `Yes, with COOP/COEP` - OPFS persistence `Yes, with COOP/COEP` Safari (iOS / iPadOS) - Min version `16` - WebAuthn passkeys (PRF) `17.4+ for PRF` - SharedArrayBuffer `Yes, with COOP/COEP` - OPFS persistence `Yes, with COOP/COEP` **WebAssembly runtime:** Chrome 100+, Firefox 110+, and Safari 16+ are the documented development targets. Older engines may fail to instantiate the daemon or lack required APIs. See the [error codes reference](/guides/handle-phases-and-errors/) for the `runtime_not_ready` and `asset_load_failed` codes to catch when instantiation fails. **SharedArrayBuffer:** Available only when the page is [cross-origin isolated](/web/get-started/cross-origin-isolation/). Verify in DevTools: `crossOriginIsolated` should be `true`. Without it, worker mode with OPFS SQLite will not initialize. **OPFS:** Requires a secure context and cross-origin isolation for the SQLite VFS the daemon uses. `runtimeThread: 'main'` only changes which thread runs the wasm runtime (main thread instead of a Web Worker); it does not change the storage backend, so it is not a fallback for missing OPFS. Passing `createWebClient({ runtimeThread: 'main' })` is useful as a debugging escape hatch in environments without Web Worker support, but do not ship that path for real wallets. If OPFS itself is unavailable, wallet persistence is not currently guaranteed; treat this as an open question and verify behavior for your target environment before relying on it. **WebAuthn passkeys:** Wavelength derives wallet keys from the WebAuthn **PRF** extension. `webPasskeyCeremony.supportsPasskeyPrf()` checks for a user-verifying platform authenticator, but the authenticator must still return a PRF result at ceremony time. Gate passkey UI on the hook’s `supported` flag: it is `null` while the probe is in flight, so hold a loading state until it settles, then offer a password or recovery-phrase path when it is `false`. See the [passkey guide](/guides/use-a-passkey/) for a full example using `useWalletPasskey`. Node.js **20+** is recommended for local development (Vite, Next.js, and similar bundlers). The runtime itself executes entirely in the user’s browser. Password-based wallets work across the full matrix. Passkeys are optional and depend on the OS authenticator as well as the browser. --- Source: https://wavelength.lightning.engineering/web/support/troubleshooting.md # Troubleshooting / FAQ ## Common errors ### Runtime asset could not be loaded (`asset_load_failed`) The client throws `WavelengthError` with code **`asset_load_failed`** when `wavewalletdk.wasm.gz`, `wasm_exec.js`, or the SQLite bridge cannot be fetched. **Fix:** Host every file listed in **`RUNTIME_ASSET_FILES`** together at one base URL and set **`runtimeBaseUrl`** on `createWebClient()`. Open the failing URL in the browser network tab; a 404 almost always means the base path or version folder is wrong. See [Hosting runtime assets](/web/get-started/hosting-runtime-assets/). ### Page is not cross-origin isolated Symptoms: `SharedArrayBuffer` is undefined, worker startup fails, or SQLite/OPFS never initializes. The SQLite worker reports an error like `Missing SharedArrayBuffer API ... the server must emit COOP/COEP`. **Fix:** Send **`Cross-Origin-Opener-Policy: same-origin`** and **`Cross-Origin-Embedder-Policy: require-corp`** (plus **`Cross-Origin-Resource-Policy: same-origin`** for your own assets) on every HTML route that loads the wallet. Confirm with: ```bash curl -sI https://your-app.example/ | grep -i cross-origin ``` In DevTools, `crossOriginIsolated` must be `true`. See [Cross-origin isolation](/web/get-started/cross-origin-isolation/). Third-party scripts or fonts that are not CORP-compatible block isolation; self-host assets when needed (the demo self-hosts its fonts for this reason). ### Passkey unsupported or PRF missing `useWalletPasskey(...).supported` stays `false`, or ceremonies fail with *“passkey PRF extension result was not returned by this authenticator”*. **Fix:** Passkeys require a **secure context** (HTTPS or localhost), a **user-verifying platform authenticator**, and browser support for the **PRF** extension (Safari 17.4+, Chrome 114+). There is no synchronous PRF probe; `supportsPasskeyPrf()` can return true and still fail on a given device. Offer password creation/unlock or recovery-phrase restore when passkeys are unavailable. On the web, `useWalletPasskey` (from `wavelength-react`) takes a `PasskeyCeremony` implementation as its argument; pass `webPasskeyCeremony` from `@lightninglabs/wavelength-web`. See the [passkey guide](/guides/use-a-passkey/) for a full wiring example. ### Connection refused (wrong URLs or ports) Daemon logs or network errors show `connection refused` or failed fetches to `arkServerAddress`, `walletEsploraUrl`, or `swapServerAddress`. **Fix:** Verify each gateway is reachable from the browser (not just from your shell). For **regtest**, start arktest and match the URLs in your `RuntimeConfig` to the ports the environment prints (`./arktest env` or the `frontend-regtest` overlay). URLs must include the scheme (`http://` or `https://`). There is no `defaultConfig` preset for regtest, so every regtest URL comes from you; the demo app ships its own local defaults (Ark on `7071`, Esplora on `8501`, swap on `10032`). Signet and testnet presets from `defaultConfig()` should work without a local backend; if they fail, check corporate proxies or ad blockers first. On web, Ark and swap values must be REST URLs. The transport always rejects `arkServerTlsCertPath`. It accepts `swapServerTlsCertPath` only when `disableSwaps: true`, which suppresses every swap field. ### 401 Unauthorized from a gateway Occasional **401** responses from the Ark or swap REST gateways are retried with refreshed credentials inside the daemon. Persistent 401s usually mean the server is unhealthy or the URL points at the wrong service. These are daemon-log-only: the daemon does not currently map gateway-originated errors to a specific `WavelengthError` code, so a persistent 401 surfaces client-side (if at all) as a generic `wavelength_error`, not a dedicated code you can match on. **Fix:** Restart the backend (arktest for regtest, or wait and retry for hosted signet). Confirm you are hitting the **REST** gateway hostname (`*-rest` pattern on public networks), not a raw gRPC port. ### OPFS not available Wallet state does not survive reload, or startup logs mention missing OPFS / SQLite VFS support. **Fix:** Serve the app with COOP/COEP headers as described in [Cross-origin isolation](/web/get-started/cross-origin-isolation/) (the demo’s Vite config does this on `dev` and `preview`). Plain static hosting without those headers will not get OPFS-backed SQLite in worker mode. Verify isolation as above. Private browsing modes may restrict storage; test in a normal profile. ### Metro or bundler cache serving stale SDK code After upgrading `@lightninglabs/wavelength-web` or rebuilding WASM, the browser still loads an old worker or runtime manifest. **Fix:** For Expo/Metro workflows, run `npx expo start --clear` or delete `node_modules/.cache`. For Vite (including `web-wallet-demo`), restart with `vite --force` or remove `node_modules/.vite`. Hard-refresh the browser and confirm `RUNTIME_MANIFEST_VERSION` matches the assets you host. ## Stuck sends Lightning sends are **atomic swaps** through the swap server, not direct channel payments. A send can sit in **`pending`** while the swap server waits for Lightning liquidity or routing. Watch the activity entry’s **`progress.phase`**. Typical outbound phases include `waiting_for_payment`, `settling`, and `confirmed`. If liquidity never arrives before the swap timeout, the daemon moves the entry to **`refunding`** and then **`refunded`**: funds return to your Ark balance automatically. You do not need to manually cancel; show the user that the payment failed and the balance will update when the refund completes. If an entry stays pending far longer than expected: 1. Call **`refresh()`** (from `useWalletRefresh()` in `wavelength-react`) or rely on the engine’s activity subscription (the engine debounces activity-driven refreshes). Without React, call **`client.list({ pendingOnly: true })`** directly. 2. Inspect daemon **`log`** events (`useWalletLogs()` or `client.subscribe()`). 3. Confirm **`swapServerAddress`** is correct and swaps are enabled (`disableSwaps` is false). 4. Retry only after the prior attempt reaches `failed`/`refunded`; duplicate sends to the same invoice can race. On-chain sends use cooperative leave and follow a different activity shape; see [Send a payment](/guides/send-a-payment/) for that path. --- Source: https://wavelength.lightning.engineering/react-native/get-started/quickstart.md # Quickstart (React Native) ## Install Install the two packages you need: `wavelength-react-native` for the native wallet runtime and `wavelength-react` for the React bindings. ```sh npm install @lightninglabs/wavelength-react-native @lightninglabs/wavelength-react ``` Requires React Native 0.76+ with the New Architecture enabled, iOS 15.1+, and Android minSdk 24. Expo apps need a development build, not Expo Go: the native wallet runtime is a compiled module that Expo Go cannot load. See [Requirements](/react-native/get-started/requirements/) for the full platform matrix. Until hosted binary distribution ships, the native wallet runtime must be staged before the first build; see [Installation](/react-native/get-started/installation/) for the staging step. The native side is built by `npx expo run:android` / `npx expo run:ios` (which installs iOS pods automatically), so there is no separate pod step to run by hand. ## Wire the provider Create a [`WalletEngine`](/reference/wavelength-core/#WalletEngine) with [`createNativeWalletEngine()`](/reference/wavelength-react-native/#createNativeWalletEngine), and wrap your app with [`WavelengthProvider`](/reference/wavelength-react/#WavelengthProvider). Pass `config` and `autoStart: true` to boot the embedded daemon as soon as the runtime is ready, no boot effect needed. Every component below the provider gains access to the wallet through hooks. ```tsx import { Text } from 'react-native'; import { createNativeWalletEngine, defaultConfig } from '@lightninglabs/wavelength-react-native'; import { WavelengthProvider, useWallet } from '@lightninglabs/wavelength-react'; const engine = createNativeWalletEngine({ config: defaultConfig('signet'), autoStart: true, }); export default function App() { return ( {/* your app */} ); } function Status() { const { phase } = useWallet(); return Wallet phase: {phase}; } ``` `defaultConfig(network, overrides?)` returns a `RuntimeConfig` preloaded with public gRPC `host:port` gateway addresses for that network. Overrides are a shallow merge, so pass only top-level fields such as `dataDir`. Keep this quickstart on the default config path; see [Networks & config](/concepts/networks-and-config/) for advanced self-hosting fields. mainnet has no preset: build its `RuntimeConfig` by hand with `allowMainnet: true`, and only after you have key-backup UX in place. Mainnet access is also gated to an approved allowlist; see [Mainnet access](/concepts/networks-and-config/#mainnet-access). ## Create a wallet Call `create` from [`useWalletCreate`](/reference/wavelength-react/#useWalletCreate) to generate a new HD seed on the device. The seed is encrypted locally and never transmitted. ```tsx import { Button } from 'react-native'; import { useWalletCreate } from '@lightninglabs/wavelength-react'; function Onboard() { const { create, createPending } = 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 ( {sendError &&

{sendError.message}

} ); } ``` See [Send a payment](/guides/send-a-payment/), [Receive a Lightning payment](/guides/receive-a-lightning-payment/), [Get a deposit address](/guides/get-a-deposit-address/), and [Show balance and activity](/guides/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: ```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 ( <> {createError &&

{createError.message}

} ); } ``` 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](/react-native/get-started/passkey-setup/) for the relying-party domain association it requires). `create` and `open` track separately, each with its own `pending`/`error`/`reset`: ```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

Passkeys are not available on this device.

; } return ( <> {createError &&

{createError.message}

} {openError &&

{openError.message}

} ); } ``` ```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 Passkeys are not available on this device.; } return ( <> {createError &&

{createError.message}

} {mnemonic && (

Write down your recovery phrase: {mnemonic.join(' ')}

)} ); } ``` --- Source: https://wavelength.lightning.engineering/guides/restore-a-wallet.md [Wavelength](/)›Guides›Restore a wallet # Restore a wallet 3 min read Guides Web SDK A restore rebuilds a wallet on-device from a recovery phrase the user already has, rather than generating a fresh seed. Restores are always local password wallets: the user supplies the recovery phrase plus a new password to encrypt the seed on this device. This guide covers restoring the seed, opting into server-assisted recovery to bring back funds and history, and observing that recovery run in the background so the user is not stuck on a loading screen. ## Restore from a recovery phrase Pass an existing `mnemonic` to [`useWalletRestore()`](/reference/wavelength-react/#useWalletRestore)’s `restore` (with `seedPassphrase` too, if the original wallet used a BIP-39 passphrase). This re-derives the same HD seed and encrypts it with the new `password`. On its own, that rebuilds the keys but leaves the wallet empty: it does not know about any funds or past activity yet. To bring those back, opt into recovery. ## Bring back balances and history Set `recoverState: true` to turn on server-assisted recovery. The daemon walks the seed’s addresses and queries the operator’s indexer to rebuild wallet state: boarding outputs, VTXOs, and Lightning receive history. Without it, a restored wallet starts at a zero balance even if the seed has funds. `recoveryWindow` is an optional per-key-family address look-ahead: how many unused addresses past the last used one the scan probes before giving up. Omit it to let the daemon pick its own default, and only raise it if a wallet skipped a large gap of addresses. ## Restore in the background The recovery scan can take minutes and reports no progress while it runs. Rather than blocking on the whole scan, `restore` from [`useWalletRestore()`](/reference/wavelength-react/#useWalletRestore) resolves (and flips `restorePending` back to `false`) as soon as the restored wallet is usable, which happens well before the scan completes; `phase` from [`useWallet()`](/reference/wavelength-react/#useWallet) advances to `'restoring'` while the wallet comes up and then to `'ready'` at that point. The scan itself keeps running in the background, tracked separately through [`useWalletRecovery()`](/reference/wavelength-react/#useWalletRecovery). The user lands in a working wallet immediately while balances and history fill in behind them. ```tsx import { useWallet, useWalletRestore } from '@lightninglabs/wavelength-react'; function Restore({ mnemonic }: { mnemonic: string[] }) { const { phase } = useWallet(); const { restore, restorePending, restoreError } = useWalletRestore(); const handleRestore = async () => { // Resolves as soon as the wallet is usable; the recovery scan (if any) // keeps rebuilding balances and history in the background, tracked // through useWalletRecovery. await restore({ password: 'a-strong-password', mnemonic, recoverState: true, }); }; if (phase !== 'needsWallet') { return null; } return ( <> {restoreError &&

{restoreError.message}

} ); } ``` ## Show recovery progress [`useWalletRecovery()`](/reference/wavelength-react/#useWalletRecovery) returns `recovery`, a discriminated union on `status`, and `acknowledge`, which resets it to `idle`. Drive a banner off `recovery` so the user knows the scan is still running, and report the outcome when it settles. ```tsx import { useWalletRecovery } from '@lightninglabs/wavelength-react'; function RecoveryBanner() { const { recovery, acknowledge } = useWalletRecovery(); switch (recovery.status) { case 'idle': return null; case 'restoring': return (

Restoring your balance and history. This can take a few minutes; your balance will fill in as it is found.

); case 'done': return (

Wallet restored. Recovered {recovery.result.recoveredVTXOs} VTXOs.

); case 'failed': return recovery.walletUsable ? (

Could not finish restoring your history, so your balance may be incomplete. The wallet is still usable. {recovery.error.message}

) : (

Restore failed: {recovery.error.message}

); } } ``` `recovery.status === 'failed'` covers two distinct situations: a background scan that errored on an already-usable wallet, or a restore that failed before the wallet came up at all (for example the recovery phrase was rejected). `recovery.walletUsable` disambiguates the two directly: `true` means the background scan errored on an already-usable wallet, which works with whatever the scan found before it failed; `false` means the restore itself never got that far, so there is no wallet to fall back to and the failure should be shown on the onboarding screen instead of a post-usability banner. This applies even to an untracked restore (one whose returned promise you did not await, or whose component unmounted while `restorePending`): the failure still lands in `recovery`, so a screen that reads it after remounting picks it back up. The wallet is fully usable throughout the first case. Recovery only reads state, so sending and receiving during the scan is safe. Until it finishes, the balance is understated (a send may briefly report insufficient funds), while receiving is unaffected. If the scan fails, the wallet still works with whatever it found; the phrase is the source of truth, so the user can restore again later. --- Source: https://wavelength.lightning.engineering/guides/get-a-deposit-address.md [Wavelength](/)›Guides›Get a deposit address # Get a deposit address 2 min read Guides Web SDK ## Get a boarding address This guide assumes your app already renders inside a [`WavelengthProvider`](/reference/wavelength-react/#WavelengthProvider) with a wallet unlocked and ready. See [Create a wallet](/guides/create-a-wallet/). `deposit()` from [`useWalletDeposit()`](/reference/wavelength-react/#useWalletDeposit) creates a tracked boarding address from your wallet key. Send any amount of on-chain bitcoin to this address and the Ark server will sweep it into your VTXO balance in the next round. The returned [`DepositResult`](/reference/wavelength-core/#DepositResult) includes both the address string and an initial activity entry. Display the address as text, or render it as a QR code with a library of your choice for easy scanning. ```tsx import { useState } from 'react'; import { useWalletDeposit } from '@lightninglabs/wavelength-react'; function DepositAddress() { const { deposit, depositPending, depositError, resetDeposit } = useWalletDeposit(); const [address, setAddress] = useState(null); const getAddress = async () => { const result = await deposit(); setAddress(result.address); }; return (
{address &&

{address}

} {depositError && (

{depositError.message}

)}
); } ``` `deposit()` also accepts an optional `{ amountSatHint }` if you want to record the amount the user intends to deposit: ```tsx const result = await deposit({ amountSatHint: 100000 }); ``` ## Watch for confirmation Use [`useWalletActivity()`](/reference/wavelength-react/#useWalletActivity) to watch for your deposit to land. The engine subscribes to the activity stream and refreshes automatically, so pending entries appear as soon as the server sees the boarding transaction and flip to `'complete'` once the round settles. An entry can also flip to `'failed'`; in that case show `entry.failureReason` so the user knows what went wrong. Filter on `entry.kind === 'deposit'` to surface only deposit events, or display the full feed with status badges so the user can see the progress. ```tsx import { useWalletActivity } from '@lightninglabs/wavelength-react'; function WatchDeposit() { const activity = useWalletActivity(); const deposits = activity.filter(entry => entry.kind === 'deposit'); return (
    {deposits.map(entry => (
  • {entry.kind} {entry.amountSat} sats - {entry.status}
  • ))}
); } ``` --- Source: https://wavelength.lightning.engineering/guides/show-balance-and-activity.md [Wavelength](/)›Guides›Show balance & activity # Show balance & activity 2 min read Guides Web SDK This guide assumes your app already renders inside a [`WavelengthProvider`](/reference/wavelength-react/#WavelengthProvider) with a wallet unlocked and ready. See [Create a wallet](/guides/create-a-wallet/). ## Read the balance [`useWalletBalance()`](/reference/wavelength-react/#useWalletBalance) is a plain selector: it returns the current [`Balance`](/reference/wavelength-core/#Balance) directly (`null` before it is known), and re-renders your component only when the balance actually changes, for example after a payment settles. There is no `pending`/`busy` flag on it; a `null` balance is your loading signal. Display `confirmedSat` as spendable balance. Add `pendingInSat` and `pendingOutSat` when you want to show in-flight funds. ```tsx import { useWalletBalance } from '@lightninglabs/wavelength-react'; function Balance() { const balance = useWalletBalance(); if (!balance) return

Loading...

; return (

{balance.confirmedSat.toLocaleString()} sats available

); } ``` ## Subscribe to activity [`useWalletActivity()`](/reference/wavelength-react/#useWalletActivity) is the same kind of selector: it returns the full transaction history array directly. Each [`Entry`](/reference/wavelength-core/#Entry) has `id`, `kind` (`send`, `receive`, `deposit`, or `exit`), `amountSat`, `status` (`pending`, `complete`, or `failed`), and timestamps. It also carries `feeSat`, `counterparty`, `note`, and `failureReason` (a human-readable message set when `status` is `failed`). The engine stays live: new entries arrive through the activity stream and pending entries update in place when they settle. This live-update behavior comes from the [`WalletEngine`](/reference/wavelength-core/#WalletEngine) your [`WavelengthProvider`](/reference/wavelength-react/#WavelengthProvider) wraps; see [Create a wallet](/guides/create-a-wallet/) for how it is set up. No polling or manual refresh is needed. ```tsx import { useWalletActivity } from '@lightninglabs/wavelength-react'; function ActivityFeed() { const activity = useWalletActivity(); if (activity.length === 0) return

No activity yet.

; return (
    {activity.map(entry => (
  • {entry.kind} {entry.kind === 'receive' || entry.kind === 'deposit' ? '+' : ''} {entry.kind === 'send' || entry.kind === 'exit' ? '-' : ''} {entry.amountSat} sats {entry.status} {entry.status === 'failed' && entry.failureReason ? `: ${entry.failureReason}` : ''}
  • ))}
); } ``` ## Pull-to-refresh [`useWalletBalance()`](/reference/wavelength-react/#useWalletBalance) and [`useWalletActivity()`](/reference/wavelength-react/#useWalletActivity) update on their own as the engine’s background processes (the activity stream, the settle reconcile, the sync poll) keep the snapshot fresh, so most UIs need nothing more. For an explicit pull-to-refresh control, call `refresh()` from [`useWalletRefresh()`](/reference/wavelength-react/#useWalletRefresh) and show its `refreshPending` flag while the call is in flight. That `refreshPending` is scoped to this hook instance’s own calls; the engine’s own background refreshes never flip it, so it only lights up when the user actually asked for a refresh. ```tsx import { useWalletRefresh } from '@lightninglabs/wavelength-react'; function RefreshButton() { const { refresh, refreshPending } = useWalletRefresh(); return ( ); } ``` ## Filter and group Filter `activity` with standard array methods to build tab views or summary cards. Use each entry’s `kind` to determine direction (`receive` and `deposit` are inbound; `send` and `exit` are outbound). `amountSat` is a plain magnitude in satoshis. Filter on `status === 'pending'` to surface in-flight payments that need user attention. All filtering happens in memory, so you can derive multiple views from the same hook call without additional network requests. ```tsx import { useWalletActivity } from '@lightninglabs/wavelength-react'; function FilteredFeed() { const activity = useWalletActivity(); const sends = activity.filter(entry => entry.kind === 'send'); const receives = activity.filter(entry => entry.kind === 'receive'); const pending = activity.filter(entry => entry.status === 'pending'); return (

Pending: {pending.length}

Received: {receives.length} txs

Sent: {sends.length} txs

); } ``` ## What’s next Now that your UI can show balance and activity, try [Send a payment](/guides/send-a-payment/), [Receive a Lightning payment](/guides/receive-a-lightning-payment/), or [Get a deposit address](/guides/get-a-deposit-address/). --- Source: https://wavelength.lightning.engineering/guides/send-a-payment.md [Wavelength](/)›Guides›Send a payment # Send a payment 2 min read Guides Web SDK ## Send over Lightning Wavelength uses a send swap to route a Lightning payment from your Ark balance. Call `prepare({ invoice })` from [`useWalletPrepareSend()`](/reference/wavelength-react/#useWalletPrepareSend) to get a fee quote and a single-use `sendIntentId`. Pass the result to `sendPrepared()` from [`useWalletSend()`](/reference/wavelength-react/#useWalletSend) to execute. The two-step flow lets you show the user the fee before committing. If the user cancels, discard the quote and call nothing. For a one-step path, call `send({ invoice })` from [`useWalletSend()`](/reference/wavelength-react/#useWalletSend) instead. The quote returned by `prepare()` expires at `expiresAtUnix`. If the user waits too long before confirming, call `prepare()` again to get a fresh quote before calling `sendPrepared()`. Check `feeKnown` before displaying `expectedFeeSat`: when `feeKnown` is `false` the fee is only an estimate, and `warning` may contain additional detail to show the user. ```tsx import { useWalletPrepareSend, useWalletSend } from '@lightninglabs/wavelength-react'; function SendLightning() { const { prepare, prepareData: quote, resetPrepare } = useWalletPrepareSend(); const { sendPrepared } = useWalletSend(); const doPrepare = async (bolt11: string) => { // prepare returns a fee quote and sendIntentId; prepareData holds it after. await prepare({ invoice: bolt11 }); }; const confirm = async () => { if (!quote) return; // sendPrepared dispatches the payment. await sendPrepared(quote); resetPrepare(); }; if (quote) { return (

Fee: {quote.expectedFeeSat} sats {!quote.feeKnown && ' (estimate)'}

{quote.warning &&

{quote.warning}

}
); } return ; } ``` ## Send on-chain (cooperative leave) An on-chain send performs a cooperative leave: the Ark server co-signs a transaction that moves your VTXO value to a plain Bitcoin address. This is faster and cheaper than a unilateral exit because the round happens in one confirmation. Pass `onchainAddress` and `amountSat` to `send()` from [`useWalletSend()`](/reference/wavelength-react/#useWalletSend). The returned [`SendResult`](/reference/wavelength-core/#SendResult) includes the activity entry and `actualAmountSat`. ```tsx import { useWalletSend } from '@lightninglabs/wavelength-react'; function SendOnChain() { const { send, sendPending } = useWalletSend(); const handleSend = async (address: string, amountSat: number) => { await send({ onchainAddress: address, amountSat, }); }; return ( ); } ``` To drain the wallet to an address, set `sweepAll` instead of `amountSat`. The daemon snapshots the live VTXO set at prepare time and spends exactly that set, so quote and dispatch cannot disagree about what “all” means. ```tsx await send({ onchainAddress: address, sweepAll: true }); ``` ## Handle errors `send` and `sendPrepared` from [`useWalletSend()`](/reference/wavelength-react/#useWalletSend) throw a [`WavelengthError`](/reference/wavelength-core/#WavelengthError) on failure and set the hook’s `sendError`, which is always `Error | null`. Check `err.code` for a machine-readable reason. Display `err.message` to the user. Errors thrown during `prepare` (from [`useWalletPrepareSend()`](/reference/wavelength-react/#useWalletPrepareSend)) are safe to retry. Errors thrown during `sendPrepared` may indicate the payment was already sent; check the activity feed before retrying. ```tsx import { useWalletSend, WavelengthError } from '@lightninglabs/wavelength-react'; function SafeSend() { const { send, sendError, resetSend } = useWalletSend(); const pay = async (bolt11: string) => { resetSend(); try { await send({ invoice: bolt11 }); } catch (err) { if (err instanceof WavelengthError) { console.error('Payment failed:', err.code, err.message); } } }; return ( <> {sendError &&

{sendError.message}

} ); } ``` --- Source: https://wavelength.lightning.engineering/guides/receive-a-lightning-payment.md [Wavelength](/)›Guides›Receive a Lightning payment # Receive a Lightning payment 2 min read Guides Web SDK ## Create a Lightning invoice `receive()` from [`useWalletReceive()`](/reference/wavelength-react/#useWalletReceive) triggers a receive swap: the Ark server sets up a Lightning payment path that deposits into your VTXO once the payer settles. Pass `amountSat` and an optional `memo` for the invoice. The returned `ReceiveResult.invoice` is a standard BOLT11 string that any compatible wallet can pay. Display it as a QR code alongside the raw string so the payer has both options. ```tsx import { useState } from 'react'; import { useWalletReceive } from '@lightninglabs/wavelength-react'; function CreateInvoice() { const { receive, receivePending } = useWalletReceive(); const [invoice, setInvoice] = useState(null); const generate = async () => { const result = await receive({ amountSat: 10_000, memo: 'Coffee', }); setInvoice(result.invoice); }; return (
{invoice &&
{invoice}
}
); } ``` ## Wait for settlement [`useWalletActivity()`](/reference/wavelength-react/#useWalletActivity) returns the live activity array and streams updates in place. Match the entry by invoice string in `entry.request?.lightningInvoice` and watch its `status` field: the entry appears as `'pending'` when the payer sends and switches to `'complete'` once the receive swap round completes. Like any entry, a receive swap can also terminate as `'failed'`, so handle that branch too. Render a loading state until the entry appears, then show a pending indicator while it settles, a failure message if it fails, and a success message once `status === 'complete'`. ```tsx import { useWalletActivity } from '@lightninglabs/wavelength-react'; function InvoiceStatus({ bolt11 }: { bolt11: string }) { const activity = useWalletActivity(); const payment = activity.find( entry => entry.kind === 'receive' && entry.request?.lightningInvoice === bolt11, ); if (!payment) return

Waiting for payment...

; if (payment.status === 'pending') return

Payment received, settling...

; if (payment.status === 'failed') { return

Payment failed: {payment.failureReason}

; } return

Settled: +{payment.amountSat} sats

; } ``` For finer-grained progress than the top-level `status` field offers (for example, distinguishing “waiting for payment” from “settling” while a receive swap is still `'pending'`), see `entry.progress?.phase` in [Handle phases and errors](/guides/handle-phases-and-errors/). This guide sticks to the simpler `status` field for clarity. --- Source: https://wavelength.lightning.engineering/guides/use-a-passkey.md [Wavelength](/)›Guides›Use a passkey # Use a passkey 4 min read Guides Web SDK ## Create a passkey-protected wallet [`useWalletPasskey(ceremony)`](/reference/wavelength-react/#useWalletPasskey) drives the passkey ceremony and opens the wallet through the engine, refreshing engine state on success so `phase` advances automatically. The `ceremony` argument is injected from your transport: `supported` is `boolean | null`, not a plain boolean: it starts `null` while the platform capability probe is in flight and settles to `true` or `false` once it resolves. Treat `null` as “still checking” and hold the UI on a loading state rather than falling into the unsupported branch, or a capable device will flash a password-only form before flipping to the passkey option a moment later: Pass [`webPasskeyCeremony`](/reference/wavelength-web/#webPasskeyCeremony) from `@lightninglabs/wavelength-web` as the ceremony implementation. ```tsx import { webPasskeyCeremony } from '@lightninglabs/wavelength-web'; import { useWalletPasskey } from '@lightninglabs/wavelength-react'; function PasskeyOnboard() { const { create, supported, createPending } = useWalletPasskey(webPasskeyCeremony); const handleCreate = async () => { const outcome = await create('My Wallet App'); // Persist outcome.credentialId to scope future unlocks. console.log('Wallet ready:', outcome.result.identityPubKey); }; if (supported === null) return

Checking device capabilities…

; if (!supported) return

Passkeys are not supported on this device.

; return ( ); } ``` Pass [`createNativePasskeyCeremony({ rpId })`](/reference/wavelength-react-native/#createNativePasskeyCeremony) from `@lightninglabs/wavelength-react-native` as the ceremony implementation. The `rpId` domain must be associated with your app first; see [Passkey setup](/react-native/get-started/passkey-setup/). ```tsx import { Button, Text } from 'react-native'; import { createNativePasskeyCeremony } from '@lightninglabs/wavelength-react-native'; import { useWalletPasskey } from '@lightninglabs/wavelength-react'; const ceremony = createNativePasskeyCeremony({ rpId: 'wallet.example.com' }); function PasskeyOnboard() { const { create, supported, createPending } = useWalletPasskey(ceremony); const handleCreate = async () => { const outcome = await create('My Wallet App'); // Persist outcome.credentialId to scope future unlocks. console.log('Wallet ready:', outcome.result.identityPubKey); }; if (supported === null) return Checking device capabilities…; if (!supported) return Passkeys are not supported on this device.; return ( )} {openError &&

{openError.message}

} ); } ``` ## Handle a cancelled ceremony If the user dismisses the OS passkey prompt, `create`/`open` reject with a [`PasskeyCancelledError`](/reference/wavelength-core/#PasskeyCancelledError) instead of a regular failure, and that rejection is never recorded into `createError`/`openError`: a dismissed prompt is not a failure to display. Check for it with `instanceof` and treat it as a no-op rather than showing an error: ```tsx import { webPasskeyCeremony } from '@lightninglabs/wavelength-web'; import { PasskeyCancelledError, useWalletPasskey } from '@lightninglabs/wavelength-react'; function PasskeyUnlock({ credentialId }: { credentialId: string }) { const { open } = useWalletPasskey(webPasskeyCeremony); const unlock = async () => { try { await open(credentialId); } catch (err) { if (err instanceof PasskeyCancelledError) { // The user dismissed the prompt; nothing to show. return; } // A genuine failure: openError is already set for a declarative render, // or handle err here for imperative flow. } }; return ; } ``` ### Troubleshooting: a ceremony that always “cancels” Browsers collapse most WebAuthn failures into the same `NotAllowedError` signal used for a genuine user dismissal: a permissions-policy block, an iframe restriction, or an `rpId` that does not match the page’s association file all surface as [`PasskeyCancelledError`](/reference/wavelength-core/#PasskeyCancelledError) here, indistinguishable from the user tapping away. If a ceremony instantly “cancels” on every attempt, before assuming users are backing out, check the configuration angle first: confirm `rpId` matches the domain hosting the app, and that the `.well-known/assetlinks.json` (Android) or `apple-app-site-association` (iOS) association file for that `rpId` is reachable and lists the app. ## Provide a recovery-phrase fallback If the passkey credential is unavailable (for example, the user is on a new device or has lost access to the registered authenticator), `open` rejects with a genuine error (not a [`PasskeyCancelledError`](/reference/wavelength-core/#PasskeyCancelledError)). Catch it and offer a recovery-phrase fallback instead of leaving the user stuck. Every successful passkey outcome includes `outcome.result.imported`, a boolean that is `true` when the ceremony created a new local wallet from the derived seed (a fresh device) and `false` when it unlocked an existing local wallet. `outcome.result.mnemonic` is populated only when `imported` is `true`, so check `imported` before reading `mnemonic` for backup display. To restore on a new device without a passkey, call `create({ password, mnemonic })` from [`useWalletCreate()`](/reference/wavelength-react/#useWalletCreate) with a new wallet password and the saved recovery phrase; `password` is required by [`CreateWalletRequest`](/reference/wavelength-core/#createWallet). This is why users should record their recovery phrase when they first create the wallet. ```tsx import { useState } from 'react'; import { webPasskeyCeremony } from '@lightninglabs/wavelength-web'; import { PasskeyCancelledError, useWalletCreate, useWalletPasskey, } from '@lightninglabs/wavelength-react'; function UnlockWithFallback({ credentialId }: { credentialId: string }) { const { open } = useWalletPasskey(webPasskeyCeremony); const { create } = useWalletCreate(); const [recovering, setRecovering] = useState(false); const [password, setPassword] = useState(''); const [mnemonic, setMnemonic] = useState(''); const tryPasskey = async () => { try { await open(credentialId); } catch (err) { if (err instanceof PasskeyCancelledError) return; setRecovering(true); } }; if (recovering) { return (
{ e.preventDefault(); create({ password, mnemonic: mnemonic.trim().split(/\s+/) }); }}> setPassword(e.target.value)} placeholder="Choose a wallet password" />