Merge pull request #2 from ranchimall/dev

multi-inputs
This commit is contained in:
Sai Raj 2022-06-26 18:45:47 +05:30 committed by GitHub
commit 334bf46fa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 200 additions and 79 deletions

View File

@ -69,17 +69,26 @@
<div class="panel-heading">Send Transaction</div> <div class="panel-heading">Send Transaction</div>
<div class="panel-body"> <div class="panel-body">
<form id="send-tx"> <form id="send-tx">
<div class="form-group"> <template class="sender-template">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i>Sender</i></span> <span class="input-group-addon"><i>Sender</i></span>
<input type="text" class="form-control" name="sender" placeholder="Sender ID"> <input type="text" class="form-control" name="sender" placeholder="Sender ID">
<span class="input-group-addon"><i>Balance</i></span>
<input type="text" class="form-control" name="balance" placeholder="Balance" disabled>
<div class="input-group-btn">
<button class="btn btn-default" name="rm-sender" type="button">X</button>
</div>
</div> </div>
</template>
<div class="form-group">
<div class="sender-container"></div>
<div class="input-group"> <div class="input-group">
<div class="input-group-btn"> <div class="input-group-btn">
<button class="btn btn-default" name="check-balance" type="button">Balance</button> <button class="btn btn-default" name="check-balance" type="button">Check Balance</button>
</div> </div>
<input type="number" class="form-control" name="balance" placeholder="Check Balance" disabled> <input type="number" class="form-control" name="total_balance" placeholder="Total Balance" disabled>
</div> </div>
<button type="button" name="add-sender" class="btn btn-default"><b>+</b></button>
</div> </div>
<template class="receiver-template"> <template class="receiver-template">
<div class="input-group"> <div class="input-group">
@ -110,7 +119,7 @@
<form id="address-details"> <form id="address-details">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i>Address</i></span> <span class="input-group-addon"><i>Address</i></span>
<input type="text" class="form-control" name="address" placeholder="Address"> <input type="text" class="form-control" name="addr" placeholder="Address">
<div class="input-group-btn"> <div class="input-group-btn">
<button class="btn btn-default" name="submit" type="button"><b class="glyphicon glyphicon-search"></b></button> <button class="btn btn-default" name="submit" type="button"><b class="glyphicon glyphicon-search"></b></button>
</div> </div>
@ -190,13 +199,34 @@
} }
let sendForm = document.forms['send-tx']; let sendForm = document.forms['send-tx'];
let sender_template = sendForm.getElementsByClassName('sender-template')[0],
sender_container = sendForm.getElementsByClassName('sender-container')[0];
sendForm['add-sender'].onclick = evt => {
sender_container.appendChild(sender_template.content.cloneNode(true));
let clone = sender_container.lastElementChild;
clone.getElementsByTagName("button")[0].onclick = evt => sender_container.removeChild(clone);
};
sendForm['add-sender'].click();
sendForm['check-balance'].onclick = evt => { sendForm['check-balance'].onclick = evt => {
let address = sendForm["sender"].value; sendForm["total_balance"].value = 0;
sendForm['check-balance'].disabled = true; let addresses = [],
btc_api.getBalance(address) total_balance = 0;
.then(result => sendForm["balance"].value = result) if (!sendForm["sender"])
.catch(error => console.error(error)) return;
.finally(_ => sendForm['check-balance'].disabled = false) else if (sendForm["sender"] instanceof RadioNodeList)
for (let i = 0; i < sendForm["sender"].length; i++)
addresses.push(sendForm["sender"][i].value);
else
addresses.push(sendForm["sender"].value);
console.debug(addresses);
let balance_inputs = sendForm["balance"] instanceof RadioNodeList ? sendForm["balance"] : [sendForm["balance"]];
//sendForm['check-balance'].disabled = true;
addresses.forEach((addr, i) => btc_api.getBalance(addr).then(result => {
console.debug(addr, result)
balance_inputs[i].value = result;
sendForm["total_balance"].value = parseFloat(sendForm["total_balance"].value) + result;
}).catch(error => console.error(error)))
// .finally(_ => sendForm['check-balance'].disabled = false)
} }
let receiver_template = sendForm.getElementsByClassName('receiver-template')[0], let receiver_template = sendForm.getElementsByClassName('receiver-template')[0],
receiver_container = sendForm.getElementsByClassName('receiver-container')[0]; receiver_container = sendForm.getElementsByClassName('receiver-container')[0];
@ -207,19 +237,36 @@
}; };
sendForm['add-receiver'].click(); sendForm['add-receiver'].click();
sendForm['submit'].onclick = evt => { sendForm['submit'].onclick = evt => {
let sender = sendForm["sender"].value, let senders = [],
receivers = {}, receivers = [],
amounts = [],
fee = parseFloat(sendForm["fee"].value); fee = parseFloat(sendForm["fee"].value);
//inputs (senders)
if (!sendForm["sender"])
return console.warn("sender cannot be empty");
else if (sendForm["sender"] instanceof RadioNodeList)
for (let i = 0; i < sendForm["sender"].length; i++) {
senders.push(sendForm["sender"][i].value);
}
else {
senders.push(sendForm["sender"].value);
}
//outputs (receivers and amounts)
if (!sendForm["receiver"]) if (!sendForm["receiver"])
return console.warn("receiver cannot be empty"); return console.warn("receiver cannot be empty");
else if (sendForm["receiver"] instanceof RadioNodeList) else if (sendForm["receiver"] instanceof RadioNodeList)
for (let i = 0; i < sendForm["receiver"].length; i++) for (let i = 0; i < sendForm["receiver"].length; i++) {
receivers[sendForm["receiver"][i].value] = parseFloat(sendForm["amount"][i].value); receivers.push(sendForm["receiver"][i].value);
else amounts.push(parseFloat(sendForm["amount"][i].value));
receivers[sendForm["receiver"].value] = parseFloat(sendForm["amount"].value); }
let privKey = prompt("Enter Private Key:"); else {
receivers.push(sendForm["receiver"].value);
amounts.push(parseFloat(sendForm["amount"].value));
}
console.debug(senders, receivers, amounts, fee);
let privkeys = senders.map(s => prompt(`Enter Private Key for ${s}:`))
sendForm['submit'].disabled = true; sendForm['submit'].disabled = true;
btc_api.sendTx(sender, privKey, receivers, fee).then(result => { btc_api.sendTx(senders, privkeys, receivers, amounts, fee).then(result => {
console.log(result); console.log(result);
alert("transaction id: " + result.txid); alert("transaction id: " + result.txid);
}).catch(error => console.error(error)).finally(_ => sendForm['submit'].disabled = false) }).catch(error => console.error(error)).finally(_ => sendForm['submit'].disabled = false)
@ -228,7 +275,7 @@
let detailsForm = document.forms['address-details']; let detailsForm = document.forms['address-details'];
detailsForm['submit'].onclick = evt => { detailsForm['submit'].onclick = evt => {
detailsForm['submit'].disabled = true; detailsForm['submit'].disabled = true;
let address = detailsForm['address'].value; let address = detailsForm['addr'].value;
let table = document.getElementById("view-details"); let table = document.getElementById("view-details");
table.innerHTML = ''; table.innerHTML = '';
detailsForm['balance'].value = ''; detailsForm['balance'].value = '';

View File

@ -1,4 +1,4 @@
(function(GLOBAL) { //lib_btc v1.0.0 (function(GLOBAL) { //lib_btc v1.0.1
/* Utility Libraries required /* Utility Libraries required
* All credits for these codes belong to their respective creators, moderators and owners. * All credits for these codes belong to their respective creators, moderators and owners.
* For more info (including license and terms of use), please visit respective source. * For more info (including license and terms of use), please visit respective source.
@ -1799,7 +1799,7 @@
}; };
} else if (this.ins[index].script.chunks[0] == 0 && this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1][this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1].length - 1] == 174) { // OP_CHECKMULTISIG } else if (this.ins[index].script.chunks[0] == 0 && this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1][this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1].length - 1] == 174) { // OP_CHECKMULTISIG
// multisig script, with signature(s) included // multisig script, with signature(s) included
sigcount = 0; var sigcount = 0;
for (i = 1; i < this.ins[index].script.chunks.length - 1; i++) { for (i = 1; i < this.ins[index].script.chunks.length - 1; i++) {
if (this.ins[index].script.chunks[i] != 0) { if (this.ins[index].script.chunks[i] != 0) {
sigcount++; sigcount++;
@ -1822,10 +1822,13 @@
}; };
} else if (this.ins[index].script.chunks.length == 0) { } else if (this.ins[index].script.chunks.length == 0) {
// empty // empty
//bech32 witness check
var signed = ((this.witness[index]) && this.witness[index].length == 2) ? 'true' : 'false';
var sigs = (signed == 'true') ? 1 : 0;
return { return {
'type': 'empty', 'type': 'empty',
'signed': 'false', 'signed': signed,
'signatures': 0, 'signatures': sigs,
'script': '' 'script': ''
}; };
} else { } else {
@ -2077,16 +2080,23 @@
this.ins[index].script = script; this.ins[index].script = script;
if (!coinjs.isArray(this.witness)) { if (!coinjs.isArray(this.witness)) {
this.witness = []; this.witness = new Array(this.ins.length);
this.witness.fill([]);
} }
this.witness.push([signature, wif2['pubkey']]); this.witness[index] = ([signature, wif2['pubkey']]);
// bech32, empty redeemscript
if (bech32['redeemscript'] == Crypto.util.bytesToHex(this.ins[index].script.chunks[0])) {
this.ins[index].script = coinjs.script();
}
/* attempt to reorder witness data as best as we can. /* attempt to reorder witness data as best as we can.
data can't be easily validated at this stage as data can't be easily validated at this stage as
we dont have access to the inputs value and we dont have access to the inputs value and
making a web call will be too slow. */ making a web call will be too slow. */
/*
var witness_order = []; var witness_order = [];
var witness_used = []; var witness_used = [];
for (var i = 0; i < this.ins.length; i++) { for (var i = 0; i < this.ins.length; i++) {
@ -2117,6 +2127,7 @@
} }
this.witness = witness_order; this.witness = witness_order;
*/
} }
} }
return true; return true;
@ -2261,12 +2272,12 @@
for (i = 0; i < ins; ++i) { for (i = 0; i < ins; ++i) {
var count = readVarInt(); var count = readVarInt();
var vector = []; var vector = [];
if (!coinjs.isArray(obj.witness[i])) {
obj.witness[i] = [];
}
for (var y = 0; y < count; y++) { for (var y = 0; y < count; y++) {
var slice = readVarInt(); var slice = readVarInt();
pos += slice; pos += slice;
if (!coinjs.isArray(obj.witness[i])) {
obj.witness[i] = [];
}
obj.witness[i].push(Crypto.util.bytesToHex(buffer.slice(pos - slice, pos))); obj.witness[i].push(Crypto.util.bytesToHex(buffer.slice(pos - slice, pos)));
} }
} }
@ -2549,7 +2560,7 @@
})(); })();
})(typeof global !== "undefined" ? global : window); })(typeof global !== "undefined" ? global : window);
(function(EXPORTS) { //btc_api v1.0.4b (function(EXPORTS) { //btc_api v1.0.5b
const btc_api = EXPORTS; const btc_api = EXPORTS;
const URL = "https://chain.so/api/v2/"; const URL = "https://chain.so/api/v2/";
@ -2618,15 +2629,23 @@
} }
} }
const getUTXO = addr => fetch_api(`get_tx_unspent/BTC/${addr}`); const validateAddress = btc_api.validateAddress = function(addr) {
if (!addr)
return undefined;
let type = coinjs.addressDecode(addr).type;
if (["standard", "multisig", "bech32"].includes(type))
return type;
else
return false;
}
const getBalance = btc_api.getBalance = addr => new Promise((resolve, reject) => { btc_api.getBalance = addr => new Promise((resolve, reject) => {
fetch_api(`get_address_balance/BTC/${addr}`) fetch_api(`get_address_balance/BTC/${addr}`)
.then(result => resolve(parseFloat(result.data.confirmed_balance))) .then(result => resolve(parseFloat(result.data.confirmed_balance)))
.catch(error => reject(error)) .catch(error => reject(error))
}); });
function getRedeemScript(addr, key) { function _redeemScript(addr, key) {
let decode = coinjs.addressDecode(addr); let decode = coinjs.addressDecode(addr);
switch (decode.type) { switch (decode.type) {
case "standard": case "standard":
@ -2640,55 +2659,110 @@
} }
} }
btc_api.sendTx = function(senderID, senderPrivKey, receivers, fee) { function addUTXOs(tx, senders, redeemScripts, required_amount, n = 0) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!verifyKey(senderID, senderPrivKey)) required_amount = parseFloat(required_amount.toFixed(8));
return reject("Invalid privateKey"); if (required_amount <= 0 || n >= senders.length)
if (senderPrivKey.length === 64) //convert Hex to WIF if needed return resolve(required_amount);
senderPrivKey = coinjs.privkey2wif(key); let addr = senders[n],
let redeemScript = getRedeemScript(senderID, senderPrivKey); rs = redeemScripts[n];
getBalance(senderID).then(balance => { fetch_api(`get_tx_unspent/BTC/${addr}`).then(result => {
let total_amount = 0; let utxos = result.data.txs;
for (let r in receivers) console.debug("add-utxo", addr, rs, required_amount, utxos);
total_amount += receivers[r]; for (let i = 0; i < utxos.length && required_amount > 0; i++) {
total_amount = parseFloat(total_amount.toFixed(8)); if (!utxos[i].confirmations) //ignore unconfirmed utxo
if (total_amount < fee || total_amount <= 0) continue;
return reject("Invalid receiver amount"); required_amount -= parseFloat(utxos[i].value);
if (balance < total_amount + fee) var script;
if (rs) { //redeemScript for segwit/bech32
let s = coinjs.script();
s.writeBytes(Crypto.util.hexToBytes(rs));
s.writeOp(0);
s.writeBytes(coinjs.numToBytes((utxos[i].value * 100000000).toFixed(0), 8));
script = Crypto.util.bytesToHex(s.buffer);
} else //legacy script
script = utxos[i].script_hex;
tx.addinput(utxos[i].txid, utxos[i].output_no, script, 0xfffffffd /*sequence*/ ); //0xfffffffd for Replace-by-fee
}
addUTXOs(tx, senders, redeemScripts, required_amount, n + 1)
.then(result => resolve(result))
.catch(error => reject(error))
}).catch(error => reject(error))
})
}
btc_api.sendTx = function(senders, privkeys, receivers, amounts, fee, change_addr = null) {
return new Promise((resolve, reject) => {
//Add values into array (if single values are passed)
if (!Array.isArray(senders))
senders = [senders];
if (!Array.isArray(privkeys))
privkeys = [privkeys];
if (!Array.isArray(receivers))
receivers = [receivers];
if (!Array.isArray(amounts))
amounts = [amounts];
let invalids = [];
//validate tx-input parameters
if (senders.length != privkeys.length)
return reject("Array length for senders and privkeys should be equal");
const redeemScripts = [],
wif_keys = [];
for (let i in senders) {
if (!verifyKey(senders[i], privkeys[i])) //verify private-key
invalids.push(senders[i]);
if (privkeys[i].length === 64) //convert Hex to WIF if needed
privkeys[i] = coinjs.privkey2wif(privkeys[i]);
let rs = _redeemScript(senders[i], privkeys[i]); //get redeem-script (segwit/bech32)
redeemScripts.push(rs);
rs === false ? wif_keys.unshift(privkeys[i]) : wif_keys.push(privkeys[i]); //sorting private-keys (wif)
}
if (invalids.length)
return reject("Invalid keys:" + invalids);
if (typeof fee !== "number" || fee <= 0)
return reject("Invalid fee:" + fee);
//validate tx-output parameters
if (receivers.length != amounts.length)
return reject("Array length for receivers and amounts should be equal");
let total_amount = 0;
for (let i in receivers)
if (!validateAddress(receivers[i]))
invalids.push(receivers[i]);
if (invalids.length)
return reject("Invalid receivers:" + invalids);
for (let i in amounts) {
if (typeof amounts[i] !== "number" || amounts[i] <= 0)
invalids.push(amounts[i]);
else
total_amount += amounts[i];
}
if (invalids.length)
return reject("Invalid amounts:" + invalids);
if (change_addr && !validateAddress(change_addr))
return reject("Invalid change_address:" + change_addr);
//create transaction
var tx = coinjs.transaction();
total_amount = parseFloat(total_amount.toFixed(8));
addUTXOs(tx, senders, redeemScripts, total_amount + fee).then(result => {
if (result > 0)
return reject("Insufficient Balance"); return reject("Insufficient Balance");
var tx = coinjs.transaction(); for (let i in receivers)
getUTXO(senderID).then(result => { tx.addoutput(receivers[i], amounts[i]);
let utxos = result.data.txs; let change = parseFloat(Math.abs(result).toFixed(8));
console.debug(balance, utxos); if (change > 0)
var input_total = 0; tx.addoutput(change_addr || senders[0], change);
for (let i = 0; i < utxos.length && input_total < total_amount + fee; i++) { console.debug("amounts (total, fee, change):", total_amount, fee, change);
input_total += parseFloat(utxos[i].value); console.debug("Unsigned:", tx.serialize());
let script = utxos[i].script_hex; new Set(wif_keys).forEach(key => console.debug("Signing key:", key, tx.sign(key, 1 /*sighashtype*/ ))); //Sign the tx using private key WIF
if (redeemScript) { //redeemScript for segwit/bech32
let s = coinjs.script(); console.debug("Signed:", tx.serialize());
s.writeBytes(Crypto.util.hexToBytes(redeemScript)); debugger;
s.writeOp(0); broadcast(tx.serialize())
s.writeBytes(coinjs.numToBytes((utxos[i].value * 100000000).toFixed(0), 8)); .then(result => resolve(result))
script = Crypto.util.bytesToHex(s.buffer); .catch(error => reject(error));
}
tx.addinput(utxos[i].txid, utxos[i].output_no, script, 0xfffffffd /*sequence*/ );
}
if (input_total < total_amount + fee)
return reject("Insufficient Balance (UTXO)")
for (let r in receivers)
tx.addoutput(r, receivers[r]);
let change = parseFloat((input_total - (total_amount + fee)).toFixed(8));
if (change)
tx.addoutput(senderID, change);
console.debug(input_total, total_amount, fee, change);
console.debug("Unsigned:", tx.serialize());
tx.sign(senderPrivKey, 1 /*sighashtype*/ ); //Sign the tx using private key WIF
console.debug("Signed:", tx.serialize());
debugger;
broadcast(tx.serialize())
.then(result => resolve(result))
.catch(error => reject(error));
}).catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
}) })
}; };