@atomiqlabs/lp-lib 16.0.0 → 16.0.2

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,16 +302,34 @@ 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
309
  const gasTokenBalancePrefetch = invoiceData.getTotalOutputGasAmount() === 0n || useToken === gasToken ?
301
310
  null : this.getBalancePrefetch(invoiceData.chainIdentifier, gasToken, abortController);
311
+ const nativeBalancePrefetch = minNativeTokenReserve === 0n ?
312
+ null : this.getBalancePrefetch(invoiceData.chainIdentifier, chainInterface.getNativeCurrencyAddress(), abortController, false);
302
313
  if (await swapContract.getInitAuthorizationExpiry(invoiceData.data, invoiceData) < Date.now()) {
314
+ if (invoiceData.metadata != null)
315
+ invoiceData.metadata.htlcOfferError = "Init authorization expired, before being sent!";
303
316
  if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED) {
304
317
  await this.cancelSwapAndInvoice(invoiceData);
305
318
  }
306
319
  return false;
307
320
  }
321
+ try {
322
+ await this.checkBalance(minNativeTokenReserve, nativeBalancePrefetch, abortController.signal);
323
+ }
324
+ catch (e) {
325
+ if (!abortController.signal.aborted) {
326
+ if ((0, Utils_1.isDefinedRuntimeError)(e) && invoiceData.metadata != null)
327
+ invoiceData.metadata.htlcOfferError = "Not enough native balance!";
328
+ if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED)
329
+ await this.cancelSwapAndInvoice(invoiceData);
330
+ }
331
+ throw e;
332
+ }
308
333
  try {
309
334
  //Check if we have enough liquidity to proceed
310
335
  if (useToken === gasToken) {
@@ -319,6 +344,8 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
319
344
  }
320
345
  catch (e) {
321
346
  if (!abortController.signal.aborted) {
347
+ if ((0, Utils_1.isDefinedRuntimeError)(e) && invoiceData.metadata != null)
348
+ invoiceData.metadata.htlcOfferError = e;
322
349
  if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED)
323
350
  await this.cancelSwapAndInvoice(invoiceData);
324
351
  }
@@ -331,6 +358,19 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
331
358
  signature: invoiceData.signature
332
359
  }, true);
333
360
  if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED) {
361
+ //Re-check the current HTLC count
362
+ try {
363
+ this.checkTooManyInflightHtlcs();
364
+ }
365
+ catch (e) {
366
+ if ((0, Utils_1.isDefinedRuntimeError)(e) && invoiceData.metadata != null)
367
+ invoiceData.metadata.htlcOfferError = e;
368
+ if (invoiceData.state === FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.RECEIVED)
369
+ await this.cancelSwapAndInvoice(invoiceData);
370
+ throw e;
371
+ }
372
+ this.swapLogger.debug(invoiceData, `offerHtlc(): Sending HTLC offer, current in flight HTLCs: ${this.swapsWithInflighHtlcs.size}, invoice: `, invoiceData.pr);
373
+ this.swapsWithInflighHtlcs.add(invoiceData);
334
374
  //Setting the state variable is done outside the promise, so is done synchronously
335
375
  await invoiceData.setState(FromBtcLnAutoSwap_1.FromBtcLnAutoSwapState.TXS_SENT);
336
376
  await this.saveSwapData(invoiceData);
@@ -471,6 +511,15 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
471
511
  return invoice;
472
512
  }
473
513
  }
