diff --git a/index.html b/index.html index b93c2f4..393786c 100644 --- a/index.html +++ b/index.html @@ -882,7 +882,7 @@ } } - window.addEventListener('hashchange', e => showPage(window.location.hash)) + window.addEventListener('hashchange', e => routeTo(window.location.hash)) window.addEventListener("load", () => { document.body.classList.remove('hidden') document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex) @@ -937,8 +937,8 @@ params: {} } - async function showPage(targetPage, options = {}) { - const { firstLoad, hashChange, isPreview } = options + async function routeTo(targetPage, options = {}) { + const { firstLoad, hashChange, isPreview, redirected } = options let pageId let params = {} let searchParams @@ -968,6 +968,9 @@ if (isPreview) { floGlobals.preview = location.hash; } + if (redirected) { + floGlobals.redirectToArticle = location.hash + } if (typeof floGlobals.myFloID === "undefined" && !(['sign_up', 'sign_in', 'loading', 'landing'].includes(pageId))) return if (searchParams) { const urlSearchParams = new URLSearchParams('?' + searchParams); @@ -986,6 +989,7 @@ floCloudAPI.requestGeneralData(`${params.articleID}_gd`) ]) } + console.log(params) switch (pageId) { case 'landing': targetPage = 'landing' @@ -2721,19 +2725,19 @@ function getSignedIn() { return new Promise((resolve, reject) => { if (window.location.hash.includes('sign_in') || window.location.hash.includes('sign_up')) { - showPage(window.location.hash) + routeTo(window.location.hash) } else { - showPage('landing', { isPreview: location.hash.includes('preview') }) + routeTo('landing', { isPreview: location.hash.includes('preview'), redirected: location.hash.includes('articleID') }) } getRef('sign_in_button').onclick = () => { resolve(getRef('private_key_field').value.trim()) getRef('private_key_field').value = '' - showPage('loading') + routeTo('loading') } getRef('sign_up_button').onclick = () => { resolve(getRef('keys_generator').keys.privKey); getRef('keys_generator').clearKeys(); - showPage('loading') + routeTo('loading') } }) } @@ -2824,10 +2828,10 @@ ` if (window.location.hash.includes('sign_in') || window.location.hash.includes('sign_up')) { - const hash = floGlobals.preview ? floGlobals.preview : ' ' + const hash = floGlobals.preview || floGlobals.redirectToArticle || ' ' history.replaceState(null, null, hash) } - showPage(window.location.hash, { firstLoad: true }) + routeTo(window.location.hash, { firstLoad: true }) console.log(result) }).catch(error => console.error(error)) } diff --git a/scripts/btcOperator.js b/scripts/btcOperator.js index c81f2e2..ff86fb9 100644 --- a/scripts/btcOperator.js +++ b/scripts/btcOperator.js @@ -1,29 +1,40 @@ -(function (EXPORTS) { //btcOperator v1.0.14b +(function (EXPORTS) { //btcOperator v1.1.2a /* BTC Crypto and API Operator */ const btcOperator = EXPORTS; //This library uses API provided by chain.so (https://chain.so/) - const URL = "https://chain.so/api/v2/"; + const URL = "https://blockchain.info/"; - const fetch_api = btcOperator.fetch = function (api) { + const fetch_api = btcOperator.fetch = function (api, json_res = true) { return new Promise((resolve, reject) => { console.debug(URL + api); fetch(URL + api).then(response => { - response.json() - .then(result => result.status === "success" ? resolve(result) : reject(result)) - .catch(error => reject(error)) + if (response.ok) { + (json_res ? response.json() : response.text()) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + response.json() + .then(result => reject(result)) + .catch(error => reject(error)) + } }).catch(error => reject(error)) }) }; const SATOSHI_IN_BTC = 1e8; + const util = btcOperator.util = {}; + + util.Sat_to_BTC = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8)); + util.BTC_to_Sat = value => parseInt(value * SATOSHI_IN_BTC); + function get_fee_rate() { return new Promise((resolve, reject) => { fetch('https://api.blockchain.info/mempool/fees').then(response => { if (response.ok) response.json() - .then(result => resolve(parseFloat((result.regular / SATOSHI_IN_BTC).toFixed(8)))) + .then(result => resolve(util.Sat_to_BTC(result.regular))) .catch(error => reject(error)); else reject(response); @@ -119,50 +130,84 @@ return coinjs.pubkeys2MultisigAddress(pubKeys, minRequired); } + btcOperator.decodeRedeemScript = function (redeemScript, bech32 = true) { + let script = coinjs.script(); + let decoded = (bech32) ? + script.decodeRedeemScriptBech32(redeemScript) : + script.decodeRedeemScript(redeemScript); + if (!decoded) + return null; + return { + address: decoded.address, + pubKeys: decoded.pubkeys, + redeemScript: decoded.redeemscript, + required: decoded.signaturesRequired + } + + } + //convert from one blockchain to another blockchain (target version) btcOperator.convert = {}; btcOperator.convert.wif = function (source_wif, target_version = coinjs.priv) { - let keyHex = decodeLegacy(source_wif).hex; + let keyHex = util.decodeLegacy(source_wif).hex; if (!keyHex || keyHex.length < 66 || !/01$/.test(keyHex)) return null; else - return encodeLegacy(keyHex, target_version); + return util.encodeLegacy(keyHex, target_version); } btcOperator.convert.legacy2legacy = function (source_addr, target_version = coinjs.pub) { - let rawHex = decodeLegacy(source_addr).hex; + let rawHex = util.decodeLegacy(source_addr).hex; if (!rawHex) return null; else - return encodeLegacy(rawHex, target_version); + return util.encodeLegacy(rawHex, target_version); } btcOperator.convert.legacy2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) { - let rawHex = decodeLegacy(source_addr).hex; + let rawHex = util.decodeLegacy(source_addr).hex; if (!rawHex) return null; else - return encodeBech32(rawHex, target_version, target_hrp); + return util.encodeBech32(rawHex, target_version, target_hrp); } btcOperator.convert.bech2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) { - let rawHex = decodeBech32(source_addr).hex; + let rawHex = util.decodeBech32(source_addr).hex; if (!rawHex) return null; else - return encodeBech32(rawHex, target_version, target_hrp); + return util.encodeBech32(rawHex, target_version, target_hrp); } btcOperator.convert.bech2legacy = function (source_addr, target_version = coinjs.pub) { - let rawHex = decodeBech32(source_addr).hex; + let rawHex = util.decodeBech32(source_addr).hex; if (!rawHex) return null; else - return encodeLegacy(rawHex, target_version); + return util.encodeLegacy(rawHex, target_version); } - function decodeLegacy(source) { + btcOperator.convert.multisig2multisig = function (source_addr, target_version = coinjs.multisig) { + let rawHex = util.decodeLegacy(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeLegacy(rawHex, target_version); + } + + btcOperator.convert.bech2multisig = function (source_addr, target_version = coinjs.multisig) { + let rawHex = util.decodeBech32(source_addr).hex; + if (!rawHex) + return null; + else { + rawHex = Crypto.util.bytesToHex(ripemd160(Crypto.util.hexToBytes(rawHex), { asBytes: true })); + return util.encodeLegacy(rawHex, target_version); + } + } + + util.decodeLegacy = function (source) { var decode = coinjs.base58decode(source); var raw = decode.slice(0, decode.length - 4), checksum = decode.slice(decode.length - 4); @@ -172,7 +217,7 @@ asBytes: true }); if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3]) - return null; + return false; let version = raw.shift(); return { version: version, @@ -180,7 +225,7 @@ } } - function encodeLegacy(hex, version) { + util.encodeLegacy = function (hex, version) { var bytes = Crypto.util.hexToBytes(hex); bytes.unshift(version); var hash = Crypto.SHA256(Crypto.SHA256(bytes, { @@ -192,10 +237,10 @@ return coinjs.base58encode(bytes.concat(checksum)); } - function decodeBech32(source) { + util.decodeBech32 = function (source) { let decode = coinjs.bech32_decode(source); if (!decode) - return null; + return false; var raw = decode.data; let version = raw.shift(); raw = coinjs.bech32_convert(raw, 5, 8, false); @@ -206,7 +251,7 @@ } } - function encodeBech32(hex, version, hrp) { + util.encodeBech32 = function (hex, version, hrp) { var bytes = Crypto.util.hexToBytes(hex); bytes = coinjs.bech32_convert(bytes, 8, 5, true); bytes.unshift(version) @@ -216,8 +261,8 @@ //BTC blockchain APIs btcOperator.getBalance = addr => new Promise((resolve, reject) => { - fetch_api(`get_address_balance/BTC/${addr}`) - .then(result => resolve(parseFloat(result.data.confirmed_balance))) + fetch_api(`q/addressbalance/${addr}`) + .then(result => resolve(util.Sat_to_BTC(result))) .catch(error => reject(error)) }); @@ -308,7 +353,7 @@ parameters.privkeys[i] = coinjs.privkey2wif(key); }); if (invalids.length) - throw "Invalid keys:" + invalids; + throw "Invalid private key for address:" + invalids; } //receiver-ids (and change-id) if (!Array.isArray(parameters.receivers)) @@ -339,9 +384,9 @@ let output_size = addOutputs(tx, receivers, amounts, change_address); addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver).then(result => { if (result.change_amount > 0 && result.change_amount > result.fee) //add change amount if any (ignore dust change) - tx.outs[tx.outs.length - 1].value = parseInt(result.change_amount * SATOSHI_IN_BTC); //values are in satoshi + tx.outs[tx.outs.length - 1].value = util.BTC_to_Sat(result.change_amount); //values are in satoshi if (fee_from_receiver) { //deduce fee from receivers if fee_from_receiver - let fee_remaining = parseInt(result.fee * SATOSHI_IN_BTC); + let fee_remaining = util.BTC_to_Sat(result.fee); for (let i = 0; i < tx.outs.length - 1 && fee_remaining > 0; i++) { if (fee_remaining < tx.outs[i].value) { tx.outs[i].value -= fee_remaining; @@ -409,29 +454,29 @@ rs = redeemScripts[rec_args.n]; let addr_type = coinjs.addressDecode(addr).type; let size_per_input = _sizePerInput(addr, rs); - fetch_api(`get_tx_unspent/BTC/${addr}`).then(result => { - let utxos = result.data.txs; + fetch_api(`unspent?active=${addr}`).then(result => { + let utxos = result.unspent_outputs; console.debug("add-utxo", addr, rs, required_amount, utxos); for (let i = 0; i < utxos.length && required_amount > 0; i++) { if (!utxos[i].confirmations) //ignore unconfirmed utxo continue; var script; if (!rs || !rs.length) //legacy script - script = utxos[i].script_hex; + script = utxos[i].script; else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi)) || addr_type === 'multisigBech32') { //redeemScript for segwit/bech32 and multisig (bech32) let s = coinjs.script(); s.writeBytes(Crypto.util.hexToBytes(rs)); s.writeOp(0); - s.writeBytes(coinjs.numToBytes((utxos[i].value * SATOSHI_IN_BTC).toFixed(0), 8)); + s.writeBytes(coinjs.numToBytes(utxos[i].value.toFixed(0), 8)); script = Crypto.util.bytesToHex(s.buffer); } else //redeemScript for multisig (segwit) script = rs; - tx.addinput(utxos[i].txid, utxos[i].output_no, script, 0xfffffffd /*sequence*/); //0xfffffffd for Replace-by-fee + tx.addinput(utxos[i].tx_hash_big_endian, utxos[i].tx_output_n, 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); + rec_args.input_amount += util.Sat_to_BTC(utxos[i].value); + required_amount -= util.Sat_to_BTC(utxos[i].value); if (fee_rate) //automatic fee calculation (dynamic) required_amount += size_per_input * fee_rate; } @@ -658,20 +703,23 @@ btcOperator.checkIfSameTx = function (tx1, tx2) { tx1 = deserializeTx(tx1); tx2 = deserializeTx(tx2); + //compare input and output length if (tx1.ins.length !== tx2.ins.length || tx1.outs.length !== tx2.outs.length) return false; + //compare inputs for (let i = 0; i < tx1.ins.length; i++) if (tx1.ins[i].outpoint.hash !== tx2.ins[i].outpoint.hash || tx1.ins[i].outpoint.index !== tx2.ins[i].outpoint.index) return false; - for (let i = 0; i < tx2.ins.length; i++) + //compare outputs + for (let i = 0; i < tx1.outs.length; i++) if (tx1.outs[i].value !== tx2.outs[i].value || Crypto.util.bytesToHex(tx1.outs[i].script.buffer) !== Crypto.util.bytesToHex(tx2.outs[i].script.buffer)) return false; return true; } const getTxOutput = (txid, i) => new Promise((resolve, reject) => { - fetch_api(`get_tx_outputs/BTC/${txid}/${i}`) - .then(result => resolve(result.data.outputs)) + fetch_api(`rawtx/${txid}`) + .then(result => resolve(result.out[i])) .catch(error => reject(error)) }); @@ -685,8 +733,8 @@ promises.push(getTxOutput(tx.ins[i].outpoint.hash, tx.ins[i].outpoint.index)); Promise.all(promises).then(inputs => { result.inputs = inputs.map(inp => Object({ - address: inp.address, - value: parseFloat(inp.value) + address: inp.addr, + value: util.Sat_to_BTC(inp.value) })); let signed = checkSigned(tx, false); result.inputs.forEach((inp, i) => inp.signed = signed[i]); @@ -695,17 +743,17 @@ var address; switch (out.script.chunks[0]) { case 0: //bech32, multisig-bech32 - address = encodeBech32(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.bech32.version, coinjs.bech32.hrp); + address = util.encodeBech32(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.bech32.version, coinjs.bech32.hrp); break; case 169: //segwit, multisig-segwit - address = encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.multisig); + address = util.encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.multisig); break; case 118: //legacy - address = encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[2]), coinjs.pub); + address = util.encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[2]), coinjs.pub); } return { address, - value: parseFloat(out.value / SATOSHI_IN_BTC) + value: util.Sat_to_BTC(out.value) } }); //Parse Totals @@ -726,22 +774,115 @@ return Crypto.util.bytesToHex(txid); } - btcOperator.getTx = txid => new Promise((resolve, reject) => { - fetch_api(`get_tx/BTC/${txid}`) - .then(result => resolve(result.data)) + const getLatestBlock = btcOperator.getLatestBlock = () => new Promise((resolve, reject) => { + fetch_api(`q/getblockcount`) + .then(result => resolve(result)) .catch(error => reject(error)) + }) + + btcOperator.getTx = txid => new Promise((resolve, reject) => { + fetch_api(`rawtx/${txid}`).then(result => { + getLatestBlock().then(latest_block => resolve({ + block: result.block_height, + txid: result.hash, + time: result.time * 1000, + confirmations: result.block_height === null ? 0 : latest_block - result.block_height, //calculate confirmations using latest block number as api doesnt relay it + size: result.size, + fee: util.Sat_to_BTC(result.fee), + inputs: result.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })), + total_input_value: util.Sat_to_BTC(result.inputs.reduce((a, i) => a + i.prev_out.value, 0)), + outputs: result.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })), + total_output_value: util.Sat_to_BTC(result.out.reduce((a, o) => a += o.value, 0)), + })) + }).catch(error => reject(error)) }); - btcOperator.getAddressData = addr => new Promise((resolve, reject) => { - fetch_api(`address/BTC/${addr}`) - .then(result => resolve(result.data)) + btcOperator.getTx.hex = txid => new Promise((resolve, reject) => { + fetch_api(`rawtx/${txid}?format=hex`, false) + .then(result => resolve(result)) .catch(error => reject(error)) + }) + + btcOperator.getAddressData = address => new Promise((resolve, reject) => { + fetch_api(`rawaddr/${address}`).then(data => { + let details = {}; + details.balance = util.Sat_to_BTC(data.final_balance); + details.address = data.address; + details.txs = data.txs.map(tx => { + let d = { + txid: tx.hash, + time: tx.time * 1000, //s to ms + block: tx.block_height, + } + //sender list + d.tx_senders = {}; + tx.inputs.forEach(i => { + if (i.prev_out.addr in d.tx_senders) + d.tx_senders[i.prev_out.addr] += i.prev_out.value; + else d.tx_senders[i.prev_out.addr] = i.prev_out.value; + }); + d.tx_input_value = 0; + for (let s in d.tx_senders) { + let val = d.tx_senders[s]; + d.tx_senders[s] = util.Sat_to_BTC(val); + d.tx_input_value += val; + } + d.tx_input_value = util.Sat_to_BTC(d.tx_input_value); + //receiver list + d.tx_receivers = {}; + tx.out.forEach(o => { + if (o.addr in d.tx_receivers) + d.tx_receivers[o.addr] += o.value; + else d.tx_receivers[o.addr] = o.value; + }); + d.tx_output_value = 0; + for (let r in d.tx_receivers) { + let val = d.tx_receivers[r]; + d.tx_receivers[r] = util.Sat_to_BTC(val); + d.tx_output_value += val; + } + d.tx_output_value = util.Sat_to_BTC(d.tx_output_value); + d.tx_fee = util.Sat_to_BTC(tx.fee); + //tx type + if (tx.result > 0) { //net > 0, balance inc => type=in + d.type = "in"; + d.amount = util.Sat_to_BTC(tx.result); + d.sender = Object.keys(d.tx_senders).filter(s => s !== address); + } else if (Object.keys(d.tx_receivers).some(r => r !== address)) { //net < 0, balance dec & receiver present => type=out + d.type = "out"; + d.amount = util.Sat_to_BTC(tx.result * -1); + d.receiver = Object.keys(d.tx_receivers).filter(r => r !== address); + d.fee = d.tx_fee; + } else { //net < 0 (fee) & no other id in receiver list => type=self + d.type = "self"; + d.amount = d.tx_receivers[address]; + d.address = address + } + return d; + }) + resolve(details); + }).catch(error => reject(error)) }); btcOperator.getBlock = block => new Promise((resolve, reject) => { - fetch_api(`get_block/BTC/${block}`) - .then(result => resolve(result.data)) - .catch(error => reject(error)) + fetch_api(`rawblock/${block}`).then(result => resolve({ + height: result.height, + hash: result.hash, + merkle_root: result.mrkl_root, + prev_block: result.prev_block, + next_block: result.next_block[0], + size: result.size, + time: result.time * 1000, //s to ms + txs: result.tx.map(t => Object({ + fee: t.fee, + size: t.size, + inputs: t.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })), + total_input_value: util.Sat_to_BTC(t.inputs.reduce((a, i) => a + i.prev_out.value, 0)), + outputs: t.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })), + total_output_value: util.Sat_to_BTC(t.out.reduce((a, o) => a += o.value, 0)), + })) + + })).catch(error => reject(error)) }); -})('object' === typeof module ? module.exports : window.btcOperator = {}); \ No newline at end of file +})('object' === typeof module ? module.exports : window.btcOperator = {}); diff --git a/scripts/floBlockchainAPI.js b/scripts/floBlockchainAPI.js index ab778fe..35f1e0a 100644 --- a/scripts/floBlockchainAPI.js +++ b/scripts/floBlockchainAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floBlockchainAPI v2.3.3e +(function (EXPORTS) { //floBlockchainAPI v2.4.3 /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ 'use strict'; const floBlockchainAPI = EXPORTS; @@ -15,6 +15,13 @@ receiverID: floGlobals.adminID }; + const SATOSHI_IN_BTC = 1e8; + + const util = floBlockchainAPI.util = {}; + + util.Sat_to_FLO = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8)); + util.FLO_to_Sat = value => parseInt(value * SATOSHI_IN_BTC); + Object.defineProperties(floBlockchainAPI, { sendAmt: { get: () => DEFAULT.sendAmt, @@ -121,17 +128,15 @@ }); } - //Send Tx to blockchain - const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) { + //create a transaction with single sender + const createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) { return new Promise((resolve, reject) => { if (!floCrypto.validateASCII(floData)) return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); - else if (!floCrypto.validateFloID(senderAddr)) + else if (!floCrypto.validateFloID(senderAddr, true)) return reject(`Invalid address : ${senderAddr}`); else if (!floCrypto.validateFloID(receiverAddr)) return reject(`Invalid address : ${receiverAddr}`); - else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr)) - return reject("Invalid Private key!"); else if (typeof sendAmt !== 'number' || sendAmt <= 0) return reject(`Invalid sendAmt : ${sendAmt}`); @@ -175,15 +180,36 @@ if (change > DEFAULT.minChangeAmt) trx.addoutput(senderAddr, change); trx.addflodata(floData.replace(/\n/g, ' ')); - var signedTxHash = trx.sign(privKey, 1); - broadcastTx(signedTxHash) - .then(txid => resolve(txid)) - .catch(error => reject(error)) + resolve(trx); } }).catch(error => reject(error)) }).catch(error => reject(error)) }).catch(error => reject(error)) }).catch(error => reject(error)) + }) + } + + floBlockchainAPI.createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + createTx(senderAddr, receiverAddr, sendAmt, floData, strict_utxo) + .then(trx => resolve(trx.serialize())) + .catch(error => reject(error)) + }) + } + + //Send Tx to blockchain + const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(senderAddr, true)) + return reject(`Invalid address : ${senderAddr}`); + else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr)) + return reject("Invalid Private key!"); + createTx(senderAddr, receiverAddr, sendAmt, floData, strict_utxo).then(trx => { + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) }); } @@ -203,7 +229,7 @@ //merge all UTXOs of a given floID into a single UTXO floBlockchainAPI.mergeUTXOs = function (floID, privKey, floData = '') { return new Promise((resolve, reject) => { - if (!floCrypto.validateFloID(floID)) + if (!floCrypto.validateFloID(floID, true)) return reject(`Invalid floID`); if (!floCrypto.verifyPrivKey(privKey, floID)) return reject("Invalid Private Key"); @@ -381,7 +407,6 @@ for (let floID in senders) promises.push(promisedAPI(`api/addr/${floID}/utxo`)); Promise.all(promises).then(results => { - let wifSeq = []; var trx = bitjs.transaction(); for (let floID in senders) { let utxos = results.shift(); @@ -391,13 +416,11 @@ sendAmt = totalSendAmt * ratio; } else sendAmt = senders[floID].coins + dividedFee; - let wif = senders[floID].wif; let utxoAmt = 0.0; for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < sendAmt); i--) { if (utxos[i].confirmations) { trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); - wifSeq.push(wif); utxoAmt += utxos[i].amount; } } @@ -410,8 +433,8 @@ for (let floID in receivers) trx.addoutput(floID, receivers[floID]); trx.addflodata(floData.replace(/\n/g, ' ')); - for (let i = 0; i < wifSeq.length; i++) - trx.signinput(i, wifSeq[i], 1); + for (let floID in senders) + trx.sign(senders[floID].wif, 1); var signedTxHash = trx.serialize(); broadcastTx(signedTxHash) .then(txid => resolve(txid)) @@ -421,6 +444,273 @@ }) } + //Create a multisig transaction + const createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + var multisig = floCrypto.decodeRedeemScript(redeemScript); + + //validate multisig script and flodata + if (!multisig) + return reject(`Invalid redeemScript`); + var senderAddr = multisig.address; + if (!floCrypto.validateFloID(senderAddr)) + return reject(`Invalid multisig : ${senderAddr}`); + else if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + //validate receiver addresses + if (!Array.isArray(receivers)) + receivers = [receivers]; + for (let r of receivers) + if (!floCrypto.validateFloID(r)) + return reject(`Invalid address : ${r}`); + //validate amounts + if (!Array.isArray(amounts)) + amounts = [amounts]; + if (amounts.length != receivers.length) + return reject("Receivers and amounts have different length"); + var sendAmt = 0; + for (let a of amounts) { + if (typeof a !== 'number' || a <= 0) + return reject(`Invalid amount : ${a}`); + sendAmt += a; + } + + getBalance(senderAddr).then(balance => { + var fee = DEFAULT.fee; + if (balance < sendAmt + fee) + return reject("Insufficient FLO balance!"); + //get unconfirmed tx list + promisedAPI(`api/addr/${senderAddr}`).then(result => { + readTxs(senderAddr, 0, result.unconfirmedTxApperances).then(result => { + let unconfirmedSpent = {}; + for (let tx of result.items) + if (tx.confirmations == 0) + for (let vin of tx.vin) + if (vin.addr === senderAddr) { + if (Array.isArray(unconfirmedSpent[vin.txid])) + unconfirmedSpent[vin.txid].push(vin.vout); + else + unconfirmedSpent[vin.txid] = [vin.vout]; + } + //get utxos list + promisedAPI(`api/addr/${senderAddr}/utxo`).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (var i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) + continue; //A transaction has already used the utxo, but is unconfirmed. + trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i in receivers) + trx.addoutput(receivers[i], amounts[i]); + var change = utxoAmt - sendAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(senderAddr, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + resolve(trx); + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + + //Same as above, but explict call should return serialized tx-hex + floBlockchainAPI.createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + createMultisigTx(redeemScript, receivers, amounts, floData, strict_utxo) + .then(trx => resolve(trx.serialize())) + .catch(error => reject(error)) + }) + } + + //Create and send multisig transaction + const sendMultisigTx = floBlockchainAPI.sendMultisigTx = function (redeemScript, privateKeys, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + var multisig = floCrypto.decodeRedeemScript(redeemScript); + if (!multisig) + return reject(`Invalid redeemScript`); + if (privateKeys.length < multisig.required) + return reject(`Insufficient privateKeys (required ${multisig.required})`); + for (let pk of privateKeys) { + var flag = false; + for (let pub of multisig.pubkeys) + if (floCrypto.verifyPrivKey(pk, pub, false)) + flag = true; + if (!flag) + return reject(`Invalid Private key`); + } + createMultisigTx(redeemScript, receivers, amounts, floData, strict_utxo).then(trx => { + for (let pk of privateKeys) + trx.sign(pk, 1); + var signedTxHash = trx.serialize(); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + floBlockchainAPI.writeMultisigData = function (redeemScript, data, privatekeys, receiverAddr = DEFAULT.receiverID, options = {}) { + let strict_utxo = options.strict_utxo === false ? false : true, + sendAmt = isNaN(options.sendAmt) ? DEFAULT.sendAmt : options.sendAmt; + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(receiverAddr)) + return reject(`Invalid receiver: ${receiverAddr}`); + sendMultisigTx(redeemScript, privatekeys, receiverAddr, sendAmt, data, strict_utxo) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }) + } + + function deserializeTx(tx) { + if (typeof tx === 'string' || Array.isArray(tx)) { + try { + tx = bitjs.transaction(tx); + } catch { + throw "Invalid transaction hex"; + } + } else if (typeof tx !== 'object' || typeof tx.sign !== 'function') + throw "Invalid transaction object"; + return tx; + } + + floBlockchainAPI.signTx = function (tx, privateKey, sighashtype = 1) { + if (!floCrypto.getFloID(privateKey)) + throw "Invalid Private key"; + //deserialize if needed + tx = deserializeTx(tx); + var signedTxHex = tx.sign(privateKey, sighashtype); + return signedTxHex; + } + + const checkSigned = floBlockchainAPI.checkSigned = function (tx, bool = true) { + tx = deserializeTx(tx); + let n = []; + for (let i = 0; i < tx.inputs.length; i++) { + var s = tx.scriptDecode(i); + if (s['type'] === 'scriptpubkey') + n.push(s.signed); + else if (s['type'] === 'multisig') { + var rs = tx.decodeRedeemScript(s['rs']); + let x = { + s: 0, + r: rs['required'], + t: rs['pubkeys'].length + }; + //check input script for signatures + var script = Array.from(tx.inputs[i].script); + if (script[0] == 0) { //script with signatures + script = tx.parseScript(script); + for (var k = 0; k < script.length; k++) + if (Array.isArray(script[k]) && script[k][0] == 48) //0x30 DERSequence + x.s++; + } + //validate counts + if (x.r > x.t) + throw "signaturesRequired is more than publicKeys"; + else if (x.s < x.r) + n.push(x); + else + n.push(true); + } + } + return bool ? !(n.filter(x => x !== true).length) : n; + } + + floBlockchainAPI.checkIfSameTx = function (tx1, tx2) { + tx1 = deserializeTx(tx1); + tx2 = deserializeTx(tx2); + //compare input and output length + if (tx1.inputs.length !== tx2.inputs.length || tx1.outputs.length !== tx2.outputs.length) + return false; + //compare flodata + if (tx1.floData !== tx2.floData) + return false + //compare inputs + for (let i = 0; i < tx1.inputs.length; i++) + if (tx1.inputs[i].outpoint.hash !== tx2.inputs[i].outpoint.hash || tx1.inputs[i].outpoint.index !== tx2.inputs[i].outpoint.index) + return false; + //compare outputs + for (let i = 0; i < tx1.outputs.length; i++) + if (tx1.outputs[i].value !== tx2.outputs[i].value || Crypto.util.bytesToHex(tx1.outputs[i].script) !== Crypto.util.bytesToHex(tx2.outputs[i].script)) + return false; + return true; + } + + floBlockchainAPI.transactionID = function (tx) { + tx = deserializeTx(tx); + let clone = bitjs.clone(tx); + let raw_bytes = Crypto.util.hexToBytes(clone.serialize()); + let txid = Crypto.SHA256(Crypto.SHA256(raw_bytes, { asBytes: true }), { asBytes: true }).reverse(); + return Crypto.util.bytesToHex(txid); + } + + const getTxOutput = (txid, i) => new Promise((resolve, reject) => { + fetch_api(`api/tx/${txid}`) + .then(result => resolve(result.vout[i])) + .catch(error => reject(error)) + }); + + function getOutputAddress(outscript) { + var bytes, version; + switch (outscript[0]) { + case 118: //legacy + bytes = outscript.slice(3, outscript.length - 2); + version = bitjs.pub; + break + case 169: //multisig + bytes = outscript.slice(2, outscript.length - 1); + version = bitjs.multisig; + break; + default: return; //unknown + } + bytes.unshift(version); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { asBytes: true }), { asBytes: true }); + var checksum = hash.slice(0, 4); + return bitjs.Base58.encode(bytes.concat(checksum)); + } + + floBlockchainAPI.parseTransaction = function (tx) { + return new Promise((resolve, reject) => { + tx = deserializeTx(tx); + let result = {}; + let promises = []; + //Parse Inputs + for (let i = 0; i < tx.inputs.length; i++) + promises.push(getTxOutput(tx.inputs[i].outpoint.hash, tx.inputs[i].outpoint.index)); + Promise.all(promises).then(inputs => { + result.inputs = inputs.map(inp => Object({ + address: inp.scriptPubKey.addresses[0], + value: parseFloat(inp.value) + })); + let signed = checkSigned(tx, false); + result.inputs.forEach((inp, i) => inp.signed = signed[i]); + //Parse Outputs + result.outputs = tx.outputs.map(out => Object({ + address: getOutputAddress(out.script), + value: util.Sat_to_FLO(out.value) + })) + //Parse Totals + result.total_input = parseFloat(result.inputs.reduce((a, inp) => a += inp.value, 0).toFixed(8)); + result.total_output = parseFloat(result.outputs.reduce((a, out) => a += out.value, 0).toFixed(8)); + result.fee = parseFloat((result.total_input - result.total_output).toFixed(8)); + result.floData = tx.floData; + resolve(result); + }).catch(error => reject(error)) + }) + } + //Broadcast signed Tx in blockchain using API const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) { return new Promise((resolve, reject) => { diff --git a/scripts/floCrypto.js b/scripts/floCrypto.js index 563d77f..ec41da4 100644 --- a/scripts/floCrypto.js +++ b/scripts/floCrypto.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floCrypto v2.3.3e +(function (EXPORTS) { //floCrypto v2.3.5a /* FLO Crypto Operators */ 'use strict'; const floCrypto = EXPORTS; @@ -222,7 +222,7 @@ key.setCompressed(true); if (isfloID && pubKey_floID == key.getBitcoinAddress()) return true; - else if (!isfloID && pubKey_floID == key.getPubKeyHex()) + else if (!isfloID && pubKey_floID.toUpperCase() == key.getPubKeyHex().toUpperCase()) return true; else return false; @@ -231,12 +231,36 @@ } } + floCrypto.getMultisigAddress = function (publicKeyList, requiredSignatures) { + if (!Array.isArray(publicKeyList) || !publicKeyList.length) + return null; + if (!Number.isInteger(requiredSignatures) || requiredSignatures < 1 || requiredSignatures > publicKeyList.length) + return null; + try { + var multisig = bitjs.pubkeys2multisig(publicKeyList, requiredSignatures); + return multisig; + } catch { + return null; + } + } + + floCrypto.decodeRedeemScript = function (redeemScript) { + try { + var decoded = bitjs.transaction().decodeRedeemScript(redeemScript); + return decoded; + } catch { + return null; + } + } + //Check if the given flo-id is valid or not - floCrypto.validateFloID = function (floID) { + floCrypto.validateFloID = function (floID, regularOnly = false) { if (!floID) return false; try { let addr = new Bitcoin.Address(floID); + if (regularOnly && addr.version != Bitcoin.Address.standardVersion) + return false; return true; } catch { return false; @@ -266,13 +290,15 @@ return false; } - //Check the public-key for the address (any blockchain) + //Check the public-key (or redeem-script) for the address (any blockchain) floCrypto.verifyPubKey = function (pubKeyHex, address) { - let raw = decodeAddress(address), - pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), { - asBytes: true - }))); - return raw ? pub_hash === raw.hex : false; + let raw = decodeAddress(address); + if (!raw) + return; + let pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), { asBytes: true }))); + if (typeof raw.bech_version !== 'undefined' && raw.bytes.length == 32) //bech32-multisig + raw.hex = Crypto.util.bytesToHex(ripemd160(raw.bytes, { asBytes: true })); + return pub_hash === raw.hex; } //Convert the given address (any blockchain) to equivalent floID @@ -282,7 +308,7 @@ let raw = decodeAddress(address); if (!raw) return; - else if (options) { + else if (options) { //if (optional) version check is passed if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version))) return; if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version))) @@ -297,6 +323,35 @@ return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4))); } + //Convert the given multisig address (any blockchain) to equivalent multisig floID + floCrypto.toMultisigFloID = function (address, options = null) { + if (!address) + return; + let raw = decodeAddress(address); + if (!raw) + return; + else if (options) { //if (optional) version check is passed + if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version))) + return; + if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version))) + return; + } + if (typeof raw.bech_version !== 'undefined') { + if (raw.bytes.length != 32) return; //multisig bech address have 32 bytes + //multisig-bech:hash=SHA256 whereas multisig:hash=r160(SHA265), thus ripemd160 the bytes from multisig-bech + raw.bytes = ripemd160(raw.bytes, { + asBytes: true + }); + } + raw.bytes.unshift(bitjs.multisig); + let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4))); + } + //Checks if the given addresses (any blockchain) are same (w.r.t keys) floCrypto.isSameAddr = function (addr1, addr2) { if (!addr1 || !addr2) @@ -305,8 +360,13 @@ raw2 = decodeAddress(addr2); if (!raw1 || !raw2) return false; - else + else { + if (typeof raw1.bech_version !== 'undefined' && raw1.bytes.length == 32) //bech32-multisig + raw1.hex = Crypto.util.bytesToHex(ripemd160(raw1.bytes, { asBytes: true })); + if (typeof raw2.bech_version !== 'undefined' && raw2.bytes.length == 32) //bech32-multisig + raw2.hex = Crypto.util.bytesToHex(ripemd160(raw2.bytes, { asBytes: true })); return raw1.hex === raw2.hex; + } } const decodeAddress = floCrypto.decodeAddr = function (address) { @@ -326,7 +386,7 @@ hex: Crypto.util.bytesToHex(bytes), bytes } - } else if (address.length == 42) { //bech encoding + } else if (address.length == 42 || address.length == 62) { //bech encoding let decode = coinjs.bech32_decode(address); if (decode) { let bytes = decode.data; diff --git a/scripts/lib.js b/scripts/lib.js index 2234db8..e383403 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -1,4 +1,4 @@ -(function (GLOBAL) { //lib v1.3.2 +(function (GLOBAL) { //lib v1.4.2b 'use strict'; /* Utility Libraries required for Standard operations * All credits for these codes belong to their respective creators, moderators and owners. @@ -4349,20 +4349,18 @@ var bitjs = GLOBAL.bitjs = function () { }; - function ascii_to_hexa(str) { - var arr1 = []; - for (var n = 0, l = str.length; n < l; n++) { - var hex = Number(str.charCodeAt(n)).toString(16); - arr1.push(hex); - } - return arr1.join(''); - } - /* public vars */ bitjs.pub = 0x23; // flochange - changed the prefix to FLO Mainnet PublicKey Prefix 0x23 bitjs.priv = 0xa3; //flochange - changed the prefix to FLO Mainnet Private key prefix 0xa3 + bitjs.multisig = 0x5e; //flochange - prefix for FLO Mainnet Multisig 0x5e bitjs.compressed = false; + if (GLOBAL.cryptocoin == 'FLO_TEST') { + bitjs.pub = 0x73; // flochange - changed the prefix to FLO TestNet PublicKey Prefix 0x73 + bitjs.priv = 0xa3; //flochange - changed the prefix to FLO TestNet Private key prefix 0xa3 + bitjs.multisig = 0xc6; //flochange - prefix for FLO TestNet Multisig 0xc6 + } + /* provide a privkey and return an WIF */ bitjs.privkey2wif = function (h) { var r = Crypto.util.hexToBytes(h); @@ -4461,7 +4459,46 @@ return B58.encode(r.concat(checksum)); } - bitjs.transaction = function () { + /* generate a multisig address from pubkeys and required signatures */ + bitjs.pubkeys2multisig = function (pubkeys, required) { + var s = []; + s.push(80 + required); //OP_1 + for (var i = 0; i < pubkeys.length; ++i) { + let bytes = Crypto.util.hexToBytes(pubkeys[i]); + s.push(bytes.length); + s = s.concat(bytes); + } + s.push(80 + pubkeys.length); //OP_1 + s.push(174); //OP_CHECKMULTISIG + + if (s.length > 520) { // too large + throw Error(`redeemScript size(=${s.length}) too large`) + } + + var x = ripemd160(Crypto.SHA256(s, { + asBytes: true + }), { + asBytes: true + }); + x.unshift(bitjs.multisig); + var r = x; + r = Crypto.SHA256(Crypto.SHA256(r, { + asBytes: true + }), { + asBytes: true + }); + var checksum = r.slice(0, 4); + var redeemScript = Crypto.util.bytesToHex(s); + var address = B58.encode(x.concat(checksum)); + + return { + 'address': address, + 'redeemScript': redeemScript, + 'size': s.length + }; + } + + bitjs.transaction = function (tx_data = undefined) { var btrx = {}; btrx.version = 2; //flochange look at this version btrx.inputs = []; @@ -4476,7 +4513,6 @@ 'hash': txid, 'index': index }; - //o.script = []; Signature and Public Key should be added after singning o.script = Crypto.util.hexToBytes(scriptPubKey); //push previous output pubkey script o.sequence = sequence || ((btrx.locktime == 0) ? 4294967295 : 0); return this.inputs.push(o); @@ -4485,26 +4521,41 @@ btrx.addoutput = function (address, value) { var o = {}; var buf = []; - var addrDecoded = btrx.addressDecode(address); + var addr = this.addressDecode(address); o.value = new BigInteger('' + Math.round((value * 1) * 1e8), 10); - buf.push(118); //OP_DUP - buf.push(169); //OP_HASH160 - buf.push(addrDecoded.length); - buf = buf.concat(addrDecoded); // address in bytes - buf.push(136); //OP_EQUALVERIFY - buf.push(172); // OP_CHECKSIG + + if (addr.version === bitjs.pub) { // regular address + buf.push(118); //OP_DUP + buf.push(169); //OP_HASH160 + buf = this.writeBytesToScriptBuffer(buf, addr.bytes);// address in bytes + buf.push(136); //OP_EQUALVERIFY + buf.push(172); //OP_CHECKSIG + } else if (addr.version === bitjs.multisig) { // multisig address + buf.push(169); //OP_HASH160 + buf = this.writeBytesToScriptBuffer(buf, addr.bytes);// address in bytes + buf.push(135); //OP_EQUAL + } + o.script = buf; return this.outputs.push(o); } + // flochange - Added fn to assign flodata to tx + btrx.addflodata = function (data) { + //checks for valid flo-data string + if (typeof data !== "string") + throw Error("floData should be String"); + if (data.length > 1040) + throw Error("floData Character Limit Exceeded"); + if (bitjs.strToBytes(data).some(c => c < 32 || c > 127)) + throw Error("floData contains Invalid characters (only ASCII characters allowed"); - btrx.addflodata = function (txcomments) { // flochange - this whole function needs to be done - this.floData = txcomments; - return this.floData; //flochange .. returning the txcomments -- check if the function return will assign + this.floData = data; + return this.floData; } - // Only standard addresses + // Only standard addresses (standard multisig supported) btrx.addressDecode = function (address) { var bytes = B58.decode(address); var front = bytes.slice(0, bytes.length - 4); @@ -4515,7 +4566,10 @@ asBytes: true }).slice(0, 4); if (checksum + "" == back + "") { - return front.slice(1); + return { + version: front[0], + bytes: front.slice(1) + }; } } @@ -4740,6 +4794,83 @@ return KBigInt; }; + btrx.writeBytesToScriptBuffer = function (buf, bytes) { + if (bytes.length < 76) { //OP_PUSHDATA1 + buf.push(bytes.length); + } else if (bytes.length <= 0xff) { + buf.push(76); //OP_PUSHDATA1 + buf.push(bytes.length); + } else if (bytes.length <= 0xffff) { + buf.push(77); //OP_PUSHDATA2 + buf.push(bytes.length & 0xff); + buf.push((bytes.length >>> 8) & 0xff); + } else { + buf.push(78); //OP_PUSHDATA4 + buf.push(bytes.length & 0xff); + buf.push((bytes.length >>> 8) & 0xff); + buf.push((bytes.length >>> 16) & 0xff); + buf.push((bytes.length >>> 24) & 0xff); + } + buf = buf.concat(bytes); + return buf; + } + + btrx.parseScript = function (script) { + + var chunks = []; + var i = 0; + + function readChunk(n) { + chunks.push(script.slice(i, i + n)); + i += n; + }; + + while (i < script.length) { + var opcode = script[i++]; + if (opcode >= 0xF0) { + opcode = (opcode << 8) | script[i++]; + } + + var len; + if (opcode > 0 && opcode < 76) { //OP_PUSHDATA1 + readChunk(opcode); + } else if (opcode == 76) { //OP_PUSHDATA1 + len = script[i++]; + readChunk(len); + } else if (opcode == 77) { //OP_PUSHDATA2 + len = (script[i++] << 8) | script[i++]; + readChunk(len); + } else if (opcode == 78) { //OP_PUSHDATA4 + len = (script[i++] << 24) | (script[i++] << 16) | (script[i++] << 8) | script[i++]; + readChunk(len); + } else { + chunks.push(opcode); + } + + if (i < 0x00) { + break; + } + } + + return chunks; + } + + btrx.decodeRedeemScript = function (rs) { + if (typeof rs == "string") + rs = Crypto.util.hexToBytes(rs); + var script = this.parseScript(rs); + if (!(script[0] > 80 && script[script.length - 2] > 80 && script[script.length - 1] == 174)) //OP_CHECKMULTISIG + throw "Invalid RedeemScript"; + var r = {}; + r.required = script[0] - 80; + r.pubkeys = []; + for (var i = 1; i < script.length - 2; i++) + r.pubkeys.push(Crypto.util.bytesToHex(script[i])); + r.address = bitjs.pubkeys2multisig(r.pubkeys, r.required).address; + r.redeemscript = Crypto.util.bytesToHex(rs); + return r; + } + /* sign a "standard" input */ btrx.signinput = function (index, wif, sigHashType) { var key = bitjs.wif2pubkey(wif); @@ -4747,8 +4878,7 @@ var signature = this.transactionSig(index, wif, shType); var buf = []; var sigBytes = Crypto.util.hexToBytes(signature); - buf.push(sigBytes.length); - buf = buf.concat(sigBytes); + buf = this.writeBytesToScriptBuffer(buf, sigBytes); var pubKeyBytes = Crypto.util.hexToBytes(key['pubkey']); buf.push(pubKeyBytes.length); buf = buf.concat(pubKeyBytes); @@ -4756,15 +4886,98 @@ return true; } + /* sign a multisig input */ + btrx.signmultisig = function (index, wif, sigHashType) { + + var script = Array.from(this.inputs[index].script); + var redeemScript, sigsList = []; + + if (script[0] == 0) { //script with signatures + script = this.parseScript(script); + for (var i = 0; i < script.length; i++) { + if (Array.isArray(script[i])) { + if (script[i][0] == 48) //0x30 DERSequence + sigsList.push(script[i]); + else if (script[i][0] >= 80 && script[i][script[i].length - 1] == 174) //OP_CHECKMULTISIG + redeemScript = script[i]; + } + } + } else { //script = redeemscript + redeemScript = script; + } + + var pubkeyList = this.decodeRedeemScript(redeemScript).pubkeys; + var pubkey = bitjs.wif2pubkey(wif)['pubkey']; + if (!pubkeyList.includes(pubkey)) //wif not a part of this multisig + return false; + + pubkeyList = pubkeyList.map(pub => Crypto.util.hexToBytes(bitjs.pubkeydecompress(pub))); //decompress pubkeys + + var shType = sigHashType || 1; + this.inputs[index].script = redeemScript; //script to be signed is redeemscript + var signature = Crypto.util.hexToBytes(this.transactionSig(index, wif, shType)); + sigsList.push(signature); + + var buf = []; + buf.push(0); + + //verify signatures and order them (also remove duplicate sigs) + for (let x in pubkeyList) { + for (let y in sigsList) { + var sighash = Crypto.util.hexToBytes(this.transactionHash(index, sigsList[y].slice(-1)[0] * 1)); + if (bitjs.verifySignature(sighash, sigsList[y], pubkeyList[x])) { + buf = this.writeBytesToScriptBuffer(buf, sigsList[y]); + break; //ensures duplicate sigs from same pubkey are not added + } + } + } + + //append redeemscript + buf = this.writeBytesToScriptBuffer(buf, redeemScript); + + this.inputs[index].script = buf; + return true; + } + /* sign inputs */ btrx.sign = function (wif, sigHashType) { var shType = sigHashType || 1; for (var i = 0; i < this.inputs.length; i++) { - this.signinput(i, wif, shType); + + var decodedScript = this.scriptDecode(i); + + if (decodedScript.type == "scriptpubkey" && decodedScript.signed == false) { //regular + var addr = bitjs.wif2address(wif)["address"];; + if (decodedScript.pubhash == Crypto.util.bytesToHex(this.addressDecode(addr).bytes)) //input belongs to wif + this.signinput(i, wif, shType); + } else if (decodedScript.type == "multisig") { //multisig + this.signmultisig(i, wif, shType); + } } return this.serialize(); } + // function to find type of the script in input + btrx.scriptDecode = function (index) { + var script = this.parseScript(this.inputs[index].script); + if (script.length == 5 && script[script.length - 1] == 172) { + //OP_DUP OP_HASH160 [address bytes] OP_EQUALVERIFY OP_CHECKSIG + // regular scriptPubkey (not signed) + return { type: 'scriptpubkey', signed: false, pubhash: Crypto.util.bytesToHex(script[2]) }; + } else if (script.length == 2 && script[0][0] == 48) { + //[signature] [pubkey] + //(probably) regular signed + return { type: 'scriptpubkey', signed: true }; + } else if (script[0] == 0 && script[script.length - 1][script[script.length - 1].length - 1] == 174) { + //0 [signatues] [redeemscript OP_CHECKMULTISIG] + // multisig with signature + return { type: 'multisig', rs: script[script.length - 1] }; + } else if (script[0] >= 80 && script[script.length - 1] == 174) { + //redeemscript: 80+ [pubkeys] OP_CHECKMULTISIG + // multisig without signature + return { type: 'multisig', rs: Array.from(this.inputs[index].script) }; + } + } /* serialize a transaction */ btrx.serialize = function () { @@ -4793,28 +5006,85 @@ } buffer = buffer.concat(bitjs.numToBytes(parseInt(this.locktime), 4)); - var flohex = ascii_to_hexa(this.floData); - var floDataCount = this.floData.length; - var floDataCountString; - //flochange -- creating unique data character count logic for floData. This string is prefixed before actual floData string in Raw Transaction - if (floDataCount < 16) { - floDataCountString = floDataCount.toString(16); - floDataCountString = "0" + floDataCountString; - } else if (floDataCount < 253) { - floDataCountString = floDataCount.toString(16); - } else if (floDataCount <= 1040) { - let floDataCountAdjusted = (floDataCount - 253) + parseInt("0xfd00fd"); - let floDataCountStringAdjusted = floDataCountAdjusted.toString(16); - floDataCountString = floDataCountStringAdjusted.substr(0, 2) + floDataCountStringAdjusted.substr(4, 2) + floDataCountStringAdjusted.substr(2, 2); - } else { - floDataCountString = "Character Limit Exceeded"; - } + //flochange -- append floData field + buffer = buffer.concat(bitjs.numToVarInt(this.floData.length)); + buffer = buffer.concat(bitjs.strToBytes(this.floData)) - return Crypto.util.bytesToHex(buffer) + floDataCountString + flohex; // flochange -- Addition of floDataCountString and floData in serialization + return Crypto.util.bytesToHex(buffer); } + /* deserialize a transaction */ + function deserialize(buffer) { + if (typeof buffer == "string") { + buffer = Crypto.util.hexToBytes(buffer) + } + var pos = 0; + + var readAsInt = function (bytes) { + if (bytes == 0) return 0; + pos++; + return buffer[pos - 1] + readAsInt(bytes - 1) * 256; + } + + var readVarInt = function () { + pos++; + if (buffer[pos - 1] < 253) { + return buffer[pos - 1]; + } + return readAsInt(buffer[pos - 1] - 251); + } + + var readBytes = function (bytes) { + pos += bytes; + return buffer.slice(pos - bytes, pos); + } + + var readVarString = function () { + var size = readVarInt(); + return readBytes(size); + } + + var bytesToStr = function (bytes) { + return bytes.map(b => String.fromCharCode(b)).join(''); + } + + const self = btrx; + + self.version = readAsInt(4); + + var ins = readVarInt(); + for (var i = 0; i < ins; i++) { + self.inputs.push({ + outpoint: { + hash: Crypto.util.bytesToHex(readBytes(32).reverse()), + index: readAsInt(4) + }, + script: readVarString(), + sequence: readAsInt(4) + }); + } + + var outs = readVarInt(); + for (var i = 0; i < outs; i++) { + self.outputs.push({ + value: bitjs.bytesToNum(readBytes(8)), + script: readVarString() + }); + } + + self.lock_time = readAsInt(4); + + //flochange - floData field + self.floData = bytesToStr(readVarString()); + + return self; + } + + //deserialize the data if passed + if (tx_data) + deserialize(tx_data); return btrx; @@ -4856,6 +5126,36 @@ else return bytes[0] + 256 * bitjs.bytesToNum(bytes.slice(1)); } + //flochange - adding fn to convert string (for flodata) to byte + bitjs.strToBytes = function (str) { + return str.split('').map(c => c.charCodeAt(0)); + } + + /* decompress an compressed public key */ + bitjs.pubkeydecompress = function (pubkey) { + if ((typeof (pubkey) == 'string') && pubkey.match(/^[a-f0-9]+$/i)) { + var curve = EllipticCurve.getSECCurveByName("secp256k1"); + try { + var pt = curve.curve.decodePointHex(pubkey); + var x = pt.getX().toBigInteger(); + var y = pt.getY().toBigInteger(); + + var publicKeyBytes = EllipticCurve.integerToBytes(x, 32); + publicKeyBytes = publicKeyBytes.concat(EllipticCurve.integerToBytes(y, 32)); + publicKeyBytes.unshift(0x04); + return Crypto.util.bytesToHex(publicKeyBytes); + } catch (e) { + // console.log(e); + return false; + } + } + return false; + } + + bitjs.verifySignature = function (hash, sig, pubkey) { + return Bitcoin.ECDSA.verify(hash, sig, pubkey); + } + /* clone an object */ bitjs.clone = function (obj) { if (obj == null || typeof (obj) != 'object') return obj; @@ -5020,17 +5320,25 @@ //https://raw.github.com/bitcoinjs/bitcoinjs-lib/09e8c6e184d6501a0c2c59d73ca64db5c0d3eb95/src/address.js Bitcoin.Address = function (bytes) { - if (GLOBAL.cryptocoin == "FLO") - this.version = 0x23; // FLO mainnet public address - else if (GLOBAL.cryptocoin == "FLO_TEST") - this.version = 0x73; // FLO testnet public address if ("string" == typeof bytes) { - bytes = Bitcoin.Address.decodeString(bytes, this.version); + var d = Bitcoin.Address.decodeString(bytes); + bytes = d.hash; + if (d.version == Bitcoin.Address.standardVersion || d.version == Bitcoin.Address.multisigVersion) + this.version = d.version; + else throw "Version (prefix) " + d.version + " not supported!"; + } else { + this.version = Bitcoin.Address.standardVersion; } this.hash = bytes; }; - Bitcoin.Address.networkVersion = 0x23; // (FLO mainnet 0x23, 35D), (Bitcoin Mainnet, 0x00, 0D) // *this has no effect * + Bitcoin.Address.standardVersion = 0x23; // (FLO mainnet 0x23, 35D), (Bitcoin Mainnet, 0x00, 0D) + Bitcoin.Address.multisigVersion = 0x5e; // (FLO multisig 0x5e, 94D) + + if (GLOBAL.cryptocoin == "FLO_TEST") { + Bitcoin.Address.standardVersion = 0x73; // (FLO testnet 0x73, 115D), (Bitcoin Mainnet, 0x00, 0D) + Bitcoin.Address.multisigVersion = 0xc6; // (FLO testnet multisig 0xc6, 198D) + } /** * Serialize this object as a standard Bitcoin address. @@ -5059,7 +5367,7 @@ /** * Parse a Bitcoin address contained in a string. */ - Bitcoin.Address.decodeString = function (string, version) { + Bitcoin.Address.decodeString = function (string) { var bytes = Bitcoin.Base58.decode(string); var hash = bytes.slice(0, 21); var checksum = Crypto.SHA256(Crypto.SHA256(hash, { @@ -5075,11 +5383,12 @@ throw "Checksum validation failed!"; } - if (version != hash.shift()) { + /*if (version != hash.shift()) { throw "Version " + hash.shift() + " not supported!"; - } + }*/ - return hash; + var version = hash.shift(); + return { version, hash }; }; //https://raw.github.com/bitcoinjs/bitcoinjs-lib/e90780d3d3b8fc0d027d2bcb38b80479902f223e/src/ecdsa.js Bitcoin.ECDSA = (function () { @@ -6488,6 +6797,7 @@ return { 'address': address, 'redeemScript': r.redeemScript, + 'scripthash': Crypto.util.bytesToHex(program), 'size': r.size }; } @@ -6581,15 +6891,16 @@ }; } - coinjs.multisigBech32Address = function (raw_redeemscript) { - var program = Crypto.SHA256(Crypto.util.hexToBytes(raw_redeemscript), { + coinjs.multisigBech32Address = function (redeemscript) { + var program = Crypto.SHA256(Crypto.util.hexToBytes(redeemscript), { asBytes: true }); var address = coinjs.bech32_encode(coinjs.bech32.hrp, [coinjs.bech32.version].concat(coinjs.bech32_convert(program, 8, 5, true))); return { 'address': address, 'type': 'multisigBech32', - 'redeemscript': Crypto.util.bytesToHex(program) + 'redeemScript': redeemscript, + 'scripthash': Crypto.util.bytesToHex(program) }; } @@ -7587,7 +7898,7 @@ var n = u.getElementsByTagName("tx_output_n")[0].childNodes[0].nodeValue; var scr = script || u.getElementsByTagName("script")[0].childNodes[0].nodeValue; - if (segwit) { //also for MULTISIG_BECH32 (p2wsh-multisig)(script = raw_redeemscript; for p2wsh-multisig) + if (segwit) { //also for MULTISIG_BECH32 (p2wsh-multisig)(script = redeemscript; for p2wsh-multisig) /* this is a small hack to include the value with the redeemscript to make the signing procedure smoother. It is not standard and removed during the signing procedure. */