@arkade-os/swap 0.0.6 → 0.0.8

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.
@@ -25,6 +25,11 @@ function onchainHtlcScript(params, network) {
25
25
  `refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
26
26
  );
27
27
  }
28
+ if (!Object.hasOwn(L1_NETWORKS, network)) {
29
+ throw new Error(
30
+ `unknown L1 network '${String(network)}' \u2014 expected one of ${Object.keys(L1_NETWORKS).join(", ")}`
31
+ );
32
+ }
28
33
  const h160 = h160FromPaymentHash(params.paymentHash);
29
34
  const claim = btc.Script.encode([
30
35
  "SIZE",
@@ -249,10 +254,27 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
249
254
 
250
255
  // src/lockupContract.ts
251
256
  import { hex as hex2 } from "@scure/base";
252
- import { VHTLCV2ContractHandler } from "@arkade-os/sdk";
257
+ import {
258
+ ArkAddress,
259
+ VHTLCV2ContractHandler
260
+ } from "@arkade-os/sdk";
253
261
  var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
254
262
  var SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
255
263
  var SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
264
+ var LockupContractMissing = class extends Error {
265
+ /** The lockup whose row is absent. */
266
+ address;
267
+ /** Its pkScript hex — the key the row would have been under. */
268
+ script;
269
+ constructor(address, script) {
270
+ super(
271
+ `no contract row for lockup ${address} (script ${script}); its covenant cannot be rebuilt from this wallet's contract store`
272
+ );
273
+ this.name = "LockupContractMissing";
274
+ this.address = address;
275
+ this.script = script;
276
+ }
277
+ };
256
278
  var LockupRegistrationFailed = class extends Error {
257
279
  /** The lockup address that was never registered — never fund it: nothing
258
280
  * is watching it. */
@@ -281,12 +303,18 @@ async function registerLockupContract(contracts, script, address) {
281
303
  throw new LockupRegistrationFailed(script, address, error);
282
304
  }
283
305
  }
306
+ async function lockupContractParams(contracts, lockupAddress) {
307
+ const script = hex2.encode(ArkAddress.decode(lockupAddress).pkScript);
308
+ const [row] = await contracts.getContracts({ script });
309
+ if (!row) throw new LockupContractMissing(lockupAddress, script);
310
+ return row.params;
311
+ }
284
312
 
285
313
  // src/rfq.ts
286
314
  import { hex as hex3 } from "@scure/base";
287
315
  import { ripemd160 as ripemd1602 } from "@noble/hashes/legacy.js";
288
316
  import {
289
- ArkAddress,
317
+ ArkAddress as ArkAddress2,
290
318
  RestArkProvider,
291
319
  VHTLC,
292
320
  getNetwork,
@@ -311,9 +339,10 @@ var solverHex = (value, field) => {
311
339
  }
312
340
  };
313
341
  var ARKADE_BTC = "arkade:BTC";
314
- var ARKADE_ASSET = "arkade:ASSET";
315
342
  var LIGHTNING_BTC = "lightning:BTC";
316
343
  var ONCHAIN_BTC = "onchain:BTC";
344
+ var arkadeAssetLeg = (id) => `arkade:${id.toString()}`;
345
+ var ARKADE_ASSET = "arkade:ASSET";
317
346
  var rfqPair = (from, to) => `${from}->${to}`;
318
347
  var LIGHTNING_SEND_PAIR = rfqPair(ARKADE_BTC, LIGHTNING_BTC);
319
348
  var LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
@@ -354,23 +383,34 @@ var lightningSendRequest = (input) => ({
354
383
  }
355
384
  });
356
385
  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)");
386
+ if (!input.wantAsset && !input.offerAsset) {
387
+ throw new Error(
388
+ "set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC) \u2014 with neither set both legs are BTC, which is not a swap"
389
+ );
359
390
  }
391
+ if (input.wantAsset && input.offerAsset) {
392
+ throw new Error(
393
+ "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"
394
+ );
395
+ }
396
+ const pair = rfqPair(
397
+ input.offerAsset ? arkadeAssetLeg(input.offerAsset) : ARKADE_BTC,
398
+ input.wantAsset ? arkadeAssetLeg(input.wantAsset) : ARKADE_BTC
399
+ );
400
+ assertPairLength(pair);
360
401
  return {
361
402
  v: 1,
362
403
  type: "rfq_request",
363
404
  rfq_id: input.rfqId,
364
- pair: rfqPair(
365
- input.offerAsset ? ARKADE_ASSET : ARKADE_BTC,
366
- input.wantAsset ? ARKADE_ASSET : ARKADE_BTC
367
- ),
405
+ pair,
368
406
  amount_side: input.amountSide,
369
407
  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
- }
408
+ // The pair is the only place the asset ids appear. Repeating them here
409
+ // would be a key the solver's `.strict()` profile schema does not
410
+ // declare, and an undeclared key is `unsupported_payload` — a refusal,
411
+ // not an ignored extra. Empty, not absent: `profile` is required on
412
+ // every other request shape this wire carries.
413
+ profile: {}
374
414
  };
