@arkade-os/swap 0.0.5 → 0.0.7

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 CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  Client-side [Arkade Intents](https://arkade.money) asset swaps: discover markets, quote and
4
4
  validate, create offers, track them, cancel them, and rebuild the whole record set from chain after
5
- a wallet restore. Framework-free TypeScript over `@arkade-os/sdk`: the core API and
6
- `InMemoryAssetSwapRepository` use no DOM and no Node-specific APIs, so they run in Node, the
7
- browser, and React Native alike. `IndexedDbAssetSwapRepository` is the one exception it needs a
8
- platform-provided or polyfilled IndexedDB.
5
+ a wallet restore. Framework-free TypeScript over `@arkade-os/sdk`: the core API uses no DOM and no
6
+ Node-specific APIs, so it runs in Node, the browser, and React Native alike. Four storage backends
7
+ ship in-memory (anywhere, nothing outlives the process), IndexedDB (browser), SQLite and Realm
8
+ (React Native, on subpath entry points) — see "Storage backends" below.
9
9
 
10
10
  ## Roles
11
11
 
@@ -100,11 +100,78 @@ arkade:BTC|asset` (quote, then take by funding an offer from layer 1).
100
100
 
101
101
  Everything the package persists — swap records, the restore-scan cursor, and the markets cache —
102
102
  goes through a single `AssetSwapRepository`, following the Arkade repository convention
103
- (versioned interface, `AsyncDisposable`, one backend per platform). Two backends ship here:
104
- `InMemoryAssetSwapRepository` and `IndexedDbAssetSwapRepository` (built on the SDK's shared
105
- IndexedDB manager, like the Boltz plugin's repositories). Construct one and pass it wherever the
106
- package asks for a repository; `discoverMarkets` also accepts none, for a one-shot uncached
107
- discovery.
103
+ (versioned interface, `AsyncDisposable`, one backend per platform). Construct one and pass it
104
+ wherever the package asks for a repository; `discoverMarkets` also accepts none, for a one-shot
105
+ uncached discovery.
106
+
107
+ ## Storage backends
108
+
109
+ | Backend | Import from | For |
110
+ | ------------------------------ | ------------------------------------- | ----------------------------------------------- |
111
+ | `InMemoryAssetSwapRepository` | `@arkade-os/swap` | tests, one-shot scripts — nothing survives exit |
112
+ | `IndexedDbAssetSwapRepository` | `@arkade-os/swap` | the browser (or a polyfilled IndexedDB) |
113
+ | `SQLiteAssetSwapRepository` | `@arkade-os/swap/repositories/sqlite` | React Native, over your SQLite driver |
114
+ | `RealmAssetSwapRepository` | `@arkade-os/swap/repositories/realm` | React Native, over your Realm instance |
115
+
116
+ Neither subpath adds a dependency: they take the SDK's structural `SQLExecutor` / `RealmLike`
117
+ handles, so you pass the database you already opened.
118
+
119
+ **Records are stored whole.** The SQLite and Realm backends serialize each record to **JSON** in a
120
+ `data` column, with only `status` / `createdAt` mapped out for querying — so a field they do not
121
+ know about survives, which is what the `quote`-shaped extension in `MIGRATION.md` relies on. JSON is
122
+ the boundary, though, and it is narrower than IndexedDB's structured clone: a `Date` in a
123
+ consumer-added field comes back an ISO **string**, a `Set` or `Map` comes back empty, and a `bigint`
124
+ makes `saveSwap` **throw**. `AssetSwap` itself is JSON-safe by design (amounts are strings); keep
125
+ your own added fields that way too.
126
+
127
+ ### SQLite
128
+
129
+ ```ts
130
+ import { SQLiteAssetSwapRepository } from "@arkade-os/swap/repositories/sqlite";
131
+ import { SQLiteWalletRepository, type SQLExecutor } from "@arkade-os/sdk/repositories/sqlite";
132
+
133
+ const db = await SQLite.openDatabaseAsync("wallet.db"); // expo-sqlite
134
+ // Build the executor ONCE and hand this same instance to every repository on
135
+ // the database: the SDK serializes transactions in a chain keyed by this
136
+ // object, so a per-repository literal splits the chain and two BEGIN
137
+ // IMMEDIATEs can interleave.
138
+ const executor: SQLExecutor = {
139
+ run: (sql, params) => db.runAsync(sql, params ?? []),
140
+ get: (sql, params) => db.getFirstAsync(sql, params ?? []),
141
+ all: (sql, params) => db.getAllAsync(sql, params ?? []),
142
+ };
143
+
144
+ const swaps = new SQLiteAssetSwapRepository(executor);
145
+ const wallet = new SQLiteWalletRepository(executor); // same instance
146
+ ```
147
+
148
+ Sharing the executor is **necessary** for that serialization, not sufficient for atomicity across
149
+ all wallet storage: it disciplines the repositories that enter the chain — this one,
150
+ `SQLiteIntentRepository`, `SQLiteVirtualTxRepository`, and the wallet repository's migration path —
151
+ and nothing else. `SQLiteWalletRepository` and `SQLiteContractRepository` still write raw, so their
152
+ writes can land inside whatever transaction happens to be open.
153
+
154
+ Three tables land in your database, prefixed `arkade_`: `arkade_asset_swaps`,
155
+ `arkade_asset_swap_scanned_txids`, `arkade_asset_swap_markets`. Pass `{ prefix: "myapp_" }` if your
156
+ app already owns those names.
157
+
158
+ ### Realm
159
+
160
+ ```ts
161
+ import Realm from "realm";
162
+ import { AssetSwapRealmSchemas, RealmAssetSwapRepository } from "@arkade-os/swap/repositories/realm";
163
+ import { ArkRealmSchemas } from "@arkade-os/sdk/repositories/realm";
164
+
165
+ const realm = await Realm.open({
166
+ schema: [...ArkRealmSchemas, ...AssetSwapRealmSchemas, ...yourOwnSchemas],
167
+ schemaVersion: YOUR_VERSION, // these schemas are new: bump yours when adding them
168
+ });
169
+ const swaps = new RealmAssetSwapRepository(realm);
170
+ ```
171
+
172
+ Three classes land in your Realm namespace: `ArkadeAssetSwap`, `ArkadeAssetSwapScannedTxid`,
173
+ `ArkadeAssetSwapMarketsCache`. Unlike SQLite there is no prefix option — a Realm schema name is
174
+ baked into the schema objects you register — so reconcile against your own models by name.
108
175
 
109
176
  ## Creating an offer
110
177
 
@@ -223,7 +290,9 @@ message anywhere: **acceptance is funding**.
223
290
  offline. The solver observes the funding on-chain, pays the invoice, and claims with the
224
291
  preimage — which lands publicly in the claim witness as the receipt. A failed swap refunds by
225
292
  covenant to the trader's address, pushable by anyone, no trader keys or state.
226
- - **Arkade ↔ arkade** (BTC↔asset, asset↔asset): the trader accepts a quote by creating and funding
293
+ - **Arkade ↔ arkade** (BTC↔asset, asset↔asset): an arkade asset leg names the asset id itself —
294
+ `arkade:<68-hex>`, built with `arkadeAssetLeg` (the deprecated coarse `ARKADE_ASSET` is served by
295
+ no solver). The trader accepts a quote by creating and funding
227
296
  an **offer** (layer 1) bound to the quoted terms before `valid_until`. The offer covenant only
228
297
  releases the deposit to a fill that delivers the quoted amount, so the solver fills or nothing
229
298
  moves; an unfilled offer is cancelled cooperatively. The quote wire shape ships here; the
@@ -0,0 +1,38 @@
1
+ // src/repository.ts
2
+ var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
3
+ var InMemoryAssetSwapRepository = class {
4
+ version = 2;
5
+ swaps = /* @__PURE__ */ new Map();
6
+ scanned = /* @__PURE__ */ new Set();
7
+ markets = /* @__PURE__ */ new Map();
8
+ async saveSwap(swap) {
9
+ this.swaps.set(swap.id, swap);
10
+ }
11
+ async getAllSwaps() {
12
+ return [...this.swaps.values()];
13
+ }
14
+ async getScannedTxids() {
15
+ return new Set(this.scanned);
16
+ }
17
+ async markTxidsScanned(txids) {
18
+ for (const txid of txids) this.scanned.add(txid);
19
+ }
20
+ async getCachedMarkets(network, registry) {
21
+ return this.markets.get(marketsCacheKey(network, registry));
22
+ }
23
+ async saveCachedMarkets(network, registry, entry) {
24
+ this.markets.set(marketsCacheKey(network, registry), entry);
25
+ }
26
+ async clear() {
27
+ this.swaps.clear();
28
+ this.scanned.clear();
29
+ this.markets.clear();
30
+ }
31
+ async [Symbol.asyncDispose]() {
32
+ }
33
+ };
34
+
35
+ export {
36
+ marketsCacheKey,
37
+ InMemoryAssetSwapRepository
38
+ };
@@ -311,9 +311,10 @@ var solverHex = (value, field) => {
311
311
  }
312
312
  };
313
313
  var ARKADE_BTC = "arkade:BTC";
314
- var ARKADE_ASSET = "arkade:ASSET";
315
314
  var LIGHTNING_BTC = "lightning:BTC";
316
315
  var ONCHAIN_BTC = "onchain:BTC";
316
+ var arkadeAssetLeg = (id) => `arkade:${id.toString()}`;
317
+ var ARKADE_ASSET = "arkade:ASSET";
317
318
  var rfqPair = (from, to) => `${from}->${to}`;
318
319
  var LIGHTNING_SEND_PAIR = rfqPair(ARKADE_BTC, LIGHTNING_BTC);
319
320
  var LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
@@ -354,23 +355,34 @@ var lightningSendRequest = (input) => ({
354
355
  }
355
356
  });
356
357
  var arkadeSwapRequest = (input) => {
357
- if (Boolean(input.wantAsset) === Boolean(input.offerAsset)) {
358
- throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
358
+ if (!input.wantAsset && !input.offerAsset) {
359
+ throw new Error(
360
+ "set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC) \u2014 with neither set both legs are BTC, which is not a swap"
361
+ );
359
362
  }
363
+ if (input.wantAsset && input.offerAsset) {
364
+ throw new Error(
365
+ "set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC) \u2014 asset->asset is nameable on the wire but no solver quotes it yet"
366
+ );
367
+ }
368
+ const pair = rfqPair(
369
+ input.offerAsset ? arkadeAssetLeg(input.offerAsset) : ARKADE_BTC,
370
+ input.wantAsset ? arkadeAssetLeg(input.wantAsset) : ARKADE_BTC
371
+ );
372
+ assertPairLength(pair);
360
373
  return {
361
374
  v: 1,
362
375
  type: "rfq_request",
363
376
  rfq_id: input.rfqId,
364
- pair: rfqPair(
365
- input.offerAsset ? ARKADE_ASSET : ARKADE_BTC,
366
- input.wantAsset ? ARKADE_ASSET : ARKADE_BTC
367
- ),
377
+ pair,
368
378
  amount_side: input.amountSide,
369
379
  amount: input.amount,
370
- profile: {
371
- ...input.offerAsset && { offer_asset: hex3.encode(input.offerAsset.serialize()) },
372
- ...input.wantAsset && { want_asset: hex3.encode(input.wantAsset.serialize()) }
373
- }
380
+ // The pair is the only place the asset ids appear. Repeating them here
381
+ // would be a key the solver's `.strict()` profile schema does not
382
+ // declare, and an undeclared key is `unsupported_payload` — a refusal,
383
+ // not an ignored extra. Empty, not absent: `profile` is required on
384
+ // every other request shape this wire carries.
385
+ profile: {}
374
386
  };
375
387
  };
376
388
  var MIN_HEADROOM_SECONDS = 90 * 60;
@@ -384,6 +396,14 @@ var assertFinite = (value, reason, label) => {
384
396
  throw gateError(reason, `${label} is not a finite number (${String(value)})`);
385
397
  }
386
398
  };
399
+ var MAX_PAIR_LENGTH = 158;
400
+ var assertPairLength = (pair) => {
401
+ if (pair.length > MAX_PAIR_LENGTH) {
402
+ throw new Error(
403
+ `pair is ${pair.length} characters, over the wire's ${MAX_PAIR_LENGTH}-character limit`
404
+ );
405
+ }
406
+ };
387
407
  var verifyLockupAddress = (quote, derivedAddress) => {
388
408
  const quoted = quote.profile?.lockup_address;
389
409
  if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
@@ -423,14 +443,20 @@ var assertFundable = (input) => {
423
443
  }
424
444
  }
