@atomiqlabs/lp-lib 17.5.3 → 17.6.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.
Files changed (28) hide show
  1. package/dist/swaps/assertions/FromBtcAmountAssertions.js +2 -2
  2. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +16 -6
  3. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +2 -2
  4. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +34 -24
  5. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +2 -2
  6. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +30 -19
  7. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +1 -1
  8. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +12 -5
  9. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +2 -2
  10. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +1 -1
  11. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +38 -4
  12. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +35 -15
  13. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +2 -2
  14. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +7 -2
  15. package/dist/utils/Utils.d.ts +33 -0
  16. package/dist/utils/Utils.js +51 -1
  17. package/package.json +1 -1
  18. package/src/swaps/assertions/FromBtcAmountAssertions.ts +2 -2
  19. package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +14 -5
  20. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +46 -29
  21. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +42 -24
  22. package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +13 -7
  23. package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +4 -4
  24. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +37 -7
  25. package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +36 -18
  26. package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +25 -2
  27. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +5 -1
  28. package/src/utils/Utils.ts +48 -0
@@ -7,7 +7,8 @@ import {
7
7
  ChainSwapType,
8
8
  ClaimEvent,
9
9
  InitializeEvent,
10
- RefundEvent, SwapCommitStateType,
10
+ RefundEvent,
11
+ SwapCommitStateType,
11
12
  SwapData
12
13
  } from "@atomiqlabs/base";
13
14
  import {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../../utils/Utils";
@@ -27,9 +28,8 @@ import {
27
28
  ProbeAndRouteResponse,
28
29
  routesMatch
29
30
  } from "../../../wallets/ILightningWallet";
30
- import { LightningAssertions } from "../../assertions/LightningAssertions";
31
+ import {LightningAssertions} from "../../assertions/LightningAssertions";
31
32
  import {isQuoteThrow} from "../../../plugins/IPlugin";
32
- import {ToBtcSwapState} from "../tobtc_abstract/ToBtcSwapAbs";
33
33
 
34
34
  export type ToBtcLnConfig = ToBtcBaseConfig & {
35
35
  routingFeeMultiplier: bigint | number,
@@ -197,7 +197,8 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
197
197
  ]);
198
198
 
199
199
  for(let {obj: swap} of queriedData) {
200
- await this.processPastSwap(swap);
200
+ await this.processPastSwap(swap)
201
+ .catch(e => this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e));
201
202
  }
202
203
  }
203
204
 
@@ -300,6 +301,10 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
300
301
  this.swapLogger.info(invoiceData, "subscribeToPayment(): result callback, outcome: "+result.status+" invoice: "+invoiceData.pr);
301
302
  this.processPaymentResult(invoiceData, result).catch(e => this.swapLogger.error(invoiceData, "subscribeToPayment(): process payment result", e));
302
303
  this.activeSubscriptions.delete(paymentHash);
304
+ }).catch(e => {
305
+ this.swapLogger.error(invoiceData, "subscribeToPayment(): error while waiting for invoice payment: ", e);
306
+ //Delete from active subscription, the watchdog will re-subscribe later
307
+ this.activeSubscriptions.delete(paymentHash);
303
308
  });
304
309
  this.swapLogger.info(invoiceData, "subscribeToPayment(): subscribe to payment outcome, invoice: "+invoiceData.pr);
305
310
 
@@ -418,6 +423,9 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
418
423
  } else throw e;
419
424
  }
420
425
 
426
+ const unlock = swap.lock(120);
427
+ if(unlock==null) return;
428
+
421
429
  await swap.setState(ToBtcLnSwapState.COMMITED);
422
430
  await this.saveSwapData(swap);
423
431
  try {
@@ -426,10 +434,22 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
426
434
  this.swapLogger.error(swap, "processInitialized(): lightning payment error", e);
427
435
  if(isDefinedRuntimeError(e)) {
428
436
  if(swap.metadata!=null) swap.metadata.payError = e;
429
- await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
430
- await this.saveSwapData(swap);
431
- return;
437
+ const payment = await this.lightning.getPayment(swap.lnPaymentHash);
438
+ if(!unlock()) return;
439
+ if(payment==null || payment.status==="failed") {
440
+ if(swap.state as ToBtcLnSwapState === ToBtcLnSwapState.COMMITED)
441
+ await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
442
+ await this.saveSwapData(swap);
443
+ return;
444
+ } else if(payment.status==="confirmed") {
445
+ await this.processPaymentResult(swap, payment);
446
+ return;
447
+ } else {
448
+ //Fall-through to subscribe
449
+ }
432
450
  } else throw e;
451
+ } finally {
452
+ unlock();
433
453
  }
