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:
parent
77ff76d787
commit
04aad28600
185
btcOperator.js
185
btcOperator.js
@ -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))
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user