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 engine, 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.
*Diagram: your app calls the typed WavelengthClient, which forwards to the embedded daemon running in your app, which reaches the backend gateways.*
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
// Show the mnemonic to the user for backup.
const { mnemonic } = await client.createWallet({ password: '…' });
```
```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
// Show the mnemonic to the user for backup.
const { mnemonic } = await client.createWallet({ password: '…' });
```
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.
*Diagram: your app calls Wavelength, the client you run. Wavelength can exit to Bitcoin on-chain at any time, and pays BOLT 11 invoices through the Wavelength Operator, the managed coordination and liquidity service, which settles over the 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 the Wavelength SDK?](/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 mailbox field in `RuntimeConfig`: you configure a single Ark gateway and mailbox traffic rides the same edge.
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()`.
*Diagram: the five payment rails. On-chain in: deposit, boarding, round, VTXO. Lightning receive: a receive swap through the swap server to a VTXO. Lightning send: a send swap through the swap server to a paid invoice. On-chain send: cooperative leave via the operator and Esplora. Emergency: unilateral exit to 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
The sections below walk through how Wavelength represents spendable funds, how boarding turns into live balance, and why confirmed and pending numbers can differ.
## VTXOs
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
// First launch: replay existing entries, then stream new ones.
await client.startActivity({
includeExisting: true,
kinds: ['send', 'receive'],
});
// Later: resume after the last cursor you processed.
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.
Clients used without a `WalletEngine` also receive a terminal `activityStream` event when a stream ends or fails. The client does not retry automatically, so such a consumer chooses its own retry policy. The payload carries a `message` when the stream failed, but no cursor marking where the gap starts, so a consumer that reconnects decides for itself how far back to resume.
`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. All five event types arrive through the same `subscribe(listener)` callback registered once. **WavelengthEvent** is a discriminated union, so narrow on `type` before reading `payload`:
| `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. `amountSat` is signed: positive for value coming into the wallet, negative for value going out.
- **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 the web and React 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. Get test coins from the [signet faucet](https://robinet.eldamar.icu/). |
| `'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, its mailbox ID, which `wavecli getinfo` reports as `identity_pubkey`.
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: 'https://headers.example.com/block-headers.bin',
walletFilterHeadersSource: 'https://headers.example.com/filter-headers.bin',
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. Some daemon-only options (such as the LND wallet backend) are deliberately not exposed through `RuntimeConfig`.
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 proof and checkpoint 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 the proof and checkpoint 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, the daemon drives it to completion on its own: the unroll actor broadcasts the proof and checkpoint transactions and the final sweep itself. Use `exitStatus({ outpoint })` to track progress, and once the funds have landed as claimable outputs, use `sweepWallet({ destinationAddress, broadcast: false })` to preview consolidating them before broadcasting for real. 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` (proof transactions being broadcast and confirmed), `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.
```bash
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 (
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. A freshly created wallet has nothing to send yet, so fund it first: generate an invoice with [`useWalletReceive`](/reference/wavelength-react/#useWalletReceive) (see [Receive a Lightning payment](/guides/receive-a-lightning-payment/)) or board on-chain via [Get a deposit address](/guides/get-a-deposit-address/). ```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 atLoading… ({phase})
; } returnSpendable: {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/