@arkade-os/swap 0.1.0-rc.8 → 0.1.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +252 -226
  2. package/dist/advanced.cjs +107 -40
  3. package/dist/advanced.d.cts +7 -7
  4. package/dist/advanced.d.ts +7 -7
  5. package/dist/advanced.js +3 -3
  6. package/dist/{chunk-4PATNGYQ.js → chunk-JFKGMPMH.js} +162 -3
  7. package/dist/{chunk-ARYFNDJY.js → chunk-LLG7RD2T.js} +74 -7
  8. package/dist/{chunk-HV7EIUE2.js → chunk-NS3K6BZG.js} +41 -20
  9. package/dist/{errors-CSpOUdCN.d.ts → errors-CNISbN0M.d.ts} +6 -4
  10. package/dist/{errors-DVn92LQd.d.cts → errors-DwNhIP0N.d.cts} +6 -4
  11. package/dist/index.cjs +202 -65
  12. package/dist/index.d.cts +6 -6
  13. package/dist/index.d.ts +6 -6
  14. package/dist/index.js +11 -3
  15. package/dist/{lockupContract-6TYZOBbn.d.ts → lockupContract-C8-Mcg69.d.ts} +177 -3
  16. package/dist/{lockupContract-aLCQQiSu.d.cts → lockupContract-mIt4syse.d.cts} +177 -3
  17. package/dist/node/index.d.cts +14 -15
  18. package/dist/node/index.d.ts +14 -15
  19. package/dist/nostr.cjs +1 -0
  20. package/dist/nostr.d.cts +1 -1
  21. package/dist/nostr.d.ts +1 -1
  22. package/dist/nostr.js +1 -1
  23. package/dist/protocol.cjs +187 -30
  24. package/dist/protocol.d.cts +7 -34
  25. package/dist/protocol.d.ts +7 -34
  26. package/dist/protocol.js +14 -9
  27. package/dist/repositories/realm/index.d.cts +2 -2
  28. package/dist/repositories/realm/index.d.ts +2 -2
  29. package/dist/repositories/sqlite/index.d.cts +2 -2
  30. package/dist/repositories/sqlite/index.d.ts +2 -2
  31. package/dist/{repository-tDT8p-Nx.d.ts → repository-CGPLbjBe.d.ts} +14 -16
  32. package/dist/{repository-D0gFF_GA.d.cts → repository-DSGGq0ct.d.cts} +14 -16
  33. package/dist/{rfq-DhkgiwQ7.d.ts → rfq-BO6o7NwS.d.cts} +0 -22
  34. package/dist/{rfq-DhkgiwQ7.d.cts → rfq-BO6o7NwS.d.ts} +0 -22
  35. package/dist/{watch-C4cMEeZT.d.cts → watch-DWyVqyEN.d.cts} +1 -1
  36. package/dist/{watch-DbWXdpzm.d.ts → watch-DmvHNX6T.d.ts} +1 -1
  37. package/package.json +4 -4
