@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
@@ -22,9 +22,14 @@ import {ISwapPrice} from "../../prices/abstract/ISwapPrice";
22
22
  import {EventEmitter} from "events";
23
23
  import {Intermediary} from "../../intermediaries/Intermediary";
24
24
  import {extendAbortController, mapArrayToObject, randomBytes, throwIfUndefined} from "../../utils/Utils";
25
- import {fromOutputScript, toCoinselectAddressType, toOutputScript} from "../../utils/BitcoinUtils";
25
+ import {
26
+ fromOutputScript,
27
+ getDummyOutputScript,
28
+ toCoinselectAddressType,
29
+ toOutputScript
30
+ } from "../../utils/BitcoinUtils";
26
31
  import {IntermediaryAPI, SpvFromBTCPrepareResponseType} from "../../intermediaries/apis/IntermediaryAPI";
27
- import {RequestError} from "../../errors/RequestError";
32
+ import {OutOfBoundsError, RequestError} from "../../errors/RequestError";
28
33
  import {IntermediaryError} from "../../errors/IntermediaryError";
29
34
  import {CoinselectAddressTypes} from "../../bitcoin/coinselect2";
30
35
  import {OutScript, Transaction} from "@scure/btc-signer";
@@ -33,8 +38,10 @@ import {IClaimableSwapWrapper} from "../IClaimableSwapWrapper";
33
38
  import {AmountData} from "../../types/AmountData";
34
39
  import {tryWithRetries} from "../../utils/RetryUtils";
35
40
  import {AllOptional} from "../../utils/TypeUtils";
36
- import {fromHumanReadableString} from "../../utils/TokenUtils";
37
41
  import {UserError} from "../../errors/UserError";
42
+ import {BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet} from "../../bitcoin/wallet/IBitcoinWallet";
43
+ import {utils} from "../../bitcoin/coinselect2/utils";
44
+ import {BitcoinWallet} from "../../bitcoin/wallet/BitcoinWallet";
38
45
 
