@atomiqlabs/lp-lib 17.6.0 → 17.6.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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.verifySchema = exports.FieldTypeEnum = exports.parseBigInt = void 0;
3
+ exports.verifySchema = exports.verifySchemaField = exports.FieldTypeEnum = exports.parseBigInt = void 0;
4
4
  function parseBigInt(str) {
5
5
  if (str == null)
6
6
  return null;
@@ -21,63 +21,109 @@ var FieldTypeEnum;
21
21
  FieldTypeEnum[FieldTypeEnum["Number"] = 2] = "Number";
22
22
  FieldTypeEnum[FieldTypeEnum["BigInt"] = 3] = "BigInt";
23
23
  FieldTypeEnum[FieldTypeEnum["Any"] = 4] = "Any";
24
+ FieldTypeEnum[FieldTypeEnum["BigIntPositive"] = 5] = "BigIntPositive";
25
+ FieldTypeEnum[FieldTypeEnum["BigIntNotNegative"] = 6] = "BigIntNotNegative";
26
+ FieldTypeEnum[FieldTypeEnum["NumberPositive"] = 7] = "NumberPositive";
27
+ FieldTypeEnum[FieldTypeEnum["NumberNotNegative"] = 8] = "NumberNotNegative";
24
28
  FieldTypeEnum[FieldTypeEnum["StringOptional"] = 100] = "StringOptional";
25
29
  FieldTypeEnum[FieldTypeEnum["BooleanOptional"] = 101] = "BooleanOptional";
26
30
  FieldTypeEnum[FieldTypeEnum["NumberOptional"] = 102] = "NumberOptional";
27
31
  FieldTypeEnum[FieldTypeEnum["BigIntOptional"] = 103] = "BigIntOptional";
28
32
  FieldTypeEnum[FieldTypeEnum["AnyOptional"] = 104] = "AnyOptional";
33
+ FieldTypeEnum[FieldTypeEnum["BigIntPositiveOptional"] = 105] = "BigIntPositiveOptional";
34
+ FieldTypeEnum[FieldTypeEnum["BigIntNotNegativeOptional"] = 106] = "BigIntNotNegativeOptional";
35
+ FieldTypeEnum[FieldTypeEnum["NumberPositiveOptional"] = 107] = "NumberPositiveOptional";
36
+ FieldTypeEnum[FieldTypeEnum["NumberNotNegativeOptional"] = 108] = "NumberNotNegativeOptional";
29
37
  })(FieldTypeEnum = exports.FieldTypeEnum || (exports.FieldTypeEnum = {}));
