@atomiqlabs/sdk 8.7.6 → 8.8.3

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 (38) hide show
  1. package/dist/bitcoin/coinselect2/accumulative.d.ts +1 -0
  2. package/dist/bitcoin/coinselect2/accumulative.js +1 -1
  3. package/dist/bitcoin/coinselect2/blackjack.d.ts +1 -0
  4. package/dist/bitcoin/coinselect2/blackjack.js +1 -1
  5. package/dist/bitcoin/coinselect2/index.d.ts +3 -2
  6. package/dist/bitcoin/coinselect2/index.js +2 -2
  7. package/dist/bitcoin/coinselect2/utils.d.ts +7 -2
  8. package/dist/bitcoin/coinselect2/utils.js +45 -10
  9. package/dist/bitcoin/wallet/BitcoinWallet.d.ts +8 -25
  10. package/dist/bitcoin/wallet/BitcoinWallet.js +31 -18
  11. package/dist/bitcoin/wallet/IBitcoinWallet.d.ts +40 -2
  12. package/dist/bitcoin/wallet/SingleAddressBitcoinWallet.d.ts +7 -2
  13. package/dist/bitcoin/wallet/SingleAddressBitcoinWallet.js +10 -4
  14. package/dist/intermediaries/apis/IntermediaryAPI.d.ts +11 -1
  15. package/dist/intermediaries/apis/IntermediaryAPI.js +18 -3
  16. package/dist/swapper/Swapper.d.ts +41 -1
  17. package/dist/swapper/Swapper.js +69 -7
  18. package/dist/swaps/spv_swaps/SpvFromBTCSwap.d.ts +9 -3
  19. package/dist/swaps/spv_swaps/SpvFromBTCSwap.js +9 -5
  20. package/dist/swaps/spv_swaps/SpvFromBTCWrapper.d.ts +38 -6
  21. package/dist/swaps/spv_swaps/SpvFromBTCWrapper.js +178 -53
  22. package/dist/utils/BitcoinUtils.d.ts +2 -0
  23. package/dist/utils/BitcoinUtils.js +40 -1
  24. package/dist/utils/BitcoinWalletUtils.d.ts +2 -2
  25. package/package.json +1 -1
  26. package/src/bitcoin/coinselect2/accumulative.ts +2 -1
  27. package/src/bitcoin/coinselect2/blackjack.ts +2 -1
  28. package/src/bitcoin/coinselect2/index.ts +5 -4
  29. package/src/bitcoin/coinselect2/utils.ts +55 -14
  30. package/src/bitcoin/wallet/BitcoinWallet.ts +69 -57
  31. package/src/bitcoin/wallet/IBitcoinWallet.ts +44 -3
  32. package/src/bitcoin/wallet/SingleAddressBitcoinWallet.ts +12 -4
  33. package/src/intermediaries/apis/IntermediaryAPI.ts +21 -5
  34. package/src/swapper/Swapper.ts +82 -7
  35. package/src/swaps/spv_swaps/SpvFromBTCSwap.ts +20 -7
  36. package/src/swaps/spv_swaps/SpvFromBTCWrapper.ts +234 -58
  37. package/src/utils/BitcoinUtils.ts +41 -0
  38. package/src/utils/BitcoinWalletUtils.ts +2 -2
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SpvFromBTCWrapper = void 0;
3
+ exports.SpvFromBTCWrapper = exports.REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE = exports.REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE = void 0;
4
4
  const ISwapWrapper_1 = require("../ISwapWrapper");
5
5
  const base_1 = require("@atomiqlabs/base");
6
6
  const SpvFromBTCSwap_1 = require("./SpvFromBTCSwap");
@@ -15,6 +15,10 @@ const IntermediaryError_1 = require("../../errors/IntermediaryError");
15
15
  const btc_signer_1 = require("@scure/btc-signer");
16
16
  const RetryUtils_1 = require("../../utils/RetryUtils");
17
17
  const UserError_1 = require("../../errors/UserError");
18
+ const utils_2 = require("../../bitcoin/coinselect2/utils");
19
+ const BitcoinWallet_1 = require("../../bitcoin/wallet/BitcoinWallet");
20
+ exports.REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE = "p2tr";
21
+ exports.REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE = "p2wpkh";
18
22
  /**
19
23
  * New spv vault (UTXO-controlled vault) based swaps for Bitcoin -> Smart chain swaps not requiring
20
24
  * any initiation on the destination chain, and with the added possibility for the user to receive
@@ -209,26 +213,21 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
209
213
  *
210
214
  * @param amountData
211
215
  * @param options Options as passed to the swap creation function
212
- * @param pricePrefetch
213
- * @param nativeTokenPricePrefetch
214
216
  * @param abortController
215
217
  * @param contractVersion
216
218
  * @private
217
219
  */