514
+ checkTooManyInflightHtlcs() {
515
+ if (this.config.maxInflightHtlcs == null)
516
+ return;
517
+ if (this.swapsWithInflighHtlcs.size >= this.config.maxInflightHtlcs)
518
+ throw {
519
+ code: 20401,
520
+ msg: "Too many in-flight HTLCs, retry later!"
521
+ };
522
+ }
474
523
  startRestServer(restServer) {
475
524
  restServer.use(this.path + "/createInvoice", (0, ServerParamDecoder_1.serverParamDecoder)(10 * 1000));
476
525
  restServer.post(this.path + "/createInvoice", (0, Utils_1.expressHandlerWrapper)(async (req, res) => {
@@ -551,16 +600,21 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
551
600
  const gasToken = parsedBody.gasToken;
552
601
  //Check request params
553
602
  this.checkDescriptionHash(parsedBody.descriptionHash);
603
+ this.checkTooManyInflightHtlcs();
554
604
  const fees = await this.AmountAssertions.preCheckFromBtcAmounts(this.type, request, requestedAmount, gasTokenAmount);
555
605
  metadata.times.requestChecked = Date.now();
556
606
  //Create abortController for parallel prefetches
557
607
  const responseStream = res.responseStream;
558
608
  const abortController = (0, Utils_1.getAbortController)(responseStream);
609
+ //Minimum reserve of native token to be kept in the wallet
610
+ const minNativeTokenReserve = this.config.minNativeBalances?.[chainIdentifier] ?? 0n;
559
611
  //Pre-fetch data
560
612
  const { pricePrefetchPromise, gasTokenPricePrefetchPromise } = this.getFromBtcPricePrefetches(chainIdentifier, useToken, gasToken, abortController);
561
613
  const balancePrefetch = this.getBalancePrefetch(chainIdentifier, useToken, abortController);
562
614
  const gasTokenBalancePrefetch = gasTokenAmount.amount === 0n || useToken === gasToken ?
563
615
  null : this.getBalancePrefetch(chainIdentifier, gasToken, abortController);
616
+ const nativeTokenBalancePrefetch = minNativeTokenReserve === 0n ?
617
+ null : this.getBalancePrefetch(chainIdentifier, chainInterface.getNativeCurrencyAddress(), abortController, false);
564
618
  const channelsPrefetch = this.LightningAssertions.getChannelsPrefetch(abortController);
565
619
  //Asynchronously send the node's public key to the client
566
620
  this.sendPublicKeyAsync(responseStream);
@@ -568,6 +622,8 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
568
622
  let { amountBD, swapFee, swapFeeInToken, totalInToken, amountBDgas, gasSwapFee, gasSwapFeeInToken, totalInGasToken } = await this.AmountAssertions.checkFromBtcAmount(this.type, request, { ...requestedAmount, pricePrefetch: pricePrefetchPromise }, fees, abortController.signal, { ...gasTokenAmount, pricePrefetch: gasTokenPricePrefetchPromise });
569
623
  metadata.times.priceCalculated = Date.now();
570
624
  const totalBtcInput = amountBD + amountBDgas;
625
+ //Check if we have at least the minimum needed native balance
626
+ await this.checkBalance(minNativeTokenReserve, nativeTokenBalancePrefetch, abortController.signal);
571
627
  //Check if we have enough funds to honor the request
572
628
  if (useToken === gasToken) {
573
629
  await this.checkBalance(totalInToken + totalInGasToken, balancePrefetch, abortController.signal);
@@ -678,5 +734,9 @@ class FromBtcLnAuto extends FromBtcBaseSwapHandler_1.FromBtcBaseSwapHandler {
678
734
  gasTokens: mappedDict
679
735
  };
680
736
  }
737
+ async removeSwapData(swap, ultimateState) {
738
+ this.swapsWithInflighHtlcs.delete(swap);
739
+ return super.removeSwapData(swap, ultimateState);
740
+ }
681
741
  }
682
742
  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.2",
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,18 +389,34 @@ 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
397
  const gasTokenBalancePrefetch: Promise<bigint> = invoiceData.getTotalOutputGasAmount()===0n || useToken===gasToken ?
386
398
  null : this.getBalancePrefetch(invoiceData.chainIdentifier, gasToken, abortController);
399
+ const nativeBalancePrefetch: Promise<bigint> = minNativeTokenReserve===0n ?
400
+ null : this.getBalancePrefetch(invoiceData.chainIdentifier, chainInterface.getNativeCurrencyAddress(), abortController, false);
387
401
 
388
402
  if(await swapContract.getInitAuthorizationExpiry(invoiceData.data, invoiceData) < Date.now()) {
403
+ if(invoiceData.metadata!=null) invoiceData.metadata.htlcOfferError = "Init authorization expired, before being sent!";
389
404
  if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) {
390
405
  await this.cancelSwapAndInvoice(invoiceData);
391
406
  }
392
407
  return false;
393
408
  }
394
409
 
410
+ try {
411
+ await this.checkBalance(minNativeTokenReserve, nativeBalancePrefetch, abortController.signal);
412
+ } catch (e) {
413
+ if(!abortController.signal.aborted) {
414
+ if(isDefinedRuntimeError(e) && invoiceData.metadata!=null) invoiceData.metadata.htlcOfferError = "Not enough native balance!";
415
+ if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) await this.cancelSwapAndInvoice(invoiceData);
416
+ }
417
+ throw e;
418
+ }
419
+
395
420
  try {
396
421
  //Check if we have enough liquidity to proceed
397
422
  if(useToken===gasToken) {
@@ -403,6 +428,7 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
403
428
  if(invoiceData.metadata!=null) invoiceData.metadata.times.offerHtlcChecked = Date.now();
404
429
  } catch (e) {
405
430
  if(!abortController.signal.aborted) {
431
+ if(isDefinedRuntimeError(e) && invoiceData.metadata!=null) invoiceData.metadata.htlcOfferError = e;
406
432
  if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) await this.cancelSwapAndInvoice(invoiceData);
407
433
  }
408
434
  throw e;
@@ -416,6 +442,16 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
416
442
  }, true);
