@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.
Files changed (34) hide show
  1. package/dist/swaps/assertions/FromBtcAmountAssertions.js +2 -2
  2. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +45 -19
  3. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +2 -2
  4. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +35 -25
  5. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +2 -2
  6. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +33 -22
  7. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +1 -1
  8. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +29 -11
  9. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +2 -2
  10. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +1 -1
  11. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +1 -0
  12. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +54 -9
  13. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +83 -30
  14. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +2 -2
  15. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +11 -17
  16. package/dist/utils/Utils.d.ts +33 -0
  17. package/dist/utils/Utils.js +51 -1
  18. package/dist/utils/paramcoders/ParamDecoder.js +4 -86
  19. package/dist/utils/paramcoders/SchemaVerifier.d.ts +11 -2
  20. package/dist/utils/paramcoders/SchemaVerifier.js +93 -47
  21. package/package.json +1 -1
  22. package/src/swaps/assertions/FromBtcAmountAssertions.ts +2 -2
  23. package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +44 -19
  24. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +47 -30
  25. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +45 -27
  26. package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +28 -13
  27. package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +4 -4
  28. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +63 -13
  29. package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +79 -33
  30. package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +25 -2
  31. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +10 -16
  32. package/src/utils/Utils.ts +48 -0
  33. package/src/utils/paramcoders/ParamDecoder.ts +10 -66
  34. 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) 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;
@@ -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.BigInt,
423
- gasAmount: FieldTypeEnum.BigInt,
431
+ amount: FieldTypeEnum.BigIntPositive,
432
+ gasAmount: FieldTypeEnum.BigIntNotNegative,
424
433
  exactOut: FieldTypeEnum.BooleanOptional,
425
- callerFeeRate: FieldTypeEnum.BigInt,
426
- frontingFeeRate: FieldTypeEnum.BigInt,
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.NumberOptional
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
- const error = {
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 = signedTx.id;
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: "+swap.btcTxId);
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
- (data as any).sending = false;
854
- swap.sending = false;
855
- vault.removeWithdrawal(data);
856
- await this.Vaults.saveVault(vault);
857
-
858
- if(isDefinedRuntimeError(e) && swap.metadata!=null) swap.metadata.postQuoteError = e;
859
- await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
860
-
861
- throw e;
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*1000);
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, "processPastSwaps(): Error ocurred while processing swap: ", e);
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
- if(await this.processPastSwap(swap)) cancelInvoices.push(swap);
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
- //Cancel invoice
260
- await invoiceData.setState(FromBtcLnTrustedSwapState.REFUNDED);
261
- await this.storageManager.saveData(invoice.id, null, invoiceData);
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
- if(txStatus==="not_found") {
285
- //Retry
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);
@@ -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 {FieldTypeEnum, parseBigInt, RequestSchema, RequestSchemaResult, verifySchema} from "./SchemaVerifier";
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
- const type: FieldTypeEnum | RequestSchema | ((val: any) => boolean) = schema[fieldName];
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
- const type: FieldTypeEnum | RequestSchema | ((val: any) => boolean) = schema[fieldName];
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
- const val: any = req[fieldName];
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
  }