@atomiqlabs/lp-lib 16.0.0 → 16.0.1

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.
@@ -106,13 +106,6 @@ export declare abstract class SwapHandler<V extends SwapHandlerSwap<S> = SwapHan
106
106
  * Returns data to be returned in swap handler info
107
107
  */
108
108
  abstract getInfoData(): any;
109
- /**
110
- * Remove swap data
111
- *
112
- * @param hash
113
- * @param sequence
114
- */
115
- protected removeSwapData(hash: string, sequence: bigint): Promise<void>;
116
109
  /**
117
110
  * Remove swap data
118
111
  *
@@ -67,18 +67,15 @@ class SwapHandler {
67
67
  }
68
68
  }
69
69
  }
70
- async removeSwapData(hashOrSwap, sequenceOrUltimateState) {
71
- let swap;
72
- if (typeof (hashOrSwap) === "string") {
73
- if (typeof (sequenceOrUltimateState) !== "bigint")
74
- throw new Error("Sequence must be a BN instance!");
75
- swap = await this.storageManager.getData(hashOrSwap, sequenceOrUltimateState);
76
- }
77
- else {
78
- swap = hashOrSwap;
79
- if (sequenceOrUltimateState != null && typeof (sequenceOrUltimateState) !== "bigint")
80
- await swap.setState(sequenceOrUltimateState);
81
- }
70
+ /**
71
+ * Remove swap data
72
+ *
73
+ * @param swap
74
+ * @param ultimateState set the ultimate state of the swap before removing
75
+ */
76
+ async removeSwapData(swap, ultimateState) {
77
+ if (ultimateState != null)
78
+ await swap.setState(ultimateState);
82
79
  if (swap != null)
83
80
  await PluginManager_1.PluginManager.swapRemove(swap);
84
81
  this.swapLogger.debug(swap, "removeSwapData(): removing swap final state: " + swap.state);
@@ -31,7 +31,6 @@ export declare abstract class EscrowHandler<V extends EscrowHandlerSwap<SwapData
31
31
  */
32
32
  protected subscribeToEvents(): void;
33
33
  protected loadData(ctor: new (data: any) => V): Promise<void>;
34
- protected removeSwapData(hash: string, sequence: bigint): Promise<void>;
35
34
  protected removeSwapData(swap: V, ultimateState?: S): Promise<void>;
36
35
  protected saveSwapData(swap: V): Promise<void>;
37
36
  protected saveSwapToEscrowHashMap(swap: V): void;
@@ -73,18 +73,9 @@ class EscrowHandler extends SwapHandler_1.SwapHandler {
73
73
  this.saveSwapToEscrowHashMap(swap);
74
74
  }
75
75
  }
