@kaleidorg/swap-sdk
v0.3.0
Published
KaleidoSwap swap SDK — Boltz-protocol atomic swaps (submarine/reverse/chain) for the browser over WebAssembly
Readme
@kaleidorg/swap-sdk
TypeScript and WebAssembly bindings for KaleidoSwap: Boltz-protocol atomic swaps (submarine, reverse, and chain) between Bitcoin, Lightning, and Liquid.
Install
npm install @kaleidorg/swap-sdkUsage
await init() takes no argument and behaves the same in both runtimes: browsers
resolve the packaged WebAssembly binary relative to the module, and Node reads it
from disk via the "node" export condition. Await it once before constructing
any client.
import { BoltzClient, init } from "@kaleidorg/swap-sdk";
await init();
const boltz = BoltzClient.forNetwork("signet");
const pairs = await boltz.submarinePairs();"signet" reaches the live KaleidoSwap maker, so the snippet above runs as
written. "regtest" resolves to http://localhost:9001/v2 and needs this
repository's local harness.
Bundlers must emit the packaged vendor/bindings_wasm_bg.wasm asset referenced
by the generated module.
BoltzClient.forNetwork resolves the default KaleidoSwap maker, which today
serves "signet" and "regtest" only. "mainnet" and "testnet" are rejected
rather than silently falling back to a third-party maker — reach one of those by
passing an explicit base URL to new BoltzClient(baseUrl, timeoutSecs?). Signet
settles on Mutinynet, so pair it with Mutinynet chain access
(https://esplora.signet.kaleidoswap.com), never a testnet3 endpoint: the two
encode addresses identically, so a mismatch raises no error and simply creates
swaps on one chain while funding or watching another.
Supplying the binary yourself
To serve the WebAssembly binary from your own CDN, or as a bundler asset URL,
pass any WasmSource (a BufferSource, URL, Request, Response, or URL
string):
await init(new URL("/assets/bindings_wasm_bg.wasm", location.origin));wasmUrl points at the packaged binary if you need to copy or re-host it. Under
Node you can also pass it straight to init — a file: source is read from
disk, since Node's fetch rejects that scheme. For a pre-compiled
WebAssembly.Module, use initWithModule — it is separate from init so that
WasmSource stays type-safe (WebAssembly.Module is an empty interface in
TypeScript's lib, so a union containing it accepts any value).
One case needs an explicit source: bundling the Node entry into a single file, as
a CLI or a serverless artifact does, moves import.meta.url away from the
packaged vendor/, so the default lookup has nothing to find. Copy
bindings_wasm_bg.wasm next to your output and pass it — that failure surfaces
at runtime rather than at build time, so reach for this before you ship a bundle.
SDK operations also require the web APIs used by the selected client, including
fetch and WebSocket. Node 22 and newer provide both.
Swap keys
Swap keys and preimages derive client-side from a wallet mnemonic via BIP85 index 26589 — no key material leaves the caller:
import { SwapMasterKey } from "@kaleidorg/swap-sdk";
const master = SwapMasterKey.fromWalletMnemonic(walletMnemonic, "regtest");
const { publicKey, secretKey } = master.deriveSwapKey(0n);Typed surface
BoltzClient— Boltz swap API (create submarine/reverse/chain swaps, pairs, fees, quotes, restore).BoltzWsApi/BoltzWsUpdates— WebSocket swap-status stream.SwapScript— reconstruct a swap from its creation response, then build claim/refund transactions (constructClaim,constructRefund) or caller-funded Liquid PSETs (prepareLiquidClaim,prepareLiquidRefund). Chain swaps claim throughconstructCooperativeClaiminstead — the cheaper MuSig2 keyspend, partial-signed with the swap's refund key rather than its claim key.constructClaimcannot carry the lockup script that path signs against, so a chain swap taking the script path must passcooperative: false.SwapMasterKey— BIP85 swap key and preimage derivation.isKaleidoSwapError— narrow a rejection to its stablecode.
Boltz request and response payloads are Rust-defined and cross the boundary as
plain objects typed any; the rest of the surface is hand-typed. The packaged
dist/index.d.ts carries the full signatures and their documentation.
Every rejection produced after an argument reaches the Rust binding is an Error
carrying a code. Input the binding rejects — a mistyped string argument, an
unparseable key or preimage, or a request object missing a required field — uses
InvalidArgument and names the argument or field. Failures from the swap engine
carry their own code; binding-internal failures use Internal.
Values rejected earlier by wasm-bindgen's generated ABI glue remain native
JavaScript errors. In particular, passing a number where a declared bigint is
required throws TypeError before Rust can attach a code.
Lossless integer values
Amounts cross the WASM boundary as bigint. Use the exported toJson helper
when serializing SDK responses:
import { toJson } from "@kaleidorg/swap-sdk";
console.log(toJson({ amount: 1000n }));This applies to arguments as well as responses, and the declared type is the rule to follow — it differs by where the value crosses the boundary.
A parameter declared bigint is passed on the wasm-bindgen ABI, which accepts a
BigInt and nothing else, so it needs the n suffix:
master.deriveSwapKey(0n); // ok
master.deriveSwapKey(0); // TypeError: Cannot convert 0 to a BigInttsc rejects the plain-number form ahead of that throw. The same applies to any
other bigint argument, such as the BoltzClient constructor's timeoutSecs.
Fields inside request objects are declared number even where the Rust type
behind them is 64-bit, because those objects are deserialized rather than passed
on the ABI. Pass what the field declares and both cases are correct.
Arkade Intents venue (@kaleidorg/swap-sdk/arkade)
An optional subpath serving the Arkade Intents RFQ routes
(arkade:BTC ↔ lightning:BTC) against any solver card. Opt-in by design:
it peer-depends on @arkade-os/sdk and @arkade-os/swap, which a
Boltz-only consumer never installs.
import {
ArkadeIntentsVenue,
InMemoryArkadeSwapStore,
} from "@kaleidorg/swap-sdk/arkade";
const venue = new ArkadeIntentsVenue({
wallet,
arkServerUrl,
transport,
store,
});
const { address, fundAmountSats } = await venue.prepareLightningSend({
invoice,
});
// The recovery record is persisted BEFORE this returns. Funding is the
// quote acceptance:
const txid = await wallet.send({ address, amount: fundAmountSats });
await venue.notifyFunded(record.id, txid);Drive venue.reconcile() from your own scheduler (MV3 chrome.alarms, a
node interval) — one evidence-driven pass that claims funded receives,
refunds matured sends, and resolves records from chain evidence. The venue
owns no timers and trusts no relay status message.
Development checks
Build fresh WASM bindings from the repository root before running the package checks:
make wasm-pack-build
cd typescript-sdk
npm ci
npm run typecheck
npm run lint
npm run format:check
npm test
npm run smoke:package
npm run smoke:browser-packagesmoke:browser-package loads the packaged browser entry in headless Firefox, so
it needs firefox on PATH — point BROWSER_BIN at the binary if it lives
elsewhere (on macOS,
/Applications/Firefox.app/Contents/MacOS/firefox). Both smoke scripts pack a
throwaway tarball when given no argument, or check a supplied one:
npm run smoke:package -- path/to/package.tgz.
