@arkade-os/boltz-swap 0.2.13 → 0.2.15

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
@@ -89,7 +89,7 @@ import {
89
89
  getSequence as getSequence2
90
90
  } from "@arkade-os/sdk";
91
91
  import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
92
- import { base64 as base643, hex as hex2 } from "@scure/base";
92
+ import { base64 as base643, hex as hex3 } from "@scure/base";
93
93
  import { randomBytes } from "@noble/hashes/utils.js";
94
94
 
95
95
  // src/boltz-swap-provider.ts
@@ -131,7 +131,7 @@ var isSubmarineSwapRefundable = (swap) => {
131
131
  return isSubmarineRefundableStatus(swap.status) && isPendingSubmarineSwap(swap) && swap.refundable !== false && swap.refunded !== true;
132
132
  };
133
133
  var isGetReverseSwapTxIdResponse = (data) => {
134
- return data && typeof data === "object" && typeof data.id === "string" && typeof data.timeoutBlockHeight === "number";
134
+ return data && typeof data === "object" && typeof data.id === "string" && typeof data.hex === "string" && typeof data.timeoutBlockHeight === "number";
135
135
  };
136
136
  var isGetSwapStatusResponse = (data) => {
137
137
  return data && typeof data === "object" && typeof data.status === "string" && (data.zeroConfRejected === void 0 || typeof data.zeroConfRejected === "boolean") && (data.transaction === void 0 || data.transaction && typeof data.transaction === "object" && typeof data.transaction.id === "string" && (data.transaction.eta === void 0 || typeof data.transaction.eta === "number") && (data.transaction.hex === void 0 || typeof data.transaction.hex === "string") && (data.transaction.preimage === void 0 || typeof data.transaction.preimage === "string"));
@@ -154,6 +154,26 @@ var isCreateReverseSwapResponse = (data) => {
154
154
  var isRefundSubmarineSwapResponse = (data) => {
155
155
  return data && typeof data === "object" && typeof data.transaction === "string" && typeof data.checkpoint === "string";
156
156
  };
157
+ var isLeaf = (data) => {
158
+ return data && typeof data === "object" && typeof data.version === "number" && typeof data.output === "string";
159
+ };
160
+ var isTree = (data) => {
161
+ return data && typeof data === "object" && isLeaf(data.claimLeaf) && isLeaf(data.refundLeaf) && isLeaf(data.refundWithoutBoltzLeaf) && isLeaf(data.unilateralClaimLeaf) && isLeaf(data.unilateralRefundLeaf) && isLeaf(data.unilateralRefundWithoutBoltzLeaf);
162
+ };
163
+ var isDetails = (data) => {
164
+ return data && typeof data === "object" && isTree(data.tree) && (data.amount === void 0 || typeof data.amount === "number") && typeof data.keyIndex === "number" && (data.transaction === void 0 || data.transaction && typeof data.transaction === "object" && typeof data.transaction.id === "string" && typeof data.transaction.vout === "number") && typeof data.lockupAddress === "string" && typeof data.serverPublicKey === "string" && typeof data.timeoutBlockHeight === "number" && (data.preimageHash === void 0 || typeof data.preimageHash === "string");
165
+ };
166
+ var isRestoredSubmarineSwap = (data) => {
167
+ return data && typeof data === "object" && data.to === "BTC" && typeof data.id === "string" && data.from === "ARK" && data.type === "submarine" && typeof data.createdAt === "number" && typeof data.preimageHash === "string" && typeof data.status === "string" && isDetails(data.refundDetails);
168
+ };
169
+ var isRestoredReverseSwap = (data) => {
170
+ return data && typeof data === "object" && data.to === "ARK" && typeof data.id === "string" && data.from === "BTC" && data.type === "reverse" && typeof data.createdAt === "number" && typeof data.preimageHash === "string" && typeof data.status === "string" && isDetails(data.claimDetails);
171
+ };
172
+ var isCreateSwapsRestoreResponse = (data) => {
173
+ return Array.isArray(data) && data.every(
174
+ (item) => isRestoredReverseSwap(item) || isRestoredSubmarineSwap(item)
175
+ );
176
+ };
157
177
  var BASE_URLS = {
158
178
  mutinynet: "https://api.boltz.mutinynet.arkade.sh",
159
179
  regtest: "http://localhost:9069"
@@ -384,6 +404,21 @@ var BoltzSwapProvider = class {
384
404
  };
385
405
  });
386
406
  }