76
- async removeSwapData(hashOrSwap, sequenceOrUltimateState) {
77
- let swap;
78
- if (typeof (hashOrSwap) === "string") {
79
- if (typeof (sequenceOrUltimateState) !== "bigint")
80
- throw new Error("Sequence must be a BN instance!");
81
- swap = await this.storageManager.getData(hashOrSwap, sequenceOrUltimateState);
82
- }
83
- else {
84
- swap = hashOrSwap;
85
- if (sequenceOrUltimateState != null && typeof (sequenceOrUltimateState) !== "bigint")
86
- await swap.setState(sequenceOrUltimateState);
87
- }
76
+ async removeSwapData(swap, ultimateState) {
77
+ if (ultimateState != null)
78
+ await swap.setState(ultimateState);
88
79
  if (swap != null)
89
80
  await PluginManager_1.PluginManager.swapRemove(swap);
90
81
  this.swapLogger.debug(swap, "removeSwapData(): removing swap final state: " + swap.state);
@@ -8,6 +8,10 @@ import { FromBtcBaseConfig, FromBtcBaseSwapHandler } from "../FromBtcBaseSwapHan
8
8
  import { ILightningWallet } from "../../../wallets/ILightningWallet";
9
9
  import { LightningAssertions } from "../../assertions/LightningAssertions";
10
10
  export type FromBtcLnAutoConfig = FromBtcBaseConfig & {
11
+ maxInflightHtlcs?: number;
12
+ minNativeBalances?: {
13
+ [chainId: string]: bigint;
14
+ };
11
15
  invoiceTimeoutSeconds?: number;
12
16
  minCltv: bigint;
13
17
  gracePeriod: bigint;
@@ -33,6 +37,7 @@ export declare class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoS
33
37
  readonly type = SwapHandlerType.FROM_BTCLN_AUTO;
34
38
  readonly swapType = ChainSwapType.HTLC;
35
39
  activeSubscriptions: Set<string>;
40
+ swapsWithInflighHtlcs: Set<FromBtcLnAutoSwap>;
36
41
  readonly config: FromBtcLnAutoConfig;
37
42
  readonly lightning: ILightningWallet;
38
43
  readonly LightningAssertions: LightningAssertions;
@@ -105,7 +110,9 @@ export declare class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoS
105
110
  * @returns the fetched lightning invoice
106
111
  */
107
112
  private checkInvoiceStatus;
113
+ private checkTooManyInflightHtlcs;
108
114
  startRestServer(restServer: Express): void;
109
115
  init(): Promise<void>;
110
116
  getInfoData(): any;
117
+ protected removeSwapData(swap: FromBtcLnAutoSwap, ultimateState?: FromBtcLnAutoSwapState): Promise<void>;
111
118
  }
@@ -20,6 +20,7 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
20
20
  this.type = SwapHandler_1.SwapHandlerType.FROM_BTCLN_AUTO;
21
21
  this.swapType = base_1.ChainSwapType.HTLC;
22
22
  this.activeSubscriptions = new Set();
23
+ this.swapsWithInflighHtlcs = new Set();
23
24
  this.config = config;
24
25
  this.config.invoiceTimeoutSeconds = this.config.invoiceTimeoutSeconds || 90;
25
26
  this.lightning = lightning;
@@ -55,13 +56,7 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
55
56
  }
56
57
  if (swap.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED) {
57
58
  try {
58
- if (!await this.offerHtlc(swap)) {
59
- //Expired
60
- if (swap.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED) {
61
- this.swapLogger.info(swap, "processPastSwap(state=RECEIVED): offer HTLC expired, cancelling invoice: " + swap.pr);
62
- await this.cancelSwapAndInvoice(swap);
63
- }
64
- }
59
+ await this.offerHtlc(swap);
65
60
  }
66
61
  catch (e) {
67
62
  this.swapLogger.error(swap, "processPastSwap(state=RECEIVED): offerHtlc error", e);
@@ -69,6 +64,7 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
69
64
  return null;
70
65
  }
71
66
  if (swap.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.TXS_SENT || swap.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.COMMITED) {
67
+ this.swapsWithInflighHtlcs.add(swap);
72
68
  const onchainStatus = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
73
69
  const state = swap.state;
74
70
  if (onchainStatus.type === base_1.SwapCommitStateType.PAID) {
@@ -255,7 +251,6 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
255
251
  if (invoiceData.metadata != null)
256
252
  invoiceData.metadata.times.htlcReceived = Date.now();
257
253
  const useToken = invoiceData.token;
258
- const gasToken = invoiceData.gasToken;
259
254
  let expiryTimeout;
260
255
  try {
261
256
  //Check if HTLC expiry is long enough
@@ -264,6 +259,8 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
264
259
  invoiceData.metadata.times.htlcTimeoutCalculated = Date.now();
265
260
  }
266
261
  catch (e) {
262
+ if ((0, Utils_1.isDefinedRuntimeError)(e) && invoiceData.metadata != null)
263
+ invoiceData.metadata.htlcReceiveError = e;
267
264
  if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.CREATED)
268
265
  await this.cancelSwapAndInvoice(invoiceData);
269
266
  throw e;
@@ -287,6 +284,16 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
287
284
  async offerHtlc(invoiceData) {
288
285
  if (invoiceData.state !== FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED)
289
286
  return;
287
+ try {
288
+ this.checkTooManyInflightHtlcs();
289
+ }
290
+ catch (e) {
291
+ if ((0, Utils_1.isDefinedRuntimeError)(e) && invoiceData.metadata != null)
292
+ invoiceData.metadata.htlcOfferError = e;
293
+ if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED)
294
+ await this.cancelSwapAndInvoice(invoiceData);
295
+ throw e;
296
+ }
290
297
  this.swapLogger.debug(invoiceData, "offerHtlc(): invoice: ", invoiceData.pr);
291
298
  if (invoiceData.metadata != null)
292
299
  invoiceData.metadata.times.offerHtlc = Date.now();
@@ -295,11 +302,15 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
295
302
  const { swapContract, signer, chainInterface } = this.getChain(invoiceData.chainIdentifier);
296
303
  //Create abort controller for parallel fetches
297
304
  const abortController = new AbortController();
305
+ //Minimum reserve of native token to be kept in the wallet
306
+ const minNativeTokenReserve = this.config.minNativeBalances?.[invoiceData.chainIdentifier] ?? 0n;
298
307
  //Pre-fetch data
299
308
  const balancePrefetch = this.getBalancePrefetch(invoiceData.chainIdentifier, useToken, abortController);
300
- const gasTokenBalancePrefetch = invoiceData.getTotalOutputGasAmount() === 0n || useToken === gasToken ?
309
+ const gasTokenBalancePrefetch = (invoiceData.getTotalOutputGasAmount() === 0n && minNativeTokenReserve === 0n) || useToken === gasToken ?
301
310
  null : this.getBalancePrefetch(invoiceData.chainIdentifier, gasToken, abortController);
302
311
  if (await swapContract.getInitAuthorizationExpiry(invoiceData.data, invoiceData) < Date.now()) {
312
+ if (invoiceData.metadata != null)
313
+ invoiceData.metadata.htlcOfferError = "Init authorization expired, before being sent!";
303
314
  if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED) {
304
315
  await this.cancelSwapAndInvoice(invoiceData);
305
316
  }
@@ -308,17 +319,19 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
308
319
  try {
309
320
  //Check if we have enough liquidity to proceed
310
321
  if (useToken === gasToken) {
311
- await this.checkBalance(invoiceData.getTotalOutputAmount() + invoiceData.getTotalOutputGasAmount(), balancePrefetch, abortController.signal);
322
+ await this.checkBalance(invoiceData.getTotalOutputAmount() + invoiceData.getTotalOutputGasAmount() + minNativeTokenReserve, balancePrefetch, abortController.signal);
312
323
  }
313
324
  else {
314
325
  await this.checkBalance(invoiceData.getTotalOutputAmount(), balancePrefetch, abortController.signal);
315
- await this.checkBalance(invoiceData.getTotalOutputGasAmount(), gasTokenBalancePrefetch, abortController.signal);
326
+ await this.checkBalance(invoiceData.getTotalOutputGasAmount() + minNativeTokenReserve, gasTokenBalancePrefetch, abortController.signal);
316
327
  }
317
328
  if (invoiceData.metadata != null)
318
329
  invoiceData.metadata.times.offerHtlcChecked = Date.now();
319
330
  }
320
331
  catch (e) {
321
332
  if (!abortController.signal.aborted) {
333
+ if ((0, Utils_1.isDefinedRuntimeError)(e) && invoiceData.metadata != null)
334
+ invoiceData.metadata.htlcOfferError = e;
322
335
  if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED)
323
336
  await this.cancelSwapAndInvoice(invoiceData);
324
337
  }
@@ -331,6 +344,19 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
331
344
  signature: invoiceData.signature
332
345
  }, true);
333
346
  if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED) {
347
+ //Re-check the current HTLC count
348
+ try {
349
+ this.checkTooManyInflightHtlcs();
350
+ }
351
+ catch (e) {
352
+ if ((0, Utils_1.isDefinedRuntimeError)(e) && invoiceData.metadata != null)
353
+ invoiceData.metadata.htlcOfferError = e;
354
+ if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED)
355
+ await this.cancelSwapAndInvoice(invoiceData);
356
+ throw e;
357
+ }
358
+ this.swapLogger.debug(invoiceData, `offerHtlc(): Sending HTLC offer, current in flight HTLCs: ${this.swapsWithInflighHtlcs.size}, invoice: `, invoiceData.pr);
359
+ this.swapsWithInflighHtlcs.add(invoiceData);
334
360
  //Setting the state variable is done outside the promise, so is done synchronously
335
361
  await invoiceData.setState(FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.TXS_SENT);
336
362
  await this.saveSwapData(invoiceData);
@@ -471,6 +497,15 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
471
497
  return invoice;
472
498
  }
