UNPKG

@xchainjs/xchain-doge

Version:

Custom Doge client and utilities used by XChain clients

601 lines (587 loc) 26.3 kB
import { ExplorerProvider, Network, FeeOption, checkFeeBounds } from '@xchainjs/xchain-client'; import { getSeed } from '@xchainjs/xchain-crypto'; import * as Dogecoin from 'bitcoinjs-lib'; import { toBitcoinJS, Client as Client$1 } from '@xchainjs/xchain-utxo'; import accumulative from 'coinselect/accumulative'; import { AssetType } from '@xchainjs/xchain-util'; import { SochainProvider, SochainNetwork, BlockcypherProvider, BlockcypherNetwork, BitgoProvider } from '@xchainjs/xchain-utxo-providers'; 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; }; /** * Minimum transaction fee for Dogecoin transactions. * Defined as 100000 satoshi/kB. * @see https://github.com/dogecoin/dogecoin/blob/master/src/validation.h#L58 */ const MIN_TX_FEE = 100000; /** * Decimal places for Dogecoin. */ const DOGE_DECIMAL = 8; /** * Lower fee bound for Dogecoin transactions. * Referenced from Dogecoin fee recommendation documentation. * @see https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md */ const LOWER_FEE_BOUND = 100; /** * Upper fee bound for Dogecoin transactions. * Referenced from Dogecoin fee recommendation documentation. * @see https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md */ const UPPER_FEE_BOUND = 20000000; /** * Chain identifier for Dogecoin. */ const DOGEChain = 'DOGE'; /** * Base asset object for Dogecoin. * Represents the Dogecoin asset in various contexts. */ const AssetDOGE = { chain: DOGEChain, symbol: 'DOGE', ticker: 'DOGE', type: AssetType.NATIVE }; /** * Explorer provider for Dogecoin mainnet and testnet. * Provides URLs for exploring Dogecoin transactions and addresses. */ const DOGE_MAINNET_EXPLORER = new ExplorerProvider('https://blockchair.com/dogecoin', 'https://blockchair.com/dogecoin/address/%%ADDRESS%%', 'https://blockchair.com/dogecoin/transaction/%%TX_ID%%'); const DOGE_TESTNET_EXPLORER = new ExplorerProvider('https://blockexplorer.one/dogecoin/testnet', 'https://blockexplorer.one/dogecoin/testnet/address/%%ADDRESS%%', 'https://blockexplorer.one/dogecoin/testnet/tx/%%TX_ID%%'); const blockstreamExplorerProviders = { [Network.Testnet]: DOGE_TESTNET_EXPLORER, [Network.Stagenet]: DOGE_MAINNET_EXPLORER, [Network.Mainnet]: DOGE_MAINNET_EXPLORER, }; /** * Sochain data providers for Dogecoin mainnet and testnet. * Provides API access to Sochain for Dogecoin. */ const testnetSochainProvider = new SochainProvider('https://sochain.com/api/v3', process.env.SOCHAIN_API_KEY || '', DOGEChain, AssetDOGE, 8, SochainNetwork.DOGETEST); const mainnetSochainProvider = new SochainProvider('https://sochain.com/api/v3', process.env.SOCHAIN_API_KEY || '', DOGEChain, AssetDOGE, 8, SochainNetwork.DOGE); const sochainDataProviders = { [Network.Testnet]: testnetSochainProvider, [Network.Stagenet]: mainnetSochainProvider, [Network.Mainnet]: mainnetSochainProvider, }; /** * Blockcypher data providers for Dogecoin mainnet and stagenet. * Provides API access to Blockcypher for Dogecoin. */ const mainnetBlockcypherProvider = new BlockcypherProvider('https://api.blockcypher.com/v1', DOGEChain, AssetDOGE, 8, BlockcypherNetwork.DOGE, process.env.BLOCKCYPHER_API_KEY || ''); const blockcypherDataProviders = { [Network.Testnet]: undefined, [Network.Stagenet]: mainnetBlockcypherProvider, [Network.Mainnet]: mainnetBlockcypherProvider, }; /** * Bitgo data providers for Dogecoin mainnet and stagenet. * Provides API access to Bitgo for Dogecoin. */ const mainnetBitgoProvider = new BitgoProvider({ baseUrl: 'https://app.bitgo.com', chain: DOGEChain, }); const BitgoProviders = { [Network.Testnet]: undefined, [Network.Stagenet]: mainnetBitgoProvider, [Network.Mainnet]: mainnetBitgoProvider, }; /** * Import statements for required modules and types. */ /** * Constant values representing transaction sizes and lengths. */ const TX_EMPTY_SIZE = 4 + 1 + 1 + 4; // 10 const TX_INPUT_BASE = 32 + 4 + 1 + 4; // 41 const TX_INPUT_PUBKEYHASH = 107; const TX_OUTPUT_BASE = 8 + 1; // 9 const TX_OUTPUT_PUBKEYHASH = 25; /** * Calculate the number of bytes required for an input. * * @returns {number} The number of bytes required for an input. */ function inputBytes() { return TX_INPUT_BASE + TX_INPUT_PUBKEYHASH; } /** * Get the Dogecoin network configuration to be used with bitcoinjs. * * @param {Network} network - The network type. * @returns {Dogecoin.networks.Network} The Dogecoin network configuration. */ const dogeNetwork = (network) => { switch (network) { case Network.Mainnet: return toBitcoinJS('dogecoin', 'main'); case Network.Stagenet: return toBitcoinJS('dogecoin', 'main'); case Network.Testnet: { return toBitcoinJS('dogecoin', 'test'); } } }; /** * Validate a Dogecoin address. * * @param {Address} address - The Dogecoin address to validate. * @param {Network} network - The network type. * @returns {boolean} `true` if the address is valid, `false` otherwise. */ const validateAddress = (address, network) => { try { Dogecoin.address.toOutputScript(address, dogeNetwork(network)); return true; } catch (error) { return false; } }; /** * Get the address prefix based on the network. * * @param {Network} network - The network type. * @returns {string} The address prefix based on the network. */ const getPrefix = (network) => { switch (network) { case Network.Mainnet: case Network.Stagenet: return ''; case Network.Testnet: return 'n'; } }; /** * Default parameters for Dogecoin UTXO client. * Contains default values for network, phrase, explorer providers, data providers, root derivation paths, and fee bounds. */ const defaultDogeParams = { network: Network.Mainnet, phrase: '', explorerProviders: blockstreamExplorerProviders, dataProviders: [BitgoProviders, blockcypherDataProviders], rootDerivationPaths: { [Network.Mainnet]: `m/44'/3'/0'/0/`, [Network.Stagenet]: `m/44'/3'/0'/0/`, [Network.Testnet]: `m/44'/1'/0'/0/`, // Default root derivation path for Testnet }, feeBounds: { lower: LOWER_FEE_BOUND, upper: UPPER_FEE_BOUND, // Default upper fee bound }, }; /** * Custom Dogecoin client extending UTXOClient. * Implements methods for Dogecoin-specific functionality. */ class Client extends Client$1 { /** * Constructor for initializing the Dogecoin client. * Initializes the client with the provided parameters. * * @param {DogecoinClientParams} params Parameters for initializing the Dogecoin client. */ constructor(params = defaultDogeParams) { super(DOGEChain, { // Call the superclass constructor with DOGEChain identifier and provided parameters network: params.network, rootDerivationPaths: params.rootDerivationPaths, phrase: params.phrase, feeBounds: params.feeBounds, explorerProviders: params.explorerProviders, dataProviders: params.dataProviders, }); /** * Builds a Dogecoin transaction (PSBT). * * Builds a Partially Signed Bitcoin Transaction (PSBT) with the specified parameters. * @param {BuildParams} params The transaction build options including sender, recipient, amount, memo, and fee rate. * @returns {Transaction} A promise that resolves to the built PSBT and the unspent transaction outputs (UTXOs) used in the transaction. * @deprecated This method is deprecated. Use the `transfer` method instead. */ this.buildTx = ({ amount, recipient, memo, feeRate, sender, }) => __awaiter(this, void 0, void 0, function* () { // Validate the recipient address if (!this.validateAddress(recipient)) throw new Error('Invalid address'); // Scan unspent transaction outputs (UTXOs) for the sender's address const utxos = yield this.scanUTXOs(sender, false); // Throw an error if no UTXOs are found if (utxos.length === 0) throw new Error('No UTXOs to send'); // Round the fee rate to the nearest whole number const feeRateWhole = Number(feeRate.toFixed(0)); // Compile the memo if provided const compiledMemo = memo ? this.compileMemo(memo) : null; const targetOutputs = []; //1. Add output for the recipient targetOutputs.push({ address: recipient, value: amount.amount().toNumber(), }); //2. Add output for the memo (if provided) if (compiledMemo) { targetOutputs.push({ script: compiledMemo, value: 0 }); } // Calculate the inputs and outputs for the transaction const { inputs, outputs } = accumulative(utxos, targetOutputs, feeRateWhole); // Throw an error if no solution was found for inputs and outputs if (!inputs || !outputs) throw new Error('Balance insufficient for transaction'); // Create a new PSBT for building the transaction const psbt = new Dogecoin.Psbt({ network: dogeNetwork(this.network) }); // Set the maximum fee rate for the PSBT psbt.setMaximumFeeRate(7500000); // Add inputs to the PSBT for (const utxo of inputs) { psbt.addInput({ hash: utxo.hash, index: utxo.index, nonWitnessUtxo: Buffer.from(utxo.txHex, 'hex'), }); } // Outputs outputs.forEach((output) => { if (!output.address) { //an empty address means this is the change address output.address = sender; } if (!output.script) { psbt.addOutput(output); } else { //we need to add the compiled memo this way to //avoid dust error tx when accumulating memo output with 0 value if (compiledMemo) { psbt.addOutput({ script: compiledMemo, value: 0 }); } } }); return { psbt, utxos, inputs }; }); } /** * Get Dogecoin asset information. * * @returns {AssetInfo} Dogecoin asset information. */ getAssetInfo() { const assetInfo = { asset: AssetDOGE, decimal: DOGE_DECIMAL, }; return assetInfo; } /** * Validate the given address. * * @param {Address} address The Dogecoin address to validate. * @returns {boolean} `true` if the address is valid, otherwise `false`. */ validateAddress(address) { return validateAddress(address, this.network); } /** * Asynchronously creates transaction information for ledger sign. * * Builds a transaction (PSBT) and prepares necessary information for ledger signing. * * @param {LedgerTxInfoParams} params The parameters for creating transaction information. * @returns {LedgerTxInfo} A promise that resolves to the transaction information used for ledger sign. */ createTxInfo(params) { return __awaiter(this, void 0, void 0, function* () { // Build the transaction (PSBT) and obtain the unspent transaction outputs (UTXOs) const { psbt, utxos } = yield this.buildTx(params); // Construct the ledger transaction information object const ledgerTxInfo = { utxos, newTxHex: psbt.data.globalMap.unsignedTx.toBuffer().toString('hex'), // Convert unsigned transaction to hexadecimal string }; return ledgerTxInfo; }); } /** * Asynchronously prepares a transaction for transfer. * * Builds a transaction (PSBT) with the specified transfer options. * @param {TxParams & { sender: Address; feeRate: FeeRate; spendPendingUTXO?: boolean }} params The transfer options including sender address, fee rate, and optional flag for spending pending UTXOs. * @returns {Promise<PreparedTx>} A promise that resolves to the raw unsigned transaction (PSBT). */ prepareTx({ sender, memo, amount, recipient, feeRate, }) { return __awaiter(this, void 0, void 0, function* () { // Build the transaction (PSBT) with the specified transfer options const { psbt, utxos, inputs } = yield this.buildTx({ sender, recipient, amount, feeRate, memo, }); // Return the raw unsigned transaction (PSBT) return { rawUnsignedTx: psbt.toBase64(), utxos, inputs }; }); } /** * Compiles the memo into a buffer for inclusion in a Dogecoin transaction. * * @param {string} memo The memo to be compiled. * @returns {Buffer} The compiled memo as a buffer. */ compileMemo(memo) { // Convert the memo to a buffer const data = Buffer.from(memo, 'utf8'); // Compile the OP_RETURN script with the memo data return Dogecoin.script.compile([Dogecoin.opcodes.OP_RETURN, data]); } /** * Calculates the transaction fee based on the provided UTXOs, fee rate, and optional data. * * @param {UTXO[]} inputs The unspent transaction outputs (UTXOs) used as inputs. * @param {FeeRate} feeRate The fee rate for the transaction. * @param {Buffer | null} data The compiled memo (optional). * @returns {number} The calculated transaction fee. */ getFeeFromUtxos(inputs, feeRate, data = null) { // Calculate the size of the transaction const inputSizeBasedOnInputs = inputs.length > 0 ? inputs.reduce((a) => a + inputBytes(), 0) + inputs.length // +1 byte for each input signature : 0; // Calculate the sum of transaction size let sum = TX_EMPTY_SIZE + inputSizeBasedOnInputs + TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH + TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH; // Add additional output size if data is provided if (data) { sum += TX_OUTPUT_BASE + data.length; } // Calculate the fee based on the sum of transaction size and the fee rate const fee = sum * feeRate; // Ensure the fee is not less than the minimum transaction fee return fee > MIN_TX_FEE ? fee : MIN_TX_FEE; } } /** * Custom Doge client extended to support keystore functionality */ class ClientKeystore extends Client { /** * Get the Dogecoin address. * * Generates a Dogecoin address using the provided phrase and index. * @param {number} index The index of the address to retrieve. Default is 0. * @returns {Address} The Dogecoin 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 is not provided. * @throws {"Address not defined"} Thrown if failed to create the address from the phrase. */ getAddress(index = 0) { if (index < 0) { throw new Error('index must be greater than zero'); } if (this.phrase) { // Get Dogecoin network and keys const dogeNetwork$1 = dogeNetwork(this.network); const dogeKeys = this.getDogeKeys(this.phrase, index); // Generate Dogecoin address const { address } = Dogecoin.payments.p2pkh({ pubkey: dogeKeys.publicKey, network: dogeNetwork$1, }); if (!address) { throw new Error('Address not defined'); } return address; } throw new Error('Phrase must be provided'); } /** * @private * Get private key. * * Private function to get keyPair from the this.phrase * * @param {string} phrase The phrase to be used for generating privkey * @returns {ECPairInterface} The privkey generated from the given phrase * * @throws {"Could not get private key from phrase"} Throws an error if failed creating Doge keys from the given phrase * */ getDogeKeys(phrase, index = 0) { const dogeNetwork$1 = dogeNetwork(this.network); const seed = getSeed(phrase); const master = Dogecoin.bip32.fromSeed(seed, dogeNetwork$1).derivePath(this.getFullDerivationPath(index)); if (!master.privateKey) { throw new Error('Could not get private key from phrase'); } return Dogecoin.ECPair.fromPrivateKey(master.privateKey, { network: dogeNetwork$1 }); } /** * Get the current address. * Asynchronous version of getAddress method. * Generates a network-specific key-pair by first converting the buffer to a Wallet-Import-Format (WIF) * The address is then decoded into type P2WPKH and returned. * @returns {Address} The current address. * * @throws {"Phrase must be provided"} Thrown if phrase has not been set before. * @throws {"Address not defined"} Thrown if failed creating account from phrase. */ getAddressAsync(index = 0) { return __awaiter(this, void 0, void 0, function* () { return this.getAddress(index); }); } /** * Asynchronously transfers Dogecoin. * * Builds, signs, and broadcasts a Dogecoin transaction with the specified parameters. * @param {TxParams & { feeRate?: FeeRate }} params The transfer parameters including transaction details and optional fee rate. * @returns {TxHash} A promise that resolves to the transaction hash once the transfer is completed. */ transfer(params) { return __awaiter(this, void 0, void 0, function* () { // Determine the fee rate for the transaction, using provided fee rate or fetching it from the network const feeRate = params.feeRate || (yield this.getFeeRates())[FeeOption.Fast]; // Check if the fee rate is within the specified fee bounds checkFeeBounds(this.feeBounds, feeRate); // Get the index of the sender's address or use the default index (0) const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0; // Prepare the transaction by building it with the specified parameters const { rawUnsignedTx } = yield this.prepareTx(Object.assign(Object.assign({}, params), { feeRate, sender: yield this.getAddressAsync(fromAddressIndex) })); // Get the Dogecoin keys for signing the transaction const dogeKeys = this.getDogeKeys(this.phrase, fromAddressIndex); // Create a Partially Signed Bitcoin Transaction (PSBT) from the raw unsigned transaction const psbt = Dogecoin.Psbt.fromBase64(rawUnsignedTx, { maximumFeeRate: 7500000 }); // Sign all inputs of the transaction with the Dogecoin keys psbt.signAllInputs(dogeKeys); // Finalize all inputs of the transaction psbt.finalizeAllInputs(); // Extract the signed transaction and format it to hexadecimal const txHex = psbt.extractTransaction().toHex(); // Broadcast the signed transaction to the Dogecoin network and return the transaction hash return yield this.roundRobinBroadcastTx(txHex); }); } } /** * 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 Doge 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: 'dogecoin' }); return this.app; }); } // Get the current address synchronously getAddress() { throw Error('Sync method not supported for Ledger'); } // Get the current address asynchronously getAddressAsync(index = 0, verify = false) { return __awaiter(this, void 0, void 0, function* () { const app = yield this.getApp(); const result = yield app.getWalletPublicKey(this.getFullDerivationPath(index), { format: 'legacy', verify, }); return result.bitcoinAddress; }); } // Transfer Doge 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 psbt = Dogecoin.Psbt.fromBase64(rawUnsignedTx); // Prepare Ledger inputs const ledgerInputs = inputs.map(({ txHex, hash, index }) => { if (!txHex) { throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`); } const splittedTx = app.splitTransaction(txHex, false /* no segwit support */); return [splittedTx, index, null, null]; }); // Prepare associated keysets const associatedKeysets = ledgerInputs.map(() => this.getFullDerivationPath(fromAddressIndex)); // Convert the raw unsigned transaction to a Transaction object // 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'); const txHex = yield app.createPaymentTransaction({ inputs: ledgerInputs, associatedKeysets, outputScriptHex, // no additionals - similar to https://github.com/shapeshift/hdwallet/blob/a61234eb83081a4de54750b8965b873b15803a03/packages/hdwallet-ledger/src/bitcoin.ts#L222 additionals: [], }); const txHash = yield this.broadcastTx(txHex); // Throw error if no transaction hash is received if (!txHash) { throw Error('No Tx hash'); } return txHash; }); } } /** * Function to get the URL for sending a transaction based on the network and Blockcypher URL. * Throws an error if the network is 'testnet' since the testnet URL is not available for Blockcypher. * @param {object} params Object containing the Blockcypher URL and network type. * @param {string} params.blockcypherUrl The Blockcypher URL. * @param {Network} params.network The network type (Mainnet, Testnet, or Stagenet). * @returns {string} The URL for sending a transaction. */ const getSendTxUrl = ({ blockcypherUrl, network }) => { if (network === 'testnet') { // Check if the network is testnet throw new Error('Testnet URL is not available for Blockcypher'); // Throw an error if testnet URL is requested } else { return `${blockcypherUrl}/doge/main/txs/push`; // Return the mainnet URL for sending a transaction } }; export { AssetDOGE, BitgoProviders, ClientKeystore as Client, ClientLedger, DOGEChain, DOGE_DECIMAL, LOWER_FEE_BOUND, MIN_TX_FEE, UPPER_FEE_BOUND, blockcypherDataProviders, blockstreamExplorerProviders, defaultDogeParams, getPrefix, getSendTxUrl, sochainDataProviders, validateAddress };