@atomiqlabs/sdk 8.7.7 → 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 +63 -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 +79 -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
@@ -2,5 +2,6 @@ import { CoinselectAddressTypes, CoinselectTxInput, CoinselectTxOutput } from ".
2
2
  export declare function accumulative(utxos: CoinselectTxInput[], outputs: CoinselectTxOutput[], feeRate: number, type: CoinselectAddressTypes, requiredInputs?: CoinselectTxInput[]): {
3
3
  inputs?: CoinselectTxInput[];
4
4
  outputs?: CoinselectTxOutput[];
5
+ effectiveFeeRate?: number;
5
6
  fee: number;
6
7
  };
@@ -7,7 +7,7 @@ const logger = (0, Logger_1.getLogger)("CoinSelect: ");
7
7
  // add inputs until we reach or surpass the target value (or deplete)
8
8
  // worst-case: O(n)
9
9
  function accumulative(utxos, outputs, feeRate, type, requiredInputs) {
10
- if (!isFinite(utils_1.utils.uintOrNaN(feeRate)))
10
+ if (!isFinite(utils_1.utils.numberOrNaN(feeRate)))
11
11
  throw new Error("Invalid feeRate passed!");
12
12
  const inputs = requiredInputs == null ? [] : [...requiredInputs];
13
13
  let bytesAccum = utils_1.utils.transactionBytes(inputs, outputs, type);
@@ -2,5 +2,6 @@ import { CoinselectAddressTypes, CoinselectTxInput, CoinselectTxOutput } from ".
2
2
  export declare function blackjack(utxos: CoinselectTxInput[], outputs: CoinselectTxOutput[], feeRate: number, type: CoinselectAddressTypes, requiredInputs?: CoinselectTxInput[]): {
3
3
  inputs?: CoinselectTxInput[];
4
4
  outputs?: CoinselectTxOutput[];
5
+ effectiveFeeRate?: number;
5
6
  fee: number;
6
7
  };
@@ -5,7 +5,7 @@ const utils_1 = require("./utils");
5
5
  // add inputs until we reach or surpass the target value (or deplete)
6
6
  // worst-case: O(n)
7
7
  function blackjack(utxos, outputs, feeRate, type, requiredInputs) {
8
- if (!isFinite(utils_1.utils.uintOrNaN(feeRate)))
8
+ if (!isFinite(utils_1.utils.numberOrNaN(feeRate)))
9
9
  throw new Error("Invalid feeRate passed!");
10
10
  const inputs = requiredInputs == null ? [] : [...requiredInputs];
11
11
  let bytesAccum = utils_1.utils.transactionBytes(inputs, outputs, type);
@@ -5,12 +5,13 @@ export { CoinselectAddressTypes, CoinselectTxInput, CoinselectTxOutput, DUST_THR
5
5
  export declare function coinSelect(utxos: CoinselectTxInput[], outputs: CoinselectTxOutput[], feeRate: number, type: CoinselectAddressTypes, requiredInputs?: CoinselectTxInput[]): {
6
6
  inputs?: CoinselectTxInput[];
7
7
  outputs?: CoinselectTxOutput[];
8
+ effectiveFeeRate?: number;
8
9
  fee: number;
9
10
  };
10
- export declare function maxSendable(utxos: CoinselectTxInput[], output: {
11
+ export declare function maxSendable(utxos: Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">[], output: {
11
12
  script: Buffer;
12
13
  type: CoinselectAddressTypes;
13
- }, feeRate: number, requiredInputs?: CoinselectTxInput[], additionalOutputs?: {
14
+ }, feeRate: number, requiredInputs?: Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">[], additionalOutputs?: {
14
15
  script: Buffer;
15
16
  value: number;
16
17
  }[]): {
@@ -28,7 +28,7 @@ function coinSelect(utxos, outputs, feeRate, type, requiredInputs) {
28
28
  }
29
29
  exports.coinSelect = coinSelect;
30
30
  function maxSendable(utxos, output, feeRate, requiredInputs, additionalOutputs) {
31
- if (!isFinite(utils_1.utils.uintOrNaN(feeRate)))
31
+ if (!isFinite(utils_1.utils.numberOrNaN(feeRate)))
32
32
  throw new Error("Invalid feeRate passed!");
33
33
  const outputs = additionalOutputs ?? [];
34
34
  const inputs = requiredInputs ?? [];
@@ -42,7 +42,7 @@ function maxSendable(utxos, output, feeRate, requiredInputs, additionalOutputs)
42
42
  const utxoFee = feeRate * utxoBytes;
43
43
  let cpfpFee = 0;
44
44
  if (utxo.cpfp != null && utxo.cpfp.txEffectiveFeeRate < feeRate)
45
- cpfpFee = utxo.cpfp.txVsize * (feeRate - utxo.cpfp.txEffectiveFeeRate);
45
+ cpfpFee = Math.ceil(utxo.cpfp.txVsize * (feeRate - utxo.cpfp.txEffectiveFeeRate));
46
46
  const utxoValue = utils_1.utils.uintOrNaN(utxo.value);
47
47
  // skip detrimental input
48
48
  if (utxoFee + cpfpFee > utxo.value) {
@@ -52,6 +52,7 @@ declare function transactionBytes(inputs: {
52
52
  script?: Buffer;
53
53
  type?: CoinselectAddressTypes;
54
54
  }[], changeType?: CoinselectAddressTypes): number;
55
+ declare function numberOrNaN(v: number): number;
55
56
  declare function uintOrNaN(v: number): number;
56
57
  declare function sumForgiving(range: {
57
58
  value: number;
@@ -59,11 +60,13 @@ declare function sumForgiving(range: {
59
60
  declare function sumOrNaN(range: {
60
61
  value: number;
61
62
  }[]): number;
62
- declare function finalize(inputs: CoinselectTxInput[], outputs: CoinselectTxOutput[], feeRate: number, changeType: CoinselectAddressTypes, cpfpAddFee?: number): {
63
- inputs?: CoinselectTxInput[];
63
+ declare function finalize<T extends Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">>(inputs: T[], outputs: CoinselectTxOutput[], feeRate: number, changeType: CoinselectAddressTypes | null, cpfpAddFee?: number): {
64
+ inputs?: T[];
64
65
  outputs?: CoinselectTxOutput[];
66
+ effectiveFeeRate?: number;
65
67
  fee: number;
66
68
  };
69
+ declare function isDetrimentalInput(feeRate: number, utxo: Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">): boolean;
67
70
  export declare const utils: {
68
71
  dustThreshold: typeof dustThreshold;
69
72
  finalize: typeof finalize;
@@ -73,5 +76,7 @@ export declare const utils: {
73
76
  sumForgiving: typeof sumForgiving;
74
77
  transactionBytes: typeof transactionBytes;
75
78
  uintOrNaN: typeof uintOrNaN;
79
+ numberOrNaN: typeof numberOrNaN;
80
+ isDetrimentalInput: typeof isDetrimentalInput;
76
81
  };
77
82
  export {};
@@ -73,6 +73,15 @@ function transactionBytes(inputs, outputs, changeType) {
73
73
  }
74
74
  return Math.ceil(size);
75
75
  }
76
+ function numberOrNaN(v) {
77
+ if (typeof v !== 'number')
78
+ return NaN;
79
+ if (!isFinite(v))
80
+ return NaN;
81
+ if (v < 0)
82
+ return NaN;
83
+ return v;
84
+ }
76
85
  function uintOrNaN(v) {
77
86
  if (typeof v !== 'number')
78
87
  return NaN;
@@ -91,26 +100,50 @@ function sumOrNaN(range) {
91
100
  return range.reduce((a, x) => a + uintOrNaN(x.value), 0);
92
101
  }
93
102
  function finalize(inputs, outputs, feeRate, changeType, cpfpAddFee = 0) {
94
- const bytesAccum = transactionBytes(inputs, outputs, changeType);
103
+ const bytesAccum = transactionBytes(inputs, outputs, changeType ?? undefined);
95
104
  logger.debug("finalize(): Transaction bytes: ", bytesAccum);
96
- const feeAfterExtraOutput = (feeRate * (bytesAccum + outputBytes({ type: changeType }))) + cpfpAddFee;
97
- logger.debug("finalize(): TX fee after adding change output: ", feeAfterExtraOutput);
98
- const remainderAfterExtraOutput = Math.floor(sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput));
99
- logger.debug("finalize(): Leaves change (changeType=" + changeType + ") value: ", remainderAfterExtraOutput);
100
- // is it worth a change output?
101
- if (remainderAfterExtraOutput >= dustThreshold({ type: changeType })) {
102
- outputs = outputs.concat({ value: remainderAfterExtraOutput, type: changeType });
105
+ if (changeType != null) {
106
+ const feeAfterExtraOutput = (feeRate * (bytesAccum + outputBytes({ type: changeType }))) + cpfpAddFee;
107
+ logger.debug("finalize(): TX fee after adding change output: ", feeAfterExtraOutput);
108
+ const remainderAfterExtraOutput = Math.floor(sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput));
109
+ logger.debug("finalize(): Leaves change (changeType=" + changeType + ") value: ", remainderAfterExtraOutput);
110
+ // is it worth a change output?
111
+ if (remainderAfterExtraOutput >= dustThreshold({ type: changeType })) {
112
+ outputs = outputs.concat({ value: remainderAfterExtraOutput, type: changeType });
113
+ }
103
114
  }
104
115
  const fee = sumOrNaN(inputs) - sumOrNaN(outputs);
105
116
  logger.debug("finalize(): Re-calculated total fee: ", fee);
106
- if (!isFinite(fee))
117
+ if (!isFinite(fee) || fee < 0)
107
118
  return { fee: (feeRate * bytesAccum) + cpfpAddFee };
119
+ let txVSize = exports.utils.transactionBytes(inputs, outputs);
120
+ let txFee = fee;
121
+ const cpfpSortedInputs = [...inputs].sort((a, b) => (b.cpfp?.txEffectiveFeeRate ?? 0) - (a.cpfp?.txEffectiveFeeRate ?? 0));
122
+ cpfpSortedInputs.forEach(input => {
123
+ if (input.cpfp == null)
124
+ return;
125
+ const currentEffectiveFeeRate = txFee / txVSize;
126
+ if (currentEffectiveFeeRate > input.cpfp.txEffectiveFeeRate) {
127
+ txVSize += input.cpfp.txVsize;
128
+ txFee += input.cpfp.txVsize * input.cpfp.txEffectiveFeeRate;
129
+ }
130
+ });
108
131
  return {
109
132
  inputs: inputs,
110
133
  outputs: outputs,
134
+ effectiveFeeRate: txFee / txVSize,
111
135
  fee: fee
112
136
  };
113
137
  }
138
+ function isDetrimentalInput(feeRate, utxo) {
139
+ const utxoBytes = exports.utils.inputBytes(utxo);
140
+ const utxoFee = feeRate * utxoBytes;
141
+ let cpfpFee = 0;
142
+ if (utxo.cpfp != null && utxo.cpfp.txEffectiveFeeRate < feeRate)
143
+ cpfpFee = Math.ceil(utxo.cpfp.txVsize * (feeRate - utxo.cpfp.txEffectiveFeeRate));
144
+ // skip detrimental input
145
+ return utxoFee + cpfpFee > utxo.value;
146
+ }
114
147
  exports.utils = {
115
148
  dustThreshold: dustThreshold,
116
149
  finalize: finalize,
@@ -119,5 +152,7 @@ exports.utils = {
119
152
  sumOrNaN: sumOrNaN,
120
153
  sumForgiving: sumForgiving,
121
154
  transactionBytes: transactionBytes,
122
- uintOrNaN: uintOrNaN
155
+ uintOrNaN: uintOrNaN,
156
+ numberOrNaN: numberOrNaN,
157
+ isDetrimentalInput
123
158
  };
@@ -1,29 +1,8 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import { CoinselectAddressTypes } from "../coinselect2";
4
2
  import { BTC_NETWORK } from "@scure/btc-signer/utils";
5
3
  import { Transaction } from "@scure/btc-signer";
6
- import { IBitcoinWallet } from "./IBitcoinWallet";
7
- import { Buffer } from "buffer";
4
+ import { BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet } from "./IBitcoinWallet";
8
5
  import { BitcoinNetwork, BitcoinRpcWithAddressIndex } from "@atomiqlabs/base";
9
- /**
10
- * UTXO data structure for Bitcoin wallets
11
- *
12
- * @category Bitcoin
13
- */
14
- export type BitcoinWalletUtxo = {
15
- vout: number;
16
- txId: string;
17
- value: number;
18
- type: CoinselectAddressTypes;
19
- outputScript: Buffer;
20
- address: string;
21
- cpfp?: {
22
- txVsize: number;
23
- txEffectiveFeeRate: number;
24
- };
25
- confirmed: boolean;
26
- };
27
6
  /**
28
7
  * Identifies the address type of a Bitcoin address
29
8
  *
@@ -96,7 +75,7 @@ export declare abstract class BitcoinWallet implements IBitcoinWallet {
96
75
  pubkey: string;
97
76
  address: string;
98
77
  addressType: CoinselectAddressTypes;
99
- }[], psbt: Transaction, feeRate?: number): Promise<{
78
+ }[], psbt: Transaction, _feeRate?: number, utxos?: BitcoinWalletUtxo[], spendFully?: boolean): Promise<{
100
79
  fee: number;
101
80
  psbt?: Transaction;
102
81
  inputAddressIndexes?: {
@@ -106,13 +85,13 @@ export declare abstract class BitcoinWallet implements IBitcoinWallet {
106
85
  protected _getSpendableBalance(sendingAccounts: {
107
86
  address: string;
108
87
  addressType: CoinselectAddressTypes;
109
- }[], psbt?: Transaction, feeRate?: number): Promise<{
88
+ }[], psbt?: Transaction, feeRate?: number, outputAddressType?: CoinselectAddressTypes, utxoPool?: BitcoinWalletUtxoBase[]): Promise<{
110
89
  balance: bigint;
111
90
  feeRate: number;
112
91
  totalFee: number;
113
92
  }>;
114
93
  abstract sendTransaction(address: string, amount: bigint, feeRate?: number): Promise<string>;
115
- abstract fundPsbt(psbt: Transaction, feeRate?: number): Promise<Transaction>;
94
+ abstract fundPsbt(psbt: Transaction, feeRate?: number, utxos?: BitcoinWalletUtxo[], spendFully?: boolean): Promise<Transaction>;
116
95
  abstract signPsbt(psbt: Transaction, signInputs: number[]): Promise<Transaction>;
117
96
  abstract getTransactionFee(address: string, amount: bigint, feeRate?: number): Promise<number>;
118
97
  abstract getFundedPsbtFee(psbt: Transaction, feeRate?: number): Promise<number>;
@@ -127,4 +106,8 @@ export declare abstract class BitcoinWallet implements IBitcoinWallet {
127
106
  totalFee: number;
128
107
  }>;
129
108
  static bitcoinNetworkToObject(network: BitcoinNetwork): BTC_NETWORK;
109
+ static getSpendableBalance(utxoPool: BitcoinWalletUtxoBase[], feeRate: number, psbt?: Transaction, outputAddressType?: CoinselectAddressTypes): {
110
+ balance: bigint;
111
+ totalFee: number;
112
+ };
130
113
  }
@@ -5,10 +5,10 @@ const coinselect2_1 = require("../coinselect2");
5
5
  const utils_1 = require("@scure/btc-signer/utils");
6
6
  const btc_signer_1 = require("@scure/btc-signer");
7
7
  const buffer_1 = require("buffer");
8
- const Utils_1 = require("../../utils/Utils");
9
8
  const BitcoinUtils_1 = require("../../utils/BitcoinUtils");
10
9
  const Logger_1 = require("../../utils/Logger");
11
10
  const base_1 = require("@atomiqlabs/base");
11
+ const utils_2 = require("../coinselect2/utils");
12
12
  /**
13
13
  * Identifies the address type of a Bitcoin address
14
14
  *
@@ -135,10 +135,11 @@ class BitcoinWallet {
135
135
  });
136
136
  return this._fundPsbt(sendingAccounts, psbt, feeRate);
137
137
  }
138
- async _fundPsbt(sendingAccounts, psbt, feeRate) {
139
- if (feeRate == null)
140
- feeRate = await this.getFeeRate();
141
- const utxoPool = (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat();
138
+ async _fundPsbt(sendingAccounts, psbt, _feeRate, utxos, spendFully) {
139
+ const feeRate = _feeRate ?? await this.getFeeRate();
140
+ const utxoPool = utxos ?? (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat();
141
+ if (spendFully && utxoPool == null)
142
+ throw new Error("Cannot fully spend when no utxos are passed!");
142
143
  logger.debug("_fundPsbt(): fee rate: " + feeRate + " utxo pool: ", utxoPool);
143
144
  const accountPubkeys = {};
144
145
  sendingAccounts.forEach(acc => accountPubkeys[acc.address] = acc.pubkey);
@@ -177,13 +178,23 @@ class BitcoinWallet {
177
178
  });
178
179
  }
179
180
  logger.debug("_fundPsbt(): Coinselect targets: ", targets);
180
- let coinselectResult = (0, coinselect2_1.coinSelect)(utxoPool, targets, feeRate, sendingAccounts[0].addressType, requiredInputs);
181
+ let coinselectResult = spendFully
182
+ ? utils_2.utils.finalize(requiredInputs.concat(utxoPool.filter(utxo => !utils_2.utils.isDetrimentalInput(feeRate, utxo))), targets, feeRate, null)
183
+ : (0, coinselect2_1.coinSelect)(utxoPool, targets, feeRate, sendingAccounts[0].addressType, requiredInputs);
181
184
  logger.debug("_fundPsbt(): Coinselect result: ", coinselectResult);
182
- if (coinselectResult.inputs == null || coinselectResult.outputs == null) {
185
+ if (coinselectResult.inputs == null || coinselectResult.outputs == null || coinselectResult.effectiveFeeRate == null) {
183
186
  return {
184
187
  fee: coinselectResult.fee
185
188
  };
186
189
  }
190
+ if (spendFully && feeRate != null) {
191
+ const maximumAllowedFeeRate = (1.5 * feeRate) + 10;
192
+ if (coinselectResult.effectiveFeeRate > maximumAllowedFeeRate)
193
+ throw new Error(`Effective fee rate too high, feeRate: ${coinselectResult.effectiveFeeRate} sats/vB, maximum: ${maximumAllowedFeeRate} sats/vB!`);
194
+ const minimumAllowedFeeRate = 0.9 * feeRate;
195
+ if (coinselectResult.effectiveFeeRate < minimumAllowedFeeRate)
196
+ throw new Error(`Effective fee rate too low, feeRate: ${coinselectResult.effectiveFeeRate} sats/vB, minimum: ${minimumAllowedFeeRate} sats/vB!`);
197
+ }
187
198
  // Remove in/outs that are already in the PSBT
188
199
  coinselectResult.inputs.splice(0, psbt.inputsLength);
189
200
  coinselectResult.outputs.splice(0, psbt.outputsLength);
@@ -264,9 +275,18 @@ class BitcoinWallet {
264
275
  inputAddressIndexes
265
276
  };
266
277
  }
267
- async _getSpendableBalance(sendingAccounts, psbt, feeRate) {
278
+ async _getSpendableBalance(sendingAccounts, psbt, feeRate, outputAddressType, utxoPool) {
268
279
  feeRate ??= await this.getFeeRate();
269
- const utxoPool = (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat();
280
+ utxoPool ??= (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat();
281
+ return {
282
+ ...BitcoinWallet.getSpendableBalance(utxoPool ?? (await Promise.all(sendingAccounts.map(acc => this._getUtxoPool(acc.address, acc.addressType)))).flat(), feeRate ?? await this.getFeeRate(), psbt, outputAddressType),
283
+ feeRate
284
+ };
285
+ }
286
+ static bitcoinNetworkToObject(network) {
287
+ return btcNetworkMapping[network];
288
+ }
289
+ static getSpendableBalance(utxoPool, feeRate, psbt, outputAddressType) {
270
290
  const requiredInputs = [];
271
291
  if (psbt != null)
272
292
  for (let i = 0; i < psbt.inputsLength; i++) {
@@ -303,20 +323,13 @@ class BitcoinWallet {
303
323
  script: buffer_1.Buffer.from(output.script)
304
324
  });
305
325
  }
306
- const target = btc_signer_1.OutScript.encode({
307
- type: "wsh",
308
- hash: (0, Utils_1.randomBytes)(32)
309
- });
310
- let coinselectResult = (0, coinselect2_1.maxSendable)(utxoPool, { script: buffer_1.Buffer.from(target), type: "p2wsh" }, feeRate, requiredInputs, additionalOutputs);
326
+ const target = (0, BitcoinUtils_1.getDummyOutputScript)(outputAddressType ?? "p2wsh");
327
+ let coinselectResult = (0, coinselect2_1.maxSendable)(utxoPool, { script: buffer_1.Buffer.from(target), type: outputAddressType ?? "p2wsh" }, feeRate, requiredInputs, additionalOutputs);
311
328
  logger.debug("_getSpendableBalance(): Max spendable result: ", coinselectResult);
312
329
  return {
313
- feeRate: feeRate,
314
330
  balance: BigInt(Math.floor(coinselectResult.value)),
315
331
  totalFee: coinselectResult.fee
316
332
  };
317
333
  }
318
- static bitcoinNetworkToObject(network) {
319
- return btcNetworkMapping[network];
320
- }
321
334
  }
322
335
  exports.BitcoinWallet = BitcoinWallet;
@@ -1,4 +1,32 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
1
3
  import { Transaction } from "@scure/btc-signer";
4
+ import { CoinselectAddressTypes } from "../coinselect2";
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
+ * Base UTXO data structure used for maximum spendable balance calculation, doesn't contain all the fields necessary
25
+ * for constructing the full transaction.
26
+ *
27
+ * @category Bitcoin
28
+ */
29
+ export type BitcoinWalletUtxoBase = Omit<BitcoinWalletUtxo, "txId" | "vout" | "outputScript" | "address" | "confirmed">;
2
30
  /**
3
31
  * Type guard to check if an object implements {@link IBitcoinWallet}
4
32
  *
@@ -25,8 +53,12 @@ export interface IBitcoinWallet {
25
53
  *
26
54
  * @param psbt PSBT to add the inputs to
27
55
  * @param feeRate Optional fee rate in sats/vB to use for the transaction
56
+ * @param utxos Pre-fetched list of UTXOs to spend from
57
+ * @param spendFully Instructs the wallet to spend all the passed UTXOs in the transaction without creating any
58
+ * change output, if the `feeRate` is passed, it will also enforce that the feeRate in sats/vB for the resulting
59
+ * transaction is not more than 50% and 10 sats/vB larger (considering also the CPFP adjustments)
28
60
  */
29
- fundPsbt(psbt: Transaction, feeRate?: number): Promise<Transaction>;
61
+ fundPsbt(psbt: Transaction, feeRate?: number, utxos?: BitcoinWalletUtxo[], spendFully?: boolean): Promise<Transaction>;
30
62
  /**
31
63
  * Signs inputs in the provided PSBT
32
64
  *
@@ -69,10 +101,16 @@ export interface IBitcoinWallet {
69
101
  *
70
102
  * @param psbt A PSBT to which additional inputs from wallet's UTXO set will be added and fee estimated
71
103
  * @param feeRate Optional fee rate in sats/vB to use for the transaction
104
+ * @param outputAddressType Expected output address type, if known
105
+ * @param utxos Optional pre-fetched UTXOs
72
106
  */
73
- getSpendableBalance(psbt?: Transaction, feeRate?: number): Promise<{
107
+ getSpendableBalance(psbt?: Transaction, feeRate?: number, outputAddressType?: CoinselectAddressTypes, utxos?: BitcoinWalletUtxoBase[]): Promise<{
74
108
  balance: bigint;
75
109
  feeRate: number;
76
110
  totalFee: number;
77
111
  }>;
112
+ /**
113
+ * Returns a list of available UTXOs for the wallet
114
+ */
115
+ getUtxoPool?(): Promise<BitcoinWalletUtxo[]>;
78
116
  }
@@ -6,6 +6,7 @@ import { Transaction } from "@scure/btc-signer";
6
6
  import { Buffer } from "buffer";
7
7
  import { BitcoinWallet } from "./BitcoinWallet";
8
8
  import { BitcoinNetwork, BitcoinRpcWithAddressIndex } from "@atomiqlabs/base";
9
+ import { BitcoinWalletUtxo, BitcoinWalletUtxoBase } from "./IBitcoinWallet";
9
10
  /**
10
11
  * Bitcoin wallet implementation deriving a single address from a WIF encoded private key
11
12
  *
@@ -37,7 +38,7 @@ export declare class SingleAddressBitcoinWallet extends BitcoinWallet {
37
38
  /**
38
39
  * @inheritDoc
39
40
  */
40
- fundPsbt(inputPsbt: Transaction, feeRate?: number): Promise<Transaction>;
41
+ fundPsbt(inputPsbt: Transaction, feeRate?: number, utxos?: BitcoinWalletUtxo[], spendFully?: boolean): Promise<Transaction>;
41
42
  /**
42
43
  * @inheritDoc
43
44
  */
@@ -68,11 +69,15 @@ export declare class SingleAddressBitcoinWallet extends BitcoinWallet {
68
69
  /**
69
70
  * @inheritDoc
70
71
  */
71
- getSpendableBalance(psbt?: Transaction, feeRate?: number): Promise<{
72
+ getSpendableBalance(psbt?: Transaction, feeRate?: number, outputAddressType?: CoinselectAddressTypes, utxos?: BitcoinWalletUtxoBase[]): Promise<{
72
73
  balance: bigint;
73
74
  feeRate: number;
74
75
  totalFee: number;
75
76
  }>;
77
+ /**
78
+ * @inheritDoc
79
+ */
80
+ getUtxoPool(): Promise<BitcoinWalletUtxo[]>;
76
81
  /**
77
82
  * Generates a new random private key WIF that can be used to instantiate the bitcoin wallet instance
78
83
  *
@@ -80,8 +80,8 @@ class SingleAddressBitcoinWallet extends BitcoinWallet_1.BitcoinWallet {
80
80
  /**
81
81
  * @inheritDoc
82
82
  */
83
- async fundPsbt(inputPsbt, feeRate) {
84
- const { psbt } = await super._fundPsbt(this.toBitcoinWalletAccounts(), inputPsbt, feeRate);
83
+ async fundPsbt(inputPsbt, feeRate, utxos, spendFully) {
84
+ const { psbt } = await super._fundPsbt(this.toBitcoinWalletAccounts(), inputPsbt, feeRate, utxos, spendFully);
85
85
  if (psbt == null) {
86
86
  throw new Error("Not enough balance!");
87
87
  }
@@ -133,8 +133,14 @@ class SingleAddressBitcoinWallet extends BitcoinWallet_1.BitcoinWallet {
133
133
  /**
134
134
  * @inheritDoc
135
135
  */
136
- getSpendableBalance(psbt, feeRate) {
137
- return this._getSpendableBalance([{ address: this.address, addressType: this.addressType }], psbt, feeRate);
136
+ getSpendableBalance(psbt, feeRate, outputAddressType, utxos) {
137
+ return this._getSpendableBalance([{ address: this.address, addressType: this.addressType }], psbt, feeRate, outputAddressType, utxos);
138
+ }
139
+ /**
140
+ * @inheritDoc
141
+ */
142
+ async getUtxoPool() {
143
+ return this._getUtxoPool(this.address, this.addressType);
138
144
  }
139
145
  /**
140
146
  * Generates a new random private key WIF that can be used to instantiate the bitcoin wallet instance
@@ -234,17 +234,27 @@ declare const SpvFromBTCPrepareResponseSchema: {
234
234
  readonly callerFeeShare: FieldTypeEnum.BigInt;
235
235
  readonly frontingFeeShare: FieldTypeEnum.BigInt;
236
236
  readonly executionFeeShare: FieldTypeEnum.BigInt;
237
+ readonly usedUtxoInputCalculation: FieldTypeEnum.BooleanOptional;
237
238
  };
238
239
  export type SpvFromBTCPrepareResponseType = RequestSchemaResult<typeof SpvFromBTCPrepareResponseSchema>;
239
240
  export type SpvFromBTCPrepare = SwapInit & {
240
241
  address: string;
241
- amount: bigint;
242
+ amount: Promise<bigint>;
242
243
  gasAmount: bigint;
243
244
  gasToken: string;
244
245
  exactOut: boolean;
245
246
  callerFeeRate: Promise<bigint>;
246
247
  frontingFeeRate: bigint;
247
248
  stickyAddress?: boolean;
249
+ amountUtxos?: Promise<{
250
+ value: number;
251
+ vSize: number;
252
+ cpfp?: {
253
+ effectiveVSize: number;
254
+ effectiveFeeRate: number;
255
+ };
256
+ }[] | undefined>;
257
+ amountFeeRate?: Promise<number | undefined>;
248
258
  };
249
259
  declare const SpvFromBTCInitResponseSchema: {
250
260
  readonly txId: FieldTypeEnum.String;
@@ -123,7 +123,8 @@ const SpvFromBTCPrepareResponseSchema = {
123
123
  gasSwapFee: SchemaVerifier_1.FieldTypeEnum.BigInt,
124
124
  callerFeeShare: SchemaVerifier_1.FieldTypeEnum.BigInt,
125
125
  frontingFeeShare: SchemaVerifier_1.FieldTypeEnum.BigInt,
126
- executionFeeShare: SchemaVerifier_1.FieldTypeEnum.BigInt
126
+ executionFeeShare: SchemaVerifier_1.FieldTypeEnum.BigInt,
127
+ usedUtxoInputCalculation: SchemaVerifier_1.FieldTypeEnum.BooleanOptional
127
128
  };
128
129
  const SpvFromBTCInitResponseSchema = {
129
130
  txId: SchemaVerifier_1.FieldTypeEnum.String
@@ -534,17 +535,31 @@ class IntermediaryAPI {
534
535
  * @throws {RequestError} If non-200 http response code is returned
535
536
  */
536
537
  static prepareSpvFromBTC(chainIdentifier, baseUrl, init, timeout, abortSignal, streamRequest) {
538
+ //We need to make sure we only send the amount parameter after the amountUtxos and amountFeeRate resolve
539
+ // this is needed, because in the LP code to maintain backwards compatibility the amountUtxos and amountFeeRate
540
+ // params are checked immediately after the amount param (and other params) are received, if amount were sent
541
+ // first without the amountUtxos or amountFeeRate populated these fields would've been skipped altogether
542
+ const amountPromise = (async () => {
543
+ if (init.amountUtxos != null)
544
+ await init.amountUtxos;
545
+ if (init.amountFeeRate != null)
546
+ await init.amountFeeRate;
547
+ const amount = await init.amount;
548
+ return amount.toString(10);
549
+ })();
537
550
  const responseBodyPromise = (0, StreamingFetchPromise_1.streamingFetchPromise)(baseUrl + "/frombtc_spv/getQuote?chain=" + encodeURIComponent(chainIdentifier), {
538
551
  exactOut: init.exactOut,
539
552
  ...init.additionalParams,
540
553
  address: init.address,
541
- amount: init.amount.toString(10),
554
+ amount: amountPromise,
542
555
  token: init.token,
543
556
  gasAmount: init.gasAmount.toString(10),
544
557
  gasToken: init.gasToken,
545
558
  frontingFeeRate: init.frontingFeeRate.toString(10),
546
559
  callerFeeRate: init.callerFeeRate.then(val => val.toString(10)),
547
- stickyAddress: init.stickyAddress
560
+ stickyAddress: init.stickyAddress,
561
+ amountUtxos: init.amountUtxos,
562
+ amountFeeRate: init.amountFeeRate
548
563
  }, {
549
564
  code: SchemaVerifier_1.FieldTypeEnum.Number,
550
565
  msg: SchemaVerifier_1.FieldTypeEnum.String,
@@ -39,6 +39,8 @@ import { LNURLPay } from "../types/lnurl/LNURLPay";
39
39
  import { NotNever } from "../utils/TypeUtils";
40
40
  import { LightningInvoiceCreateService } from "../types/wallets/LightningInvoiceCreateService";
41
41
  import { SwapSide } from "../enums/SwapSide";
42
+ import { BitcoinWalletUtxo, IBitcoinWallet } from "../bitcoin/wallet/IBitcoinWallet";
43
+ import { MinimalBitcoinWalletInterface } from "../types/wallets/MinimalBitcoinWalletInterface";
42
44
  /**
43
45
  * Configuration options for the Swapper
44
46
  * @category Core
@@ -346,7 +348,7 @@ export declare class Swapper<T extends MultiChain> extends EventEmitter<{
346
348
  * @param additionalParams Additional parameters sent to the LP when creating the swap
347
349
  * @param options Additional options for the swap
348
350
  */
349
- createFromBTCSwapNew<ChainIdentifier extends ChainIds<T>>(chainIdentifier: ChainIdentifier, recipient: string, tokenAddress: string, amount: bigint, exactOut?: boolean, additionalParams?: Record<string, any> | undefined, options?: SpvFromBTCOptions): Promise<SpvFromBTCSwap<T[ChainIdentifier]>>;
351
+ createFromBTCSwapNew<ChainIdentifier extends ChainIds<T>>(chainIdentifier: ChainIdentifier, recipient: string, tokenAddress: string, amount: bigint | null, exactOut?: boolean, additionalParams?: Record<string, any> | undefined, options?: SpvFromBTCOptions): Promise<SpvFromBTCSwap<T[ChainIdentifier]>>;
350
352
  /**
351
353
  * Creates LEGACY Bitcoin -> Smart chain ({@link SwapType.FROM_BTC}) swap
352
354
  *
@@ -505,6 +507,44 @@ export declare class Swapper<T extends MultiChain> extends EventEmitter<{
505
507
  swap<C extends ChainIds<T>>(srcToken: Token<C> | string, dstToken: Token<C> | string, amount: bigint | string | undefined, exactIn: boolean | SwapAmountType, src: undefined | string | LNURLWithdraw, dst: string | LNURLPay | LightningInvoiceCreateService, options?: FromBTCLNOptions | SpvFromBTCOptions | FromBTCOptions | ToBTCOptions | (ToBTCLNOptions & {
506
508
  comment?: string;
507
509
  }) | FromBTCLNAutoOptions): Promise<ISwap<T[C]>>;
510
+ /**
511
+ * A helper function to sweep all the funds from a given wallet in a single swap, after getting the quote you can
512
+ * execute the swap by passing the returned `feeRate` and `utxos` to the {@link SpvFromBTCSwap.execute},
513
+ * {@link SpvFromBTCSwap.getFundedPsbt} or {@link SpvFromBTCSwap.sendBitcoinTransaction} functions along
514
+ * with `spendFully=true`.
515
+ *
516
+ * @example
517
+ * Create the swap first using this function
518
+ * ```ts
519
+ * const {swap, utxos, btcFeeRate} = await swapper.sweepBitcoinWallet(wallet, Tokens.CITREA.CBTC, dstAddress);
520
+ * ```
521
+ * Then execute it using one of these execution paths - ensure that you supply the returned `utxos`, `btcFeeRate`
522
+ * params and also set `spendFully` to `true`!
523
+ *
524
+ * a) Execute and pass the returned utxos and btcFeeRate:
525
+ * ```ts
526
+ * await swap.execute(wallet, undefined, {feeRate: btcFeeRate, utxos: utxos, spendFully: true});
527
+ * ```
528
+ *
529
+ * b) Get funded PSBT to sign externally:
530
+ * ```ts
531
+ * const {psbt, psbtHex, psbtBase64, signInputs} = await swap.getFundedPsbt(wallet, btcFeeRate, undefined, utxos, true);
532
+ * // Sign the psbt at the specified signInputs indices
533
+ * const signedPsbt = ...;
534
+ * // Then submit back to the SDK
535
+ * await swap.submitPsbt(signedPsbt);
536
+ * ```
537
+ *
538
+ * c) Only sign and send the signed PSBT with the provided wallet:
539
+ * ```ts
540
+ * await swap.sendBitcoinTransaction(wallet, btcFeeRate, utxos, true);
541
+ * ```
542
+ */
543
+ sweepBitcoinWallet<C extends ChainIds<T>>(srcWallet: IBitcoinWallet | MinimalBitcoinWalletInterface, _dstToken: SCToken<C> | string, dstAddress: string, options?: SpvFromBTCOptions): Promise<{
544
+ swap: SpvFromBTCSwap<T[C]>;
545
+ utxos: BitcoinWalletUtxo[];
546
+ btcFeeRate: number;
547
+ }>;
508
548
  /**
509
549
  * Returns all swaps
510
550
  */