Create the Client
Creating the client does no work. It opens no connection, reads no wallet state, and touches no storage until a call needs them. You can create it anywhere, such as in a component body, at no cost.client.ready once at startup. It reads the repository, finds the swaps
that were in progress when the process last stopped, and resumes them.
If you restore a wallet, run wallet.restore() before you create the
client. The client rebuilds swaps from the wallet’s history, so it finds only
what the history holds when it starts. Swaps saved by an older 0.0.x client in
the same repository are picked up automatically.
Keep nostr-tools in the install. It is an optional peer dependency, so
package managers skip it. The client uses it to reach solvers over Nostr: every
Lightning and onchain quote needs it, and discovery reads solver cards with it.
Choose Storage
The repository stores each swap’s record and secrets. There is no default storage and no in-memory fallback, because a swap with no stored record can be lost. Without a repository,accept throws MissingCorridorDep.
The React Native backends add no dependency. Each takes a database handle you
already opened. In Node,
createNodeSqlExecutor opens the file for you. It has
its own entry point, @arkade-os/swap/node, because it is the only one that
imports Node built-in modules.
Open one executor per file, for the life of the process. A second executor
over the same file can interleave two writes. swapDatabasePath names one file
per network in the platform’s config directory.
In Node the wallet needs SQLite storage
(Storage Repositories) and an EventSource
too. This client keeps the wallet and the swaps in two SQLite files, and loads
the mnemonic from the ARKADE_MNEMONIC environment variable:
Date
comes back as a string and a bigint throws on save.
Name Assets and Amounts
An asset id names an asset and the rail it settles on:<rail>:<network>/<asset-namespace>:<asset-reference>, a format called CAIP-19.
The rail is arkade, bolt11, or bitcoin, and BTC has one id per rail. On
mainnet, arkade:bitcoin/slip44:0 and bolt11:bitcoin/slip44:0 are the same
coin on two rails, and one id does not replace the other. Test networks use
slip44:1 for BTC.
Build ids with btcOn and arkadeAsset, not as strings. This example names BTC
and USDT on mutinynet, where the registry lists a BTC/USDT market:
canonicalAssetId passes a valid id through unchanged. It throws when no asset
matches the ticker, or when two different assets do.
The client counts amounts as bigint in the smallest unit, such as sats for
BTC. Amount converts to and from the decimal strings your interface shows:
Find Markets
A market is a pair of assets that a solver quotes, listed on the solver’s card. Discovery works with no configuration: the client reads the solver registry for the wallet’s network. To add a card that the registry does not list, pin it withdiscovery.localCards. The mainnet registry does not list the Lightning preview
solver, so this example pins its card, saved as beta-solver.card.json from
Lightning Payments:
sig signs them with its discovery_pubkey.
discovery.registryUrl points the client at another registry. A pinned card
adds to a registry. It does not replace one. With registryUrl: null,
discovery stops before it reads localCards, so nothing resolves. To work with
no network access, pass a fixed market list: discovery: { snapshot }.
You do not need client.markets() to swap, because quote finds the market
itself. Use it when your interface must show which pairs are available before
anyone asks for a price.
client.resolve(input) checks what the current market list would serve
without sending anything to a solver. Use it to reject a route before an
invoice or an amount leaves your application. It reads only the markets the
client already holds: pinned cards, markets that client.markets() or a
quote loaded, the repository’s cache, or discovery: { snapshot }. With none
of these, it throws DiscoverySnapshotUnavailable.
Handle Errors
Every swap error is thrown before money moves. After funding, the client reports a problem as an outcome instead. The swap errors are sixteen classes, each named for a condition, all exported from the package root.SWAP_ERROR_NAMES lists them, and isSwapError matches any of them:
UnsupportedRoute: no market the client holds serves the route at that amount. The client contacted no solver. Check the registry or pin a card.SwapRefusal: the solver declined, for example for an amount outside its bounds or for lack of inventory. Nothing was committed, so show the reason or ask again.
SwapDriveRefusedError, which recover and start throw, is not a swap error,
and isSwapError does not match it.
Follow a Swap
Every route reports progress with the sameOutcome values. When you subscribe,
you first get the current outcome of every swap the client knows, then each
change. A swap that finished before you subscribed still shows up:
onUpdate returns a function that unsubscribes. To read swaps without
subscribing, call client.swaps(), which takes an optional filter such as
{ outcome: 'open' }.
Which outcome means done depends on the route:
On any route,
accept(quote, { validUntil }) throws if it cannot fund the swap
before validUntil (Unix seconds), and the swap reports cancelled: nothing
was funded.
The values differ because the lockup holds different money on each route. The
lockup is the Arkade contract that holds a swap’s money until it is claimed or
refunded. On a send, it holds your money: if it is spent without a claim, your
money comes back, and refunded always means that. On a Lightning receive, the
lockup holds the solver’s money, so the same spend means your payment never
arrived. That outcome is lapsed: no payment arrived before the window closed,
or the solver took back a lockup that was not claimed in time. A deposit from a
Bitcoin address is the exception among receives: you fund it first, so if it
fails, your deposit comes back as refunded.
To take back an unfilled asset swap, see
Track and Cancel. Lightning and
onchain swaps end with a claim or a timelocked refund instead.
Recover a Swept Swap
needs_recovery is always reported, never retried silently, and never
final. It has two causes, and each needs a different response:
- Blocked. Nothing moved, and the money is still in the lockup.
swap.blockedReasonsays why. The client checks again on every pass, and the swap resumes when the cause clears. Show the reason. - Swept. The swap’s outputs expired and the operator swept them.
client.recover(swapId)renews them. This is the one manual step a swap can need.
recover tells the two apart. On a blocked swap it changes nothing and throws
SwapDriveRefusedError, so you can call it on every needs_recovery and show
the reason when it refuses:
SwapDriveRefusedError has a reason: nothing-swept (the blocked case
above), refund-window-open, readonly, no-recovery-support, or
unknown-swap.
Set the Client Policy
policy sets how much the client does on its own, and the most a swap may cost.
The drive is the background loop that claims, refunds, and recovers swaps.
policy.drive sets how much it does on its own:
You can call
start() and stop() more than once. stop() pauses the client:
actions in progress finish, records and contract registrations stay, and you
can start it again. Disposing the client ends that instance, but records and
wallet registrations remain. A new client on the same repository resumes the
work. In "readonly" mode, start() and recover() throw
SwapDriveRefusedError.
A policy fee ceiling can only make limits stricter. Before anything is
funded, the client applies the lower of policy.maxFee and the call’s own
maxFee:
Error, not one of the sixteen swap errors. The
client does not convert between assets. Asset swaps charge the fee in the asset
you receive, so a BTC ceiling refuses an asset swap from BTC to USDT.
Want to learn more?
Lightning Payments
Pay Lightning invoices from an Arkade wallet, and receive Lightning payments into it.
Onchain Payments
Pay Bitcoin addresses and take Bitcoin deposits through a solver.
Exchange Assets
Swap bitcoin for other assets, settled through Arkade.
Markets and Quotes
How quotes set the terms, and which contract settles each route.
Corridor Swaps
Routes with one endpoint outside Arkade, settled by paired HTLCs.
Asset Swaps
The swap flow for exchanges whose two sides settle through Arkade.