473
499
  }
500
+ checkTooManyInflightHtlcs() {
501
+ if (this.config.maxInflightHtlcs == null)
502
+ return;
503
+ if (this.swapsWithInflighHtlcs.size >= this.config.maxInflightHtlcs)
504
+ throw {
505
+ code: 20401,
506
+ msg: "Too many in-flight HTLCs, retry later!"
507
+ };
508
+ }
474
509
  startRestServer(restServer) {
475
510
  restServer.use(this.path + "/createInvoice", (0, ServerParamDecoder_1.serverParamDecoder)(10 * 1000));
476
511
  restServer.post(this.path + "/createInvoice", (0, Utils_1.expressHandlerWrapper)(async (req, res) => {
@@ -551,15 +586,18 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
551
586
  const gasToken = parsedBody.gasToken;
552
587
  //Check request params
553
588
  this.checkDescriptionHash(parsedBody.descriptionHash);
589
+ this.checkTooManyInflightHtlcs();
554
590
  const fees = await this.AmountAssertions.preCheckFromBtcAmounts(this.type, request, requestedAmount, gasTokenAmount);
555
591
  metadata.times.requestChecked = Date.now();
556
592
  //Create abortController for parallel prefetches
557
593
  const responseStream = res.responseStream;
558
594
  const abortController = (0, Utils_1.getAbortController)(responseStream);
595
+ //Minimum reserve of native token to be kept in the wallet
596
+ const minNativeTokenReserve = this.config.minNativeBalances?.[chainIdentifier] ?? 0n;
559
597
  //Pre-fetch data
560
598
  const { pricePrefetchPromise, gasTokenPricePrefetchPromise } = this.getFromBtcPricePrefetches(chainIdentifier, useToken, gasToken, abortController);
561
599
  const balancePrefetch = this.getBalancePrefetch(chainIdentifier, useToken, abortController);
562
- const gasTokenBalancePrefetch = gasTokenAmount.amount === 0n || useToken === gasToken ?
600
+ const gasTokenBalancePrefetch = (gasTokenAmount.amount === 0n && minNativeTokenReserve == 0n) || useToken === gasToken ?
563
601
  null : this.getBalancePrefetch(chainIdentifier, gasToken, abortController);
564
602
  const channelsPrefetch = this.LightningAssertions.getChannelsPrefetch(abortController);
565
603
  //Asynchronously send the node's public key to the client
@@ -570,11 +608,11 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
570
608
  const totalBtcInput = amountBD + amountBDgas;
571
609
  //Check if we have enough funds to honor the request
572
610
  if (useToken === gasToken) {
573
- await this.checkBalance(totalInToken + totalInGasToken, balancePrefetch, abortController.signal);
611
+ await this.checkBalance(totalInToken + totalInGasToken + minNativeTokenReserve, balancePrefetch, abortController.signal);
574
612
  }
575
613
  else {
576
614
  await this.checkBalance(totalInToken, balancePrefetch, abortController.signal);
577
- await this.checkBalance(totalInGasToken, gasTokenBalancePrefetch, abortController.signal);
615
+ await this.checkBalance(totalInGasToken + minNativeTokenReserve, gasTokenBalancePrefetch, abortController.signal);
578
616
  }
579
617
  await this.LightningAssertions.checkInboundLiquidity(totalBtcInput, channelsPrefetch, abortController.signal);
580
618
  metadata.times.balanceChecked = Date.now();
@@ -678,5 +716,9 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
678
716
  gasTokens: mappedDict
679
717
  };
680
718
  }
719
+ async removeSwapData(swap, ultimateState) {
720
+ this.swapsWithInflighHtlcs.delete(swap);
721
+ return super.removeSwapData(swap, ultimateState);
722
+ }
681
723
  }
682
724
  exports.FromBtcLnAuto = FromBtcLnAuto;
@@ -189,7 +189,7 @@ class FromBtcLnTrusted extends SwapHandler_1.SwapHandler {
189
189
  await this.storageManager.saveData(invoice.id, null, invoiceData);
190
190
  await this.lightning.cancelHodlInvoice(invoice.id);
191
191
  this.unsubscribeInvoice(invoice.id);
192
- await this.removeSwapData(invoice.id, null);
192
+ await this.removeSwapData(invoiceData);
193
193
  this.swapLogger.info(invoiceData, "htlcReceived(): transaction sending failed, refunding lightning: ", invoiceData.pr);
194
194
  throw {
195
195
  code: 20002,
@@ -221,7 +221,7 @@ class FromBtcLnTrusted extends SwapHandler_1.SwapHandler {
221
221
  await this.storageManager.saveData(invoice.id, null, invoiceData);
222
222
  await this.lightning.cancelHodlInvoice(invoice.id);
223
223
  this.unsubscribeInvoice(invoice.id);
224
- await this.removeSwapData(invoice.id, null);
224
+ await this.removeSwapData(invoiceData);
225
225
  this.swapLogger.info(invoiceData, "htlcReceived(): transaction reverted, refunding lightning: ", invoiceData.pr);
226
226
  throw {
227
227
  code: 20002,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/lp-lib",
3
- "version": "16.0.0",
3
+ "version": "16.0.1",
4
4
  "description": "Main functionality implementation for atomiq LP node",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -165,31 +165,14 @@ export abstract class SwapHandler<V extends SwapHandlerSwap<S> = SwapHandlerSwap
165
165
  */
166
166
  abstract getInfoData(): any;
167
167
 
168
- /**
169
- * Remove swap data
170
- *
171
- * @param hash
172
- * @param sequence
173
- */
174
- protected removeSwapData(hash: string, sequence: bigint): Promise<void>;
175
-
176
168
  /**
177
169
  * Remove swap data
178
170
  *
179
171
  * @param swap
180
172
  * @param ultimateState set the ultimate state of the swap before removing
181
173
  */
182
- protected removeSwapData(swap: V, ultimateState?: S): Promise<void>;
183
-
184
- protected async removeSwapData(hashOrSwap: string | V, sequenceOrUltimateState?: bigint | S) {
185
- let swap: V;
186
- if(typeof(hashOrSwap)==="string") {
187
- if(typeof(sequenceOrUltimateState)!=="bigint") throw new Error("Sequence must be a BN instance!");
188
- swap = await this.storageManager.getData(hashOrSwap, sequenceOrUltimateState);
189
- } else {
190
- swap = hashOrSwap;
191
- if(sequenceOrUltimateState!=null && typeof(sequenceOrUltimateState)!=="bigint") await swap.setState(sequenceOrUltimateState);
192
- }
174
+ protected async removeSwapData(swap: V, ultimateState?: S) {
175
+ if(ultimateState!=null) await swap.setState(ultimateState);
193
176
  if(swap!=null) await PluginManager.swapRemove(swap);
194
177
  this.swapLogger.debug(swap, "removeSwapData(): removing swap final state: "+swap.state);
195
178
  await this.storageManager.removeData(swap.getIdentifierHash(), swap.getSequence());
@@ -89,17 +89,9 @@ export abstract class EscrowHandler<V extends EscrowHandlerSwap<SwapData, S>, S>
89
89
  }
90
90
  }
91
91
 
92
- protected removeSwapData(hash: string, sequence: bigint): Promise<void>;
93
92
  protected removeSwapData(swap: V, ultimateState?: S): Promise<void>;
94
- protected async removeSwapData(hashOrSwap: string | V, sequenceOrUltimateState?: bigint | S) {
95
- let swap: V;
96
- if(typeof(hashOrSwap)==="string") {
97
- if(typeof(sequenceOrUltimateState)!=="bigint") throw new Error("Sequence must be a BN instance!");
98
- swap = await this.storageManager.getData(hashOrSwap, sequenceOrUltimateState);
99
- } else {
100
- swap = hashOrSwap;
101
- if(sequenceOrUltimateState!=null && typeof(sequenceOrUltimateState)!=="bigint") await swap.setState(sequenceOrUltimateState);
102
- }
93
+ protected async removeSwapData(swap: V, ultimateState?: S) {
94
+ if(ultimateState!=null) await swap.setState(ultimateState);
103
95
  if(swap!=null) await PluginManager.swapRemove(swap);
104
96
  this.swapLogger.debug(swap, "removeSwapData(): removing swap final state: "+swap.state);
105
97
  this.removeSwapFromEscrowHashMap(swap);
@@ -4,7 +4,7 @@ import {FromBtcLnAutoSwap, FromBtcLnAutoSwapState} from "./FromBtcLnAutoSwap";
4
4
  import {MultichainData, SwapHandlerType} from "../../SwapHandler";
5
5
  import {ISwapPrice} from "../../../prices/ISwapPrice";
6
6
  import {ChainSwapType, ClaimEvent, InitializeEvent, RefundEvent, SwapCommitStateType, SwapData} from "@atomiqlabs/base";
7
- import {expressHandlerWrapper, getAbortController, HEX_REGEX} from "../../../utils/Utils";
7
+ import {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../../utils/Utils";
8
8
  import {PluginManager} from "../../../plugins/PluginManager";
9
9
  import {IIntermediaryStorage} from "../../../storage/IIntermediaryStorage";
10
10
  import {FieldTypeEnum, verifySchema} from "../../../utils/paramcoders/SchemaVerifier";
@@ -21,6 +21,10 @@ import {
21
21
  import {LightningAssertions} from "../../assertions/LightningAssertions";
22
22
 
23
23
  export type FromBtcLnAutoConfig = FromBtcBaseConfig & {
24
+ maxInflightHtlcs?: number,
25
+ minNativeBalances?: {
26
+ [chainId: string]: bigint
27
+ },
24
28
  invoiceTimeoutSeconds?: number,
25
29
  minCltv: bigint,
26
30
  gracePeriod: bigint,
@@ -47,6 +51,7 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
47
51
  readonly swapType = ChainSwapType.HTLC;
48
52
 
49
53
  activeSubscriptions: Set<string> = new Set<string>();
54
+ swapsWithInflighHtlcs: Set<FromBtcLnAutoSwap> = new Set();
50
55
 
51
56
  readonly config: FromBtcLnAutoConfig;
52
57
  readonly lightning: ILightningWallet;
@@ -100,13 +105,7 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
100
105
 
101
106
  if(swap.state===FromBtcLnAutoSwapState.RECEIVED) {
102
107
  try {
103
- if(!await this.offerHtlc(swap)) {
104
- //Expired
105
- if(swap.state===FromBtcLnAutoSwapState.RECEIVED) {
106
- this.swapLogger.info(swap, "processPastSwap(state=RECEIVED): offer HTLC expired, cancelling invoice: "+swap.pr);
107
- await this.cancelSwapAndInvoice(swap);
108
- }
109
- }
108
+ await this.offerHtlc(swap);
110
109
  } catch (e) {
111
110
  this.swapLogger.error(swap, "processPastSwap(state=RECEIVED): offerHtlc error", e);
112
111
  }
@@ -115,6 +114,8 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
115
114
  }
116
115
 
117
116
  if(swap.state===FromBtcLnAutoSwapState.TXS_SENT || swap.state===FromBtcLnAutoSwapState.COMMITED) {
117
+ this.swapsWithInflighHtlcs.add(swap);
118
+
118
119
  const onchainStatus = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
119
120
  const state: FromBtcLnAutoSwapState = swap.state as FromBtcLnAutoSwapState;
120
121
  if(onchainStatus.type===SwapCommitStateType.PAID) {
@@ -319,7 +320,6 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
319
320
  if(invoiceData.metadata!=null) invoiceData.metadata.times.htlcReceived = Date.now();
320
321
 
321
322
  const useToken = invoiceData.token;
322
- const gasToken = invoiceData.gasToken;
323
323
 
324
324
  let expiryTimeout: bigint;
325
325
  try {
@@ -327,6 +327,7 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
327
327
  expiryTimeout = await this.checkHtlcExpiry(invoice);
328
328
  if(invoiceData.metadata!=null) invoiceData.metadata.times.htlcTimeoutCalculated = Date.now();
329
329
  } catch (e) {
330
+ if(isDefinedRuntimeError(e) && invoiceData.metadata!=null) invoiceData.metadata.htlcReceiveError = e;
330
331
  if(invoiceData.state===FromBtcLnAutoSwapState.CREATED) await this.cancelSwapAndInvoice(invoiceData);
331
332
  throw e;
332
333
  }
@@ -369,6 +370,14 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
369
370
  private async offerHtlc(invoiceData: FromBtcLnAutoSwap) {
370
371
  if(invoiceData.state!==FromBtcLnAutoSwapState.RECEIVED) return;
371
372
 
373
+ try {
374
+ this.checkTooManyInflightHtlcs();
375
+ } catch (e) {
376
+ if(isDefinedRuntimeError(e) && invoiceData.metadata!=null) invoiceData.metadata.htlcOfferError = e;
377
+ if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) await this.cancelSwapAndInvoice(invoiceData);
378
+ throw e;
379
+ }
380
+
372
381
  this.swapLogger.debug(invoiceData, "offerHtlc(): invoice: ", invoiceData.pr);
373
382
  if(invoiceData.metadata!=null) invoiceData.metadata.times.offerHtlc = Date.now();
374
383
 
@@ -380,12 +389,16 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
380
389
  //Create abort controller for parallel fetches
381
390
  const abortController = new AbortController();
382
391
 
392
+ //Minimum reserve of native token to be kept in the wallet
393
+ const minNativeTokenReserve: bigint = this.config.minNativeBalances?.[invoiceData.chainIdentifier] ?? 0n;
394
+
383
395
  //Pre-fetch data
384
396
  const balancePrefetch: Promise<bigint> = this.getBalancePrefetch(invoiceData.chainIdentifier, useToken, abortController);
385
- const gasTokenBalancePrefetch: Promise<bigint> = invoiceData.getTotalOutputGasAmount()===0n || useToken===gasToken ?
397
+ const gasTokenBalancePrefetch: Promise<bigint> = (invoiceData.getTotalOutputGasAmount()===0n && minNativeTokenReserve===0n) || useToken===gasToken ?
386
398
  null : this.getBalancePrefetch(invoiceData.chainIdentifier, gasToken, abortController);
387
399
 
388
400
  if(await swapContract.getInitAuthorizationExpiry(invoiceData.data, invoiceData) < Date.now()) {
401
+ if(invoiceData.metadata!=null) invoiceData.metadata.htlcOfferError = "Init authorization expired, before being sent!";
389
402
  if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) {
390
403
  await this.cancelSwapAndInvoice(invoiceData);
391
404
  }
@@ -395,14 +408,15 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
395
408
  try {
396
409
  //Check if we have enough liquidity to proceed
397
410
  if(useToken===gasToken) {
398
- await this.checkBalance(invoiceData.getTotalOutputAmount() + invoiceData.getTotalOutputGasAmount(), balancePrefetch, abortController.signal);
411
+ await this.checkBalance(invoiceData.getTotalOutputAmount() + invoiceData.getTotalOutputGasAmount() + minNativeTokenReserve, balancePrefetch, abortController.signal);
399
412
  } else {
400
413
  await this.checkBalance(invoiceData.getTotalOutputAmount(), balancePrefetch, abortController.signal);
401
- await this.checkBalance(invoiceData.getTotalOutputGasAmount(), gasTokenBalancePrefetch, abortController.signal);
414
+ await this.checkBalance(invoiceData.getTotalOutputGasAmount() + minNativeTokenReserve, gasTokenBalancePrefetch, abortController.signal);
402
415
  }
403
416
  if(invoiceData.metadata!=null) invoiceData.metadata.times.offerHtlcChecked = Date.now();
404
417
  } catch (e) {
405
418
  if(!abortController.signal.aborted) {
419
+ if(isDefinedRuntimeError(e) && invoiceData.metadata!=null) invoiceData.metadata.htlcOfferError = e;
406
420
  if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) await this.cancelSwapAndInvoice(invoiceData);
407
421
  }
408
422
  throw e;
@@ -416,6 +430,16 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
416
430
  }, true);
417
431
 
418
432
  if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) {
433
+ //Re-check the current HTLC count
434
+ try {
435
+ this.checkTooManyInflightHtlcs();
436
+ } catch (e) {
437
+ if(isDefinedRuntimeError(e) && invoiceData.metadata!=null) invoiceData.metadata.htlcOfferError = e;
438
+ if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) await this.cancelSwapAndInvoice(invoiceData);
439
+ throw e;
440
+ }
441
+ this.swapLogger.debug(invoiceData, `offerHtlc(): Sending HTLC offer, current in flight HTLCs: ${this.swapsWithInflighHtlcs.size}, invoice: `, invoiceData.pr);
442
+ this.swapsWithInflighHtlcs.add(invoiceData);
419
443
  //Setting the state variable is done outside the promise, so is done synchronously
420
444
  await invoiceData.setState(FromBtcLnAutoSwapState.TXS_SENT);
421
445
  await this.saveSwapData(invoiceData);
@@ -565,6 +589,14 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
565
589
  }
566
590
  }
567
591
 
592
+ private checkTooManyInflightHtlcs() {
593
+ if(this.config.maxInflightHtlcs==null) return;
594
+ if(this.swapsWithInflighHtlcs.size>=this.config.maxInflightHtlcs) throw {
595
+ code: 20401,
596
+ msg: "Too many in-flight HTLCs, retry later!"
597
+ }
598
+ }
599
+
568
600
  startRestServer(restServer: Express) {
569
601
 
570
602
  restServer.use(this.path+"/createInvoice", serverParamDecoder(10*1000));
@@ -653,6 +685,7 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
653
685
 
654
686
  //Check request params
655
687
  this.checkDescriptionHash(parsedBody.descriptionHash);
688
+ this.checkTooManyInflightHtlcs();
656
689
  const fees = await this.AmountAssertions.preCheckFromBtcAmounts(this.type, request, requestedAmount, gasTokenAmount);
657
690
  metadata.times.requestChecked = Date.now();
658
691
 
@@ -660,13 +693,16 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
660
693
  const responseStream = res.responseStream;
661
694
  const abortController = getAbortController(responseStream);
662
695
 
696
+ //Minimum reserve of native token to be kept in the wallet
697
+ const minNativeTokenReserve: bigint = this.config.minNativeBalances?.[chainIdentifier] ?? 0n;
698
+
663
699
  //Pre-fetch data
664
700
  const {
665
701
  pricePrefetchPromise,
666
702
  gasTokenPricePrefetchPromise
667
703
  } = this.getFromBtcPricePrefetches(chainIdentifier, useToken, gasToken, abortController);
668
704
  const balancePrefetch: Promise<bigint> = this.getBalancePrefetch(chainIdentifier, useToken, abortController);
669
- const gasTokenBalancePrefetch: Promise<bigint> = gasTokenAmount.amount===0n || useToken===gasToken ?
705
+ const gasTokenBalancePrefetch: Promise<bigint> = (gasTokenAmount.amount===0n && minNativeTokenReserve==0n) || useToken===gasToken ?
670
706
  null : this.getBalancePrefetch(chainIdentifier, gasToken, abortController);
671
707
  const channelsPrefetch: Promise<LightningNetworkChannel[]> = this.LightningAssertions.getChannelsPrefetch(abortController);
672
708
 
@@ -695,10 +731,10 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
695
731
 
696
732
  //Check if we have enough funds to honor the request
697
733
  if(useToken===gasToken) {
698
- await this.checkBalance(totalInToken + totalInGasToken, balancePrefetch, abortController.signal);
734
+ await this.checkBalance(totalInToken + totalInGasToken + minNativeTokenReserve, balancePrefetch, abortController.signal);
699
735
  } else {
700
736
  await this.checkBalance(totalInToken, balancePrefetch, abortController.signal);
701
- await this.checkBalance(totalInGasToken, gasTokenBalancePrefetch, abortController.signal);
737
+ await this.checkBalance(totalInGasToken + minNativeTokenReserve, gasTokenBalancePrefetch, abortController.signal);
702
738
  }
703
739
  await this.LightningAssertions.checkInboundLiquidity(totalBtcInput, channelsPrefetch, abortController.signal);
704
740
  metadata.times.balanceChecked = Date.now();
@@ -843,5 +879,10 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
843
879
  };
844
880
  }
845
881
 
882
+ protected async removeSwapData(swap: FromBtcLnAutoSwap, ultimateState?: FromBtcLnAutoSwapState) {
883
+ this.swapsWithInflighHtlcs.delete(swap);
884
+ return super.removeSwapData(swap, ultimateState);
885
+ }
886
+
846
887
  }
847
888
 
@@ -234,7 +234,7 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
234
234
  await this.storageManager.saveData(invoice.id, null, invoiceData);
235
235
  await this.lightning.cancelHodlInvoice(invoice.id);
236
236
  this.unsubscribeInvoice(invoice.id);
237
- await this.removeSwapData(invoice.id, null);
237
+ await this.removeSwapData(invoiceData);
238
238
  this.swapLogger.info(invoiceData, "htlcReceived(): transaction sending failed, refunding lightning: ", invoiceData.pr);
239
239
  throw {
240
240
  code: 20002,
@@ -267,7 +267,7 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
267
267
  await this.storageManager.saveData(invoice.id, null, invoiceData);
268
268
  await this.lightning.cancelHodlInvoice(invoice.id);
269
269
  this.unsubscribeInvoice(invoice.id);
270
- await this.removeSwapData(invoice.id, null);
270
+ await this.removeSwapData(invoiceData);
271
271
  this.swapLogger.info(invoiceData, "htlcReceived(): transaction reverted, refunding lightning: ", invoiceData.pr);
272
272
  throw {
273
273
  code: 20002,