UNPKG

@thorwallet/xchain-bitcoincash

Version:

Custom bitcoincash client and utilities used by XChainJS clients

326 lines 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDefaultFees = exports.getDefaultFeesWithRates = exports.calcFee = exports.buildTx = exports.scanUTXOs = exports.validateAddress = exports.toBCHAddressNetwork = exports.parseTransaction = exports.toCashAddress = exports.toLegacyAddress = exports.stripPrefix = exports.getPrefix = exports.bchNetwork = exports.isTestnet = exports.getBalance = exports.getFee = exports.compileMemo = exports.DEFAULT_SUGGESTED_TRANSACTION_FEE = exports.BCH_DECIMAL = void 0; const tslib_1 = require("tslib"); const bitcash = require('@psf/bitcoincashjs-lib'); const bchaddr = tslib_1.__importStar(require("bchaddrjs")); const coininfo_1 = tslib_1.__importDefault(require("coininfo")); const lib_1 = require("@thorwallet/xchain-util/lib"); const haskoin_api_1 = require("./haskoin-api"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const accumulative_1 = tslib_1.__importDefault(require("coinselect/accumulative")); exports.BCH_DECIMAL = 8; exports.DEFAULT_SUGGESTED_TRANSACTION_FEE = 1; 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; /** * Compile memo. * * @param {string} memo The memo to be compiled. * @returns {Buffer} The compiled memo. */ const compileMemo = (memo) => { const data = Buffer.from(memo, 'utf8'); // converts MEMO to buffer return bitcash.script.compile([bitcash.opcodes.OP_RETURN, data]); // Compile OP_RETURN script }; exports.compileMemo = compileMemo; /** * Get the transaction fee. * * reference to https://github.com/Permissionless-Software-Foundation/bch-js/blob/acc0300a444059d612daec2564da743c11e27139/src/bitcoincash.js#L408 * * @param {number} inputs The inputs count. * @param {number} outputs The outputs count. * @param {FeeRate} feeRate The fee rate. * @param {Buffer} data The compiled memo (Optional). * @returns {number} The fee amount. */ function getFee(inputs, feeRate, data = null) { let totalWeight = TX_EMPTY_SIZE; totalWeight += (TX_INPUT_PUBKEYHASH + TX_INPUT_BASE) * inputs; totalWeight += (TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH) * 2; if (data) { totalWeight += 9 + data.length; } return Math.ceil(totalWeight * feeRate); } exports.getFee = getFee; /** * Get the balances of an address. * * @param {AddressParams} params * @returns {Array<Balance>} The balances of the given address. */ const getBalance = (params) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { try { const account = yield haskoin_api_1.getAccount(params); if (!account) { return Promise.reject(new Error('No bchBalance found')); } const confirmed = lib_1.baseAmount(account.confirmed, exports.BCH_DECIMAL); const unconfirmed = lib_1.baseAmount(account.unconfirmed, exports.BCH_DECIMAL); account.confirmed; return [ { asset: lib_1.AssetBCH, amount: lib_1.baseAmount(confirmed.amount().plus(unconfirmed.amount()), exports.BCH_DECIMAL), }, ]; } catch (error) { return Promise.reject(new Error('Invalid address')); } }); exports.getBalance = getBalance; /** * Check if give network is a testnet. * * @param {Network} network * @returns {boolean} `true` or `false` */ const isTestnet = (network) => { return network === 'testnet'; }; exports.isTestnet = isTestnet; /** * Get BCH network to be used with bitcore-lib. * * @param {Network} network * @returns {} The BCH network. */ const bchNetwork = (network) => { return exports.isTestnet(network) ? coininfo_1.default.bitcoincash.test.toBitcoinJS() : coininfo_1.default.bitcoincash.main.toBitcoinJS(); }; exports.bchNetwork = bchNetwork; /** * BCH new addresses strategy has no any prefixes. * Any possible prefixes at the TX addresses will be stripped out with parseTransaction **/ const getPrefix = () => ''; exports.getPrefix = getPrefix; /** * Strips bchtest or bitcoincash prefix from address * * @param {Address} address * @returns {Address} The address with prefix removed * */ const stripPrefix = (address) => address.replace(/(bchtest:|bitcoincash:)/, ''); exports.stripPrefix = stripPrefix; /** * Convert to Legacy Address. * * @param {Address} address * @returns {Address} Legacy address. */ const toLegacyAddress = (address) => { return bchaddr.toLegacyAddress(address); }; exports.toLegacyAddress = toLegacyAddress; /** * Convert to Cash Address. * * @param {Address} address * @returns {Address} Cash address. */ const toCashAddress = (address) => { return bchaddr.toCashAddress(address); }; exports.toCashAddress = toCashAddress; /** * Parse transaction. * * @param {Transaction} tx * @returns {Tx} Parsed transaction. * **/ const parseTransaction = (tx) => { return { asset: lib_1.AssetBCH, from: tx.inputs // For correct type inference `Array.prototype.filter` needs manual type guard to be defined .filter((input) => !!input.address) .map((input) => ({ from: exports.stripPrefix(input.address), amount: lib_1.baseAmount(input.value, exports.BCH_DECIMAL), })), to: tx.outputs // For correct type inference `Array.prototype.filter` needs manual type guard to be defined .filter((output) => !!output.address) .map((output) => ({ to: exports.stripPrefix(output.address), amount: lib_1.baseAmount(output.value, exports.BCH_DECIMAL), })), date: new Date(tx.time * 1000), type: 'transfer', hash: tx.txid, binanceFee: null, confirmations: null, ethCumulativeGasUsed: null, ethGas: null, ethGasPrice: null, ethGasUsed: null, ethTokenName: null, ethTokenSymbol: null, memo: null, }; }; exports.parseTransaction = parseTransaction; /** * Converts `Network` to `bchaddr.Network` * * @param {Network} network * @returns {string} bchaddr network */ const toBCHAddressNetwork = (network) => network === 'testnet' ? bchaddr.Network.Testnet : bchaddr.Network.Mainnet; exports.toBCHAddressNetwork = toBCHAddressNetwork; /** * Validate the BCH address. * * @param {string} address * @param {Network} network * @returns {boolean} `true` or `false`. */ const validateAddress = (address, network) => { const toAddress = exports.toCashAddress(address); return bchaddr.isValidAddress(toAddress) && bchaddr.detectAddressNetwork(toAddress) === exports.toBCHAddressNetwork(network); }; exports.validateAddress = validateAddress; /** * Scan UTXOs from sochain. * * @param {string} haskoinUrl sochain Node URL. * @param {Address} address * @returns {Array<UTXO>} The UTXOs of the given address. */ const scanUTXOs = (haskoinUrl, address) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const unspents = yield haskoin_api_1.getUnspentTransactions({ haskoinUrl, address }); const utxos = []; for (const utxo of unspents || []) { utxos.push({ hash: utxo.txid, value: utxo.value, index: utxo.index, witnessUtxo: { value: utxo.value, script: bitcash.script.compile(Buffer.from(utxo.pkscript, 'hex')), }, address: utxo.address, txHex: yield haskoin_api_1.getRawTransaction({ haskoinUrl, txId: utxo.txid }), }); } return utxos; }); exports.scanUTXOs = scanUTXOs; /** * Build transcation. * * @param {BuildParams} params The transaction build options. * @returns {Transaction} */ const buildTx = ({ amount, recipient, memo, feeRate, sender, network, haskoinUrl, }) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { try { const recipientCashAddress = exports.toCashAddress(recipient); if (!exports.validateAddress(recipientCashAddress, network)) { return Promise.reject(new Error('Invalid address')); } const utxos = yield exports.scanUTXOs(haskoinUrl, sender); if (utxos.length === 0) { return Promise.reject(Error('No utxos to send')); } const feeRateWhole = Number(feeRate.toFixed(0)); const compiledMemo = memo ? exports.compileMemo(memo) : null; const targetOutputs = []; // output to recipient targetOutputs.push({ address: recipient, value: amount.amount().toNumber(), }); const { inputs, outputs } = accumulative_1.default(utxos, targetOutputs, feeRateWhole); // .inputs and .outputs will be undefined if no solution was found if (!inputs || !outputs) { return Promise.reject(Error('Balance insufficient for transaction')); } const transactionBuilder = new bitcash.TransactionBuilder(exports.bchNetwork(network)); //Inputs inputs.forEach((utxo) => transactionBuilder.addInput(bitcash.Transaction.fromBuffer(Buffer.from(utxo.txHex, 'hex')), utxo.index)); // Outputs // eslint-disable-next-line @typescript-eslint/no-explicit-any outputs.forEach((output) => { let out = undefined; if (!output.address) { //an empty address means this is the change address out = bitcash.address.toOutputScript(exports.toLegacyAddress(sender), exports.bchNetwork(network)); } else if (output.address) { out = bitcash.address.toOutputScript(exports.toLegacyAddress(output.address), exports.bchNetwork(network)); } transactionBuilder.addOutput(out, output.value); }); // add output for memo if (compiledMemo) { transactionBuilder.addOutput(compiledMemo, 0); // Add OP_RETURN {script, value} } return { builder: transactionBuilder, inputUTXOs: inputs, }; } catch (e) { return Promise.reject(e); } }); exports.buildTx = buildTx; /** * Calculate fees based on fee rate and memo. * * @param {FeeRate} feeRate * @param {string} memo (optional) * @param {UnspentOutput} utxos (optional) * @returns {BaseAmount} The calculated fees based on fee rate and the memo. */ const calcFee = (feeRate, memo, utxos = []) => { const compiledMemo = memo ? exports.compileMemo(memo) : null; const fee = getFee(utxos.length, feeRate, compiledMemo); return lib_1.baseAmount(fee); }; exports.calcFee = calcFee; /** * Get the default fees with rates. * * @returns {FeesWithRates} The default fees and rates. */ const getDefaultFeesWithRates = () => { const nextBlockFeeRate = 1; const rates = { fastest: nextBlockFeeRate * 5, fast: nextBlockFeeRate * 1, average: nextBlockFeeRate * 0.5, }; const fees = { type: 'byte', fast: exports.calcFee(rates.fast), average: exports.calcFee(rates.average), fastest: exports.calcFee(rates.fastest), }; return { fees, rates, }; }; exports.getDefaultFeesWithRates = getDefaultFeesWithRates; /** * Get the default fees. * * @returns {Fees} The default fees. */ const getDefaultFees = () => { const { fees } = exports.getDefaultFeesWithRates(); return fees; }; exports.getDefaultFees = getDefaultFees; //# sourceMappingURL=utils.js.map