mirror of https://github.com/lukechilds/Agama.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
612 lines
20 KiB
612 lines
20 KiB
const bitcoinJSForks = require('bitcoinforksjs-lib');
|
|
const bitcoinZcash = require('bitcoinjs-lib-zcash');
|
|
const bitcoinPos = require('bitcoinjs-lib-pos');
|
|
|
|
module.exports = (shepherd) => {
|
|
// unsigned tx
|
|
shepherd.buildUnsignedTx = (sendTo, changeAddress, network, utxo, changeValue, spendValue) => {
|
|
let tx;
|
|
|
|
// TODO: finish unsigned for zcash, btc forks and pos coins
|
|
if (network === 'btg') {
|
|
shepherd.log('enable btg');
|
|
tx = new bitcoinJSForks.TransactionBuilder(shepherd.getNetworkData(network));
|
|
tx.enableBitcoinGold(true);
|
|
} else {
|
|
tx = new shepherd.bitcoinJS.TransactionBuilder(shepherd.getNetworkData(network));
|
|
}
|
|
|
|
shepherd.log('buildSignedTx');
|
|
// console.log(`buildSignedTx priv key ${wif}`);
|
|
shepherd.log(`buildSignedTx pub key ${changeAddress}`, 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);
|
|
}
|
|
|
|
tx.addOutput(sendTo, Number(spendValue));
|
|
|
|
if (changeValue > 0) {
|
|
tx.addOutput(changeAddress, Number(changeValue));
|
|
}
|
|
|
|
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);
|
|
|
|
const rawtx = tx.buildIncomplete().toHex();
|
|
|
|
shepherd.log('buildUnsignedTx tx hex', true);
|
|
shepherd.log(rawtx, true);
|
|
|
|
return rawtx;
|
|
}
|
|
|
|
// single sig
|
|
shepherd.buildSignedTx = (sendTo, changeAddress, wif, network, utxo, changeValue, spendValue) => {
|
|
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');
|
|
// 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);
|
|
}
|
|
|
|
if (shepherd.isPos(network)) {
|
|
tx.addOutput(sendTo, Number(spendValue), shepherd.getNetworkData(network));
|
|
} else {
|
|
tx.addOutput(sendTo, Number(spendValue));
|
|
}
|
|
|
|
if (changeValue > 0) {
|
|
if (shepherd.isPos(network)) {
|
|
tx.addOutput(changeAddress, Number(changeValue), shepherd.getNetworkData(network));
|
|
} else {
|
|
tx.addOutput(changeAddress, Number(changeValue));
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// btg
|
|
shepherd.buildSignedTxForks = (sendTo, changeAddress, wif, network, utxo, changeValue, spendValue) => {
|
|
let tx;
|
|
|
|
if (network === 'btg' ||
|
|
network === 'bch') {
|
|
tx = new bitcoinJSForks.TransactionBuilder(shepherd.getNetworkData(network));
|
|
}
|
|
|
|
const keyPair = bitcoinJSForks.ECPair.fromWIF(wif, shepherd.getNetworkData(network));
|
|
const pk = bitcoinJSForks.crypto.hash160(keyPair.getPublicKeyBuffer());
|
|
const spk = bitcoinJSForks.script.pubKeyHash.output.encode(pk);
|
|
|
|
shepherd.log(`buildSignedTx${network.toUpperCase()}`);
|
|
|
|
for (let i = 0; i < utxo.length; i++) {
|
|
tx.addInput(utxo[i].txid, utxo[i].vout, bitcoinJSForks.Transaction.DEFAULT_SEQUENCE, spk);
|
|
}
|
|
|
|
tx.addOutput(sendTo, Number(spendValue));
|
|
|
|
if (changeValue > 0) {
|
|
tx.addOutput(changeAddress, Number(changeValue));
|
|
}
|
|
|
|
if (network === 'btg') {
|
|
tx.enableBitcoinGold(true);
|
|
} else if (network === 'bch') {
|
|
tx.enableBitcoinCash(true);
|
|
}
|
|
|
|
tx.setVersion(2);
|
|
|
|
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);
|
|
|
|
const hashType = bitcoinJSForks.Transaction.SIGHASH_ALL | bitcoinJSForks.Transaction.SIGHASH_BITCOINCASHBIP143;
|
|
|
|
for (let i = 0; i < utxo.length; i++) {
|
|
tx.sign(i, keyPair, null, hashType, utxo[i].value);
|
|
}
|
|
|
|
const rawtx = tx.build().toHex();
|
|
|
|
shepherd.log('buildSignedTx signed tx hex', true);
|
|
shepherd.log(rawtx, true);
|
|
|
|
return rawtx;
|
|
}
|
|
|
|
shepherd.maxSpendBalance = (utxoList, fee) => {
|
|
let maxSpendBalance = 0;
|
|
|
|
for (let i = 0; i < utxoList.length; i++) {
|
|
maxSpendBalance += Number(utxoList[i].value);
|
|
}
|
|
|
|
if (fee) {
|
|
return Number(maxSpendBalance) - Number(fee);
|
|
} else {
|
|
return maxSpendBalance;
|
|
}
|
|
}
|
|
|
|
shepherd.get('/electrum/createrawtx', (req, res, next) => {
|
|
// TODO: unconf output(s) error message
|
|
const network = req.query.network || shepherd.findNetworkObj(req.query.coin);
|
|
const ecl = new shepherd.electrumJSCore(shepherd.electrumServers[network].port, shepherd.electrumServers[network].address, shepherd.electrumServers[network].proto); // tcp or tls
|
|
const outputAddress = req.query.address;
|
|
const changeAddress = req.query.change;
|
|
const push = req.query.push;
|
|
let fee = shepherd.electrumServers[network].txfee;
|
|
let value = Number(req.query.value);
|
|
let wif = req.query.wif;
|
|
|
|
if (req.query.gui) {
|
|
wif = shepherd.electrumKeys[req.query.coin].priv;
|
|
}
|
|
|
|
shepherd.log('electrum createrawtx =>', true);
|
|
|
|
ecl.connect();
|
|
shepherd.listunspent(ecl, changeAddress, network, true, true)
|
|
.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);
|
|
|
|
const feeRate = shepherd.getNetworkData(network).messagePrefix === '\x19Komodo Signed Message:\n' || shepherd.getNetworkData(network).messagePrefix === '\x19Chips Signed Message:\n' ? 20 : 0; // sats/byte
|
|
|
|
if (!feeRate) {
|
|
targets[0].value = targets[0].value + fee;
|
|
}
|
|
|
|
shepherd.log(`fee rate ${feeRate}`, true);
|
|
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, feeRate);
|
|
let inputs = firstRun.inputs;
|
|
let outputs = firstRun.outputs;
|
|
|
|
if (feeRate) {
|
|
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, feeRate);
|
|
inputs = secondRun.inputs;
|
|
outputs = secondRun.outputs;
|
|
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 === 2) {
|
|
_change = outputs[1].value;
|
|
}
|
|
|
|
// non komodo coins, subtract fee from output value
|
|
if (!feeRate) {
|
|
outputs[0].value = outputs[0].value - fee;
|
|
|
|
shepherd.log('non komodo 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;
|
|
|
|
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 === 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;
|
|
}
|
|
|
|
const _estimatedFee = vinSum - outputs[0].value - _change;
|
|
|
|
shepherd.log(`vin sum ${vinSum} (${vinSum * 0.00000001})`, true);
|
|
shepherd.log(`estimatedFee ${_estimatedFee} (${_estimatedFee * 0.00000001})`, true);
|
|
|
|
let _rawtx;
|
|
|
|
if (req.query.unsigned) {
|
|
_rawtx = shepherd.buildUnsignedTx(
|
|
outputAddress,
|
|
changeAddress,
|
|
network,
|
|
inputs,
|
|
_change,
|
|
value
|
|
);
|
|
} else {
|
|
if (network === 'btg' ||
|
|
network === 'bch') {
|
|
_rawtx = shepherd.buildSignedTxForks(
|
|
outputAddress,
|
|
changeAddress,
|
|
wif,
|
|
network,
|
|
inputs,
|
|
_change,
|
|
value
|
|
);
|
|
} else {
|
|
_rawtx = shepherd.buildSignedTx(
|
|
outputAddress,
|
|
changeAddress,
|
|
wif,
|
|
network,
|
|
inputs,
|
|
_change,
|
|
value
|
|
);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
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,
|
|
},
|
|
};
|
|
|
|
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: {
|
|
utxoSet: inputs,
|
|
change: _change,
|
|
changeAdjusted: _change,
|
|
totalInterest,
|
|
fee,
|
|
value,
|
|
outputAddress,
|
|
changeAddress,
|
|
network,
|
|
rawtx: _rawtx,
|
|
txid,
|
|
utxoVerified,
|
|
},
|
|
};
|
|
|
|
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,
|
|
},
|
|
};
|
|
|
|
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: {
|
|
utxoSet: inputs,
|
|
change: _change,
|
|
changeAdjusted: _change,
|
|
totalInterest,
|
|
fee,
|
|
value,
|
|
outputAddress,
|
|
changeAddress,
|
|
network,
|
|
rawtx: _rawtx,
|
|
txid,
|
|
utxoVerified,
|
|
},
|
|
};
|
|
|
|
res.end(JSON.stringify(successObj));
|
|
} else {
|
|
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,
|
|
},
|
|
};
|
|
|
|
res.end(JSON.stringify(successObj));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const successObj = {
|
|
msg: 'error',
|
|
result: utxoList,
|
|
};
|
|
|
|
res.end(JSON.stringify(successObj));
|
|
}
|
|
});
|
|
});
|
|
|
|
shepherd.get('/electrum/pushtx', (req, res, next) => {
|
|
const rawtx = req.query.rawtx;
|
|
const ecl = new shepherd.electrumJSCore(shepherd.electrumServers[req.query.network].port, shepherd.electrumServers[req.query.network].address, shepherd.electrumServers[req.query.network].proto); // tcp or tls
|
|
|
|
ecl.connect();
|
|
ecl.blockchainTransactionBroadcast(rawtx)
|
|
.then((json) => {
|
|
ecl.close();
|
|
shepherd.log('electrum pushtx ==>', true);
|
|
shepherd.log(json, true);
|
|
|
|
const successObj = {
|
|
msg: 'success',
|
|
result: json,
|
|
};
|
|
|
|
res.end(JSON.stringify(successObj));
|
|
});
|
|
});
|
|
|
|
return shepherd;
|
|
};
|