434
454
  this.subscribeToPayment(swap);
435
455
  return;
@@ -1078,6 +1098,16 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
1078
1098
  };
1079
1099
 
1080
1100
  if(data.state===ToBtcLnSwapState.NON_PAYABLE) {
1101
+ if(data.payInitiated) {
1102
+ //Check that the ln payment is indeed non-existent or failed
1103
+ const lnPayment = await this.lightning.getPayment(data.lnPaymentHash);
1104
+ if(lnPayment!=null && lnPayment.status!=="failed") throw {
1105
+ _httpStatus: 200,
1106
+ code: 20008,
1107
+ msg: "Payment in-flight"
1108
+ };
1109
+ }
1110
+
1081
1111
  const refundSigData = await swapContract.getRefundSignature(signer, data.data, this.config.refundAuthorizationTimeout);
1082
1112
 
1083
1113
  //Double check the state after promise result
@@ -311,7 +311,8 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
311
311
  ]);
312
312
 
313
313
  for(let {obj: swap} of swaps) {
314
- await this.processPastSwap(swap);
314
+ await this.processPastSwap(swap)
315
+ .catch(e => this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e));
315
316
  }
316
317
  }
317
318
 
@@ -794,13 +795,12 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
794
795
  swap
795
796
  );
796
797
  if(isQuoteThrow(pluginCheckResult)) {
797
- const error = {
798
+ if(swap.state===SpvVaultSwapState.CREATED)
799
+ await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
800
+ throw {
798
801
  code: 29999,
799
802
  msg: pluginCheckResult.message
800
- }
801
- if(swap.metadata!=null) swap.metadata.postQuoteError = error;
802
- await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
803
- throw error;
803
+ };
804
804
  }
805
805
 
806
806
  await this.Vaults.checkVaultReplacedTransactions(vault, true);
@@ -811,16 +811,22 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
811
811
  };
812
812
  }
813
813
 
814
+ const unlock = swap.lock(120);
815
+ if(!unlock) throw {
816
+ code: 20517,
817
+ msg: "Bitcoin transaction submission already in progress, please retry later!"
818
+ };
819
+
820
+ let swapSendingSet = false;
821
+ let dataSendingSet = false;
814
822
  try {
815
823
  const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
816
824
 
817
825
  //Double-check the state to prevent race condition
818
- if(swap.state!==SpvVaultSwapState.CREATED) {
819
- throw {
820
- code: 20505,
821
- msg: "Invalid quote ID, not found or expired!"
822
- };
823
- }
826
+ if(swap.state!==SpvVaultSwapState.CREATED) throw {
827
+ code: 20505,
828
+ msg: "Invalid quote ID, not found or expired!"
829
+ };
824
830
 
825
831
  //Double check in-flight swap count
826
832
  this.checkTooManyInflightSwaps();
@@ -828,11 +834,13 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
828
834
  swap.btcTxId = signedTx.id;
829
835
  swap.state = SpvVaultSwapState.SIGNED;
830
836
  swap.sending = true;
837
+ swapSendingSet = true;
831
838
  await this.saveSwapData(swap);
832
839
 
833
840
  data.btcTx.raw = btcRawTx;
834
841
  (data as any).sending = true;
835
842
  vault.addWithdrawal(data);
843
+ dataSendingSet = true;
836
844
  await this.Vaults.saveVault(vault);
837
845
 
838
846
  this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
@@ -850,15 +858,25 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
850
858
  };
851
859
  }
