mirror of https://github.com/lukechilds/Agama.git
7 changed files with 800 additions and 199 deletions
@ -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; |
|||
}; |
@ -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; |
|||
}; |
@ -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 |
@ -1 +1 @@ |
|||
0.2.0.28b-beta |
|||
0.2.0.28c-beta |
Loading…
Reference in new issue