218
- async preFetchCallerFeeShare(amountData, options, pricePrefetch, nativeTokenPricePrefetch, abortController, contractVersion) {
220
+ async preFetchCallerFeeInNativeToken(amountData, options, abortController, contractVersion) {
219
221
  if (options.unsafeZeroWatchtowerFee)
220
222
  return 0n;
221
223
  if (amountData.amount === 0n)
222
224
  return 0n;
223
225
  try {
224
- const [feePerBlock, btcRelayData, currentBtcBlock, claimFeeRate, nativeTokenPrice] = await Promise.all([
226
+ const [feePerBlock, btcRelayData, currentBtcBlock, claimFeeRate] = await Promise.all([
225
227
  this.btcRelay(contractVersion).getFeePerBlock(),
226
228
  this.btcRelay(contractVersion).getTipData(),
227
229
  this._btcRpc.getTipHeight(),
228
- this._contract(contractVersion).getClaimFee(this._chain.randomAddress()),
229
- nativeTokenPricePrefetch ?? (amountData.token === this._chain.getNativeCurrencyAddress() ?
230
- pricePrefetch :
231
- this._prices.preFetchPrice(this.chainIdentifier, this._chain.getNativeCurrencyAddress(), abortController.signal))
230
+ this._contract(contractVersion).getClaimFee(this._chain.randomAddress())
232
231
  ]);
233
232
  if (btcRelayData == null)
234
233
  throw new Error("Btc relay doesn't seem to be initialized!");
@@ -236,35 +235,58 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
236
235
  const blockDelta = Math.max(currentBtcBlock - currentBtcRelayBlock + this._options.maxConfirmations, 0);
237
236
  const totalFeeInNativeToken = ((BigInt(blockDelta) * feePerBlock) +
238
237
  (claimFeeRate * BigInt(this._options.maxTransactionsDelta))) * BigInt(Math.floor(options.feeSafetyFactor * 1000000)) / 1000000n;
239
- let payoutAmount;
240
- if (amountData.exactIn) {
241
- //Convert input amount in BTC to
242
- const amountInNativeToken = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, amountData.amount, this._chain.getNativeCurrencyAddress(), abortController.signal, nativeTokenPrice);
243
- payoutAmount = amountInNativeToken - totalFeeInNativeToken;
244
- }
245
- else {
246
- if (amountData.token === this._chain.getNativeCurrencyAddress()) {
247
- //Both amounts in same currency
248
- payoutAmount = amountData.amount;
249
- }
250
- else {
251
- //Need to convert both to native currency
252
- const btcAmount = await this._prices.getToBtcSwapAmount(this.chainIdentifier, amountData.amount, amountData.token, abortController.signal, await pricePrefetch);
253
- payoutAmount = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, btcAmount, this._chain.getNativeCurrencyAddress(), abortController.signal, nativeTokenPrice);
254
- }
255
- }
256
- this.logger.debug("preFetchCallerFeeShare(): Caller fee in native token: " + totalFeeInNativeToken.toString(10) + " total payout in native token: " + payoutAmount.toString(10));
257
- const callerFeeShare = ((totalFeeInNativeToken * 100000n) + payoutAmount - 1n) / payoutAmount; //Make sure to round up here
258
- if (callerFeeShare < 0n)
259
- return 0n;
260
- if (callerFeeShare >= 2n ** 20n)
261
- return 2n ** 20n - 1n;
262
- return callerFeeShare;
238
+ return totalFeeInNativeToken;
263
239
  }
264
240
  catch (e) {
265
241
  abortController.abort(e);
266
242
  }
267
243
  }
244
+ /**
245
+ * Pre-fetches caller (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
246
+ * provided abortController
247
+ *
248
+ * @param amountPrefetch
249
+ * @param totalFeeInNativeTokenPrefetch
250
+ * @param amountData
251
+ * @param options Options as passed to the swap creation function
252
+ * @param pricePrefetch
253
+ * @param nativeTokenPricePrefetch
254
+ * @param abortSignal
255
+ * @private
256
+ */
257
+ async computeCallerFeeShare(amountPrefetch, totalFeeInNativeTokenPrefetch, amountData, options, pricePrefetch, nativeTokenPricePrefetch, abortSignal) {
258
+ if (options.unsafeZeroWatchtowerFee)
259
+ return 0n;
260
+ const amount = await (0, Utils_1.throwIfUndefined)(amountPrefetch, "Cannot get swap amount!");
261
+ if (amount === 0n)
262
+ return 0n;
263
+ const totalFeeInNativeToken = await (0, Utils_1.throwIfUndefined)(totalFeeInNativeTokenPrefetch, "Cannot get total fee in native token!");
264
+ const nativeTokenPrice = await nativeTokenPricePrefetch;
265
+ let payoutAmount;
266
+ if (amountData.exactIn) {
267
+ //Convert input amount in BTC to
268
+ const amountInNativeToken = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, amount, this._chain.getNativeCurrencyAddress(), abortSignal, nativeTokenPrice);
269
+ payoutAmount = amountInNativeToken - totalFeeInNativeToken;
270
+ }
271
+ else {
272
+ if (amountData.token === this._chain.getNativeCurrencyAddress()) {
273
+ //Both amounts in same currency
274
+ payoutAmount = amount;
275
+ }
276
+ else {
277
+ //Need to convert both to native currency
278
+ const btcAmount = await this._prices.getToBtcSwapAmount(this.chainIdentifier, amount, amountData.token, abortSignal, await pricePrefetch);
279
+ payoutAmount = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, btcAmount, this._chain.getNativeCurrencyAddress(), abortSignal, nativeTokenPrice);
280
+ }
281
+ }
282
+ this.logger.debug("computeCallerFeeShare(): Caller fee in native token: " + totalFeeInNativeToken.toString(10) + " total payout in native token: " + payoutAmount.toString(10));
283
+ const callerFeeShare = ((totalFeeInNativeToken * 100000n) + payoutAmount - 1n) / payoutAmount; //Make sure to round up here
284
+ if (callerFeeShare < 0n)
285
+ return 0n;
286
+ if (callerFeeShare >= 2n ** 20n)
287
+ return 2n ** 20n - 1n;
288
+ return callerFeeShare;
289
+ }
268
290
  /**
269
291
  * Verifies response returned from intermediary
270
292
  *
@@ -273,13 +295,14 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
273
295
  * @param lp Intermediary
274
296
  * @param options Options as passed to the swap creation function
275
297
  * @param callerFeeShare
276
- * @param bitcoinFeeRatePromise Maximum accepted fee rate from the LPs
298
+ * @param maxBitcoinFeeRatePromise Maximum accepted fee rate from the LPs
299
+ * @param bitcoinFeeRatePromise
277
300
  * @param abortSignal
278
301
  * @private
279
302
  * @throws {IntermediaryError} in case the response is invalid
280
303
  */
