From e628f53e8bee4cdbd489f44ea2157fa46d3a8cd8 Mon Sep 17 00:00:00 2001 From: pbca26 Date: Wed, 21 Feb 2018 16:23:08 +0300 Subject: [PATCH] spv sendmany, elections update --- routes/shepherd.js | 2 + routes/shepherd/elections.js | 233 ++++++++-- routes/shepherd/electrum/createtx-multi.js | 500 +++++++++++++++++++++ routes/shepherd/electrum/createtx-split.js | 86 ++++ routes/shepherd/electrum/createtx.js | 172 +------ version | 4 +- version_build | 2 +- 7 files changed, 800 insertions(+), 199 deletions(-) create mode 100644 routes/shepherd/electrum/createtx-multi.js create mode 100644 routes/shepherd/electrum/createtx-split.js diff --git a/routes/shepherd.js b/routes/shepherd.js index 7e693ac..6564f53 100644 --- a/routes/shepherd.js +++ b/routes/shepherd.js @@ -94,6 +94,8 @@ shepherd = require('./shepherd/electrum/balance.js')(shepherd); shepherd = require('./shepherd/electrum/transactions.js')(shepherd); shepherd = require('./shepherd/electrum/block.js')(shepherd); shepherd = require('./shepherd/electrum/createtx.js')(shepherd); +shepherd = require('./shepherd/electrum/createtx-split.js')(shepherd); +shepherd = require('./shepherd/electrum/createtx-multi.js')(shepherd); shepherd = require('./shepherd/electrum/interest.js')(shepherd); shepherd = require('./shepherd/electrum/listunspent.js')(shepherd); shepherd = require('./shepherd/electrum/estimate.js')(shepherd); diff --git a/routes/shepherd/elections.js b/routes/shepherd/elections.js index d64ce5e..0b39369 100644 --- a/routes/shepherd/elections.js +++ b/routes/shepherd/elections.js @@ -40,33 +40,41 @@ module.exports = (shepherd) => { let keys; let isWif = false; - try { - bs58check.decode(_seed); - isWif = true; - } catch (e) {} - - if (isWif) { + if (_seed.match('^[a-zA-Z0-9]{34}$')) { + shepherd.log('watchonly elections pub addr'); + shepherd.elections = { + priv: _seed, + pub: _seed, + }; + } else { try { - let key = bitcoin.ECPair.fromWIF(_seed, shepherd.getNetworkData(_network.toLowerCase()), true); - keys = { - priv: key.toWIF(), - pub: key.getAddress(), - }; - } catch (e) { - _wifError = true; + bs58check.decode(_seed); + isWif = true; + } catch (e) {} + + if (isWif) { + try { + let key = bitcoin.ECPair.fromWIF(_seed, shepherd.getNetworkData(_network.toLowerCase()), true); + keys = { + priv: key.toWIF(), + pub: key.getAddress(), + }; + } catch (e) { + _wifError = true; + } + } else { + keys = shepherd.seedToWif(_seed, _network, req.body.iguana); } - } else { - keys = shepherd.seedToWif(_seed, _network, req.body.iguana); - } - shepherd.elections = { - priv: keys.priv, - pub: keys.pub, - }; + shepherd.elections = { + priv: keys.priv, + pub: keys.pub, + }; + } const successObj = { msg: 'success', - result: keys.pub, + result: shepherd.elections.pub, }; res.end(JSON.stringify(successObj)); @@ -100,6 +108,61 @@ module.exports = (shepherd) => { } }); + shepherd.electionsDecodeTx = (decodedTx, ecl, network, _network, transaction, blockInfo, address) => { + let txInputs = []; + + return new shepherd.Promise((resolve, reject) => { + if (decodedTx && + decodedTx.inputs) { + shepherd.Promise.all(decodedTx.inputs.map((_decodedInput, index) => { + return new shepherd.Promise((_resolve, _reject) => { + if (_decodedInput.txid !== '0000000000000000000000000000000000000000000000000000000000000000') { + ecl.blockchainTransactionGet(_decodedInput.txid) + .then((rawInput) => { + const decodedVinVout = shepherd.electrumJSTxDecoder(rawInput, network, _network); + + shepherd.log('electrum raw input tx ==>', true); + + if (decodedVinVout) { + shepherd.log(decodedVinVout.outputs[_decodedInput.n], true); + txInputs.push(decodedVinVout.outputs[_decodedInput.n]); + _resolve(true); + } else { + _resolve(true); + } + }); + } else { + _resolve(true); + } + }); + })) + .then(promiseResult => { + const _parsedTx = { + network: decodedTx.network, + format: decodedTx.format, + inputs: txInputs, + outputs: decodedTx.outputs, + height: transaction.height, + timestamp: Number(transaction.height) === 0 ? Math.floor(Date.now() / 1000) : blockInfo.timestamp, + }; + + resolve(shepherd.parseTransactionAddresses(_parsedTx, address, network, true)); + }); + } else { + const _parsedTx = { + network: decodedTx.network, + format: 'cant parse', + inputs: 'cant parse', + outputs: 'cant parse', + height: transaction.height, + timestamp: Number(transaction.height) === 0 ? Math.floor(Date.now() / 1000) : blockInfo.timestamp, + }; + + resolve(shepherd.parseTransactionAddresses(_parsedTx, address, network)); + } + }); + }; + shepherd.get('/elections/listtransactions', (req, res, next) => { if (shepherd.checkToken(req.query.token)) { const network = req.query.network || shepherd.findNetworkObj(req.query.coin); @@ -116,10 +179,11 @@ module.exports = (shepherd) => { .then((json) => { if (json && json.length) { - json = shepherd.sortTransactions(json); - json = json.length > MAX_TX ? json.slice(0, MAX_TX) : json; let _rawtx = []; + json = shepherd.sortTransactions(json); + // json = json.length > MAX_TX ? json.slice(0, MAX_TX) : json; + shepherd.log(json.length, true); shepherd.Promise.all(json.map((transaction, index) => { @@ -130,9 +194,9 @@ module.exports = (shepherd) => { blockInfo.timestamp) { ecl.blockchainTransactionGet(transaction['tx_hash']) .then((_rawtxJSON) => { - shepherd.log('electrum gettransaction ==>', true); - shepherd.log((index + ' | ' + (_rawtxJSON.length - 1)), true); - shepherd.log(_rawtxJSON, true); + //shepherd.log('electrum gettransaction ==>', true); + //shepherd.log((index + ' | ' + (_rawtxJSON.length - 1)), true); + //shepherd.log(_rawtxJSON, true); // decode tx const _network = shepherd.getNetworkData(network); @@ -162,35 +226,112 @@ module.exports = (shepherd) => { decodedTx.outputs[i].scriptPubKey.addresses && decodedTx.outputs[i].scriptPubKey.addresses[0] && decodedTx.outputs[i].scriptPubKey.addresses[0] !== _address) { - shepherd.log(`i voted ${decodedTx.outputs[i].value} for ${decodedTx.outputs[i].scriptPubKey.addresses[0]}`); - _rawtx.push({ - address: decodedTx.outputs[i].scriptPubKey.addresses[0], - amount: decodedTx.outputs[i].value, - region: _region, - timestamp: blockInfo.timestamp, - }); + if (_region === 'ne2k18-na-1-eu-2-ae-3-sh-4') { + const _regionsLookup = [ + 'ne2k18-na', + 'ne2k18-eu', + 'ne2k18-ae', + 'ne2k18-sh' + ]; + + shepherd.log(`i voted ${decodedTx.outputs[i].value} for ${decodedTx.outputs[i].scriptPubKey.addresses[0]}`); + _rawtx.push({ + address: decodedTx.outputs[i].scriptPubKey.addresses[0], + amount: decodedTx.outputs[i].value, + region: _regionsLookup[i], + timestamp: blockInfo.timestamp, + }); + resolve(true); + } else { + shepherd.log(`i voted ${decodedTx.outputs[i].value} for ${decodedTx.outputs[i].scriptPubKey.addresses[0]}`); + _rawtx.push({ + address: decodedTx.outputs[i].scriptPubKey.addresses[0], + amount: decodedTx.outputs[i].value, + region: _region, + timestamp: blockInfo.timestamp, + }); + resolve(true); + } } if (type === 'candidate') { - if (decodedTx.outputs[i].scriptPubKey.addresses[0] === _address) { - _candidate.amount = decodedTx.outputs[i].value; - } else if (decodedTx.outputs[i].scriptPubKey.addresses[0] !== _address && decodedTx.outputs[i].scriptPubKey.asm.indexOf('OP_RETURN') === -1) { - _candidate.address = decodedTx.outputs[i].scriptPubKey.addresses[0]; - _candidate.region = _region; - _candidate.timestamp = blockInfo.timestamp; - } - - if (i === decodedTx.outputs.length - 1) { - shepherd.log(`i received ${_candidate.amount} from ${_candidate.address}`); - _rawtx.push(_candidate); + if (_region === 'ne2k18-na-1-eu-2-ae-3-sh-4') { + if (decodedTx.outputs[i].scriptPubKey.addresses[0] === _address && decodedTx.outputs[i].scriptPubKey.asm.indexOf('OP_RETURN') === -1) { + const _regionsLookup = [ + 'ne2k18-na', + 'ne2k18-eu', + 'ne2k18-ae', + 'ne2k18-sh' + ]; + + shepherd.electionsDecodeTx(decodedTx, ecl, network, _network, transaction, blockInfo, _address) + .then((res) => { + shepherd.log(`i received ${decodedTx.outputs[i].value} from ${res.outputAddresses[0]} out ${i} region ${_regionsLookup[i]}`); + _rawtx.push({ + address: res.outputAddresses[0], + timestamp: blockInfo.timestamp, + amount: decodedTx.outputs[i].value, + region: _regionsLookup[i], + }); + resolve(true); + }); + } + } else { + shepherd.electionsDecodeTx(decodedTx, ecl, network, _network, transaction, blockInfo, _address) + .then((res) => { + if (decodedTx.outputs[i].scriptPubKey.addresses[0] === _address) { + _candidate.amount = decodedTx.outputs[i].value; + } else if (decodedTx.outputs[i].scriptPubKey.addresses[0] !== _address && decodedTx.outputs[i].scriptPubKey.asm.indexOf('OP_RETURN') === -1) { + _candidate.address = decodedTx.outputs[i].scriptPubKey.addresses[0]; + _candidate.region = _region; + _candidate.timestamp = blockInfo.timestamp; + } + + if (i === decodedTx.outputs.length - 1) { + shepherd.log(`i received ${_candidate.amount} from ${_candidate.address} region ${_region}`); + _rawtx.push(_candidate); + resolve(true); + } + }); } } } + } else { + shepherd.log('elections regular tx', true); + shepherd.electionsDecodeTx(decodedTx, ecl, network, _network, transaction, blockInfo, _address) + .then((_regularTx) => { + if (_regularTx[0] && + _regularTx[1]) { + _rawtx.push({ + address: _regularTx[type === 'voter' ? 0 : 1].address, + timestamp: _regularTx[type === 'voter' ? 0 : 1].timestamp, + amount: _regularTx[type === 'voter' ? 0 : 1].amount, + region: 'unknown', + regularTx: true, + hash: transaction['tx_hash'], + }); + } else { + _rawtx.push({ + address: _regularTx.address, + timestamp: _regularTx.timestamp, + amount: _regularTx.amount, + region: 'unknown', + regularTx: true, + hash: transaction['tx_hash'], + }); + } + resolve(true); + }); } - - resolve(true); }); } else { + _rawtx.push({ + address: 'unknown', + timestamp: 'cant get block info', + amount: 'unknown', + region: 'unknown', + regularTx: true, + }); resolve(false); } }); diff --git a/routes/shepherd/electrum/createtx-multi.js b/routes/shepherd/electrum/createtx-multi.js new file mode 100644 index 0000000..fbf613b --- /dev/null +++ b/routes/shepherd/electrum/createtx-multi.js @@ -0,0 +1,500 @@ +const bitcoinJSForks = require('bitcoinforksjs-lib'); +const bitcoinZcash = require('bitcoinjs-lib-zcash'); +const bitcoinPos = require('bitcoinjs-lib-pos'); + +// not prod ready, only for voting! +// needs a fix + +module.exports = (shepherd) => { + shepherd.post('/electrum/createrawtx-multiout', (req, res, next) => { + if (shepherd.checkToken(req.body.token)) { + // TODO: 1) unconf output(s) error message + // 2) check targets integrity + const network = req.body.network || shepherd.findNetworkObj(req.body.coin); + const ecl = new shepherd.electrumJSCore(shepherd.electrumServers[network].port, shepherd.electrumServers[network].address, shepherd.electrumServers[network].proto); // tcp or tls + const initTargets = JSON.parse(JSON.stringify(req.body.targets)); + let targets = req.body.targets; + const changeAddress = req.body.change; + const push = req.body.push; + const opreturn = req.body.opreturn; + const btcFee = req.body.btcfee ? Number(req.body.btcfee) : null; + let fee = shepherd.electrumServers[network].txfee; + let wif = req.body.wif; + + if (req.body.gui) { + wif = shepherd.electrumKeys[req.body.coin].priv; + } + + if (req.body.vote) { + wif = shepherd.elections.priv; + } + + if (btcFee) { + fee = 0; + } + + shepherd.log('electrum createrawtx =>', true); + + ecl.connect(); + shepherd.listunspent(ecl, changeAddress, network, true, req.body.verify === 'true' ? true : null) + .then((utxoList) => { + ecl.close(); + + if (utxoList && + utxoList.length && + utxoList[0] && + utxoList[0].txid) { + let utxoListFormatted = []; + let totalInterest = 0; + let totalInterestUTXOCount = 0; + let interestClaimThreshold = 200; + let utxoVerified = true; + + for (let i = 0; i < utxoList.length; i++) { + if (network === 'komodo') { + utxoListFormatted.push({ + txid: utxoList[i].txid, + vout: utxoList[i].vout, + value: Number(utxoList[i].amountSats), + interestSats: Number(utxoList[i].interestSats), + verified: utxoList[i].verified ? utxoList[i].verified : false, + }); + } else { + utxoListFormatted.push({ + txid: utxoList[i].txid, + vout: utxoList[i].vout, + value: Number(utxoList[i].amountSats), + verified: utxoList[i].verified ? utxoList[i].verified : false, + }); + } + } + + shepherd.log('electrum listunspent unformatted ==>', true); + shepherd.log(utxoList, true); + + shepherd.log('electrum listunspent formatted ==>', true); + shepherd.log(utxoListFormatted, true); + + const _maxSpendBalance = Number(shepherd.maxSpendBalance(utxoListFormatted)); + /*let targets = [{ + address: outputAddress, + value: value > _maxSpendBalance ? _maxSpendBalance : value, + }];*/ + shepherd.log('targets =>', true); + shepherd.log(targets, true); + + targets[0].value = targets[0].value + fee; + + shepherd.log(`default fee ${fee}`, true); + shepherd.log(`targets ==>`, true); + shepherd.log(targets, true); + + // default coin selection algo blackjack with fallback to accumulative + // make a first run, calc approx tx fee + // if ins and outs are empty reduce max spend by txfee + const firstRun = shepherd.coinSelect(utxoListFormatted, targets, btcFee ? btcFee : 0); + let inputs = firstRun.inputs; + let outputs = firstRun.outputs; + + if (btcFee) { + shepherd.log(`btc fee per byte ${btcFee}`, true); + fee = firstRun.fee; + } + + shepherd.log('coinselect res =>', true); + shepherd.log('coinselect inputs =>', true); + shepherd.log(inputs, true); + shepherd.log('coinselect outputs =>', true); + shepherd.log(outputs, true); + shepherd.log('coinselect calculated fee =>', true); + shepherd.log(fee, true); + + if (!outputs) { + targets[0].value = targets[0].value - fee; + shepherd.log('second run', true); + shepherd.log('coinselect adjusted targets =>', true); + shepherd.log(targets, true); + + const secondRun = shepherd.coinSelect(utxoListFormatted, targets, 0); + inputs = secondRun.inputs; + outputs = secondRun.outputs; + fee = fee ? fee : secondRun.fee; + + shepherd.log('second run coinselect inputs =>', true); + shepherd.log(inputs, true); + shepherd.log('second run coinselect outputs =>', true); + shepherd.log(outputs, true); + shepherd.log('second run coinselect fee =>', true); + shepherd.log(fee, true); + } + + let _change = 0; + + if (outputs && + outputs.length > 1) { + _change = outputs[outputs.length - 1].value - fee; + } + + if (!btcFee && + _change === 0) { + outputs[0].value = outputs[0].value - fee; + } + + shepherd.log('adjusted outputs'); + shepherd.log(outputs, true); + + shepherd.log('init targets', true); + shepherd.log(initTargets, true); + + if (initTargets[0].value < targets[0].value) { + targets[0].value = initTargets[0].value; + } + + let _targetsSum = 0; + + for (let i = 0; i < targets.length; i++) { + _targetsSum += Number(targets[i].value); + } + + shepherd.log(`total targets sum ${_targetsSum}`); + + /*if (btcFee) { + value = _targetsSum; + } else { + if (_change > 0) { + value = _targetsSum - fee; + } + }*/ + value = _targetsSum; + + shepherd.log('adjusted outputs, value - default fee =>', true); + shepherd.log(outputs, true); + + // check if any outputs are unverified + if (inputs && + inputs.length) { + for (let i = 0; i < inputs.length; i++) { + if (!inputs[i].verified) { + utxoVerified = false; + break; + } + } + + for (let i = 0; i < inputs.length; i++) { + if (Number(inputs[i].interestSats) > interestClaimThreshold) { + totalInterest += Number(inputs[i].interestSats); + totalInterestUTXOCount++; + } + } + } + + const _maxSpend = shepherd.maxSpendBalance(utxoListFormatted); + + if (value > _maxSpend) { + const successObj = { + msg: 'error', + result: `Spend value is too large. Max available amount is ${Number((_maxSpend * 0.00000001.toFixed(8)))}`, + }; + + res.end(JSON.stringify(successObj)); + } else { + shepherd.log(`maxspend ${_maxSpend} (${_maxSpend * 0.00000001})`, true); + shepherd.log(`value ${value}`, true); + // shepherd.log(`sendto ${outputAddress} amount ${value} (${value * 0.00000001})`, true); + shepherd.log(`changeto ${changeAddress} amount ${_change} (${_change * 0.00000001})`, true); + + // account for KMD interest + if (network === 'komodo' && + totalInterest > 0) { + // account for extra vout + // const _feeOverhead = outputs.length === 1 ? shepherd.estimateTxSize(0, 1) * feeRate : 0; + const _feeOverhead = 0; + + shepherd.log(`max interest to claim ${totalInterest} (${totalInterest * 0.00000001})`, true); + shepherd.log(`estimated fee overhead ${_feeOverhead}`, true); + shepherd.log(`current change amount ${_change} (${_change * 0.00000001}), boosted change amount ${_change + (totalInterest - _feeOverhead)} (${(_change + (totalInterest - _feeOverhead)) * 0.00000001})`, true); + + if (_maxSpend - fee === value) { + _change = totalInterest - _change - _feeOverhead; + + if (outputAddress === changeAddress) { + value += _change; + _change = 0; + shepherd.log(`send to self ${outputAddress} = ${changeAddress}`, true); + shepherd.log(`send to self old val ${value}, new val ${value + _change}`, true); + } + } else { + _change = _change + (totalInterest - _feeOverhead); + } + } + + if (!inputs && + !outputs) { + const successObj = { + msg: 'error', + result: 'Can\'t find best fit utxo. Try lower amount.', + }; + + res.end(JSON.stringify(successObj)); + } else { + let vinSum = 0; + + for (let i = 0; i < inputs.length; i++) { + vinSum += inputs[i].value; + } + + let voutSum = 0; + + for (let i = 0; i < outputs.length; i++) { + voutSum += outputs[i].value; + } + + const _estimatedFee = vinSum - voutSum; + + shepherd.log(`vin sum ${vinSum} (${vinSum * 0.00000001})`, true); + shepherd.log(`vout sum ${voutSum} (${voutSum * 0.00000001})`, true); + shepherd.log(`estimatedFee ${_estimatedFee} (${_estimatedFee * 0.00000001})`, true); + // double check no extra fee is applied + shepherd.log(`vin - vout - change ${vinSum - value - _change}`); + + if ((vinSum - value - _change) > fee) { + _change += fee; + shepherd.log(`double fee, increase change by ${fee}`); + shepherd.log(`adjusted vin - vout - change ${vinSum - value - _change}`); + } + + // TODO: use individual dust thresholds + if (_change > 0 && + _change <= 1000) { + shepherd.log(`change is < 1000 sats, donate ${_change} sats to miners`, true); + _change = 0; + } + + outputAddress = outputs; + + if (outputAddress.length > 1) { + outputAddress.pop(); + } + + let _rawtx; + + if (network === 'btg' || + network === 'bch') { + /*_rawtx = shepherd.buildSignedTxForks( + outputAddress, + changeAddress, + wif, + network, + inputs, + _change, + value + );*/ + } else { + _rawtx = shepherd.buildSignedTxMulti( + outputAddress, + changeAddress, + wif, + network, + inputs, + _change, + value, + opreturn + ); + } + + if (!push || + push === 'false') { + const successObj = { + msg: 'success', + result: { + utxoSet: inputs, + change: _change, + changeAdjusted: _change, + totalInterest, + // wif, + fee, + value, + outputAddress, + changeAddress, + network, + rawtx: _rawtx, + utxoVerified, + }, + }; + + res.end(JSON.stringify(successObj)); + } else { + const ecl = new shepherd.electrumJSCore(shepherd.electrumServers[network].port, shepherd.electrumServers[network].address, shepherd.electrumServers[network].proto); // tcp or tls + + ecl.connect(); + ecl.blockchainTransactionBroadcast(_rawtx) + .then((txid) => { + ecl.close(); + + const _rawObj = { + utxoSet: inputs, + change: _change, + changeAdjusted: _change, + totalInterest, + fee, + value, + outputAddress, + changeAddress, + network, + rawtx: _rawtx, + txid, + utxoVerified, + }; + + if (txid && + txid.indexOf('bad-txns-inputs-spent') > -1) { + const successObj = { + msg: 'error', + result: 'Bad transaction inputs spent', + raw: _rawObj, + }; + + res.end(JSON.stringify(successObj)); + } else { + if (txid && + txid.length === 64) { + if (txid.indexOf('bad-txns-in-belowout') > -1) { + const successObj = { + msg: 'error', + result: 'Bad transaction inputs spent', + raw: _rawObj, + }; + + res.end(JSON.stringify(successObj)); + } else { + const successObj = { + msg: 'success', + result: _rawObj, + }; + + res.end(JSON.stringify(successObj)); + } + } else { + if (txid && + txid.indexOf('bad-txns-in-belowout') > -1) { + const successObj = { + msg: 'error', + result: 'Bad transaction inputs spent', + raw: _rawObj, + }; + + res.end(JSON.stringify(successObj)); + } else { + const successObj = { + msg: 'error', + result: 'Can\'t broadcast transaction', + raw: _rawObj, + }; + + res.end(JSON.stringify(successObj)); + } + } + } + }); + } + } + } + } else { + const successObj = { + msg: 'error', + result: utxoList, + }; + + res.end(JSON.stringify(successObj)); + } + }); + } else { + const errorObj = { + msg: 'error', + result: 'unauthorized access', + }; + + res.end(JSON.stringify(errorObj)); + } + }); + + // single sig + shepherd.buildSignedTxMulti = (sendTo, changeAddress, wif, network, utxo, changeValue, spendValue, opreturn) => { + let key = shepherd.isZcash(network) ? bitcoinZcash.ECPair.fromWIF(wif, shepherd.getNetworkData(network)) : shepherd.bitcoinJS.ECPair.fromWIF(wif, shepherd.getNetworkData(network)); + let tx; + + if (shepherd.isZcash(network)) { + tx = new bitcoinZcash.TransactionBuilder(shepherd.getNetworkData(network)); + } else if (shepherd.isPos(network)) { + tx = new bitcoinPos.TransactionBuilder(shepherd.getNetworkData(network)); + } else { + tx = new shepherd.bitcoinJS.TransactionBuilder(shepherd.getNetworkData(network)); + } + + shepherd.log('buildSignedTx', true); + // console.log(`buildSignedTx priv key ${wif}`); + shepherd.log(`buildSignedTx pub key ${key.getAddress().toString()}`, true); + // console.log('buildSignedTx std tx fee ' + shepherd.electrumServers[network].txfee); + + for (let i = 0; i < utxo.length; i++) { + tx.addInput(utxo[i].txid, utxo[i].vout); + } + + for (let i = 0; i < sendTo.length; i++) { + if (shepherd.isPos(network)) { + tx.addOutput(sendTo[i].address, Number(sendTo[i].value), shepherd.getNetworkData(network)); + } else { + tx.addOutput(sendTo[i].address, Number(sendTo[i].value)); + } + } + + if (changeValue > 0) { + if (shepherd.isPos(network)) { + tx.addOutput(changeAddress, Number(changeValue), shepherd.getNetworkData(network)); + } else { + tx.addOutput(changeAddress, Number(changeValue)); + } + } + + if (opreturn && + opreturn.length) { + for (let i = 0; i < opreturn.length; i++) { + shepherd.log(`opreturn ${i} ${opreturn[i]}`); + const data = Buffer.from(opreturn[i], 'utf8'); + const dataScript = shepherd.bitcoinJS.script.nullData.output.encode(data); + tx.addOutput(dataScript, 1000); + } + } + + if (network === 'komodo' || + network === 'KMD') { + const _locktime = Math.floor(Date.now() / 1000) - 777; + tx.setLockTime(_locktime); + shepherd.log(`kmd tx locktime set to ${_locktime}`, true); + } + + shepherd.log('buildSignedTx unsigned tx data vin', true); + shepherd.log(tx.tx.ins, true); + shepherd.log('buildSignedTx unsigned tx data vout', true); + shepherd.log(tx.tx.outs, true); + shepherd.log('buildSignedTx unsigned tx data', true); + shepherd.log(tx, true); + + for (let i = 0; i < utxo.length; i++) { + if (shepherd.isPos(network)) { + tx.sign(shepherd.getNetworkData(network), i, key); + } else { + tx.sign(i, key); + } + } + + const rawtx = tx.build().toHex(); + + shepherd.log('buildSignedTx signed tx hex', true); + shepherd.log(rawtx, true); + + return rawtx; + } + + return shepherd; +}; \ No newline at end of file diff --git a/routes/shepherd/electrum/createtx-split.js b/routes/shepherd/electrum/createtx-split.js new file mode 100644 index 0000000..c161a62 --- /dev/null +++ b/routes/shepherd/electrum/createtx-split.js @@ -0,0 +1,86 @@ +const bitcoinJSForks = require('bitcoinforksjs-lib'); +const bitcoinZcash = require('bitcoinjs-lib-zcash'); +const bitcoinPos = require('bitcoinjs-lib-pos'); + +module.exports = (shepherd) => { + // utxo split 1 -> 1, multiple outputs + shepherd.post('/electrum/createrawtx-split', (req, res, next) => { + if (shepherd.checkToken(req.body.token)) { + const wif = req.body.payload.wif; + const utxo = req.body.payload.utxo; + const targets = req.body.payload.targets; + const network = req.body.payload.network; + const change = req.body.payload.change; + const outputAddress = req.body.payload.outputAddress; + const changeAddress = req.body.payload.changeAddress; + + let key = shepherd.isZcash(network) ? bitcoinZcash.ECPair.fromWIF(wif, shepherd.getNetworkData(network)) : shepherd.bitcoinJS.ECPair.fromWIF(wif, shepherd.getNetworkData(network)); + let tx; + + if (shepherd.isZcash(network)) { + tx = new bitcoinZcash.TransactionBuilder(shepherd.getNetworkData(network)); + } else if (shepherd.isPos(network)) { + tx = new bitcoinPos.TransactionBuilder(shepherd.getNetworkData(network)); + } else { + tx = new shepherd.bitcoinJS.TransactionBuilder(shepherd.getNetworkData(network)); + } + + shepherd.log('buildSignedTx', true); + shepherd.log(`buildSignedTx pub key ${key.getAddress().toString()}`, true); + + for (let i = 0; i < utxo.length; i++) { + tx.addInput(utxo[i].txid, utxo[i].vout); + } + + for (let i = 0; i < targets.length; i++) { + if (shepherd.isPos(network)) { + tx.addOutput(outputAddress, Number(targets[i]), shepherd.getNetworkData(network)); + } else { + tx.addOutput(outputAddress, Number(targets[i])); + } + } + + if (Number(change) > 0) { + if (shepherd.isPos(network)) { + tx.addOutput(changeAddress, Number(change), shepherd.getNetworkData(network)); + } else { + shepherd.log(`change ${change}`, true); + tx.addOutput(changeAddress, Number(change)); + } + } + + if (network === 'komodo' || + network === 'KMD') { + const _locktime = Math.floor(Date.now() / 1000) - 777; + tx.setLockTime(_locktime); + shepherd.log(`kmd tx locktime set to ${_locktime}`, true); + } + + for (let i = 0; i < utxo.length; i++) { + if (shepherd.isPos(network)) { + tx.sign(shepherd.getNetworkData(network), i, key); + } else { + tx.sign(i, key); + } + } + + const rawtx = tx.build().toHex(); + + const successObj = { + msg: 'success', + result: rawtx, + }; + + res.end(JSON.stringify(successObj)); + } else { + const errorObj = { + msg: 'error', + result: 'unauthorized access', + }; + + res.end(JSON.stringify(errorObj)); + } + }); + + return shepherd; +}; \ No newline at end of file diff --git a/routes/shepherd/electrum/createtx.js b/routes/shepherd/electrum/createtx.js index ae2cb79..04b9795 100644 --- a/routes/shepherd/electrum/createtx.js +++ b/routes/shepherd/electrum/createtx.js @@ -424,7 +424,8 @@ module.exports = (shepherd) => { } // TODO: use individual dust thresholds - if (_change < 1000) { + if (_change > 0 && + _change <= 1000) { shepherd.log(`change is < 1000 sats, donate ${_change} sats to miners`, true); _change = 0; } @@ -497,25 +498,27 @@ module.exports = (shepherd) => { .then((txid) => { ecl.close(); + const _rawObj = { + utxoSet: inputs, + change: _change, + changeAdjusted: _change, + totalInterest, + fee, + value, + outputAddress, + changeAddress, + network, + rawtx: _rawtx, + txid, + utxoVerified, + }; + if (txid && txid.indexOf('bad-txns-inputs-spent') > -1) { const successObj = { msg: 'error', result: 'Bad transaction inputs spent', - raw: { - utxoSet: inputs, - change: _change, - changeAdjusted: _change, - totalInterest, - fee, - value, - outputAddress, - changeAddress, - network, - rawtx: _rawtx, - txid, - utxoVerified, - }, + raw: _rawObj, }; res.end(JSON.stringify(successObj)); @@ -526,41 +529,14 @@ module.exports = (shepherd) => { const successObj = { msg: 'error', result: 'Bad transaction inputs spent', - raw: { - utxoSet: inputs, - change: _change, - changeAdjusted: _change, - totalInterest, - fee, - value, - outputAddress, - changeAddress, - network, - rawtx: _rawtx, - txid, - utxoVerified, - }, + raw: _rawObj, }; res.end(JSON.stringify(successObj)); } else { const successObj = { msg: 'success', - result: { - utxoSet: inputs, - change: _change, - changeAdjusted: _change, - totalInterest, - fee, - // wif, - value, - outputAddress, - changeAddress, - network, - rawtx: _rawtx, - txid, - utxoVerified, - }, + result: _rawObj, }; res.end(JSON.stringify(successObj)); @@ -571,20 +547,7 @@ module.exports = (shepherd) => { const successObj = { msg: 'error', result: 'Bad transaction inputs spent', - raw: { - utxoSet: inputs, - change: _change, - changeAdjusted: _change, - totalInterest, - fee, - value, - outputAddress, - changeAddress, - network, - rawtx: _rawtx, - txid, - utxoVerified, - }, + raw: _rawObj, }; res.end(JSON.stringify(successObj)); @@ -592,20 +555,7 @@ module.exports = (shepherd) => { const successObj = { msg: 'error', result: 'Can\'t broadcast transaction', - raw: { - utxoSet: inputs, - change: _change, - changeAdjusted: _change, - totalInterest, - fee, - value, - outputAddress, - changeAddress, - network, - rawtx: _rawtx, - txid, - utxoVerified, - }, + raw: _rawObj, }; res.end(JSON.stringify(successObj)); @@ -665,83 +615,5 @@ module.exports = (shepherd) => { } }); - shepherd.post('/electrum/createrawtx-test', (req, res, next) => { - if (shepherd.checkToken(req.body.token)) { - const wif = req.body.payload.wif; - const utxo = req.body.payload.utxo; - const targets = req.body.payload.targets; - const network = req.body.payload.network; - const change = req.body.payload.change; - const outputAddress = req.body.payload.outputAddress; - const changeAddress = req.body.payload.changeAddress; - - let key = shepherd.isZcash(network) ? bitcoinZcash.ECPair.fromWIF(wif, shepherd.getNetworkData(network)) : shepherd.bitcoinJS.ECPair.fromWIF(wif, shepherd.getNetworkData(network)); - let tx; - - if (shepherd.isZcash(network)) { - tx = new bitcoinZcash.TransactionBuilder(shepherd.getNetworkData(network)); - } else if (shepherd.isPos(network)) { - tx = new bitcoinPos.TransactionBuilder(shepherd.getNetworkData(network)); - } else { - tx = new shepherd.bitcoinJS.TransactionBuilder(shepherd.getNetworkData(network)); - } - - shepherd.log('buildSignedTx', true); - shepherd.log(`buildSignedTx pub key ${key.getAddress().toString()}`, true); - - for (let i = 0; i < utxo.length; i++) { - tx.addInput(utxo[i].txid, utxo[i].vout); - } - - for (let i = 0; i < targets.length; i++) { - if (shepherd.isPos(network)) { - tx.addOutput(outputAddress, Number(targets[i]), shepherd.getNetworkData(network)); - } else { - tx.addOutput(outputAddress, Number(targets[i])); - } - } - - if (Number(change) > 0) { - if (shepherd.isPos(network)) { - tx.addOutput(changeAddress, Number(change), shepherd.getNetworkData(network)); - } else { - shepherd.log(`change ${change}`, true); - tx.addOutput(changeAddress, Number(change)); - } - } - - if (network === 'komodo' || - network === 'KMD') { - const _locktime = Math.floor(Date.now() / 1000) - 777; - tx.setLockTime(_locktime); - shepherd.log(`kmd tx locktime set to ${_locktime}`, true); - } - - for (let i = 0; i < utxo.length; i++) { - if (shepherd.isPos(network)) { - tx.sign(shepherd.getNetworkData(network), i, key); - } else { - tx.sign(i, key); - } - } - - const rawtx = tx.build().toHex(); - - const successObj = { - msg: 'success', - result: rawtx, - }; - - res.end(JSON.stringify(successObj)); - } else { - const errorObj = { - msg: 'error', - result: 'unauthorized access', - }; - - res.end(JSON.stringify(errorObj)); - } - }); - return shepherd; }; \ No newline at end of file diff --git a/version b/version index 0a74b63..77d4194 100644 --- a/version +++ b/version @@ -1,3 +1,3 @@ -version=0.2.0.28b -type=b-beta +version=0.2.0.28c +type=c-beta minversion=0.2.0.28 \ No newline at end of file diff --git a/version_build b/version_build index fea66b8..64427e2 100644 --- a/version_build +++ b/version_build @@ -1 +1 @@ -0.2.0.28b-beta \ No newline at end of file +0.2.0.28c-beta \ No newline at end of file