39
46
  export type SpvFromBTCOptions = {
40
47
  /**
@@ -75,6 +82,16 @@ export type SpvFromBTCOptions = {
75
82
  * whitelist-only wallets
76
83
  */
77
84
  stickyAddress?: boolean,
85
+ /**
86
+ * A bitcoin wallet UTXOs to fully use as an input for this swap, use this option along with passing `amount` as
87
+ * `undefined` when you want to swap the full BTC balance of the wallet in a single swap
88
+ */
89
+ sourceWalletUtxos?: BitcoinWalletUtxoBase[] | Promise<BitcoinWalletUtxoBase[]>,
90
+ /**
91
+ * Bitcoin fee rate to use when deriving `maxAllowedBitcoinFeeRate` and when calculating the input amount based
92
+ * on the `sourceWalletUtxos`
93
+ */
94
+ bitcoinFeeRate?: Promise<number> | number,
78
95
 
79
96
  /**
80
97
  * @deprecated Use `maxAllowedBitcoinFeeRate` instead!
@@ -94,6 +111,9 @@ export type SpvFromBTCWrapperOptions = ISwapWrapperOptions & {
94
111
 
95
112
  export type SpvFromBTCTypeDefinition<T extends ChainType> = SwapTypeDefinition<T, SpvFromBTCWrapper<T>, SpvFromBTCSwap<T>>;
96
113
 
114
+ export const REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE: CoinselectAddressTypes = "p2tr";
115
+ export const REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE: CoinselectAddressTypes = "p2wpkh";
116
+
97
117
  /**
98
118
  * New spv vault (UTXO-controlled vault) based swaps for Bitcoin -> Smart chain swaps not requiring
99
119
  * any initiation on the destination chain, and with the added possibility for the user to receive
@@ -347,20 +367,16 @@ export class SpvFromBTCWrapper<
347
367
  *
348
368
  * @param amountData
349
369
  * @param options Options as passed to the swap creation function
350
- * @param pricePrefetch
351
- * @param nativeTokenPricePrefetch
352
370
  * @param abortController
353
371
  * @param contractVersion
354
372
  * @private
355
373
  */
356
- private async preFetchCallerFeeShare(
357
- amountData: AmountData,
374
+ private async preFetchCallerFeeInNativeToken(
375
+ amountData: {amount?: bigint},
358
376
  options: {
359
377
  unsafeZeroWatchtowerFee: boolean,
360
378
  feeSafetyFactor: number
361
379
  },
362
- pricePrefetch: Promise<bigint | undefined>,
363
- nativeTokenPricePrefetch: Promise<bigint | undefined> | undefined,
364
380
  abortController: AbortController,
365
381
  contractVersion: string
366
382
  ): Promise<bigint | undefined> {
@@ -372,16 +388,12 @@ export class SpvFromBTCWrapper<
372
388
  feePerBlock,
373
389
  btcRelayData,
374
390
  currentBtcBlock,
375
- claimFeeRate,
376
- nativeTokenPrice
391
+ claimFeeRate
377
392
  ] = await Promise.all([
378
393
  this.btcRelay(contractVersion).getFeePerBlock(),
379
394
  this.btcRelay(contractVersion).getTipData(),
380
395
  this._btcRpc.getTipHeight(),
381
- this._contract(contractVersion).getClaimFee(this._chain.randomAddress()),
382
- nativeTokenPricePrefetch ?? (amountData.token===this._chain.getNativeCurrencyAddress() ?
383
- pricePrefetch :
384
- this._prices.preFetchPrice(this.chainIdentifier, this._chain.getNativeCurrencyAddress(), abortController.signal))
396
+ this._contract(contractVersion).getClaimFee(this._chain.randomAddress())
385
397
  ]);
386
398
 
387
399
  if(btcRelayData==null) throw new Error("Btc relay doesn't seem to be initialized!");
@@ -394,33 +406,65 @@ export class SpvFromBTCWrapper<
394
406
  (claimFeeRate * BigInt(this._options.maxTransactionsDelta))
395
407
  ) * BigInt(Math.floor(options.feeSafetyFactor*1000000)) / 1_000_000n;
396
408
 
397
- let payoutAmount: bigint;
398
- if(amountData.exactIn) {
399
- //Convert input amount in BTC to
400
- const amountInNativeToken = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, amountData.amount, this._chain.getNativeCurrencyAddress(), abortController.signal, nativeTokenPrice);
401
- payoutAmount = amountInNativeToken - totalFeeInNativeToken;
402
- } else {
403
- if(amountData.token===this._chain.getNativeCurrencyAddress()) {
404
- //Both amounts in same currency
405
- payoutAmount = amountData.amount;
406
- } else {
407
- //Need to convert both to native currency
408
- const btcAmount = await this._prices.getToBtcSwapAmount(this.chainIdentifier, amountData.amount, amountData.token, abortController.signal, await pricePrefetch);
409
- payoutAmount = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, btcAmount, this._chain.getNativeCurrencyAddress(), abortController.signal, nativeTokenPrice);
410
- }
411
- }
412
-
413
- this.logger.debug("preFetchCallerFeeShare(): Caller fee in native token: "+totalFeeInNativeToken.toString(10)+" total payout in native token: "+payoutAmount.toString(10));
414
-
415
- const callerFeeShare = ((totalFeeInNativeToken * 100_000n) + payoutAmount - 1n) / payoutAmount; //Make sure to round up here
416
- if(callerFeeShare < 0n) return 0n;
417
- if(callerFeeShare >= 2n**20n) return 2n**20n - 1n;
418
- return callerFeeShare;
409
+ return totalFeeInNativeToken;
419
410
  } catch (e) {
420
411
  abortController.abort(e);
421
412
  }
422
413
  }
423
414
 
