@arkade-os/swap 0.1.0-rc.7 → 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.
- package/README.md +252 -226
- package/dist/advanced.cjs +229 -65
- package/dist/advanced.d.cts +7 -7
- package/dist/advanced.d.ts +7 -7
- package/dist/advanced.js +3 -3
- package/dist/{chunk-BQLGQ4WB.js → chunk-JFKGMPMH.js} +287 -16
- package/dist/{chunk-6HIU2LNP.js → chunk-LLG7RD2T.js} +152 -10
- package/dist/{chunk-O3XMAZXS.js → chunk-NS3K6BZG.js} +45 -29
- package/dist/{errors-BVT_j3eG.d.ts → errors-CNISbN0M.d.ts} +10 -8
- package/dist/{errors-cn-FJT6r.d.cts → errors-DwNhIP0N.d.cts} +10 -8
- package/dist/index.cjs +503 -98
- package/dist/index.d.cts +63 -7
- package/dist/index.d.ts +63 -7
- package/dist/index.js +140 -5
- package/dist/lockupContract-C8-Mcg69.d.ts +664 -0
- package/dist/lockupContract-mIt4syse.d.cts +664 -0
- package/dist/node/index.d.cts +14 -15
- package/dist/node/index.d.ts +14 -15
- package/dist/nostr.cjs +77 -3
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- package/dist/protocol.cjs +346 -50
- package/dist/protocol.d.cts +8 -415
- package/dist/protocol.d.ts +8 -415
- package/dist/protocol.js +16 -10
- package/dist/repositories/realm/index.d.cts +2 -2
- package/dist/repositories/realm/index.d.ts +2 -2
- package/dist/repositories/sqlite/index.d.cts +2 -2
- package/dist/repositories/sqlite/index.d.ts +2 -2
- package/dist/{repository-BIHMUO6i.d.cts → repository-CGPLbjBe.d.ts} +14 -16
- package/dist/{repository-OUoAxV1Z.d.ts → repository-DSGGq0ct.d.cts} +14 -16
- package/dist/{rfq-RONK76D7.d.ts → rfq-BO6o7NwS.d.cts} +20 -24
- package/dist/{rfq-RONK76D7.d.cts → rfq-BO6o7NwS.d.ts} +20 -24
- package/dist/{watch-BUfoPEMC.d.cts → watch-DWyVqyEN.d.cts} +1 -1
- package/dist/{watch-Ci61IXyG.d.ts → watch-DmvHNX6T.d.ts} +1 -1
- package/package.json +5 -5
- package/dist/lockupContract-BvfhGH62.d.cts +0 -108
- package/dist/lockupContract-BvfhGH62.d.ts +0 -108
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
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.
|