@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.
- package/dist/bitcoin/coinselect2/accumulative.d.ts +1 -0
- package/dist/bitcoin/coinselect2/accumulative.js +1 -1
- package/dist/bitcoin/coinselect2/blackjack.d.ts +1 -0
- package/dist/bitcoin/coinselect2/blackjack.js +1 -1
- package/dist/bitcoin/coinselect2/index.d.ts +3 -2
- package/dist/bitcoin/coinselect2/index.js +2 -2
- package/dist/bitcoin/coinselect2/utils.d.ts +7 -2
- package/dist/bitcoin/coinselect2/utils.js +45 -10
- package/dist/bitcoin/wallet/BitcoinWallet.d.ts +8 -25
- package/dist/bitcoin/wallet/BitcoinWallet.js +31 -18
- package/dist/bitcoin/wallet/IBitcoinWallet.d.ts +40 -2
- package/dist/bitcoin/wallet/SingleAddressBitcoinWallet.d.ts +7 -2
- package/dist/bitcoin/wallet/SingleAddressBitcoinWallet.js +10 -4
- package/dist/intermediaries/apis/IntermediaryAPI.d.ts +11 -1
- package/dist/intermediaries/apis/IntermediaryAPI.js +18 -3
- package/dist/swapper/Swapper.d.ts +41 -1
- package/dist/swapper/Swapper.js +69 -7
- package/dist/swaps/spv_swaps/SpvFromBTCSwap.d.ts +9 -3
- package/dist/swaps/spv_swaps/SpvFromBTCSwap.js +9 -5
- package/dist/swaps/spv_swaps/SpvFromBTCWrapper.d.ts +38 -6
- package/dist/swaps/spv_swaps/SpvFromBTCWrapper.js +178 -53
- package/dist/utils/BitcoinUtils.d.ts +2 -0
- package/dist/utils/BitcoinUtils.js +40 -1
- package/dist/utils/BitcoinWalletUtils.d.ts +2 -2
- package/package.json +1 -1
- package/src/bitcoin/coinselect2/accumulative.ts +2 -1
- package/src/bitcoin/coinselect2/blackjack.ts +2 -1
- package/src/bitcoin/coinselect2/index.ts +5 -4
- package/src/bitcoin/coinselect2/utils.ts +55 -14
- package/src/bitcoin/wallet/BitcoinWallet.ts +69 -57
- package/src/bitcoin/wallet/IBitcoinWallet.ts +44 -3
- package/src/bitcoin/wallet/SingleAddressBitcoinWallet.ts +12 -4
- package/src/intermediaries/apis/IntermediaryAPI.ts +21 -5
- package/src/swapper/Swapper.ts +82 -7
- package/src/swaps/spv_swaps/SpvFromBTCSwap.ts +20 -7
- package/src/swaps/spv_swaps/SpvFromBTCWrapper.ts +234 -58
- package/src/utils/BitcoinUtils.ts +41 -0
- 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.
|
|
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.
|
|
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.
|
|
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:
|
|
63
|
-
inputs?:
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
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,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
307
|
-
|
|
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:
|
|
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
|
*/
|