@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
391
|
-
|
|
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:
|
|
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,
|
package/src/swapper/Swapper.ts
CHANGED
|
@@ -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:
|
|
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(
|
|
863
|
-
|
|
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(
|
|
1028
|
-
|
|
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) {
|