425
445
  };
426
- var expectQuote = (payload, rfqId) => {
446
+ var expectQuote = (payload, rfqId, requestedPair) => {
427
447
  const p = payload;
428
448
  if (p?.type === "rfq_refusal") throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId);
429
449
  if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
430
450
  throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
431
451
  }
452
+ if (requestedPair !== void 0 && p.pair !== requestedPair) {
453
+ throw new Error(
454
+ `solver quoted ${JSON.stringify(p.pair)}, not the requested ${requestedPair}`
455
+ );
456
+ }
432
457
  return payload;
433
458
  };
459
+ var pairOf = (payload) => typeof payload.pair === "string" ? payload.pair : void 0;
434
460
  var httpTransport = (baseUrl, options = {}) => {
435
461
  const fetchImpl = options.fetchImpl ?? fetch;
436
462
  const readJson = async (response, what) => {
@@ -450,7 +476,11 @@ var httpTransport = (baseUrl, options = {}) => {
450
476
  headers: { "content-type": "application/json" },
451
477
  body: JSON.stringify(payload)
452
478
  });
453
- return expectQuote(await readJson(response, "quote request"), String(payload.rfq_id));
479
+ return expectQuote(
480
+ await readJson(response, "quote request"),
481
+ String(payload.rfq_id),
482
+ pairOf(payload)
483
+ );
454
484
  },
