UNPKG

@xchainjs/xchain-bitcoincash

Version:

Custom bitcoincash client and utilities used by XChainJS clients

890 lines (876 loc) 34 kB
import bitcore from 'bitcore-lib-cash'; import { ExplorerProvider, Network as Network$1, TxType, FeeOption, checkFeeBounds } from '@xchainjs/xchain-client'; import { getSeed } from '@xchainjs/xchain-crypto'; import { Client as Client$1 } from '@xchainjs/xchain-utxo'; import accumulative from 'coinselect/accumulative.js'; import { AssetType, baseAmount } from '@xchainjs/xchain-util'; import { HaskoinProvider, HaskoinNetwork, BitgoProvider } from '@xchainjs/xchain-utxo-providers'; import bs58check from 'bs58check'; import * as cashaddr from 'cashaddrjs'; import { HDKey } from '@scure/bip32'; import AppBtc from '@ledgerhq/hw-app-btc'; /****************************************************************************** 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; }; /** * Lower bound for transaction fee rate. */ const LOWER_FEE_BOUND = 1; /** * Upper bound for transaction fee rate. */ const UPPER_FEE_BOUND = 500; /** * Decimal places for Bitcoin Cash. */ const BCH_DECIMAL = 8; /** * Chain identifier for Bitcoin Cash. */ const BCHChain = 'BCH'; /** * Base "chain" asset on Bitcoin Cash mainnet. * Defined according to Thorchain's asset structure. * @see https://gitlab.com/thorchain/thornode/-/blob/master/common/asset.go#L12-24 */ const AssetBCH = { chain: BCHChain, symbol: 'BCH', ticker: 'BCH', type: AssetType.NATIVE }; /** * Explorer provider URLs for Bitcoin Cash. */ const BCH_MAINNET_EXPLORER = new ExplorerProvider('https://www.blockchain.com/bch/', 'https://www.blockchain.com/bch/address/%%ADDRESS%%', 'https://www.blockchain.com/bch/tx/%%TX_ID%%'); const BCH_TESTNET_EXPLORER = new ExplorerProvider('https://www.blockchain.com/bch-testnet/', 'https://www.blockchain.com/bch-testnet/address/%%ADDRESS%%', 'https://www.blockchain.com/bch-testnet/tx/%%TX_ID%%'); const explorerProviders = { [Network$1.Testnet]: BCH_TESTNET_EXPLORER, [Network$1.Stagenet]: BCH_MAINNET_EXPLORER, [Network$1.Mainnet]: BCH_MAINNET_EXPLORER, }; /** * Haskoin data providers for Bitcoin Cash. */ const testnetHaskoinProvider = new HaskoinProvider('https://api.haskoin.com', BCHChain, AssetBCH, 8, HaskoinNetwork.BCHTEST); const mainnetHaskoinProvider = new HaskoinProvider('https://haskoin.ninerealms.com', BCHChain, AssetBCH, 8, HaskoinNetwork.BCH); const HaskoinDataProviders = { [Network$1.Testnet]: testnetHaskoinProvider, [Network$1.Stagenet]: mainnetHaskoinProvider, [Network$1.Mainnet]: mainnetHaskoinProvider, }; /** * Bitgo data providers for Bitcoin Cash. */ const mainnetBitgoProvider = new BitgoProvider({ baseUrl: 'https://app.bitgo.com', chain: BCHChain, }); const BitgoProviders = { [Network$1.Testnet]: undefined, [Network$1.Stagenet]: mainnetBitgoProvider, [Network$1.Mainnet]: mainnetBitgoProvider, }; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-use-before-define */ // @ts-nocheck /*** * @license * https://github.com/ealmansi/bchaddrjs * Copyright (c) 2018-2020 Emilio Almansi * Distributed under the MIT software license, see the accompanying * file LICENSE or http://www.opensource.org/licenses/mit-license.php. */ /** * General purpose Bitcoin Cash address detection and translation.<br /> * Supports all major Bitcoin Cash address formats.<br /> * Currently: * <ul> * <li> Legacy format </li> * <li> Bitpay format </li> * <li> Cashaddr format </li> * </ul> * @module bchaddr */ /** * @static * Supported Bitcoin Cash address formats. */ const Format = {}; Format.Legacy = 'legacy'; Format.Bitpay = 'bitpay'; Format.Cashaddr = 'cashaddr'; /** * @static * Supported networks. */ const Network = {}; Network.Mainnet = 'mainnet'; Network.Testnet = 'testnet'; /** * @static * Supported address types. */ const Type = {}; Type.P2PKH = 'p2pkh'; Type.P2SH = 'p2sh'; /** * Returns a boolean indicating whether the given input is a valid Bitcoin Cash address. * @static * @param {*} input - Any input to check for validity. * @returns {boolean} */ function isValidAddress(input) { try { decodeAddress(input); return true; } catch (_error) { return false; } } /** * Detects what is the given address' format. * @static * @param {string} address - A valid Bitcoin Cash address in any format. * @return {string} * @throws {InvalidAddressError} */ function detectAddressFormat(address) { return decodeAddress(address).format; } /** * Detects what is the given address' network. * @static * @param {string} address - A valid Bitcoin Cash address in any format. * @return {string} * @throws {InvalidAddressError} */ function detectAddressNetwork(address) { return decodeAddress(address).network; } /** * Translates the given address into legacy format. * @static * @param {string} address - A valid Bitcoin Cash address in any format. * @return {string} * @throws {InvalidAddressError} */ function toLegacyAddress$1(address) { const decoded = decodeAddress(address); if (decoded.format === Format.Legacy) { return address; } return encodeAsLegacy(decoded); } /** * Translates the given address into cashaddr format. * @static * @param {string} address - A valid Bitcoin Cash address in any format. * @return {string} * @throws {InvalidAddressError} */ function toCashAddress$1(address) { const decoded = decodeAddress(address); return encodeAsCashaddr(decoded); } /** * Version byte table for base58 formats. * @private */ const VERSION_BYTE = {}; VERSION_BYTE[Format.Legacy] = {}; VERSION_BYTE[Format.Legacy][Network.Mainnet] = {}; VERSION_BYTE[Format.Legacy][Network.Mainnet][Type.P2PKH] = 0; VERSION_BYTE[Format.Legacy][Network.Mainnet][Type.P2SH] = 5; VERSION_BYTE[Format.Legacy][Network.Testnet] = {}; VERSION_BYTE[Format.Legacy][Network.Testnet][Type.P2PKH] = 111; VERSION_BYTE[Format.Legacy][Network.Testnet][Type.P2SH] = 196; VERSION_BYTE[Format.Bitpay] = {}; VERSION_BYTE[Format.Bitpay][Network.Mainnet] = {}; VERSION_BYTE[Format.Bitpay][Network.Mainnet][Type.P2PKH] = 28; VERSION_BYTE[Format.Bitpay][Network.Mainnet][Type.P2SH] = 40; VERSION_BYTE[Format.Bitpay][Network.Testnet] = {}; VERSION_BYTE[Format.Bitpay][Network.Testnet][Type.P2PKH] = 111; VERSION_BYTE[Format.Bitpay][Network.Testnet][Type.P2SH] = 196; /** * Decodes the given address into its constituting hash, format, network and type. * @private * @param {string} address - A valid Bitcoin Cash address in any format. * @return {object} * @throws {InvalidAddressError} */ function decodeAddress(address) { try { return decodeBase58Address(address); } catch (_error) { } try { return decodeCashAddress(address); } catch (_error) { } throw new InvalidAddressError(); } /** * Length of a valid base58check encoding payload: 1 byte for * the version byte plus 20 bytes for a RIPEMD-160 hash. * @private */ const BASE_58_CHECK_PAYLOAD_LENGTH = 21; /** * Attempts to decode the given address assuming it is a base58 address. * @private * @param {string} address - A valid Bitcoin Cash address in any format. * @return {object} * @throws {InvalidAddressError} */ function decodeBase58Address(address) { try { const payload = bs58check.decode(address); if (payload.length !== BASE_58_CHECK_PAYLOAD_LENGTH) { throw new InvalidAddressError(); } const versionByte = payload[0]; const hash = Array.prototype.slice.call(payload, 1); switch (versionByte) { case VERSION_BYTE[Format.Legacy][Network.Mainnet][Type.P2PKH]: return { hash: hash, format: Format.Legacy, network: Network.Mainnet, type: Type.P2PKH, }; case VERSION_BYTE[Format.Legacy][Network.Mainnet][Type.P2SH]: return { hash: hash, format: Format.Legacy, network: Network.Mainnet, type: Type.P2SH, }; case VERSION_BYTE[Format.Legacy][Network.Testnet][Type.P2PKH]: return { hash: hash, format: Format.Legacy, network: Network.Testnet, type: Type.P2PKH, }; case VERSION_BYTE[Format.Legacy][Network.Testnet][Type.P2SH]: return { hash: hash, format: Format.Legacy, network: Network.Testnet, type: Type.P2SH, }; case VERSION_BYTE[Format.Bitpay][Network.Mainnet][Type.P2PKH]: return { hash: hash, format: Format.Bitpay, network: Network.Mainnet, type: Type.P2PKH, }; case VERSION_BYTE[Format.Bitpay][Network.Mainnet][Type.P2SH]: return { hash: hash, format: Format.Bitpay, network: Network.Mainnet, type: Type.P2SH, }; } } catch (_error) { } throw new InvalidAddressError(); } /** * Attempts to decode the given address assuming it is a cashaddr address. * @private * @param {string} address - A valid Bitcoin Cash address in any format. * @return {object} * @throws {InvalidAddressError} */ function decodeCashAddress(address) { if (address.indexOf(':') !== -1) { try { return decodeCashAddressWithPrefix(address); } catch (_error) { } } else { const prefixes = ['bitcoincash', 'bchtest', 'bchreg']; for (let i = 0; i < prefixes.length; ++i) { try { const prefix = prefixes[i]; return decodeCashAddressWithPrefix(prefix + ':' + address); } catch (_error) { } } } throw new InvalidAddressError(); } /** * Attempts to decode the given address assuming it is a cashaddr address with explicit prefix. * @private * @param {string} address - A valid Bitcoin Cash address in any format. * @return {object} * @throws {InvalidAddressError} */ function decodeCashAddressWithPrefix(address) { try { const decoded = cashaddr.decode(address); const hash = Array.prototype.slice.call(decoded.hash, 0); const type = decoded.type === 'P2PKH' ? Type.P2PKH : Type.P2SH; switch (decoded.prefix) { case 'bitcoincash': return { hash: hash, format: Format.Cashaddr, network: Network.Mainnet, type: type, }; case 'bchtest': case 'bchreg': return { hash: hash, format: Format.Cashaddr, network: Network.Testnet, type: type, }; } } catch (_error) { } throw new InvalidAddressError(); } /** * Encodes the given decoded address into legacy format. * @private * @param {object} decoded * @returns {string} */ function encodeAsLegacy(decoded) { const versionByte = VERSION_BYTE[Format.Legacy][decoded.network][decoded.type]; const buffer = Buffer.alloc(1 + decoded.hash.length); buffer[0] = versionByte; buffer.set(decoded.hash, 1); return bs58check.encode(buffer); } /** * Encodes the given decoded address into cashaddr format. * @private * @param {object} decoded * @returns {string} */ function encodeAsCashaddr(decoded) { const prefix = decoded.network === Network.Mainnet ? 'bitcoincash' : 'bchtest'; const type = decoded.type === Type.P2PKH ? 'P2PKH' : 'P2SH'; const hash = new Uint8Array(decoded.hash); return cashaddr.encode(prefix, type, hash); } /** * Returns a boolean indicating whether the address is in cashaddr format. * @static * @param {string} address - A valid Bitcoin Cash address in any format. * @returns {boolean} * @throws {InvalidAddressError} */ function isCashAddress$1(address) { return detectAddressFormat(address) === Format.Cashaddr; } /** * Error thrown when the address given as input is not a valid Bitcoin Cash address. * @constructor * InvalidAddressError */ function InvalidAddressError() { const error = new Error(); this.name = error.name = 'InvalidAddressError'; this.message = error.message = 'Received an invalid Bitcoin Cash address as input.'; this.stack = error.stack; } InvalidAddressError.prototype = Object.create(Error.prototype); /** * Module importing and providing utilities for Bitcoin Cash (BCH) transactions and addresses. */ /** * Size constants for BCH transactions. */ const TX_EMPTY_SIZE = 4 + 1 + 1 + 4; const TX_INPUT_BASE = 32 + 4 + 1 + 4; const TX_INPUT_PUBKEYHASH = 107; const TX_OUTPUT_BASE = 8 + 1; const TX_OUTPUT_PUBKEYHASH = 25; /** * Retrieves the BCH network to be used with bitcore-lib. * @param {Network} network The network type. * @returns {BCHNetwork} The BCH network. */ const bchNetwork = (network) => { switch (network) { case Network$1.Mainnet: case Network$1.Stagenet: return bitcore.Networks.mainnet; case Network$1.Testnet: return bitcore.Networks.testnet; } }; /** * Retrieves the BCH address prefix. * * @returns {string} The BCH address prefix. */ const getPrefix = () => ''; /** * Strips the BCH address prefix. * @param {Address} address The BCH address. * @returns {Address} The address with the prefix removed. */ const stripPrefix = (address) => address.replace(/(bchtest:|bitcoincash:)/, ''); /** * Converts the BCH address to a legacy address format. * @param {Address} address The BCH address. * @returns {Address} The legacy address. */ const toLegacyAddress = (address) => { return toLegacyAddress$1(address); }; /** * Converts the BCH address to a cash address format. * @param {Address} address The BCH address. * @returns {Address} The cash address. */ const toCashAddress = (address) => { return toCashAddress$1(address); }; /** * Checks whether the address is a cash address. * @param {Address} address The BCH address. * @returns {boolean} Whether the address is a cash address. */ const isCashAddress = (address) => { return isCashAddress$1(address); }; /** * Parses a BCH transaction. * @param {Transaction} tx The BCH transaction. * @returns {Tx} The parsed transaction. */ const parseTransaction = (tx) => { return { asset: AssetBCH, from: tx.inputs .filter((input) => !!input.address) .map((input) => ({ from: stripPrefix(input.address), amount: baseAmount(input.value, BCH_DECIMAL), })), to: tx.outputs .filter((output) => !!output.address) .map((output) => ({ to: stripPrefix(output.address), amount: baseAmount(output.value, BCH_DECIMAL), })), date: new Date(tx.time * 1000), type: TxType.Transfer, hash: tx.txid, }; }; /** * Converts the XChain network to a BCH address network. * @param {Network} network The XChain network. * @returns {string} The BCH address network. */ const toBCHAddressNetwork = (network) => { switch (network) { case Network$1.Mainnet: case Network$1.Stagenet: return Network.Mainnet; case Network$1.Testnet: return Network.Testnet; } }; /** * Validates the BCH address. * @param {string} address The BCH address. * @param {Network} network The XChain network. * @returns {boolean} Whether the address is valid. */ const validateAddress = (address, network) => { const toAddress = toCashAddress(address); return isValidAddress(toAddress) && detectAddressNetwork(toAddress) === toBCHAddressNetwork(network); }; // Default parameters for Bitcoin Cash (BCH) client const defaultBchParams = { network: Network$1.Mainnet, // Default network is Mainnet phrase: '', // Default empty phrase explorerProviders: explorerProviders, // Default explorer providers dataProviders: [BitgoProviders, HaskoinDataProviders], // Default data providers rootDerivationPaths: { [Network$1.Mainnet]: `m/44'/145'/0'/0/`, // Default root derivation path for Mainnet [Network$1.Testnet]: `m/44'/1'/0'/0/`, // Default root derivation path for Testnet [Network$1.Stagenet]: `m/44'/145'/0'/0/`, // Default root derivation path for Stagenet }, feeBounds: { lower: LOWER_FEE_BOUND, // Default lower fee bound upper: UPPER_FEE_BOUND, // Default upper fee bound }, }; /** * Custom Bitcoin Cash client class. */ class Client extends Client$1 { /** * Constructor for the Client class. * * @param {UtxoClientParams} params - Parameters for initializing the client. */ constructor(params = defaultBchParams) { // Call the constructor of the parent class (UTXOClient) with BCHChain as the chain and provided parameters super(BCHChain, { network: params.network, rootDerivationPaths: params.rootDerivationPaths, phrase: params.phrase, feeBounds: params.feeBounds, explorerProviders: params.explorerProviders, dataProviders: params.dataProviders, }); } /** * Get information about the BCH asset. * @returns Information about the BCH asset. */ getAssetInfo() { const assetInfo = { asset: AssetBCH, // Asset symbol decimal: BCH_DECIMAL, // Decimal precision }; return assetInfo; } /** * Validate the given address. * * @param {Address} address * @returns {boolean} `true` or `false` */ validateAddress(address) { return validateAddress(address, this.network); } /** * Build a BCH transaction. * @param {BuildParams} params - The transaction build options. * @returns {Transaction} A promise that resolves with the transaction builder, UTXOs, and inputs. * @deprecated */ buildTx(_a) { return __awaiter(this, arguments, void 0, function* ({ amount, recipient, memo, feeRate, sender, }) { // Convert recipient address to CashAddress format const recipientCashAddress = toCashAddress(recipient); // Validate recipient address if (!this.validateAddress(recipientCashAddress)) throw new Error('Invalid address'); // Scan UTXOs for the sender address const utxos = yield this.scanUTXOs(sender, false); // Throw error if no UTXOs are found if (utxos.length === 0) throw new Error('No utxos to send'); // Convert fee rate to a whole number const feeRateWhole = Number(feeRate.toFixed(0)); // Compile memo if provided const compiledMemo = memo ? this.compileMemo(memo) : null; const targetOutputs = []; // Add output amount and recipient to target outputs targetOutputs.push({ address: recipient, value: amount.amount().toNumber(), }); // Calculate transaction inputs and outputs const { inputs, outputs } = accumulative(utxos, targetOutputs, feeRateWhole); // Throw error if no solution is found if (!inputs || !outputs) throw new Error('Insufficient Balance for transaction'); const tx = new bitcore.Transaction().from(inputs.map((utxo) => { var _a; return ({ txId: utxo.hash, outputIndex: utxo.index, address: sender, script: bitcore.Script.fromHex(((_a = utxo.witnessUtxo) === null || _a === void 0 ? void 0 : _a.script.toString('hex')) || ''), satoshis: utxo.value, }); })); // eslint-disable-next-line @typescript-eslint/no-explicit-any outputs.forEach((output) => { if (!output.address) { tx.to(sender, output.value); // change back to sender } else { tx.to(output.address, output.value); } }); if (compiledMemo) { tx.addOutput(new bitcore.Transaction.Output({ script: compiledMemo, satoshis: 0, })); } // Return transaction builder, UTXOs, and inputs return { builder: tx, utxos, inputs, }; }); } /** * Prepare a BCH transaction. * @param {TxParams&Address&FeeRate} params - The transaction preparation options. * @returns {PreparedTx} A promise that resolves with the prepared transaction and UTXOs. */ prepareTx(_a) { return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, feeRate, }) { // Build the transaction using provided options const { builder, utxos, inputs } = yield this.buildTx({ sender, recipient, amount, memo, feeRate, }); // Return the raw unsigned transaction and UTXOs // ESLint disabled: Bitcoin transaction builder has proper toString() method that returns hex string // Left as-is during ESLint 8 upgrade as this core crypto functionality is tested and working // eslint-disable-next-line @typescript-eslint/no-base-to-string return { rawUnsignedTx: builder.toString(), utxos, inputs }; }); } /** * Compile a memo. * @param {string} memo - The memo to be compiled. * @returns {Buffer} - The compiled memo. */ compileMemo(memo) { const data = Buffer.from(memo, 'utf8'); const script = bitcore.Script.buildDataOut(data); return script.toBuffer(); } /** * Calculate the transaction fee. * @param {UTXO[]} inputs - The UTXOs. * @param {FeeRate} feeRate - The fee rate. * @param {Buffer | null} data - The compiled memo (optional). * @returns {number} - The fee amount. */ getFeeFromUtxos(inputs, feeRate, data = null) { let totalWeight = TX_EMPTY_SIZE; totalWeight += (TX_INPUT_PUBKEYHASH + TX_INPUT_BASE) * inputs.length; totalWeight += (TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH) * 2; if (data) { totalWeight += 9 + data.length; } return Math.ceil(totalWeight * feeRate); } } /** * Custom Bitcoin client extended to support keystore functionality */ class ClientKeystore extends Client { /** * @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 bchKeys = this.getBCHKeys(this.phrase, this.getFullDerivationPath(index)); // Generate the address using the Bitcoinjs library const address = bchKeys.toAddress().toString(); // Throw an error if the address is not defined if (!address) { throw new Error('Address not defined'); } return stripPrefix(toCashAddress(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 BitcoinCash 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 function to get BCH keys. * Generates a key pair from the provided phrase and derivation path. * @param {string} phrase - The phrase used for generating the private key. * @param {string} derivationPath - The BIP44 derivation path. * @returns {PrivateKey} The key pair generated from the phrase and derivation path. * * @throws {"Invalid phrase"} Thrown if an invalid phrase is provided. * */ getBCHKeys(phrase, derivationPath) { const rootSeed = getSeed(phrase); // Get seed from the phrase const root = HDKey.fromMasterSeed(Uint8Array.from(rootSeed)); const child = root.derive(derivationPath); if (!child.privateKey) { throw new Error('Invalid derived private key'); } const privateKey = new bitcore.PrivateKey(Buffer.from(child.privateKey).toString('hex'), bchNetwork(this.network)); return privateKey; } /** * Transfer BCH. * @param {TxParams & { feeRate?: FeeRate }} params - The transfer options. * @returns {Promise<TxHash>} A promise that resolves with the transaction hash. */ transfer(params) { return __awaiter(this, void 0, void 0, function* () { // Set the default fee rate to 'fast' const feeRate = params.feeRate || (yield this.getFeeRates())[FeeOption.Fast]; // Check if the fee rate is within the specified bounds checkFeeBounds(this.feeBounds, feeRate); // Get the index of the address to send funds from const fromAddressIndex = params.walletIndex || 0; // Prepare the transaction by gathering necessary data const { rawUnsignedTx, inputs } = yield this.prepareTx(Object.assign(Object.assign({}, params), { feeRate, sender: yield this.getAddressAsync(fromAddressIndex) })); // Get key from mnemonic and path const derivationPath = this.getFullDerivationPath(fromAddressIndex); const privateKey = this.getBCHKeys(this.phrase, derivationPath); // Recreate the transaction from rawUnsignedTx const unsignedTx = new bitcore.Transaction(rawUnsignedTx); // Rebuild the transaction with inputs enriched with UTXO values and scripts const tx = new bitcore.Transaction(); const sender = yield this.getAddressAsync(fromAddressIndex); tx.from(inputs.map((input) => { var _a; return new bitcore.Transaction.UnspentOutput({ txId: input.hash, outputIndex: input.index, address: sender, script: bitcore.Script.fromHex(((_a = input.witnessUtxo) === null || _a === void 0 ? void 0 : _a.script.toString('hex')) || ''), satoshis: input.value, }); })); unsignedTx.outputs.forEach((out) => { tx.addOutput(new bitcore.Transaction.Output({ script: out.script, satoshis: out.satoshis, })); }); tx.sign(privateKey); // ESLint disabled: Bitcoin transaction has proper toString() method that returns hex string // Left as-is during ESLint 8 upgrade as this core crypto functionality is tested and working // eslint-disable-next-line @typescript-eslint/no-base-to-string const txHex = tx.toString(); return yield this.roundRobinBroadcastTx(txHex); }); } } /** * Custom Ledger BitcoinCash 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 BTCCash application instance getApp() { return __awaiter(this, void 0, void 0, function* () { if (this.app) { return this.app; } this.app = new AppBtc({ transport: this.transport, currency: 'bitcoin_cash' }); 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: 'cashaddr', verify, }); return result.bitcoinAddress; }); } // Transfer BTCCash 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())[FeeOption.Fast]; checkFeeBounds(this.feeBounds, feeRate); // Get sender address const sender = yield this.getAddressAsync(fromAddressIndex); // Prepare transaction const { rawUnsignedTx, inputs } = yield this.prepareTx(Object.assign(Object.assign({}, params), { sender, feeRate })); const ledgerInputs = inputs.map(({ txHex, hash, index }) => { if (!txHex) { throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`); } const splittedTx = app.splitTransaction(txHex, false); return [splittedTx, index, null, null]; }); // Prepare associated keysets const associatedKeysets = ledgerInputs.map(() => this.getFullDerivationPath(fromAddressIndex)); // Convert the raw unsigned transaction to a Transaction object const newTx = app.splitTransaction(rawUnsignedTx); const outputScriptHex = app.serializeTransactionOutputs(newTx).toString('hex'); const txHex = yield app.createPaymentTransaction({ inputs: ledgerInputs, associatedKeysets, outputScriptHex, // 'abc' for BCH // @see https://github.com/LedgerHQ/ledgerjs/tree/v6.7.0/packages/hw-app-btc#createpaymenttransactionnew // Under the hood `hw-app-btc` uses `bip143` then // @see https://github.com/LedgerHQ/ledgerjs/blob/90360f1b00a11af4e64a7fc9d980a153ee6f092a/packages/hw-app-btc/src/createTransaction.ts#L120-L123 additionals: ['abc'], sigHashType: 0x41, // If not set, Ledger will throw LEDGER DEVICE: INVALID DATA RECEIVED (0X6A80) }); const txHash = yield this.broadcastTx(txHex); // Throw error if no transaction hash is received if (!txHash) { throw Error('No Tx hash'); } return txHash; }); } } export { AssetBCH, BCHChain, BCH_DECIMAL, BitgoProviders, ClientKeystore as Client, ClientLedger, HaskoinDataProviders, LOWER_FEE_BOUND, TX_EMPTY_SIZE, TX_INPUT_BASE, TX_INPUT_PUBKEYHASH, TX_OUTPUT_BASE, TX_OUTPUT_PUBKEYHASH, UPPER_FEE_BOUND, bchNetwork, defaultBchParams, explorerProviders, getPrefix, isCashAddress, parseTransaction, stripPrefix, toBCHAddressNetwork, toCashAddress, toLegacyAddress, validateAddress };