38
+ function verifySchemaField(val, type, fieldName, resultSchema) {
39
+ if (typeof (type) === "function") {
40
+ const result = type(val);
41
+ if (result == null)
42
+ return false;
43
+ resultSchema[fieldName] = result;
44
+ return true;
45
+ }
46
+ if (val == null && type >= 100) {
47
+ resultSchema[fieldName] = null;
48
+ return true;
49
+ }
50
+ if (type === FieldTypeEnum.Any || type === FieldTypeEnum.AnyOptional) {
51
+ resultSchema[fieldName] = val;
52
+ }
53
+ else if (type === FieldTypeEnum.Boolean || type === FieldTypeEnum.BooleanOptional) {
54
+ if (typeof (val) !== "boolean")
55
+ return false;
56
+ resultSchema[fieldName] = val;
57
+ }
58
+ else if (type === FieldTypeEnum.Number || type === FieldTypeEnum.NumberOptional) {
59
+ if (typeof (val) !== "number")
60
+ return false;
61
+ if (isNaN(val))
62
+ return false;
63
+ resultSchema[fieldName] = val;
64
+ }
65
+ else if (type === FieldTypeEnum.NumberPositive || type === FieldTypeEnum.NumberPositiveOptional) {
66
+ if (typeof (val) !== "number")
67
+ return false;
68
+ if (isNaN(val))
69
+ return false;
70
+ if (val <= 0)
71
+ return false;
72
+ resultSchema[fieldName] = val;
73
+ }
74
+ else if (type === FieldTypeEnum.NumberNotNegative || type === FieldTypeEnum.NumberNotNegativeOptional) {
75
+ if (typeof (val) !== "number")
76
+ return false;
77
+ if (isNaN(val))
78
+ return false;
79
+ if (val < 0)
80
+ return false;
81
+ resultSchema[fieldName] = val;
82
+ }
83
+ else if (type === FieldTypeEnum.BigInt || type === FieldTypeEnum.BigIntOptional) {
84
+ const result = parseBigInt(val);
85
+ if (result == null)
86
+ return false;
87
+ resultSchema[fieldName] = result;
88
+ }
89
+ else if (type === FieldTypeEnum.BigIntPositive || type === FieldTypeEnum.BigIntPositiveOptional) {
90
+ const result = parseBigInt(val);
91
+ if (result == null)
92
+ return false;
93
+ if (result <= 0n)
94
+ return false;
95
+ resultSchema[fieldName] = result;
96
+ }
97
+ else if (type === FieldTypeEnum.BigIntNotNegative || type === FieldTypeEnum.BigIntNotNegativeOptional) {
98
+ const result = parseBigInt(val);
99
+ if (result == null)
100
+ return false;
101
+ if (result < 0n)
102
+ return false;
103
+ resultSchema[fieldName] = result;
104
+ }
105
+ else if (type === FieldTypeEnum.String || type === FieldTypeEnum.StringOptional) {
106
+ if (typeof (val) !== "string")
107
+ return false;
108
+ resultSchema[fieldName] = val;
109
+ }
110
+ else {
111
+ //Probably another request schema
112
+ const result = verifySchema(val, type);
113
+ if (result == null)
114
+ return false;
115
+ resultSchema[fieldName] = result;
116
+ }
117
+ return true;
118
+ }
119
+ exports.verifySchemaField = verifySchemaField;
30
120
  function verifySchema(req, schema) {
31
121
  if (req == null)
32
122
  return null;
33
123
  const resultSchema = {};
34
124
  for (let fieldName in schema) {
35
- const val = req[fieldName];
36
- const type = schema[fieldName];
37
- if (typeof (type) === "function") {
38
- const result = type(val);
39
- if (result == null)
40
- return null;
41
- resultSchema[fieldName] = result;
42
- continue;
43
- }
44
- if (val == null && type >= 100) {
45
- resultSchema[fieldName] = null;
46
- continue;
47
- }
48
- if (type === FieldTypeEnum.Any || type === FieldTypeEnum.AnyOptional) {
49
- resultSchema[fieldName] = val;
50
- }
51
- else if (type === FieldTypeEnum.Boolean || type === FieldTypeEnum.BooleanOptional) {
52
- if (typeof (val) !== "boolean")
53
- return null;
54
- resultSchema[fieldName] = val;
55
- }
56
- else if (type === FieldTypeEnum.Number || type === FieldTypeEnum.NumberOptional) {
57
- if (typeof (val) !== "number")
58
- return null;
59
- if (isNaN(val))
60
- return null;
61
- resultSchema[fieldName] = val;
62
- }
63
- else if (type === FieldTypeEnum.BigInt || type === FieldTypeEnum.BigIntOptional) {
64
- const result = parseBigInt(val);
65
- if (result == null)
66
- return null;
67
- resultSchema[fieldName] = result;
68
- }
69
- else if (type === FieldTypeEnum.String || type === FieldTypeEnum.StringOptional) {
70
- if (typeof (val) !== "string")
71
- return null;
72
- resultSchema[fieldName] = val;
73
- }
74
- else {
75
- //Probably another request schema
76
- const result = verifySchema(val, type);
77
- if (result == null)
78
- return null;
79
- resultSchema[fieldName] = result;
80
- }
125
+ if (!verifySchemaField(req[fieldName], schema[fieldName], fieldName, resultSchema))
126
+ return null;
81
127
  }
82
128
  return resultSchema;
83
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/lp-lib",
3
- "version": "17.6.0",
3
+ "version": "17.6.1",
4
4
  "description": "Main functionality implementation for atomiq LP node",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -8,6 +8,7 @@ import {
8
8
  ClaimEvent,
9
9
  InitializeEvent,
10
10
  RefundEvent,
11
+ SwapCommitStateType,
11
12
  SwapData
12
13
  } from "@atomiqlabs/base";
13
14
  import {randomBytes} from "crypto";
@@ -21,7 +22,6 @@ import {ServerParamEncoder} from "../../../utils/paramcoders/server/ServerParamE
21
22
  import {FromBtcBaseConfig, FromBtcBaseSwapHandler} from "../FromBtcBaseSwapHandler";
22
23
  import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
23
24
  import {isQuoteThrow} from "../../../plugins/IPlugin";
24
- import {FromBtcLnSwapState} from "../frombtcln_abstract/FromBtcLnSwapAbs";
25
25
 
26
26
  export type FromBtcConfig = FromBtcBaseConfig & {
27
27
  confirmations: number,
@@ -74,7 +74,7 @@ export class FromBtcAbs extends FromBtcBaseSwapHandler<FromBtcSwapAbs, FromBtcSw
74
74
  private getHash(chainIdentifier: string, address: string, amount: bigint): Buffer {
75
75
  const parsedOutputScript = this.bitcoin.toOutputScript(address);
76
76
  const {swapContract} = this.getChain(chainIdentifier);
77
- return swapContract.getHashForOnchain(parsedOutputScript, amount, this.config.confirmations, 0n);
77
+ return swapContract.getHashForOnchain(parsedOutputScript, amount, this.config.confirmations);
78
78
  }
79
79
 
80
80
  /**
@@ -91,13 +91,18 @@ export class FromBtcAbs extends FromBtcBaseSwapHandler<FromBtcSwapAbs, FromBtcSw
91
91
  if(swap.state===FromBtcSwapState.CREATED) {
92
92
  if(!await swapContract.isInitAuthorizationExpired(swap.data, swap)) return false;
93
93
 
94
- const isCommited = await swapContract.isCommited(swap.data);
95
- if(isCommited) {
94
+ const commitState = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
95
+ if(commitState.type===SwapCommitStateType.COMMITED || commitState.type===SwapCommitStateType.REFUNDABLE) {
96
96
  this.swapLogger.info(swap, "processPastSwap(state=CREATED): swap was commited, but processed from watchdog, address: "+swap.address);
97
97
  await swap.setState(FromBtcSwapState.COMMITED);
98
98
  await this.saveSwapData(swap);
99
99
  return false;
100
100
  }
101
+ if(commitState.type===SwapCommitStateType.PAID) {
102
+ this.swapLogger.info(swap, "processPastSwap(state=CREATED): swap was claimed, but processed from watchdog, address: "+swap.address);
103
+ await this.removeSwapData(swap, FromBtcSwapState.CLAIMED);
104
+ return false;
105
+ }
101
106
 
102
107
  this.swapLogger.info(swap, "processPastSwap(state=CREATED): removing past swap due to authorization expiry, address: "+swap.address);
103
108
  await this.removeSwapData(swap, FromBtcSwapState.CANCELED);
@@ -109,11 +114,16 @@ export class FromBtcAbs extends FromBtcBaseSwapHandler<FromBtcSwapAbs, FromBtcSw
109
114
  if(swap.state===FromBtcSwapState.COMMITED) {
110
115
  if(!await swapContract.isExpired(signer.getAddress(), swap.data)) return false;
111
116
 
112
- const isCommited = await swapContract.isCommited(swap.data);
113
- if(isCommited) {
117
+ const commitState = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
118
+ if(commitState.type===SwapCommitStateType.COMMITED || commitState.type===SwapCommitStateType.REFUNDABLE) {
114
119
  this.swapLogger.info(swap, "processPastSwap(state=COMMITED): swap expired, will refund, address: "+swap.address);
115
120
  return true;
116
121
  }
122
+ if(commitState.type===SwapCommitStateType.PAID) {
123
+ this.swapLogger.info(swap, "processPastSwap(state=COMMITED): swap was claimed, but processed from watchdog, address: "+swap.address);
124
+ await this.removeSwapData(swap, FromBtcSwapState.CLAIMED);
125
+ return false;
126
+ }
117
127
 
118
128
  this.swapLogger.warn(swap, "processPastSwap(state=COMMITED): commited swap expired and not committed anymore (already refunded?), address: "+swap.address);
119
129
  await this.removeSwapData(swap, FromBtcSwapState.CANCELED);
@@ -210,11 +220,11 @@ export class FromBtcAbs extends FromBtcBaseSwapHandler<FromBtcSwapAbs, FromBtcSw
210
220
  private async getClaimerBounty(req: Request & {paramReader: IParamReader}, expiry: bigint, signal: AbortSignal): Promise<bigint> {
211
221
  const parsedClaimerBounty = await req.paramReader.getParams({
212
222
  claimerBounty: {
213
- feePerBlock: FieldTypeEnum.BigInt,
214
- safetyFactor: FieldTypeEnum.BigInt,
215
- startTimestamp: FieldTypeEnum.BigInt,
216
- addBlock: FieldTypeEnum.BigInt,
217
- addFee: FieldTypeEnum.BigInt,
223
+ feePerBlock: FieldTypeEnum.BigIntNotNegative,
224
+ safetyFactor: FieldTypeEnum.BigIntNotNegative,
225
+ startTimestamp: FieldTypeEnum.BigIntNotNegative,
226
+ addBlock: FieldTypeEnum.BigIntNotNegative,
227
+ addFee: FieldTypeEnum.BigIntNotNegative,
218
228
  },
219
229
  }).catch(e => null);
220
230
 
@@ -228,6 +238,12 @@ export class FromBtcAbs extends FromBtcBaseSwapHandler<FromBtcSwapAbs, FromBtcSw
228
238
  }
229
239
 
230
240
  const tsDelta: bigint = expiry - parsedClaimerBounty.claimerBounty.startTimestamp;
241
+ if(tsDelta < 0n) {
242
+ throw {
243
+ code: 20043,
244
+ msg: "Invalid claimerBounty (ts delta < 0)"
245
+ };
246
+ }
231
247
  const blocksDelta: bigint = tsDelta / this.config.bitcoinBlocktime * parsedClaimerBounty.claimerBounty.safetyFactor;
232
248
  const totalBlock: bigint = blocksDelta + parsedClaimerBounty.claimerBounty.addBlock;
233
249
  return parsedClaimerBounty.claimerBounty.addFee + (totalBlock * parsedClaimerBounty.claimerBounty.feePerBlock);
@@ -242,7 +258,7 @@ export class FromBtcAbs extends FromBtcBaseSwapHandler<FromBtcSwapAbs, FromBtcSw
242
258
  address,
243
259
  useToken,
244
260
  dummyAmount,
245
- swapContract.getHashForOnchain(randomBytes(32), dummyAmount, 3, null).toString("hex"),
261
+ swapContract.getHashForOnchain(randomBytes(32), dummyAmount, 3).toString("hex"),
246
262
  BigIntBufferUtils.fromBuffer(randomBytes(8)),
247
263
  BigInt(Math.floor(Date.now()/1000)) + this.config.swapTsCsvDelta,
248
264
  false,
@@ -292,11 +308,11 @@ export class FromBtcAbs extends FromBtcBaseSwapHandler<FromBtcSwapAbs, FromBtcSw
292
308
  address: (val: string) => val!=null &&
293
309
  typeof(val)==="string" &&
294
310
  chainInterface.isValidAddress(val, true) ? val : null,
295
- amount: FieldTypeEnum.BigInt,
311
+ amount: FieldTypeEnum.BigIntPositive,
296
312
  token: (val: string) => val!=null &&
297
313
  typeof(val)==="string" &&
298
314
  this.isTokenSupported(chainIdentifier, val) ? val : null,
299
- sequence: FieldTypeEnum.BigInt,
315
+ sequence: FieldTypeEnum.BigIntNotNegative,
300
316
  exactOut: FieldTypeEnum.BooleanOptional
301
317
  });
302
318
  if(parsedBody==null) throw {
@@ -646,7 +646,7 @@ export class FromBtcLnAbs extends FromBtcBaseSwapHandler<FromBtcLnSwapAbs, FromB
646
646
  typeof(val)==="string" &&
647
647
  val.length===64 &&
648
648
  HEX_REGEX.test(val) ? val: null,
649
- amount: FieldTypeEnum.BigInt,
649
+ amount: FieldTypeEnum.BigIntPositive,
650
650
  token: (val: string) => val!=null &&
651
651
  typeof(val)==="string" &&
652
652
  this.isTokenSupported(chainIdentifier, val) ? val : null,
@@ -657,7 +657,7 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
657
657
  typeof(val)==="string" &&
658
658
  val.length===64 &&
659
659
  HEX_REGEX.test(val) ? val: null,
660
- amount: FieldTypeEnum.BigInt,
660
+ amount: FieldTypeEnum.BigIntPositive,
661
661
  token: (val: string) => val!=null &&
662
662
  typeof(val)==="string" &&
663
663
  this.isTokenSupported(chainIdentifier, val) ? val : null,
@@ -666,8 +666,8 @@ export class FromBtcLnAuto extends FromBtcBaseSwapHandler<FromBtcLnAutoSwap, Fro
666
666
  gasToken: (val: string) => val!=null &&
667
667
  typeof(val)==="string" &&
668
668
  chainInterface.isValidToken(val) ? val : null,
669
- gasAmount: FieldTypeEnum.BigInt,
670
- claimerBounty: FieldTypeEnum.BigInt
669
+ gasAmount: FieldTypeEnum.BigIntNotNegative,
670
+ claimerBounty: FieldTypeEnum.BigIntNotNegative
671
671
  });
672
672
  if(parsedBody==null) throw {
673
673
  code: 20100,
@@ -540,7 +540,7 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
540
540
  * @throws {DefinedRuntimeError} will throw an error if the nonce is invalid
541
541
  */
542
542
  private checkNonceValid(nonce: bigint): void {
543
- if(nonce < 0 || nonce >= (2n ** 64n)) throw {
543
+ if(nonce < 1 || nonce >= (2n ** 64n)) throw {
544
544
  code: 20021,
545
545
  msg: "Invalid request body (nonce - cannot be parsed)"
546
546
  };
@@ -561,6 +561,10 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
561
561
  * @throws {DefinedRuntimeError} will throw an error if the confirmationTarget is out of bounds
562
562
  */
563
563
  protected checkConfirmationTarget(confirmationTarget: number): void {
564
+ if(!Number.isFinite(confirmationTarget) || !Number.isSafeInteger(confirmationTarget)) throw {
565
+ code: 20028,
566
+ msg: "Invalid request body (confirmationTarget - not whole number)"
567
+ };
564
568
  if(confirmationTarget>this.config.maxConfTarget) throw {
565
569
  code: 20023,
566
570
  msg: "Invalid request body (confirmationTarget - too high)"
@@ -578,6 +582,10 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
578
582
  * @throws {DefinedRuntimeError} will throw an error if the confirmations are out of bounds
579
583
  */
580
584
  protected checkRequiredConfirmations(confirmations: number): void {
585
+ if(!Number.isFinite(confirmations) || !Number.isSafeInteger(confirmations)) throw {
586
+ code: 20027,
587
+ msg: "Invalid request body (confirmations - not whole number)"
588
+ };
581
589
  if(confirmations>this.config.maxConfirmations) throw {
582
590
  code: 20025,
583
591
  msg: "Invalid request body (confirmations - too high)"
@@ -678,10 +686,10 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
678
686
  */
679
687
  const parsedBody: ToBtcRequestType = await req.paramReader.getParams({
680
688
  address: FieldTypeEnum.String,
681
- amount: FieldTypeEnum.BigInt,
682
- confirmationTarget: FieldTypeEnum.Number,
683
- confirmations: FieldTypeEnum.Number,
684
- nonce: FieldTypeEnum.BigInt,
689
+ amount: FieldTypeEnum.BigIntPositive,
690
+ confirmationTarget: FieldTypeEnum.NumberPositive,
691
+ confirmations: FieldTypeEnum.NumberPositive,
692
+ nonce: FieldTypeEnum.BigIntNotNegative,
685
693
  token: (val: string) => val!=null &&
686
694
  typeof(val)==="string" &&
687
695
  this.isTokenSupported(chainIdentifier, val) ? val : null,
@@ -805,7 +813,8 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
805
813
  data: {
806
814
  amount: amountBD.toString(10),
807
815
  address: signer.getAddress(),
808
- satsPervByte: networkFeeData.satsPerVbyte.toString(10),
816
+ satsPervByte: Math.floor(networkFeeData.satsPerVbyte).toString(10),
817
+ satsPervByteNumber: networkFeeData.satsPerVbyte,
809
818
  networkFee: networkFeeInToken.toString(10),
810
819
  swapFee: swapFeeInToken.toString(10),
811
820
  totalFee: (swapFeeInToken + networkFeeInToken).toString(10),
@@ -11,7 +11,13 @@ import {
11
11
  SwapCommitStateType,
12
12
  SwapData
13
13
  } from "@atomiqlabs/base";
14
- import {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../../utils/Utils";
14
+ import {
15
+ expressHandlerWrapper,
16
+ getAbortController,
17
+ getMinSafeBlockWindowSlow,
18
+ HEX_REGEX,
19
+ isDefinedRuntimeError
20
+ } from "../../../utils/Utils";
15
21
  import {PluginManager} from "../../../plugins/PluginManager";
16
22
  import {IIntermediaryStorage} from "../../../storage/IIntermediaryStorage";
17
23
  import {randomBytes} from "crypto";
@@ -105,6 +111,8 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
105
111
  readonly lightning: ILightningWallet;
106
112
  readonly LightningAssertions: LightningAssertions;
107
113
 
114
+ readonly cltvDeltaLowerBound: bigint;
115
+
108
116
  constructor(
109
117
  storageDirectory: IIntermediaryStorage<ToBtcLnSwapAbs>,
110
118
  path: string,
@@ -123,9 +131,13 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
123
131
  this.config.minLnBaseFee = this.config.minLnBaseFee || 5n;
124
132
  this.config.exactInExpiry = this.config.exactInExpiry || 10*1000;
125
133
  this.config.lnSendBitcoinBlockTimeSafetyFactorPPM = this.config.lnSendBitcoinBlockTimeSafetyFactorPPM ?? (this.config.safetyFactor * 1_000_000n);
126
- if(this.config.lnSendBitcoinBlockTimeSafetyFactorPPM <= 1_100_000n) {
127
- throw new Error("Lightning network send block safety factor set below 1.1, this is insecure!");
134
+ if(this.config.lnSendBitcoinBlockTimeSafetyFactorPPM <= 1_250_000n) {
135
+ throw new Error("Lightning network send block safety factor set below 1.25, this is insecure!");
128
136
  }
137
+
138
+ this.cltvDeltaLowerBound = getMinSafeBlockWindowSlow(
139
+ Number(this.config.lnSendBitcoinBlockTimeSafetyFactorPPM) / 1_000_000
140
+ );
129
141
  }
130
142
 
131
143
  /**
@@ -334,6 +346,10 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
334
346
  const maxFee = swap.quotedNetworkFee;
335
347
  const maxUsableCLTVdelta = (expiryTimestamp - currentTimestamp - this.config.gracePeriod)
336
348
  / (this.config.bitcoinBlocktime * this.config.lnSendBitcoinBlockTimeSafetyFactorPPM / 1_000_000n);
349
+ if(maxUsableCLTVdelta < this.cltvDeltaLowerBound) throw {
350
+ code: 90008,
351
+ msg: "Calculated CLTV delta is too low for current safety factor!"
352
+ }
337
353
 
338
354
  //Initiate payment
339
355
  this.swapLogger.info(swap, "sendLightningPayment(): paying lightning network invoice,"+
@@ -591,6 +607,10 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
591
607
  }> {
592
608
  const maxUsableCLTV: bigint = (expiryTimestamp - currentTimestamp - this.config.gracePeriod)
593
609
  / (this.config.bitcoinBlocktime * this.config.lnSendBitcoinBlockTimeSafetyFactorPPM / 1_000_000n);
610
+ if(maxUsableCLTV < this.cltvDeltaLowerBound) throw {
611
+ code: 20002,
612
+ msg: "Cannot route the payment (calculated CLTV delta too short - increase timeout)!"
613
+ };
594
614
 
595
615
  const blockHeight = await this.lightning.getBlockheight();
596
616
  abortSignal.throwIfAborted();
@@ -863,8 +883,8 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
863
883
  */
864
884
  const parsedBody: ToBtcLnRequestType = await req.paramReader.getParams({
865
885
  pr: FieldTypeEnum.String,
866
- maxFee: FieldTypeEnum.BigInt,
867
- expiryTimestamp: FieldTypeEnum.BigInt,
886
+ maxFee: FieldTypeEnum.BigIntPositive,
887
+ expiryTimestamp: FieldTypeEnum.BigIntPositive,
868
888
  token: (val: string) => val!=null &&
869
889
  typeof(val)==="string" &&
870
890
  this.isTokenSupported(chainIdentifier, val) ? val : null,
@@ -872,7 +892,7 @@ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwap
872
892
  typeof(val)==="string" &&
873
893
  chainInterface.isValidAddress(val, true) ? val : null,
874
894
  exactIn: FieldTypeEnum.BooleanOptional,
875
- amount: FieldTypeEnum.BigIntOptional
895
+ amount: FieldTypeEnum.BigIntPositiveOptional
876
896
  });
877
897
  if (parsedBody==null) {
878
898
  throw {
@@ -35,7 +35,7 @@ import {FromBtcAmountAssertions} from "../assertions/FromBtcAmountAssertions";
35
35
  import {randomBytes} from "crypto";
36
36
  import {Transaction} from "@scure/btc-signer";
37
37
  import {SpvVaults, VAULT_DUST_AMOUNT} from "./SpvVaults";
38
- import {isLegacyInput} from "../../utils/BitcoinUtils";
38
+ import {checkTransactionReplaced, isLegacyInput} from "../../utils/BitcoinUtils";
39
39
  import {AmountAssertions} from "../assertions/AmountAssertions";
40
40
  import {isQuoteThrow} from "../../plugins/IPlugin";
41
41
  import {StickyAddress} from "./StickyAddress";
@@ -263,12 +263,20 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
263
263
  const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
264
264
  const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
265
265
  let tx = foundWithdrawal?.btcTx;
266
- if(tx==null) tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
266
+ if(tx==null) {
267
+ tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
268
+ } else {
269
+ tx = await checkTransactionReplaced(tx.txid, tx.raw, this.bitcoinRpc);
270
+ }
267
271
 
268
272
  if(tx==null) {
269
273
  await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
274
+ if(foundWithdrawal!=null) {
275
+ vault.removeWithdrawal(foundWithdrawal);
276
+ await this.Vaults.saveVault(vault);
277
+ }
270
278
  return;
271
- } else if(tx.confirmations===0) {
279
+ } else if(tx.confirmations==null || tx.confirmations===0) {
272
280
  await swap.setState(SpvVaultSwapState.SENT)
273
281
  await this.saveSwapData(swap);
274
282
  return;
@@ -420,11 +428,11 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
420
428
  * frontingFeeRate: string Fronting fee (in output token) to assign to the swap
421
429
  */
422
430
  const actualParsedBody = await req.paramReader.getParams({
423
- amount: FieldTypeEnum.BigInt,
424
- gasAmount: FieldTypeEnum.BigInt,
431
+ amount: FieldTypeEnum.BigIntPositive,
432
+ gasAmount: FieldTypeEnum.BigIntNotNegative,
425
433
  exactOut: FieldTypeEnum.BooleanOptional,
426
- callerFeeRate: FieldTypeEnum.BigInt,
427
- frontingFeeRate: FieldTypeEnum.BigInt,
434
+ callerFeeRate: FieldTypeEnum.BigIntNotNegative,
435
+ frontingFeeRate: FieldTypeEnum.BigIntNotNegative,
428
436
  });
429
437
  abortController.signal.throwIfAborted();
430
438
  if(actualParsedBody==null) throw {
@@ -434,7 +442,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
434
442
 
435
443
  const inputAmountAdjustments = req.paramReader.getExistingParamsOrNull({
436
444
  amountUtxos: FieldTypeEnum.AnyOptional,
437
- amountFeeRate: FieldTypeEnum.NumberOptional
445
+ amountFeeRate: FieldTypeEnum.NumberPositiveOptional
438
446
  });
439
447
  if(inputAmountAdjustments==null) throw {
440
448
  code: 20100,
@@ -811,27 +819,29 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
811
819
  };
812
820
  }
813
821
 
822
+ //Double-check the state to prevent race condition
823
+ if(swap.state!==SpvVaultSwapState.CREATED) throw {
824
+ code: 20505,
825
+ msg: "Invalid quote ID, not found or expired!"
826
+ }; //Continues here is only synchronous code until the state change to SIGNED
827
+
814
828
  const unlock = swap.lock(120);
815
829
  if(!unlock) throw {
816
830
  code: 20517,
817
831
  msg: "Bitcoin transaction submission already in progress, please retry later!"
818
832
  };
819
833
 
834
+ const txId = signedTx.id;
835
+
820
836
  let swapSendingSet = false;
821
837
  let dataSendingSet = false;
822
838
  try {
823
839
  const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
824
840
 
825
- //Double-check the state to prevent race condition
826
- if(swap.state!==SpvVaultSwapState.CREATED) throw {
827
- code: 20505,
828
- msg: "Invalid quote ID, not found or expired!"
829
- };
830
-
831
841
  //Double check in-flight swap count
832
842
  this.checkTooManyInflightSwaps();
833
843
 
834
- swap.btcTxId = signedTx.id;
844
+ swap.btcTxId = txId;
835
845
  swap.state = SpvVaultSwapState.SIGNED;
836
846
  swap.sending = true;
837
847
  swapSendingSet = true;
@@ -843,7 +853,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
843
853
  dataSendingSet = true;
844
854
  await this.Vaults.saveVault(vault);
845
855
 
846
- this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
856
+ this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+txId);
847
857
 
848
858
  try {
849
859
  await this.bitcoin.sendRawTransaction(btcRawTx);
@@ -859,22 +869,40 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
859
869
  }
860
870
  } catch (e) {
861
871
  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
- }
867
-
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);
872
+ if(dataSendingSet) (data as any).sending = false;
873
+
874
+ //We only make the swap failed if the error happened in CREATED or SIGNED states
875
+ if(swap.state===SpvVaultSwapState.CREATED || swap.state===SpvVaultSwapState.SIGNED) {
876
+ try {
877
+ const fetchedBtcTx = await this.bitcoin.getWalletTransaction(txId);
878
+ if(fetchedBtcTx==null) {
879
+ if(dataSendingSet) {
880
+ vault.removeWithdrawal(data);
881
+ await this.Vaults.saveVault(vault);
882
+ }
883
+ if(isDefinedRuntimeError(e) && swap.metadata!=null) swap.metadata.postQuoteError = e;
884
+ await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
885
+ throw e; //This will get caught locally and throw the post quote error
886
+ } else {
887
+ //Transaction not-null set state to sent and fall through without throwing
888
+ await swap.setState(SpvVaultSwapState.SENT);
889
+ }
890
+ } catch (getTxErr) {
891
+ if(getTxErr===e) throw e;
892
+ //Cannot determine whether the bitcoin transaction was broadcasted or not
893
+ // do nothing here and let the watchdog handle this!
894
+ throw {
895
+ _httpStatus: 200,
896
+ code: 20001,
897
+ msg: "Transaction status unknown",
898
+ data: {
899
+ txId: swap.btcTxId
900
+ }
901
+ };
874
902
  }
903
+ } else {
904
+ throw e;
875
905
  }
876
-
877
- throw e;
878
906
  } finally {
879
907
  unlock();
880
908
  }
@@ -260,13 +260,9 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
260
260
  }).catch(e => this.swapLogger.error(invoiceData, "htlcReceived(): Error sending transfer txns", e));
261
261
 
262
262
  if(result==null) {
263
- //Cancel invoice
264
- await invoiceData.setState(FromBtcLnTrustedSwapState.REFUNDED);
265
- await this.storageManager.saveData(invoice.id, null, invoiceData);
266
- await this.lightning.cancelHodlInvoice(invoice.id);
267
- this.unsubscribeInvoice(invoice.id);
268
- await this.removeSwapData(invoiceData);
269
- this.swapLogger.info(invoiceData, "htlcReceived(): transaction sending failed, refunding lightning: ", invoiceData.pr);
263
+ this.swapLogger.info(invoiceData, "htlcReceived(): transaction sending failed: ", invoiceData.pr);
264
+ //Let the swap watchdog handle this case
265
+ unlock();
270
266
  throw {
271
267
  code: 20002,
272
268
  msg: "Transaction sending failed"
@@ -285,14 +281,8 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
285
281
  if(invoiceData.isLocked()) return;
286
282
 
287
283
  const txStatus = await chainInterface.getTxStatus(invoiceData.scRawTx);
288
- if(txStatus==="not_found") {
289
- //Retry
290
- invoiceData.txIds = {init: null};
291
- invoiceData.scRawTx = null;
292
- await invoiceData.setState(FromBtcLnTrustedSwapState.RECEIVED);
293
- await this.storageManager.saveData(invoice.id, null, invoiceData);
294
- }
295
- if(txStatus==="reverted") {
284
+
285
+ if(txStatus==="reverted" || txStatus==="not_found") {
296
286
  //Cancel invoice
297
287
  await invoiceData.setState(FromBtcLnTrustedSwapState.REFUNDED);
298
288
  await this.storageManager.saveData(invoice.id, null, invoiceData);