@xchainjs/xchain-bitcoin
Version:
Custom Bitcoin client and utilities used by XChainJS clients
886 lines (868 loc) • 41 kB
JavaScript
;
var ecc = require('@bitcoin-js/tiny-secp256k1-asmjs');
var xchainClient = require('@xchainjs/xchain-client');
var xchainCrypto = require('@xchainjs/xchain-crypto');
var bip32 = require('@scure/bip32');
var Bitcoin = require('bitcoinjs-lib');
var ecpair = require('ecpair');
var xchainUtxo = require('@xchainjs/xchain-utxo');
var xchainUtil = require('@xchainjs/xchain-util');
var xchainUtxoProviders = require('@xchainjs/xchain-utxo-providers');
var AppBtc = require('@ledgerhq/hw-app-btc');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var ecc__namespace = /*#__PURE__*/_interopNamespace(ecc);
var Bitcoin__namespace = /*#__PURE__*/_interopNamespace(Bitcoin);
var AppBtc__default = /*#__PURE__*/_interopDefault(AppBtc);
exports.AddressFormat = void 0;
(function (AddressFormat) {
AddressFormat[AddressFormat["P2WPKH"] = 0] = "P2WPKH";
AddressFormat[AddressFormat["P2TR"] = 1] = "P2TR";
})(exports.AddressFormat || (exports.AddressFormat = {}));
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* Minimum transaction fee
* 1000 satoshi/kB (similar to current `minrelaytxfee`)
* @see https://github.com/bitcoin/bitcoin/blob/db88db47278d2e7208c50d16ab10cb355067d071/src/validation.h#L56
*/
const MIN_TX_FEE = 1000;
// Decimal places for Bitcoin
const BTC_DECIMAL = 8;
// Lower and upper bounds for fee rates
const LOWER_FEE_BOUND = 1;
const UPPER_FEE_BOUND = 1000;
// Symbols for Bitcoin
const BTC_SYMBOL = '₿';
const BTC_SATOSHI_SYMBOL = '⚡';
/**
* Chain identifier for Bitcoin mainnet
*/
const BTCChain = 'BTC';
/**
* Base "chain" asset on bitcoin main net.
*/
const AssetBTC = { chain: BTCChain, symbol: 'BTC', ticker: 'BTC', type: xchainUtil.AssetType.NATIVE };
// Explorer providers for Bitcoin
const BTC_MAINNET_EXPLORER = new xchainClient.ExplorerProvider('https://blockstream.info/', 'https://blockstream.info/address/%%ADDRESS%%', 'https://blockstream.info/tx/%%TX_ID%%');
const BTC_TESTNET_EXPLORER = new xchainClient.ExplorerProvider('https://blockstream.info/testnet/', 'https://blockstream.info/testnet/address/%%ADDRESS%%', 'https://blockstream.info/testnet/tx/%%TX_ID%%');
const blockstreamExplorerProviders = {
[xchainClient.Network.Testnet]: BTC_TESTNET_EXPLORER,
[xchainClient.Network.Stagenet]: BTC_MAINNET_EXPLORER,
[xchainClient.Network.Mainnet]: BTC_MAINNET_EXPLORER,
};
// Sochain data providers for Bitcoin
const testnetSochainProvider = new xchainUtxoProviders.SochainProvider('https://sochain.com/api/v3', process.env.SOCHAIN_API_KEY || '', BTCChain, AssetBTC, 8, xchainUtxoProviders.SochainNetwork.BTCTEST);
const mainnetSochainProvider = new xchainUtxoProviders.SochainProvider('https://sochain.com/api/v3', process.env.SOCHAIN_API_KEY || '', BTCChain, AssetBTC, 8, xchainUtxoProviders.SochainNetwork.BTC);
const SochainDataProviders = {
[xchainClient.Network.Testnet]: testnetSochainProvider,
[xchainClient.Network.Stagenet]: mainnetSochainProvider,
[xchainClient.Network.Mainnet]: mainnetSochainProvider,
};
// Haskoin data providers for Bitcoin
const testnetHaskoinProvider = new xchainUtxoProviders.HaskoinProvider('https://api.haskoin.com', BTCChain, AssetBTC, 8, xchainUtxoProviders.HaskoinNetwork.BTCTEST);
const mainnetHaskoinProvider = new xchainUtxoProviders.HaskoinProvider('https://api.haskoin.com', BTCChain, AssetBTC, 8, xchainUtxoProviders.HaskoinNetwork.BTC);
const HaskoinDataProviders = {
[xchainClient.Network.Testnet]: testnetHaskoinProvider,
[xchainClient.Network.Stagenet]: mainnetHaskoinProvider,
[xchainClient.Network.Mainnet]: mainnetHaskoinProvider,
};
// Blockcypher data providers for Bitcoin
const testnetBlockcypherProvider = new xchainUtxoProviders.BlockcypherProvider('https://api.blockcypher.com/v1', BTCChain, AssetBTC, 8, xchainUtxoProviders.BlockcypherNetwork.BTCTEST, process.env.BLOCKCYPHER_API_KEY || '');
const mainnetBlockcypherProvider = new xchainUtxoProviders.BlockcypherProvider('https://api.blockcypher.com/v1', BTCChain, AssetBTC, 8, xchainUtxoProviders.BlockcypherNetwork.BTC, process.env.BLOCKCYPHER_API_KEY || '');
const BlockcypherDataProviders = {
[xchainClient.Network.Testnet]: testnetBlockcypherProvider,
[xchainClient.Network.Stagenet]: mainnetBlockcypherProvider,
[xchainClient.Network.Mainnet]: mainnetBlockcypherProvider,
};
// Bitgo data providers for Bitcoin
const mainnetBitgoProvider = new xchainUtxoProviders.BitgoProvider({
baseUrl: 'https://app.bitgo.com',
chain: BTCChain,
});
const BitgoProviders = {
[xchainClient.Network.Testnet]: undefined,
[xchainClient.Network.Stagenet]: mainnetBitgoProvider,
[xchainClient.Network.Mainnet]: mainnetBitgoProvider,
};
const tapRootDerivationPaths = {
[xchainClient.Network.Mainnet]: `m/86'/0'/0'/0/`,
[xchainClient.Network.Testnet]: `m/86'/1'/0'/0/`,
[xchainClient.Network.Stagenet]: `m/86'/0'/0'/0/`,
};
// Import statements for necessary modules and types
// Constants defining the sizes of various components in a Bitcoin transaction
const TX_EMPTY_SIZE = 4 + 1 + 1 + 4; // Total size of an empty transaction
const TX_INPUT_BASE = 32 + 4 + 1 + 4; // Size of a base input in a transaction
const TX_INPUT_PUBKEYHASH = 107; // Size of an input with a public key hash
const TX_OUTPUT_BASE = 8 + 1; // Size of a base output in a transaction
const TX_OUTPUT_PUBKEYHASH = 25; // Size of an output with a public key hash
/**
* Function to calculate the size of an input in a transaction.
* @param {UTXO} input - The UTXO (Unspent Transaction Output) for which to calculate the size.
* @returns {number} The size of the input.
*/
const inputBytes = (input) => {
var _a, _b;
return TX_INPUT_BASE + (((_a = input.witnessUtxo) === null || _a === void 0 ? void 0 : _a.script) ? (_b = input.witnessUtxo) === null || _b === void 0 ? void 0 : _b.script.length : TX_INPUT_PUBKEYHASH);
};
/**
* Function to get the Bitcoin network to be used with bitcoinjs.
*
* @param {Network} network - The network type (Mainnet, Testnet, or Stagenet).
* @returns {Bitcoin.Network} The Bitcoin network.
*/
const btcNetwork = (network) => {
switch (network) {
case xchainClient.Network.Mainnet:
case xchainClient.Network.Stagenet:
return Bitcoin__namespace.networks.bitcoin; // Return the Bitcoin mainnet or stagenet network
case xchainClient.Network.Testnet:
return Bitcoin__namespace.networks.testnet; // Return the Bitcoin testnet network
}
};
/**
* Function to validate a Bitcoin address.
* @param {Address} address - The Bitcoin address to validate.
* @param {Network} network - The network type (Mainnet, Testnet, or Stagenet).
* @returns {boolean} `true` if the address is valid, `false` otherwise.
*/
const validateAddress = (address, network) => {
try {
Bitcoin__namespace.address.toOutputScript(address, btcNetwork(network)); // Try to convert the address to an output script using the specified network
return true; // If successful, the address is valid
}
catch (_a) {
return false; // If an error occurs, the address is invalid
}
};
/**
* Function to get the address prefix based on the network.
* @param {Network} network - The network type (Mainnet, Testnet, or Stagenet).
* @returns {string} The address prefix based on the network.
*/
const getPrefix = (network) => {
switch (network) {
case xchainClient.Network.Mainnet:
case xchainClient.Network.Stagenet:
return 'bc1'; // Return the address prefix for Bitcoin mainnet or stagenet
case xchainClient.Network.Testnet:
return 'tb1'; // Return the address prefix for Bitcoin testnet
}
};
/**
* Converts a public key to an X-only public key.
* @param pubKey The public key to convert.
* @returns The X-only public key.
*/
const toXOnly = (pubKey) => (pubKey.length === 32 ? pubKey : pubKey.subarray(1, 33));
// Default parameters for the Bitcoin UTXO client
const defaultBTCParams = {
network: xchainClient.Network.Mainnet,
phrase: '',
explorerProviders: blockstreamExplorerProviders,
dataProviders: [BitgoProviders, BlockcypherDataProviders],
rootDerivationPaths: {
[xchainClient.Network.Mainnet]: `m/84'/0'/0'/0/`, // Not BIP44 compliant but compatible with pre-HD wallets
[xchainClient.Network.Testnet]: `m/84'/1'/0'/0/`,
[xchainClient.Network.Stagenet]: `m/84'/0'/0'/0/`,
},
feeBounds: {
lower: LOWER_FEE_BOUND,
upper: UPPER_FEE_BOUND,
},
};
/**
* Custom Bitcoin client
*/
class Client extends xchainUtxo.Client {
/**
* Constructor
* Initializes the client with network type and other parameters.
* @param {UtxoClientParams} params
*/
constructor(params = Object.assign(Object.assign({}, defaultBTCParams), { addressFormat: exports.AddressFormat.P2WPKH })) {
var _a, _b, _c;
super(BTCChain, {
network: params.network,
rootDerivationPaths: params.rootDerivationPaths,
phrase: params.phrase,
feeBounds: params.feeBounds,
explorerProviders: params.explorerProviders,
dataProviders: params.dataProviders,
});
this.addressFormat = params.addressFormat || exports.AddressFormat.P2WPKH;
if (this.addressFormat === exports.AddressFormat.P2TR) {
if (!((_a = this.rootDerivationPaths) === null || _a === void 0 ? void 0 : _a.mainnet.startsWith(`m/86'`)) ||
!((_b = this.rootDerivationPaths) === null || _b === void 0 ? void 0 : _b.testnet.startsWith(`m/86'`)) ||
!((_c = this.rootDerivationPaths) === null || _c === void 0 ? void 0 : _c.stagenet.startsWith(`m/86'`))) {
throw Error(`Unsupported derivation paths for Taproot client. Use 86' paths`);
}
}
Bitcoin__namespace.initEccLib(ecc__namespace);
}
/**
* Get BTC asset info.
* @returns {AssetInfo} BTC asset information.
*/
getAssetInfo() {
const assetInfo = {
asset: AssetBTC,
decimal: BTC_DECIMAL,
};
return assetInfo;
}
/**
* Validate the given Bitcoin address.
* @param {string} address Bitcoin address to validate.
* @returns {boolean} `true` if the address is valid, `false` otherwise.
*/
validateAddress(address) {
return validateAddress(address, this.network);
}
/**
* Compile memo into a buffer.
* @param {string} memo Memo to compile.
* @returns {Buffer} Compiled memo.
*/
compileMemo(memo) {
const data = Buffer.from(memo, 'utf8'); // converts MEMO to buffer
return Bitcoin__namespace.script.compile([Bitcoin__namespace.opcodes.OP_RETURN, data]); // Compile OP_RETURN script
}
/**
* Get transaction fee from UTXOs.
* @param {UTXO[]} inputs UTXOs to calculate fee from.
* @param {FeeRate} feeRate Fee rate.
* @param {Buffer | null} data Compiled memo (Optional).
* @returns {number} Transaction fee.
*/
getFeeFromUtxos(inputs, feeRate, data = null) {
// Calculate input size based on inputs
const inputSizeBasedOnInputs = inputs.length > 0
? inputs.reduce((a, x) => a + inputBytes(x), 0) + inputs.length // +1 byte for each input signature
: 0;
// Calculate sum
let sum = TX_EMPTY_SIZE +
inputSizeBasedOnInputs +
TX_OUTPUT_BASE +
TX_OUTPUT_PUBKEYHASH +
TX_OUTPUT_BASE +
TX_OUTPUT_PUBKEYHASH;
if (data) {
sum += TX_OUTPUT_BASE + data.length;
}
// Calculate fee
const fee = sum * feeRate;
return fee > MIN_TX_FEE ? fee : MIN_TX_FEE;
}
/**
* Enhanced Bitcoin transaction builder with comprehensive validation and optimal UTXO selection
* @param params Transaction parameters
* @returns Enhanced transaction build result with PSBT, UTXOs, and inputs
*/
buildTxEnhanced(_a) {
return __awaiter(this, arguments, void 0, function* ({ amount, recipient, memo, feeRate, sender, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
try {
// Comprehensive input validation
this.validateTransactionInputs({
amount,
recipient,
memo,
sender,
feeRate,
});
// Use provided UTXOs (coin control) or fetch from chain
let utxos;
if (selectedUtxos && selectedUtxos.length > 0) {
xchainUtxo.UtxoTransactionValidator.validateUtxoSet(selectedUtxos);
utxos = selectedUtxos;
}
else {
const confirmedOnly = !spendPendingUTXO;
utxos = yield this.getValidatedUtxos(sender, confirmedOnly);
}
const compiledMemo = memo ? this.compileMemo(memo) : null;
const targetValue = amount.amount().toNumber();
const extraOutputs = 1 + (compiledMemo ? 1 : 0); // recipient + optional memo (change calculated separately)
// Enhanced UTXO selection
const selectionResult = this.selectUtxosForTransaction(utxos, targetValue, Math.ceil(feeRate), extraOutputs, utxoSelectionPreferences);
const psbt = new Bitcoin__namespace.Psbt({ network: btcNetwork(this.network) });
// Add inputs based on selection
if (this.addressFormat === exports.AddressFormat.P2WPKH) {
selectionResult.inputs.forEach((utxo) => {
if (!utxo.witnessUtxo) {
throw xchainUtxo.UtxoError.fromUnknown(new Error(`Missing witnessUtxo for UTXO ${utxo.hash}:${utxo.index}`), 'buildTxPsbt');
}
psbt.addInput({
hash: utxo.hash,
index: utxo.index,
witnessUtxo: utxo.witnessUtxo,
});
});
}
else {
const { pubkey, output } = Bitcoin__namespace.payments.p2tr({
address: sender,
});
selectionResult.inputs.forEach((utxo) => psbt.addInput({
hash: utxo.hash,
index: utxo.index,
witnessUtxo: { value: utxo.value, script: output },
tapInternalKey: pubkey,
}));
}
// Add recipient output
psbt.addOutput({
address: recipient,
value: targetValue,
});
// Add change output if needed
if (selectionResult.changeAmount > 0) {
psbt.addOutput({
address: sender,
value: selectionResult.changeAmount,
});
}
// Add memo output if present
if (compiledMemo) {
psbt.addOutput({ script: compiledMemo, value: 0 });
}
return { psbt, utxos, inputs: selectionResult.inputs };
}
catch (error) {
if (xchainUtxo.UtxoError.isUtxoError(error)) {
throw error;
}
throw xchainUtxo.UtxoError.fromUnknown(error, 'buildTxEnhanced');
}
});
}
/**
* Build a Bitcoin transaction with enhanced validation and performance.
* Now uses the enhanced logic internally while maintaining the same API.
* @param param0
*/
buildTx(_a) {
return __awaiter(this, arguments, void 0, function* ({ amount, recipient, memo, feeRate, sender, spendPendingUTXO = true, }) {
// Use the enhanced logic internally while maintaining the same API
return this.buildTxEnhanced({
amount,
recipient,
memo,
feeRate,
sender,
spendPendingUTXO,
});
});
}
/**
* Send maximum possible amount (sweep) with optimal fee calculation
* @param params Send max parameters
* @returns Transaction details with maximum sendable amount
*/
sendMax(_a) {
return __awaiter(this, arguments, void 0, function* ({ sender, recipient, memo, feeRate, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
try {
// Basic validation (skip amount validation since we're calculating max)
if (!(recipient === null || recipient === void 0 ? void 0 : recipient.trim())) {
throw xchainUtxo.UtxoError.invalidAddress(recipient, this.network);
}
if (!this.validateAddress(recipient)) {
throw xchainUtxo.UtxoError.invalidAddress(recipient, this.network);
}
if (!this.validateAddress(sender)) {
throw xchainUtxo.UtxoError.invalidAddress(sender, this.network);
}
// Memo validation is handled by validateTransactionInputs
// Use provided UTXOs (coin control) or fetch from chain
let utxos;
if (selectedUtxos && selectedUtxos.length > 0) {
xchainUtxo.UtxoTransactionValidator.validateUtxoSet(selectedUtxos);
utxos = selectedUtxos;
}
else {
const confirmedOnly = !spendPendingUTXO;
utxos = yield this.getValidatedUtxos(sender, confirmedOnly);
}
// Calculate maximum sendable amount
const maxCalc = this.calculateMaxSendableAmount(utxos, Math.ceil(feeRate), !!memo, utxoSelectionPreferences);
const compiledMemo = memo ? this.compileMemo(memo) : null;
const psbt = new Bitcoin__namespace.Psbt({ network: btcNetwork(this.network) });
// Add inputs
if (this.addressFormat === exports.AddressFormat.P2WPKH) {
maxCalc.inputs.forEach((utxo) => {
if (!utxo.witnessUtxo) {
throw xchainUtxo.UtxoError.fromUnknown(new Error(`Missing witnessUtxo for UTXO ${utxo.hash}:${utxo.index}`), 'sendMax');
}
psbt.addInput({
hash: utxo.hash,
index: utxo.index,
witnessUtxo: utxo.witnessUtxo,
});
});
}
else {
const { pubkey, output } = Bitcoin__namespace.payments.p2tr({
address: sender,
});
maxCalc.inputs.forEach((utxo) => psbt.addInput({
hash: utxo.hash,
index: utxo.index,
witnessUtxo: { value: utxo.value, script: output },
tapInternalKey: pubkey,
}));
}
// Add recipient output (max amount - no change)
psbt.addOutput({
address: recipient,
value: maxCalc.amount,
});
// Add memo output if present
if (compiledMemo) {
psbt.addOutput({ script: compiledMemo, value: 0 });
}
return {
psbt,
utxos,
inputs: maxCalc.inputs,
maxAmount: maxCalc.amount,
fee: maxCalc.fee,
};
}
catch (error) {
if (xchainUtxo.UtxoError.isUtxoError(error)) {
throw error;
}
throw xchainUtxo.UtxoError.fromUnknown(error, 'sendMax');
}
});
}
/**
* Prepare maximum amount transfer (sweep transaction)
* @param params Send max parameters
* @returns Prepared transaction with maximum sendable amount
*/
prepareMaxTx(_a) {
return __awaiter(this, arguments, void 0, function* ({ sender, recipient, memo, feeRate, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
try {
const { psbt, utxos, inputs, maxAmount, fee } = yield this.sendMax({
sender,
recipient,
memo,
feeRate,
spendPendingUTXO,
utxoSelectionPreferences,
selectedUtxos,
});
return {
rawUnsignedTx: psbt.toBase64(),
utxos,
inputs,
maxAmount,
fee,
};
}
catch (error) {
if (xchainUtxo.UtxoError.isUtxoError(error)) {
console.error('Bitcoin max transaction preparation failed:', error.toJSON());
throw error;
}
throw xchainUtxo.UtxoError.fromUnknown(error, 'prepareMaxTx');
}
});
}
/**
* Enhanced prepare transfer with comprehensive validation and optimal UTXO selection.
*
* @param params The transfer options with enhanced UTXO selection preferences.
* @returns The raw unsigned transaction with enhanced error handling.
*/
prepareTxEnhanced(_a) {
return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, spendPendingUTXO = true, feeRate, utxoSelectionPreferences, selectedUtxos, }) {
try {
const { psbt, utxos, inputs } = yield this.buildTxEnhanced({
sender,
recipient,
amount,
feeRate,
memo,
spendPendingUTXO,
utxoSelectionPreferences,
selectedUtxos,
});
return { rawUnsignedTx: psbt.toBase64(), utxos, inputs };
}
catch (error) {
if (xchainUtxo.UtxoError.isUtxoError(error)) {
console.error('Enhanced Bitcoin transaction preparation failed:', error.toJSON());
throw error;
}
throw xchainUtxo.UtxoError.fromUnknown(error, 'prepareTxEnhanced');
}
});
}
/**
* Prepare transfer with enhanced validation and performance.
* Now uses the enhanced logic internally while maintaining the same API.
*
* @deprecated Use `prepareTxEnhanced` directly for explicit enhanced UTXO selection.
* @param {TxParams&Address&FeeRate&boolean} params The transfer options.
* @returns {PreparedTx} The raw unsigned transaction.
*/
prepareTx(_a) {
return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, spendPendingUTXO = true, feeRate, }) {
// Use the enhanced logic internally while maintaining the same API
return this.prepareTxEnhanced({
sender,
recipient,
amount,
feeRate,
memo,
spendPendingUTXO,
});
});
}
}
const ECPair = ecpair.ECPairFactory(ecc__namespace);
/**
* Custom Bitcoin client extended to support keystore functionality
*/
class ClientKeystore extends Client {
constructor(params = Object.assign(Object.assign({}, defaultBTCParams), { addressFormat: exports.AddressFormat.P2WPKH })) {
super(params);
}
/**
* @deprecated This function eventually will be removed. Use getAddressAsync instead.
* Get the address associated with the given index.
* @param {number} index The index of the address.
* @returns {Address} The Bitcoin address.
* @throws {"index must be greater than zero"} Thrown if the index is less than zero.
* @throws {"Phrase must be provided"} Thrown if the phrase has not been set before.
* @throws {"Address not defined"} Thrown if failed to create the address from the phrase.
*/
getAddress(index = 0) {
// Check if the index is valid
if (index < 0) {
throw new Error('index must be greater than zero');
}
// Check if the phrase has been set
if (this.phrase) {
const btcNetwork$1 = btcNetwork(this.network);
const btcKeys = this.getBtcKeys(this.phrase, index);
// Generate the address using the Bitcoinjs library
let address;
if (this.addressFormat === exports.AddressFormat.P2WPKH) {
address = Bitcoin__namespace.payments.p2wpkh({
pubkey: btcKeys.publicKey,
network: btcNetwork$1,
}).address;
}
else {
address = Bitcoin__namespace.payments.p2tr({
internalPubkey: toXOnly(btcKeys.publicKey),
network: btcNetwork$1,
}).address;
}
// Throw an error if the address is not defined
if (!address) {
throw new Error('Address not defined');
}
return address;
}
throw new Error('Phrase must be provided');
}
/**
* Get the current address asynchronously.
* @param {number} index The index of the address.
* @returns {Promise<Address>} A promise that resolves to the Bitcoin address.
* @throws {"Phrase must be provided"} Thrown if the phrase has not been set before.
*/
getAddressAsync() {
return __awaiter(this, arguments, void 0, function* (index = 0) {
return this.getAddress(index);
});
}
/**
* @private
* Get the Bitcoin keys derived from the given phrase.
*
* @param {string} phrase The phrase to be used for generating the keys.
* @param {number} index The index of the address.
* @returns {Bitcoin.ECPair.ECPairInterface} The Bitcoin key pair.
* @throws {"Could not get private key from phrase"} Thrown if failed to create BTC keys from the given phrase.
*/
getBtcKeys(phrase, index = 0) {
const btcNetwork$1 = btcNetwork(this.network);
const seed = xchainCrypto.getSeed(phrase);
const master = bip32.HDKey.fromMasterSeed(Uint8Array.from(seed)).derive(this.getFullDerivationPath(index));
if (!master.privateKey) {
throw new Error('Could not get private key from phrase');
}
return ECPair.fromPrivateKey(Buffer.from(master.privateKey), { network: btcNetwork$1 });
}
/**
* Transfer BTC.
*
* @param {TxParams&FeeRate} params The transfer options including the fee rate.
* @returns {Promise<TxHash>} A promise that resolves to the transaction hash.
* @throws {"memo too long"} Thrown if the memo is longer than 80 characters.
*/
transfer(params) {
return __awaiter(this, void 0, void 0, function* () {
// Set the default fee rate to `fast`
const feeRate = params.feeRate || (yield this.getFeeRates())[xchainClient.FeeOption.Fast];
// Check if the fee rate is within the fee bounds
xchainClient.checkFeeBounds(this.feeBounds, feeRate);
// Get the address index from the parameters or use the default value
const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
// Get the Bitcoin keys
const btcKeys = this.getBtcKeys(this.phrase, fromAddressIndex);
// Merge default preferences with caller-provided preferences
const mergedUtxoSelectionPreferences = Object.assign({ minimizeFee: true, avoidDust: true, minimizeInputs: false }, params.utxoSelectionPreferences);
// Prepare the transaction
const { rawUnsignedTx } = yield this.prepareTxEnhanced(Object.assign(Object.assign({}, params), { sender: this.getAddress(fromAddressIndex), feeRate, utxoSelectionPreferences: mergedUtxoSelectionPreferences, selectedUtxos: params.selectedUtxos }));
// Build the PSBT
const psbt = Bitcoin__namespace.Psbt.fromBase64(rawUnsignedTx);
// Sign all inputs
psbt.signAllInputs(this.addressFormat === exports.AddressFormat.P2WPKH
? btcKeys
: btcKeys.tweak(Bitcoin__namespace.crypto.taggedHash('TapTweak', toXOnly(btcKeys.publicKey))));
// Finalize inputs
psbt.finalizeAllInputs();
// Extract the transaction hex
const txHex = psbt.extractTransaction().toHex();
// Broadcast the transaction and return the transaction hash
return yield this.roundRobinBroadcastTx(txHex);
});
}
/**
* Transfer the maximum amount of Bitcoin (sweep).
*
* Calculates the maximum sendable amount after fees, signs, and broadcasts the transaction.
* @param {Object} params The transfer parameters.
* @param {string} params.recipient The recipient address.
* @param {string} [params.memo] Optional memo for the transaction.
* @param {FeeRate} [params.feeRate] Optional fee rate. Defaults to 'fast' rate.
* @param {number} [params.walletIndex] Optional wallet index. Defaults to 0.
* @param {UtxoSelectionPreferences} [params.utxoSelectionPreferences] Optional UTXO selection preferences.
* @returns {Promise<{ hash: TxHash; maxAmount: number; fee: number }>} The transaction hash, amount sent, and fee.
*/
transferMax(params) {
return __awaiter(this, void 0, void 0, function* () {
const feeRate = params.feeRate || (yield this.getFeeRates())[xchainClient.FeeOption.Fast];
xchainClient.checkFeeBounds(this.feeBounds, feeRate);
const fromAddressIndex = params.walletIndex || 0;
const sender = yield this.getAddressAsync(fromAddressIndex);
const { psbt, maxAmount, fee } = yield this.sendMax({
sender,
recipient: params.recipient,
memo: params.memo,
feeRate,
utxoSelectionPreferences: params.utxoSelectionPreferences,
selectedUtxos: params.selectedUtxos,
});
const btcKeys = this.getBtcKeys(this.phrase, fromAddressIndex);
psbt.signAllInputs(this.addressFormat === exports.AddressFormat.P2WPKH
? btcKeys
: btcKeys.tweak(Bitcoin__namespace.crypto.taggedHash('TapTweak', toXOnly(btcKeys.publicKey))));
psbt.finalizeAllInputs();
const txHex = psbt.extractTransaction().toHex();
const hash = yield this.roundRobinBroadcastTx(txHex);
return { hash, maxAmount, fee };
});
}
}
/**
* Custom Ledger Bitcoin client
*/
class ClientLedger extends Client {
// Constructor
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(params) {
super(params);
this.transport = params.transport;
}
// Get the Ledger BTC application instance
getApp() {
return __awaiter(this, void 0, void 0, function* () {
if (this.app) {
return this.app;
}
this.app = new AppBtc__default.default({ transport: this.transport });
return this.app;
});
}
// Get the current address synchronously
getAddress() {
throw Error('Sync method not supported for Ledger');
}
// Get the current address asynchronously
getAddressAsync() {
return __awaiter(this, arguments, void 0, function* (index = 0, verify = false) {
const app = yield this.getApp();
const result = yield app.getWalletPublicKey(this.getFullDerivationPath(index), {
format: this.addressFormat === exports.AddressFormat.P2TR ? 'bech32m' : 'bech32',
verify,
});
return result.bitcoinAddress;
});
}
// Transfer BTC from Ledger
transfer(params) {
return __awaiter(this, void 0, void 0, function* () {
const app = yield this.getApp();
const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
// Get fee rate
const feeRate = params.feeRate || (yield this.getFeeRates())[xchainClient.FeeOption.Fast];
// Check if the fee rate is within the fee bounds
xchainClient.checkFeeBounds(this.feeBounds, feeRate);
// Get sender address
const sender = yield this.getAddressAsync(fromAddressIndex);
// Create defaults and merge with caller-provided preferences
const defaults = {
minimizeFee: true,
avoidDust: true,
minimizeInputs: false,
};
const mergedUtxoSelectionPreferences = Object.assign(Object.assign({}, defaults), params.utxoSelectionPreferences);
// Prepare transaction using enhanced method with optimal UTXO selection
const { rawUnsignedTx, inputs } = yield this.prepareTxEnhanced(Object.assign(Object.assign({}, params), { sender,
feeRate, utxoSelectionPreferences: mergedUtxoSelectionPreferences }));
const psbt = Bitcoin__namespace.Psbt.fromBase64(rawUnsignedTx);
// Prepare Ledger inputs
const ledgerInputs = inputs.map(({ txHex, hash, index }) => {
if (!txHex) {
throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`);
}
const utxoTx = Bitcoin__namespace.Transaction.fromHex(txHex);
const splittedTx = app.splitTransaction(txHex, utxoTx.hasWitnesses());
return [splittedTx, index, null, null];
});
// Prepare associated keysets
const associatedKeysets = ledgerInputs.map(() => this.getFullDerivationPath(fromAddressIndex));
// Serialize unsigned transaction
const unsignedHex = psbt.data.globalMap.unsignedTx.toBuffer().toString('hex');
const newTx = app.splitTransaction(unsignedHex, true);
const outputScriptHex = app.serializeTransactionOutputs(newTx).toString('hex');
// Create payment transaction
const txHex = yield app.createPaymentTransaction({
inputs: ledgerInputs,
associatedKeysets,
outputScriptHex,
segwit: true,
useTrustedInputForSegwit: true,
additionals: [this.addressFormat === exports.AddressFormat.P2TR ? 'bech32m' : 'bech32'],
});
// Broadcast transaction
const txHash = yield this.broadcastTx(txHex);
// Throw error if no transaction hash is received
if (!txHash) {
throw Error('No Tx hash');
}
return txHash;
});
}
// Transfer max BTC from Ledger (sweep transaction)
transferMax(params) {
return __awaiter(this, void 0, void 0, function* () {
const app = yield this.getApp();
const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
const feeRate = params.feeRate || (yield this.getFeeRates())[xchainClient.FeeOption.Fast];
xchainClient.checkFeeBounds(this.feeBounds, feeRate);
const sender = yield this.getAddressAsync(fromAddressIndex);
const { rawUnsignedTx, inputs, maxAmount, fee } = yield this.prepareMaxTx({
sender,
recipient: params.recipient,
memo: params.memo,
feeRate,
utxoSelectionPreferences: params.utxoSelectionPreferences,
selectedUtxos: params.selectedUtxos,
});
const psbt = Bitcoin__namespace.Psbt.fromBase64(rawUnsignedTx);
const ledgerInputs = inputs.map(({ txHex, hash, index }) => {
if (!txHex) {
throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`);
}
const utxoTx = Bitcoin__namespace.Transaction.fromHex(txHex);
const splittedTx = app.splitTransaction(txHex, utxoTx.hasWitnesses());
return [splittedTx, index, null, null];
});
const associatedKeysets = ledgerInputs.map(() => this.getFullDerivationPath(fromAddressIndex));
const unsignedHex = psbt.data.globalMap.unsignedTx.toBuffer().toString('hex');
const newTx = app.splitTransaction(unsignedHex, true);
const outputScriptHex = app.serializeTransactionOutputs(newTx).toString('hex');
const txHex = yield app.createPaymentTransaction({
inputs: ledgerInputs,
associatedKeysets,
outputScriptHex,
segwit: true,
useTrustedInputForSegwit: true,
additionals: [this.addressFormat === exports.AddressFormat.P2TR ? 'bech32m' : 'bech32'],
});
const hash = yield this.broadcastTx(txHex);
if (!hash) {
throw Error('No Tx hash');
}
return { hash, maxAmount, fee };
});
}
}
exports.AssetBTC = AssetBTC;
exports.BTCChain = BTCChain;
exports.BTC_DECIMAL = BTC_DECIMAL;
exports.BTC_SATOSHI_SYMBOL = BTC_SATOSHI_SYMBOL;
exports.BTC_SYMBOL = BTC_SYMBOL;
exports.BitgoProviders = BitgoProviders;
exports.BlockcypherDataProviders = BlockcypherDataProviders;
exports.Client = ClientKeystore;
exports.ClientLedger = ClientLedger;
exports.HaskoinDataProviders = HaskoinDataProviders;
exports.LOWER_FEE_BOUND = LOWER_FEE_BOUND;
exports.MIN_TX_FEE = MIN_TX_FEE;
exports.SochainDataProviders = SochainDataProviders;
exports.UPPER_FEE_BOUND = UPPER_FEE_BOUND;
exports.blockstreamExplorerProviders = blockstreamExplorerProviders;
exports.defaultBTCParams = defaultBTCParams;
exports.getPrefix = getPrefix;
exports.tapRootDerivationPaths = tapRootDerivationPaths;
exports.validateAddress = validateAddress;