@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.
- package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +29 -13
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +1 -1
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +3 -3
- package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +17 -6
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +1 -0
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +16 -5
- package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +60 -27
- package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +4 -15
- package/dist/utils/paramcoders/ParamDecoder.js +4 -86
- package/dist/utils/paramcoders/SchemaVerifier.d.ts +11 -2
- package/dist/utils/paramcoders/SchemaVerifier.js +93 -47
- package/package.json +1 -1
- package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +30 -14
- package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +1 -1
- package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +3 -3
- package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +15 -6
- package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +26 -6
- package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +58 -30
- package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +5 -15
- package/src/utils/paramcoders/ParamDecoder.ts +10 -66
- package/src/utils/paramcoders/SchemaVerifier.ts +80 -37
|
@@ -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
|
-
|
|
36
|
-
|
|
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
|
@@ -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
|
|
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
|
|
95
|
-
if(
|
|
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
|
|
113
|
-
if(
|
|
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.
|
|
214
|
-
safetyFactor: FieldTypeEnum.
|
|
215
|
-
startTimestamp: FieldTypeEnum.
|
|
216
|
-
addBlock: FieldTypeEnum.
|
|
217
|
-
addFee: FieldTypeEnum.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
670
|
-
claimerBounty: FieldTypeEnum.
|
|
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 <
|
|
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.
|
|
682
|
-
confirmationTarget: FieldTypeEnum.
|
|
683
|
-
confirmations: FieldTypeEnum.
|
|
684
|
-
nonce: FieldTypeEnum.
|
|
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 {
|
|
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 <=
|
|
127
|
-
throw new Error("Lightning network send block safety factor set below 1.
|
|
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.
|
|
867
|
-
expiryTimestamp: FieldTypeEnum.
|
|
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.
|
|
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)
|
|
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.
|
|
424
|
-
gasAmount: FieldTypeEnum.
|
|
431
|
+
amount: FieldTypeEnum.BigIntPositive,
|
|
432
|
+
gasAmount: FieldTypeEnum.BigIntNotNegative,
|
|
425
433
|
exactOut: FieldTypeEnum.BooleanOptional,
|
|
426
|
-
callerFeeRate: FieldTypeEnum.
|
|
427
|
-
frontingFeeRate: FieldTypeEnum.
|
|
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.
|
|
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 =
|
|
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: "+
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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);
|