852
860
  } catch (e) {
853
- (data as any).sending = false;
854
- swap.sending = false;
855
- vault.removeWithdrawal(data);
856
- await this.Vaults.saveVault(vault);
861
+ if(swapSendingSet) swap.sending = false;
862
+ if(dataSendingSet) {
863
+ (data as any).sending = false;
864
+ vault.removeWithdrawal(data);
865
+ await this.Vaults.saveVault(vault);
866
+ }
857
867
 
858
- if(isDefinedRuntimeError(e) && swap.metadata!=null) swap.metadata.postQuoteError = e;
859
- await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
868
+ //Check if the error is only because the state has already changed
869
+ if(!isDefinedRuntimeError(e) || e.code!==20505) {
870
+ //We only make the swap failed if the error happened in CREATED or SIGNED states
871
+ if(swap.state===SpvVaultSwapState.CREATED || swap.state===SpvVaultSwapState.SIGNED) {
872
+ if(isDefinedRuntimeError(e) && swap.metadata!=null) swap.metadata.postQuoteError = e;
873
+ await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
874
+ }
875
+ }
860
876
 
861
877
  throw e;
878
+ } finally {
879
+ unlock();
862
880
  }
863
881
 
864
882
  await responseStream.writeParamsAndEnd({
@@ -13,6 +13,29 @@ import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
13
13
  import {FromBtcAmountAssertions} from "../../assertions/FromBtcAmountAssertions";
14
14
  import {isQuoteThrow} from "../../../plugins/IPlugin";
15
15
 
16
+ /*
17
+ TODO: IN CASE THIS EVER GETS USED, FIX THE FOLLOWING:
18
+
19
+ HIGH 5 — FromBtcTrusted (0-conf): no RBF check, fee gate is min not max, and the burn defense only fires when it shouldn't
20
+
21
+ Handler is opt-in (ONCHAIN_TRUSTED, not in shipped configs), but if enabled:
22
+
23
+ • Acceptance (FromBtcTrusted.js:186-194) checks only: direct parents confirmed + fee threshold. Nothing inspects tx.ins[].sequence for BIP-125 (grep: only storage-sequence hits). A double-spendable opt-in-RBF tx is
24
+ accepted and tokens are sent within seconds.
25
+ • The fee gate is fee >= recommendedFee || fee >= currentRate (line 187) — the weaker of the two wins; the documented intent is max(...). The runner even overrides the library's 1.25 multiplier to 1
26
+ (IntermediaryRunner.ts FromBtcTrusted section). A stale low fee maximizes the replacement window and minimizes the attacker's BIP-125 cost.
27
+ • The burn defense (checkDoubleSpends → burn() → sendRawPackage([tx1, burnChild])) is rejected by any node whose mempool already holds the replacement — so it structurally loses against a broadcast double-spend, and
28
+ with mempoolfullrbf (default since Core 28) even non-signaling txs are replaceable.
29
+ • Worse, checkDoubleSpends (lines 636-644) treats getTransaction == null as "double-spent" — but null also means mempool eviction (fee spike, node restart). With no conflict anywhere, the burn package is valid and
30
+ burns the entire deposit to miners — after tokens were already sent, that's a full LP loss with no attacker at all.
31
+
32
+ MEDIUM 6 — FromBtcTrusted /setRefundAddress is dead code; refundable user funds strand permanently
33
+
34
+ FromBtcTrusted.js:606 looks up getData(paymentHash, null); IntermediaryStorageManager.getData (line 64-66) converts null → sequence 0x0, but every swap is stored with a random 64-bit sequence — so the lookup always
35
+ misses and the endpoint always throws 10001. (The status endpoint at line 515 does it correctly.) Any swap that becomes REFUNDABLE (over-max payment, plugin veto, chain revert, balance failure) without a refund
36
+ address set at creation can never be refunded (refundSwap early-returns without one). User funds sit in the LP wallet forever. One-line fix: pass parsedBody.sequence.
37
+ */
38
+
16
39
  export type FromBtcTrustedConfig = SwapBaseConfig & {
17
40
  doubleSpendCheckInterval: number,
18
41
  swapAddressExpiry: number,
@@ -298,7 +321,7 @@ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrust
298
321
 
299
322
  const txns = await chainInterface.txsTransfer(signer.getAddress(), swap.token, swap.adjustedOutput, swap.dstAddress);
300
323
 
301
- let unlock = swap.lock(30*1000);
324
+ let unlock = swap.lock(30);
302
325
  if(unlock==null) return;
303
326
 
304
327
  const pluginCheckResult = await PluginManager.onHandlePreFromBtcExecute(
@@ -399,7 +422,7 @@ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrust
399
422
  try {
400
423
  await this.processPastSwap(swap, txs[0]?.tx, txs[0]?.vout);
401
424
  } catch (e) {
402
- this.swapLogger.error(swap, "processPastSwaps(): Error ocurred while processing swap: ", e);
425
+ this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e);
403
426
  }
404
427
  }
405
428
  }
@@ -171,7 +171,11 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
171
171
  ]);