417
443
 
418
444
  if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) {
445
+ //Re-check the current HTLC count
446
+ try {
447
+ this.checkTooManyInflightHtlcs();
448
+ } catch (e) {
449
+ if(isDefinedRuntimeError(e) && invoiceData.metadata!=null) invoiceData.metadata.htlcOfferError = e;
450
+ if(invoiceData.state===FromBtcLnAutoSwapState.RECEIVED) await this.cancelSwapAndInvoice(invoiceData);
451
+ throw e;
452
+ }
453
+ this.swapLogger.debug(invoiceData, `offerHtlc(): Sending HTLC offer, current in flight HTLCs: ${this.swapsWithInflighHtlcs.size}, invoice: `, invoiceData.pr);
454
+ this.swapsWithInflighHtlcs.add(invoiceData);
419
455
  //Setting the state variable is done outside the promise, so is done synchronously
420
456
  await invoiceData.setState(FromBtcLnAutoSwapState.TXS_SENT);
421
457
  await this.saveSwapData(invoiceData);
@@ -565,6 +601,14 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
565
601
  }
566
602
  }
567
603
 
604
+ private checkTooManyInflightHtlcs() {
605
+ if(this.config.maxInflightHtlcs==null) return;
606
+ if(this.swapsWithInflighHtlcs.size>=this.config.maxInflightHtlcs) throw {
607
+ code: 20401,
608
+ msg: "Too many in-flight HTLCs, retry later!"
609
+ }
610
+ }
611
+
568
612
  startRestServer(restServer: Express) {
569
613
 
570
614
  restServer.use(this.path+"/createInvoice", serverParamDecoder(10*1000));
@@ -653,6 +697,7 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
653
697
 
654
698
  //Check request params
655
699
  this.checkDescriptionHash(parsedBody.descriptionHash);
700
+ this.checkTooManyInflightHtlcs();
656
701
  const fees = await this.AmountAssertions.preCheckFromBtcAmounts(this.type, request, requestedAmount, gasTokenAmount);
657
702
  metadata.times.requestChecked = Date.now();
658
703
 
@@ -660,6 +705,9 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
660
705
  const responseStream = res.responseStream;
661
706
  const abortController = getAbortController(responseStream);
662
707
 
708
+ //Minimum reserve of native token to be kept in the wallet
709
+ const minNativeTokenReserve: bigint = this.config.minNativeBalances?.[chainIdentifier] ?? 0n;
710
+
663
711
  //Pre-fetch data
664
712
  const {
665
713
  pricePrefetchPromise,
@@ -668,6 +716,8 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
668
716
  const balancePrefetch: Promise<bigint> = this.getBalancePrefetch(chainIdentifier, useToken, abortController);
669
717
  const gasTokenBalancePrefetch: Promise<bigint> = gasTokenAmount.amount===0n || useToken===gasToken ?
670
718
  null : this.getBalancePrefetch(chainIdentifier, gasToken, abortController);
719
+ const nativeTokenBalancePrefetch: Promise<bigint> = minNativeTokenReserve===0n ?
720
+ null : this.getBalancePrefetch(chainIdentifier, chainInterface.getNativeCurrencyAddress(), abortController, false);
671
721
  const channelsPrefetch: Promise<LightningNetworkChannel[]> = this.LightningAssertions.getChannelsPrefetch(abortController);
672
722
 
673
723
  //Asynchronously send the node's public key to the client
@@ -693,6 +743,9 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
693
743
 
694
744
  const totalBtcInput = amountBD + amountBDgas;
695
745
 
746
+ //Check if we have at least the minimum needed native balance
747
+ await this.checkBalance(minNativeTokenReserve, nativeTokenBalancePrefetch, abortController.signal);
748
+
696
749
  //Check if we have enough funds to honor the request
697
750
  if(useToken===gasToken) {
698
751
  await this.checkBalance(totalInToken + totalInGasToken, balancePrefetch, abortController.signal);
@@ -843,5 +896,10 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
843
896
  };
844
897
  }
845
898
 
899
+ protected async removeSwapData(swap: FromBtcLnAutoSwap, ultimateState?: FromBtcLnAutoSwapState) {
900
+ this.swapsWithInflighHtlcs.delete(swap);
901
+ return super.removeSwapData(swap, ultimateState);
902
+ }
903
+
846
904
  }
847
905
 
@@ -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,