375
415
  };
376
416
  var MIN_HEADROOM_SECONDS = 90 * 60;
@@ -384,6 +424,14 @@ var assertFinite = (value, reason, label) => {
384
424
  throw gateError(reason, `${label} is not a finite number (${String(value)})`);
385
425
  }
386
426
  };
427
+ var MAX_PAIR_LENGTH = 158;
428
+ var assertPairLength = (pair) => {
429
+ if (pair.length > MAX_PAIR_LENGTH) {
430
+ throw new Error(
431
+ `pair is ${pair.length} characters, over the wire's ${MAX_PAIR_LENGTH}-character limit`
432
+ );
433
+ }
434
+ };
387
435
  var verifyLockupAddress = (quote, derivedAddress) => {
388
436
  const quoted = quote.profile?.lockup_address;
389
437
  if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
@@ -423,14 +471,20 @@ var assertFundable = (input) => {
423
471
  }
424
472
  }
425
473
  };
426
- var expectQuote = (payload, rfqId) => {
474
+ var expectQuote = (payload, rfqId, requestedPair) => {
427
475
  const p = payload;
428
476
  if (p?.type === "rfq_refusal") throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId);
429
477
  if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
430
478
  throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
431
479
  }
480
+ if (requestedPair !== void 0 && p.pair !== requestedPair) {
481
+ throw new Error(
482
+ `solver quoted ${JSON.stringify(p.pair)}, not the requested ${requestedPair}`
483
+ );
484
+ }
432
485
  return payload;
433
486
  };
487
+ var pairOf = (payload) => typeof payload.pair === "string" ? payload.pair : void 0;
434
488
  var httpTransport = (baseUrl, options = {}) => {
435
489
  const fetchImpl = options.fetchImpl ?? fetch;
436
490
  const readJson = async (response, what) => {
@@ -450,7 +504,11 @@ var httpTransport = (baseUrl, options = {}) => {
450
504
  headers: { "content-type": "application/json" },
451
505
  body: JSON.stringify(payload)
452
506
  });
453
- return expectQuote(await readJson(response, "quote request"), String(payload.rfq_id));
507
+ return expectQuote(
508
+ await readJson(response, "quote request"),
509
+ String(payload.rfq_id),
510
+ pairOf(payload)
511
+ );
454
512
  },