package/README.md CHANGED
@@ -1,226 +1,252 @@
1
- # @arkade-os/swap
2
-
3
- The swap client for [Arkade Intents](https://arkade.money). You state a route — what to give,
4
- what to take, and where the value ends up — and the client resolves the market, picks the
5
- contract, decodes the destination, seals the secret, funds with the right packet, persists before
6
- it watches, claims, and refunds. Framework-free TypeScript over `@arkade-os/sdk`: no DOM and no
7
- Node-specific APIs in the core, so it runs in Node, the browser and React Native alike.
8
-
9
- ```ts
10
- import { createSwapClient, IndexedDbAssetSwapRepository } from "@arkade-os/swap";
11
-
12
- const client = createSwapClient({ wallet, repository: new IndexedDbAssetSwapRepository() });
13
- ```
14
-
15
- Construction is synchronous and inert: no network, no wallet read, no repository open. The first
16
- call that needs one does it.
17
-
18
- Market discovery needs no configuration either: the client defaults to the reference solver
19
- registry's index for the wallet's network (`REGISTRY_URL[network]`, exported). Follow your own
20
- registry instead with `discovery: { registryUrl }`, or opt out of registries entirely with
21
- `discovery: { registryUrl: null }` — `discovery: { snapshot }` resolves against a fixed market
22
- list without touching the network.
23
-
24
- ## The four routes
25
-
26
- Each is one `quote` → `accept` chain. `quote` returns binding, verified terms and touches nothing
27
- durable; `accept` writes the record, then funds.
28
-
29
- ```ts
30
- const BTC = btcOn("arkade", "bitcoin"); // arkade:bitcoin/slip44:0
31
- const USDT = "arkade:bitcoin/asset:…";
32
-
33
- // arkade -> lightning: pay an invoice. The amount is the invoice's.
34
- await client.accept(await client.quote({ give: BTC, to: bolt11 }));
35
-
36
- // arkade -> arkade: swap one asset for another.
37
- await client.accept(
38
- await client.quote({ give: BTC, take: USDT, amount: 1_000_000n, amountOn: "give" }),
39
- );
40
-
41
- // arkade -> onchain: withdraw to a bitcoin address.
42
- await client.accept(
43
- await client.quote({ give: BTC, to: "bc1p…", amount: 100_000n, amountOn: "take" }),
44
- );
45
-
46
- // lightning -> arkade: receive. The artifact is the invoice the solver minted.
47
- const r = await client.receive({ via: "lightning", amount: 50_000n });
48
- showToPayer(r.artifact.bolt11);
49
- ```
50
-
51
- The receive is the one route that is not a two-step, and the asymmetry is deliberate. Its
52
- artifact is an invoice whose claim secret has to be durable before a payer can act on it, so
53
- `receive` returns only after `accept` has persisted. Reaching for `quote` and reading the invoice
54
- off it would show a payer an invoice this client could not yet claim; the verb removes the
55
- ordering from the caller.
56
-
57
- `onchain -> arkade` is not in the union. It resolves and quotes to `UnsupportedRoute` until the
58
- client owns the trader's L1 refund path end to end.
59
-
60
- An `arkade -> onchain` withdrawal claims the solver's L1 HTLC itself, and that claim's miner fee
61
- comes out of the HTLC output — so the client grosses the take leg up by the claim's cost and the
62
- recipient nets exactly the `amount` written, with the estimate folded into the reported `fee`. The
63
- claim is built and broadcast by the client itself once the fill is claimable, signed by the
64
- wallet's payout key and priced off a per-network fee-rate floor
65
- (`discovery`/corridor overrides aside, that floor is the only environment-specific input). A
66
- routing UI should raise it in congestion — the claim has a consensus deadline — via
67
- `corridors: { onchain: { claimFeeRateSatVb } }`; set it to `null` to take the claim over manually
68
- (the drive then reports the L1 half as your job rather than blocking it). Wire your own builder
69
- with `corridors: { onchain: { claim } }` and it wins over the default.
70
-
71
- ### One call instead of two
72
-
73
- `pay`, `receive` and `exchange` are `quote` → fee ceiling → `accept`, and add no capability the
74
- client did not already have. What they add is the ceiling; what they subtract is vocabulary — a
75
- product integrating payments never types the words route, corridor, market or quote.
76
-
77
- ```ts
78
- await client.pay(destination, { amount: 50_000n, maxFee: { amount: 500n, asset: BTC } });
79
- await client.receive({ via: "lightning", amount: 50_000n });
80
- await client.exchange({ give: BTC, take: USDT, amount: 1_000_000n, amountOn: "give" });
81
- ```
82
-
83
- `pay` takes any of the four destination forms — a bolt11 invoice, a bitcoin address, an Arkade
84
- address, or a BIP21 URI carrying one of them — and exactly one corridor claims each. A plain
85
- Arkade address is not a swap and does not become one: same asset, same rail, rate 1. It returns a
86
- txid and no swap id, which is why `PayResult` has two arms.
87
-
88
- Omit `amount` exactly when the destination pins it. An amount-bearing invoice does; passing one
89
- beside it is `AmountMismatch` rather than a silent preference.
90
-
91
- ## Asset ids and amounts
92
-
93
- Asset ids are CAIP-19 with the rail as the CAIP-2 namespace, `<rail>:<network>/<namespace>:<ref>`.
94
- `arkade`, `bolt11` and `bitcoin` are the implemented rails, and BTC has one id per rail. Use
95
- `btcOn(rail, network)` and `arkadeAsset(network, id)` rather than writing the strings, and
96
- `canonicalAssetId` when the input is human:
97
-
98
- ```ts
99
- const asset = canonicalAssetId("BTC", {
100
- network: "regtest",
101
- assets: [{ ticker: "BTC", id: "arkade:regtest/slip44:0" }],
102
- });
103
- ```
104
-
105
- Ticker matching is case-insensitive, scoped to the wallet's network, and refuses a collision
106
- instead of guessing.
107
-
108
- Amounts are `bigint` atomic units everywhere inside the client. Decimal strings exist at two
109
- boundaries and mean different things at each: display decimals (`"0.001"`) belong to the UI, atomic
110
- decimals (`"100000"`) belong to records and RFQ payloads. `Amount.parse` and `Amount.format` cross
111
- the first; the client crosses the second itself.
112
-
113
- ## Watching, history and cancelling
114
-
115
- There is no required `start()`. The client reads its repository once — `await client.ready` is
116
- that read — and arms the drive when it finds live work, or on the first `accept` when it does
117
- not. `start()` and `stop()` exist for manual control; `stop()` is a pause, not a cancellation, and
118
- disposal is terminal cleanup that leaves every durable record and wallet registration recoverable.
119
-
120
- ```ts
121
- const off = client.onUpdate(({ swap, outcome, detail }) => render(swap.id, outcome, detail));
122
-
123
- const live = await client.swaps({ outcome: "funded" });
124
- const { outcome } = await client.cancel(swapId);
125
- const result = await client.recover(swapId);
126
- ```
127
-
128
- `onUpdate` replays the current outcome of every swap it knows, then streams transitions, keyed on
129
- the derived outcome so a legal backslide is delivered once. `Outcome` is one trader-centric
130
- vocabulary across both families: `refunded` always means the value came back and a receive-leg
131
- solver reclaim is `lapsed`, never the same word. The protocol's own state string is on `detail`
132
- for logs.
133
-
134
- `cancel` is typed to asset swaps, because that is where a cancel right exists. Corridor swaps
135
- decompose into quote expiry, a timelocked refund and a lapse instead. A cancel that loses the race
136
- to a fill reports the fill rather than throwing.
137
-
138
- ## Errors
139
-
140
- Sixteen classes, each a condition noun, all reachable from the root; `SWAP_ERROR_NAMES` is the
141
- complete list. `SwapRefusal` is the solver declining — a decision, not a fault — and it is the one
142
- member the protocol layer owns. Everything else names what the client refused and why:
143
- `UnsupportedRoute`, `AmbiguousDestination`, `AmountMismatch`, `QuoteExpired`, `MaxFeeExceeded`,
144
- `InsufficientFunds`, `QuoteVerificationFailed`, `NotCancellable`, `ClientDisposed`,
145
- `MissingCorridorDep` and the rest.
146
-
147
- ## Storage backends
148
-
149
- | Backend | Import from | For |
150
- | ------------------------------ | ------------------------------------- | ----------------------------------------------- |
151
- | `InMemoryAssetSwapRepository` | `@arkade-os/swap` | tests, one-shot scripts — nothing survives exit |
152
- | `IndexedDbAssetSwapRepository` | `@arkade-os/swap` | the browser (or a polyfilled IndexedDB) |
153
- | `SQLiteAssetSwapRepository` | `@arkade-os/swap/repositories/sqlite` | React Native, over your SQLite driver |
154
- | `RealmAssetSwapRepository` | `@arkade-os/swap/repositories/realm` | React Native, over your Realm instance |
155
- | `nodeSwapRepository()` | `@arkade-os/swap/node` | Node — file-backed SQLite, opened for you |
156
-
157
- There is no implicit default and never an in-memory fallback: accepting a swap with nowhere to
158
- write it is the silent loss the rule exists to forbid, so `accept` refuses with
159
- `MissingCorridorDep("arkade", "repository")` instead. In-memory is available, explicitly, which is
160
- the only way ephemeral storage is on the table.
161
-
162
- Neither React Native subpath adds a dependency — they take the SDK's structural `SQLExecutor` and
163
- `RealmLike` handles, so you pass the database you already opened. `@arkade-os/swap/node` is the
164
- exception and the only entry point that imports `node:` builtins, which is why it is a subpath
165
- rather than something the main entry falls back to. It opens the database under the platform
166
- config directory at `arkade/swaps/swaps-<network>.sqlite`, and it is the one backend whose
167
- disposal closes a connection, because it is the one that opened it:
168
-
169
- ```ts
170
- import { nodeSwapRepository } from "@arkade-os/swap/node";
171
-
172
- await using swaps = nodeSwapRepository({ network: "mainnet" }); // or { path } to choose the file
173
- ```
174
-
175
- Records are stored whole. The SQLite and Realm backends serialize each record to JSON with only
176
- the queryable columns mapped out, so a field they do not know about survives — which is what a
177
- consumer's cast-extended record relies on. JSON is narrower than IndexedDB's structured clone,
178
- though: a `Date` in a field you added comes back an ISO string, a `Set` or `Map` comes back empty,
179
- and a `bigint` throws on save. The package's own records are JSON-safe by design; keep yours that
180
- way too.
181
-
182
- ## Runtime requirements
183
-
184
- The one global the core requires is `crypto.getRandomValues`. Node and browsers have it; React
185
- Native does not, so install `react-native-get-random-values` (or `expo-crypto`) and import it
186
- before this package. `crypto.subtle` is unused. `EventSource` and `WebSocket` are needed only by
187
- the watch and relay transports, each of which takes an injected implementation.
188
-
189
- The client takes no server URL anywhere. Server info, chain reads and broadcast are all derived
190
- from the wallet, which is the single place that knows which operator it speaks to.
191
-
192
- ## Subpaths
193
-
194
- | Subpath | What it is |
195
- | ---------------------------------- | ----------------------------------------------------------------- |
196
- | `@arkade-os/swap` | the client, the verbs, the vocabulary, the error taxonomy |
197
- | `@arkade-os/swap/advanced` | the orchestration below the verbs: the drive, corridors, RFQ wire |
198
- | `@arkade-os/swap/node` | the Node storage default |
199
- | `@arkade-os/swap/repositories/*` | the React Native backends |
200
- | `@arkade-os/swap/nostr` | the Nostr RFQ transport, for hand-building one |
201
- | `@arkade-os/swap/protocol` | the v1 building blocks, deprecated |
202
-
203
- The root is a curated surface — if a name the client's modules define is not on it, it is on
204
- `./advanced` (manual driving with `createSwapDrive`, destination claiming with `corridorSet`,
205
- custom quote flows with `acceptQuote`/`quoteViaRfq`, record reading with `recordLeg`/`swapOf`).
206
- "Advanced" is a deliberate deep subpath, not a second compatibility promise: those names move
207
- with the client's internals across minor versions; the root is what stays put.
208
-
209
- `./nostr` is a floor and not a deprecation: the client opens the card's rendezvous itself, and the
210
- subpath is what keeps that an escape hatch rather than a wall. It is a separate entry point
211
- because `nostr-tools` is an optional peer dependency, so a consumer who never hand-builds a
212
- transport never pays for it.
213
-
214
- `./protocol` is the other kind of subpath, and it is a floor rather than a staging area. Every
215
- name on it was the integration surface before this client — requests, covenants, records, the RFQ
216
- manager, the restore scan — and each carries an `@deprecated` pointer naming what replaces it.
217
- None of them is on the root: this release breaks against `0.1.0-rc.1` regardless, so a period of
218
- re-exports would have split one migration into two and left 200 v1 names on a root whose claim is
219
- to be the v2 surface. Nothing on the subpath is scheduled to be removed, and the tags mean "the
220
- client does this for you now" rather than "this goes away next release". `MIGRATION.md` has the
221
- table, including the names that have no floor and why.
222
-
223
- ## Further reading
224
-
225
- - [MIGRATION.md](./MIGRATION.md) — every rename, every removal, and where each v1 name went.
226
- - [V2_API.md](./V2_API.md) — the developer UX note on the client surface.
1
+ # @arkade-os/swap
2
+
3
+ The swap client for [Arkade Intents](https://arkade.money). You state a route — what to give,
4
+ what to take, and where the value ends up — and the client resolves the market, picks the
5
+ contract, decodes the destination, seals the secret, funds with the right packet, persists before
6
+ it watches, claims, and refunds. Framework-free TypeScript over `@arkade-os/sdk`: no DOM and no
7
+ Node-specific APIs in the core, so it runs in Node, the browser and React Native alike.
8
+
9
+ ```ts
10
+ import { createSwapClient, IndexedDbAssetSwapRepository } from "@arkade-os/swap";
11
+
12
+ const client = createSwapClient({ wallet, repository: new IndexedDbAssetSwapRepository() });
13
+ ```
14
+
15
+ Construction is synchronous and inert: no network, no wallet read, no repository open. The first
16
+ call that needs one does it.
17
+
18
+ Market discovery needs no configuration either: the client defaults to the reference solver
19
+ registry's index for the wallet's network (`REGISTRY_URL[network]`, exported). Follow your own
20
+ registry instead with `discovery: { registryUrl }`, or opt out of registries entirely with
21
+ `discovery: { registryUrl: null }` — `discovery: { snapshot }` resolves against a fixed market
22
+ list without touching the network.
23
+
24
+ ## The four routes
25
+
26
+ Each is one `quote` → `accept` chain. `quote` returns binding, verified terms and touches nothing
27
+ durable; `accept` writes the record, then funds.
28
+
29
+ ```ts
30
+ const BTC = btcOn("arkade", "bitcoin"); // arkade:bitcoin/slip44:0
31
+ const USDT = "arkade:bitcoin/asset:…";
32
+
33
+ // arkade -> lightning: pay an invoice. The amount is the invoice's.
34
+ await client.accept(await client.quote({ give: BTC, to: bolt11 }));
35
+
36
+ // arkade -> arkade: swap one asset for another.
37
+ await client.accept(
38
+ await client.quote({ give: BTC, take: USDT, amount: 1_000_000n, amountOn: "give" }),
39
+ );
40
+
41
+ // arkade -> onchain: withdraw to a bitcoin address.
42
+ await client.accept(
43
+ await client.quote({ give: BTC, to: "bc1p…", amount: 100_000n, amountOn: "take" }),
44
+ );
45
+
46
+ // lightning -> arkade: receive. The artifact is the invoice the solver minted.
47
+ const r = await client.receive({ via: "lightning", amount: 50_000n });
48
+ showToPayer(r.artifact.bolt11);
49
+ ```
50
+
51
+ The receive is the one route that is not a two-step, and the asymmetry is deliberate. Its
52
+ artifact is an invoice whose claim secret has to be durable before a payer can act on it, so
53
+ `receive` returns only after `accept` has persisted. Reaching for `quote` and reading the invoice
54
+ off it would show a payer an invoice this client could not yet claim; the verb removes the
55
+ ordering from the caller.
56
+
57
+ `onchain -> arkade` is not in the union. It resolves and quotes to `UnsupportedRoute` until the
58
+ client owns the trader's L1 refund path end to end.
59
+
60
+ An `arkade -> onchain` withdrawal claims the solver's L1 HTLC itself, and that claim's miner fee
61
+ comes out of the HTLC output — so the client grosses the take leg up by the claim's cost and the
62
+ recipient nets exactly the `amount` written, with the estimate folded into the reported `fee`. The
63
+ claim is built and broadcast by the client itself once the fill is claimable, signed by the
64
+ wallet's payout key and priced off a per-network fee-rate floor
65
+ (`discovery`/corridor overrides aside, that floor is the only environment-specific input). A
66
+ routing UI should raise it in congestion — the claim has a consensus deadline — via
67
+ `corridors: { onchain: { claimFeeRateSatVb } }`; set it to `null` to take the claim over manually
68
+ (the drive then reports the L1 half as your job rather than blocking it). Wire your own builder
69
+ with `corridors: { onchain: { claim } }` and it wins over the default.
70
+
71
+ ### One call instead of two
72
+
73
+ `pay`, `receive` and `exchange` are `quote` → fee ceiling → `accept`, and add no capability the
74
+ client did not already have. What they add is the ceiling; what they subtract is vocabulary — a
75
+ product integrating payments never types the words route, corridor, market or quote.
76
+
77
+ ```ts
78
+ await client.pay(destination, { amount: 50_000n, maxFee: { amount: 500n, asset: BTC } });
79
+ await client.receive({ via: "lightning", amount: 50_000n });
80
+ await client.exchange({ give: BTC, take: USDT, amount: 1_000_000n, amountOn: "give" });
81
+ ```
82
+
83
+ `pay` takes any of the four destination forms — a bolt11 invoice, a bitcoin address, an Arkade
84
+ address, or a BIP21 URI carrying one of them — and exactly one corridor claims each. A plain
85
+ Arkade address is not a swap and does not become one: same asset, same rail, rate 1. It returns a
86
+ txid and no swap id, which is why `PayResult` has two arms.
87
+
88
+ Omit `amount` exactly when the destination pins it. An amount-bearing invoice does; passing one
89
+ beside it is `AmountMismatch` rather than a silent preference.
90
+
91
+ ## Asset ids and amounts
92
+
93
+ Asset ids are CAIP-19 with the rail as the CAIP-2 namespace, `<rail>:<network>/<namespace>:<ref>`.
94
+ `arkade`, `bolt11` and `bitcoin` are the implemented rails, and BTC has one id per rail. Use
95
+ `btcOn(rail, network)` and `arkadeAsset(network, id)` rather than writing the strings, and
96
+ `canonicalAssetId` when the input is human:
97
+
98
+ ```ts
99
+ const asset = canonicalAssetId("BTC", {
100
+ network: "regtest",
101
+ assets: [{ ticker: "BTC", id: "arkade:regtest/slip44:0" }],
102
+ });
103
+ ```
104
+
105
+ Ticker matching is case-insensitive, scoped to the wallet's network, and refuses a collision
106
+ instead of guessing.
107
+
108
+ Amounts are `bigint` atomic units everywhere inside the client. Decimal strings exist at two
109
+ boundaries and mean different things at each: display decimals (`"0.001"`) belong to the UI, atomic
110
+ decimals (`"100000"`) belong to records and RFQ payloads. `Amount.parse` and `Amount.format` cross
111
+ the first; the client crosses the second itself.
112
+
113
+ ## Watching, history and cancelling
114
+
115
+ There is no required `start()`. The client reads its repository once — `await client.ready` is
116
+ that read — and arms the drive when it finds live work, or on the first `accept` when it does
117
+ not. `start()` and `stop()` exist for manual control; `stop()` is a pause, not a cancellation, and
118
+ disposal is terminal cleanup that leaves every durable record and wallet registration recoverable.
119
+
120
+ ```ts
121
+ const off = client.onUpdate(({ swap, outcome, detail }) => render(swap.id, outcome, detail));
122
+
123
+ const live = await client.swaps({ outcome: "funded" });
124
+ const { outcome } = await client.cancel(swapId);
125
+ const result = await client.recover(swapId);
126
+ ```
127
+
128
+ `onUpdate` replays the current outcome of every swap it knows, then streams transitions, keyed on
129
+ the derived outcome so a legal backslide is delivered once. `Outcome` is one trader-centric
130
+ vocabulary across both families: `refunded` always means the value came back and a receive-leg
131
+ solver reclaim is `lapsed`, never the same word. The protocol's own state string is on `detail`
132
+ for logs.
133
+
134
+ `cancel` is typed to asset swaps, because that is where a cancel right exists. Corridor swaps
135
+ decompose into quote expiry, a timelocked refund and a lapse instead. A cancel that loses the race
136
+ to a fill reports the fill rather than throwing.
137
+
138
+ ## Errors
139
+
140
+ Sixteen classes, each a condition noun, all reachable from the root; `SWAP_ERROR_NAMES` is the
141
+ complete list. `SwapRefusal` is the solver declining — a decision, not a fault — and it is the one
142
+ member the protocol layer owns. Everything else names what the client refused and why:
143
+ `UnsupportedRoute`, `AmbiguousDestination`, `AmountMismatch`, `QuoteExpired`, `MaxFeeExceeded`,
144
+ `InsufficientFunds`, `QuoteVerificationFailed`, `NotCancellable`, `ClientDisposed`,
145
+ `MissingCorridorDep` and the rest.
146
+
147
+ ## Storage backends
148
+
149
+ | Backend | Import from | For |
150
+ | ------------------------------ | ------------------------------------- | ----------------------------------------------- |
151
+ | `InMemoryAssetSwapRepository` | `@arkade-os/swap` | tests, one-shot scripts — nothing survives exit |
152
+ | `IndexedDbAssetSwapRepository` | `@arkade-os/swap` | the browser (or a polyfilled IndexedDB) |
153
+ | `SQLiteAssetSwapRepository` | `@arkade-os/swap/repositories/sqlite` | React Native, over your SQLite driver |
154
+ | `RealmAssetSwapRepository` | `@arkade-os/swap/repositories/realm` | React Native, over your Realm instance |
155
+ | `nodeSwapRepository()` | `@arkade-os/swap/node` | Node — file-backed SQLite, opened for you |
156
+
157
+ There is no implicit default and never an in-memory fallback: accepting a swap with nowhere to
158
+ write it is the silent loss the rule exists to forbid, so `accept` refuses with
159
+ `MissingCorridorDep("arkade", "repository")` instead. In-memory is available, explicitly, which is
160
+ the only way ephemeral storage is on the table.
161
+
162
+ Neither React Native subpath adds a dependency — they take the SDK's structural `SQLExecutor` and
163
+ `RealmLike` handles, so you pass the database you already opened. `@arkade-os/swap/node` is the
164
+ exception and the only entry point that imports `node:` builtins, which is why it is a subpath
165
+ rather than something the main entry falls back to. It opens the database under the platform
166
+ config directory at `arkade/swaps/swaps-<network>.sqlite`, and it is the one backend whose
167
+ disposal closes a connection, because it is the one that opened it:
168
+
169
+ ```ts
170
+ import { nodeSwapRepository } from "@arkade-os/swap/node";
171
+
172
+ await using swaps = nodeSwapRepository({ network: "mainnet" }); // or { path } to choose the file
173
+ ```
174
+
175
+ Records are stored whole. The SQLite and Realm backends serialize each record to JSON with only
176
+ the queryable columns mapped out, so a field they do not know about survives — which is what a
177
+ consumer's cast-extended record relies on. JSON is narrower than IndexedDB's structured clone,
178
+ though: a `Date` in a field you added comes back an ISO string, a `Set` or `Map` comes back empty,
179
+ and a `bigint` throws on save. The package's own records are JSON-safe by design; keep yours that
180
+ way too.
181
+
182
+ ### Restore an imported wallet
183
+
184
+ Register swap recovery before calling the core wallet's explicit `restore()`:
185
+
186
+ ```ts
187
+ import { IndexedDbAssetSwapRepository, registerAssetSwapRestore } from "@arkade-os/swap";
188
+
189
+ const repository = new IndexedDbAssetSwapRepository();
190
+ const unregisterSwapRestore = registerAssetSwapRestore(wallet, {
191
+ arkServerUrl,
192
+ repository,
193
+ onResult: ({ changes, coverageError }) => {
194
+ if (coverageError) console.warn("Swap coverage was incomplete", coverageError);
195
+ console.info(`Restored or updated ${changes.length} swaps`);
196
+ },
197
+ });
198
+
199
+ await wallet.restore();
200
+ ```
201
+
202
+ Core address, contract, history, and balance recovery finishes before the swap scan. Registering
203
+ again replaces the prior hook, so setup is idempotent; call `unregisterSwapRestore()` when the
204
+ integration no longer owns the wallet. A proxy or custom `IWallet` must also pass `indexer` and
205
+ `serverPubkey` when it does not expose them. Keep calling `restoreAssetSwapRepository` directly
206
+ during ordinary startup: hooks run only for an explicit `wallet.restore()`.
207
+
208
+ ## Runtime requirements
209
+
210
+ The one global the core requires is `crypto.getRandomValues`. Node and browsers have it; React
211
+ Native does not, so install `react-native-get-random-values` (or `expo-crypto`) and import it
212
+ before this package. `crypto.subtle` is unused. `EventSource` and `WebSocket` are needed only by
213
+ the watch and relay transports, each of which takes an injected implementation.
214
+
215
+ The client takes no server URL anywhere. Server info, chain reads and broadcast are all derived
216
+ from the wallet, which is the single place that knows which operator it speaks to.
217
+
218
+ ## Subpaths
219
+
220
+ | Subpath | What it is |
221
+ | ---------------------------------- | ----------------------------------------------------------------- |
222
+ | `@arkade-os/swap` | the client, the verbs, the vocabulary, the error taxonomy |
223
+ | `@arkade-os/swap/advanced` | the orchestration below the verbs: the drive, corridors, RFQ wire |
224
+ | `@arkade-os/swap/node` | the Node storage default |
225
+ | `@arkade-os/swap/repositories/*` | the React Native backends |
226
+ | `@arkade-os/swap/nostr` | the Nostr RFQ transport, for hand-building one |
227
+ | `@arkade-os/swap/protocol` | the v1 building blocks, deprecated |
228
+
229
+ The root is a curated surface — if a name the client's modules define is not on it, it is on
230
+ `./advanced` (manual driving with `createSwapDrive`, destination claiming with `corridorSet`,
231
+ custom quote flows with `acceptQuote`/`quoteViaRfq`, record reading with `recordLeg`/`swapOf`).
232
+ "Advanced" is a deliberate deep subpath, not a second compatibility promise: those names move
233
+ with the client's internals across minor versions; the root is what stays put.
234
+
235
+ `./nostr` is a floor and not a deprecation: the client opens the card's rendezvous itself, and the
236
+ subpath is what keeps that an escape hatch rather than a wall. It is a separate entry point
237
+ because `nostr-tools` is an optional peer dependency, so a consumer who never hand-builds a
238
+ transport never pays for it.
239
+
240
+ `./protocol` is the other kind of subpath, and it is a floor rather than a staging area. Every
241
+ name on it was the integration surface before this client — requests, covenants, records, the RFQ
242
+ manager, the restore scan — and each carries an `@deprecated` pointer naming what replaces it.
243
+ None of them is on the root: this release breaks against `0.1.0-rc.1` regardless, so a period of
244
+ re-exports would have split one migration into two and left 200 v1 names on a root whose claim is
245
+ to be the v2 surface. Nothing on the subpath is scheduled to be removed, and the tags mean "the
246
+ client does this for you now" rather than "this goes away next release". `MIGRATION.md` has the
247
+ table, including the names that have no floor and why.
248
+
249
+ ## Further reading
250
+
251
+ - [MIGRATION.md](./MIGRATION.md) — every rename, every removal, and where each v1 name went.
252
+ - [V2_API.md](./V2_API.md) — the developer UX note on the client surface.