415
+ /**
416
+ * Pre-fetches caller (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
417
+ * provided abortController
418
+ *
419
+ * @param amountPrefetch
420
+ * @param totalFeeInNativeTokenPrefetch
421
+ * @param amountData
422
+ * @param options Options as passed to the swap creation function
423
+ * @param pricePrefetch
424
+ * @param nativeTokenPricePrefetch
425
+ * @param abortSignal
426
+ * @private
427
+ */
428
+ private async computeCallerFeeShare(
429
+ amountPrefetch: Promise<bigint | undefined>,
430
+ totalFeeInNativeTokenPrefetch: Promise<bigint | undefined>,
431
+ amountData: {exactIn: boolean, token: string},
432
+ options: {unsafeZeroWatchtowerFee: boolean},
433
+ pricePrefetch: Promise<bigint | undefined>,
434
+ nativeTokenPricePrefetch: Promise<bigint | undefined> | undefined,
435
+ abortSignal?: AbortSignal
436
+ ): Promise<bigint> {
437
+ if(options.unsafeZeroWatchtowerFee) return 0n;
438
+
439
+ const amount = await throwIfUndefined(amountPrefetch, "Cannot get swap amount!");
440
+ if(amount===0n) return 0n;
441
+
442
+ const totalFeeInNativeToken = await throwIfUndefined(totalFeeInNativeTokenPrefetch, "Cannot get total fee in native token!");
443
+ const nativeTokenPrice = await nativeTokenPricePrefetch;
444
+
445
+ let payoutAmount: bigint;
446
+ if(amountData.exactIn) {
447
+ //Convert input amount in BTC to
448
+ const amountInNativeToken = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, amount, this._chain.getNativeCurrencyAddress(), abortSignal, nativeTokenPrice);
449
+ payoutAmount = amountInNativeToken - totalFeeInNativeToken;
450
+ } else {
451
+ if(amountData.token===this._chain.getNativeCurrencyAddress()) {
452
+ //Both amounts in same currency
453
+ payoutAmount = amount;
454
+ } else {
455
+ //Need to convert both to native currency
456
+ const btcAmount = await this._prices.getToBtcSwapAmount(this.chainIdentifier, amount, amountData.token, abortSignal, await pricePrefetch);
457
+ payoutAmount = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, btcAmount, this._chain.getNativeCurrencyAddress(), abortSignal, nativeTokenPrice);
458
+ }
459
+ }
460
+
461
+ this.logger.debug("computeCallerFeeShare(): Caller fee in native token: "+totalFeeInNativeToken.toString(10)+" total payout in native token: "+payoutAmount.toString(10));
462
+
463
+ const callerFeeShare = ((totalFeeInNativeToken * 100_000n) + payoutAmount - 1n) / payoutAmount; //Make sure to round up here
464
+ if(callerFeeShare < 0n) return 0n;
465
+ if(callerFeeShare >= 2n**20n) return 2n**20n - 1n;
466
+ return callerFeeShare;
467
+ }
424
468
 