455
485
  async status(rfqId) {
456
486
  const response = await fetchImpl(`${baseUrl}/v1/rfq/${rfqId}`, { method: "GET" });
@@ -527,7 +557,8 @@ var relayTransport = (relayUrl, options) => {
527
557
  async requestQuote(payload) {
528
558
  return expectQuote(
529
559
  await roundTrip(payload, String(payload.rfq_id)),
530
- String(payload.rfq_id)
560
+ String(payload.rfq_id),
561
+ pairOf(payload)
531
562
  );
532
563
  },
533
564
  async status(rfqId) {
@@ -1151,9 +1182,10 @@ export {
1151
1182
  LockupRegistrationFailed,
1152
1183
  registerLockupContract,
1153
1184
  ARKADE_BTC,
1154
- ARKADE_ASSET,
1155
1185
  LIGHTNING_BTC,
1156
1186
  ONCHAIN_BTC,
1187
+ arkadeAssetLeg,
1188
+ ARKADE_ASSET,
1157
1189
  rfqPair,
1158
1190
  LIGHTNING_SEND_PAIR,
1159
1191
  LIGHTNING_RECEIVE_PAIR,
@@ -1168,6 +1200,8 @@ export {
1168
1200
  MIN_HEADROOM_SECONDS,
1169
1201
  verifyLockupAddress,
1170
1202
  assertFundable,
1203
+ expectQuote,
1204
+ pairOf,
1171
1205
  httpTransport,
1172
1206
  relayTransport,
1173
1207
  SOLO_REFUND_HEADROOM_SECONDS,
package/dist/index.cjs CHANGED
@@ -67,6 +67,7 @@ __export(index_exports, {
67
67
  SWAP_LOCKUP_CONTRACT_TYPE: () => SWAP_LOCKUP_CONTRACT_TYPE,
68
68
  SwapRefusal: () => SwapRefusal,
69
69
  addAssetSwap: () => addAssetSwap,
70
+ arkadeAssetLeg: () => arkadeAssetLeg,
70
71
  arkadeSwapRequest: () => arkadeSwapRequest,
71
72
  assertFundable: () => assertFundable,
72
73
  assertReceivable: () => assertReceivable,
@@ -128,6 +129,7 @@ __export(index_exports, {
128
129
  senderIdentityForSwapRecord: () => senderIdentityForSwapRecord,
129
130
  spendTxidsOf: () => spendTxidsOf,
130
131
  spendUpdate: () => spendUpdate,
132
+ swapActivityResolver: () => swapActivityResolver,
131
133
  swapPrograms: () => swapPrograms,
132
134
  swapSecretsToRecord: () => swapSecretsToRecord,
133
135
  unilateralClaimDelay: () => unilateralClaimDelay,
@@ -1445,9 +1447,10 @@ var solverHex = (value, field) => {
1445
1447
  }
1446
1448
  };
1447
1449
  var ARKADE_BTC = "arkade:BTC";
1448
- var ARKADE_ASSET = "arkade:ASSET";
1449
1450
  var LIGHTNING_BTC = "lightning:BTC";
1450
1451
  var ONCHAIN_BTC = "onchain:BTC";
1452
+ var arkadeAssetLeg = (id) => `arkade:${id.toString()}`;
1453
+ var ARKADE_ASSET = "arkade:ASSET";
1451
1454
  var rfqPair = (from, to) => `${from}->${to}`;
1452
1455
  var LIGHTNING_SEND_PAIR = rfqPair(ARKADE_BTC, LIGHTNING_BTC);
1453
1456
  var LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
@@ -1488,23 +1491,34 @@ var lightningSendRequest = (input) => ({
1488
1491
  }
1489
1492
  });
1490
1493
  var arkadeSwapRequest = (input) => {
1491
- if (Boolean(input.wantAsset) === Boolean(input.offerAsset)) {
1492
- throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
1494
+ if (!input.wantAsset && !input.offerAsset) {
1495
+ throw new Error(
1496
+ "set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC) \u2014 with neither set both legs are BTC, which is not a swap"
1497
+ );
1498
+ }
1499
+ if (input.wantAsset && input.offerAsset) {
1500
+ throw new Error(
1501
+ "set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC) \u2014 asset->asset is nameable on the wire but no solver quotes it yet"
1502
+ );
1493
1503
  }
1504
+ const pair = rfqPair(
1505
+ input.offerAsset ? arkadeAssetLeg(input.offerAsset) : ARKADE_BTC,
1506
+ input.wantAsset ? arkadeAssetLeg(input.wantAsset) : ARKADE_BTC
1507
+ );
1508
+ assertPairLength(pair);
1494
1509
  return {
1495
1510
  v: 1,
1496
1511
  type: "rfq_request",
1497
1512
  rfq_id: input.rfqId,
1498
- pair: rfqPair(
1499
- input.offerAsset ? ARKADE_ASSET : ARKADE_BTC,
1500
- input.wantAsset ? ARKADE_ASSET : ARKADE_BTC
1501
- ),
1513
+ pair,
1502
1514
  amount_side: input.amountSide,
1503
1515
  amount: input.amount,
1504
- profile: {
1505
- ...input.offerAsset && { offer_asset: import_base8.hex.encode(input.offerAsset.serialize()) },
1506
- ...input.wantAsset && { want_asset: import_base8.hex.encode(input.wantAsset.serialize()) }
1507
- }
1516
+ // The pair is the only place the asset ids appear. Repeating them here
1517
+ // would be a key the solver's `.strict()` profile schema does not
1518
+ // declare, and an undeclared key is `unsupported_payload` — a refusal,
1519
+ // not an ignored extra. Empty, not absent: `profile` is required on
1520
+ // every other request shape this wire carries.
1521
+ profile: {}
1508
1522
  };
1509
1523
  };
1510
1524
  var MIN_HEADROOM_SECONDS = 90 * 60;
@@ -1518,6 +1532,14 @@ var assertFinite = (value, reason, label) => {
1518
1532
  throw gateError(reason, `${label} is not a finite number (${String(value)})`);
1519
1533
  }
1520
1534
  };
1535
+ var MAX_PAIR_LENGTH = 158;
1536
+ var assertPairLength = (pair) => {
1537
+ if (pair.length > MAX_PAIR_LENGTH) {
1538
+ throw new Error(
1539
+ `pair is ${pair.length} characters, over the wire's ${MAX_PAIR_LENGTH}-character limit`
1540
+ );
1541
+ }
1542
+ };
1521
1543
  var verifyLockupAddress = (quote, derivedAddress) => {
1522
1544
  const quoted = quote.profile?.lockup_address;
1523
1545
  if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
@@ -1557,14 +1579,20 @@ var assertFundable = (input) => {
1557
1579
  }
1558
1580
  }
1559
1581
  };
1560
- var expectQuote = (payload, rfqId) => {
1582
+ var expectQuote = (payload, rfqId, requestedPair) => {
1561
1583
  const p = payload;
1562
1584
  if (p?.type === "rfq_refusal") throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId);
1563
1585
  if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
1564
1586
  throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
1565
1587
  }
1588
+ if (requestedPair !== void 0 && p.pair !== requestedPair) {
1589
+ throw new Error(
1590
+ `solver quoted ${JSON.stringify(p.pair)}, not the requested ${requestedPair}`
1591
+ );
1592
+ }
1566
1593
  return payload;
1567
1594
  };
1595
+ var pairOf = (payload) => typeof payload.pair === "string" ? payload.pair : void 0;
1568
1596
  var httpTransport = (baseUrl, options = {}) => {
1569
1597
  const fetchImpl = options.fetchImpl ?? fetch;
1570
1598
  const readJson = async (response, what) => {
@@ -1584,7 +1612,11 @@ var httpTransport = (baseUrl, options = {}) => {
1584
1612
  headers: { "content-type": "application/json" },
1585
1613
  body: JSON.stringify(payload)
1586
1614
  });
1587
- return expectQuote(await readJson(response, "quote request"), String(payload.rfq_id));
1615
+ return expectQuote(
1616
+ await readJson(response, "quote request"),
1617
+ String(payload.rfq_id),
1618
+ pairOf(payload)
1619
+ );
1588
1620
  },
1589
1621
  async status(rfqId) {
1590
1622
  const response = await fetchImpl(`${baseUrl}/v1/rfq/${rfqId}`, { method: "GET" });
@@ -1661,7 +1693,8 @@ var relayTransport = (relayUrl, options) => {
1661
1693
  async requestQuote(payload) {
1662
1694
  return expectQuote(
1663
1695
  await roundTrip(payload, String(payload.rfq_id)),
1664
- String(payload.rfq_id)
1696
+ String(payload.rfq_id),
1697
+ pairOf(payload)
1665
1698
  );
1666
1699
  },
1667
1700
  async status(rfqId) {
@@ -3376,6 +3409,60 @@ var outcomeOf = (swap) => {
3376
3409
  };
3377
3410
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
3378
3411
  var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
3412
+
3413
+ // src/activity.ts
3414
+ var LABELS = {
3415
+ lightning_send: "Lightning send",
3416
+ lightning_receive: "Lightning receive",
3417
+ onchain_send: "Onchain send"
3418
+ };
3419
+ var OUTCOME = {
3420
+ pending: "pending",
3421
+ // `claimable` and `claimed` are both in-progress states with no
3422
+ // user-visible phase distinct from "pending". `needs_counterparty` is
3423
+ // different in kind — the swap is BLOCKED, not merely in flight, since no
3424
+ // unilateral trader move exists (see `RfqSwapState`). Collapsing it into
3425
+ // `pending` here is a deliberate choice the opaque-token design permits —
3426
+ // apps map tokens themselves — but a future reader weighing a `"blocked"`
3427
+ // or `"stuck"` token should know this was already considered.
3428
+ claimable: "pending",
3429
+ claimed: "pending",
3430
+ needs_counterparty: "pending",
3431
+ settled: "settled",
3432
+ refunded: "refunded",
3433
+ failed: "failed"
3434
+ };
3435
+ function swapActivityResolver(deps) {
3436
+ let byTxid = /* @__PURE__ */ new Map();
3437
+ return {
3438
+ id: "arkade:swap",
3439
+ async prepare() {
3440
+ const swaps = await deps.listSwaps();
3441
+ const index = /* @__PURE__ */ new Map();
3442
+ for (const swap of swaps) {
3443
+ for (const txid of swap.txids) {
3444
+ if (txid) index.set(txid, swap);
3445
+ }
3446
+ }
3447
+ byTxid = index;
3448
+ },
3449
+ resolve(tx) {
3450
+ const key = tx.key.arkTxid || tx.key.commitmentTxid || tx.key.boardingTxid;
3451
+ const swap = key ? byTxid.get(key) : void 0;
3452
+ if (!swap) return void 0;
3453
+ const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
3454
+ return [
3455
+ {
3456
+ groupId: `swap:${swap.rfqId}`,
3457
+ label: LABELS[swap.kind],
3458
+ kind: "swap",
3459
+ outcome: lostReceive ? "lost" : OUTCOME[swap.state],
3460
+ metadata: { rfqId: swap.rfqId, swapKind: swap.kind }
3461
+ }
3462
+ ];
3463
+ }
3464
+ };
3465
+ }
3379
3466
  // Annotate the CommonJS export names for ESM import in node:
3380
3467
  0 && (module.exports = {
3381
3468
  ARKADE_ASSET,
@@ -3415,6 +3502,7 @@ var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
3415
3502
  SWAP_LOCKUP_CONTRACT_TYPE,
3416
3503
  SwapRefusal,
3417
3504
  addAssetSwap,
3505
+ arkadeAssetLeg,
3418
3506
  arkadeSwapRequest,
3419
3507
  assertFundable,
3420
3508
  assertReceivable,
@@ -3476,6 +3564,7 @@ var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
3476
3564
  senderIdentityForSwapRecord,
3477
3565
  spendTxidsOf,
3478
3566
  spendUpdate,
3567
+ swapActivityResolver,
3479
3568
  swapPrograms,
3480
3569
  swapSecretsToRecord,
3481
3570
  unilateralClaimDelay,