@atomiqlabs/lp-lib 17.4.0 → 17.5.0

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.
@@ -37,10 +37,16 @@ export type SwapBaseConfig = {
37
37
  bitcoinBlocktime: bigint;
38
38
  baseFee: bigint;
39
39
  feePPM: bigint;
40
- max: bigint;
41
- min: bigint;
42
40
  safetyFactor: bigint;
43
41
  swapCheckInterval: number;
42
+ max: bigint;
43
+ min: bigint;
44
+ minMaxOverrides?: {
45
+ [chainIdentifier: string]: {
46
+ min: bigint;
47
+ max: bigint;
48
+ };
49
+ };
44
50
  };
45
51
  export type MultichainData = {
46
52
  chains: {
@@ -4,19 +4,28 @@ export type AmountAssertionsConfig = {
4
4
  max: bigint;
5
5
  baseFee: bigint;
6
6
  feePPM: bigint;
7
+ minMaxOverrides?: {
8
+ [chainIdentifier: string]: {
9
+ min: bigint;
10
+ max: bigint;
11
+ };
12
+ };
7
13
  };
8
14
  export declare abstract class AmountAssertions {
9
15
  readonly config: AmountAssertionsConfig;
10
16
  readonly swapPricing: ISwapPrice;
11
17
  constructor(config: AmountAssertionsConfig, swapPricing: ISwapPrice);
18
+ getSwapMinimum(chainIdentifier: string): bigint;
19
+ getSwapMaximum(chainIdentifier: string): bigint;
12
20
  /**
13
21
  * Checks whether the bitcoin amount is within specified min/max bounds
14
22
  *
15
23
  * @param amount
24
+ * @param chainIdentifier
16
25
  * @protected
17
26
  * @throws {DefinedRuntimeError} will throw an error if the amount is outside minimum/maximum bounds
18
27
  */
19
- protected checkBtcAmountInBounds(amount: bigint): void;
28
+ protected checkBtcAmountInBounds(amount: bigint, chainIdentifier: string): void;
20
29
  /**
21
30
  * Handles and throws plugin errors
22
31
  *
@@ -7,31 +7,40 @@ class AmountAssertions {
7
7
  this.config = config;
8
8
  this.swapPricing = swapPricing;
9
9
  }
10
+ getSwapMinimum(chainIdentifier) {
11
+ return this.config.minMaxOverrides?.[chainIdentifier]?.min ?? this.config.min;
12
+ }
13
+ getSwapMaximum(chainIdentifier) {
14
+ return this.config.minMaxOverrides?.[chainIdentifier]?.max ?? this.config.max;
15
+ }
10
16
  /**
11
17
  * Checks whether the bitcoin amount is within specified min/max bounds
12
18
  *
13
19
  * @param amount
20
+ * @param chainIdentifier
14
21
  * @protected
15
22
  * @throws {DefinedRuntimeError} will throw an error if the amount is outside minimum/maximum bounds
16
23
  */
17
- checkBtcAmountInBounds(amount) {
18
- if (amount < this.config.min) {
24
+ checkBtcAmountInBounds(amount, chainIdentifier) {
25
+ const min = this.getSwapMinimum(chainIdentifier);
26
+ const max = this.getSwapMaximum(chainIdentifier);
27
+ if (amount < min) {
19
28
  throw {
20
29
  code: 20003,
21
30
  msg: "Amount too low!",
22
31
  data: {
23
- min: this.config.min.toString(10),
24
- max: this.config.max.toString(10)
32
+ min: min.toString(10),
33
+ max: max.toString(10)
25
34
  }
26
35
  };
27
36
  }
28
- if (amount > this.config.max) {
37
+ if (amount > max) {
29
38
  throw {
30
39
  code: 20004,
31
40
  msg: "Amount too high!",
32
41
  data: {
33
- min: this.config.min.toString(10),
34
- max: this.config.max.toString(10)
42
+ min: min.toString(10),
43
+ max: max.toString(10)
35
44
  }
36
45
  };
37
46
  }
@@ -19,7 +19,9 @@ class FromBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
19
19
  * @throws {DefinedRuntimeError} will throw an error if the amount is outside minimum/maximum bounds
20
20
  */
21
21
  async preCheckFromBtcAmounts(swapType, request, requestedAmount, gasAmount) {
22
- const res = await PluginManager_1.PluginManager.onHandlePreFromBtcQuote(swapType, request, requestedAmount, request.chainIdentifier, { minInBtc: this.config.min, maxInBtc: this.config.max }, { baseFeeInBtc: this.config.baseFee, feePPM: this.config.feePPM }, gasAmount);
22
+ const min = this.getSwapMinimum(request.chainIdentifier);
23
+ const max = this.getSwapMaximum(request.chainIdentifier);
24
+ const res = await PluginManager_1.PluginManager.onHandlePreFromBtcQuote(swapType, request, requestedAmount, request.chainIdentifier, { minInBtc: min, maxInBtc: max }, { baseFeeInBtc: this.config.baseFee, feePPM: this.config.feePPM }, gasAmount);
23
25
  if (res != null) {
24
26
  AmountAssertions_1.AmountAssertions.handlePluginErrorResponses(res);
25
27
  if ((0, IPlugin_1.isQuoteSetFees)(res)) {
@@ -32,7 +34,7 @@ class FromBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
32
34
  }
33
35
  }
34
36
  if (requestedAmount.input)
35
- this.checkBtcAmountInBounds(requestedAmount.amount);
37
+ this.checkBtcAmountInBounds(requestedAmount.amount, request.chainIdentifier);
36
38
  if (gasAmount != null && gasAmount.amount !== 0n) {
37
39
  if (gasAmount.amount > (this.config.gasTokenMax?.[request.chainIdentifier] ?? 0n)) {
38
40
  throw {
@@ -64,7 +66,9 @@ class FromBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
64
66
  const chainIdentifier = request.chainIdentifier;
65
67
  let securityDepositApyPPM;
66
68
  let securityDepositBaseMultiplierPPM;
67
- const res = await PluginManager_1.PluginManager.onHandlePostFromBtcQuote(swapType, request, requestedAmount, chainIdentifier, { minInBtc: this.config.min, maxInBtc: this.config.max }, { baseFeeInBtc: fees.baseFee, feePPM: fees.feePPM }, gasTokenAmount);
69
+ const min = this.getSwapMinimum(chainIdentifier);
70
+ const max = this.getSwapMaximum(chainIdentifier);
71
+ const res = await PluginManager_1.PluginManager.onHandlePostFromBtcQuote(swapType, request, requestedAmount, chainIdentifier, { minInBtc: min, maxInBtc: max }, { baseFeeInBtc: fees.baseFee, feePPM: fees.feePPM }, gasTokenAmount);
68
72
  signal.throwIfAborted();
69
73
  if (res != null) {
70
74
  AmountAssertions_1.AmountAssertions.handlePluginErrorResponses(res);
@@ -117,11 +121,11 @@ class FromBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
117
121
  const _amountBD = ((amountBD + fees.baseFee) * 1000000n + denominator - 1n) / denominator;
118
122
  swapFee = _amountBD - amountBD;
119
123
  amountBD = _amountBD;
120
- const tooLow = amountBD < (this.config.min * 95n / 100n);
121
- const tooHigh = amountBD > (this.config.max * 105n / 100n);
124
+ const tooLow = amountBD < (min * 95n / 100n);
125
+ const tooHigh = amountBD > (max * 105n / 100n);
122
126
  if (tooLow || tooHigh) {
123
- const adjustedMin = this.config.min * (1000000n - fees.feePPM) / (1000000n - fees.baseFee);
124
- const adjustedMax = this.config.max * (1000000n - fees.feePPM) / (1000000n - fees.baseFee);
127
+ const adjustedMin = min * (1000000n - fees.feePPM) / (1000000n - fees.baseFee);
128
+ const adjustedMax = max * (1000000n - fees.feePPM) / (1000000n - fees.baseFee);
125
129
  const minIn = await this.swapPricing.getFromBtcSwapAmount(adjustedMin, requestedAmount.token, chainIdentifier, null, requestedAmount.pricePrefetch);
126
130
  const maxIn = await this.swapPricing.getFromBtcSwapAmount(adjustedMax, requestedAmount.token, chainIdentifier, null, requestedAmount.pricePrefetch);
127
131
  throw {
@@ -135,7 +139,7 @@ class FromBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
135
139
  }
136
140
  }
137
141
  else {
138
- this.checkBtcAmountInBounds(requestedAmount.amount);
142
+ this.checkBtcAmountInBounds(requestedAmount.amount, chainIdentifier);
139
143
  amountBD = requestedAmount.amount - amountBDgas;
140
144
  swapFee = fees.baseFee + ((amountBD * fees.feePPM + 999999n) / 1000000n);
141
145
  if (amountBD - swapFee < 0n) {
@@ -143,8 +147,8 @@ class FromBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
143
147
  code: 20003,
144
148
  msg: "Amount too low!",
145
149
  data: {
146
- min: this.config.min.toString(10),
147
- max: this.config.max.toString(10)
150
+ min: min.toString(10),
151
+ max: max.toString(10)
148
152
  }
149
153
  };
150
154
  }
@@ -14,7 +14,9 @@ class ToBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
14
14
  * @throws {DefinedRuntimeError} will throw an error if the amount is outside minimum/maximum bounds
15
15
  */
16
16
  async preCheckToBtcAmounts(swapType, request, requestedAmount) {
17
- const res = await PluginManager_1.PluginManager.onHandlePreToBtcQuote(swapType, request, requestedAmount, request.chainIdentifier, { minInBtc: this.config.min, maxInBtc: this.config.max }, { baseFeeInBtc: this.config.baseFee, feePPM: this.config.feePPM });
17
+ const min = this.getSwapMinimum(request.chainIdentifier);
18
+ const max = this.getSwapMaximum(request.chainIdentifier);
19
+ const res = await PluginManager_1.PluginManager.onHandlePreToBtcQuote(swapType, request, requestedAmount, request.chainIdentifier, { minInBtc: min, maxInBtc: max }, { baseFeeInBtc: this.config.baseFee, feePPM: this.config.feePPM });
18
20
  if (res != null) {
19
21
  AmountAssertions_1.AmountAssertions.handlePluginErrorResponses(res);
20
22
  if ((0, IPlugin_1.isQuoteSetFees)(res)) {
@@ -25,7 +27,7 @@ class ToBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
25
27
  }
26
28
  }
27
29
  if (!requestedAmount.input) {
28
- this.checkBtcAmountInBounds(requestedAmount.amount);
30
+ this.checkBtcAmountInBounds(requestedAmount.amount, request.chainIdentifier);
29
31
  }
30
32
  return {
31
33
  baseFee: this.config.baseFee,
@@ -46,7 +48,9 @@ class ToBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
46
48
  */
47
49
  async checkToBtcAmount(swapType, request, requestedAmount, fees, getNetworkFee, signal) {
48
50
  const chainIdentifier = request.chainIdentifier;
49
- const res = await PluginManager_1.PluginManager.onHandlePostToBtcQuote(swapType, request, requestedAmount, request.chainIdentifier, { minInBtc: this.config.min, maxInBtc: this.config.max }, { baseFeeInBtc: fees.baseFee, feePPM: fees.feePPM, networkFeeGetter: getNetworkFee });
51
+ const min = this.getSwapMinimum(chainIdentifier);
52
+ const max = this.getSwapMaximum(chainIdentifier);
53
+ const res = await PluginManager_1.PluginManager.onHandlePostToBtcQuote(swapType, request, requestedAmount, request.chainIdentifier, { minInBtc: min, maxInBtc: max }, { baseFeeInBtc: fees.baseFee, feePPM: fees.feePPM, networkFeeGetter: getNetworkFee });
50
54
  signal.throwIfAborted();
51
55
  if (res != null) {
52
56
  AmountAssertions_1.AmountAssertions.handlePluginErrorResponses(res);
@@ -90,19 +94,19 @@ class ToBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
90
94
  //Decrease by base fee
91
95
  amountBD = amountBD - fees.baseFee;
92
96
  //If it's already smaller than minimum, set it to minimum so we can calculate the network fee
93
- if (amountBD < (this.config.min * 95n / 100n)) {
94
- amountBD = this.config.min;
97
+ if (amountBD < (min * 95n / 100n)) {
98
+ amountBD = min;
95
99
  tooLow = true;
96
100
  }
97
101
  //If it's already larger than maximum, set it to maximum so we can calculate the network fee
98
- if (amountBD > (this.config.max * 105n / 100n)) {
99
- amountBD = this.config.max;
102
+ if (amountBD > (max * 105n / 100n)) {
103
+ amountBD = max;
100
104
  tooHigh = true;
101
105
  }
102
106
  }
103
107
  else {
104
108
  amountBD = requestedAmount.amount;
105
- this.checkBtcAmountInBounds(amountBD);
109
+ this.checkBtcAmountInBounds(amountBD, chainIdentifier);
106
110
  }
107
111
  const resp = await getNetworkFee(amountBD);
108
112
  signal.throwIfAborted();
@@ -111,12 +115,12 @@ class ToBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
111
115
  amountBD = amountBD - resp.networkFee;
112
116
  //Decrease by percentage fee
113
117
  amountBD = amountBD * 1000000n / (fees.feePPM + 1000000n);
114
- tooHigh || (tooHigh = amountBD > (this.config.max * 105n / 100n));
115
- tooLow || (tooLow = amountBD < (this.config.min * 95n / 100n));
118
+ tooHigh || (tooHigh = amountBD > (max * 105n / 100n));
119
+ tooLow || (tooLow = amountBD < (min * 95n / 100n));
116
120
  if (tooLow || tooHigh) {
117
121
  //Compute min/max
118
- let adjustedMin = this.config.min * (fees.feePPM + 1000000n) / 1000000n;
119
- let adjustedMax = this.config.max * (fees.feePPM + 1000000n) / 1000000n;
122
+ let adjustedMin = min * (fees.feePPM + 1000000n) / 1000000n;
123
+ let adjustedMax = max * (fees.feePPM + 1000000n) / 1000000n;
120
124
  adjustedMin = adjustedMin + fees.baseFee + resp.networkFee;
121
125
  adjustedMax = adjustedMax + fees.baseFee + resp.networkFee;
122
126
  const minIn = await this.swapPricing.getFromBtcSwapAmount(adjustedMin, requestedAmount.token, chainIdentifier, null, requestedAmount.pricePrefetch);
@@ -17,6 +17,17 @@ const AmountAssertions_1 = require("../assertions/AmountAssertions");
17
17
  const IPlugin_1 = require("../../plugins/IPlugin");
18
18
  const StickyAddress_1 = require("./StickyAddress");
19
19
  const TX_MAX_VSIZE = 16 * 1024;
20
+ function parseAmountAdjustUtxos(amountAdjustUtxos) {
21
+ if (!Array.isArray(amountAdjustUtxos))
22
+ return null;
23
+ if (amountAdjustUtxos.length > 250)
24
+ return null;
25
+ const validArray = amountAdjustUtxos.every(value => value != null && typeof (value) === "object" && typeof (value.value) === "number" && typeof (value.vSize) === "number" &&
26
+ (value.cpfp == null || (typeof (value.cpfp) === "object" && typeof (value.cpfp.effectiveVSize) === "number" && typeof (value.cpfp.effectiveFeeRate) === "number")));
27
+ if (!validArray)
28
+ return null;
29
+ return amountAdjustUtxos;
30
+ }
20
31
  class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
21
32
  constructor(storageDirectory, vaultStorage, path, chainsData, swapPricing, bitcoin, bitcoinRpc, spvVaultSigner, config, stickyAddresses) {
22
33
  super(storageDirectory, path, chainsData, swapPricing);
@@ -309,6 +320,23 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
309
320
  code: 20100,
310
321
  msg: "Invalid request body"
311
322
  };
323
+ const inputAmountAdjustments = req.paramReader.getExistingParamsOrNull({
324
+ amountUtxos: SchemaVerifier_1.FieldTypeEnum.AnyOptional,
325
+ amountFeeRate: SchemaVerifier_1.FieldTypeEnum.NumberOptional
326
+ });
327
+ if (inputAmountAdjustments == null)
328
+ throw {
329
+ code: 20100,
330
+ msg: "Invalid request body"
331
+ };
332
+ const clientInputUtxos = inputAmountAdjustments?.amountUtxos != null
333
+ ? parseAmountAdjustUtxos(inputAmountAdjustments.amountUtxos)
334
+ : null;
335
+ if (inputAmountAdjustments?.amountUtxos != null && clientInputUtxos == null)
336
+ throw {
337
+ code: 20100,
338
+ msg: "Invalid request body (amountUtxos)"
339
+ };
312
340
  const parsedBody = { ...preFetchParsedBody, ...actualParsedBody };
313
341
  metadata.request = parsedBody;
314
342
  if (parsedBody.gasToken !== chainInterface.getNativeCurrencyAddress())
@@ -333,6 +361,44 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
333
361
  parsedBody.amount,
334
362
  token: parsedBody.token
335
363
  };
364
+ if (clientInputUtxos != null) {
365
+ if (parsedBody.exactOut)
366
+ throw {
367
+ code: 20193,
368
+ msg: "amountAdjustUtxos cannot be specified for exactOut swaps!"
369
+ };
370
+ let btcFeeRate = await btcFeeRatePrefetch;
371
+ if (inputAmountAdjustments.amountFeeRate != null && inputAmountAdjustments.amountFeeRate > btcFeeRate)
372
+ btcFeeRate = inputAmountAdjustments.amountFeeRate;
373
+ let feeAccumulator = 0;
374
+ let valueAccumulator = 0;
375
+ for (let utxo of clientInputUtxos) {
376
+ const cpfpAdditionalFee = utxo.cpfp == null ? 0 : Math.ceil(utxo.cpfp.effectiveVSize * Math.max(0, btcFeeRate - utxo.cpfp.effectiveFeeRate));
377
+ const spendFee = utxo.vSize * btcFeeRate;
378
+ const totalFee = cpfpAdditionalFee + spendFee;
379
+ if (totalFee > utxo.value)
380
+ continue; //Skip detrimental UTXO
381
+ feeAccumulator += totalFee;
382
+ valueAccumulator += utxo.value;
383
+ }
384
+ let baseTxVSize = 10.5; // 4b version, 1b inputs, 1b outputs, 4b locktime, 0.5vB witness flag + witness elements count
385
+ //vault input and output
386
+ baseTxVSize += 32 + 4 + 1 + 4; //Input base
387
+ baseTxVSize += this.vaultSigner.getAddressType() === "p2tr" ? (1 + 1 + 65) / 4 : (1 + 1 + 72 + 1 + 33) / 4;
388
+ baseTxVSize += 8 + 1; //Output base
389
+ baseTxVSize += this.vaultSigner.getAddressType() === "p2tr" ? 34 : 22;
390
+ //opreturn output
391
+ baseTxVSize += 8 + 1; //Output base
392
+ const opReturnDataSize = spvVaultContract.toOpReturnData(parsedBody.address, parsedBody.gasAmount > 0 ? [0xffffffffffffffffn, 0xffffffffffffffffn] : [0xffffffffffffffffn]).length;
393
+ baseTxVSize += (opReturnDataSize <= 0x4b ? 2 : 3 /*Needs an OP_PUSHDATA1 opcode*/) + opReturnDataSize;
394
+ //LP output
395
+ baseTxVSize += 8 + 1; //Output base
396
+ baseTxVSize += this.bitcoin.getAddressType() === "p2tr" ? 34 : this.bitcoin.getAddressType() === "p2wpkh" ? 22 : 23;
397
+ const baseTxFee = Math.ceil(baseTxVSize) * btcFeeRate;
398
+ feeAccumulator += baseTxFee;
399
+ const amount = Math.floor(valueAccumulator - Math.ceil(feeAccumulator));
400
+ requestedAmount.amount = BigInt(amount);
401
+ }
336
402
  const gasTokenAmount = {
337
403
  input: false,
338
404
  amount: parsedBody.gasAmount * (100000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate) / 100000n,
@@ -420,7 +486,8 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
420
486
  gasSwapFee: gasSwapFeeInToken.toString(10),
421
487
  callerFeeShare: callerFeeShare.toString(10),
422
488
  frontingFeeShare: frontingFeeShare.toString(10),
423
- executionFeeShare: executionFeeShare.toString(10)
489
+ executionFeeShare: executionFeeShare.toString(10),
490
+ usedUtxoInputCalculation: clientInputUtxos != null
424
491
  }
425
492
  });
426
493
  }));
@@ -134,9 +134,9 @@ class FromBtcTrusted extends SwapHandler_1.SwapHandler {
134
134
  }
135
135
  else {
136
136
  //If lower than minimum then ignore
137
- if (sentSats < this.config.min)
137
+ if (sentSats < this.AmountAssertions.getSwapMinimum(swap.chainIdentifier))
138
138
  return;
139
- if (sentSats > this.config.max) {
139
+ if (sentSats > this.AmountAssertions.getSwapMaximum(swap.chainIdentifier)) {
140
140
  swap.adjustedInput = sentSats;
141
141
  swap.btcTx = tx;
142
142
  swap.txId = tx.txid;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/lp-lib",
3
- "version": "17.4.0",
3
+ "version": "17.5.0",
4
4
  "description": "Main functionality implementation for atomiq LP node",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -41,10 +41,15 @@ export type SwapBaseConfig = {
41
41
  bitcoinBlocktime: bigint,
42
42
  baseFee: bigint,
43
43
  feePPM: bigint,
44
+ safetyFactor: bigint,
45
+ swapCheckInterval: number,
46
+
44
47
  max: bigint,
45
48
  min: bigint,
46
- safetyFactor: bigint,
47
- swapCheckInterval: number
49
+
50
+ minMaxOverrides?: {
51
+ [chainIdentifier: string]: {min: bigint, max: bigint}
52
+ }
48
53
  };
49
54
 
50
55
  export type MultichainData = {
@@ -1,7 +1,16 @@
1
1
  import {ISwapPrice} from "../../prices/ISwapPrice";
2
2
  import {isQuoteAmountTooHigh, isQuoteAmountTooLow, isQuoteThrow} from "../../plugins/IPlugin";
3
3
 
4
- export type AmountAssertionsConfig = {min: bigint, max: bigint, baseFee: bigint, feePPM: bigint};
4
+ export type AmountAssertionsConfig = {
5
+ min: bigint,
6
+ max: bigint,
7
+ baseFee: bigint,
8
+ feePPM: bigint,
9
+
10
+ minMaxOverrides?: {
11
+ [chainIdentifier: string]: {min: bigint, max: bigint}
12
+ }
13
+ };
5
14
 
6
15
  export abstract class AmountAssertions {
7
16
 
@@ -13,32 +22,43 @@ export abstract class AmountAssertions {
13
22
  this.swapPricing = swapPricing;
14
23
  }
15
24
 
25
+ getSwapMinimum(chainIdentifier: string) {
26
+ return this.config.minMaxOverrides?.[chainIdentifier]?.min ?? this.config.min;
27
+ }
28
+
29
+ getSwapMaximum(chainIdentifier: string) {
30
+ return this.config.minMaxOverrides?.[chainIdentifier]?.max ?? this.config.max;
31
+ }
32
+
16
33
  /**
17
34
  * Checks whether the bitcoin amount is within specified min/max bounds
18
35
  *
19
36
  * @param amount
37
+ * @param chainIdentifier
20
38
  * @protected
21
39
  * @throws {DefinedRuntimeError} will throw an error if the amount is outside minimum/maximum bounds
22
40
  */
23
- protected checkBtcAmountInBounds(amount: bigint): void {
24
- if (amount < this.config.min) {
41
+ protected checkBtcAmountInBounds(amount: bigint, chainIdentifier: string): void {
42
+ const min = this.getSwapMinimum(chainIdentifier);
43
+ const max = this.getSwapMaximum(chainIdentifier);
44
+ if (amount < min) {
25
45
  throw {
26
46
  code: 20003,
27
47
  msg: "Amount too low!",
28
48
  data: {
29
- min: this.config.min.toString(10),
30
- max: this.config.max.toString(10)
49
+ min: min.toString(10),
50
+ max: max.toString(10)
31
51
  }
32
52
  };
33
53
  }
34
54
 
35
- if(amount > this.config.max) {
55
+ if(amount > max) {
36
56
  throw {
37
57
  code: 20004,
38
58
  msg: "Amount too high!",
39
59
  data: {
40
- min: this.config.min.toString(10),
41
- max: this.config.max.toString(10)
60
+ min: min.toString(10),
61
+ max: max.toString(10)
42
62
  }
43
63
  };
44
64
  }
@@ -42,12 +42,15 @@ export class FromBtcAmountAssertions extends AmountAssertions {
42
42
  securityDepositApyPPM?: bigint,
43
43
  securityDepositBaseMultiplierPPM?: bigint,
44
44
  }> {
45
+ const min = this.getSwapMinimum(request.chainIdentifier);
46
+ const max = this.getSwapMaximum(request.chainIdentifier);
47
+
45
48
  const res = await PluginManager.onHandlePreFromBtcQuote(
46
49
  swapType,
47
50
  request,
48
51
  requestedAmount,
49
52
  request.chainIdentifier,
50
- {minInBtc: this.config.min, maxInBtc: this.config.max},
53
+ {minInBtc: min, maxInBtc: max},
51
54
  {baseFeeInBtc: this.config.baseFee, feePPM: this.config.feePPM},
52
55
  gasAmount
53
56
  );
@@ -62,7 +65,7 @@ export class FromBtcAmountAssertions extends AmountAssertions {
62
65
  }
63
66
  }
64
67
  }
65
- if(requestedAmount.input) this.checkBtcAmountInBounds(requestedAmount.amount);
68
+ if(requestedAmount.input) this.checkBtcAmountInBounds(requestedAmount.amount, request.chainIdentifier);
66
69
 
67
70
  if(gasAmount!=null && gasAmount.amount!==0n) {
68
71
  if(gasAmount.amount > (this.config.gasTokenMax?.[request.chainIdentifier] ?? 0n)) {
@@ -117,12 +120,15 @@ export class FromBtcAmountAssertions extends AmountAssertions {
117
120
  let securityDepositApyPPM: bigint;
118
121
  let securityDepositBaseMultiplierPPM: bigint;
119
122
 
123
+ const min = this.getSwapMinimum(chainIdentifier);
124
+ const max = this.getSwapMaximum(chainIdentifier);
125
+
120
126
  const res = await PluginManager.onHandlePostFromBtcQuote(
121
127
  swapType,
122
128
  request,
123
129
  requestedAmount,
124
130
  chainIdentifier,
125
- {minInBtc: this.config.min, maxInBtc: this.config.max},
131
+ {minInBtc: min, maxInBtc: max},
126
132
  {baseFeeInBtc: fees.baseFee, feePPM: fees.feePPM},
127
133
  gasTokenAmount
128
134
  );
@@ -177,11 +183,11 @@ export class FromBtcAmountAssertions extends AmountAssertions {
177
183
  swapFee = _amountBD - amountBD;
178
184
  amountBD = _amountBD;
179
185
 
180
- const tooLow = amountBD < (this.config.min * 95n / 100n);
181
- const tooHigh = amountBD > (this.config.max * 105n / 100n);
186
+ const tooLow = amountBD < (min * 95n / 100n);
187
+ const tooHigh = amountBD > (max * 105n / 100n);
182
188
  if(tooLow || tooHigh) {
183
- const adjustedMin = this.config.min * (1000000n - fees.feePPM) / (1000000n - fees.baseFee);
184
- const adjustedMax = this.config.max * (1000000n - fees.feePPM) / (1000000n - fees.baseFee);
189
+ const adjustedMin = min * (1000000n - fees.feePPM) / (1000000n - fees.baseFee);
190
+ const adjustedMax = max * (1000000n - fees.feePPM) / (1000000n - fees.baseFee);
185
191
  const minIn = await this.swapPricing.getFromBtcSwapAmount(
186
192
  adjustedMin, requestedAmount.token, chainIdentifier, null, requestedAmount.pricePrefetch
187
193
  );
@@ -198,7 +204,7 @@ export class FromBtcAmountAssertions extends AmountAssertions {
198
204
  };
199
205
  }
200
206
  } else {
201
- this.checkBtcAmountInBounds(requestedAmount.amount);
207
+ this.checkBtcAmountInBounds(requestedAmount.amount, chainIdentifier);
202
208
  amountBD = requestedAmount.amount - amountBDgas;
203
209
  swapFee = fees.baseFee + ((amountBD * fees.feePPM + 999_999n) / 1000000n);
204
210
  if(amountBD - swapFee < 0n) {
@@ -206,8 +212,8 @@ export class FromBtcAmountAssertions extends AmountAssertions {
206
212
  code: 20003,
207
213
  msg: "Amount too low!",
208
214
  data: {
209
- min: this.config.min.toString(10),
210
- max: this.config.max.toString(10)
215
+ min: min.toString(10),
216
+ max: max.toString(10)
211
217
  }
212
218
  };
213
219
  }
@@ -21,12 +21,15 @@ export class ToBtcAmountAssertions extends AmountAssertions {
21
21
  request: RequestData<ToBtcLnRequestType | ToBtcRequestType>,
22
22
  requestedAmount: {input: boolean, amount: bigint, token: string}
23
23
  ): Promise<{baseFee: bigint, feePPM: bigint}> {
24
+ const min = this.getSwapMinimum(request.chainIdentifier);
25
+ const max = this.getSwapMaximum(request.chainIdentifier);
26
+
24
27
  const res = await PluginManager.onHandlePreToBtcQuote(
25
28
  swapType,
26
29
  request,
27
30
  requestedAmount,
28
31
  request.chainIdentifier,
29
- {minInBtc: this.config.min, maxInBtc: this.config.max},
32
+ {minInBtc: min, maxInBtc: max},
30
33
  {baseFeeInBtc: this.config.baseFee, feePPM: this.config.feePPM},
31
34
  );
32
35
  if(res!=null) {
@@ -39,7 +42,7 @@ export class ToBtcAmountAssertions extends AmountAssertions {
39
42
  }
40
43
  }
41
44
  if(!requestedAmount.input) {
42
- this.checkBtcAmountInBounds(requestedAmount.amount);
45
+ this.checkBtcAmountInBounds(requestedAmount.amount, request.chainIdentifier);
43
46
  }
44
47
  return {
45
48
  baseFee: this.config.baseFee,
@@ -77,12 +80,15 @@ export class ToBtcAmountAssertions extends AmountAssertions {
77
80
  }> {
78
81
  const chainIdentifier = request.chainIdentifier;
79
82
 
83
+ const min = this.getSwapMinimum(chainIdentifier);
84
+ const max = this.getSwapMaximum(chainIdentifier);
85
+
80
86
  const res = await PluginManager.onHandlePostToBtcQuote<T>(
81
87
  swapType,
82
88
  request,
83
89
  requestedAmount,
84
90
  request.chainIdentifier,
85
- {minInBtc: this.config.min, maxInBtc: this.config.max},
91
+ {minInBtc: min, maxInBtc: max},
86
92
  {baseFeeInBtc: fees.baseFee, feePPM: fees.feePPM, networkFeeGetter: getNetworkFee}
87
93
  );
88
94
  signal.throwIfAborted();
@@ -128,18 +134,18 @@ export class ToBtcAmountAssertions extends AmountAssertions {
128
134
  amountBD = amountBD - fees.baseFee;
129
135
 
130
136
  //If it's already smaller than minimum, set it to minimum so we can calculate the network fee
131
- if(amountBD < (this.config.min * 95n / 100n)) {
132
- amountBD = this.config.min;
137
+ if(amountBD < (min * 95n / 100n)) {
138
+ amountBD = min;
133
139
  tooLow = true;
134
140
  }
135
141
  //If it's already larger than maximum, set it to maximum so we can calculate the network fee
136
- if(amountBD > (this.config.max * 105n / 100n)) {
137
- amountBD = this.config.max;
142
+ if(amountBD > (max * 105n / 100n)) {
143
+ amountBD = max;
138
144
  tooHigh = true;
139
145
  }
140
146
  } else {
141
147
  amountBD = requestedAmount.amount;
142
- this.checkBtcAmountInBounds(amountBD);
148
+ this.checkBtcAmountInBounds(amountBD, chainIdentifier);
143
149
  }
144
150
 
145
151
  const resp = await getNetworkFee(amountBD);
@@ -152,12 +158,12 @@ export class ToBtcAmountAssertions extends AmountAssertions {
152
158
  //Decrease by percentage fee
153
159
  amountBD = amountBD * 1000000n / (fees.feePPM + 1000000n);
154
160
 
155
- tooHigh ||= amountBD > (this.config.max * 105n / 100n);
156
- tooLow ||= amountBD < (this.config.min * 95n / 100n);
161
+ tooHigh ||= amountBD > (max * 105n / 100n);
162
+ tooLow ||= amountBD < (min * 95n / 100n);
157
163
  if(tooLow || tooHigh) {
158
164
  //Compute min/max
159
- let adjustedMin = this.config.min * (fees.feePPM + 1000000n) / 1000000n;
160
- let adjustedMax = this.config.max * (fees.feePPM + 1000000n) / 1000000n;
165
+ let adjustedMin = min * (fees.feePPM + 1000000n) / 1000000n;
166
+ let adjustedMax = max * (fees.feePPM + 1000000n) / 1000000n;
161
167
  adjustedMin = adjustedMin + fees.baseFee + resp.networkFee;
162
168
  adjustedMax = adjustedMax + fees.baseFee + resp.networkFee;
163
169
  const minIn = await this.swapPricing.getFromBtcSwapAmount(
@@ -64,6 +64,26 @@ export type SpvVaultPostQuote = {
64
64
 
65
65
  const TX_MAX_VSIZE = 16*1024;
66
66
 
67
+ type AmountAdjustUtxo = {
68
+ value: number,
69
+ vSize: number,
70
+ cpfp?: {
71
+ effectiveVSize: number,
72
+ effectiveFeeRate: number
73
+ }
74
+ }
75
+
76
+ function parseAmountAdjustUtxos(amountAdjustUtxos: any): AmountAdjustUtxo[] {
77
+ if(!Array.isArray(amountAdjustUtxos)) return null;
78
+ if(amountAdjustUtxos.length > 250) return null;
79
+ const validArray = amountAdjustUtxos.every(value =>
80
+ value!=null && typeof(value)==="object" && typeof(value.value)==="number" && typeof(value.vSize)==="number" &&
81
+ (value.cpfp==null || (typeof(value.cpfp)==="object" && typeof(value.cpfp.effectiveVSize)==="number" && typeof(value.cpfp.effectiveFeeRate)==="number"))
82
+ );
83
+ if(!validArray) return null;
84
+ return amountAdjustUtxos;
85
+ }
86
+
67
87
  export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapState> {
68
88
  readonly type = SwapHandlerType.FROM_BTC_SPV;
69
89
  readonly inflightSwapStates = new Set([SpvVaultSwapState.SIGNED, SpvVaultSwapState.SENT, SpvVaultSwapState.BTC_CONFIRMED]);
@@ -354,7 +374,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
354
374
  gasTokenPricePrefetchPromise
355
375
  } = this.getPricePrefetches(chainIdentifier, preFetchParsedBody.token, preFetchParsedBody.gasToken, abortController);
356
376
  const nativeBalancePrefetch = this.prefetchNativeBalanceIfNeeded(chainIdentifier, abortController);
357
- const btcFeeRatePrefetch = this.bitcoin.getFeeRate().catch(e => {
377
+ const btcFeeRatePrefetch: Promise<number> = this.bitcoin.getFeeRate().catch(e => {
358
378
  abortController.abort(e);
359
379
  return null;
360
380
  });
@@ -405,6 +425,23 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
405
425
  msg: "Invalid request body"
406
426
  };
407
427
 
428
+ const inputAmountAdjustments = req.paramReader.getExistingParamsOrNull({
429
+ amountUtxos: FieldTypeEnum.AnyOptional,
430
+ amountFeeRate: FieldTypeEnum.NumberOptional
431
+ });
432
+ if(inputAmountAdjustments==null) throw {
433
+ code: 20100,
434
+ msg: "Invalid request body"
435
+ };
436
+
437
+ const clientInputUtxos: AmountAdjustUtxo[] | null = inputAmountAdjustments?.amountUtxos!=null
438
+ ? parseAmountAdjustUtxos(inputAmountAdjustments.amountUtxos)
439
+ : null;
440
+ if(inputAmountAdjustments?.amountUtxos!=null && clientInputUtxos==null) throw {
441
+ code: 20100,
442
+ msg: "Invalid request body (amountUtxos)"
443
+ };
444
+
408
445
  const parsedBody: SpvVaultSwapRequestType = {...preFetchParsedBody, ...actualParsedBody};
409
446
  metadata.request = parsedBody;
410
447
 
@@ -429,6 +466,48 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
429
466
  parsedBody.amount,
430
467
  token: parsedBody.token
431
468
  };
469
+ if(clientInputUtxos!=null) {
470
+ if(parsedBody.exactOut) throw {
471
+ code: 20193,
472
+ msg: "amountAdjustUtxos cannot be specified for exactOut swaps!"
473
+ };
474
+
475
+ let btcFeeRate = await btcFeeRatePrefetch;
476
+ if(inputAmountAdjustments.amountFeeRate!=null && inputAmountAdjustments.amountFeeRate>btcFeeRate)
477
+ btcFeeRate = inputAmountAdjustments.amountFeeRate;
478
+
479
+ let feeAccumulator: number = 0;
480
+ let valueAccumulator: number = 0;
481
+ for(let utxo of clientInputUtxos) {
482
+ const cpfpAdditionalFee: number = utxo.cpfp==null ? 0 : Math.ceil(utxo.cpfp.effectiveVSize * Math.max(0, btcFeeRate - utxo.cpfp.effectiveFeeRate));
483
+ const spendFee: number = utxo.vSize * btcFeeRate;
484
+ const totalFee: number = cpfpAdditionalFee + spendFee;
485
+ if(totalFee > utxo.value) continue; //Skip detrimental UTXO
486
+ feeAccumulator += totalFee;
487
+ valueAccumulator += utxo.value;
488
+ }
489
+
490
+ let baseTxVSize: number = 10.5; // 4b version, 1b inputs, 1b outputs, 4b locktime, 0.5vB witness flag + witness elements count
491
+ //vault input and output
492
+ baseTxVSize += 32 + 4 + 1 + 4; //Input base
493
+ baseTxVSize += this.vaultSigner.getAddressType()==="p2tr" ? (1+1+65)/4 : (1+1+72+1+33)/4;
494
+ baseTxVSize += 8 + 1; //Output base
495
+ baseTxVSize += this.vaultSigner.getAddressType()==="p2tr" ? 34 : 22;
496
+ //opreturn output
497
+ baseTxVSize += 8 + 1; //Output base
498
+ const opReturnDataSize = spvVaultContract.toOpReturnData(parsedBody.address, parsedBody.gasAmount > 0 ? [0xffffffffffffffffn, 0xffffffffffffffffn] : [0xffffffffffffffffn]).length;
499
+ baseTxVSize += (opReturnDataSize <= 0x4b ? 2 : 3 /*Needs an OP_PUSHDATA1 opcode*/) + opReturnDataSize;
500
+ //LP output
501
+ baseTxVSize += 8 + 1; //Output base
502
+ baseTxVSize += this.bitcoin.getAddressType()==="p2tr" ? 34 : this.bitcoin.getAddressType()==="p2wpkh" ? 22 : 23;
503
+
504
+ const baseTxFee = Math.ceil(baseTxVSize) * btcFeeRate;
505
+ feeAccumulator += baseTxFee;
506
+
507
+ const amount = Math.floor(valueAccumulator - Math.ceil(feeAccumulator));
508
+ requestedAmount.amount = BigInt(amount);
509
+ }
510
+
432
511
  const gasTokenAmount = {
433
512
  input: false,
434
513
  amount: parsedBody.gasAmount * (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate) / 100_000n,
@@ -562,7 +641,9 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
562
641
 
563
642
  callerFeeShare: callerFeeShare.toString(10),
564
643
  frontingFeeShare: frontingFeeShare.toString(10),
565
- executionFeeShare: executionFeeShare.toString(10)
644
+ executionFeeShare: executionFeeShare.toString(10),
645
+
646
+ usedUtxoInputCalculation: clientInputUtxos!=null
566
647
  }
567
648
  });
568
649
  }));
@@ -185,8 +185,8 @@ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrust
185
185
  swap.adjustedOutput = swap.outputTokens;
186
186
  } else {
187
187
  //If lower than minimum then ignore
188
- if(sentSats < this.config.min) return;
189
- if(sentSats > this.config.max) {
188
+ if(sentSats < this.AmountAssertions.getSwapMinimum(swap.chainIdentifier)) return;
189
+ if(sentSats > this.AmountAssertions.getSwapMaximum(swap.chainIdentifier)) {
190
190
  swap.adjustedInput = sentSats;
191
191
  swap.btcTx = tx;
192
192
  swap.txId = tx.txid;