172
172
 
173
173
  for(let {obj: swap} of queriedData) {
174
- if(await this.processPastSwap(swap)) cancelInvoices.push(swap);
174
+ try {
175
+ if(await this.processPastSwap(swap)) cancelInvoices.push(swap);
176
+ } catch (e) {
177
+ this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e);
178
+ }
175
179
  }
176
180
 
177
181
  await this.cancelInvoices(cancelInvoices);
@@ -150,3 +150,51 @@ export function parsePsbt(btcTx: Transaction): BtcTx {
150
150
  })
151
151
  };
152
152
  }
153
+
154
+ /**
155
+ * Returns the minimum Bitcoin block window (in blocks) for which a "fast blocks" safety
156
+ * factor is genuinely usable: the probability that N consecutive blocks are produced
157
+ * with an average interval below (blocktime / safetyFactor) is less than `probability`.
158
+ *
159
+ * Model: block arrivals are a Poisson process (i.i.d. exponential intervals), so the
160
+ * production time of N blocks is Erlang/Gamma(shape=N). A Chernoff (Cramer) bound on
161
+ * the lower tail gives the per-block large-deviation rate I(S) = ln(S) + 1/S - 1, i.e.
162
+ * P(N blocks faster than blocktime/S on average) <= exp(-N * I(S))
163
+ * which this function inverts for N. Conservative: the exact Erlang CDF already
164
+ * satisfies the bound ~25% earlier. Use for the fast-block tail (e.g. incoming LN
165
+ * HTLC expiry racing an escrow claim window).
166
+ *
167
+ * @param {number|bigint} safetyFactor Assumed max block-speed multiplier (must be > 1)
168
+ * @param {number} [probability=0.0001] Tolerated failure probability (default 0.01%)
169
+ * @returns {number} Minimum safe block window in blocks
170
+ */
171
+ export function getMinSafeBlockWindowFast(safetyFactor: number | bigint, probability: number = 0.0001): bigint {
172
+ const S = Number(safetyFactor);
173
+ const rate = Math.log(S) + 1/S - 1;
174
+ if (!(rate > 0)) throw new RangeError("safetyFactor must be > 1");
175
+ return BigInt(Math.ceil(Math.log(1/probability) / rate));
176
+ }
177
+
178
+ /**
179
+ * Slow-tail mirror of getMinSafeBlockWindowFast(): returns the minimum block window
180
+ * (in blocks) for which a "slow blocks" safety factor is genuinely usable: the
181
+ * probability that N consecutive blocks take LONGER than N * blocktime * safetyFactor
182
+ * is less than `probability`. Chernoff bound on the upper Erlang tail gives the rate
183
+ * I(S) = S - 1 - ln(S). Use when converting a wall-clock deadline into a block count
184
+ * (e.g. payout/claim timeouts that must expire before some real-world time even if
185
+ * blocks come slowly).
186
+ *
187
+ * @param {number|bigint} safetyFactor Assumed max block-slowness multiplier (must be > 1)
188
+ * @param {number} [probability=0.0001] Tolerated failure probability (default 0.01%)
189
+ * @returns {number} Minimum safe block window in blocks
190
+ */
191
+ export function getMinSafeBlockWindowSlow(safetyFactor: bigint | number, probability: number = 0.0001): bigint {
192
+ const S = Number(safetyFactor);
193
+ const rate = S - 1 - Math.log(S);
194
+ if (!(rate > 0)) throw new RangeError("safetyFactor must be > 1");
195
+ return BigInt(Math.ceil(Math.log(1/probability) / rate));
196
+ }
197
+
198
+ export function bigIntMax(a: bigint, b: bigint): bigint {
199
+ return a > b ? a : b;
200
+ }