@arkade-os/swap 0.0.11 → 0.1.0-rc.0

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/dist/index.js CHANGED
@@ -38,9 +38,10 @@ import {
38
38
  deriveOnchainSend,
39
39
  extractPreimage,
40
40
  httpTransport,
41
+ lightningReceiveContract,
41
42
  lightningReceiveRequest,
43
+ lightningSendContract,
42
44
  lightningSendRequest,
43
- lightningSendVtxoScript,
44
45
  lockupContractParams,
45
46
  newPreimage,
46
47
  newRfqId,
@@ -49,7 +50,6 @@ import {
49
50
  onchainReceiveRequest,
50
51
  onchainSendRequest,
51
52
  paymentHashOf,
52
- receiveVtxoScript,
53
53
  registerLockupContract,
54
54
  relayTransport,
55
55
  requestLightningReceive,
@@ -63,7 +63,7 @@ import {
63
63
  unilateralRefundWithoutReceiverDelay,
64
64
  verifyLockupAddress,
65
65
  verifyReceiveInvoice
66
- } from "./chunk-RIC5ZTZK.js";
66
+ } from "./chunk-AM3NNUMR.js";
67
67
  import {
68
68
  InMemoryAssetSwapRepository,
69
69
  marketsCacheKey
@@ -74,11 +74,9 @@ import { hex as hex2 } from "@scure/base";
74
74
  import { concatBytes } from "@scure/btc-signer/utils.js";
75
75
  import {
76
76
  ArkAddress,
77
- RestArkProvider,
78
- RestIndexerProvider,
79
77
  arkade,
80
78
  asset,
81
- getNetwork,
79
+ networkFromArkadeInfo,
82
80
  resolveEmulatorPubkey,
83
81
  toXOnlySignerHex
84
82
  } from "@arkade-os/sdk";
@@ -344,7 +342,7 @@ function withExitClosure(program, exit) {
344
342
  }
345
343
  };
346
344
  }
