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.

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.

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

Terminal
curl -sI https://your-app.example/ | grep -i cross-origin

In DevTools, crossOriginIsolated must be true. See 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 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 (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 for that path.