425
469
  /**
426
470
  * Verifies response returned from intermediary
@@ -430,7 +474,8 @@ export class SpvFromBTCWrapper<
430
474
  * @param lp Intermediary
431
475
  * @param options Options as passed to the swap creation function
432
476
  * @param callerFeeShare
433
- * @param bitcoinFeeRatePromise Maximum accepted fee rate from the LPs
477
+ * @param maxBitcoinFeeRatePromise Maximum accepted fee rate from the LPs
478
+ * @param bitcoinFeeRatePromise
434
479
  * @param abortSignal
435
480
  * @private
436
481
  * @throws {IntermediaryError} in case the response is invalid
@@ -440,16 +485,18 @@ export class SpvFromBTCWrapper<
440
485
  amountData: AmountData,
441
486
  lp: Intermediary,
442
487
  options: {
443
- gasAmount: bigint
488
+ gasAmount: bigint,
489
+ sourceWalletUtxos?: Promise<BitcoinWalletUtxoBase[]>
444
490
  },
445
491
  callerFeeShare: bigint,
446
- bitcoinFeeRatePromise: Promise<number | undefined>,
492
+ maxBitcoinFeeRatePromise: Promise<number | undefined>,
493
+ bitcoinFeeRatePromise: Promise<number | undefined> | undefined,
447
494
  abortSignal: AbortSignal
448
495
  ): Promise<{
449
496
  vault: T["SpvVaultData"],
450
497
  vaultUtxoValue: number
451
498
  }> {
452
- const btcFeeRate = await throwIfUndefined(bitcoinFeeRatePromise, "Bitcoin fee rate promise failed!");
499
+ const btcFeeRate = await throwIfUndefined(maxBitcoinFeeRatePromise, "Bitcoin fee rate promise failed!");
453
500
  abortSignal.throwIfAborted();
454
501
  if(btcFeeRate!=null && resp.btcFeeRate > btcFeeRate) throw new IntermediaryError(`Required bitcoin fee rate returned from the LP is too high! Maximum accepted: ${btcFeeRate} sats/vB, required by LP: ${resp.btcFeeRate} sats/vB`);
455
502
 
@@ -459,11 +506,13 @@ export class SpvFromBTCWrapper<
459
506
  let vaultScript: Uint8Array;
460
507
  let vaultAddressType: CoinselectAddressTypes;
461
508
  let btcAddressScript: Uint8Array;
509
+ let btcAddressType: CoinselectAddressTypes;
462
510
  //Ensure valid btc addresses returned
463
511
  try {
464
512
  vaultScript = toOutputScript(this._options.bitcoinNetwork, resp.vaultBtcAddress);
465
513
  vaultAddressType = toCoinselectAddressType(vaultScript);
466
514
  btcAddressScript = toOutputScript(this._options.bitcoinNetwork, resp.btcAddress);
515
+ btcAddressType = toCoinselectAddressType(btcAddressScript);
467
516
  } catch (e) {
468
517
  throw new IntermediaryError("Invalid btc address data returned", e);
469
518
  }
@@ -473,7 +522,8 @@ export class SpvFromBTCWrapper<
473
522
  resp.vaultId < 0n || //Ensure vaultId is not negative
474
523
  vaultScript==null || //Make sure vault script is parsable and of known type
475
524
  btcAddressScript==null || //Make sure btc address script is parsable and of known type
476
- vaultAddressType==="p2pkh" || vaultAddressType==="p2sh-p2wpkh" || //Constrain the vault script type to witness types
525
+ btcAddressType!==REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE || //Constrain the btc address script type
526
+ vaultAddressType!==REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE || //Constrain the vault script type
477
527
  decodedUtxo.length!==2 || decodedUtxo[0].length!==64 || isNaN(parseInt(decodedUtxo[1])) || //Check valid UTXO
478
528
  resp.btcFeeRate < 1 || resp.btcFeeRate > 10000 //Sanity check on the returned BTC fee rate
479
529
  ) throw new IntermediaryError("Invalid vault data returned!");
@@ -517,7 +567,22 @@ export class SpvFromBTCWrapper<
517
567
 
518
568
  //Amounts - make sure the amounts match
519
569
  if(amountData.exactIn) {
520
- if(resp.btcAmount !== amountData.amount) throw new IntermediaryError("Invalid amount returned");
570
+ if(!resp.usedUtxoInputCalculation) {
571
+ //Legacy calculation
572
+ if(resp.btcAmount !== amountData.amount) throw new IntermediaryError("Invalid amount returned");
573
+ } else {
574
+ //Implies the raw UTXOs were passed for amount derivation
575
+ //Verify the derivation was done correctly
576
+ if(options.sourceWalletUtxos==null) throw new IntermediaryError("Invalid usedUtxoInputCalcuation return value");
577
+ if(bitcoinFeeRatePromise==null) throw new Error("bitcoinFeeRatePromise must be passed for UTXO-based input amount calculation checks");
578
+ const walletUtxos = await options.sourceWalletUtxos;
579
+ const bitcoinFeeRate = await throwIfUndefined(bitcoinFeeRatePromise, "Failed to fetch bitcoin fee rate!");
580
+ const {balance} = BitcoinWallet.getSpendableBalance(
581
+ walletUtxos, Math.max(resp.btcFeeRate, bitcoinFeeRate),
582
+ this.getDummySwapPsbt(options.gasAmount!==0n), REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE
583
+ );
584
+ if(resp.btcAmount !== balance) throw new IntermediaryError(`Invalid amount returned, expected: ${balance.toString(10)}, got: ${resp.btcAmount.toString(10)}`);
585
+ }
521
586
  } else {
522
587
  //Check the difference between amount adjusted due to scaling to raw amount
523
588
  const adjustedAmount = amountData.amount / tokenData[0].multiplier * tokenData[0].multiplier;
@@ -613,6 +678,72 @@ export class SpvFromBTCWrapper<
613
678
  };
614
679
  }
615
680
 
681
+ private async amountPrefetch(
682
+ amountData: {token: string, exactIn: boolean, amount?: bigint},
683
+ bitcoinFeeRatePromise: Promise<number | undefined>,
684
+ walletUtxosPromise: Promise<BitcoinWalletUtxoBase[]> | undefined,
685
+ includeGas: boolean,
686
+ abortController: AbortController
687
+ ): Promise<bigint | undefined> {
688
+ if(amountData.amount!=null) return amountData.amount;
689
+ try {
690
+ const bitcoinFeeRate = await throwIfUndefined(bitcoinFeeRatePromise, "Cannot fetch Bitcoin fee rate!");
691
+ if(walletUtxosPromise==null) throw new UserError("Cannot use empty amount without passing UTXOs!");
692
+ const walletUtxos = await walletUtxosPromise;
693
+ if(walletUtxos.length===0)
694
+ throw new UserError("Wallet doesn't have any BTC balance");
695
+ const spendableBalance = await BitcoinWallet.getSpendableBalance(
696
+ walletUtxos, bitcoinFeeRate,
697
+ this.getDummySwapPsbt(includeGas), REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE
698
+ );
699
+ return spendableBalance.balance;
700
+ } catch (e) {
701
+ abortController.abort(e);
702
+ }
703
+ }
704
+
705
+ private bitcoinFeeRatePrefetch(
706
+ options: {
707
+ maxAllowedBitcoinFeeRate: number,
708
+ sourceWalletUtxos?: Promise<BitcoinWalletUtxoBase[]>,
709
+ bitcoinFeeRate?: Promise<number>
710
+ },
711
+ abortController: AbortController
712
+ ) {
713
+ let bitcoinFeeRatePromise: Promise<number | undefined> | undefined;
714
+ if(options?.sourceWalletUtxos!=null) {
715
+ if(options.bitcoinFeeRate!=null) {
716
+ bitcoinFeeRatePromise = options.bitcoinFeeRate.then(value => {
717
+ if(options.maxAllowedBitcoinFeeRate!=Infinity && options.maxAllowedBitcoinFeeRate<value)
718
+ throw new Error("Passed `maxAllowedBitcoinFeeRate` cannot be lower than `bitcoinFeeRate`");
719
+ return value;
720
+ });
721
+ } else {
722
+ bitcoinFeeRatePromise = this._btcRpc.getFeeRate().then(value => {
723
+ if(options.maxAllowedBitcoinFeeRate!=Infinity && value > options.maxAllowedBitcoinFeeRate) return options.maxAllowedBitcoinFeeRate;
724
+ return value;
725
+ });
726
+ }
727
+ bitcoinFeeRatePromise = bitcoinFeeRatePromise.catch(e => {
728
+ abortController.abort(e);
729
+ return undefined;
730
+ });
731
+ }
732
+ const maxBitcoinFeeRatePromise: Promise<number | undefined> = options.maxAllowedBitcoinFeeRate!=Infinity
733
+ ? Promise.resolve(options.maxAllowedBitcoinFeeRate)
734
+ : throwIfUndefined(bitcoinFeeRatePromise ?? options.bitcoinFeeRate ?? this._btcRpc.getFeeRate())
735
+ .then(x => this._options.maxBtcFeeOffset + (x*this._options.maxBtcFeeMultiplier))
736
+ .catch(e => {
737
+ abortController.abort(e);
738
+ return undefined;
739
+ });
740
+
741
+ return {
742
+ bitcoinFeeRatePromise,
743
+ maxBitcoinFeeRatePromise
744
+ }
745
+ }
746
+
616
747
  /**
617
748
  * Returns a newly created Bitcoin -> Smart chain swap using the SPV vault (UTXO-controlled vault) swap protocol,
618
749
  * with the passed amount. Also allows specifying additional "gas drop" native token that the receipient receives
@@ -627,7 +758,7 @@ export class SpvFromBTCWrapper<
627
758
  */
