Multi-protocol Bitcoin L2 wallet engine — native or WDK-backed adapters for Spark · RGB-LN · RGB-L1 · Liquid · Arkade behind one
IProtocolAdaptercontract, with a cross-protocol router, BIP321 unified receive, and lite/advanced disclosure.
wallet-engine is the headless core you build a multi-protocol Bitcoin wallet on.
It hides the differences between Bitcoin L2s behind one interface, keeps the app code
the same across React Native, browser extension, and Node hosts, and ships the hard
parts — routing, unified receive, swaps, lite/advanced UX — as reusable primitives.
It powers KaleidoSwap's apps (rate mobile wallet, the browser extension, the desktop
agent).
┌──────────────────────────────────────────────────────────────┐
│ your app (rate · extension · desktop) │
├──────────────────────────────────────────────────────────────┤
│ @kaleidorg/wallet-engine │
│ ProtocolManager · CrossProtocolRouter · UnifiedReceive │
│ Capability manifest · Disclosure (lite/advanced) · Swap │
│ IProtocolAdapter contract · Platform ports (injected) │
├───────────┬───────────┬───────────┬───────────┬──────────────┤
│ Spark │ RGB/RLN │ Liquid │ Arkade │ (your proto) │
│ adapter │ adapter │ adapter │ adapter │ adapter │
├───────────┴───────────┴───────────┴───────────┴──────────────┤
│ WDK modules · native SDKs · kaleido-sdk (RFQ/RLN client) │
└──────────────────────────────────────────────────────────────┘
Every Bitcoin L2 (Spark, RGB-on-Lightning, Liquid, Arkade) ships its own SDK, its
own address formats, its own quirks (channel liquidity, boarding, invoice expiry,
zero-fee transfers). A wallet that wants to support more than one of them ends up
with if (protocol === …) smeared across every screen.
wallet-engine collapses that into one contract + a data manifest of differences:
- Screens call one API (
ProtocolManager/CrossProtocolRouter), never a protocol SDK directly. - Protocol differences live as data in a capability manifest, not as branches in app code — adding or changing a protocol never edits another protocol's path.
- The same engine runs on every host; platform specifics are injected.
The adapters are the part you could write yourself. These four are the part you'd rather not write twice:
CrossProtocolRouter |
Hand it lnbc1…, a BIP321 URI, or a receive layer — get back the ranked protocols that can settle it, filtered to what's registered and connected. .best is the auto-route. |
src/router |
| Unified receive | One bitcoin: QR carrying on-chain + BOLT11/BOLT12 + Spark + Arkade + Liquid + RGB. Foreign wallets ignore the params they don't know. |
src/receive |
| Capability manifest | Every protocol's layers, quirks and limits as data. The router and your UI read it; nothing special-cases a protocol by name. | src/capabilities |
| Disclosure | Lite vs advanced as one reversible setting rather than two codebases — lite collapses every BTC representation into "BTC". | src/disclosure |
Add a protocol and all four pick it up with zero changes to existing protocol code.
Warning
Alpha — experimental, not production-ready. This engine moves real funds across Bitcoin L2s. APIs may change without notice, and it has not been independently audited. Do not use it with mainnet funds you cannot afford to lose. Use at your own risk.
Maturity is the maturity field each protocol carries in the capability manifest —
read it at runtime, don't hardcode it.
| Protocol | Maturity | Layers | Assets | Swaps | Notable quirks | Backing module |
|---|---|---|---|---|---|---|
| BTC | stable |
on-chain | — | — | base on-chain only | (native) |
| SPARK | beta |
Spark, LN, on-chain | Spark tokens | — | zero-fee, static receive addr | @tetherto/wdk-wallet-spark |
| RGB-LN | beta |
RGB-L1, RGB-LN, BTC-L1, BTC-LN | RGB (USDT, XAUT) | ✅ | needs channel liquidity (LSPS1) | @kaleidorg/wdk-wallet-rln |
| RGB-L1 | beta |
RGB-L1, BTC-L1 | RGB (USDT, XAUT) | — | on-chain only (no LN/channels), local rgb-lib | @utexo/wdk-wallet-rgb |
| LIQUID | beta |
Liquid, Liquid assets | USDt (lite "USD") | — | own L1, no LN | @kaleidorg/wdk-wallet-liquid |
| ARKADE | beta |
Arkade, LN | Arkade assets | — | boarding addr, static receive | @arkade-os/wdk |
Each protocol is described once in src/capabilities/index.ts.
The router and UI read that manifest — they never special-case a protocol by name.
pnpm add @kaleidorg/wallet-enginePublished as
@kaleidorg/wallet-engine(renamed from the earlier@kaleidorg/wallet-protocols; versions ≤ 1.0.0-beta.11 were published under the old name).
The only hard dependencies are @noble/* and @scure/* (the pure-core crypto
primitives). Every protocol SDK is an optional peerDependency — install only the
ones whose adapters you use. Importing the root barrel pulls in no protocol SDK, and
each adapter lazy-loads its SDK inside connect(), so a missing peer only errors when you
actually load that adapter's subpath.
| You import… | Also install |
|---|---|
@kaleidorg/wallet-engine/adapters/wdk (Spark) |
@tetherto/wdk-wallet-spark |
…/adapters/wdk (RGB/RLN) |
@kaleidorg/wdk-wallet-rln |
…/adapters/wdk/wasm-liquid |
@kaleidorg/wdk-wallet-liquid |
…/adapters/wdk/wasm-rgb |
@utexo/rgb-lib-wasm |
…/adapters/wdk (Arkade) |
@arkade-os/wdk |
…/swap |
@kaleidorg/wdk-protocol-swap-kaleidoswap |
…/format |
kaleido-sdk |
legacy …/adapters/spark | /arkade | /rgb | /flashnet |
@buildonspark/spark-sdk | @arkade-os/sdk (+@arkade-os/boltz-swap) | kaleido-sdk | @flashnet/sdk |
# RGB/RLN + Liquid only, for example
pnpm add @kaleidorg/wallet-engine @kaleidorg/wdk-wallet-rln @kaleidorg/wdk-wallet-liquidMigration (≤ beta.53 → beta.54): protocol SDKs moved from
dependencies/optionalDependenciesto optionalpeerDependencies. They are no longer installed transitively — add the packages for the adapters you use (table above) to your ownpackage.json.
import {
ProtocolManager,
CrossProtocolRouter,
buildUnifiedReceiveURI,
aggregateForLite,
} from '@kaleidorg/wallet-engine'
// Adapters + registry live behind the SDK-bearing subpath, so the root stays SDK-free:
import { createWdkRegistry } from '@kaleidorg/wallet-engine/adapters/wdk'
// 1. Build a registry of WDK-backed adapters (pick the protocols you want).
const registry = createWdkRegistry({ enabled: ['RGB_LN', 'LIQUID', 'SPARK'] })
// 2. Connect each protocol (config carries the mnemonic + endpoints).
await registry.get('RGB_LN')!.connect({ protocol: 'RGB_LN', network: 'mainnet', /* … */ })
await registry.get('LIQUID')!.connect({ protocol: 'LIQUID', network: 'mainnet', /* … */ })
// 3. Drive everything through the manager — no protocol SDK in app code.
const manager = new ProtocolManager({ defaultProtocol: 'RGB_LN' })
for (const a of registry.getAll()) manager.registerAdapter(a)
const assets = await manager.listAllAssets() // unified across protocols
const lite = aggregateForLite(assets) // { btc, usd, other }
// 4. Let the router choose which protocol pays a destination.
const router = new CrossProtocolRouter(registry)
const { best, routes } = router.resolveSend('lnbc1…') // best = auto-route for lite mode
// 5. One QR that any wallet can pay; Kaleido wallets read the richer params.
const uri = buildUnifiedReceiveURI({
btcAddress: 'bc1q…',
lightningInvoice: 'lnbc1…',
rgbInvoice: 'rgb:…',
liquidAddress: 'lq1…',
})The quickstart above needs a wallet behind it. This doesn't — it's the same router, manifest and unified receive against four in-memory stub adapters, so you can see the shape before you wire a single SDK:
git clone https://github.com/kaleidoswap/wallet-engine && cd wallet-engine
npm install
npm run example:tourNo node, no credentials, no network. See examples/tour for what
each section demonstrates, and examples/minimal-adapter
for the whole contract in ~170 dependency-free lines.
Every protocol implements the same interface: connect, list assets/transactions,
create/decode invoices, send/receive, and (optionally) getSwapQuote / executeSwap.
See src/adapters/IProtocolAdapter.ts. Two flavours
ship: native adapters (direct SDK integrations) and WDK-backed adapters — both
satisfy the same contract, so the app can't tell which is underneath.
The contract is decomposed into a small required core (ICoreProtocolAdapter) plus
optional capability groups (IRgbOperations, ISparkOperations, IArkadeOperations,
IBackupOperations, ISigningOperations, ISwapOperations, …). IProtocolAdapter is
their composition (Core & Partial<each group>), so the flat surface is unchanged. A new
adapter can implements ICoreProtocolAdapter & IRgbOperations to opt into a group with
required (not optional) methods, and callers can reach a group cleanly with the narrowing
helpers — asRgbOperations(adapter), asSwapOperations(adapter), etc. Third-party
protocols implement the core and connect with any BaseProtocolConfig-shaped config.
src/capabilities/index.ts is the single source of truth
for what each protocol can do (layers, swaps, channel liquidity, zero-fee, static
addresses, boarding…). Rule: when tempted to add a method to the contract for one
protocol, add a capability flag here instead.
src/manager/ProtocolManager.ts routes calls to the
active adapter and provides cross-protocol aggregates (listAllAssets,
listAllTransactions, getPortfolioSummary).
src/router/index.ts takes a destination string or a receive
layer and returns the protocol(s) that can fulfil it, filtered to what's registered and
connected. resolveSend().best is the auto-route that makes lite mode possible.
For a unified payment URI that carries several rails at once (BIP21/BIP321 with a
BOLT12 offer + BOLT11 + Spark/Arkade/Liquid/RGB/on-chain), resolveUnifiedSend(uri, { preference }) matches every rail to the protocols that can settle it and ranks them
by the user's RoutePreference (a per-asset layer ranking) — falling back to a
Lightning-first default. .best is the lite-mode pick; advanced mode shows the
full ranked list. (BIP353 ₿user@domain is resolved to a URI by the host first.)
src/receive/unifiedReceive.ts builds one bitcoin:
URI carrying on-chain + Lightning (BOLT11/BOLT12) + Spark + Arkade + Liquid + RGB. Other
wallets ignore the unknown params; Kaleido-aware wallets get the full menu. The address is
optional, so a lite wallet can publish a single LN/asset-only QR.
src/disclosure/index.ts — lite vs advanced is one
reversible setting, not a code fork. It controls how much the UI reveals (networks,
route selector, channel management, raw ids) and how much the router auto-decides. Lite
mode collapses every BTC representation into one "BTC" and USDt-on-Liquid into one "USD".
src/ports/index.ts — the engine never touches platform APIs.
Each host injects IStorageProvider + IRuntimeProvider (storage, CSPRNG, clock) so
the same engine runs on React Native (SecureStore/MMKV), the extension (chrome.storage),
and Node unchanged.
KaleidoswapSwap wraps the Kaleidoswap RFQ flow
(quote → execute → status) behind domain Quote / SwapResult types — no SDK types
leak across the boundary.
import { KaleidoswapSwap } from '@kaleidorg/wallet-engine'
const swap = new KaleidoswapSwap(rlnAccount, {
baseUrl: 'https://api.kaleidoswap.com',
// Optional: defaults to 100 bps (1%); use 0 for exact from-leg matching.
maxQuoteSlippageBps: 100,
})
const quote = await swap.getQuote({
fromAsset: 'rgb:USDT…', toAsset: 'BTC',
fromLayer: 'RGB_LN', toLayer: 'BTC_LN',
fromAmount: 100,
})
const result = await swap.executeSwap({
...quote, receiverAddress: 'lnbc1…', receiverAddressFormat: 'BOLT11',
})
const status = await swap.getSwapStatus(result.swapId) // pending → confirmed/failed- Implement
IProtocolAdapter(native or WDK-backed). - Add one entry to the capability manifest describing its layers + quirks.
- Register it:
manager.registerAdapter(new MyAdapter()).
The router, unified receive, lite aggregation, and every screen pick it up with zero changes to existing protocol code. New protocol-specific behaviour is a capability flag, never a new method on the contract.
The root barrel, src/index.ts, is deliberately SDK-free: types, the
IProtocolAdapter contract + registry, capability manifest, platform ports,
CrossProtocolRouter + destination classifier, unified receive, the disclosure model,
ProtocolManager, and the Arkade VTXO-lifecycle helpers. Adapters, createWdkRegistry,
KaleidoswapSwap, and the client managers live behind their own subpath exports (see
package.json exports) so importing the root pulls in no protocol SDK.
wallet-engine wallet engine (this package)
└─ depends on
kaleido-sdk protocol client (RFQ/maker + RLN), Python + TypeScript
wallet-engine consumes kaleido-sdk internally for the Kaleidoswap protocol;
consumers of wallet-engine never import kaleido-sdk directly.
Alpha — experimental. Published under a 1.0.0-beta version tag, but treat the
project as alpha: interfaces are unstable and nothing has been audited. Per-protocol
readiness is not prose — it's the maturity field in the capability manifest, shown in
Supported protocols and readable at runtime:
import { PROTOCOL_CAPABILITIES } from '@kaleidorg/wallet-engine'
PROTOCOL_CAPABILITIES.RGB_LN.maturity // 'beta'Native fallback adapters remain available alongside the WDK-backed ones.
Import BarkReactNativeAdapter from
@kaleidorg/wallet-engine/adapters/bark-react-native for an on-device Bark wallet.
It implements the same BARK protocol as the browser BarkAdapter, using the
native UniFFI SDK and SQLite instead of WASM and IndexedDB. Its entry point stays
separate from the browser binding and loads native code only during connect().
Install @secondts/bark-react-native@0.25.0 in the host app, add the
@secondts/bark-react-native Expo plugin, and rebuild the native app. The package
requires New Architecture and Hermes (React Native 0.75+); Expo Go is unsupported.
See Second's React Native guide.
This integration targets the published Wallet.open API; the older
Wallet.create quickstart is not compatible with this pinned version.
import { ProtocolManager } from '@kaleidorg/wallet-engine'
import { BarkReactNativeAdapter } from '@kaleidorg/wallet-engine/adapters/bark-react-native'
// Alternatively, supply the runtime once through setPlatform().
const bark = new BarkReactNativeAdapter({ runtime: { now: () => Date.now() } })
const manager = new ProtocolManager()
manager.registerAdapter(bark)
await manager.connect('BARK', {
protocol: 'BARK',
network: 'signet',
arkServerUrl: 'https://ark.signet.2nd.dev',
esploraUrl: 'https://esplora.signet.2nd.dev',
dataDir, // existing app-private absolute path, supplied by the host
mnemonic, // retrieved from secure storage; never log it
createIfMissing: true, // explicit local initialization/recovery only
})
await manager.setActiveProtocol('BARK')
const address = await manager.getReceiveAddress()
const invoice = await manager.createInvoice({ amount: 1000, layer: 'BTC_LN' })Supported operations:
- Ark and Lightning: native Ark addresses, BOLT11 invoices, sends, status, categorized balances and history. The native SDK checks an Ark destination's network and server before payment. BOLT11 and Bitcoin destinations include BARK in router candidates. Ark and Arkade share address prefixes, so direct Ark sends must select the account explicitly; a prefix cannot select a server.
- Boarding:
bark.backend.getOnchainAddress()supplies a BDK funding address. Fund it explicitly, callsyncOnchain(), thenbark.boardAmount(sats)orboardAll().boardingTerms()andpendingBoards()expose server terms and pending results.boardFundingAddress()is the separate board output address; it is not the BDK wallet's deposit address. UsegetOnchainBalance()to display BDK funds separately from Ark balances.restoreOnchain()explicitly scans existing on-chain history after seed restoration. - Offboarding and recovery:
bark.backend.offboard(address, vtxoIds),refreshVtxos(vtxoIds),progressPendingRounds(),startExit(vtxoIds),progressExits(feeRate?),getVtxos(),getPendingRounds(),getExitStatus(), andrecoverVtxos(vtxoIds). Empty selections are rejected.prepareExitClaimsigns selected claims and returns transaction hex plus its fee; broadcast separately throughbark.broadcastTransactionafter host authorization. - Fees:
bark.backend.estimatePaymentFee(kind, sats, address?)supplies an estimate for Ark, Lightning, or on-chain sends. It is not an enforceable cap. PaymentmaxFeeSats, custom invoice expiry, and on-chain send fee-rate overrides are rejected because these SDK methods cannot honor them.
The host authorizes each fund-moving operation. Raw adapter/backend access
bypasses ProtocolManager policies; use the manager for ordinary sends and an
explicit host authorization flow for boarding and lifecycle operations. A daemon
never starts automatically. Call bark.backend.sync() while the app is active to
process mailbox and Lightning state. Refreshing VTXOs and advancing exits require
explicit lifecycle calls and must not be inferred from a balance read.
A Lightning send starts with wait: false; call sync and poll status until it
settles. A matching preimage is required to report it as confirmed. An Ark send
returns pending with an empty payment hash because the SDK returns no receipt;
reconcile it through history. feeKnown: false means the numeric fee placeholder
is unavailable, not a zero-fee payment. A native error after submission is
PAYMENT_OUTCOME_UNKNOWN; inspect wallet state before retrying.
Hosts own the directory, mnemonic storage and continuous wallet database backup.
Never share a directory between wallets, runtimes or symlink aliases. Opening
requires an existing wallet unless createIfMissing is explicit, and never
retries by recreating it. bark.backend.getWalletInfo() reports recovery as
complete, incomplete, failed, or not-run; incomplete/failed recovery can omit
funds from displayed balances. Disconnect drains queued work before closing both
native wallet handles. Retry failed disconnects before reopening.
The lower-level BarkReactNativeBackend remains available from
@kaleidorg/wallet-engine/backends/bark-react-native. Its BarkReactNativeConfig
uses serverUrl and an optional onchain flag; the protocol adapter uses the
shared BarkConfig (arkServerUrl) and always attaches BDK.
Build and tests cover SDK type compatibility, conversions, payment proof checks, wallet lifecycle races, manager routing and import isolation. They mock native execution. Android/iOS native linking and funded signet flows have not been run in this repository session; this remains a beta release. Verify them in a native development build before using the new backend with funds.