@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,32 +1,14 @@
1
1
  import {coinSelect, maxSendable, CoinselectAddressTypes, CoinselectTxInput} from "../coinselect2";
2
2
  import {BTC_NETWORK, NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils"
3
3
  import {p2wpkh, OutScript, Transaction, p2tr, Address} from "@scure/btc-signer";
4
- import {IBitcoinWallet} from "./IBitcoinWallet";
4
+ import {BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet} from "./IBitcoinWallet";
5
5
  import {Buffer} from "buffer";
6
6
  import {randomBytes} from "../../utils/Utils";
7
- import {toCoinselectAddressType, toOutputScript} from "../../utils/BitcoinUtils";
7
+ import {getDummyOutputScript, toCoinselectAddressType, toOutputScript} from "../../utils/BitcoinUtils";
8
8
  import {TransactionInputUpdate} from "@scure/btc-signer/psbt";
9
9
  import {getLogger} from "../../utils/Logger";
10
10
  import {BitcoinNetwork, BitcoinRpcWithAddressIndex} from "@atomiqlabs/base";
11
-
12
- /**
13
- * UTXO data structure for Bitcoin wallets
14
- *
15
- * @category Bitcoin
16
- */
17
- export type BitcoinWalletUtxo = {
18
- vout: number,
19
- txId: string,
20
- value: number,
21
- type: CoinselectAddressTypes,
22
- outputScript: Buffer,
23
- address: string,
24
- cpfp?: {
25
- txVsize: number,
26
- txEffectiveFeeRate: number
27
- },
28
- confirmed: boolean
29
- };
11
+ import {utils} from "../coinselect2/utils";
30
12
 
31
13
  /**
32
14
  * Identifies the address type of a Bitcoin address
@@ -200,15 +182,18 @@ export abstract class BitcoinWallet implements IBitcoinWallet {
200
182
  addressType: CoinselectAddressTypes,
201
183
  }[],
202
184
  psbt: Transaction,
203
- feeRate?: number
185
+ _feeRate?: number,
186
+ utxos?: BitcoinWalletUtxo[],
187
+ spendFully?: boolean
204
188
  ): Promise<{
205
189
  fee: number,
206
190
  psbt?: Transaction,
207
191
  inputAddressIndexes?: {[address: string]: number[]}
208
192
  }> {
209
- if(feeRate==null) feeRate = await this.getFeeRate();
193
+ const feeRate = _feeRate ?? await this.getFeeRate();
194
+ const utxoPool: BitcoinWalletUtxo[] = utxos ?? (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat();
210
195
 
211
- const utxoPool: BitcoinWalletUtxo[] = (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat();
196
+ if(spendFully && utxoPool==null) throw new Error("Cannot fully spend when no utxos are passed!");
212
197
 
213
198
  logger.debug("_fundPsbt(): fee rate: "+feeRate+" utxo pool: ", utxoPool);
214
199
 
@@ -247,15 +232,26 @@ export abstract class BitcoinWallet implements IBitcoinWallet {
247
232
  }
248
233
  logger.debug("_fundPsbt(): Coinselect targets: ", targets);
249
234
 
250
- let coinselectResult = coinSelect(utxoPool, targets, feeRate, sendingAccounts[0].addressType, requiredInputs);
235
+ let coinselectResult = spendFully
236
+ ? utils.finalize(requiredInputs.concat(utxoPool.filter(utxo => !utils.isDetrimentalInput(feeRate, utxo))), targets, feeRate, null)
237
+ : coinSelect(utxoPool, targets, feeRate, sendingAccounts[0].addressType, requiredInputs);
251
238
  logger.debug("_fundPsbt(): Coinselect result: ", coinselectResult);
252
239
 
253
- if(coinselectResult.inputs==null || coinselectResult.outputs==null) {
240
+ if(coinselectResult.inputs==null || coinselectResult.outputs==null || coinselectResult.effectiveFeeRate==null) {
254
241
  return {
255
242
  fee: coinselectResult.fee
256
243
  };
257
244
  }
258
245
 
246
+ if(spendFully && feeRate!=null) {
247
+ const maximumAllowedFeeRate = (1.5*feeRate) + 10;
248
+ if(coinselectResult.effectiveFeeRate > maximumAllowedFeeRate)
249
+ throw new Error(`Effective fee rate too high, feeRate: ${coinselectResult.effectiveFeeRate} sats/vB, maximum: ${maximumAllowedFeeRate} sats/vB!`);
250
+ const minimumAllowedFeeRate = 0.9*feeRate;
251
+ if(coinselectResult.effectiveFeeRate < minimumAllowedFeeRate)
252
+ throw new Error(`Effective fee rate too low, feeRate: ${coinselectResult.effectiveFeeRate} sats/vB, minimum: ${minimumAllowedFeeRate} sats/vB!`);
253
+ }
254
+
259
255
  // Remove in/outs that are already in the PSBT
260
256
  coinselectResult.inputs.splice(0, psbt.inputsLength);
261
257
  coinselectResult.outputs.splice(0, psbt.outputsLength);
@@ -346,16 +342,59 @@ export abstract class BitcoinWallet implements IBitcoinWallet {
346
342
  addressType: CoinselectAddressTypes,
347
343
  }[],
348
344
  psbt?: Transaction,
349
- feeRate?: number
345
+ feeRate?: number,
346
+ outputAddressType?: CoinselectAddressTypes,
347
+ utxoPool?: BitcoinWalletUtxoBase[]
350
348
  ): Promise<{
351
349
  balance: bigint,
352
350
  feeRate: number,
353
351
  totalFee: number
354
352
  }> {
355
353
  feeRate ??= await this.getFeeRate();
354
+ utxoPool ??= (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat();
355
+
356
+ return {
357
+ ...BitcoinWallet.getSpendableBalance(
358
+ utxoPool ?? (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat(),
359
+ feeRate ?? await this.getFeeRate(),
360
+ psbt,
361
+ outputAddressType
362
+ ),
363
+ feeRate
364
+ };
365
+ }
366
+
367
+ abstract sendTransaction(address: string, amount: bigint, feeRate?: number): Promise<string>;
368
+ abstract fundPsbt(psbt: Transaction, feeRate?: number, utxos?: BitcoinWalletUtxo[], spendFully?: boolean): Promise<Transaction>;
369
+ abstract signPsbt(psbt: Transaction, signInputs: number[]): Promise<Transaction>;
370
+
371
+ abstract getTransactionFee(address: string, amount: bigint, feeRate?: number): Promise<number>;
372
+ abstract getFundedPsbtFee(psbt: Transaction, feeRate?: number): Promise<number>;
373
+
374
+ abstract getReceiveAddress(): string;
375
+ abstract getBalance(): Promise<{
376
+ confirmedBalance: bigint,
377
+ unconfirmedBalance: bigint
378
+ }>;
379
+ abstract getSpendableBalance(psbt?: Transaction, feeRate?: number): Promise<{
380
+ balance: bigint,
381
+ feeRate: number,
382
+ totalFee: number
383
+ }>;
356
384
 
357
- const utxoPool: BitcoinWalletUtxo[] = (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat();
385
+ static bitcoinNetworkToObject(network: BitcoinNetwork): BTC_NETWORK {
386
+ return btcNetworkMapping[network];
387
+ }
358
388
 
389
+ static getSpendableBalance(
390
+ utxoPool: BitcoinWalletUtxoBase[],
391
+ feeRate: number,
392
+ psbt?: Transaction,
393
+ outputAddressType?: CoinselectAddressTypes
394
+ ): {
395
+ balance: bigint,
396
+ totalFee: number
397
+ } {
359
398
  const requiredInputs: CoinselectTxInput[] = [];
360
399
  if(psbt!=null) for(let i=0;i<psbt.inputsLength;i++) {
361
400
  const input = psbt.getInput(i);
@@ -387,42 +426,15 @@ export abstract class BitcoinWallet implements IBitcoinWallet {
387
426
  })
388
427
  }
389
428
 
390
- const target = OutScript.encode({
391
- type: "wsh",
392
- hash: randomBytes(32)
393
- });
394
-
395
- let coinselectResult = maxSendable(utxoPool, {script: Buffer.from(target), type: "p2wsh"}, feeRate, requiredInputs, additionalOutputs);
429
+ const target: Uint8Array = getDummyOutputScript(outputAddressType ?? "p2wsh");
430
+ let coinselectResult = maxSendable(utxoPool, {script: Buffer.from(target), type: outputAddressType ?? "p2wsh"}, feeRate, requiredInputs, additionalOutputs);
396
431
 
397
432
  logger.debug("_getSpendableBalance(): Max spendable result: ", coinselectResult);
398
433
 
399
434
  return {
400
- feeRate: feeRate,
401
435
  balance: BigInt(Math.floor(coinselectResult.value)),
402
436
  totalFee: coinselectResult.fee
403
437
  }
404
438
  }
405
439
 
406
- abstract sendTransaction(address: string, amount: bigint, feeRate?: number): Promise<string>;
407
- abstract fundPsbt(psbt: Transaction, feeRate?: number): Promise<Transaction>;
408
- abstract signPsbt(psbt: Transaction, signInputs: number[]): Promise<Transaction>;
409
-
410
- abstract getTransactionFee(address: string, amount: bigint, feeRate?: number): Promise<number>;
411
- abstract getFundedPsbtFee(psbt: Transaction, feeRate?: number): Promise<number>;
412
-
413
- abstract getReceiveAddress(): string;
414
- abstract getBalance(): Promise<{
415
- confirmedBalance: bigint,
416
- unconfirmedBalance: bigint
417
- }>;
418
- abstract getSpendableBalance(psbt?: Transaction, feeRate?: number): Promise<{
419
- balance: bigint,
420
- feeRate: number,
421
- totalFee: number
422
- }>;
423
-
424
- static bitcoinNetworkToObject(network: BitcoinNetwork): BTC_NETWORK {
425
- return btcNetworkMapping[network];
426
- }
427
-
428
440
  }
@@ -1,4 +1,33 @@
1
- import {Transaction} from "@scure/btc-signer";
1
+ import {Address, Transaction} from "@scure/btc-signer";
2
+ import {BTC_NETWORK} from "@scure/btc-signer/utils";
3
+ import {CoinselectAddressTypes} from "../coinselect2";
4
+
5
+ /**
6
+ * UTXO data structure for Bitcoin wallets
7
+ *
8
+ * @category Bitcoin
9
+ */
10
+ export type BitcoinWalletUtxo = {
11
+ vout: number,
12
+ txId: string,
13
+ value: number,
14
+ type: CoinselectAddressTypes,
15
+ outputScript: Buffer,
16
+ address: string,
17
+ cpfp?: {
18
+ txVsize: number,
19
+ txEffectiveFeeRate: number
20
+ },
21
+ confirmed: boolean
22
+ };
23
+
24
+ /**
25
+ * Base UTXO data structure used for maximum spendable balance calculation, doesn't contain all the fields necessary
26
+ * for constructing the full transaction.
27
+ *
28
+ * @category Bitcoin
29
+ */
30
+ export type BitcoinWalletUtxoBase = Omit<BitcoinWalletUtxo, "txId" | "vout" | "outputScript" | "address" | "confirmed">;
2
31
 
3
32
  /**
4
33
  * Type guard to check if an object implements {@link IBitcoinWallet}
@@ -39,8 +68,12 @@ export interface IBitcoinWallet {
39
68
  *
40
69
  * @param psbt PSBT to add the inputs to
41
70
  * @param feeRate Optional fee rate in sats/vB to use for the transaction
71
+ * @param utxos Pre-fetched list of UTXOs to spend from
72
+ * @param spendFully Instructs the wallet to spend all the passed UTXOs in the transaction without creating any
73
+ * change output, if the `feeRate` is passed, it will also enforce that the feeRate in sats/vB for the resulting
74
+ * transaction is not more than 50% and 10 sats/vB larger (considering also the CPFP adjustments)
42
75
  */
43
- fundPsbt(psbt: Transaction, feeRate?: number): Promise<Transaction>;
76
+ fundPsbt(psbt: Transaction, feeRate?: number, utxos?: BitcoinWalletUtxo[], spendFully?: boolean): Promise<Transaction>;
44
77
 
45
78
  /**
46
79
  * Signs inputs in the provided PSBT
@@ -90,10 +123,18 @@ export interface IBitcoinWallet {
90
123
  *
91
124
  * @param psbt A PSBT to which additional inputs from wallet's UTXO set will be added and fee estimated
92
125
  * @param feeRate Optional fee rate in sats/vB to use for the transaction
126
+ * @param outputAddressType Expected output address type, if known
127
+ * @param utxos Optional pre-fetched UTXOs
93
128
  */
94
- getSpendableBalance(psbt?: Transaction, feeRate?: number): Promise<{
129
+ getSpendableBalance(psbt?: Transaction, feeRate?: number, outputAddressType?: CoinselectAddressTypes, utxos?: BitcoinWalletUtxoBase[]): Promise<{
95
130
  balance: bigint,
96
131
  feeRate: number,
97
132
  totalFee: number
98
133
  }>;
134
+
135
+ /**
136
+ * Returns a list of available UTXOs for the wallet
137
+ */
138
+ getUtxoPool?(): Promise<BitcoinWalletUtxo[]>;
139
+
99
140
  }
@@ -8,6 +8,7 @@ import {HDKey} from "@scure/bip32";
8
8
  import {entropyToMnemonic, generateMnemonic, mnemonicToSeed} from "@scure/bip39";
9
9
  import {wordlist} from "@scure/bip39/wordlists/english.js";
10
10
  import {sha256} from "@noble/hashes/sha2";
11
+ import {BitcoinWalletUtxo, BitcoinWalletUtxoBase} from "./IBitcoinWallet";
11
12
 
12
13
  const logger = getLogger("SingleAddressBitcoinWallet: ");
13
14
 
@@ -88,8 +89,8 @@ export class SingleAddressBitcoinWallet extends BitcoinWallet {
88
89
  /**
89
90
  * @inheritDoc
90
91
  */
91
- async fundPsbt(inputPsbt: Transaction, feeRate?: number): Promise<Transaction> {
92
- const {psbt} = await super._fundPsbt(this.toBitcoinWalletAccounts(), inputPsbt, feeRate);
92
+ async fundPsbt(inputPsbt: Transaction, feeRate?: number, utxos?: BitcoinWalletUtxo[], spendFully?: boolean): Promise<Transaction> {
93
+ const {psbt} = await super._fundPsbt(this.toBitcoinWalletAccounts(), inputPsbt, feeRate, utxos, spendFully);
93
94
  if(psbt==null) {
94
95
  throw new Error("Not enough balance!");
95
96
  }
@@ -150,12 +151,19 @@ export class SingleAddressBitcoinWallet extends BitcoinWallet {
150
151
  /**
151
152
  * @inheritDoc
152
153
  */
153
- getSpendableBalance(psbt?: Transaction, feeRate?: number): Promise<{
154
+ getSpendableBalance(psbt?: Transaction, feeRate?: number, outputAddressType?: CoinselectAddressTypes, utxos?: BitcoinWalletUtxoBase[]): Promise<{
154
155
  balance: bigint,
155
156
  feeRate: number,
156
157
  totalFee: number
157
158
  }> {
158
- return this._getSpendableBalance([{address: this.address, addressType: this.addressType}], psbt, feeRate);
159
+ return this._getSpendableBalance([{address: this.address, addressType: this.addressType}], psbt, feeRate, outputAddressType, utxos);
160
+ }
161
+
162
+ /**
163
+ * @inheritDoc
164
+ */
165
+ async getUtxoPool(): Promise<BitcoinWalletUtxo[]> {
166
+ return this._getUtxoPool(this.address, this.addressType);
159
167
  }
160
168
 
161
169
  /**
@@ -298,20 +298,24 @@ const SpvFromBTCPrepareResponseSchema = {
298
298
 
299
299
  callerFeeShare: FieldTypeEnum.BigInt,
300
300
  frontingFeeShare: FieldTypeEnum.BigInt,
301
- executionFeeShare: FieldTypeEnum.BigInt
301
+ executionFeeShare: FieldTypeEnum.BigInt,
302
+
303
+ usedUtxoInputCalculation: FieldTypeEnum.BooleanOptional
302
304
  } as const;
303
305
 
304
306
  export type SpvFromBTCPrepareResponseType = RequestSchemaResult<typeof SpvFromBTCPrepareResponseSchema>;
305
307
 
306
308
  export type SpvFromBTCPrepare = SwapInit & {
307
309
  address: string,
308
- amount: bigint,
310
+ amount: Promise<bigint>,
309
311
  gasAmount: bigint,
310
312
  gasToken: string,
311
313
  exactOut: boolean,
312
314
  callerFeeRate: Promise<bigint>,
313
315
  frontingFeeRate: bigint,
314
- stickyAddress?: boolean
316
+ stickyAddress?: boolean,
317
+ amountUtxos?: Promise<{ value: number, vSize: number, cpfp?: { effectiveVSize: number, effectiveFeeRate: number }}[] | undefined>,
318
+ amountFeeRate?: Promise<number | undefined>
315
319
  }
316
320
 
317
321
  const SpvFromBTCInitResponseSchema = {
@@ -870,17 +874,29 @@ export class IntermediaryAPI {
870
874
  abortSignal?: AbortSignal,
871
875
  streamRequest?: boolean
872
876
  ): Promise<SpvFromBTCPrepareResponseType> {
877
+ //We need to make sure we only send the amount parameter after the amountUtxos and amountFeeRate resolve
878
+ // this is needed, because in the LP code to maintain backwards compatibility the amountUtxos and amountFeeRate
879
+ // params are checked immediately after the amount param (and other params) are received, if amount were sent
880
+ // first without the amountUtxos or amountFeeRate populated these fields would've been skipped altogether
881
+ const amountPromise = (async () => {
882
+ if(init.amountUtxos!=null) await init.amountUtxos;
883
+ if(init.amountFeeRate!=null) await init.amountFeeRate;
884
+ const amount = await init.amount;
885
+ return amount.toString(10);
886
+ })();
873
887
  const responseBodyPromise = streamingFetchPromise(baseUrl+"/frombtc_spv/getQuote?chain="+encodeURIComponent(chainIdentifier), {
874
888
  exactOut: init.exactOut,
875
889
  ...init.additionalParams,
876
890
  address: init.address,
877
- amount: init.amount.toString(10),
891
+ amount: amountPromise,
878
892
  token: init.token,
879
893
  gasAmount: init.gasAmount.toString(10),
880
894
  gasToken: init.gasToken,
881
895
  frontingFeeRate: init.frontingFeeRate.toString(10),
882
896
  callerFeeRate: init.callerFeeRate.then(val => val.toString(10)),
883
- stickyAddress: init.stickyAddress
897
+ stickyAddress: init.stickyAddress,
898
+ amountUtxos: init.amountUtxos,
899
+ amountFeeRate: init.amountFeeRate
884
900
  }, {
885
901
  code: FieldTypeEnum.Number,
886
902
  msg: FieldTypeEnum.String,
@@ -65,6 +65,9 @@ import {NotNever} from "../utils/TypeUtils";
65
65
  import {IEscrowSwap} from "../swaps/escrow_swaps/IEscrowSwap";
66
66
  import {LightningInvoiceCreateService, isLightningInvoiceCreateService} from "../types/wallets/LightningInvoiceCreateService";
67
67
  import {SwapSide} from "../enums/SwapSide";
68
+ import {BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet} from "../bitcoin/wallet/IBitcoinWallet";
69
+ import {MinimalBitcoinWalletInterface} from "../types/wallets/MinimalBitcoinWalletInterface";
70
+ import {toBitcoinWallet} from "../utils/BitcoinWalletUtils";
68
71
 
69
72
  /**
70
73
  * Configuration options for the Swapper
@@ -745,7 +748,7 @@ export class Swapper<T extends MultiChain> extends EventEmitter<{
745
748
  quote: Promise<S>,
746
749
  intermediary: Intermediary
747
750
  }[]>,
748
- amountData: AmountData,
751
+ amountData: { amount?: bigint, token: string, exactIn: boolean },
749
752
  swapType: SwapType,
750
753
  maxWaitTimeMS: number = 2000
751
754
  ): Promise<S> {
@@ -755,7 +758,7 @@ export class Swapper<T extends MultiChain> extends EventEmitter<{
755
758
 
756
759
  const inBtc: boolean = swapType===SwapType.TO_BTCLN || swapType===SwapType.TO_BTC ? !amountData.exactIn : amountData.exactIn;
757
760
 
758
- if(!inBtc) {
761
+ if(!inBtc || amountData.amount==null) {
759
762
  //Get candidates not based on the amount
760
763
  candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token);
761
764
  } else {
@@ -769,7 +772,7 @@ export class Swapper<T extends MultiChain> extends EventEmitter<{
769
772
  await this.intermediaryDiscovery.reloadIntermediaries();
770
773
  swapLimitsChanged = true;
771
774
 
772
- if(!inBtc) {
775
+ if(!inBtc || amountData.amount==null) {
773
776
  //Get candidates not based on the amount
774
777
  candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token);
775
778
  } else {
@@ -859,8 +862,10 @@ export class Swapper<T extends MultiChain> extends EventEmitter<{
859
862
  }
860
863
  if(min!=null && max!=null) {
861
864
  let msg = "Swap amount too high or too low! Try swapping a different amount.";
862
- if(min > amountData.amount) msg = "Swap amount too low! Try swapping a higher amount.";
863
- if(max < amountData.amount) msg = "Swap amount too high! Try swapping a lower amount.";
865
+ if(amountData.amount!=null) {
866
+ if(min > amountData.amount) msg = "Swap amount too low! Try swapping a higher amount.";
867
+ if(max < amountData.amount) msg = "Swap amount too high! Try swapping a lower amount.";
868
+ }
864
869
  reject(new OutOfBoundsError(msg, 400, min, max));
865
870
  return;
866
871
  }
@@ -1102,16 +1107,17 @@ export class Swapper<T extends MultiChain> extends EventEmitter<{
1102
1107
  chainIdentifier: ChainIdentifier,
1103
1108
  recipient: string,
1104
1109
  tokenAddress: string,
1105
- amount: bigint,
1110
+ amount: bigint | null,
1106
1111
  exactOut: boolean = false,
1107
1112
  additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
1108
1113
  options?: SpvFromBTCOptions
1109
1114
  ): Promise<SpvFromBTCSwap<T[ChainIdentifier]>> {
1110
1115
  if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
1116
+ if(this._chains[chainIdentifier].wrappers[SwapType.SPV_VAULT_FROM_BTC]==null) throw new Error("Chain "+chainIdentifier+" doesn't support new BTC swap protocol (spv vault swaps)!");
1111
1117
  if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
1112
1118
  recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
1113
1119
  const amountData = {
1114
- amount,
1120
+ amount: amount ?? undefined,
1115
1121
  token: tokenAddress,
1116
1122
  exactIn: !exactOut
1117
1123
  };
@@ -1285,6 +1291,7 @@ export class Swapper<T extends MultiChain> extends EventEmitter<{
1285
1291
  options?: FromBTCLNAutoOptions
1286
1292
  ): Promise<FromBTCLNAutoSwap<T[ChainIdentifier]>> {
1287
1293
  if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
1294
+ if(this._chains[chainIdentifier].wrappers[SwapType.FROM_BTCLN_AUTO]==null) throw new Error("Chain "+chainIdentifier+" doesn't support new lightning swap protocol (from btcln auto)!");
1288
1295
  if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
1289
1296
  recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
1290
1297
  const amountData = {
@@ -1331,6 +1338,7 @@ export class Swapper<T extends MultiChain> extends EventEmitter<{
1331
1338
  options?: FromBTCLNAutoOptions
1332
1339
  ): Promise<FromBTCLNAutoSwap<T[ChainIdentifier]>> {
1333
1340
  if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
1341
+ if(this._chains[chainIdentifier].wrappers[SwapType.FROM_BTCLN_AUTO]==null) throw new Error("Chain "+chainIdentifier+" doesn't support new lightning swap protocol (from btcln auto)!");
1334
1342
  if(typeof(lnurl)==="string" && !this.Utils.isValidLNURL(lnurl)) throw new Error("Invalid LNURL-withdraw link");
1335
1343
  if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
1336
1344
  recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
@@ -1573,6 +1581,73 @@ export class Swapper<T extends MultiChain> extends EventEmitter<{
1573
1581
  throw new Error("Unsupported swap type");
1574
1582
  }
1575
1583
 
1584
+ /**
1585
+ * A helper function to sweep all the funds from a given wallet in a single swap, after getting the quote you can
1586
+ * execute the swap by passing the returned `feeRate` and `utxos` to the {@link SpvFromBTCSwap.execute},
1587
+ * {@link SpvFromBTCSwap.getFundedPsbt} or {@link SpvFromBTCSwap.sendBitcoinTransaction} functions along
1588
+ * with `spendFully=true`.
1589
+ *
1590
+ * @example
1591
+ * Create the swap first using this function
1592
+ * ```ts
1593
+ * const {swap, utxos, btcFeeRate} = await swapper.sweepBitcoinWallet(wallet, Tokens.CITREA.CBTC, dstAddress);
1594
+ * ```
1595
+ * Then execute it using one of these execution paths - ensure that you supply the returned `utxos`, `btcFeeRate`
1596
+ * params and also set `spendFully` to `true`!
1597
+ *
1598
+ * a) Execute and pass the returned utxos and btcFeeRate:
1599
+ * ```ts
1600
+ * await swap.execute(wallet, undefined, {feeRate: btcFeeRate, utxos: utxos, spendFully: true});
1601
+ * ```
1602
+ *
1603
+ * b) Get funded PSBT to sign externally:
1604
+ * ```ts
1605
+ * const {psbt, psbtHex, psbtBase64, signInputs} = await swap.getFundedPsbt(wallet, btcFeeRate, undefined, utxos, true);
1606
+ * // Sign the psbt at the specified signInputs indices
1607
+ * const signedPsbt = ...;
1608
+ * // Then submit back to the SDK
1609
+ * await swap.submitPsbt(signedPsbt);
1610
+ * ```
1611
+ *
1612
+ * c) Only sign and send the signed PSBT with the provided wallet:
1613
+ * ```ts
1614
+ * await swap.sendBitcoinTransaction(wallet, btcFeeRate, utxos, true);
1615
+ * ```
1616
+ */
1617
+ async sweepBitcoinWallet<C extends ChainIds<T>>(
1618
+ srcWallet: IBitcoinWallet | MinimalBitcoinWalletInterface,
1619
+ _dstToken: SCToken<C> | string,
1620
+ dstAddress: string,
1621
+ options?: SpvFromBTCOptions
1622
+ ): Promise<{
1623
+ swap: SpvFromBTCSwap<T[C]>,
1624
+ utxos: BitcoinWalletUtxo[],
1625
+ btcFeeRate: number
1626
+ }> {
1627
+ const dstToken = typeof(_dstToken)==="string" ? this.getToken(_dstToken) as Token<C> : _dstToken;
1628
+ if(!isSCToken<C>(dstToken)) throw new Error("Destination token must be a smart chain token!");
1629
+
1630
+ const wallet = toBitcoinWallet(srcWallet, this._bitcoinRpc, this.bitcoinNetwork);
1631
+ if(wallet.getUtxoPool==null) throw new Error("Wallet needs to support the `getUtxoPool()` function!");
1632
+
1633
+ const walletUtxosPromise = wallet.getUtxoPool();
1634
+ const bitcoinFeeRatePromise = options?.bitcoinFeeRate ?? wallet.getFeeRate();
1635
+
1636
+ const swap = await this.createFromBTCSwapNew(
1637
+ dstToken.chainId, dstAddress, dstToken.address, null, false, undefined, {
1638
+ ...options,
1639
+ sourceWalletUtxos: walletUtxosPromise,
1640
+ bitcoinFeeRate: bitcoinFeeRatePromise
1641
+ }
1642
+ );
1643
+
1644
+ return {
1645
+ swap,
1646
+ utxos: await walletUtxosPromise,
1647
+ btcFeeRate: Math.max(swap.minimumBtcFeeRate, await bitcoinFeeRatePromise)
1648
+ };
1649
+ }
1650
+
1576
1651
  /**
1577
1652
  * Returns all swaps
1578
1653
  */
@@ -16,7 +16,7 @@ import {parsePsbtTransaction, toCoinselectAddressType, toOutputScript} from "../
16
16
  import {getInputType, Transaction} from "@scure/btc-signer";
17
17
  import {Buffer} from "buffer";
18
18
  import {Fee} from "../../types/fees/Fee";
19
- import {IBitcoinWallet, isIBitcoinWallet} from "../../bitcoin/wallet/IBitcoinWallet";
19
+ import {BitcoinWalletUtxo, IBitcoinWallet, isIBitcoinWallet} from "../../bitcoin/wallet/IBitcoinWallet";
20
20
  import {IntermediaryAPI} from "../../intermediaries/apis/IntermediaryAPI";
21
21
  import {IBTCWalletSwap} from "../IBTCWalletSwap";
22
22
  import {ISwapWithGasDrop} from "../ISwapWithGasDrop";
@@ -877,11 +877,17 @@ export class SpvFromBTCSwap<T extends ChainType>
877
877
  * @param _bitcoinWallet Sender's bitcoin wallet
878
878
  * @param feeRate Optional fee rate in sats/vB for the transaction
879
879
  * @param additionalOutputs additional outputs to add to the PSBT - can be used to collect fees from users
880
+ * @param utxos Pre-fetched list of UTXOs to spend from
881
+ * @param spendFully Instructs the wallet to spend all the passed UTXOs in the transaction without creating any
882
+ * change output, if the `feeRate` is passed, it will also enforce that the feeRate in sats/vB for the resulting
883
+ * transaction is not more than 50% and 10 sats/vB larger (considering also the CPFP adjustments)
880
884
  */
881
885
  async getFundedPsbt(
882
886
  _bitcoinWallet: IBitcoinWallet | MinimalBitcoinWalletInterface,
883
887
  feeRate?: number,
884
- additionalOutputs?: ({amount: bigint, outputScript: Uint8Array} | {amount: bigint, address: string})[]
888
+ additionalOutputs?: ({amount: bigint, outputScript: Uint8Array} | {amount: bigint, address: string})[],
889
+ utxos?: BitcoinWalletUtxo[],
890
+ spendFully?: boolean
885
891
  ): Promise<{
886
892
  psbt: Transaction,
887
893
  psbtHex: string,
@@ -901,7 +907,7 @@ export class SpvFromBTCSwap<T extends ChainType>
901
907
  script: (output as {outputScript: Uint8Array}).outputScript ?? toOutputScript(this.wrapper._options.bitcoinNetwork, (output as {address: string}).address)
902
908
  });
903
909
  });
904
- psbt = await bitcoinWallet.fundPsbt(psbt, feeRate);
910
+ psbt = await bitcoinWallet.fundPsbt(psbt, feeRate, utxos, spendFully);
905
911
  psbt.updateInput(1, {sequence: in1sequence});
906
912
  //Sign every input except the first one
907
913
  const signInputs: number[] = [];
@@ -1024,8 +1030,13 @@ export class SpvFromBTCSwap<T extends ChainType>
1024
1030
  /**
1025
1031
  * @inheritDoc
1026
1032
  */
1027
- async sendBitcoinTransaction(wallet: IBitcoinWallet | MinimalBitcoinWalletInterfaceWithSigner, feeRate?: number): Promise<string> {
1028
- const {psbt, psbtBase64, psbtHex, signInputs} = await this.getFundedPsbt(wallet, feeRate);
1033
+ async sendBitcoinTransaction(
1034
+ wallet: IBitcoinWallet | MinimalBitcoinWalletInterfaceWithSigner,
1035
+ feeRate?: number,
1036
+ utxos?: BitcoinWalletUtxo[],
1037
+ spendFully?: boolean
1038
+ ): Promise<string> {
1039
+ const {psbt, psbtBase64, psbtHex, signInputs} = await this.getFundedPsbt(wallet, feeRate, undefined, utxos, spendFully);
1029
1040
  let signedPsbt: Transaction | string;
1030
1041
  if(isIBitcoinWallet(wallet)) {
1031
1042
  signedPsbt = await wallet.signPsbt(psbt, signInputs);
@@ -1060,7 +1071,9 @@ export class SpvFromBTCSwap<T extends ChainType>
1060
1071
  feeRate?: number,
1061
1072
  abortSignal?: AbortSignal,
1062
1073
  btcTxCheckIntervalSeconds?: number,
1063
- maxWaitTillAutomaticSettlementSeconds?: number
1074
+ maxWaitTillAutomaticSettlementSeconds?: number,
1075
+ utxos?: BitcoinWalletUtxo[],
1076
+ spendFully?: boolean
1064
1077
  }
1065
1078
  ): Promise<boolean> {
1066
1079
  if(this._state===SpvFromBTCSwapState.CLOSED) throw new Error("Swap encountered a catastrophic failure!");
@@ -1070,7 +1083,7 @@ export class SpvFromBTCSwap<T extends ChainType>
1070
1083
  if(this._state===SpvFromBTCSwapState.CLAIMED || this._state===SpvFromBTCSwapState.FRONTED) throw new Error("Swap already settled or fronted!");
1071
1084
 
1072
1085
  if(this._state===SpvFromBTCSwapState.CREATED) {
1073
- const txId = await this.sendBitcoinTransaction(wallet, options?.feeRate);
1086
+ const txId = await this.sendBitcoinTransaction(wallet, options?.feeRate, options?.utxos, options?.spendFully);
1074
1087
  if(callbacks?.onSourceTransactionSent!=null) callbacks.onSourceTransactionSent(txId);
1075
1088
  }
1076
1089
  if(this._state===SpvFromBTCSwapState.POSTED || this._state===SpvFromBTCSwapState.BROADCASTED) {