628
759
  public create(
629
760
  recipient: string,
630
- amountData: AmountData,
761
+ amountData: { amount?: bigint, token: string, exactIn: boolean },
631
762
  lps: Intermediary[],
632
763
  options?: SpvFromBTCOptions,
633
764
  additionalParams?: Record<string, any>,
@@ -640,7 +771,13 @@ export class SpvFromBTCWrapper<
640
771
  gasAmount: this.parseGasAmount(options?.gasAmount),
641
772
  unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false,
642
773
  feeSafetyFactor: options?.feeSafetyFactor ?? 1.25,
643
- maxAllowedBitcoinFeeRate: options?.maxAllowedBitcoinFeeRate ?? options?.maxAllowedNetworkFeeRate ?? Infinity
774
+ maxAllowedBitcoinFeeRate: options?.maxAllowedBitcoinFeeRate ?? options?.maxAllowedNetworkFeeRate ?? Infinity,
775
+ sourceWalletUtxos: options?.sourceWalletUtxos==undefined
776
+ ? undefined
777
+ : options?.sourceWalletUtxos instanceof Promise ? options.sourceWalletUtxos : Promise.resolve(options.sourceWalletUtxos),
778
+ bitcoinFeeRate: options?.bitcoinFeeRate==undefined
779
+ ? undefined
780
+ : options?.bitcoinFeeRate instanceof Promise ? options.bitcoinFeeRate : Promise.resolve(options.bitcoinFeeRate),
644
781
  };
