Improved auto-fee calc

- createTx and createMultiSigTx will now resolve an object with tx_hex and other values (amount, size, etc)
This commit is contained in:
sairajzero 2022-08-25 03:04:56 +05:30
parent 77ff76d787
commit 04aad28600

View File

@ -1,4 +1,4 @@
(function(EXPORTS) { //btcOperator v1.0.8 (function(EXPORTS) { //btcOperator v1.0.9
/* BTC Crypto and API Operator */ /* BTC Crypto and API Operator */
const btcOperator = EXPORTS; const btcOperator = EXPORTS;
@ -16,14 +16,14 @@
}) })
}; };
const SIGN_SIZE = 73; const SATOSHI_IN_BTC = 1e8;
function get_fee_rate() { function get_fee_rate() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch('https://api.blockchain.info/mempool/fees').then(response => { fetch('https://api.blockchain.info/mempool/fees').then(response => {
if (response.ok) if (response.ok)
response.json() response.json()
.then(result => resolve(result.regular)) .then(result => resolve(parseFloat((result.regular / SATOSHI_IN_BTC).toFixed(8))))
.catch(error => reject(error)); .catch(error => reject(error));
else else
reject(response); reject(response);
@ -204,6 +204,17 @@
.catch(error => reject(error)) .catch(error => reject(error))
}); });
const BASE_TX_SIZE = 12,
BASE_INPUT_SIZE = 41,
LEGACY_INPUT_SIZE = 107,
BECH32_INPUT_SIZE = 27,
SEGWIT_INPUT_SIZE = 59,
MULTISIG_INPUT_SIZE_ES = 351,
BASE_OUTPUT_SIZE = 9,
LEGACY_OUTPUT_SIZE = 25,
BECH32_OUTPUT_SIZE = 23,
SEGWIT_OUTPUT_SIZE = 23;
function _redeemScript(addr, key) { function _redeemScript(addr, key) {
let decode = coinjs.addressDecode(addr); let decode = coinjs.addressDecode(addr);
switch (decode.type) { switch (decode.type) {
@ -218,6 +229,39 @@
} }
} }
function _sizePerInput(addr, rs) {
switch (coinjs.addressDecode(addr).type) {
case "standard":
return BASE_INPUT_SIZE + LEGACY_INPUT_SIZE;
case "bech32":
return BASE_INPUT_SIZE + BECH32_INPUT_SIZE;
case "multisig":
switch (coinjs.script().decodeRedeemScript(rs).type) {
case "segwit__":
return BASE_INPUT_SIZE + SEGWIT_INPUT_SIZE;
case "multisig__":
return BASE_INPUT_SIZE + MULTISIG_INPUT_SIZE_ES;
default:
return null;
};
default:
return null;
}
}
function _sizePerOutput(addr) {
switch (coinjs.addressDecode(addr).type) {
case "standard":
return BASE_OUTPUT_SIZE + LEGACY_OUTPUT_SIZE;
case "bech32":
return BASE_OUTPUT_SIZE + BECH32_OUTPUT_SIZE;
case "multisig":
return BASE_OUTPUT_SIZE + SEGWIT_OUTPUT_SIZE;
default:
return null;
}
}
function validateTxParameters(parameters) { function validateTxParameters(parameters) {
let invalids = []; let invalids = [];
//sender-ids //sender-ids
@ -265,48 +309,71 @@
return parameters; return parameters;
} }
const TMP_FEE = 0.00001;
function createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr) { function createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let auto_fee = false, let total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8));
total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8));
if (fee === null) {
auto_fee = true;
fee = TMP_FEE;
}
const tx = coinjs.transaction(); const tx = coinjs.transaction();
addUTXOs(tx, senders, redeemScripts, total_amount + fee).then(result => { let output_size = addOutputs(tx, receivers, amounts, change_addr);
if (result > 0) addInputs(tx, senders, redeemScripts, total_amount, fee, output_size).then(result => {
return reject("Insufficient Balance"); if (result.change_amount > 0)
let change = addOutputs(tx, receivers, amounts, Math.abs(result), change_addr); tx.outs[tx.outs.length - 1].value = parseInt(result.change_amount * SATOSHI_IN_BTC); //values are in satoshi
if (!auto_fee) else
return resolve(tx); tx.outs.pop(); //remove the change output if no change_amount
autoFeeCalc(tx).then(fee_calc => { result.output_size = output_size;
fee = Math.round((fee * 1) * 1e8); //satoshi convertion result.output_amount = total_amount;
if (!change) result.total_size = BASE_TX_SIZE + output_size + result.input_size;
tx.addoutput(change_addr, 0); result.transaction = tx;
editFee(tx, fee, fee_calc); resolve(result);
resolve(tx); }).catch(error => reject(error))
}).catch(error => reject(error))
})
}) })
} }
function addUTXOs(tx, senders, redeemScripts, required_amount, n = 0) { function addInputs(tx, senders, redeemScripts, total_amount, fee, output_size) {
return new Promise((resolve, reject) => {
if (fee !== null) {
addUTXOs(tx, senders, redeemScripts, total_amount + fee, false).then(result => {
result.fee = fee;
resolve(result);
}).catch(error => reject(error))
} else {
get_fee_rate().then(fee_rate => {
let net_fee = BASE_TX_SIZE * fee_rate;
net_fee += (output_size * fee_rate);
addUTXOs(tx, senders, redeemScripts, total_amount + net_fee, fee_rate).then(result => {
result.fee_amount = parseFloat((net_fee + (result.input_size * fee_rate)).toFixed(8));
result.fee_rate = fee_rate;
resolve(result);
}).catch(error => reject(error))
}).catch(error => reject(error))
}
})
}
function addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
required_amount = parseFloat(required_amount.toFixed(8)); required_amount = parseFloat(required_amount.toFixed(8));
if (required_amount <= 0 || n >= senders.length) if (typeof rec_args.n === "undefined") {
return resolve(required_amount); rec_args.n = 0;
let addr = senders[n], rec_args.input_size = 0;
rs = redeemScripts[n]; rec_args.input_amount = 0;
}
if (required_amount <= 0)
return resolve({
input_size: rec_args.input_size,
input_amount: rec_args.input_amount,
change_amount: required_amount * -1 //required_amount will be -ve of change_amount
});
else if (rec_args.n >= senders.length)
return reject("Insufficient Balance");
let addr = senders[rec_args.n],
rs = redeemScripts[rec_args.n];
let size_per_input = _sizePerInput(addr, rs);
fetch_api(`get_tx_unspent/BTC/${addr}`).then(result => { fetch_api(`get_tx_unspent/BTC/${addr}`).then(result => {
let utxos = result.data.txs; let utxos = result.data.txs;
console.debug("add-utxo", addr, rs, required_amount, utxos); console.debug("add-utxo", addr, rs, required_amount, utxos);
for (let i = 0; i < utxos.length && required_amount > 0; i++) { for (let i = 0; i < utxos.length && required_amount > 0; i++) {
if (!utxos[i].confirmations) //ignore unconfirmed utxo if (!utxos[i].confirmations) //ignore unconfirmed utxo
continue; continue;
required_amount -= parseFloat(utxos[i].value);
var script; var script;
if (!rs || !rs.length) //legacy script if (!rs || !rs.length) //legacy script
script = utxos[i].script_hex; script = utxos[i].script_hex;
@ -315,29 +382,38 @@
let s = coinjs.script(); let s = coinjs.script();
s.writeBytes(Crypto.util.hexToBytes(rs)); s.writeBytes(Crypto.util.hexToBytes(rs));
s.writeOp(0); s.writeOp(0);
s.writeBytes(coinjs.numToBytes((utxos[i].value * 100000000).toFixed(0), 8)); s.writeBytes(coinjs.numToBytes((utxos[i].value * SATOSHI_IN_BTC).toFixed(0), 8));
script = Crypto.util.bytesToHex(s.buffer); script = Crypto.util.bytesToHex(s.buffer);
} else //redeemScript for multisig } else //redeemScript for multisig
script = rs; script = rs;
tx.addinput(utxos[i].txid, utxos[i].output_no, script, 0xfffffffd /*sequence*/ ); //0xfffffffd for Replace-by-fee tx.addinput(utxos[i].txid, utxos[i].output_no, script, 0xfffffffd /*sequence*/ ); //0xfffffffd for Replace-by-fee
//update track values
rec_args.input_size += size_per_input;
rec_args.input_amount += parseFloat(utxos[i].value);
required_amount -= parseFloat(utxos[i].value);
if (fee_rate) //automatic fee calculation (dynamic)
required_amount += size_per_input * fee_rate;
} }
addUTXOs(tx, senders, redeemScripts, required_amount, n + 1) rec_args.n += 1;
addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)) .catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
}) })
} }
function addOutputs(tx, receivers, amounts, change, change_addr) { function addOutputs(tx, receivers, amounts, change_addr) {
for (let i in receivers) let size = 0;
for (let i in receivers) {
tx.addoutput(receivers[i], amounts[i]); tx.addoutput(receivers[i], amounts[i]);
if (parseFloat(change.toFixed(8)) > 0) { size += _sizePerOutput(receivers[i]);
tx.addoutput(change_addr, change); }
return true; tx.addoutput(change_addr, 0);
} else size += _sizePerOutput(change_addr);
return false; return size;
} }
/*
function autoFeeCalc(tx) { function autoFeeCalc(tx) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
get_fee_rate().then(fee_rate => { get_fee_rate().then(fee_rate => {
@ -362,16 +438,17 @@
function editFee(tx, current_fee, target_fee, index = -1) { function editFee(tx, current_fee, target_fee, index = -1) {
//values are in satoshi //values are in satoshi
index = parseInt(index >= 0 ? index : tx.out.length - index); index = parseInt(index >= 0 ? index : tx.outs.length - index);
if (index < 0 || index >= tx.out.length) if (index < 0 || index >= tx.outs.length)
throw "Invalid index"; throw "Invalid index";
let edit_value = parseInt(current_fee - target_fee), //rip of any decimal places let edit_value = parseInt(current_fee - target_fee), //rip of any decimal places
current_value = tx.out[index].value; //could be BigInterger current_value = tx.outs[index].value; //could be BigInterger
if (edit_value < 0 && edit_value > current_value) if (edit_value < 0 && edit_value > current_value)
throw "Insufficient value at vout"; throw "Insufficient value at vout";
tx.out[index].value = current_value instanceof BigInteger ? tx.outs[index].value = current_value instanceof BigInteger ?
current_value.add(new BigInteger('' + edit_value)) : parseInt(current_value + edit_value); current_value.add(new BigInteger('' + edit_value)) : parseInt(current_value + edit_value);
} }
*/
btcOperator.sendTx = function(senders, privkeys, receivers, amounts, fee, change_addr = null) { btcOperator.sendTx = function(senders, privkeys, receivers, amounts, fee, change_addr = null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -402,7 +479,8 @@
if (redeemScripts.includes(null)) //TODO: segwit if (redeemScripts.includes(null)) //TODO: segwit
return reject("Unable to get redeem-script"); return reject("Unable to get redeem-script");
//create transaction //create transaction
createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr || senders[0]).then(tx => { createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr || senders[0]).then(result => {
let tx = result.transaction;
console.debug("Unsigned:", tx.serialize()); console.debug("Unsigned:", tx.serialize());
new Set(wif_keys).forEach(key => console.debug("Signing key:", key, tx.sign(key, 1 /*sighashtype*/ ))); //Sign the tx using private key WIF new Set(wif_keys).forEach(key => console.debug("Signing key:", key, tx.sign(key, 1 /*sighashtype*/ ))); //Sign the tx using private key WIF
console.debug("Signed:", tx.serialize()); console.debug("Signed:", tx.serialize());
@ -435,9 +513,11 @@
if (redeemScripts.includes(null)) //TODO: segwit if (redeemScripts.includes(null)) //TODO: segwit
return reject("Unable to get redeem-script"); return reject("Unable to get redeem-script");
//create transaction //create transaction
createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr || senders[0]) createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr || senders[0]).then(result => {
.then(tx => resolve(tx.serialize())) result.tx_hex = result.transaction.serialize();
.catch(error => reject(error)) delete result.transaction;
resolve(result);
}).catch(error => reject(error))
}) })
} }
@ -465,9 +545,12 @@
return reject(e) return reject(e)
} }
//create transaction //create transaction
createTransaction([sender], [redeemScript], receivers, amounts, fee, sender) createTransaction([sender], [redeemScript], receivers, amounts, fee, sender).then(result => {
.then(tx => resolve(tx.serialize())) result.tx_hex = result.transaction.serialize();
.catch(error => reject(error)) delete result.transaction;
resolve(result);
}).catch(error => reject(error))
}) })
} }