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