281
- async verifyReturnedData(resp, amountData, lp, options, callerFeeShare, bitcoinFeeRatePromise, abortSignal) {
282
- const btcFeeRate = await (0, Utils_1.throwIfUndefined)(bitcoinFeeRatePromise, "Bitcoin fee rate promise failed!");
304
+ async verifyReturnedData(resp, amountData, lp, options, callerFeeShare, maxBitcoinFeeRatePromise, bitcoinFeeRatePromise, abortSignal) {
305
+ const btcFeeRate = await (0, Utils_1.throwIfUndefined)(maxBitcoinFeeRatePromise, "Bitcoin fee rate promise failed!");
283
306
  abortSignal.throwIfAborted();
284
307
  if (btcFeeRate != null && resp.btcFeeRate > btcFeeRate)
285
308
  throw new IntermediaryError_1.IntermediaryError(`Required bitcoin fee rate returned from the LP is too high! Maximum accepted: ${btcFeeRate} sats/vB, required by LP: ${resp.btcFeeRate} sats/vB`);
@@ -288,11 +311,13 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
288
311
  let vaultScript;
289
312
  let vaultAddressType;
290
313
  let btcAddressScript;
314
+ let btcAddressType;
291
315
  //Ensure valid btc addresses returned
292
316
  try {
293
317
  vaultScript = (0, BitcoinUtils_1.toOutputScript)(this._options.bitcoinNetwork, resp.vaultBtcAddress);
294
318
  vaultAddressType = (0, BitcoinUtils_1.toCoinselectAddressType)(vaultScript);
295
319
  btcAddressScript = (0, BitcoinUtils_1.toOutputScript)(this._options.bitcoinNetwork, resp.btcAddress);
320
+ btcAddressType = (0, BitcoinUtils_1.toCoinselectAddressType)(btcAddressScript);
296
321
  }
297
322
  catch (e) {
298
323
  throw new IntermediaryError_1.IntermediaryError("Invalid btc address data returned", e);
@@ -302,7 +327,8 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
302
327
  resp.vaultId < 0n || //Ensure vaultId is not negative
303
328
  vaultScript == null || //Make sure vault script is parsable and of known type
304
329
  btcAddressScript == null || //Make sure btc address script is parsable and of known type
305
- vaultAddressType === "p2pkh" || vaultAddressType === "p2sh-p2wpkh" || //Constrain the vault script type to witness types
330
+ btcAddressType !== exports.REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE || //Constrain the btc address script type
331
+ vaultAddressType !== exports.REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE || //Constrain the vault script type
306
332
  decodedUtxo.length !== 2 || decodedUtxo[0].length !== 64 || isNaN(parseInt(decodedUtxo[1])) || //Check valid UTXO
307
333
  resp.btcFeeRate < 1 || resp.btcFeeRate > 10000 //Sanity check on the returned BTC fee rate
308
334
  )
@@ -345,8 +371,24 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
345
371
  const tokenData = vault.getTokenData();
346
372
  //Amounts - make sure the amounts match
347
373
  if (amountData.exactIn) {
348
- if (resp.btcAmount !== amountData.amount)
349
- throw new IntermediaryError_1.IntermediaryError("Invalid amount returned");
374
+ if (!resp.usedUtxoInputCalculation) {
375
+ //Legacy calculation
376
+ if (resp.btcAmount !== amountData.amount)
377
+ throw new IntermediaryError_1.IntermediaryError("Invalid amount returned");
378
+ }
379
+ else {
380
+ //Implies the raw UTXOs were passed for amount derivation
381
+ //Verify the derivation was done correctly
382
+ if (options.sourceWalletUtxos == null)
383
+ throw new IntermediaryError_1.IntermediaryError("Invalid usedUtxoInputCalcuation return value");
384
+ if (bitcoinFeeRatePromise == null)
385
+ throw new Error("bitcoinFeeRatePromise must be passed for UTXO-based input amount calculation checks");
386
+ const walletUtxos = await options.sourceWalletUtxos;
387
+ const bitcoinFeeRate = await (0, Utils_1.throwIfUndefined)(bitcoinFeeRatePromise, "Failed to fetch bitcoin fee rate!");
388
+ const { balance } = BitcoinWallet_1.BitcoinWallet.getSpendableBalance(walletUtxos, Math.max(resp.btcFeeRate, bitcoinFeeRate), this.getDummySwapPsbt(options.gasAmount !== 0n), exports.REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE);
389
+ if (resp.btcAmount !== balance)
390
+ throw new IntermediaryError_1.IntermediaryError(`Invalid amount returned, expected: ${balance.toString(10)}, got: ${resp.btcAmount.toString(10)}`);
391
+ }
350
392
  }
351
393
  else {
352
394
  //Check the difference between amount adjusted due to scaling to raw amount
@@ -447,6 +489,58 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
447
489
  vaultUtxoValue
448
490
  };
449
491
  }
492
+ async amountPrefetch(amountData, bitcoinFeeRatePromise, walletUtxosPromise, includeGas, abortController) {
493
+ if (amountData.amount != null)
494
+ return amountData.amount;
495
+ try {
496
+ const bitcoinFeeRate = await (0, Utils_1.throwIfUndefined)(bitcoinFeeRatePromise, "Cannot fetch Bitcoin fee rate!");
497
+ if (walletUtxosPromise == null)
498
+ throw new UserError_1.UserError("Cannot use empty amount without passing UTXOs!");
499
+ const walletUtxos = await walletUtxosPromise;
500
+ if (walletUtxos.length === 0)
501
+ throw new UserError_1.UserError("Wallet doesn't have any BTC balance");
502
+ const spendableBalance = await BitcoinWallet_1.BitcoinWallet.getSpendableBalance(walletUtxos, bitcoinFeeRate, this.getDummySwapPsbt(includeGas), exports.REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE);
503
+ return spendableBalance.balance;
504
+ }
505
+ catch (e) {
506
+ abortController.abort(e);
507
+ }
508
+ }
509
+ bitcoinFeeRatePrefetch(options, abortController) {
510
+ let bitcoinFeeRatePromise;
511
+ if (options?.sourceWalletUtxos != null) {
512
+ if (options.bitcoinFeeRate != null) {
513
+ bitcoinFeeRatePromise = options.bitcoinFeeRate.then(value => {
514
+ if (options.maxAllowedBitcoinFeeRate != Infinity && options.maxAllowedBitcoinFeeRate < value)
515
+ throw new Error("Passed `maxAllowedBitcoinFeeRate` cannot be lower than `bitcoinFeeRate`");
516
+ return value;
517
+ });
518
+ }
519
+ else {
520
+ bitcoinFeeRatePromise = this._btcRpc.getFeeRate().then(value => {
521
+ if (options.maxAllowedBitcoinFeeRate != Infinity && value > options.maxAllowedBitcoinFeeRate)
522
+ return options.maxAllowedBitcoinFeeRate;
523
+ return value;
524
+ });
525
+ }
526
+ bitcoinFeeRatePromise = bitcoinFeeRatePromise.catch(e => {
527
+ abortController.abort(e);
528
+ return undefined;
529
+ });
530
+ }
531
+ const maxBitcoinFeeRatePromise = options.maxAllowedBitcoinFeeRate != Infinity
532
+ ? Promise.resolve(options.maxAllowedBitcoinFeeRate)
533
+ : (0, Utils_1.throwIfUndefined)(bitcoinFeeRatePromise ?? options.bitcoinFeeRate ?? this._btcRpc.getFeeRate())
534
+ .then(x => this._options.maxBtcFeeOffset + (x * this._options.maxBtcFeeMultiplier))
535
+ .catch(e => {
536
+ abortController.abort(e);
537
+ return undefined;
538
+ });
539
+ return {
540
+ bitcoinFeeRatePromise,
541
+ maxBitcoinFeeRatePromise
542
+ };
543
+ }
450
544
  /**
451
545
  * Returns a newly created Bitcoin -> Smart chain swap using the SPV vault (UTXO-controlled vault) swap protocol,
452
546
  * with the passed amount. Also allows specifying additional "gas drop" native token that the receipient receives
@@ -464,13 +558,25 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
464
558
  gasAmount: this.parseGasAmount(options?.gasAmount),
465
559
  unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false,
466
560
  feeSafetyFactor: options?.feeSafetyFactor ?? 1.25,
467
- maxAllowedBitcoinFeeRate: options?.maxAllowedBitcoinFeeRate ?? options?.maxAllowedNetworkFeeRate ?? Infinity
561
+ maxAllowedBitcoinFeeRate: options?.maxAllowedBitcoinFeeRate ?? options?.maxAllowedNetworkFeeRate ?? Infinity,
562
+ sourceWalletUtxos: options?.sourceWalletUtxos == undefined
563
+ ? undefined
564
+ : options?.sourceWalletUtxos instanceof Promise ? options.sourceWalletUtxos : Promise.resolve(options.sourceWalletUtxos),
565
+ bitcoinFeeRate: options?.bitcoinFeeRate == undefined
566
+ ? undefined
567
+ : options?.bitcoinFeeRate instanceof Promise ? options.bitcoinFeeRate : Promise.resolve(options.bitcoinFeeRate),
468
568
  };
469
569
  if (_options.gasAmount !== 0n &&
470
570
  (this._chain.shouldGetNativeTokenDrop != null
471
571
  ? !this._chain.shouldGetNativeTokenDrop(amountData.token)
472
572
  : amountData.token === this._chain.getNativeCurrencyAddress()))
473
573
  throw new UserError_1.UserError("Cannot specify `gasAmount` for swaps to a native token!");
574
+ if (amountData.amount == null && options?.sourceWalletUtxos == null)
575
+ throw new UserError_1.UserError("Source wallet UTXOs need to be passed when amount is null!");
576
+ if (amountData.amount == null && !amountData.exactIn)
577
+ throw new UserError_1.UserError("Amount can be null only for exactIn swaps!");
578
+ if (amountData.amount != null && options?.sourceWalletUtxos != null)
579
+ throw new UserError_1.UserError("Source wallet UTXOs cannot be passed while specifying an input amount!");
474
580
  const lpVersions = Intermediary_1.Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
475
581
  const _abortController = (0, Utils_1.extendAbortController)(abortSignal);
476
582
  const pricePrefetchPromise = this.preFetchPrice(amountData, _abortController.signal);
@@ -481,14 +587,10 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
481
587
  undefined :
482
588
  this.preFetchPrice({ token: nativeTokenAddress }, _abortController.signal);
483
589
  const callerFeePrefetchPromise = (0, Utils_1.mapArrayToObject)(lpVersions, (contractVersion) => {
484
- return this.preFetchCallerFeeShare(amountData, _options, pricePrefetchPromise, gasTokenPricePrefetchPromise, _abortController, contractVersion);
590
+ return this.preFetchCallerFeeInNativeToken(amountData, _options, _abortController, contractVersion);
485
591
  });
486
- const bitcoinFeeRatePromise = _options.maxAllowedBitcoinFeeRate != Infinity ?
487
- Promise.resolve(_options.maxAllowedBitcoinFeeRate) :
488
- this._btcRpc.getFeeRate().then(x => this._options.maxBtcFeeOffset + (x * this._options.maxBtcFeeMultiplier)).catch(e => {
489
- _abortController.abort(e);
490
- return undefined;
491
- });
592
+ const { maxBitcoinFeeRatePromise, bitcoinFeeRatePromise } = this.bitcoinFeeRatePrefetch(_options, _abortController);
593
+ const amountPromise = this.amountPrefetch(amountData, maxBitcoinFeeRatePromise, _options.sourceWalletUtxos, _options.gasAmount !== 0n, _abortController);
492
594
  return lps.map(lp => {
493
595
  return {
494
596
  intermediary: lp,
@@ -497,29 +599,46 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
497
599
  throw new Error("LP service for processing spv vault swaps not found!");
498
600
  const version = lp.getContractVersion(this.chainIdentifier);
499
601
  const abortController = (0, Utils_1.extendAbortController)(_abortController.signal);
602
+ const callerFeeRatePromise = this.computeCallerFeeShare(amountPromise, callerFeePrefetchPromise[version], amountData, _options, pricePrefetchPromise, gasTokenPricePrefetchPromise, abortController.signal);
500
603
  try {
501
604
  const resp = await (0, RetryUtils_1.tryWithRetries)(async (retryCount) => {
502
605
  return await IntermediaryAPI_1.IntermediaryAPI.prepareSpvFromBTC(this.chainIdentifier, lp.url, {
503
606
  address: recipient,
504
- amount: amountData.amount,
607
+ amount: (0, Utils_1.throwIfUndefined)(amountPromise, "Failed to compute swap amount"),
505
608
  token: amountData.token.toString(),
506
609
  exactOut: !amountData.exactIn,
507
610
  gasToken: nativeTokenAddress,
508
611
  gasAmount: _options.gasAmount,
509
- callerFeeRate: (0, Utils_1.throwIfUndefined)(callerFeePrefetchPromise[version], "Caller fee prefetch failed!"),
612
+ callerFeeRate: (0, Utils_1.throwIfUndefined)(callerFeeRatePromise, "Caller fee prefetch failed!"),
510
613
  frontingFeeRate: 0n,
511
614
  stickyAddress: options?.stickyAddress,
615
+ amountUtxos: _options.sourceWalletUtxos != null
616
+ ? _options.sourceWalletUtxos.then(utxos => {
617
+ if (utxos.length === 0)
618
+ return undefined;
619
+ return utxos.map(utxo => ({
620
+ value: utxo.value,
621
+ vSize: utils_2.utils.inputBytes({ type: utxo.type }),
622
+ cpfp: utxo.cpfp == null ? undefined : { effectiveVSize: utxo.cpfp?.txVsize, effectiveFeeRate: utxo.cpfp?.txEffectiveFeeRate }
623
+ }));
624
+ })
625
+ : undefined,
626
+ amountFeeRate: bitcoinFeeRatePromise,
512
627
  additionalParams
513
628
  }, this._options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : undefined);
514
629
  }, undefined, e => e instanceof RequestError_1.RequestError, abortController.signal);
515
630
  this.logger.debug("create(" + lp.url + "): LP response: ", resp);
516
- const callerFeeShare = (await callerFeePrefetchPromise[version]);
631
+ const callerFeeShare = await callerFeeRatePromise;
632
+ const amount = await (0, Utils_1.throwIfUndefined)(amountPromise);
517
633
  const [pricingInfo, gasPricingInfo, { vault, vaultUtxoValue }] = await Promise.all([
518
634
  this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.SPV_VAULT_FROM_BTC], false, resp.btcAmountSwap, resp.total * (100000n + callerFeeShare) / 100000n, amountData.token, { swapFeeBtc: resp.swapFeeBtc }, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal),
519
635
  _options.gasAmount === 0n ? Promise.resolve(undefined) : this.verifyReturnedPrice({ ...lp.services[SwapType_1.SwapType.SPV_VAULT_FROM_BTC], swapBaseFee: 0 }, //Base fee should be charged only on the amount, not on gas
520
636
  false, resp.btcAmountGas, resp.totalGas * (100000n + callerFeeShare) / 100000n, nativeTokenAddress, { swapFeeBtc: resp.gasSwapFeeBtc }, gasTokenPricePrefetchPromise, usdPricePrefetchPromise, abortController.signal),
521
- this.verifyReturnedData(resp, amountData, lp, _options, callerFeeShare, bitcoinFeeRatePromise, abortController.signal)
637
+ this.verifyReturnedData(resp, { ...amountData, amount }, lp, _options, callerFeeShare, maxBitcoinFeeRatePromise, bitcoinFeeRatePromise, abortController.signal)
522
638
  ]);
639
+ let minimumBtcFeeRate = resp.btcFeeRate;
640
+ if (bitcoinFeeRatePromise != null)
641
+ minimumBtcFeeRate = Math.max(minimumBtcFeeRate, await (0, Utils_1.throwIfUndefined)(bitcoinFeeRatePromise));
523
642
  const swapInit = {
524
643
  pricingInfo,
525
644
  url: lp.url,
@@ -540,7 +659,7 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
540
659
  btcAmount: resp.btcAmount,
541
660
  btcAmountSwap: resp.btcAmountSwap,
542
661
  btcAmountGas: resp.btcAmountGas,
543
- minimumBtcFeeRate: resp.btcFeeRate,
662
+ minimumBtcFeeRate,
544
663
  outputTotalSwap: resp.total,
545
664
  outputSwapToken: amountData.token,
546
665
  outputTotalGas: resp.totalGas,
@@ -558,6 +677,12 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
558
677
  return quote;
559
678
  }
560
679
  catch (e) {
680
+ if (e instanceof RequestError_1.OutOfBoundsError) {
681
+ const amountResult = await amountPromise.catch(() => undefined);
682
+ if (_options.sourceWalletUtxos != null && amountResult != null && amountResult <= 0n) {
683
+ e = new UserError_1.UserError("Wallet doesn't have enough BTC balance to cover transaction fees");
684
+ }
685
+ }
561
686
  abortController.abort(e);
562
687
  throw e;
563
688
  }
@@ -682,7 +807,7 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
682
807
  allowLegacyWitnessUtxo: true,
683
808
  allowUnknownOutputs: true
684
809
  });
685
- const randomVaultOutScript = btc_signer_1.OutScript.encode({ type: "tr", pubkey: Buffer.from("0101010101010101010101010101010101010101010101010101010101010101", "hex") });
810
+ const randomVaultOutScript = (0, BitcoinUtils_1.getDummyOutputScript)(exports.REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE);
686
811
  psbt.addInput({
687
812
  txid: (0, Utils_1.randomBytes)(32),
688
813
  index: 0,
@@ -7,6 +7,8 @@ import { CoinselectAddressTypes } from "../bitcoin/coinselect2";
7
7
  export declare function fromOutputScript(network: BTC_NETWORK, outputScriptHex: string): string;
8
8
  export declare function toOutputScript(network: BTC_NETWORK, address: string): Buffer;
9
9
  export declare function toCoinselectAddressType(outputScript: Uint8Array): CoinselectAddressTypes;
10
+ export declare function getDummyOutputScript(type: CoinselectAddressTypes): Uint8Array;
11
+ export declare function getDummyAddress(network: BTC_NETWORK, type: CoinselectAddressTypes): string;
10
12
  /**
11
13
  * General parsers for PSBTs, can parse hex or base64 encoded PSBTs
12
14
  * @param _psbt
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parsePsbtTransaction = exports.toCoinselectAddressType = exports.toOutputScript = exports.fromOutputScript = void 0;
3
+ exports.parsePsbtTransaction = exports.getDummyAddress = exports.getDummyOutputScript = exports.toCoinselectAddressType = exports.toOutputScript = exports.fromOutputScript = void 0;
4
4
  const utils_1 = require("@scure/btc-signer/utils");
5
5
  const buffer_1 = require("buffer");
6
6
  const btc_signer_1 = require("@scure/btc-signer");
7
+ const Utils_1 = require("./Utils");
7
8
  function fromOutputScript(network, outputScriptHex) {
8
9
  return (0, btc_signer_1.Address)(network).encode(btc_signer_1.OutScript.decode(buffer_1.Buffer.from(outputScriptHex, "hex")));
9
10
  }
@@ -71,6 +72,44 @@ function toCoinselectAddressType(outputScript) {
71
72
  throw new Error("Unrecognized address type!");
72
73
  }
73
74
  exports.toCoinselectAddressType = toCoinselectAddressType;
75
+ function getDummySpec(type) {
76
+ switch (type) {
77
+ case "p2pkh":
78
+ return {
79
+ type: "pkh",
80
+ hash: (0, Utils_1.randomBytes)(20)
81
+ };
82
+ case "p2sh-p2wpkh":
83
+ return {
84
+ type: "sh",
85
+ hash: (0, Utils_1.randomBytes)(20)
86
+ };
87
+ case "p2wpkh":
88
+ return {
89
+ type: "wpkh",
90
+ hash: (0, Utils_1.randomBytes)(20)
91
+ };
92
+ case "p2wsh":
93
+ return {
94
+ type: "wsh",
95
+ hash: (0, Utils_1.randomBytes)(32)
96
+ };
97
+ case "p2tr":
98
+ return {
99
+ type: "tr",
100
+ pubkey: buffer_1.Buffer.from("0101010101010101010101010101010101010101010101010101010101010101", "hex")
101
+ };
102
+ }
103
+ throw new Error("Unrecognized address type!");
104
+ }
105
+ function getDummyOutputScript(type) {
106
+ return btc_signer_1.OutScript.encode(getDummySpec(type));
107
+ }
108
+ exports.getDummyOutputScript = getDummyOutputScript;
109
+ function getDummyAddress(network, type) {
110
+ return (0, btc_signer_1.Address)(network).encode(getDummySpec(type));
111
+ }
112
+ exports.getDummyAddress = getDummyAddress;
74
113
  /**
75
114
  * General parsers for PSBTs, can parse hex or base64 encoded PSBTs
76
115
  * @param _psbt
@@ -1,7 +1,7 @@
1
1
  import { IBitcoinWallet } from "../bitcoin/wallet/IBitcoinWallet";
2
2
  import { BTC_NETWORK } from "@scure/btc-signer/utils";
3
- import { BitcoinRpcWithAddressIndex } from "@atomiqlabs/base";
3
+ import { BitcoinNetwork, BitcoinRpcWithAddressIndex } from "@atomiqlabs/base";
4
4
  export declare function toBitcoinWallet(_bitcoinWallet: IBitcoinWallet | {
5
5
  address: string;
6
6
  publicKey: string;
7
- }, btcRpc: BitcoinRpcWithAddressIndex<any>, bitcoinNetwork: BTC_NETWORK): IBitcoinWallet;
7
+ }, btcRpc: BitcoinRpcWithAddressIndex<any>, bitcoinNetwork: BTC_NETWORK | BitcoinNetwork): IBitcoinWallet;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/sdk",
3
- "version": "8.7.6",
3
+ "version": "8.8.3",
4
4
  "description": "atomiq labs SDK for cross-chain swaps between smart chains and bitcoin",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -15,9 +15,10 @@ export function accumulative (
15
15
  ): {
16
16
  inputs?: CoinselectTxInput[],
17
17
  outputs?: CoinselectTxOutput[],
18
+ effectiveFeeRate?: number,
18
19
  fee: number
19
20
  } {
20
- if (!isFinite(utils.uintOrNaN(feeRate))) throw new Error("Invalid feeRate passed!");
21
+ if (!isFinite(utils.numberOrNaN(feeRate))) throw new Error("Invalid feeRate passed!");
21
22
 
22
23
  const inputs = requiredInputs==null ? [] : [...requiredInputs];
23
24
  let bytesAccum = utils.transactionBytes(inputs, outputs, type);
@@ -11,9 +11,10 @@ export function blackjack (
11
11
  ): {
12
12
  inputs?: CoinselectTxInput[],
13
13
  outputs?: CoinselectTxOutput[],
14
+ effectiveFeeRate?: number,
14
15
  fee: number
15
16
  } {
16
- if (!isFinite(utils.uintOrNaN(feeRate))) throw new Error("Invalid feeRate passed!");
17
+ if (!isFinite(utils.numberOrNaN(feeRate))) throw new Error("Invalid feeRate passed!");
17
18
 
18
19
  const inputs = requiredInputs==null ? [] : [...requiredInputs];
19
20
  let bytesAccum = utils.transactionBytes(inputs, outputs, type);
@@ -20,6 +20,7 @@ export function coinSelect (
20
20
  ): {
21
21
  inputs?: CoinselectTxInput[],
22
22
  outputs?: CoinselectTxOutput[],
23
+ effectiveFeeRate?: number,
23
24
  fee: number
24
25
  } {
25
26
  // order by descending value, minus the inputs approximate fee
@@ -38,16 +39,16 @@ export function coinSelect (
38
39
  }
39
40
 
40
41
  export function maxSendable (
41
- utxos: CoinselectTxInput[],
42
+ utxos: Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">[],
42
43
  output: {script: Buffer, type: CoinselectAddressTypes},
43
44
  feeRate: number,
44
- requiredInputs?: CoinselectTxInput[],
45
+ requiredInputs?: Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">[],
45
46
  additionalOutputs?: {script: Buffer, value: number}[],
46
47
  ): {
47
48
  value: number,
48
49
  fee: number
49
50
  } {
50
- if (!isFinite(utils.uintOrNaN(feeRate))) throw new Error("Invalid feeRate passed!");
51
+ if (!isFinite(utils.numberOrNaN(feeRate))) throw new Error("Invalid feeRate passed!");
51
52
 
52
53
  const outputs = additionalOutputs ?? [];
53
54
  const inputs = requiredInputs ?? [];
@@ -61,7 +62,7 @@ export function maxSendable (
61
62
  const utxoBytes = utils.inputBytes(utxo);
62
63
  const utxoFee = feeRate * utxoBytes;
63
64
  let cpfpFee = 0;
64
- if(utxo.cpfp!=null && utxo.cpfp.txEffectiveFeeRate<feeRate) cpfpFee = utxo.cpfp.txVsize*(feeRate - utxo.cpfp.txEffectiveFeeRate);
65
+ if(utxo.cpfp!=null && utxo.cpfp.txEffectiveFeeRate<feeRate) cpfpFee = Math.ceil(utxo.cpfp.txVsize*(feeRate - utxo.cpfp.txEffectiveFeeRate));
65
66
  const utxoValue = utils.uintOrNaN(utxo.value);
66
67
 
67
68
  // skip detrimental input
@@ -132,6 +132,13 @@ function transactionBytes (
132
132
  return Math.ceil(size);
133
133
  }
134
134
 
135
+ function numberOrNaN(v: number): number {
136
+ if (typeof v !== 'number') return NaN;
137
+ if (!isFinite(v)) return NaN;
138
+ if (v < 0) return NaN;
139
+ return v;
140
+ }
141
+
135
142
  function uintOrNaN(v: number): number {
136
143
  if (typeof v !== 'number') return NaN;
137
144
  if (!isFinite(v)) return NaN;
@@ -148,41 +155,73 @@ function sumOrNaN(range: {value: number}[]): number {
148
155
  return range.reduce((a, x) => a + uintOrNaN(x.value), 0);
149
156
  }
150
157
 
151
- function finalize(
152
- inputs: CoinselectTxInput[],
158
+ function finalize<T extends Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">>(
159
+ inputs: T[],
153
160
  outputs: CoinselectTxOutput[],
154
161
  feeRate: number,
155
- changeType: CoinselectAddressTypes,
162
+ changeType: CoinselectAddressTypes | null,
156
163
  cpfpAddFee: number = 0
157
164
  ): {
158
- inputs?: CoinselectTxInput[],
165
+ inputs?: T[],
159
166
  outputs?: CoinselectTxOutput[],
167
+ effectiveFeeRate?: number,
160
168
  fee: number
161
169
  } {
162
- const bytesAccum = transactionBytes(inputs, outputs, changeType);
170
+ const bytesAccum = transactionBytes(inputs, outputs, changeType ?? undefined);
163
171
  logger.debug("finalize(): Transaction bytes: ", bytesAccum);
164
172
 
165
- const feeAfterExtraOutput = (feeRate * (bytesAccum + outputBytes({type: changeType}))) + cpfpAddFee;
166
- logger.debug("finalize(): TX fee after adding change output: ", feeAfterExtraOutput);
167
- const remainderAfterExtraOutput = Math.floor(sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput));
168
- logger.debug("finalize(): Leaves change (changeType="+changeType+") value: ", remainderAfterExtraOutput);
173
+ if(changeType!=null) {
174
+ const feeAfterExtraOutput = (feeRate * (bytesAccum + outputBytes({type: changeType}))) + cpfpAddFee;
175
+ logger.debug("finalize(): TX fee after adding change output: ", feeAfterExtraOutput);
176
+ const remainderAfterExtraOutput = Math.floor(sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput));
177
+ logger.debug("finalize(): Leaves change (changeType="+changeType+") value: ", remainderAfterExtraOutput);
169
178
 
170
- // is it worth a change output?
171
- if (remainderAfterExtraOutput >= dustThreshold({type: changeType})) {
172
- outputs = outputs.concat({ value: remainderAfterExtraOutput, type: changeType })
179
+ // is it worth a change output?
180
+ if (remainderAfterExtraOutput >= dustThreshold({type: changeType})) {
181
+ outputs = outputs.concat({ value: remainderAfterExtraOutput, type: changeType })
182
+ }
173
183
  }
174
184
 
175
185
  const fee = sumOrNaN(inputs) - sumOrNaN(outputs);
176
186
  logger.debug("finalize(): Re-calculated total fee: ", fee);
177
- if (!isFinite(fee)) return { fee: (feeRate * bytesAccum) + cpfpAddFee }
187
+ if (!isFinite(fee) || fee<0) return { fee: (feeRate * bytesAccum) + cpfpAddFee }
188
+
189
+ let txVSize = utils.transactionBytes(inputs, outputs);
190
+ let txFee = fee;
191
+ const cpfpSortedInputs = [...inputs].sort(
192
+ (a, b) => (b.cpfp?.txEffectiveFeeRate ?? 0) - (a.cpfp?.txEffectiveFeeRate ?? 0)
193
+ );
194
+ cpfpSortedInputs.forEach(input => {
195
+ if(input.cpfp==null) return;
196
+ const currentEffectiveFeeRate = txFee / txVSize;
197
+ if(currentEffectiveFeeRate > input.cpfp.txEffectiveFeeRate) {
198
+ txVSize += input.cpfp.txVsize;
199
+ txFee += input.cpfp.txVsize * input.cpfp.txEffectiveFeeRate;
200
+ }
201
+ }
202
+ );
178
203
 
179
204
  return {
180
205
  inputs: inputs,
181
206
  outputs: outputs,
207
+ effectiveFeeRate: txFee / txVSize,
182
208
  fee: fee
183
209
  }
184
210
  }
185
211
 
212
+ function isDetrimentalInput(
213
+ feeRate: number,
214
+ utxo: Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">
215
+ ) {
216
+ const utxoBytes = utils.inputBytes(utxo);
217
+ const utxoFee = feeRate * utxoBytes;
218
+ let cpfpFee = 0;
219
+ if(utxo.cpfp!=null && utxo.cpfp.txEffectiveFeeRate<feeRate) cpfpFee = Math.ceil(utxo.cpfp.txVsize*(feeRate - utxo.cpfp.txEffectiveFeeRate));
220
+
221
+ // skip detrimental input
222
+ return utxoFee + cpfpFee > utxo.value;
223
+ }
224
+
186
225
  export const utils = {
187
226
  dustThreshold: dustThreshold,
188
227
  finalize: finalize,
@@ -191,5 +230,7 @@ export const utils = {
191
230
  sumOrNaN: sumOrNaN,
192
231
  sumForgiving: sumForgiving,
193
232
  transactionBytes: transactionBytes,
194
- uintOrNaN: uintOrNaN
233
+ uintOrNaN: uintOrNaN,
234
+ numberOrNaN: numberOrNaN,
235
+ isDetrimentalInput
195
236
  };