455
513
  async status(rfqId) {
456
514
  const response = await fetchImpl(`${baseUrl}/v1/rfq/${rfqId}`, { method: "GET" });
@@ -527,7 +585,8 @@ var relayTransport = (relayUrl, options) => {
527
585
  async requestQuote(payload) {
528
586
  return expectQuote(
529
587
  await roundTrip(payload, String(payload.rfq_id)),
530
- String(payload.rfq_id)
588
+ String(payload.rfq_id),
589
+ pairOf(payload)
531
590
  );
532
591
  },
533
592
  async status(rfqId) {
@@ -618,7 +677,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
618
677
  }
619
678
  const serverPubkey = xOnly(hex3.decode(info.signerPubkey), "ark signer key");
620
679
  const network = getNetwork(info.network);
621
- const script = lightningSendVtxoScript({
680
+ const treeParams = {
622
681
  solverPubkey: xOnly(hex3.decode(quote.solver_pubkey), "solver key"),
623
682
  refundLocktime: quote.refund_locktime,
624
683
  serverPubkey,
@@ -630,8 +689,9 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
630
689
  ),
631
690
  senderPubkey,
632
691
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
633
- refundPkScript: ArkAddress.decode(refundAddress).pkScript
634
- });
692
+ refundPkScript: ArkAddress2.decode(refundAddress).pkScript
693
+ };
694
+ const script = lightningSendVtxoScript(treeParams);
635
695
  const address = script.address(network.hrp, serverPubkey).encode();
636
696
  verifyLockupAddress(quote, address);
637
697
  assertFundable({
@@ -651,7 +711,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
651
711
  script,
652
712
  refundAddress,
653
713
  senderPubkey,
654
- secrets
714
+ secrets,
715
+ treeParams
655
716
  };
656
717
  }
657
718
  var offerTermsFromQuote = (quote, assets) => {
@@ -725,25 +786,25 @@ function deriveOnchainSend(input) {
725
786
  emulatorPubkey: input.emulatorPubkey,
726
787
  senderPubkey: input.senderPubkey,
727
788
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
728
- refundPkScript: ArkAddress.decode(input.refundAddress).pkScript
789
+ refundPkScript: ArkAddress2.decode(input.refundAddress).pkScript
729
790
  });
730
791
  const address = script.address(input.hrp, input.serverPubkey).encode();
731
792
  verifyLockupAddress(quote, address);
732
- const htlc = onchainHtlcScript(
733
- {
734
- paymentHash: input.paymentHash,
735
- claimKey: input.payoutPubkey,
736
- refundKey: xOnly(hex3.decode(htlcPubkey), "solver L1 htlc key"),
737
- refundLocktime: htlcLocktime
738
- },
739
- input.l1Network
740
- );
793
+ const htlcParams = {
794
+ paymentHash: input.paymentHash,
795
+ claimKey: input.payoutPubkey,
796
+ refundKey: xOnly(hex3.decode(htlcPubkey), "solver L1 htlc key"),
797
+ refundLocktime: htlcLocktime
798
+ };
799
+ const htlc = onchainHtlcScript(htlcParams, input.l1Network);
741
800
  if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
742
801
  return {
743
802
  address,
744
803
  swapPkScript: script.pkScript,
745
804
  script,
746
805
  htlc,
806
+ htlcParams,
807
+ l1Network: input.l1Network,
747
808
  refundLocktime,
748
809
  htlcLocktime,
749
810
  minConfirmations
@@ -813,6 +874,9 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
813
874
  script: derived.script,
814
875
  refundAddress,
815
876
  htlc: derived.htlc,
877
+ htlcParams: derived.htlcParams,
878
+ l1Network: derived.l1Network,
879
+ minConfirmations: derived.minConfirmations,
816
880
  senderPubkey,
817
881
  secrets
818
882
  };
@@ -919,7 +983,7 @@ function deriveLightningReceive(input) {
919
983
  if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
920
984
  throw new Error("lightning-receive quote is missing a binding field");
921
985
  }
922
- const script = receiveVtxoScript({
986
+ const treeParams = {
923
987
  solverPubkey: xOnly(hex3.decode(quote.solver_pubkey), "solver key"),
924
988
  refundLocktime,
925
989
  serverPubkey: input.serverPubkey,
@@ -928,11 +992,12 @@ function deriveLightningReceive(input) {
928
992
  emulatorPubkey: input.emulatorPubkey,
929
993
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
930
994
  payoutPubkey: input.payoutPubkey,
931
- payoutPkScript: ArkAddress.decode(input.payoutAddress).pkScript
932
- });
995
+ payoutPkScript: ArkAddress2.decode(input.payoutAddress).pkScript
996
+ };
997
+ const script = receiveVtxoScript(treeParams);
933
998
  const address = script.address(input.hrp, input.serverPubkey).encode();
934
999
  verifyLockupAddress(quote, address);
935
- return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime };
1000
+ return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
936
1001
  }
937
1002
  async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
938
1003
  const rfqId = params.rfqId ?? newRfqId();
@@ -1004,7 +1069,8 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
1004
1069
  script: derived.script,
1005
1070
  payoutAddress,
1006
1071
  payoutPubkey,
1007
- secrets
1072
+ secrets,
1073
+ treeParams: derived.treeParams
1008
1074
  };
1009
1075
  }
1010
1076
  function deriveOnchainReceive(input) {
@@ -1028,7 +1094,7 @@ function deriveOnchainReceive(input) {
1028
1094
  emulatorPubkey: input.emulatorPubkey,
1029
1095
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
1030
1096
  payoutPubkey: input.payoutPubkey,
1031
- payoutPkScript: ArkAddress.decode(input.payoutAddress).pkScript
1097
+ payoutPkScript: ArkAddress2.decode(input.payoutAddress).pkScript
1032
1098
  });
1033
1099
  const address = script.address(input.hrp, input.serverPubkey).encode();
1034
1100
  verifyLockupAddress(quote, address);
@@ -1148,12 +1214,15 @@ export {
1148
1214
  SWAP_LOCKUP_CONTRACT_TYPE,
1149
1215
  SWAP_LOCKUP_CONTRACT_LABEL,
1150
1216
  SWAP_LOCKUP_CONTRACT_KIND,
1217
+ LockupContractMissing,
1151
1218
  LockupRegistrationFailed,
1152
1219
  registerLockupContract,
1220
+ lockupContractParams,
1153
1221
  ARKADE_BTC,
1154
- ARKADE_ASSET,
1155
1222
  LIGHTNING_BTC,
1156
1223
  ONCHAIN_BTC,
1224
+ arkadeAssetLeg,
1225
+ ARKADE_ASSET,
1157
1226
  rfqPair,
1158
1227
  LIGHTNING_SEND_PAIR,
1159
1228
  LIGHTNING_RECEIVE_PAIR,
@@ -1168,6 +1237,8 @@ export {
1168
1237
  MIN_HEADROOM_SECONDS,
1169
1238
  verifyLockupAddress,
1170
1239
  assertFundable,
1240
+ expectQuote,
1241
+ pairOf,
1171
1242
  httpTransport,
1172
1243
  relayTransport,
1173
1244
  SOLO_REFUND_HEADROOM_SECONDS,