407
+ async restoreSwaps(publicKey) {
408
+ const requestBody = {
409
+ publicKey
410
+ };
411
+ const response = await this.request(
412
+ "/v2/swap/restore",
413
+ "POST",
414
+ requestBody
415
+ );
416
+ if (!isCreateSwapsRestoreResponse(response))
417
+ throw new SchemaError({
418
+ message: "Invalid schema in response for swap restoration"
419
+ });
420
+ return response;
421
+ }
387
422
  async request(path, method, body) {
388
423
  const url = `${this.apiUrl}${path}`;
389
424
  try {
@@ -451,6 +486,50 @@ var verifySignatures = (tx, inputIndex, requiredSigners) => {
451
486
  }
452
487
  };
453
488
 
489
+ // src/utils/restoration.ts
490
+ import { hex } from "@scure/base";
491
+ import { Script } from "@scure/btc-signer";
492
+ import bip68 from "bip68";
493
+ function extractTimeLockFromLeafOutput(scriptHex) {
494
+ if (!scriptHex) return 0;
495
+ try {
496
+ const opcodes = Script.decode(hex.decode(scriptHex));
497
+ const hasCLTV = opcodes.findIndex((op) => op === "CHECKLOCKTIMEVERIFY");
498
+ if (hasCLTV > 0) {
499
+ const data = opcodes[hasCLTV - 1];
500
+ if (data instanceof Uint8Array) {
501
+ const dataBytes = new Uint8Array(data).reverse();
502
+ return parseInt(hex.encode(dataBytes), 16);
503
+ }
504
+ }
505
+ const hasCSV = opcodes.findIndex((op) => op === "CHECKSEQUENCEVERIFY");
506
+ if (hasCSV > 0) {
507
+ const data = opcodes[hasCSV - 1];
508
+ if (data instanceof Uint8Array) {
509
+ const dataBytes = new Uint8Array(data).reverse();
510
+ const {
511
+ blocks,
512
+ seconds
513
+ } = bip68.decode(
514
+ parseInt(hex.encode(dataBytes), 16)
515
+ );
516
+ return blocks ?? seconds ?? 0;
517
+ }
518
+ }
519
+ } catch (error) {
520
+ return 0;
521
+ }
522
+ return 0;
523
+ }
524
+ function extractInvoiceAmount(amountSats, fees) {
525
+ if (!amountSats) return 0;
526
+ const { percentage, minerFees } = fees.reverse;
527
+ const miner = minerFees.lockup + minerFees.claim;
528
+ if (percentage >= 100 || percentage < 0) return 0;
529
+ if (miner >= amountSats) return 0;
530
+ return Math.ceil((amountSats - miner) / (1 - percentage / 100));
531
+ }
532
+
454
533
  // src/logger.ts
455
534
  var logger = console;
456
535
  function setLogger(customLogger) {
@@ -948,6 +1027,12 @@ var SwapManager = class {
948
1027
  try {
949
1028
  this.swapsInProgress.add(swap.id);
950
1029
  if (isPendingReverseSwap(swap)) {
1030
+ if (!swap.preimage || swap.preimage.length === 0) {
1031
+ logger.log(
1032
+ `Skipping claim for swap ${swap.id}: missing preimage (restored swap)`
1033
+ );
1034
+ return;
1035
+ }
951
1036
  if (isReverseClaimableStatus(swap.status)) {
952
1037
  logger.log(`Auto-claiming reverse swap ${swap.id}`);
953
1038
  await this.executeClaimAction(swap);
@@ -956,6 +1041,12 @@ var SwapManager = class {
956
1041
  );
957
1042
  }
958
1043
  } else if (isPendingSubmarineSwap(swap)) {
1044
+ if (!swap.request?.invoice || swap.request.invoice.length === 0) {
1045
+ logger.log(
1046
+ `Skipping refund for swap ${swap.id}: missing invoice (restored swap)`
1047
+ );
1048
+ return;
1049
+ }
959
1050
  if (isSubmarineRefundableStatus(swap.status)) {
960
1051
  logger.log(`Auto-refunding submarine swap ${swap.id}`);
961
1052
  await this.executeRefundAction(swap);
@@ -1175,13 +1266,13 @@ import {
1175
1266
  getSequence
1176
1267
  } from "@arkade-os/sdk";
1177
1268
  import { sha256 } from "@noble/hashes/sha2.js";
1178
- import { base64 as base642, hex } from "@scure/base";
1269
+ import { base64 as base642, hex as hex2 } from "@scure/base";
1179
1270
  import { SigHash } from "@scure/btc-signer";
1180
1271
  import { tapLeafHash } from "@scure/btc-signer/payment.js";
1181
1272
  function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session, sweepPublicKey, forfeitOutputScript, connectorIndex = 0) {
1182
1273
  const utf8IntentId = new TextEncoder().encode(intentId);
1183
1274
  const intentIdHash = sha256(utf8IntentId);
1184
- const intentIdHashStr = hex.encode(intentIdHash);
1275
+ const intentIdHashStr = hex2.encode(intentIdHash);
1185
1276
  let sweepTapTreeRoot;
1186
1277
  return {
1187
1278
  onBatchStarted: async (event) => {
@@ -1220,7 +1311,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
1220
1311
  );
1221
1312
  const signerPublicKey = await session.getPublicKey();
1222
1313
  const xonlySignerPublicKey = signerPublicKey.subarray(1);
1223
- if (!xOnlyPublicKeys.includes(hex.encode(xonlySignerPublicKey))) {
1314
+ if (!xOnlyPublicKeys.includes(hex2.encode(xonlySignerPublicKey))) {
1224
1315
  return { skip: true };
1225
1316
  }
1226
1317
  const commitmentTx = Transaction3.fromPSBT(
@@ -1232,7 +1323,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
1232
1323
  throw new Error("Shared output not found");
1233
1324
  }
1234
1325
  await session.init(vtxoTree, sweepTapTreeRoot, sharedOutput.amount);
1235
- const pubkey = hex.encode(await session.getPublicKey());
1326
+ const pubkey = hex2.encode(await session.getPublicKey());
1236
1327
  const nonces = await session.getNonces();
1237
1328
  await arkProvider.submitTreeNonces(event.id, pubkey, nonces);
1238
1329
  return { skip: false };
@@ -1247,7 +1338,7 @@ function createVHTLCBatchHandler(intentId, vhtlc, arkProvider, identity, session
1247
1338
  );
1248
1339
  if (!hasAllNonces) return { fullySigned: false };
1249
1340
  const signatures = await session.sign();
1250
- const pubkey = hex.encode(await session.getPublicKey());
1341
+ const pubkey = hex2.encode(await session.getPublicKey());
1251
1342
  await arkProvider.submitTreeSignatures(
1252
1343
  event.id,
1253
1344
  pubkey,
@@ -1500,7 +1591,7 @@ var ArkadeLightning = class {
1500
1591
  * @returns The created pending submarine swap.
1501
1592
  */
1502
1593
  async createSubmarineSwap(args) {
1503
- const refundPublicKey = hex2.encode(
1594
+ const refundPublicKey = hex3.encode(
1504
1595
  await this.wallet.identity.compressedPublicKey()
1505
1596
  );
1506
1597
  if (!refundPublicKey)
@@ -1536,7 +1627,7 @@ var ArkadeLightning = class {
1536
1627
  async createReverseSwap(args) {
1537
1628
  if (args.amount <= 0)
1538
1629
  throw new SwapError({ message: "Amount must be greater than 0" });
1539
- const claimPublicKey = hex2.encode(
1630
+ const claimPublicKey = hex3.encode(
1540
1631
  await this.wallet.identity.compressedPublicKey()
1541
1632
  );
1542
1633
  if (!claimPublicKey)
@@ -1544,7 +1635,7 @@ var ArkadeLightning = class {
1544
1635
  message: "Failed to get claim public key from wallet"
1545
1636
  });
1546
1637
  const preimage = randomBytes(32);
1547
- const preimageHash = hex2.encode(sha2562(preimage));
1638
+ const preimageHash = hex3.encode(sha2562(preimage));
1548
1639
  if (!preimageHash)
1549
1640
  throw new SwapError({ message: "Failed to get preimage hash" });
1550
1641
  const swapRequest = {
@@ -1558,7 +1649,7 @@ var ArkadeLightning = class {
1558
1649
  id: swapResponse.id,
1559
1650
  type: "reverse",
1560
1651
  createdAt: Math.floor(Date.now() / 1e3),
1561
- preimage: hex2.encode(preimage),
1652
+ preimage: hex3.encode(preimage),
1562
1653
  request: swapRequest,
1563
1654
  response: swapResponse,
1564
1655
  status: "swap.created"
@@ -1575,7 +1666,9 @@ var ArkadeLightning = class {
1575
1666
  * @param pendingSwap - The pending reverse swap to claim the VHTLC.
1576
1667
  */
1577
1668
  async claimVHTLC(pendingSwap) {
1578
- const preimage = hex2.decode(pendingSwap.preimage);
1669
+ if (!pendingSwap.preimage)
1670
+ throw new Error("Preimage is required to claim VHTLC");
1671
+ const preimage = hex3.decode(pendingSwap.preimage);
1579
1672
  const aspInfo = await this.arkProvider.getInfo();
1580
1673
  const address = await this.wallet.getAddress();
1581
1674
  const ourXOnlyPublicKey = this.normalizeToXOnlyPublicKey(
@@ -1584,21 +1677,21 @@ var ArkadeLightning = class {
1584
1677
  pendingSwap.id
1585
1678
  );
1586
1679
  const boltzXOnlyPublicKey = this.normalizeToXOnlyPublicKey(
1587
- hex2.decode(pendingSwap.response.refundPublicKey),
1680
+ hex3.decode(pendingSwap.response.refundPublicKey),
1588
1681
  "boltz",
1589
1682
  pendingSwap.id
1590
1683
  );
1591
1684
  const serverXOnlyPublicKey = this.normalizeToXOnlyPublicKey(
1592
- hex2.decode(aspInfo.signerPubkey),
1685
+ hex3.decode(aspInfo.signerPubkey),
1593
1686
  "server",
1594
1687
  pendingSwap.id
1595
1688
  );
1596
1689
  const { vhtlcScript, vhtlcAddress } = this.createVHTLCScript({
1597
1690
  network: aspInfo.network,
1598
1691
  preimageHash: sha2562(preimage),
1599
- receiverPubkey: hex2.encode(ourXOnlyPublicKey),
1600
- senderPubkey: hex2.encode(boltzXOnlyPublicKey),
1601
- serverPubkey: hex2.encode(serverXOnlyPublicKey),
1692
+ receiverPubkey: hex3.encode(ourXOnlyPublicKey),
1693
+ senderPubkey: hex3.encode(boltzXOnlyPublicKey),
1694
+ serverPubkey: hex3.encode(serverXOnlyPublicKey),
1602
1695
  timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights
1603
1696
  });
1604
1697
  if (!vhtlcScript)
@@ -1606,7 +1699,7 @@ var ArkadeLightning = class {
1606
1699
  if (vhtlcAddress !== pendingSwap.response.lockupAddress)
1607
1700
  throw new Error("Boltz is trying to scam us");
1608
1701
  const { vtxos } = await this.indexerProvider.getVtxos({
1609
- scripts: [hex2.encode(vhtlcScript.pkScript)]
1702
+ scripts: [hex3.encode(vhtlcScript.pkScript)]
1610
1703
  });
1611
1704
  if (vtxos.length === 0)
1612
1705
  throw new Error("No spendable virtual coins found");
@@ -1654,11 +1747,14 @@ var ArkadeLightning = class {
1654
1747
  * @param pendingSwap - The pending submarine swap to refund the VHTLC.
1655
1748
  */
1656
1749
  async refundVHTLC(pendingSwap) {
1750
+ const preimageHash = pendingSwap.request.invoice ? getInvoicePaymentHash(pendingSwap.request.invoice) : pendingSwap.preimageHash;
1751
+ if (!preimageHash)
1752
+ throw new Error("Preimage hash is required to refund VHTLC");
1657
1753
  const vhtlcPkScript = ArkAddress.decode(
1658
1754
  pendingSwap.response.address
1659
1755
  ).pkScript;
1660
1756
  const { vtxos } = await this.indexerProvider.getVtxos({
1661
- scripts: [hex2.encode(vhtlcPkScript)]
1757
+ scripts: [hex3.encode(vhtlcPkScript)]
1662
1758
  });
1663
1759
  if (vtxos.length === 0) {
1664
1760
  throw new Error(
@@ -1678,23 +1774,21 @@ var ArkadeLightning = class {
1678
1774
  pendingSwap.id
1679
1775
  );
1680
1776
  const serverXOnlyPublicKey = this.normalizeToXOnlyPublicKey(
1681
- hex2.decode(aspInfo.signerPubkey),
1777
+ hex3.decode(aspInfo.signerPubkey),
1682
1778
  "server",
1683
1779
  pendingSwap.id
1684
1780
  );
1685
1781
  const boltzXOnlyPublicKey = this.normalizeToXOnlyPublicKey(
1686
- hex2.decode(pendingSwap.response.claimPublicKey),
1782
+ hex3.decode(pendingSwap.response.claimPublicKey),
1687
1783
  "boltz",
1688
1784
  pendingSwap.id
1689
1785
  );
1690
1786
  const { vhtlcScript } = this.createVHTLCScript({
1691
1787
  network: aspInfo.network,
1692
- preimageHash: hex2.decode(
1693
- getInvoicePaymentHash(pendingSwap.request.invoice)
1694
- ),
1695
- receiverPubkey: hex2.encode(boltzXOnlyPublicKey),
1696
- senderPubkey: hex2.encode(ourXOnlyPublicKey),
1697
- serverPubkey: hex2.encode(serverXOnlyPublicKey),
1788
+ preimageHash: hex3.decode(preimageHash),
1789
+ receiverPubkey: hex3.encode(boltzXOnlyPublicKey),
1790
+ senderPubkey: hex3.encode(ourXOnlyPublicKey),
1791
+ serverPubkey: hex3.encode(serverXOnlyPublicKey),
1698
1792
  timeoutBlockHeights: pendingSwap.response.timeoutBlockHeights
1699
1793
  });
1700
1794
  if (!vhtlcScript)
@@ -1750,14 +1844,14 @@ var ArkadeLightning = class {
1750
1844
  onchain_output_indexes: [],
1751
1845
  valid_at: 0,
1752
1846
  expire_at: 0,
1753
- cosigners_public_keys: [hex2.encode(signerPublicKey)]
1847
+ cosigners_public_keys: [hex3.encode(signerPublicKey)]
1754
1848
  };
1755
1849
  const deleteMessage = {
1756
1850
  type: "delete",
1757
1851
  expire_at: 0
1758
1852
  };
1759
1853
  const intentInput = {
1760
- txid: hex2.decode(input.txid),
1854
+ txid: hex3.decode(input.txid),
1761
1855
  index: input.vout,
1762
1856
  witnessUtxo: {
1763
1857
  amount: BigInt(input.value),
@@ -1792,11 +1886,11 @@ var ArkadeLightning = class {
1792
1886
  this.arkProvider,
1793
1887
  identity,
1794
1888
  signerSession,
1795
- hex2.decode(forfeitPubkey).slice(1),
1889
+ hex3.decode(forfeitPubkey).slice(1),
1796
1890
  isRecoverable2 ? void 0 : OutScript.encode(decodedAddress)
1797
1891
  );
1798
1892
  const topics = [
1799
- hex2.encode(signerPublicKey),
1893
+ hex3.encode(signerPublicKey),
1800
1894
  `${input.txid}:${input.vout}`
1801
1895
  ];
1802
1896
  const eventStream = this.arkProvider.getEventStream(
@@ -1969,8 +2063,179 @@ var ArkadeLightning = class {
1969
2063
  });
1970
2064
  });
1971
2065
  }
2066
+ /**
2067
+ * Restore swaps from Boltz API.
2068
+ *
2069
+ * Note: restored swaps may lack local-only data such as the original
2070
+ * Lightning invoice or preimage. They are intended primarily for
2071
+ * display/monitoring and are not automatically wired into the SwapManager.
2072
+ * Do not call `claimVHTLC` / `refundVHTLC` on them unless you have
2073
+ * enriched the objects with the missing fields.
2074
+ *
2075
+ * @param boltzFees - Optional fees response to use for restoration.
2076
+ * @returns An object containing arrays of restored reverse and submarine swaps.
2077
+ */
2078
+ async restoreSwaps(boltzFees) {
2079
+ const publicKey = hex3.encode(
2080
+ await this.wallet.identity.compressedPublicKey()
2081
+ );
2082
+ if (!publicKey) throw new Error("Failed to get public key from wallet");
2083
+ const fees = boltzFees ?? await this.swapProvider.getFees();
2084
+ const reverseSwaps = [];
2085
+ const submarineSwaps = [];
2086
+ const restoredSwaps = await this.swapProvider.restoreSwaps(publicKey);
2087
+ for (const swap of restoredSwaps) {
2088
+ const { id, createdAt, status } = swap;
2089
+ if (isRestoredReverseSwap(swap)) {
2090
+ const {
2091
+ amount,
2092
+ lockupAddress,
2093
+ preimageHash,
2094
+ serverPublicKey,
2095
+ tree
2096
+ } = swap.claimDetails;
2097
+ reverseSwaps.push({
2098
+ id,
2099
+ createdAt,
2100
+ request: {
2101
+ invoiceAmount: extractInvoiceAmount(amount, fees),
2102
+ claimPublicKey: publicKey,
2103
+ preimageHash
2104
+ },
2105
+ response: {
2106
+ id,
2107
+ invoice: "",
2108
+ // TODO check if we can get the invoice from boltz
2109
+ onchainAmount: amount,
2110
+ lockupAddress,
2111
+ refundPublicKey: serverPublicKey,
2112
+ timeoutBlockHeights: {
2113
+ refund: extractTimeLockFromLeafOutput(
2114
+ tree.refundWithoutBoltzLeaf.output
2115
+ ),
2116
+ unilateralClaim: extractTimeLockFromLeafOutput(
2117
+ tree.unilateralClaimLeaf.output
2118
+ ),
2119
+ unilateralRefund: extractTimeLockFromLeafOutput(
2120
+ tree.unilateralRefundLeaf.output
2121
+ ),
2122
+ unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
2123
+ tree.unilateralRefundWithoutBoltzLeaf.output
2124
+ )
2125
+ }
2126
+ },
2127
+ status,
2128
+ type: "reverse",
2129
+ preimage: ""
2130
+ });
2131
+ } else if (isRestoredSubmarineSwap(swap)) {
2132
+ const { amount, lockupAddress, serverPublicKey, tree } = swap.refundDetails;
2133
+ let preimage = "";
2134
+ try {
2135
+ const data = await this.swapProvider.getSwapPreimage(
2136
+ swap.id
2137
+ );
2138
+ preimage = data.preimage;
2139
+ } catch (error) {
2140
+ logger.warn(
2141
+ `Failed to restore preimage for submarine swap ${id}`,
2142
+ error
2143
+ );
2144
+ }
2145
+ submarineSwaps.push({
2146
+ id,
2147
+ type: "submarine",
2148
+ createdAt,
2149
+ preimage,
2150
+ preimageHash: swap.preimageHash,
2151
+ status,
2152
+ request: {
2153
+ invoice: "",
2154
+ // TODO check if we can get the invoice from boltz
2155
+ refundPublicKey: publicKey
2156
+ },
2157
+ response: {
2158
+ id,
2159
+ address: lockupAddress,
2160
+ expectedAmount: amount,
2161
+ claimPublicKey: serverPublicKey,
2162
+ timeoutBlockHeights: {
2163
+ refund: extractTimeLockFromLeafOutput(
2164
+ tree.refundWithoutBoltzLeaf.output
2165
+ ),
2166
+ unilateralClaim: extractTimeLockFromLeafOutput(
2167
+ tree.unilateralClaimLeaf.output
2168
+ ),
2169
+ unilateralRefund: extractTimeLockFromLeafOutput(
2170
+ tree.unilateralRefundLeaf.output
2171
+ ),
2172
+ unilateralRefundWithoutReceiver: extractTimeLockFromLeafOutput(
2173
+ tree.unilateralRefundWithoutBoltzLeaf.output
2174
+ )
2175
+ }
2176
+ }
2177
+ });
2178
+ }
2179
+ }
2180
+ return { reverseSwaps, submarineSwaps };
2181
+ }
2182
+ // Swap enrichment and validation helpers
2183
+ /**
2184
+ * Enrich a restored reverse swap with its preimage.
2185
+ * This makes the swap claimable via `claimVHTLC`.
2186
+ * Validates that the preimage hash matches the swap's expected preimageHash.
2187
+ *
2188
+ * @param swap - The restored reverse swap to enrich.
2189
+ * @param preimage - The preimage (hex-encoded) for the swap.
2190
+ * @returns The enriched swap object (same reference, mutated).
2191
+ * @throws Error if the preimage does not match the swap's preimageHash.
2192
+ */
2193
+ enrichReverseSwapPreimage(swap, preimage) {
2194
+ const computedHash = hex3.encode(sha2562(hex3.decode(preimage)));
2195
+ if (computedHash !== swap.request.preimageHash) {
2196
+ throw new Error(
2197
+ `Preimage does not match swap: expected hash ${swap.request.preimageHash}, got ${computedHash}`
2198
+ );
2199
+ }
2200
+ swap.preimage = preimage;
2201
+ return swap;
2202
+ }
2203
+ /**
2204
+ * Enrich a restored submarine swap with its invoice.
2205
+ * This makes the swap refundable via `refundVHTLC`.
2206
+ * Validates that the invoice is well-formed and its payment hash can be extracted.
2207
+ * If the swap has a preimageHash (from restoration), validates that the invoice's
2208
+ * payment hash matches.
2209
+ *
2210
+ * @param swap - The restored submarine swap to enrich.
2211
+ * @param invoice - The Lightning invoice for the swap.
2212
+ * @returns The enriched swap object (same reference, mutated).
2213
+ * @throws Error if the invoice is invalid, cannot be decoded, or payment hash doesn't match.
2214
+ */
2215
+ enrichSubmarineSwapInvoice(swap, invoice) {
2216
+ let paymentHash;
2217
+ try {
2218
+ const decoded = decodeInvoice(invoice);
2219
+ if (!decoded.paymentHash) {
2220
+ throw new Error("Invoice missing payment hash");
2221
+ }
2222
+ paymentHash = decoded.paymentHash;
2223
+ } catch (error) {
2224
+ if (error instanceof Error) {
2225
+ throw new Error(`Invalid Lightning invoice: ${error.message}`);
2226
+ }
2227
+ throw new Error(`Invalid Lightning invoice format`);
2228
+ }
2229
+ if (swap.preimageHash && paymentHash !== swap.preimageHash) {
2230
+ throw new Error(
2231
+ `Invoice payment hash does not match swap: expected ${swap.preimageHash}, got ${paymentHash}`
2232
+ );
2233
+ }
2234
+ swap.request.invoice = invoice;
2235
+ return swap;
2236
+ }
1972
2237
  async claimVHTLCwithOffchainTx(vhtlcIdentity, vhtlcScript, serverXOnlyPublicKey, input, output, arkInfos) {
1973
- const rawCheckpointTapscript = hex2.decode(arkInfos.checkpointTapscript);
2238
+ const rawCheckpointTapscript = hex3.decode(arkInfos.checkpointTapscript);
1974
2239
  const serverUnrollScript = CSVMultisigTapscript2.decode(
1975
2240
  rawCheckpointTapscript
1976
2241
  );
@@ -2003,7 +2268,7 @@ var ArkadeLightning = class {
2003
2268
  await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
2004
2269
  }
2005
2270
  async refundVHTLCwithOffchainTx(pendingSwap, boltzXOnlyPublicKey, ourXOnlyPublicKey, serverXOnlyPublicKey, input, output, arkInfos) {
2006
- const rawCheckpointTapscript = hex2.decode(arkInfos.checkpointTapscript);
2271
+ const rawCheckpointTapscript = hex3.decode(arkInfos.checkpointTapscript);
2007
2272
  const serverUnrollScript = CSVMultisigTapscript2.decode(
2008
2273
  rawCheckpointTapscript
2009
2274
  );
@@ -2021,7 +2286,7 @@ var ArkadeLightning = class {
2021
2286
  unsignedRefundTx,
2022
2287
  unsignedCheckpointTx
2023
2288
  );
2024
- const boltzXOnlyPublicKeyHex = hex2.encode(boltzXOnlyPublicKey);
2289
+ const boltzXOnlyPublicKeyHex = hex3.encode(boltzXOnlyPublicKey);
2025
2290
  if (!verifySignatures(boltzSignedRefundTx, 0, [boltzXOnlyPublicKeyHex])) {
2026
2291
  throw new Error("Invalid Boltz signature in refund transaction");
2027
2292
  }
@@ -2049,9 +2314,9 @@ var ArkadeLightning = class {
2049
2314
  const tx = Transaction4.fromPSBT(base643.decode(finalArkTx));
2050
2315
  const inputIndex = 0;
2051
2316
  const requiredSigners = [
2052
- hex2.encode(ourXOnlyPublicKey),
2053
- hex2.encode(boltzXOnlyPublicKey),
2054
- hex2.encode(serverXOnlyPublicKey)
2317
+ hex3.encode(ourXOnlyPublicKey),
2318
+ hex3.encode(boltzXOnlyPublicKey),
2319
+ hex3.encode(serverXOnlyPublicKey)
2055
2320
  ];
2056
2321
  if (!verifySignatures(tx, inputIndex, requiredSigners)) {
2057
2322
  throw new Error("Invalid refund transaction");
@@ -2112,15 +2377,15 @@ var ArkadeLightning = class {
2112
2377
  timeoutBlockHeights
2113
2378
  }) {
2114
2379
  const receiverXOnlyPublicKey = this.normalizeToXOnlyPublicKey(
2115
- hex2.decode(receiverPubkey),
2380
+ hex3.decode(receiverPubkey),
2116
2381
  "receiver"
2117
2382
  );
2118
2383
  const senderXOnlyPublicKey = this.normalizeToXOnlyPublicKey(
2119
- hex2.decode(senderPubkey),
2384
+ hex3.decode(senderPubkey),
2120
2385
  "sender"
2121
2386
  );
2122
2387
  const serverXOnlyPublicKey = this.normalizeToXOnlyPublicKey(
2123
- hex2.decode(serverPubkey),
2388
+ hex3.decode(serverPubkey),
2124
2389
  "server"
2125
2390
  );
2126
2391
  const delayType = (num) => num < 512 ? "blocks" : "seconds";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/boltz-swap",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
5
  "description": "A production-ready TypeScript package that brings Boltz submarine-swaps to Arkade.",
6
6
  "main": "./dist/index.js",
@@ -34,6 +34,7 @@
34
34
  "@noble/hashes": "2.0.1",
35
35
  "@scure/base": "2.0.0",
36
36
  "@scure/btc-signer": "2.0.1",
37
+ "bip68": "^1.0.4",
37
38
  "light-bolt11-decoder": "3.2.0"
38
39
  },
39
40
  "devDependencies": {