@thorwallet/xchain-bitcoincash
Version:
Custom bitcoincash client and utilities used by XChainJS clients
326 lines • 11.6 kB
JavaScript
;
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