347
- function swapProgramBinding(offer, serverPubkey) {
345
+ function swapProgramBinding(offer, operatorPubkey) {
348
346
  if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
349
347
  throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
350
348
  }
@@ -356,7 +354,7 @@ function swapProgramBinding(offer, serverPubkey) {
356
354
  args: {
357
355
  makerWP: offer.makerPkScript.subarray(2),
358
356
  wantAmount: offer.wantAmount,
359
- server: serverPubkey,
357
+ server: operatorPubkey,
360
358
  user: offer.makerPublicKey,
361
359
  // internal byte order
362
360
  ...offer.wantAsset && {
@@ -366,14 +364,14 @@ function swapProgramBinding(offer, serverPubkey) {
366
364
  ...offer.exitDelay && { exitDelay: offer.exitDelay.value }
367
365
  },
368
366
  keys: {
369
- serverKey: serverPubkey,
367
+ serverKey: operatorPubkey,
370
368
  userKey: offer.makerPublicKey,
371
369
  emulatorKey: offer.emulatorPubkey
372
370
  }
373
371
  };
374
372
  }
375
- function offerVtxoScript(offer, serverPubkey) {
376
- const { program, args, keys } = swapProgramBinding(offer, serverPubkey);
373
+ function offerContract(offer, operatorPubkey) {
374
+ const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
377
375
  return new arkade.ArkadeProgramScript(program, args, keys);
378
376
  }
379
377
  var OFFER_PACKET_TYPE = 3;
@@ -487,14 +485,14 @@ function decodeOffer(data) {
487
485
  throw new Error(`missing/invalid ${name}`);
488
486
  }
489
487
  }
490
- const need = (name) => {
488
+ const need2 = (name) => {
491
489
  const v = fields[name];
492
490
  const len = FIELDS[name].width;
493
491
  if (!v || len !== void 0 && v.length !== len)
494
492
  throw new Error(`missing/invalid ${name}`);
495
493
  return v;
496
494
  };
497
- const amount = need("wantAmount");
495
+ const amount = need2("wantAmount");
498
496
  if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
499
497
  throw new Error("offer must carry exactly one of wantAsset or offerAsset");
500
498
  }
@@ -511,13 +509,13 @@ function decodeOffer(data) {
511
509
  throw new Error("offer must carry both ratioNum and ratioDen, or neither");
512
510
  }
513
511
  return {
514
- swapPkScript: need("swapPkScript"),
512
+ swapPkScript: need2("swapPkScript"),
515
513
  wantAmount: readU64(amount),
516
514
  ...fields.wantAsset && { wantAsset: asset.AssetId.fromBytes(fields.wantAsset) },
517
515
  ...fields.offerAsset && { offerAsset: asset.AssetId.fromBytes(fields.offerAsset) },
518
- makerPkScript: need("makerPkScript"),
519
- makerPublicKey: need("makerPublicKey"),
520
- emulatorPubkey: need("emulatorPubkey"),
516
+ makerPkScript: need2("makerPkScript"),
517
+ makerPublicKey: need2("makerPublicKey"),
518
+ emulatorPubkey: need2("emulatorPubkey"),
521
519
  ...ratioNum !== void 0 && { ratioNum },
522
520
  ...ratioDen !== void 0 && { ratioDen },
523
521
  ...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
@@ -530,17 +528,21 @@ function decodeExitDelay(value) {
530
528
  }
531
529
  var OFFER_CONTRACT_LABEL = "Arkade swap offer";
532
530
  var OFFER_CONTRACT_KIND = "asset-swap-offer";
533
- async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript) {
534
- const { program, args, keys } = swapProgramBinding(binding, serverPubkey);
531
+ async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
532
+ const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
535
533
  const contractManager = await wallet.getContractManager();
536
534
  const client = await arkade.Arkade.connect({
537
- arkade: new RestArkProvider(arkServerUrl),
538
- indexer: new RestIndexerProvider(arkServerUrl),
535
+ // Registration derives and persists; it never broadcasts and never
536
+ // reads UTXOs, so the only thing the client needs off the server is the
537
+ // info the caller already resolved. Handing that back — rather than a
538
+ // provider built from a URL — is what lets `createOffer` take just a
539
+ // wallet, and spares a second `/v1/info` round-trip.
540
+ arkade: { getInfo: async () => info },
539
541
  identity: wallet.identity,
540
542
  // without this the row's `address` would be derived against the SDK's
541
543
  // default network while its script is right — a row that disagrees with
542
544
  // the address the user is about to fund
543
- network: getNetwork(network),
545
+ network: networkFromArkadeInfo(info),
544
546
  contractManager
545
547
  });
546
548
  const contract = new arkade.ArkadeContract(client, program, args, keys);
@@ -561,17 +563,21 @@ function serverExitDelay(delay) {
561
563
  }
562
564
  return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
563
565
  }
564
- async function createOffer(wallet, arkServerUrl, params) {
566
+ async function createOffer(wallet, params) {
565
567
  if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
566
568
  throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
567
569
  }
568
570
  const [info, makerAddress, makerPublicKey] = await Promise.all([
569
- new RestArkProvider(arkServerUrl).getInfo(),
571
+ // requireLive: this info binds signerPubkey into the covenant — a
572
+ // snapshot could derive an address the operator no longer co-signs
573
+ // for, so an unreachable operator fails the call instead (fail closed,
574
+ // the same behaviour the pre-#734 caller-held provider had)
575
+ wallet.getArkadeInfo({ requireLive: true }),
570
576
  wallet.getAddress(),
571
577
  wallet.identity.xOnlyPublicKey()
572
578
  ]);
573
- const serverPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
574
- const network = getNetwork(info.network);
579
+ const operatorPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
580
+ const network = networkFromArkadeInfo(info);
575
581
  const emuKey = hex2.decode(
576
582
  toXOnlySignerHex(resolveEmulatorPubkey(network, params.emulatorPubkey))
577
583
  );
@@ -587,33 +593,33 @@ async function createOffer(wallet, arkServerUrl, params) {
587
593
  // an offer that then fails to encode. @see assertExitDelay
588
594
  exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
589
595
  };
590
- const script = offerVtxoScript(binding, serverPubKey);
596
+ const script = offerContract(binding, operatorPubKey);
591
597
  const offer = { ...binding, swapPkScript: script.pkScript };
592
- await registerOfferContract(
593
- wallet,
594
- arkServerUrl,
595
- info.network,
596
- binding,
597
- serverPubKey,
598
- script.pkScript
599
- );
598
+ await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
600
599
  const payload = encodeOffer(offer);
601
600
  return {
602
601
  offerHex: hex2.encode(payload),
603
602
  extension: { type: OFFER_PACKET_TYPE, payload },
604
- // VtxoScript.address owns address construction; assembling an ArkAddress
603
+ // the contract's .address() builds the address; assembling an ArkAddress
605
604
  // from tweakedPublicKey here would silently miss any future step it gains
606
- address: script.address(network.hrp, serverPubKey).encode(),
605
+ address: script.address(network.hrp, operatorPubKey).encode(),
607
606
  swapPkScript: script.pkScript
608
607
  };
609
608
  }
610
- async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
609
+ async function cancelOffer(wallet, offerHex, opts) {
611
610
  const { repository, fundingTxid, swapAddress } = opts;
612
611
  const offer = decodeOffer(hex2.decode(offerHex));
613
- const contractManager = await wallet.getContractManager();
612
+ const [contractManager, info, reader, broadcaster] = await Promise.all([
613
+ wallet.getContractManager(),
614
+ wallet.getArkadeInfo({ requireLive: true }),
615
+ wallet.getArkadeReader(),
616
+ wallet.getArkadeBroadcaster()
617
+ ]);
614
618
  const client = await arkade.Arkade.connect({
615
- arkade: new RestArkProvider(arkServerUrl),
616
- indexer: new RestIndexerProvider(arkServerUrl),
619
+ // info the wallet already holds, plus its broadcast pair — the shape
620
+ // `ArkadeServerProvider` describes, without a second `/v1/info`
621
+ arkade: { getInfo: async () => info, ...broadcaster },
622
+ indexer: reader,
617
623
  identity: wallet.identity,
618
624
  // registered offers resolve their VTXOs from the contract repository
619
625
  // instead of a direct indexer query; the indexer above stays as the
@@ -628,7 +634,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
628
634
  const rebuilt = new arkade.ArkadeProgramScript(program, args, keys);
629
635
  if (hex2.encode(rebuilt.pkScript) !== hex2.encode(offer.swapPkScript)) {
630
636
  throw new Error(
631
- "rebuilt covenant does not match the offer's swapPkScript \u2014 the server signing key has likely rotated since funding; pass swapAddress (the funded address) to pin the original key"
637
+ "rebuilt covenant does not match the offer's swapPkScript \u2014 the operator signing key has likely rotated since funding; pass swapAddress (the funded address) to pin the original key"
632
638
  );
633
639
  }
634
640
  const contract = new arkade.ArkadeContract(client, program, args, keys);
@@ -1036,7 +1042,7 @@ var LightningReceiveCorridor = {
1036
1042
  const receive = swap;
1037
1043
  return {
1038
1044
  expectedAmount: receive.expectedAmount,
1039
- ...receive.claimArkTxid ? { claimArkTxid: receive.claimArkTxid } : {}
1045
+ ...receive.claimTxid ? { claimTxid: receive.claimTxid } : {}
1040
1046
  };
1041
1047
  },
1042
1048
  hydrate(profile) {
@@ -1046,13 +1052,13 @@ var LightningReceiveCorridor = {
1046
1052
  return {
1047
1053
  ...hydrateHashlock(profile),
1048
1054
  expectedAmount: profile.expectedAmount,
1049
- ...profile.claimArkTxid ? { claimArkTxid: profile.claimArkTxid } : {}
1055
+ ...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
1050
1056
  };
1051
1057
  },
1052
1058
  // We are the claimant here, so the preimage material on the hashlock is
1053
1059
  // ours to use.
1054
1060
  claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
1055
- activityTxids: (profile) => profile.claimArkTxid ? [profile.claimArkTxid] : []
1061
+ activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
1056
1062
  };
1057
1063
  function onchainSendProfile(result) {
1058
1064
  return {
@@ -1136,13 +1142,32 @@ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
1136
1142
 
1137
1143
  // src/rfqRecord.ts
1138
1144
  var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
1145
+ function renameLegacyClaimTxid(profile) {
1146
+ const { claimArkTxid, ...rest } = profile;
1147
+ return { ...rest, claimTxid: rest.claimTxid ?? claimArkTxid };
1148
+ }
1149
+ function normalizeRfqSwapRecord(record) {
1150
+ const { fundingArkTxid, refundArkTxid, lockupSpendArkTxids, ...rest } = record;
1151
+ const legacyClaim = record.kind === "lightning_receive" && record.profile.claimArkTxid !== void 0;
1152
+ if (!fundingArkTxid && !refundArkTxid && !lockupSpendArkTxids && !legacyClaim) return record;
1153
+ const fundingTxid = rest.fundingTxid ?? fundingArkTxid;
1154
+ const refundTxid = rest.refundTxid ?? refundArkTxid;
1155
+ const lockupSpendTxids = rest.lockupSpendTxids ?? lockupSpendArkTxids;
1156
+ return {
1157
+ ...rest,
1158
+ ...fundingTxid ? { fundingTxid } : {},
1159
+ ...refundTxid ? { refundTxid } : {},
1160
+ ...lockupSpendTxids?.length ? { lockupSpendTxids } : {},
1161
+ ...legacyClaim ? { profile: renameLegacyClaimTxid(record.profile) } : {}
1162
+ };
1163
+ }
1139
1164
  var managerState = (swap) => ({
1140
1165
  rfqId: swap.rfqId,
1141
1166
  state: swap.state,
1142
1167
  createdAt: swap.createdAt,
1143
1168
  updatedAt: swap.updatedAt,
1144
- ...swap.refundArkTxid ? { refundArkTxid: swap.refundArkTxid } : {},
1145
- ...swap.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...swap.lockupSpendArkTxids] } : {},
1169
+ ...swap.refundTxid ? { refundTxid: swap.refundTxid } : {},
1170
+ ...swap.lockupSpendTxids?.length ? { lockupSpendTxids: [...swap.lockupSpendTxids] } : {},
1146
1171
  ...swap.failure ? { failure: swap.failure } : {},
1147
1172
  ...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
1148
1173
  });
@@ -1173,27 +1198,29 @@ function createRfqSwapRecord(origin, swap) {
1173
1198
  }
1174
1199
  function updateRfqSwapRecord(record, swap) {
1175
1200
  assertSameSwap(record, swap);
1201
+ const stored = normalizeRfqSwapRecord(record);
1176
1202
  const {
1177
- refundArkTxid: _refundArkTxid,
1178
- lockupSpendArkTxids: _lockupSpendArkTxids,
1203
+ refundTxid: _refundTxid,
1204
+ lockupSpendTxids: _lockupSpendTxids,
1179
1205
  failure: _failure,
1180
1206
  blockedReason: _blockedReason,
1181
1207
  ...origin
1182
- } = record;
1183
- const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1208
+ } = stored;
1209
+ const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
1184
1210
  return {
1185
1211
  ...origin,
1186
1212
  ...managerState(swap),
1187
- profile: { ...record.profile, ...handler.project(swap) }
1213
+ profile: { ...stored.profile, ...handler.project(swap) }
1188
1214
  };
1189
1215
  }
1190
1216
  function rfqSwapOriginOf(record) {
1217
+ const stored = normalizeRfqSwapRecord(record);
1191
1218
  return {
1192
- kind: record.kind,
1193
- lockupAddress: record.lockupAddress,
1194
- profile: { ...record.profile },
1195
- ...record.amount !== void 0 ? { amount: record.amount } : {},
1196
- ...record.fundingArkTxid ? { fundingArkTxid: record.fundingArkTxid } : {}
1219
+ kind: stored.kind,
1220
+ lockupAddress: stored.lockupAddress,
1221
+ profile: { ...stored.profile },
1222
+ ...stored.amount !== void 0 ? { amount: stored.amount } : {},
1223
+ ...stored.fundingTxid ? { fundingTxid: stored.fundingTxid } : {}
1197
1224
  };
1198
1225
  }
1199
1226
  function lockupScript(params, lockupAddress) {
@@ -1207,27 +1234,28 @@ function lockupScript(params, lockupAddress) {
1207
1234
  return script;
1208
1235
  }
1209
1236
  function rebuildRfqSwap(record, params) {
1210
- const script = lockupScript(params, record.lockupAddress);
1237
+ const stored = normalizeRfqSwapRecord(record);
1238
+ const script = lockupScript(params, stored.lockupAddress);
1211
1239
  const common = {
1212
- rfqId: record.rfqId,
1213
- state: record.state,
1240
+ rfqId: stored.rfqId,
1241
+ state: stored.state,
1214
1242
  lockupPkScript: script.pkScript,
1215
- lockup: { script, address: record.lockupAddress },
1243
+ lockup: { script, address: stored.lockupAddress },
1216
1244
  // From the covenant, which binds it: the record's own copy would be a
1217
1245
  // second source for the deadline the refund is gated on.
1218
1246
  refundLocktime: Number(script.options.refundLocktime),
1219
- createdAt: record.createdAt,
1220
- updatedAt: record.updatedAt,
1221
- ...record.refundArkTxid ? { refundArkTxid: record.refundArkTxid } : {},
1222
- ...record.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...record.lockupSpendArkTxids] } : {},
1223
- ...record.failure ? { failure: record.failure } : {},
1224
- ...record.blockedReason ? { blockedReason: record.blockedReason } : {}
1247
+ createdAt: stored.createdAt,
1248
+ updatedAt: stored.updatedAt,
1249
+ ...stored.refundTxid ? { refundTxid: stored.refundTxid } : {},
1250
+ ...stored.lockupSpendTxids?.length ? { lockupSpendTxids: [...stored.lockupSpendTxids] } : {},
1251
+ ...stored.failure ? { failure: stored.failure } : {},
1252
+ ...stored.blockedReason ? { blockedReason: stored.blockedReason } : {}
1225
1253
  };
1226
- const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1254
+ const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
1227
1255
  return {
1228
1256
  ...common,
1229
- kind: record.kind,
1230
- ...handler.hydrate(record.profile, { lockup: script })
1257
+ kind: stored.kind,
1258
+ ...handler.hydrate(stored.profile, { lockup: script })
1231
1259
  };
1232
1260
  }
1233
1261
  function shouldRetainRfqSwap(record, now) {
@@ -1268,10 +1296,10 @@ async function fetchParsedTxs(indexer, txids) {
1268
1296
  var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
1269
1297
  (tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
1270
1298
  );
1271
- function classifySpend(offer, serverPubkey, spendTx, deposit) {
1299
+ function classifySpend(offer, operatorPubkey, spendTx, deposit) {
1272
1300
  let leaves;
1273
1301
  try {
1274
- const script = offerVtxoScript(offer, serverPubkey);
1302
+ const script = offerContract(offer, operatorPubkey);
1275
1303
  if (hex5.encode(script.pkScript) !== hex5.encode(offer.swapPkScript)) return "indeterminate";
1276
1304
  leaves = {
1277
1305
  // both routes that hand the deposit back; `exit` is absent on an
@@ -1295,15 +1323,15 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1295
1323
  return "indeterminate";
1296
1324
  }
1297
1325
  var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
1298
- function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
1326
+ function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
1299
1327
  for (const tx of spendTxs) {
1300
- const kind = classifySpend(offer, serverPubkey, tx, deposit);
1328
+ const kind = classifySpend(offer, operatorPubkey, tx, deposit);
1301
1329
  if (kind !== "indeterminate") return kind;
1302
1330
  }
1303
1331
  return "indeterminate";
1304
1332
  }
1305
1333
  async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1306
- const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
1334
+ const { operatorPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
1307
1335
  const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
1308
1336
  if (candidates.length === 0) return { restored: [], scannedTxids: [] };
1309
1337
  const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
@@ -1372,7 +1400,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1372
1400
  if (state === "swept") status = "recoverable";
1373
1401
  else if (state === "spent") {
1374
1402
  const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
1375
- const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
1403
+ const kind = classifyDepositSpend(offer, operatorPubkey, spendTxs, {
1376
1404
  txid: vtxo.txid,
1377
1405
  vout: vtxo.vout
1378
1406
  });
@@ -1389,7 +1417,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1389
1417
  fromAmount,
1390
1418
  toAmount: offer.wantAmount.toString(),
1391
1419
  // ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
1392
- // to the current server key; store the funded address if server-key
1420
+ // to the current operator key; store the funded address if operator-key
1393
1421
  // rotations become real (cancelOffer now at least diagnoses the
1394
1422
  // mismatch instead of reporting a missing VTXO)
1395
1423
  swapAddress: "",
@@ -1411,7 +1439,6 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1411
1439
  import { base64 as base642, hex as hex6 } from "@scure/base";
1412
1440
  import {
1413
1441
  ArkAddress as ArkAddress3,
1414
- RestIndexerProvider as RestIndexerProvider3,
1415
1442
  Transaction as Transaction2
1416
1443
  } from "@arkade-os/sdk";
1417
1444
  var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
@@ -1428,13 +1455,15 @@ function spendUpdate(swap, spend) {
1428
1455
  }
1429
1456
  async function watchOfferSwaps({
1430
1457
  wallet,
1431
- arkServerUrl,
1432
1458
  repository,
1433
1459
  onUpdate
1434
1460
  }) {
1435
- const manager = await wallet.getContractManager();
1436
- const serverPubkey = ArkAddress3.decode(await wallet.getAddress()).serverPubKey;
1437
- const indexer = new RestIndexerProvider3(arkServerUrl);
1461
+ const [manager, address, indexer] = await Promise.all([
1462
+ wallet.getContractManager(),
1463
+ wallet.getAddress(),
1464
+ wallet.getArkadeReader()
1465
+ ]);
1466
+ const operatorPubkey = ArkAddress3.decode(address).serverPubKey;
1438
1467
  let queue = Promise.resolve();
1439
1468
  const enqueue = (task) => {
1440
1469
  queue = queue.then(task).catch(() => {
@@ -1448,7 +1477,7 @@ async function watchOfferSwaps({
1448
1477
  const { txs } = await indexer.getVirtualTxs(candidates);
1449
1478
  return classifyDepositSpend(
1450
1479
  decodeOffer(hex6.decode(swap.offerHex)),
1451
- serverPubkey,
1480
+ operatorPubkey,
1452
1481
  txs.map((psbt) => Transaction2.fromPSBT(base642.decode(psbt))),
1453
1482
  { txid: vtxo.txid, vout: vtxo.vout }
1454
1483
  );
@@ -1543,11 +1572,10 @@ var LockupNeedsRecoveryError = class extends Error {
1543
1572
  * into one settlement with no CLTV awareness, so an early attempt can fail
1544
1573
  * the whole batch — including unrelated outputs that were otherwise fine.
1545
1574
  *
1546
- * Exposed as a value, not only inside the message, so a caller can encode
1547
- * `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
1548
- * parsing prose. Seconds-based locktimes mature against the chain tip's
1549
- * timestamp rather than wall clock, so treat this as a floor to wait past,
1550
- * not an exact alarm.
1575
+ * Exposed as a value, not only inside the message, so a caller can skip
1576
+ * pre-CLTV recoverable outputs without parsing prose. Seconds-based
1577
+ * locktimes mature against the chain tip's timestamp rather than wall
1578
+ * clock, so treat this as a floor to wait past, not an exact alarm.
1551
1579
  */
1552
1580
  recoverableAfter;
1553
1581
  constructor(outpoints, recoverableAfter) {
@@ -1608,7 +1636,7 @@ async function readLockupFate(indexer, input) {
1608
1636
  if (vtxo.spentBy)
1609
1637
  spentBy.set(vtxo.spentBy, {
1610
1638
  checkpointTxid: vtxo.spentBy,
1611
- arkTxid: vtxo.arkTxId
1639
+ txid: vtxo.arkTxId
1612
1640
  });
1613
1641
  else everySpendNamed = false;
1614
1642
  }
@@ -1637,32 +1665,32 @@ async function readLockupFate(indexer, input) {
1637
1665
  }
1638
1666
  return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
1639
1667
  }
1640
- async function pushRefundWithoutReceiver(ark, input) {
1668
+ async function pushRefundWithoutReceiver(operator, input) {
1641
1669
  if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
1642
1670
  const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
1643
1671
  if (swept.length > 0) {
1644
1672
  throw new LockupNeedsRecoveryError(
1645
1673
  swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
1646
- input.script.options.refundLocktime
1674
+ input.contract.options.refundLocktime
1647
1675
  );
1648
1676
  }
1649
- const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveParameters?.senderPkScript;
1677
+ const refundPkScript = input.refundPkScript ?? input.contract.options.nonInteractiveParameters?.senderPkScript;
1650
1678
  if (!refundPkScript) {
1651
1679
  throw new Error(
1652
1680
  "no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
1653
1681
  );
1654
1682
  }
1655
- const info = await ark.getInfo();
1656
- let serverUnrollScript;
1683
+ const info = await operator.getInfo();
1684
+ let operatorUnrollScript;
1657
1685
  try {
1658
- serverUnrollScript = CSVMultisigTapscript.decode(hex7.decode(info.checkpointTapscript));
1686
+ operatorUnrollScript = CSVMultisigTapscript.decode(hex7.decode(info.checkpointTapscript));
1659
1687
  } catch {
1660
- throw new Error("invalid checkpointTapscript from the Arkade server");
1688
+ throw new Error("invalid checkpointTapscript from the operator");
1661
1689
  }
1662
- const leaf = input.script.refundWithoutReceiver();
1663
- const tapTree = input.script.encode();
1690
+ const leaf = input.contract.refundWithoutReceiver();
1691
+ const tapTree = input.contract.encode();
1664
1692
  const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
1665
- const { arkTx, checkpoints } = buildOffchainTx(
1693
+ const { arkTx: tx, checkpoints } = buildOffchainTx(
1666
1694
  input.vtxos.map((vtxo) => ({
1667
1695
  txid: vtxo.txid,
1668
1696
  vout: vtxo.vout,
@@ -1671,14 +1699,14 @@ async function pushRefundWithoutReceiver(ark, input) {
1671
1699
  tapTree
1672
1700
  })),
1673
1701
  [{ script: refundPkScript, amount: BigInt(amount) }],
1674
- serverUnrollScript
1702
+ operatorUnrollScript
1675
1703
  );
1676
- const signedArkTx = await input.sender.sign(arkTx);
1677
- const submitted = await ark.submitTx(
1678
- base643.encode(signedArkTx.toPSBT()),
1704
+ const signedTx = await input.sender.sign(tx);
1705
+ const submitted = await operator.submitTx(
1706
+ base643.encode(signedTx.toPSBT()),
1679
1707
  checkpoints.map((c) => base643.encode(c.toPSBT()))
1680
1708
  );
1681
- assertSubmittedArkTxid(submitted, signedArkTx, "refundWithoutReceiver");
1709
+ assertSubmittedArkTxid(submitted, signedTx, "refundWithoutReceiver");
1682
1710
  const matched = matchServerCheckpoints(
1683
1711
  submitted.signedCheckpointTxs,
1684
1712
  checkpoints,
@@ -1689,11 +1717,11 @@ async function pushRefundWithoutReceiver(ark, input) {
1689
1717
  async ({ server }) => base643.encode((await input.sender.sign(server, [0])).toPSBT())
1690
1718
  )
1691
1719
  );
1692
- await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
1693
- return { arkTxid: submitted.arkTxid, amount };
1720
+ await operator.finalizeTx(submitted.arkTxid, finalCheckpoints);
1721
+ return { txid: submitted.arkTxid, amount };
1694
1722
  }
1695
1723
  var REFUND_MTP_LAG_SECONDS = 2 * 60 * 60;
1696
- async function refundIfUnresolved(transport, ark, indexer, input) {
1724
+ async function refundIfUnresolved(transport, operator, indexer, input) {
1697
1725
  const pollMs = input.pollMs ?? 5e3;
1698
1726
  const now = input.now ?? (() => Math.floor(Date.now() / 1e3));
1699
1727
  const attemptDeadline = input.attemptDeadline ?? input.refundLocktime + REFUND_MTP_LAG_SECONDS;
@@ -1704,7 +1732,7 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
1704
1732
  let fate = { fate: "unknown" };
1705
1733
  try {
1706
1734
  fate = await readLockupFate(indexer, {
1707
- swapPkScript: input.script.pkScript,
1735
+ swapPkScript: input.contract.pkScript,
1708
1736
  paymentHash: input.paymentHash
1709
1737
  });
1710
1738
  } catch {
@@ -1716,11 +1744,11 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
1716
1744
  status
1717
1745
  };
1718
1746
  }
1719
- const vtxos = await findLockupVtxos(indexer, input.script.pkScript);
1747
+ const vtxos = await findLockupVtxos(indexer, input.contract.pkScript);
1720
1748
  if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
1721
1749
  try {
1722
- const pushed = await pushRefundWithoutReceiver(ark, {
1723
- script: input.script,
1750
+ const pushed = await pushRefundWithoutReceiver(operator, {
1751
+ contract: input.contract,
1724
1752
  sender: input.sender,
1725
1753
  vtxos,
1726
1754
  refundPkScript: input.refundPkScript
@@ -1763,13 +1791,13 @@ var assertFiniteAmount = (value, reason, label) => {
1763
1791
  error.reason = reason;
1764
1792
  throw error;
1765
1793
  };
1766
- async function pushClaim(ark, input) {
1794
+ async function pushClaim(operator, input) {
1767
1795
  if (input.vtxos.length === 0) throw new Error("nothing to claim: no funded outputs");
1768
1796
  const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
1769
1797
  if (swept.length > 0) {
1770
1798
  throw new LockupNeedsRecoveryError(
1771
1799
  swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
1772
- input.script.options.refundLocktime
1800
+ input.contract.options.refundLocktime
1773
1801
  );
1774
1802
  }
1775
1803
  const locked = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
@@ -1780,22 +1808,22 @@ async function pushClaim(ark, input) {
1780
1808
  throw new LockupAmountMismatchError(input.expectedAmount, locked);
1781
1809
  }
1782
1810
  }
1783
- const committed = input.script.options.preimageHash;
1811
+ const committed = input.contract.options.preimageHash;
1784
1812
  if (hex8.encode(ripemd160(sha2563(input.preimage))) !== hex8.encode(committed)) {
1785
1813
  throw new Error("preimage does not match the covenant's payment hash");
1786
1814
  }
1787
- const info = await ark.getInfo();
1788
- let serverUnrollScript;
1815
+ const info = await operator.getInfo();
1816
+ let operatorUnrollScript;
1789
1817
  try {
1790
- serverUnrollScript = CSVMultisigTapscript2.decode(hex8.decode(info.checkpointTapscript));
1818
+ operatorUnrollScript = CSVMultisigTapscript2.decode(hex8.decode(info.checkpointTapscript));
1791
1819
  } catch {
1792
- throw new Error("invalid checkpointTapscript from the Arkade server");
1820
+ throw new Error("invalid checkpointTapscript from the operator");
1793
1821
  }
1794
- const leaf = input.script.claim();
1795
- const tapTree = input.script.encode();
1796
- const arkTxid = await signAndSubmitOffchainTx({
1822
+ const leaf = input.contract.claim();
1823
+ const tapTree = input.contract.encode();
1824
+ const txid = await signAndSubmitOffchainTx({
1797
1825
  identity: claimWithPreimageIdentity(input.receiver, input.preimage),
1798
- provider: ark,
1826
+ provider: operator,
1799
1827
  inputs: input.vtxos.map((vtxo) => ({
1800
1828
  txid: vtxo.txid,
1801
1829
  vout: vtxo.vout,
@@ -1806,10 +1834,10 @@ async function pushClaim(ark, input) {
1806
1834
  // One aggregate output: unlike the covenant refund, this leaf inspects
1807
1835
  // nothing about the output set.
1808
1836
  outputs: [{ script: input.destinationPkScript, amount: BigInt(locked) }],
1809
- serverUnrollScript,
1810
- verifyServerSignatures: { serverPubkey: input.script.options.server }
1837
+ serverUnrollScript: operatorUnrollScript,
1838
+ verifyServerSignatures: { serverPubkey: input.contract.options.server }
1811
1839
  });
1812
- return { arkTxid, amount: locked };
1840
+ return { txid, amount: locked };
1813
1841
  }
1814
1842
  async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
1815
1843
  const pollMs = options.pollMs ?? 5e3;
@@ -1824,13 +1852,13 @@ async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
1824
1852
  await sleep2(pollMs);
1825
1853
  }
1826
1854
  }
1827
- async function claimReceiveLockup(indexer, ark, input) {
1855
+ async function claimReceiveLockup(indexer, operator, input) {
1828
1856
  const vtxos = await awaitLockupFunding(indexer, input.swapPkScript, {
1829
1857
  pollMs: input.pollMs,
1830
1858
  deadline: input.deadline
1831
1859
  });
1832
- return pushClaim(ark, {
1833
- script: input.script,
1860
+ return pushClaim(operator, {
1861
+ contract: input.contract,
1834
1862
  receiver: input.receiver,
1835
1863
  preimage: input.preimage,
1836
1864
  vtxos,
@@ -1885,8 +1913,8 @@ async function senderIdentityForSwapRecord(wallet, record) {
1885
1913
  // src/arkadeRefunder.ts
1886
1914
  function arkadeRefunder(deps) {
1887
1915
  return async (swap) => {
1888
- const script = swap.lockup?.script;
1889
- if (!script) {
1916
+ const contract = swap.lockup?.script;
1917
+ if (!contract) {
1890
1918
  throw new Error(
1891
1919
  `swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
1892
1920
  );
@@ -1901,7 +1929,7 @@ function arkadeRefunder(deps) {
1901
1929
  );
1902
1930
  }
1903
1931
  const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
1904
- return pushRefundWithoutReceiver(deps.ark, { script, sender, vtxos });
1932
+ return pushRefundWithoutReceiver(deps.operator, { contract, sender, vtxos });
1905
1933
  };
1906
1934
  }
1907
1935
 
@@ -2297,6 +2325,19 @@ var RfqSwapManager = class {
2297
2325
  async getPendingSwaps() {
2298
2326
  return [...this.monitored.values()];
2299
2327
  }
2328
+ /**
2329
+ * Every swap this manager holds — {@link getPendingSwaps} plus the ones
2330
+ * that already ended. The two sets are disjoint: a swap leaves `monitored`
2331
+ * as it enters `finished`.
2332
+ *
2333
+ * The finished half is what this process has seen, which after
2334
+ * {@link restoreFromRepository} is the stored history minus what retention
2335
+ * pruned. A manager that has restored nothing answers with the live swaps
2336
+ * alone.
2337
+ */
2338
+ async getAllSwaps() {
2339
+ return [...this.monitored.values(), ...this.finished.values()];
2340
+ }
2300
2341
  async hasSwap(rfqId) {
2301
2342
  return this.monitored.has(rfqId);
2302
2343
  }
@@ -2575,14 +2616,14 @@ var RfqSwapManager = class {
2575
2616
  return this.claimIfFunded(swap, vtxos);
2576
2617
  }
2577
2618
  if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
2578
- if (swap.claimArkTxid) return;
2619
+ if (swap.claimTxid) return;
2579
2620
  return this.block(
2580
2621
  swap,
2581
2622
  "the claim window closed with the lockup unclaimed \u2014 only the solver can act now"
2582
2623
  );
2583
2624
  }
2584
2625
  const failure = this.lastClaimError.get(swap.rfqId);
2585
- if (failure && !swap.claimArkTxid) {
2626
+ if (failure && !swap.claimTxid) {
2586
2627
  return this.fail(swap, new Error(failure));
2587
2628
  }
2588
2629
  this.setState(swap, "refunded");
@@ -2597,7 +2638,7 @@ var RfqSwapManager = class {
2597
2638
  */
2598
2639
  async claimIfFunded(swap, vtxos) {
2599
2640
  if (vtxos.length === 0) return this.unblock(swap);
2600
- const partiallyClaimed = swap.claimArkTxid !== void 0;
2641
+ const partiallyClaimed = swap.claimTxid !== void 0;
2601
2642
  if (partiallyClaimed && !this.hasUnclaimedOutpoint(swap.rfqId, vtxos)) {
2602
2643
  return;
2603
2644
  }
@@ -2625,10 +2666,10 @@ var RfqSwapManager = class {
2625
2666
  this.setState(swap, "claimable");
2626
2667
  if (!this.config.enableAutoActions) return;
2627
2668
  try {
2628
- const { arkTxid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
2669
+ const { txid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
2629
2670
  this.lastClaimError.delete(swap.rfqId);
2630
2671
  this.rememberClaimed(swap.rfqId, vtxos);
2631
- swap.claimArkTxid = arkTxid;
2672
+ swap.claimTxid = txid;
2632
2673
  this.touch(swap);
2633
2674
  this.setState(swap, "claimed");
2634
2675
  this.emitAction(swap, "claimLockup");
@@ -2720,7 +2761,7 @@ var RfqSwapManager = class {
2720
2761
  try {
2721
2762
  const pushed = await this.callbacks.refundArkade(swap);
2722
2763
  if (pushed) {
2723
- swap.refundArkTxid = pushed.arkTxid;
2764
+ swap.refundTxid = pushed.txid;
2724
2765
  this.touch(swap);
2725
2766
  }
2726
2767
  this.setState(swap, "refunded");
@@ -2793,7 +2834,7 @@ var RfqSwapManager = class {
2793
2834
  /**
2794
2835
  * Record which ark transactions ended the lockup.
2795
2836
  *
2796
- * Only the ones the indexer actually named: `LockupSpend.arkTxid` is
2837
+ * Only the ones the indexer actually named: `LockupSpend.txid` is
2797
2838
  * optional, and a checkpoint txid is not what history correlates on — a
2798
2839
  * record carrying one would name a transaction the wallet's own activity
2799
2840
  * never shows. Fewer txids is the right failure here.
@@ -2803,9 +2844,9 @@ var RfqSwapManager = class {
2803
2844
  * verdict once.
2804
2845
  */
2805
2846
  stampLockupSpends(swap, spends) {
2806
- const arkTxids = spends.map((spend) => spend.arkTxid).filter((txid) => txid !== void 0);
2807
- if (arkTxids.length === 0) return;
2808
- swap.lockupSpendArkTxids = arkTxids;
2847
+ const txids = spends.map((spend) => spend.txid).filter((txid) => txid !== void 0);
2848
+ if (txids.length === 0) return;
2849
+ swap.lockupSpendTxids = txids;
2809
2850
  this.touch(swap);
2810
2851
  }
2811
2852
  touch(swap) {
@@ -2886,10 +2927,9 @@ var RfqSwapManager = class {
2886
2927
  /**
2887
2928
  * Drop a terminal swap from monitoring and report it exactly once.
2888
2929
  *
2889
- * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
2890
- * Boltz's manager, which fires completion for every swap that leaves
2891
- * monitoring including the failed ones — a listener named "completed" that
2892
- * also fires on failure is a trap worth not inheriting.
2930
+ * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here: a
2931
+ * listener named "completed" that also fires on failure is a trap, so a
2932
+ * swap that leaves monitoring reports through exactly one of them.
2893
2933
  */
2894
2934
  finalize(swap) {
2895
2935
  if (!this.monitored.has(swap.rfqId)) return;
@@ -2925,7 +2965,7 @@ var traderClaimTxid = (swap) => {
2925
2965
  case "onchain_send":
2926
2966
  return swap.claimTxid;
2927
2967
  case "lightning_receive":
2928
- return swap.claimArkTxid;
2968
+ return swap.claimTxid;
2929
2969
  default:
2930
2970
  return void 0;
2931
2971
  }
@@ -2935,7 +2975,7 @@ var outcomeOf = (swap) => {
2935
2975
  const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
2936
2976
  return {
2937
2977
  state: swap.state,
2938
- txid: lostReceive ? swap.refundArkTxid : traderClaimTxid(swap) ?? swap.refundArkTxid
2978
+ txid: lostReceive ? swap.refundTxid : traderClaimTxid(swap) ?? swap.refundTxid
2939
2979
  };
2940
2980
  };
2941
2981
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
@@ -3002,16 +3042,17 @@ async function rfqSwapActivityInputs(deps) {
3002
3042
  const records = await deps.repository.getAllRfqSwaps();
3003
3043
  return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
3004
3044
  }
3005
- async function activityInputOf(record, indexer) {
3045
+ async function activityInputOf(stored, indexer) {
3046
+ const record = normalizeRfqSwapRecord(stored);
3006
3047
  const txids = /* @__PURE__ */ new Set();
3007
- if (record.fundingArkTxid) txids.add(record.fundingArkTxid);
3008
- if (record.refundArkTxid) txids.add(record.refundArkTxid);
3048
+ if (record.fundingTxid) txids.add(record.fundingTxid);
3049
+ if (record.refundTxid) txids.add(record.refundTxid);
3009
3050
  const handler = rfqCorridorHandlers.getOrThrow(record.kind);
3010
3051
  for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
3011
- for (const txid of record.lockupSpendArkTxids ?? []) txids.add(txid);
3012
- const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundArkTxid && !record.lockupSpendArkTxids?.length;
3013
- if (indexer && (!record.fundingArkTxid || spendUnknown)) {
3014
- for (const txid of await lockupTxids(indexer, record, !record.fundingArkTxid)) {
3052
+ for (const txid of record.lockupSpendTxids ?? []) txids.add(txid);
3053
+ const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundTxid && !record.lockupSpendTxids?.length;
3054
+ if (indexer && (!record.fundingTxid || spendUnknown)) {
3055
+ for (const txid of await lockupTxids(indexer, record, !record.fundingTxid)) {
3015
3056
  txids.add(txid);
3016
3057
  }
3017
3058
  }
@@ -3036,6 +3077,334 @@ async function lockupTxids(indexer, record, wantFunding) {
3036
3077
  return [];
3037
3078
  }
3038
3079
  }
3080
+
3081
+ // src/swapClient.ts
3082
+ import { hex as hex11 } from "@scure/base";
3083
+ import { ArkAddress as ArkAddress5, asset as asset2, contractSigner as contractSigner2 } from "@arkade-os/sdk";
3084
+ import { quoteOffer } from "@arkade-os/solver-discovery";
3085
+ var sideCorridorsOf = (market) => {
3086
+ const read = (value) => value === "lightning" || value === "onchain" ? value : "arkade";
3087
+ const m = market;
3088
+ return { base: read(m.base_corridor), quote: read(m.quote_corridor) };
3089
+ };
3090
+ var resolveKind = (market, give) => {
3091
+ const corridors = sideCorridorsOf(market);
3092
+ const giveCorridor = corridors[give];
3093
+ const receiveCorridor = corridors[give === "base" ? "quote" : "base"];
3094
+ if (giveCorridor === "arkade" && receiveCorridor === "arkade") return "spot";
3095
+ if (receiveCorridor === "lightning") return "ln_send";
3096
+ if (giveCorridor === "lightning") return "ln_receive";
3097
+ if (receiveCorridor === "onchain") return "onchain_send";
3098
+ return "onchain_receive";
3099
+ };
3100
+ var need = (value, what, leg) => {
3101
+ if (value === void 0) throw new Error(`a ${leg} quote needs ${what}`);
3102
+ return value;
3103
+ };
3104
+ var corridorAmount = (input, leg) => {
3105
+ const raw = need(input.amount, "an amount", leg);
3106
+ const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", leg);
3107
+ const amount = Number(raw);
3108
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
3109
+ throw new Error(`a ${leg} amount must be a positive integer of sats, got ${String(raw)}`);
3110
+ }
3111
+ return { amount, amountSide: amountOn === "give" ? "from" : "to" };
3112
+ };
3113
+ function createSwapClient(deps) {
3114
+ const { wallet, repository, transportFor } = deps;
3115
+ let broadcaster;
3116
+ const broadcasting = () => broadcaster ??= wallet.getArkadeBroadcaster();
3117
+ const ark = deps.ark ?? {
3118
+ getInfo: () => wallet.getArkadeInfo({ requireLive: true }),
3119
+ submitTx: async (...args) => (await broadcasting()).submitTx(...args),
3120
+ finalizeTx: async (...args) => (await broadcasting()).finalizeTx(...args)
3121
+ };
3122
+ let reader;
3123
+ const reading = () => reader ??= wallet.getArkadeReader();
3124
+ const indexer = deps.indexer ?? {
3125
+ getVtxos: async (opts) => {
3126
+ if (!opts)
3127
+ throw new Error("getVtxos on the swap indexer requires scripts or outpoints");
3128
+ return (await reading()).getVtxos(opts);
3129
+ },
3130
+ getVirtualTxs: async (...args) => (await reading()).getVirtualTxs(...args)
3131
+ };
3132
+ const managerDeps = { indexer, chain: deps.chain, repository };
3133
+ const manager = new RfqSwapManager(managerDeps);
3134
+ const listeners = /* @__PURE__ */ new Set();
3135
+ const notify2 = (swap) => {
3136
+ for (const listener of listeners) {
3137
+ try {
3138
+ listener(swap);
3139
+ } catch {
3140
+ }
3141
+ }
3142
+ };
3143
+ manager.onSwapUpdate((swap) => notify2({ family: "rfq", swap }));
3144
+ const claimLockup = async (swap, vtxos, { partiallyClaimed }) => {
3145
+ const record = await repository.getRfqSwap(swap.rfqId);
3146
+ if (!record) throw new Error(`rfq swap ${swap.rfqId} has no stored record to claim from`);
3147
+ const secret = rfqClaimSecretOf(record);
3148
+ if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
3149
+ const script = swap.lockup?.script;
3150
+ if (!script) throw new Error(`rfq swap ${swap.rfqId} carries no lockup covenant`);
3151
+ const payoutAddress = record.profile.payoutAddress;
3152
+ if (!payoutAddress) throw new Error(`rfq swap ${swap.rfqId} carries no payoutAddress`);
3153
+ return pushClaim(ark, {
3154
+ contract: script,
3155
+ receiver: await contractSigner2(wallet, secret.signingDescriptor),
3156
+ preimage: await preimageForSwapRecord(wallet, secret),
3157
+ vtxos,
3158
+ destinationPkScript: ArkAddress5.decode(payoutAddress).pkScript,
3159
+ expectedAmount: swap.expectedAmount,
3160
+ partiallyClaimed
3161
+ });
3162
+ };
3163
+ manager.setCallbacks({
3164
+ refundArkade: arkadeRefunder({ operator: ark, indexer, wallet, repository }),
3165
+ claimLockup,
3166
+ ...deps.claimOnchain ? { claimOnchain: deps.claimOnchain } : {}
3167
+ });
3168
+ let watcher;
3169
+ const quote = async (market, input) => {
3170
+ const kind = resolveKind(market, input.give);
3171
+ switch (kind) {
3172
+ case "spot": {
3173
+ const raw = need(input.amount, "an amount", kind);
3174
+ const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", kind);
3175
+ const plan = await quoteOffer(market, {
3176
+ give: input.give,
3177
+ ...amountOn === "give" ? { giveAmount: raw } : { wantAmount: raw },
3178
+ fetchImpl: deps.discovery.fetchImpl
3179
+ });
3180
+ return { kind, market, plan };
3181
+ }
3182
+ case "ln_send": {
3183
+ const invoice = need(input.invoice, "the invoice to pay", kind);
3184
+ const request = await requestLightningSend(wallet, transportFor(market), {
3185
+ invoice,
3186
+ emulatorPubkey: deps.emulatorPubkey
3187
+ });
3188
+ return { kind, market, request };
3189
+ }
3190
+ case "ln_receive": {
3191
+ const params = {
3192
+ ...corridorAmount(input, kind),
3193
+ covclaimdPubkey: need(deps.covclaimdPubkey, "deps.covclaimdPubkey", kind),
3194
+ decodeInvoice: need(deps.decodeInvoice, "deps.decodeInvoice", kind),
3195
+ maxPayAmount: input.maxPayAmount,
3196
+ emulatorPubkey: deps.emulatorPubkey
3197
+ };
3198
+ const request = await requestLightningReceive(wallet, transportFor(market), params);
3199
+ return { kind, market, request, invoice: request.invoice };
3200
+ }
3201
+ case "onchain_send": {
3202
+ need(deps.chain, "deps.chain (L1 access)", kind);
3203
+ const params = {
3204
+ ...corridorAmount(input, kind),
3205
+ payoutPubkey: need(input.payoutPubkey, "a payoutPubkey", kind),
3206
+ preimage: input.preimage,
3207
+ emulatorPubkey: deps.emulatorPubkey
3208
+ };
3209
+ const request = await requestOnchainSend(wallet, transportFor(market), params);
3210
+ return { kind, market, request };
3211
+ }
3212
+ case "onchain_receive":
3213
+ throw new Error(
3214
+ "onchain->arkade is not driven by RfqSwapManager yet; quote it directly with requestOnchainReceive"
3215
+ );
3216
+ }
3217
+ };
3218
+ const admit = async (swap, origin) => {
3219
+ let announced = false;
3220
+ const heard = manager.onSwapUpdate((updated) => {
3221
+ if (updated.rfqId === swap.rfqId) announced = true;
3222
+ });
3223
+ try {
3224
+ await manager.addSwap(swap, origin);
3225
+ } finally {
3226
+ heard();
3227
+ }
3228
+ const unified = { family: "rfq", swap };
3229
+ if (!announced) notify2(unified);
3230
+ return unified;
3231
+ };
3232
+ const fundPersisted = async (swap, origin, funding) => {
3233
+ await repository.saveRfqSwap(createRfqSwapRecord(origin, swap));
3234
+ return wallet.send({ address: funding.address, amount: funding.amount });
3235
+ };
3236
+ const accept = async (accepted) => {
3237
+ const now = Math.floor(Date.now() / 1e3);
3238
+ switch (accepted.kind) {
3239
+ case "spot": {
3240
+ const { plan } = accepted;
3241
+ const depositIsBtc = plan.deposit.asset.id === BTC_ASSET_ID;
3242
+ const offer = await createOffer(wallet, {
3243
+ wantAmount: plan.receive.atomic,
3244
+ ...plan.receive.asset.id === BTC_ASSET_ID ? { offerAsset: asset2.AssetId.fromString(plan.deposit.asset.id) } : { wantAsset: asset2.AssetId.fromString(plan.receive.asset.id) },
3245
+ emulatorPubkey: deps.emulatorPubkey
3246
+ });
3247
+ const txid = await wallet.send({
3248
+ address: offer.address,
3249
+ // asset deposits ride the SDK's dust-sat carrier
3250
+ amount: depositIsBtc ? Number(plan.deposit.atomic) : void 0,
3251
+ assets: depositIsBtc ? void 0 : [{ assetId: plan.deposit.asset.id, amount: plan.deposit.atomic }],
3252
+ extensions: [offer.extension]
3253
+ });
3254
+ const swap = {
3255
+ id: txid,
3256
+ fromAsset: plan.deposit.asset.id,
3257
+ toAsset: plan.receive.asset.id,
3258
+ fromAmount: plan.deposit.atomic.toString(),
3259
+ toAmount: plan.receive.atomic.toString(),
3260
+ swapAddress: offer.address,
3261
+ swapPkScript: hex11.encode(offer.swapPkScript),
3262
+ offerHex: offer.offerHex,
3263
+ fundingTxid: txid,
3264
+ status: "pending",
3265
+ createdAt: Date.now()
3266
+ };
3267
+ await addAssetSwap(repository, swap);
3268
+ const unified = { family: "offer", swap };
3269
+ notify2(unified);
3270
+ return unified;
3271
+ }
3272
+ case "ln_send": {
3273
+ const { request } = accepted;
3274
+ const swap = {
3275
+ kind: "lightning_send",
3276
+ rfqId: request.rfqId,
3277
+ state: "pending",
3278
+ lockupPkScript: request.swapPkScript,
3279
+ lockup: { script: request.script, address: request.address },
3280
+ // the trader's own decode, never the solver's echo
3281
+ paymentHash: request.contractParams.paymentHash,
3282
+ refundLocktime: request.contractParams.refundLocktime,
3283
+ createdAt: now,
3284
+ updatedAt: now
3285
+ };
3286
+ const origin = {
3287
+ kind: "lightning_send",
3288
+ lockupAddress: request.address,
3289
+ profile: rfqSecretsProfile(request.secrets, swap.paymentHash),
3290
+ amount: request.fundAmount
3291
+ };
3292
+ const txid = await fundPersisted(swap, origin, {
3293
+ address: request.address,
3294
+ amount: request.fundAmount
3295
+ });
3296
+ return admit(swap, { ...origin, fundingTxid: txid });
3297
+ }
3298
+ case "ln_receive": {
3299
+ const { request } = accepted;
3300
+ const paymentHash = hex11.encode(request.secrets.paymentHash);
3301
+ const swap = {
3302
+ kind: "lightning_receive",
3303
+ rfqId: request.rfqId,
3304
+ state: "pending",
3305
+ lockupPkScript: request.swapPkScript,
3306
+ lockup: { script: request.script, address: request.address },
3307
+ paymentHash,
3308
+ refundLocktime: request.contractParams.refundLocktime,
3309
+ expectedAmount: request.expectedAmount,
3310
+ createdAt: now,
3311
+ updatedAt: now
3312
+ };
3313
+ return admit(swap, {
3314
+ kind: "lightning_receive",
3315
+ lockupAddress: request.address,
3316
+ profile: {
3317
+ ...rfqSecretsProfile(request.secrets, paymentHash),
3318
+ expectedAmount: request.expectedAmount,
3319
+ payoutAddress: request.payoutAddress
3320
+ },
3321
+ amount: request.expectedAmount
3322
+ });
3323
+ }
3324
+ case "onchain_send": {
3325
+ const { request } = accepted;
3326
+ const paymentHash = hex11.encode(request.secrets.paymentHash);
3327
+ const swap = {
3328
+ kind: "onchain_send",
3329
+ rfqId: request.rfqId,
3330
+ state: "pending",
3331
+ lockupPkScript: request.swapPkScript,
3332
+ lockup: { script: request.script, address: request.address },
3333
+ paymentHash,
3334
+ refundLocktime: request.refundLocktime,
3335
+ htlc: request.htlc,
3336
+ minConfirmations: request.minConfirmations,
3337
+ createdAt: now,
3338
+ updatedAt: now
3339
+ };
3340
+ const origin = {
3341
+ kind: "onchain_send",
3342
+ lockupAddress: request.address,
3343
+ profile: {
3344
+ ...rfqSecretsProfile(request.secrets, paymentHash),
3345
+ ...onchainSendProfile(request)
3346
+ },
3347
+ amount: request.fundAmount
3348
+ };
3349
+ const txid = await fundPersisted(swap, origin, {
3350
+ address: request.address,
3351
+ amount: request.fundAmount
3352
+ });
3353
+ return admit(swap, { ...origin, fundingTxid: txid });
3354
+ }
3355
+ }
3356
+ };
3357
+ return {
3358
+ manager,
3359
+ markets: (useCache) => discoverMarkets({ ...deps.discovery, repository, useCache }),
3360
+ quote,
3361
+ accept,
3362
+ cancel: async (fundingTxid) => {
3363
+ const swaps = await getAssetSwapsOrThrow(repository);
3364
+ const swap = swaps.find((s) => s.id === fundingTxid);
3365
+ if (!swap) throw new Error(`no offer swap with funding txid ${fundingTxid}`);
3366
+ if (swap.status !== "pending") {
3367
+ throw new Error(`offer swap ${fundingTxid} is ${swap.status}, not cancellable`);
3368
+ }
3369
+ await updateAssetSwap(repository, fundingTxid, { status: "cancelling" });
3370
+ await cancelOffer(wallet, swap.offerHex, {
3371
+ repository,
3372
+ fundingTxid
3373
+ });
3374
+ },
3375
+ // Both families, live AND terminal — the offer half reads the whole
3376
+ // stored history, and the RFQ half is every swap the manager holds.
3377
+ // Its history is what `start()` restored plus what this session
3378
+ // accepted; records past `RFQ_SWAP_RETENTION_SECONDS` are pruned.
3379
+ swaps: async () => {
3380
+ const offers = await getAssetSwaps(repository);
3381
+ const rfq = await manager.getAllSwaps();
3382
+ return [
3383
+ ...offers.map((swap) => ({ family: "offer", swap })),
3384
+ ...rfq.map((swap) => ({ family: "rfq", swap }))
3385
+ ];
3386
+ },
3387
+ onUpdate: (listener) => {
3388
+ listeners.add(listener);
3389
+ return () => listeners.delete(listener);
3390
+ },
3391
+ start: async () => {
3392
+ managerDeps.contracts ??= await wallet.getContractManager();
3393
+ await manager.restoreFromRepository();
3394
+ await manager.start();
3395
+ watcher ??= await watchOfferSwaps({
3396
+ wallet,
3397
+ repository,
3398
+ onUpdate: (swap) => notify2({ family: "offer", swap })
3399
+ });
3400
+ },
3401
+ stop: async () => {
3402
+ await manager.stop();
3403
+ watcher?.stop();
3404
+ watcher = void 0;
3405
+ }
3406
+ };
3407
+ }
3039
3408
  export {
3040
3409
  ARKADE_ASSET,
3041
3410
  ARKADE_BTC,
@@ -3096,6 +3465,7 @@ export {
3096
3465
  classifySpend,
3097
3466
  createOffer,
3098
3467
  createRfqSwapRecord,
3468
+ createSwapClient,
3099
3469
  decodeOffer,
3100
3470
  deriveLightningReceive,
3101
3471
  deriveOnchainReceive,
@@ -3110,16 +3480,18 @@ export {
3110
3480
  httpTransport,
3111
3481
  isRfqSwapTerminal,
3112
3482
  isRfqTerminal,
3483
+ lightningReceiveContract,
3113
3484
  lightningReceiveRequest,
3485
+ lightningSendContract,
3114
3486
  lightningSendRequest,
3115
- lightningSendVtxoScript,
3116
3487
  lockupContractParams,
3117
3488
  makeCachedFeedFetch,
3118
3489
  newPreimage,
3119
3490
  newRfqId,
3120
3491
  nextOnchainAction,
3492
+ normalizeRfqSwapRecord,
3493
+ offerContract,
3121
3494
  offerTermsFromQuote,
3122
- offerVtxoScript,
3123
3495
  onchainHtlcScript,
3124
3496
  onchainReceiveRequest,
3125
3497
  onchainSendProfile,
@@ -3130,7 +3502,6 @@ export {
3130
3502
  pushRefundWithoutReceiver,
3131
3503
  readLockupFate,
3132
3504
  rebuildRfqSwap,
3133
- receiveVtxoScript,
3134
3505
  refundIfUnresolved,
3135
3506
  registerLockupContract,
3136
3507
  relayTransport,