---
title: "Troubleshooting / FAQ"
description: "Solutions to common Wavelength SDK integration errors and guidance for diagnosing stuck or pending sends."
canonical: https://wavelength.lightning.engineering/web/support/troubleshooting/
---

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

# 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, when the connection drops partway through the download, or when a fully downloaded body is not recognizable as a runtime asset at all. (A blocked `script-src` is also reported this way on the main thread; see the dedicated section below.)

**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, while a request that starts but never completes points at a flaky connection or an interrupted proxy instead. See [Hosting runtime assets](/web/get-started/hosting-runtime-assets/).

### Runtime asset failed integrity verification (`asset_integrity_failed`)

The client throws `WavelengthError` with code **`asset_integrity_failed`** when a fully-downloaded runtime asset’s bytes are recognizable as a runtime asset but do not match the SHA-256 digest pinned for the SDK’s paired `RUNTIME_MANIFEST_VERSION`. Unlike `asset_load_failed`, the asset’s content was read in full and made sense as a runtime asset; it just did not match what this SDK version expects.

**Fix:** The hosted asset set almost always does not match the SDK release’s pinned `RUNTIME_MANIFEST_VERSION`. Redeploy the matching release asset set, then reload the page. In main-thread mode a failed load is sticky for the lifetime of the client that hit it (it never retries the scripts it already loaded), so a reload, not a retry in place on the same client, is required. If you are intentionally running a runtime built from source, set `runtimeIntegrity: false` on `createWebClient()` or `createWebWalletEngine()` to skip verification. See [Hosting runtime assets](/web/get-started/hosting-runtime-assets/#integrity-verification).

### Runtime asset blocked by Content-Security-Policy

Symptoms: the asset’s URL loads fine when opened directly, and the network tab shows the fetch itself succeeding, yet the runtime never boots. This is a different failure than a fetch that never completes: the SDK fetches and verifies each bootstrap script before executing it, and executes it from a `blob:` URL. A CSP `script-src` without `blob:` blocks that execution step, even though the bytes were fetched and passed verification. The error code differs by thread: in main-thread mode this surfaces as `asset_load_failed` (the blocked `<script>` reports the same way a failed load does); in the default worker transport the browser’s CSP violation message does not match any error the SDK recognizes, so it surfaces as the generic `wavelength_error` instead.

**Fix:** Add `blob:` to your `script-src` directive alongside `'self'`. See [Hosting runtime assets](/web/get-started/hosting-runtime-assets/#integrity-verification) for the full CSP requirement.

### Wallet is open in another tab (`wallet_locked`)

The client throws `WavelengthError` with code **`wallet_locked`** when `start()` runs while another tab or window of the same origin is already running the wallet. The daemon’s OPFS storage is exclusive to one runtime, so the second tab fails immediately instead of colliding on the database. This is unrelated to the `locked` phase, which means a wallet on this device is waiting to be unlocked.

**Fix:** This is expected, not a bug. Tell the user to close the other tab and offer a retry (the demo shows “Already open in another tab. Close it, then try again.” with a retry button, not a wipe hatch). Handle it on the path that starts the runtime, matching on `err.code === 'wallet_locked'`; see [Handle phases and errors](/guides/handle-phases-and-errors/). A related code, **`runtime_lock_unavailable`**, means the browser refused or dropped the lock request itself (for example while the document is shutting down) and says nothing about another tab; offer a plain retry.

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