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:
curl -sI https://your-app.example/ | grep -i cross-originIn 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:
- Call
refresh()(fromuseWalletRefresh()inwavelength-react) or rely on the engine’s activity subscription (the engine debounces activity-driven refreshes). Without React, callclient.list({ pendingOnly: true })directly. - Inspect daemon
logevents (useWalletLogs()orclient.subscribe()). - Confirm
swapServerAddressis correct and swaps are enabled (disableSwapsis false). - 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.