645
782
 
646
783
  if(
@@ -652,6 +789,13 @@ export class SpvFromBTCWrapper<
652
789
  )
653
790
  ) throw new UserError("Cannot specify `gasAmount` for swaps to a native token!");
654
791
 
792
+ if(amountData.amount==null && options?.sourceWalletUtxos==null)
793
+ throw new UserError("Source wallet UTXOs need to be passed when amount is null!");
794
+ if(amountData.amount==null && !amountData.exactIn)
795
+ throw new UserError("Amount can be null only for exactIn swaps!");
796
+ if(amountData.amount!=null && options?.sourceWalletUtxos!=null)
797
+ throw new UserError("Source wallet UTXOs cannot be passed while specifying an input amount!");
798
+
655
799
  const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
656
800
 
657
801
  const _abortController = extendAbortController(abortSignal);
@@ -663,14 +807,12 @@ export class SpvFromBTCWrapper<
663
807
  undefined :
664
808
  this.preFetchPrice({token: nativeTokenAddress}, _abortController.signal);
665
809
  const callerFeePrefetchPromise = mapArrayToObject(lpVersions, (contractVersion: string) => {
666
- return this.preFetchCallerFeeShare(amountData, _options, pricePrefetchPromise, gasTokenPricePrefetchPromise, _abortController, contractVersion);
810
+ return this.preFetchCallerFeeInNativeToken(amountData, _options, _abortController, contractVersion);
667
811
  });
668
- const bitcoinFeeRatePromise: Promise<number | undefined> = _options.maxAllowedBitcoinFeeRate!=Infinity ?
669
- Promise.resolve(_options.maxAllowedBitcoinFeeRate) :
670
- this._btcRpc.getFeeRate().then(x => this._options.maxBtcFeeOffset + (x*this._options.maxBtcFeeMultiplier)).catch(e => {
671
- _abortController.abort(e);
672
- return undefined;
673
- });
812
+ const {maxBitcoinFeeRatePromise, bitcoinFeeRatePromise} = this.bitcoinFeeRatePrefetch(_options, _abortController);
813
+ const amountPromise = this.amountPrefetch(
814
+ amountData, maxBitcoinFeeRatePromise, _options.sourceWalletUtxos, _options.gasAmount!==0n, _abortController
815
+ );
674
816
 
