@thorwallet/xchain-bitcoin
Version:
Custom Bitcoin client and utilities used by XChainJS clients
318 lines • 11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPrefix = exports.getDefaultFees = exports.getDefaultFeesWithRates = exports.calcFee = exports.broadcastTx = exports.buildTx = exports.scanUTXOs = exports.validateAddress = exports.getBalance = exports.btcNetwork = exports.isTestnet = exports.arrayAverage = exports.getFee = exports.compileMemo = exports.BTC_DECIMAL = void 0;
const tslib_1 = require("tslib");
const Bitcoin = tslib_1.__importStar(require("bitcoinjs-lib")); // https://github.com/bitcoinjs/bitcoinjs-lib
const sochain = tslib_1.__importStar(require("./sochain-api"));
const blockStream = tslib_1.__importStar(require("./blockstream-api"));
const xchain_util_1 = require("@thorwallet/xchain-util");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const accumulative_1 = tslib_1.__importDefault(require("coinselect/accumulative"));
const const_1 = require("./const");
const haskoinApi = tslib_1.__importStar(require("./haskoin-api"));
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;
exports.BTC_DECIMAL = 8;
const inputBytes = (input) => {
return TX_INPUT_BASE + (input.witnessUtxo.script ? input.witnessUtxo.script.length : TX_INPUT_PUBKEYHASH);
};
/**
* 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 Bitcoin.script.compile([Bitcoin.opcodes.OP_RETURN, data]); // Compile OP_RETURN script
};
exports.compileMemo = compileMemo;
/**
* Get the transaction fee.
*
* @param {UTXOs} inputs The UTXOs.
* @param {FeeRate} feeRate The fee rate.
* @param {Buffer} data The compiled memo (Optional).
* @returns {number} The fee amount.
*/
const getFee = (inputs, feeRate, data = null) => {
let sum = TX_EMPTY_SIZE +
inputs.reduce((a, x) => a + inputBytes(x), 0) +
inputs.length + // +1 byte for each input signature
TX_OUTPUT_BASE +
TX_OUTPUT_PUBKEYHASH +
TX_OUTPUT_BASE +
TX_OUTPUT_PUBKEYHASH;
if (data) {
sum += TX_OUTPUT_BASE + data.length;
}
const fee = sum * feeRate;
return fee > const_1.MIN_TX_FEE ? fee : const_1.MIN_TX_FEE;
};
exports.getFee = getFee;
/**
* Get the average value of an array.
*
* @param {Array<number>} array
* @returns {number} The average value.
*/
const arrayAverage = (array) => {
let sum = 0;
array.forEach((value) => (sum += value));
return sum / array.length;
};
exports.arrayAverage = arrayAverage;
/**
* 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 Bitcoin network to be used with bitcoinjs.
*
* @param {Network} network
* @returns {Bitcoin.Network} The BTC network.
*/
const btcNetwork = (network) => {
return exports.isTestnet(network) ? Bitcoin.networks.testnet : Bitcoin.networks.bitcoin;
};
exports.btcNetwork = btcNetwork;
/**
* Get the balances of an address.
*
* @param {string} sochainUrl sochain Node URL.
* @param {Network} network
* @param {Address} address
* @returns {Array<Balance>} The balances of the given address.
*/
const getBalance = (params) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
switch (params.network) {
case 'mainnet':
return [
{
asset: xchain_util_1.AssetBTC,
amount: yield haskoinApi.getBalance({
haskoinUrl: 'https://haskoin.ninerealms.com/btc',
address: params.address,
}),
},
];
case 'testnet':
return [
{
asset: xchain_util_1.AssetBTC,
amount: yield sochain.getBalance(params),
},
];
}
});
exports.getBalance = getBalance;
/**
* Validate the BTC address.
*
* @param {Address} address
* @param {Network} network
* @returns {boolean} `true` or `false`.
*/
const validateAddress = (address, network) => {
try {
Bitcoin.address.toOutputScript(address, exports.btcNetwork(network));
return true;
}
catch (error) {
return false;
}
};
exports.validateAddress = validateAddress;
/**
* Scan UTXOs from sochain.
*
* @param {string} sochainUrl sochain Node URL.
* @param {Network} network
* @param {Address} address
* @returns {Array<UTXO>} The UTXOs of the given address.
*/
const scanUTXOs = ({ sochainUrl, haskoinUrl, network, address, confirmedOnly = true, // default: scan only confirmed UTXOs
}) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (network === 'testnet') {
let utxos = [];
const addressParam = {
sochainUrl,
network,
address,
};
if (confirmedOnly) {
utxos = yield sochain.getConfirmedUnspentTxs(addressParam);
}
else {
utxos = yield sochain.getUnspentTxs(addressParam);
}
return utxos.map((utxo) => ({
hash: utxo.txid,
index: utxo.output_no,
value: xchain_util_1.assetToBase(xchain_util_1.assetAmount(utxo.value, exports.BTC_DECIMAL)).amount().toNumber(),
witnessUtxo: {
value: xchain_util_1.assetToBase(xchain_util_1.assetAmount(utxo.value, exports.BTC_DECIMAL)).amount().toNumber(),
script: Buffer.from(utxo.script_hex, 'hex'),
},
}));
}
let utxos = [];
if (confirmedOnly) {
utxos = yield haskoinApi.getConfirmedUnspentTxs({ address, haskoinUrl });
}
else {
utxos = yield haskoinApi.getUnspentTxs({ address, haskoinUrl });
}
return utxos.map((utxo) => ({
hash: utxo.txid,
index: utxo.index,
value: xchain_util_1.baseAmount(utxo.value, exports.BTC_DECIMAL).amount().toNumber(),
witnessUtxo: {
value: xchain_util_1.baseAmount(utxo.value, exports.BTC_DECIMAL).amount().toNumber(),
script: Buffer.from(utxo.pkscript, 'hex'),
},
}));
});
exports.scanUTXOs = scanUTXOs;
/**
* Build transcation.
*
* @param {BuildParams} params The transaction build options.
* @returns {Transaction}
*/
const buildTx = ({ amount, recipient, memo, feeRate, sender, network, sochainUrl, haskoinUrl, spendPendingUTXO = false, // default: prevent spending uncomfirmed UTXOs
}) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
try {
// search only confirmed UTXOs if pending UTXO is not allowed
const confirmedOnly = !spendPendingUTXO;
const utxos = yield exports.scanUTXOs({ sochainUrl, haskoinUrl, network, address: sender, confirmedOnly });
if (utxos.length === 0) {
return Promise.reject(Error('No utxos to send'));
}
if (!exports.validateAddress(recipient, network)) {
return Promise.reject(new Error('Invalid address'));
}
const feeRateWhole = Number(feeRate.toFixed(0));
const compiledMemo = memo ? exports.compileMemo(memo) : null;
const targetOutputs = [];
//1. add output amount and recipient to targets
targetOutputs.push({
address: recipient,
value: amount.amount().toNumber(),
});
//2. add output memo to targets (optional)
if (compiledMemo) {
targetOutputs.push({ script: compiledMemo, value: 0 });
}
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('Insufficient Balance for transaction'));
}
const psbt = new Bitcoin.Psbt({ network: exports.btcNetwork(network) }); // Network-specific
// psbt add input from accumulative inputs
inputs.forEach((utxo) => psbt.addInput({
hash: utxo.hash,
index: utxo.index,
witnessUtxo: utxo.witnessUtxo,
}));
// psbt add outputs from accumulative outputs
outputs.forEach((output) => {
if (!output.address) {
//an empty address means this is the change ddress
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 };
}
catch (e) {
return Promise.reject(e);
}
});
exports.buildTx = buildTx;
/**
* Broadcast the transaction.
*
* @param {BroadcastTxParams} params The transaction broadcast options.
* @returns {TxHash} The transaction hash.
*/
const broadcastTx = ({ network, txHex, blockstreamUrl }) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
return yield blockStream.broadcastTx({ network, txHex, blockstreamUrl });
});
exports.broadcastTx = broadcastTx;
/**
* Calculate fees based on fee rate and memo.
*
* @param {FeeRate} feeRate
* @param {string} memo
* @returns {BaseAmount} The calculated fees based on fee rate and the memo.
*/
const calcFee = (feeRate, memo) => {
const compiledMemo = memo ? exports.compileMemo(memo) : null;
const fee = exports.getFee([], feeRate, compiledMemo);
return xchain_util_1.baseAmount(fee);
};
exports.calcFee = calcFee;
/**
* Get the default fees with rates.
*
* @returns {FeesWithRates} The default fees and rates.
*/
const getDefaultFeesWithRates = () => {
const rates = {
fastest: 50,
fast: 20,
average: 10,
};
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;
/**
* Get address prefix based on the network.
*
* @param {Network} network
* @returns {string} The address prefix based on the network.
*
**/
const getPrefix = (network) => (network === 'testnet' ? 'tb1' : 'bc1');
exports.getPrefix = getPrefix;
//# sourceMappingURL=utils.js.map