@atomiqlabs/lp-lib 17.5.3 → 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/assertions/FromBtcAmountAssertions.js +2 -2
- package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +45 -19
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +2 -2
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +35 -25
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +2 -2
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +33 -22
- package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +1 -1
- package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +29 -11
- package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +2 -2
- package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +1 -1
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +1 -0
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +54 -9
- package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +83 -30
- package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +2 -2
- package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +11 -17
- package/dist/utils/Utils.d.ts +33 -0
- package/dist/utils/Utils.js +51 -1
- 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/assertions/FromBtcAmountAssertions.ts +2 -2
- package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +44 -19
- package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +47 -30
- package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +45 -27
- package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +28 -13
- package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +4 -4
- package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +63 -13
- package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +79 -33
- package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +25 -2
- package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +10 -16
- package/src/utils/Utils.ts +48 -0
- package/src/utils/paramcoders/ParamDecoder.ts +10 -66
- package/src/utils/paramcoders/SchemaVerifier.ts +80 -37
|
@@ -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;
|
|
@@ -311,7 +319,8 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
311
319
|
]);
|
|
312
320
|
|
|
313
321
|
for(let {obj: swap} of swaps) {
|
|
314
|
-
await this.processPastSwap(swap)
|
|
322
|
+
await this.processPastSwap(swap)
|
|
323
|
+
.catch(e => this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e));
|
|
315
324
|
}
|
|
316
325
|
}
|
|
317
326
|
|
|
@@ -419,11 +428,11 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
419
428
|
* frontingFeeRate: string Fronting fee (in output token) to assign to the swap
|
|
420
429
|
*/
|
|
421
430
|
const actualParsedBody = await req.paramReader.getParams({
|
|
422
|
-
amount: FieldTypeEnum.
|
|
423
|
-
gasAmount: FieldTypeEnum.
|
|
431
|
+
amount: FieldTypeEnum.BigIntPositive,
|
|
432
|
+
gasAmount: FieldTypeEnum.BigIntNotNegative,
|
|
424
433
|
exactOut: FieldTypeEnum.BooleanOptional,
|
|
425
|
-
callerFeeRate: FieldTypeEnum.
|
|
426
|
-
frontingFeeRate: FieldTypeEnum.
|
|
434
|
+
callerFeeRate: FieldTypeEnum.BigIntNotNegative,
|
|
435
|
+
frontingFeeRate: FieldTypeEnum.BigIntNotNegative,
|
|
427
436
|
});
|
|
428
437
|
abortController.signal.throwIfAborted();
|
|
429
438
|
if(actualParsedBody==null) throw {
|
|
@@ -433,7 +442,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
433
442
|
|
|
434
443
|
const inputAmountAdjustments = req.paramReader.getExistingParamsOrNull({
|
|
435
444
|
amountUtxos: FieldTypeEnum.AnyOptional,
|
|
436
|
-
amountFeeRate: FieldTypeEnum.
|
|
445
|
+
amountFeeRate: FieldTypeEnum.NumberPositiveOptional
|
|
437
446
|
});
|
|
438
447
|
if(inputAmountAdjustments==null) throw {
|
|
439
448
|
code: 20100,
|
|
@@ -794,13 +803,12 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
794
803
|
swap
|
|
795
804
|
);
|
|
796
805
|
if(isQuoteThrow(pluginCheckResult)) {
|
|
797
|
-
|
|
806
|
+
if(swap.state===SpvVaultSwapState.CREATED)
|
|
807
|
+
await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
|
|
808
|
+
throw {
|
|
798
809
|
code: 29999,
|
|
799
810
|
msg: pluginCheckResult.message
|
|
800
|
-
}
|
|
801
|
-
if(swap.metadata!=null) swap.metadata.postQuoteError = error;
|
|
802
|
-
await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
|
|
803
|
-
throw error;
|
|
811
|
+
};
|
|
804
812
|
}
|
|
805
813
|
|
|
806
814
|
await this.Vaults.checkVaultReplacedTransactions(vault, true);
|
|
@@ -811,31 +819,41 @@ 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
|
+
|
|
828
|
+
const unlock = swap.lock(120);
|
|
829
|
+
if(!unlock) throw {
|
|
830
|
+
code: 20517,
|
|
831
|
+
msg: "Bitcoin transaction submission already in progress, please retry later!"
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const txId = signedTx.id;
|
|
835
|
+
|
|
836
|
+
let swapSendingSet = false;
|
|
837
|
+
let dataSendingSet = false;
|
|
814
838
|
try {
|
|
815
839
|
const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
|
|
816
840
|
|
|
817
|
-
//Double-check the state to prevent race condition
|
|
818
|
-
if(swap.state!==SpvVaultSwapState.CREATED) {
|
|
819
|
-
throw {
|
|
820
|
-
code: 20505,
|
|
821
|
-
msg: "Invalid quote ID, not found or expired!"
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
|
|
825
841
|
//Double check in-flight swap count
|
|
826
842
|
this.checkTooManyInflightSwaps();
|
|
827
843
|
|
|
828
|
-
swap.btcTxId =
|
|
844
|
+
swap.btcTxId = txId;
|
|
829
845
|
swap.state = SpvVaultSwapState.SIGNED;
|
|
830
846
|
swap.sending = true;
|
|
847
|
+
swapSendingSet = true;
|
|
831
848
|
await this.saveSwapData(swap);
|
|
832
849
|
|
|
833
850
|
data.btcTx.raw = btcRawTx;
|
|
834
851
|
(data as any).sending = true;
|
|
835
852
|
vault.addWithdrawal(data);
|
|
853
|
+
dataSendingSet = true;
|
|
836
854
|
await this.Vaults.saveVault(vault);
|
|
837
855
|
|
|
838
|
-
this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+
|
|
856
|
+
this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+txId);
|
|
839
857
|
|
|
840
858
|
try {
|
|
841
859
|
await this.bitcoin.sendRawTransaction(btcRawTx);
|
|
@@ -850,15 +868,43 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
850
868
|
};
|
|
851
869
|
}
|
|
852
870
|
} catch (e) {
|
|
853
|
-
(
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
871
|
+
if(swapSendingSet) swap.sending = false;
|
|
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
|
+
};
|
|
902
|
+
}
|
|
903
|
+
} else {
|
|
904
|
+
throw e;
|
|
905
|
+
}
|
|
906
|
+
} finally {
|
|
907
|
+
unlock();
|
|
862
908
|
}
|
|
863
909
|
|
|
864
910
|
await responseStream.writeParamsAndEnd({
|
|
@@ -13,6 +13,29 @@ import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
|
|
|
13
13
|
import {FromBtcAmountAssertions} from "../../assertions/FromBtcAmountAssertions";
|
|
14
14
|
import {isQuoteThrow} from "../../../plugins/IPlugin";
|
|
15
15
|
|
|
16
|
+
/*
|
|
17
|
+
TODO: IN CASE THIS EVER GETS USED, FIX THE FOLLOWING:
|
|
18
|
+
|
|
19
|
+
HIGH 5 — FromBtcTrusted (0-conf): no RBF check, fee gate is min not max, and the burn defense only fires when it shouldn't
|
|
20
|
+
|
|
21
|
+
Handler is opt-in (ONCHAIN_TRUSTED, not in shipped configs), but if enabled:
|
|
22
|
+
|
|
23
|
+
• Acceptance (FromBtcTrusted.js:186-194) checks only: direct parents confirmed + fee threshold. Nothing inspects tx.ins[].sequence for BIP-125 (grep: only storage-sequence hits). A double-spendable opt-in-RBF tx is
|
|
24
|
+
accepted and tokens are sent within seconds.
|
|
25
|
+
• The fee gate is fee >= recommendedFee || fee >= currentRate (line 187) — the weaker of the two wins; the documented intent is max(...). The runner even overrides the library's 1.25 multiplier to 1
|
|
26
|
+
(IntermediaryRunner.ts FromBtcTrusted section). A stale low fee maximizes the replacement window and minimizes the attacker's BIP-125 cost.
|
|
27
|
+
• The burn defense (checkDoubleSpends → burn() → sendRawPackage([tx1, burnChild])) is rejected by any node whose mempool already holds the replacement — so it structurally loses against a broadcast double-spend, and
|
|
28
|
+
with mempoolfullrbf (default since Core 28) even non-signaling txs are replaceable.
|
|
29
|
+
• Worse, checkDoubleSpends (lines 636-644) treats getTransaction == null as "double-spent" — but null also means mempool eviction (fee spike, node restart). With no conflict anywhere, the burn package is valid and
|
|
30
|
+
burns the entire deposit to miners — after tokens were already sent, that's a full LP loss with no attacker at all.
|
|
31
|
+
|
|
32
|
+
MEDIUM 6 — FromBtcTrusted /setRefundAddress is dead code; refundable user funds strand permanently
|
|
33
|
+
|
|
34
|
+
FromBtcTrusted.js:606 looks up getData(paymentHash, null); IntermediaryStorageManager.getData (line 64-66) converts null → sequence 0x0, but every swap is stored with a random 64-bit sequence — so the lookup always
|
|
35
|
+
misses and the endpoint always throws 10001. (The status endpoint at line 515 does it correctly.) Any swap that becomes REFUNDABLE (over-max payment, plugin veto, chain revert, balance failure) without a refund
|
|
36
|
+
address set at creation can never be refunded (refundSwap early-returns without one). User funds sit in the LP wallet forever. One-line fix: pass parsedBody.sequence.
|
|
37
|
+
*/
|
|
38
|
+
|
|
16
39
|
export type FromBtcTrustedConfig = SwapBaseConfig & {
|
|
17
40
|
doubleSpendCheckInterval: number,
|
|
18
41
|
swapAddressExpiry: number,
|
|
@@ -298,7 +321,7 @@ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrust
|
|
|
298
321
|
|
|
299
322
|
const txns = await chainInterface.txsTransfer(signer.getAddress(), swap.token, swap.adjustedOutput, swap.dstAddress);
|
|
300
323
|
|
|
301
|
-
let unlock = swap.lock(30
|
|
324
|
+
let unlock = swap.lock(30);
|
|
302
325
|
if(unlock==null) return;
|
|
303
326
|
|
|
304
327
|
const pluginCheckResult = await PluginManager.onHandlePreFromBtcExecute(
|
|
@@ -399,7 +422,7 @@ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrust
|
|
|
399
422
|
try {
|
|
400
423
|
await this.processPastSwap(swap, txs[0]?.tx, txs[0]?.vout);
|
|
401
424
|
} catch (e) {
|
|
402
|
-
this.swapLogger.error(swap, "
|
|
425
|
+
this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e);
|
|
403
426
|
}
|
|
404
427
|
}
|
|
405
428
|
}
|
|
@@ -171,7 +171,11 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
|
|
|
171
171
|
]);
|
|
172
172
|
|
|
173
173
|
for(let {obj: swap} of queriedData) {
|
|
174
|
-
|
|
174
|
+
try {
|
|
175
|
+
if(await this.processPastSwap(swap)) cancelInvoices.push(swap);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
this.swapLogger.error(swap, "processPastSwap(): Error executing watchdog function: ", e);
|
|
178
|
+
}
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
await this.cancelInvoices(cancelInvoices);
|
|
@@ -256,13 +260,9 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
|
|
|
256
260
|
}).catch(e => this.swapLogger.error(invoiceData, "htlcReceived(): Error sending transfer txns", e));
|
|
257
261
|
|
|
258
262
|
if(result==null) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
await this.lightning.cancelHodlInvoice(invoice.id);
|
|
263
|
-
this.unsubscribeInvoice(invoice.id);
|
|
264
|
-
await this.removeSwapData(invoiceData);
|
|
265
|
-
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();
|
|
266
266
|
throw {
|
|
267
267
|
code: 20002,
|
|
268
268
|
msg: "Transaction sending failed"
|
|
@@ -281,14 +281,8 @@ export class FromBtcLnTrusted extends SwapHandler<FromBtcLnTrustedSwap, FromBtcL
|
|
|
281
281
|
if(invoiceData.isLocked()) return;
|
|
282
282
|
|
|
283
283
|
const txStatus = await chainInterface.getTxStatus(invoiceData.scRawTx);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
invoiceData.txIds = {init: null};
|
|
287
|
-
invoiceData.scRawTx = null;
|
|
288
|
-
await invoiceData.setState(FromBtcLnTrustedSwapState.RECEIVED);
|
|
289
|
-
await this.storageManager.saveData(invoice.id, null, invoiceData);
|
|
290
|
-
}
|
|
291
|
-
if(txStatus==="reverted") {
|
|
284
|
+
|
|
285
|
+
if(txStatus==="reverted" || txStatus==="not_found") {
|
|
292
286
|
//Cancel invoice
|
|
293
287
|
await invoiceData.setState(FromBtcLnTrustedSwapState.REFUNDED);
|
|
294
288
|
await this.storageManager.saveData(invoice.id, null, invoiceData);
|
package/src/utils/Utils.ts
CHANGED
|
@@ -150,3 +150,51 @@ export function parsePsbt(btcTx: Transaction): BtcTx {
|
|
|
150
150
|
})
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns the minimum Bitcoin block window (in blocks) for which a "fast blocks" safety
|
|
156
|
+
* factor is genuinely usable: the probability that N consecutive blocks are produced
|
|
157
|
+
* with an average interval below (blocktime / safetyFactor) is less than `probability`.
|
|
158
|
+
*
|
|
159
|
+
* Model: block arrivals are a Poisson process (i.i.d. exponential intervals), so the
|
|
160
|
+
* production time of N blocks is Erlang/Gamma(shape=N). A Chernoff (Cramer) bound on
|
|
161
|
+
* the lower tail gives the per-block large-deviation rate I(S) = ln(S) + 1/S - 1, i.e.
|
|
162
|
+
* P(N blocks faster than blocktime/S on average) <= exp(-N * I(S))
|
|
163
|
+
* which this function inverts for N. Conservative: the exact Erlang CDF already
|
|
164
|
+
* satisfies the bound ~25% earlier. Use for the fast-block tail (e.g. incoming LN
|
|
165
|
+
* HTLC expiry racing an escrow claim window).
|
|
166
|
+
*
|
|
167
|
+
* @param {number|bigint} safetyFactor Assumed max block-speed multiplier (must be > 1)
|
|
168
|
+
* @param {number} [probability=0.0001] Tolerated failure probability (default 0.01%)
|
|
169
|
+
* @returns {number} Minimum safe block window in blocks
|
|
170
|
+
*/
|
|
171
|
+
export function getMinSafeBlockWindowFast(safetyFactor: number | bigint, probability: number = 0.0001): bigint {
|
|
172
|
+
const S = Number(safetyFactor);
|
|
173
|
+
const rate = Math.log(S) + 1/S - 1;
|
|
174
|
+
if (!(rate > 0)) throw new RangeError("safetyFactor must be > 1");
|
|
175
|
+
return BigInt(Math.ceil(Math.log(1/probability) / rate));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Slow-tail mirror of getMinSafeBlockWindowFast(): returns the minimum block window
|
|
180
|
+
* (in blocks) for which a "slow blocks" safety factor is genuinely usable: the
|
|
181
|
+
* probability that N consecutive blocks take LONGER than N * blocktime * safetyFactor
|
|
182
|
+
* is less than `probability`. Chernoff bound on the upper Erlang tail gives the rate
|
|
183
|
+
* I(S) = S - 1 - ln(S). Use when converting a wall-clock deadline into a block count
|
|
184
|
+
* (e.g. payout/claim timeouts that must expire before some real-world time even if
|
|
185
|
+
* blocks come slowly).
|
|
186
|
+
*
|
|
187
|
+
* @param {number|bigint} safetyFactor Assumed max block-slowness multiplier (must be > 1)
|
|
188
|
+
* @param {number} [probability=0.0001] Tolerated failure probability (default 0.01%)
|
|
189
|
+
* @returns {number} Minimum safe block window in blocks
|
|
190
|
+
*/
|
|
191
|
+
export function getMinSafeBlockWindowSlow(safetyFactor: bigint | number, probability: number = 0.0001): bigint {
|
|
192
|
+
const S = Number(safetyFactor);
|
|
193
|
+
const rate = S - 1 - Math.log(S);
|
|
194
|
+
if (!(rate > 0)) throw new RangeError("safetyFactor must be > 1");
|
|
195
|
+
return BigInt(Math.ceil(Math.log(1/probability) / rate));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function bigIntMax(a: bigint, b: bigint): bigint {
|
|
199
|
+
return a > b ? a : b;
|
|
200
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
FieldTypeEnum,
|
|
3
|
+
parseBigInt,
|
|
4
|
+
RequestSchema,
|
|
5
|
+
RequestSchemaResult,
|
|
6
|
+
verifySchema,
|
|
7
|
+
verifySchemaField
|
|
8
|
+
} from "./SchemaVerifier";
|
|
2
9
|
import {IParamReader} from "./IParamReader";
|
|
3
10
|
|
|
4
11
|
|
|
@@ -133,41 +140,7 @@ export class ParamDecoder implements IParamReader {
|
|
|
133
140
|
const resultSchema: any = {};
|
|
134
141
|
for(let fieldName in schema) {
|
|
135
142
|
const val: any = await this.getParam(fieldName);
|
|
136
|
-
|
|
137
|
-
if(typeof(type)==="function") {
|
|
138
|
-
const result = type(val);
|
|
139
|
-
if(result==null) return null;
|
|
140
|
-
resultSchema[fieldName] = result;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if(val==null && (type as number)>=100) {
|
|
145
|
-
resultSchema[fieldName] = null;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if(type===FieldTypeEnum.Any || type===FieldTypeEnum.AnyOptional) {
|
|
150
|
-
resultSchema[fieldName] = val;
|
|
151
|
-
} else if(type===FieldTypeEnum.Boolean || type===FieldTypeEnum.BooleanOptional) {
|
|
152
|
-
if(typeof(val)!=="boolean") return null;
|
|
153
|
-
resultSchema[fieldName] = val;
|
|
154
|
-
} else if(type===FieldTypeEnum.Number || type===FieldTypeEnum.NumberOptional) {
|
|
155
|
-
if(typeof(val)!=="number") return null;
|
|
156
|
-
if(isNaN(val as number)) return null;
|
|
157
|
-
resultSchema[fieldName] = val;
|
|
158
|
-
} else if(type===FieldTypeEnum.BigInt || type===FieldTypeEnum.BigIntOptional) {
|
|
159
|
-
const result = parseBigInt(val);
|
|
160
|
-
if(result==null) return null;
|
|
161
|
-
resultSchema[fieldName] = result;
|
|
162
|
-
} else if(type===FieldTypeEnum.String || type===FieldTypeEnum.StringOptional) {
|
|
163
|
-
if(typeof(val)!=="string") return null;
|
|
164
|
-
resultSchema[fieldName] = val;
|
|
165
|
-
} else {
|
|
166
|
-
//Probably another request schema
|
|
167
|
-
const result = verifySchema(val, type as RequestSchema);
|
|
168
|
-
if(result==null) return null;
|
|
169
|
-
resultSchema[fieldName] = result;
|
|
170
|
-
}
|
|
143
|
+
if(!verifySchemaField(val, schema[fieldName], fieldName, resultSchema)) return null;
|
|
171
144
|
}
|
|
172
145
|
return resultSchema;
|
|
173
146
|
}
|
|
@@ -182,36 +155,7 @@ export class ParamDecoder implements IParamReader {
|
|
|
182
155
|
continue;
|
|
183
156
|
}
|
|
184
157
|
|
|
185
|
-
|
|
186
|
-
if(typeof(type)==="function") {
|
|
187
|
-
const result = type(val);
|
|
188
|
-
if(result==null) return null;
|
|
189
|
-
resultSchema[fieldName] = result;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if(type===FieldTypeEnum.Any || type===FieldTypeEnum.AnyOptional) {
|
|
194
|
-
resultSchema[fieldName] = val;
|
|
195
|
-
} else if(type===FieldTypeEnum.Boolean || type===FieldTypeEnum.BooleanOptional) {
|
|
196
|
-
if(typeof(val)!=="boolean") return null;
|
|
197
|
-
resultSchema[fieldName] = val;
|
|
198
|
-
} else if(type===FieldTypeEnum.Number || type===FieldTypeEnum.NumberOptional) {
|
|
199
|
-
if(typeof(val)!=="number") return null;
|
|
200
|
-
if(isNaN(val as number)) return null;
|
|
201
|
-
resultSchema[fieldName] = val;
|
|
202
|
-
} else if(type===FieldTypeEnum.BigInt || type===FieldTypeEnum.BigIntOptional) {
|
|
203
|
-
const result = parseBigInt(val);
|
|
204
|
-
if(result==null) return null;
|
|
205
|
-
resultSchema[fieldName] = result;
|
|
206
|
-
} else if(type===FieldTypeEnum.String || type===FieldTypeEnum.StringOptional) {
|
|
207
|
-
if(typeof(val)!=="string") return null;
|
|
208
|
-
resultSchema[fieldName] = val;
|
|
209
|
-
} else {
|
|
210
|
-
//Probably another request schema
|
|
211
|
-
const result = verifySchema(val, type as RequestSchema);
|
|
212
|
-
if(result==null) return null;
|
|
213
|
-
resultSchema[fieldName] = result;
|
|
214
|
-
}
|
|
158
|
+
if(!verifySchemaField(val, schema[fieldName], fieldName, resultSchema)) return null;
|
|
215
159
|
}
|
|
216
160
|
return resultSchema;
|
|
217
161
|
}
|
|
@@ -15,25 +15,41 @@ export enum FieldTypeEnum {
|
|
|
15
15
|
Number=2,
|
|
16
16
|
BigInt=3,
|
|
17
17
|
Any=4,
|
|
18
|
+
BigIntPositive=5,
|
|
19
|
+
BigIntNotNegative=6,
|
|
20
|
+
NumberPositive=7,
|
|
21
|
+
NumberNotNegative=8,
|
|
18
22
|
|
|
19
23
|
StringOptional=100,
|
|
20
24
|
BooleanOptional=101,
|
|
21
25
|
NumberOptional=102,
|
|
22
26
|
BigIntOptional=103,
|
|
23
27
|
AnyOptional=104,
|
|
28
|
+
BigIntPositiveOptional=105,
|
|
29
|
+
BigIntNotNegativeOptional=106,
|
|
30
|
+
NumberPositiveOptional=107,
|
|
31
|
+
NumberNotNegativeOptional=108,
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export type FieldType<T extends FieldTypeEnum | RequestSchema | ((val: any) => (string | boolean | number | bigint | any))> =
|
|
27
35
|
T extends FieldTypeEnum.String ? string :
|
|
28
36
|
T extends FieldTypeEnum.Boolean ? boolean :
|
|
29
37
|
T extends FieldTypeEnum.Number ? number :
|
|
38
|
+
T extends FieldTypeEnum.NumberPositive ? number :
|
|
39
|
+
T extends FieldTypeEnum.NumberNotNegative ? number :
|
|
30
40
|
T extends FieldTypeEnum.BigInt ? bigint :
|
|
31
41
|
T extends FieldTypeEnum.Any ? any :
|
|
42
|
+
T extends FieldTypeEnum.BigIntPositive ? bigint :
|
|
43
|
+
T extends FieldTypeEnum.BigIntNotNegative ? bigint :
|
|
32
44
|
T extends FieldTypeEnum.StringOptional ? string :
|
|
33
45
|
T extends FieldTypeEnum.BooleanOptional ? boolean :
|
|
34
46
|
T extends FieldTypeEnum.NumberOptional ? number :
|
|
47
|
+
T extends FieldTypeEnum.NumberPositiveOptional ? number :
|
|
48
|
+
T extends FieldTypeEnum.NumberNotNegativeOptional ? number :
|
|
35
49
|
T extends FieldTypeEnum.BigIntOptional ? bigint :
|
|
36
50
|
T extends FieldTypeEnum.AnyOptional ? any :
|
|
51
|
+
T extends FieldTypeEnum.BigIntPositiveOptional ? bigint :
|
|
52
|
+
T extends FieldTypeEnum.BigIntNotNegativeOptional ? bigint :
|
|
37
53
|
T extends RequestSchema ? RequestSchemaResult<T> :
|
|
38
54
|
T extends ((val: any) => string) ? string :
|
|
39
55
|
T extends ((val: any) => boolean) ? boolean :
|
|
@@ -50,47 +66,74 @@ export type RequestSchema = {
|
|
|
50
66
|
[fieldName: string]: FieldTypeEnum | RequestSchema | ((val: any) => any)
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
export function verifySchemaField(
|
|
70
|
+
val: any,
|
|
71
|
+
type: FieldTypeEnum | RequestSchema | ((val: any) => any),
|
|
72
|
+
fieldName: string,
|
|
73
|
+
resultSchema: any
|
|
74
|
+
): boolean {
|
|
75
|
+
if(typeof(type)==="function") {
|
|
76
|
+
const result = type(val);
|
|
77
|
+
if(result==null) return false;
|
|
78
|
+
resultSchema[fieldName] = result;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if(val==null && (type as number)>=100) {
|
|
83
|
+
resultSchema[fieldName] = null;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if(type===FieldTypeEnum.Any || type===FieldTypeEnum.AnyOptional) {
|
|
88
|
+
resultSchema[fieldName] = val;
|
|
89
|
+
} else if(type===FieldTypeEnum.Boolean || type===FieldTypeEnum.BooleanOptional) {
|
|
90
|
+
if(typeof(val)!=="boolean") return false;
|
|
91
|
+
resultSchema[fieldName] = val;
|
|
92
|
+
} else if(type===FieldTypeEnum.Number || type===FieldTypeEnum.NumberOptional) {
|
|
93
|
+
if(typeof(val)!=="number") return false;
|
|
94
|
+
if(isNaN(val as number)) return false;
|
|
95
|
+
resultSchema[fieldName] = val;
|
|
96
|
+
} else if(type===FieldTypeEnum.NumberPositive || type===FieldTypeEnum.NumberPositiveOptional) {
|
|
97
|
+
if(typeof(val)!=="number") return false;
|
|
98
|
+
if(isNaN(val as number)) return false;
|
|
99
|
+
if(val<=0) return false;
|
|
100
|
+
resultSchema[fieldName] = val;
|
|
101
|
+
} else if(type===FieldTypeEnum.NumberNotNegative || type===FieldTypeEnum.NumberNotNegativeOptional) {
|
|
102
|
+
if(typeof(val)!=="number") return false;
|
|
103
|
+
if(isNaN(val as number)) return false;
|
|
104
|
+
if(val<0) return false;
|
|
105
|
+
resultSchema[fieldName] = val;
|
|
106
|
+
} else if(type===FieldTypeEnum.BigInt || type===FieldTypeEnum.BigIntOptional) {
|
|
107
|
+
const result = parseBigInt(val);
|
|
108
|
+
if(result==null) return false;
|
|
109
|
+
resultSchema[fieldName] = result;
|
|
110
|
+
} else if(type===FieldTypeEnum.BigIntPositive || type===FieldTypeEnum.BigIntPositiveOptional) {
|
|
111
|
+
const result = parseBigInt(val);
|
|
112
|
+
if(result==null) return false;
|
|
113
|
+
if(result<=0n) return false;
|
|
114
|
+
resultSchema[fieldName] = result;
|
|
115
|
+
} else if(type===FieldTypeEnum.BigIntNotNegative || type===FieldTypeEnum.BigIntNotNegativeOptional) {
|
|
116
|
+
const result = parseBigInt(val);
|
|
117
|
+
if(result==null) return false;
|
|
118
|
+
if(result<0n) return false;
|
|
119
|
+
resultSchema[fieldName] = result;
|
|
120
|
+
} else if(type===FieldTypeEnum.String || type===FieldTypeEnum.StringOptional) {
|
|
121
|
+
if(typeof(val)!=="string") return false;
|
|
122
|
+
resultSchema[fieldName] = val;
|
|
123
|
+
} else {
|
|
124
|
+
//Probably another request schema
|
|
125
|
+
const result = verifySchema(val, type as RequestSchema);
|
|
126
|
+
if(result==null) return false;
|
|
127
|
+
resultSchema[fieldName] = result;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
53
132
|
export function verifySchema<T extends RequestSchema>(req: any, schema: T): RequestSchemaResult<T> {
|
|
54
133
|
if(req==null) return null;
|
|
55
134
|
const resultSchema: any = {};
|
|
56
135
|
for(let fieldName in schema) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const type: FieldTypeEnum | RequestSchema | ((val: any) => boolean) = schema[fieldName];
|
|
60
|
-
if(typeof(type)==="function") {
|
|
61
|
-
const result = type(val);
|
|
62
|
-
if(result==null) return null;
|
|
63
|
-
resultSchema[fieldName] = result;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if(val==null && (type as number)>=100) {
|
|
68
|
-
resultSchema[fieldName] = null;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if(type===FieldTypeEnum.Any || type===FieldTypeEnum.AnyOptional) {
|
|
73
|
-
resultSchema[fieldName] = val;
|
|
74
|
-
} else if(type===FieldTypeEnum.Boolean || type===FieldTypeEnum.BooleanOptional) {
|
|
75
|
-
if(typeof(val)!=="boolean") return null;
|
|
76
|
-
resultSchema[fieldName] = val;
|
|
77
|
-
} else if(type===FieldTypeEnum.Number || type===FieldTypeEnum.NumberOptional) {
|
|
78
|
-
if(typeof(val)!=="number") return null;
|
|
79
|
-
if(isNaN(val as number)) return null;
|
|
80
|
-
resultSchema[fieldName] = val;
|
|
81
|
-
} else if(type===FieldTypeEnum.BigInt || type===FieldTypeEnum.BigIntOptional) {
|
|
82
|
-
const result = parseBigInt(val);
|
|
83
|
-
if(result==null) return null;
|
|
84
|
-
resultSchema[fieldName] = result;
|
|
85
|
-
} else if(type===FieldTypeEnum.String || type===FieldTypeEnum.StringOptional) {
|
|
86
|
-
if(typeof(val)!=="string") return null;
|
|
87
|
-
resultSchema[fieldName] = val;
|
|
88
|
-
} else {
|
|
89
|
-
//Probably another request schema
|
|
90
|
-
const result = verifySchema(val, type as RequestSchema);
|
|
91
|
-
if(result==null) return null;
|
|
92
|
-
resultSchema[fieldName] = result;
|
|
93
|
-
}
|
|
136
|
+
if(!verifySchemaField(req[fieldName], schema[fieldName], fieldName, resultSchema)) return null;
|
|
94
137
|
}
|
|
95
138
|
return resultSchema;
|
|
96
139
|
}
|