675
817
  return lps.map(lp => {
676
818
  return {
@@ -680,6 +822,15 @@ export class SpvFromBTCWrapper<
680
822
  const version = lp.getContractVersion(this.chainIdentifier);
681
823
 
682
824
  const abortController = extendAbortController(_abortController.signal);
825
+ const callerFeeRatePromise = this.computeCallerFeeShare(
826
+ amountPromise,
827
+ callerFeePrefetchPromise[version],
828
+ amountData,
829
+ _options,
830
+ pricePrefetchPromise,
831
+ gasTokenPricePrefetchPromise,
832
+ abortController.signal
833
+ );
683
834
 
684
835
  try {
685
836
  const resp = await tryWithRetries(async(retryCount: number) => {
@@ -687,14 +838,25 @@ export class SpvFromBTCWrapper<
687
838
  this.chainIdentifier, lp.url,
688
839
  {
689
840
  address: recipient,
690
- amount: amountData.amount,
841
+ amount: throwIfUndefined(amountPromise, "Failed to compute swap amount"),
691
842
  token: amountData.token.toString(),
692
843
  exactOut: !amountData.exactIn,
693
844
  gasToken: nativeTokenAddress,
694
845
  gasAmount: _options.gasAmount,
695
- callerFeeRate: throwIfUndefined(callerFeePrefetchPromise[version], "Caller fee prefetch failed!"),
846
+ callerFeeRate: throwIfUndefined(callerFeeRatePromise, "Caller fee prefetch failed!"),
696
847
  frontingFeeRate: 0n,
697
848
  stickyAddress: options?.stickyAddress,
849
+ amountUtxos: _options.sourceWalletUtxos!=null
850
+ ? _options.sourceWalletUtxos.then(utxos => {
851
+ if(utxos.length===0) return undefined;
852
+ return utxos.map(utxo => ({
853
+ value: utxo.value,
854
+ vSize: utils.inputBytes({type: utxo.type}),
855
+ cpfp: utxo.cpfp==null ? undefined : {effectiveVSize: utxo.cpfp?.txVsize, effectiveFeeRate: utxo.cpfp?.txEffectiveFeeRate}
856
+ }));
857
+ })
858
+ : undefined,
859
+ amountFeeRate: bitcoinFeeRatePromise,
698
860
  additionalParams
699
861
  },
700
862
  this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined
@@ -703,7 +865,8 @@ export class SpvFromBTCWrapper<
703
865
 
704
866
  this.logger.debug("create("+lp.url+"): LP response: ", resp)
705
867
 
706
- const callerFeeShare = (await callerFeePrefetchPromise[version])!;
868
+ const callerFeeShare = await callerFeeRatePromise;
869
+ const amount = await throwIfUndefined(amountPromise);
707
870
 
708
871
  const [
709
872
  pricingInfo,
@@ -722,9 +885,16 @@ export class SpvFromBTCWrapper<
722
885
  resp.totalGas * (100_000n + callerFeeShare) / 100_000n,
723
886
  nativeTokenAddress, {swapFeeBtc: resp.gasSwapFeeBtc}, gasTokenPricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
724
887
  ),
725
- this.verifyReturnedData(resp, amountData, lp, _options, callerFeeShare, bitcoinFeeRatePromise, abortController.signal)
888
+ this.verifyReturnedData(
889
+ resp,
890
+ {...amountData, amount},
891
+ lp, _options, callerFeeShare, maxBitcoinFeeRatePromise, bitcoinFeeRatePromise, abortController.signal
892
+ )
726
893
  ]);
727
894
 
895
+ let minimumBtcFeeRate: number = resp.btcFeeRate;
896
+ if(bitcoinFeeRatePromise!=null) minimumBtcFeeRate = Math.max(minimumBtcFeeRate, await throwIfUndefined(bitcoinFeeRatePromise));
897
+
728
898
  const swapInit: SpvFromBTCSwapInit = {
729
899
  pricingInfo,
730
900
  url: lp.url,
@@ -749,7 +919,7 @@ export class SpvFromBTCWrapper<
749
919
  btcAmount: resp.btcAmount,
750
920
  btcAmountSwap: resp.btcAmountSwap,
751
921
  btcAmountGas: resp.btcAmountGas,
752
- minimumBtcFeeRate: resp.btcFeeRate,
922
+ minimumBtcFeeRate,
753
923
 
754
924
  outputTotalSwap: resp.total,
755
925
  outputSwapToken: amountData.token,
@@ -772,6 +942,12 @@ export class SpvFromBTCWrapper<
772
942
  const quote = new SpvFromBTCSwap<T>(this, swapInit);
773
943
  return quote;
774
944
  } catch (e) {
945
+ if(e instanceof OutOfBoundsError) {
946
+ const amountResult = await amountPromise.catch(() => undefined);
947
+ if(_options.sourceWalletUtxos!=null && amountResult!=null && amountResult<=0n) {
948
+ e = new UserError("Wallet doesn't have enough BTC balance to cover transaction fees");
949
+ }
950
+ }
775
951
  abortController.abort(e);
776
952
  throw e;
777
953
  }
@@ -902,7 +1078,7 @@ export class SpvFromBTCWrapper<
902
1078
  allowUnknownOutputs: true
903
1079
  });
904
1080
 
905
- const randomVaultOutScript = OutScript.encode({type: "tr", pubkey: Buffer.from("0101010101010101010101010101010101010101010101010101010101010101", "hex")});
1081
+ const randomVaultOutScript = getDummyOutputScript(REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE);
906
1082
 
907
1083
  psbt.addInput({
908
1084
  txid: randomBytes(32),
@@ -2,6 +2,7 @@ import {BTC_NETWORK, isBytes, PubT, validatePubkey} from "@scure/btc-signer/util
2
2
  import {Buffer} from "buffer";
3
3
  import {Address, OutScript, Transaction} from "@scure/btc-signer";
4
4
  import {CoinselectAddressTypes} from "../bitcoin/coinselect2";
5
+ import { randomBytes } from "./Utils";
5
6
 
6
7
 
7
8
  export function fromOutputScript(network: BTC_NETWORK, outputScriptHex: string): string {
@@ -63,6 +64,46 @@ export function toCoinselectAddressType(outputScript: Uint8Array): CoinselectAdd
63
64
  throw new Error("Unrecognized address type!");
64
65
  }
65
66
 
67
+
68
+ function getDummySpec(type: CoinselectAddressTypes) {
69
+ switch(type) {
70
+ case "p2pkh":
71
+ return {
72
+ type: "pkh",
73
+ hash: randomBytes(20)
74
+ } as const;
75
+ case "p2sh-p2wpkh":
76
+ return {
77
+ type: "sh",
78
+ hash: randomBytes(20)
79
+ } as const;
80
+ case "p2wpkh":
81
+ return {
82
+ type: "wpkh",
83
+ hash: randomBytes(20)
84
+ } as const;
85
+ case "p2wsh":
86
+ return {
87
+ type: "wsh",
88
+ hash: randomBytes(32)
89
+ } as const;
90
+ case "p2tr":
91
+ return {
92
+ type: "tr",
93
+ pubkey: Buffer.from("0101010101010101010101010101010101010101010101010101010101010101", "hex")
94
+ } as const;
95
+ }
96
+ throw new Error("Unrecognized address type!");
97
+ }
98
+
99
+ export function getDummyOutputScript(type: CoinselectAddressTypes): Uint8Array {
100
+ return OutScript.encode(getDummySpec(type));
101
+ }
102
+
103
+ export function getDummyAddress(network: BTC_NETWORK, type: CoinselectAddressTypes): string {
104
+ return Address(network).encode(getDummySpec(type));
105
+ }
106
+
66
107
  /**
67
108
  * General parsers for PSBTs, can parse hex or base64 encoded PSBTs
68
109
  * @param _psbt
@@ -1,12 +1,12 @@
1
1
  import {IBitcoinWallet, isIBitcoinWallet} from "../bitcoin/wallet/IBitcoinWallet";
2
2
  import {BTC_NETWORK} from "@scure/btc-signer/utils";
3
3
  import {SingleAddressBitcoinWallet} from "../bitcoin/wallet/SingleAddressBitcoinWallet";
4
- import {BitcoinRpcWithAddressIndex} from "@atomiqlabs/base";
4
+ import {BitcoinNetwork, BitcoinRpcWithAddressIndex} from "@atomiqlabs/base";
5
5
 
6
6
  export function toBitcoinWallet(
7
7
  _bitcoinWallet: IBitcoinWallet | { address: string, publicKey: string },
8
8
  btcRpc: BitcoinRpcWithAddressIndex<any>,
9
- bitcoinNetwork: BTC_NETWORK
9
+ bitcoinNetwork: BTC_NETWORK | BitcoinNetwork
10
10
  ): IBitcoinWallet {
11
11
  if (isIBitcoinWallet(_bitcoinWallet)) {
12
12
  return _bitcoinWallet;