remove tx legacy code
This commit is contained in:
parent
520dc6ecfd
commit
bc9354bf1f
|
@ -281,17 +281,6 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
router.post('/v1/txproposals/', function(req, res) {
|
|
||||||
log.warn('DEPRECATED v1/txproposals');
|
|
||||||
getServerWithAuth(req, res, function(server) {
|
|
||||||
server.createTxLegacy(req.body, function(err, txp) {
|
|
||||||
if (err) return returnError(err, res, req);
|
|
||||||
res.json(txp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/v2/txproposals/', function(req, res) {
|
router.post('/v2/txproposals/', function(req, res) {
|
||||||
getServerWithAuth(req, res, function(server) {
|
getServerWithAuth(req, res, function(server) {
|
||||||
server.createTx(req.body, function(err, txp) {
|
server.createTx(req.body, function(err, txp) {
|
||||||
|
|
|
@ -2,7 +2,6 @@ var Model = {};
|
||||||
|
|
||||||
Model.Wallet = require('./wallet');
|
Model.Wallet = require('./wallet');
|
||||||
Model.Copayer = require('./copayer');
|
Model.Copayer = require('./copayer');
|
||||||
Model.TxProposalLegacy = require('./txproposal_legacy');
|
|
||||||
Model.TxProposal = require('./txproposal');
|
Model.TxProposal = require('./txproposal');
|
||||||
Model.Address = require('./address');
|
Model.Address = require('./address');
|
||||||
Model.Notification = require('./notification');
|
Model.Notification = require('./notification');
|
||||||
|
|
|
@ -13,7 +13,6 @@ var Common = require('../common');
|
||||||
var Constants = Common.Constants;
|
var Constants = Common.Constants;
|
||||||
var Defaults = Common.Defaults;
|
var Defaults = Common.Defaults;
|
||||||
|
|
||||||
var TxProposalLegacy = require('./txproposal_legacy');
|
|
||||||
var TxProposalAction = require('./txproposalaction');
|
var TxProposalAction = require('./txproposalaction');
|
||||||
|
|
||||||
function TxProposal() {};
|
function TxProposal() {};
|
||||||
|
@ -68,7 +67,7 @@ TxProposal.create = function(opts) {
|
||||||
|
|
||||||
TxProposal.fromObj = function(obj) {
|
TxProposal.fromObj = function(obj) {
|
||||||
if (!(obj.version >= 3)) {
|
if (!(obj.version >= 3)) {
|
||||||
return TxProposalLegacy.fromObj(obj);
|
throw new Error('TxProposal < v3 no longer supported.')
|
||||||
}
|
}
|
||||||
|
|
||||||
var x = new TxProposal();
|
var x = new TxProposal();
|
||||||
|
|
|
@ -1,441 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var _ = require('lodash');
|
|
||||||
var $ = require('preconditions').singleton();
|
|
||||||
var Uuid = require('uuid');
|
|
||||||
var log = require('npmlog');
|
|
||||||
log.debug = log.verbose;
|
|
||||||
log.disableColor();
|
|
||||||
|
|
||||||
var Bitcore = require('bitcore-lib');
|
|
||||||
|
|
||||||
var Common = require('../common');
|
|
||||||
var Constants = Common.Constants;
|
|
||||||
var Defaults = Common.Defaults;
|
|
||||||
|
|
||||||
var TxProposalAction = require('./txproposalaction');
|
|
||||||
|
|
||||||
function TxProposal() {};
|
|
||||||
|
|
||||||
TxProposal.Types = {
|
|
||||||
SIMPLE: 'simple',
|
|
||||||
MULTIPLEOUTPUTS: 'multiple_outputs',
|
|
||||||
EXTERNAL: 'external'
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.isTypeSupported = function(type) {
|
|
||||||
return _.contains(_.values(TxProposal.Types), type);
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal._create = {};
|
|
||||||
|
|
||||||
TxProposal._create.simple = function(txp, opts) {
|
|
||||||
txp.toAddress = opts.toAddress;
|
|
||||||
txp.amount = opts.amount;
|
|
||||||
txp.outputOrder = _.shuffle(_.range(2));
|
|
||||||
try {
|
|
||||||
txp.network = Bitcore.Address(txp.toAddress).toObject().network;
|
|
||||||
} catch (ex) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal._create.undefined = TxProposal._create.simple;
|
|
||||||
|
|
||||||
TxProposal._create.multiple_outputs = function(txp, opts) {
|
|
||||||
txp.outputs = _.map(opts.outputs, function(output) {
|
|
||||||
return _.pick(output, ['amount', 'toAddress', 'message']);
|
|
||||||
});
|
|
||||||
txp.outputOrder = _.shuffle(_.range(txp.outputs.length + 1));
|
|
||||||
txp.amount = txp.getTotalAmount();
|
|
||||||
try {
|
|
||||||
txp.network = Bitcore.Address(txp.outputs[0].toAddress).toObject().network;
|
|
||||||
} catch (ex) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal._create.external = function(txp, opts) {
|
|
||||||
txp.setInputs(opts.inputs || []);
|
|
||||||
txp.outputs = opts.outputs;
|
|
||||||
txp.outputOrder = _.range(txp.outputs.length + 1);
|
|
||||||
txp.amount = txp.getTotalAmount();
|
|
||||||
try {
|
|
||||||
txp.network = Bitcore.Address(txp.outputs[0].toAddress).toObject().network;
|
|
||||||
} catch (ex) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.create = function(opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
|
|
||||||
var x = new TxProposal();
|
|
||||||
|
|
||||||
x.version = '2.0.0';
|
|
||||||
x.type = opts.type || TxProposal.Types.SIMPLE;
|
|
||||||
|
|
||||||
var now = Date.now();
|
|
||||||
x.createdOn = Math.floor(now / 1000);
|
|
||||||
x.id = _.padLeft(now, 14, '0') + Uuid.v4();
|
|
||||||
x.walletId = opts.walletId;
|
|
||||||
x.creatorId = opts.creatorId;
|
|
||||||
x.message = opts.message;
|
|
||||||
x.payProUrl = opts.payProUrl;
|
|
||||||
x.proposalSignature = opts.proposalSignature;
|
|
||||||
x.changeAddress = opts.changeAddress;
|
|
||||||
x.inputs = [];
|
|
||||||
x.inputPaths = [];
|
|
||||||
x.requiredSignatures = opts.requiredSignatures;
|
|
||||||
x.requiredRejections = opts.requiredRejections;
|
|
||||||
x.walletN = opts.walletN;
|
|
||||||
x.status = 'pending';
|
|
||||||
x.actions = [];
|
|
||||||
x.fee = null;
|
|
||||||
x.feePerKb = opts.feePerKb;
|
|
||||||
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
|
|
||||||
x.proposalSignaturePubKey = opts.proposalSignaturePubKey;
|
|
||||||
x.proposalSignaturePubKeySig = opts.proposalSignaturePubKeySig;
|
|
||||||
x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH;
|
|
||||||
x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
|
|
||||||
x.customData = opts.customData;
|
|
||||||
|
|
||||||
if (_.isFunction(TxProposal._create[x.type])) {
|
|
||||||
TxProposal._create[x.type](x, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
return x;
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.fromObj = function(obj) {
|
|
||||||
var x = new TxProposal();
|
|
||||||
|
|
||||||
x.version = obj.version;
|
|
||||||
if (obj.version === '1.0.0') {
|
|
||||||
x.type = TxProposal.Types.SIMPLE;
|
|
||||||
} else {
|
|
||||||
x.type = obj.type;
|
|
||||||
}
|
|
||||||
x.createdOn = obj.createdOn;
|
|
||||||
x.id = obj.id;
|
|
||||||
x.walletId = obj.walletId;
|
|
||||||
x.creatorId = obj.creatorId;
|
|
||||||
x.outputs = obj.outputs;
|
|
||||||
x.toAddress = obj.toAddress;
|
|
||||||
x.amount = obj.amount;
|
|
||||||
x.message = obj.message;
|
|
||||||
x.payProUrl = obj.payProUrl;
|
|
||||||
x.proposalSignature = obj.proposalSignature;
|
|
||||||
x.changeAddress = obj.changeAddress;
|
|
||||||
x.inputs = obj.inputs;
|
|
||||||
x.requiredSignatures = obj.requiredSignatures;
|
|
||||||
x.requiredRejections = obj.requiredRejections;
|
|
||||||
x.walletN = obj.walletN;
|
|
||||||
x.status = obj.status;
|
|
||||||
x.txid = obj.txid;
|
|
||||||
x.broadcastedOn = obj.broadcastedOn;
|
|
||||||
x.inputPaths = obj.inputPaths;
|
|
||||||
x.actions = _.map(obj.actions, function(action) {
|
|
||||||
return TxProposalAction.fromObj(action);
|
|
||||||
});
|
|
||||||
x.outputOrder = obj.outputOrder;
|
|
||||||
x.network = obj.network;
|
|
||||||
x.fee = obj.fee;
|
|
||||||
x.feePerKb = obj.feePerKb;
|
|
||||||
x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos;
|
|
||||||
x.proposalSignaturePubKey = obj.proposalSignaturePubKey;
|
|
||||||
x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig;
|
|
||||||
x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH;
|
|
||||||
x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
|
|
||||||
x.customData = obj.customData;
|
|
||||||
|
|
||||||
return x;
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.toObject = function() {
|
|
||||||
var x = _.cloneDeep(this);
|
|
||||||
x.isPending = this.isPending();
|
|
||||||
return x;
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.setInputs = function(inputs) {
|
|
||||||
this.inputs = inputs;
|
|
||||||
this.inputPaths = _.pluck(inputs, 'path');
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype._updateStatus = function() {
|
|
||||||
if (this.status != 'pending') return;
|
|
||||||
|
|
||||||
if (this.isRejected()) {
|
|
||||||
this.status = 'rejected';
|
|
||||||
} else if (this.isAccepted()) {
|
|
||||||
this.status = 'accepted';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype._buildTx = function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var t = new Bitcore.Transaction();
|
|
||||||
|
|
||||||
$.checkState(_.contains(_.values(Constants.SCRIPT_TYPES), self.addressType));
|
|
||||||
|
|
||||||
switch (self.addressType) {
|
|
||||||
case Constants.SCRIPT_TYPES.P2SH:
|
|
||||||
_.each(self.inputs, function(i) {
|
|
||||||
t.from(i, i.publicKeys, self.requiredSignatures);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case Constants.SCRIPT_TYPES.P2PKH:
|
|
||||||
t.from(self.inputs);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.toAddress && self.amount && !self.outputs) {
|
|
||||||
t.to(self.toAddress, self.amount);
|
|
||||||
} else if (self.outputs) {
|
|
||||||
_.each(self.outputs, function(o) {
|
|
||||||
$.checkState(o.script || o.toAddress, 'Output should have either toAddress or script specified');
|
|
||||||
if (o.script) {
|
|
||||||
t.addOutput(new Bitcore.Transaction.Output({
|
|
||||||
script: o.script,
|
|
||||||
satoshis: o.amount
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
t.to(o.toAddress, o.amount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.startsWith(self.version, '1.')) {
|
|
||||||
Bitcore.Transaction.FEE_SECURITY_MARGIN = 1;
|
|
||||||
t.feePerKb(self.feePerKb);
|
|
||||||
} else {
|
|
||||||
t.fee(self.fee);
|
|
||||||
}
|
|
||||||
|
|
||||||
t.change(self.changeAddress.address);
|
|
||||||
|
|
||||||
// Shuffle outputs for improved privacy
|
|
||||||
if (t.outputs.length > 1) {
|
|
||||||
var outputOrder = _.reject(self.outputOrder, function(order) {
|
|
||||||
return order >= t.outputs.length;
|
|
||||||
});
|
|
||||||
$.checkState(t.outputs.length == outputOrder.length);
|
|
||||||
t.sortOutputs(function(outputs) {
|
|
||||||
return _.map(outputOrder, function(i) {
|
|
||||||
return outputs[i];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate inputs vs outputs independently of Bitcore
|
|
||||||
var totalInputs = _.reduce(self.inputs, function(memo, i) {
|
|
||||||
return +i.satoshis + memo;
|
|
||||||
}, 0);
|
|
||||||
var totalOutputs = _.reduce(t.outputs, function(memo, o) {
|
|
||||||
return +o.satoshis + memo;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
$.checkState(totalInputs - totalOutputs <= Defaults.MAX_TX_FEE);
|
|
||||||
|
|
||||||
return t;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
TxProposal.prototype._getCurrentSignatures = function() {
|
|
||||||
var acceptedActions = _.filter(this.actions, {
|
|
||||||
type: 'accept'
|
|
||||||
});
|
|
||||||
|
|
||||||
return _.map(acceptedActions, function(x) {
|
|
||||||
return {
|
|
||||||
signatures: x.signatures,
|
|
||||||
xpub: x.xpub,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.getBitcoreTx = function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var t = this._buildTx();
|
|
||||||
|
|
||||||
var sigs = this._getCurrentSignatures();
|
|
||||||
_.each(sigs, function(x) {
|
|
||||||
self._addSignaturesToBitcoreTx(t, x.signatures, x.xpub);
|
|
||||||
});
|
|
||||||
|
|
||||||
return t;
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.getNetworkName = function() {
|
|
||||||
return Bitcore.Address(this.changeAddress.address).toObject().network;
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.getRawTx = function() {
|
|
||||||
var t = this.getBitcoreTx();
|
|
||||||
|
|
||||||
return t.uncheckedSerialize();
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.getEstimatedSizeForSingleInput = function() {
|
|
||||||
return this.requiredSignatures * 72 + this.walletN * 36 + 44;
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.getEstimatedSize = function() {
|
|
||||||
// Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits.
|
|
||||||
var safetyMargin = 0.05;
|
|
||||||
|
|
||||||
var overhead = 4 + 4 + 9 + 9;
|
|
||||||
var inputSize = this.getEstimatedSizeForSingleInput();
|
|
||||||
var outputSize = 34;
|
|
||||||
var nbInputs = this.inputs.length;
|
|
||||||
var nbOutputs = (_.isArray(this.outputs) ? Math.max(1, this.outputs.length) : 1) + 1;
|
|
||||||
|
|
||||||
var size = overhead + inputSize * nbInputs + outputSize * nbOutputs;
|
|
||||||
|
|
||||||
return parseInt((size * (1 + safetyMargin)).toFixed(0));
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.estimateFee = function() {
|
|
||||||
var fee = this.feePerKb * this.getEstimatedSize() / 1000;
|
|
||||||
|
|
||||||
this.fee = parseInt(fee.toFixed(0));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getTotalAmount
|
|
||||||
*
|
|
||||||
* @return {Number} total amount of all outputs excluding change output
|
|
||||||
*/
|
|
||||||
TxProposal.prototype.getTotalAmount = function() {
|
|
||||||
if (this.type == TxProposal.Types.MULTIPLEOUTPUTS || this.type == TxProposal.Types.EXTERNAL) {
|
|
||||||
return _.pluck(this.outputs, 'amount')
|
|
||||||
.reduce(function(total, n) {
|
|
||||||
return total + n;
|
|
||||||
}, 0);
|
|
||||||
} else {
|
|
||||||
return this.amount;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getActors
|
|
||||||
*
|
|
||||||
* @return {String[]} copayerIds that performed actions in this proposal (accept / reject)
|
|
||||||
*/
|
|
||||||
TxProposal.prototype.getActors = function() {
|
|
||||||
return _.pluck(this.actions, 'copayerId');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getApprovers
|
|
||||||
*
|
|
||||||
* @return {String[]} copayerIds that approved the tx proposal (accept)
|
|
||||||
*/
|
|
||||||
TxProposal.prototype.getApprovers = function() {
|
|
||||||
return _.pluck(
|
|
||||||
_.filter(this.actions, {
|
|
||||||
type: 'accept'
|
|
||||||
}), 'copayerId');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getActionBy
|
|
||||||
*
|
|
||||||
* @param {String} copayerId
|
|
||||||
* @return {Object} type / createdOn
|
|
||||||
*/
|
|
||||||
TxProposal.prototype.getActionBy = function(copayerId) {
|
|
||||||
return _.find(this.actions, {
|
|
||||||
copayerId: copayerId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.addAction = function(copayerId, type, comment, signatures, xpub) {
|
|
||||||
var action = TxProposalAction.create({
|
|
||||||
copayerId: copayerId,
|
|
||||||
type: type,
|
|
||||||
signatures: signatures,
|
|
||||||
xpub: xpub,
|
|
||||||
comment: comment,
|
|
||||||
});
|
|
||||||
this.actions.push(action);
|
|
||||||
this._updateStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype._addSignaturesToBitcoreTx = function(tx, signatures, xpub) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (signatures.length != this.inputs.length)
|
|
||||||
throw new Error('Number of signatures does not match number of inputs');
|
|
||||||
|
|
||||||
var i = 0,
|
|
||||||
x = new Bitcore.HDPublicKey(xpub);
|
|
||||||
|
|
||||||
_.each(signatures, function(signatureHex) {
|
|
||||||
var input = self.inputs[i];
|
|
||||||
try {
|
|
||||||
var signature = Bitcore.crypto.Signature.fromString(signatureHex);
|
|
||||||
var pub = x.derive(self.inputPaths[i]).publicKey;
|
|
||||||
var s = {
|
|
||||||
inputIndex: i,
|
|
||||||
signature: signature,
|
|
||||||
sigtype: Bitcore.crypto.Signature.SIGHASH_ALL,
|
|
||||||
publicKey: pub,
|
|
||||||
};
|
|
||||||
tx.inputs[i].addSignature(tx, s);
|
|
||||||
i++;
|
|
||||||
} catch (e) {};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (i != tx.inputs.length)
|
|
||||||
throw new Error('Wrong signatures');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
TxProposal.prototype.sign = function(copayerId, signatures, xpub) {
|
|
||||||
try {
|
|
||||||
// Tests signatures are OK
|
|
||||||
var tx = this.getBitcoreTx();
|
|
||||||
this._addSignaturesToBitcoreTx(tx, signatures, xpub);
|
|
||||||
|
|
||||||
this.addAction(copayerId, 'accept', null, signatures, xpub);
|
|
||||||
|
|
||||||
if (this.status == 'accepted') {
|
|
||||||
this.raw = tx.uncheckedSerialize();
|
|
||||||
this.txid = tx.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
log.debug(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.reject = function(copayerId, reason) {
|
|
||||||
this.addAction(copayerId, 'reject', reason);
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.isPending = function() {
|
|
||||||
return !_.contains(['broadcasted', 'rejected'], this.status);
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.isAccepted = function() {
|
|
||||||
var votes = _.countBy(this.actions, 'type');
|
|
||||||
return votes['accept'] >= this.requiredSignatures;
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.isRejected = function() {
|
|
||||||
var votes = _.countBy(this.actions, 'type');
|
|
||||||
return votes['reject'] >= this.requiredRejections;
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.isBroadcasted = function() {
|
|
||||||
return this.status == 'broadcasted';
|
|
||||||
};
|
|
||||||
|
|
||||||
TxProposal.prototype.setBroadcasted = function() {
|
|
||||||
$.checkState(this.txid);
|
|
||||||
this.status = 'broadcasted';
|
|
||||||
this.broadcastedOn = Math.floor(Date.now() / 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = TxProposal;
|
|
214
lib/server.js
214
lib/server.js
|
@ -569,22 +569,6 @@ WalletService.prototype._parseClientVersion = function() {
|
||||||
return this.parsedClientVersion;
|
return this.parsedClientVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService.prototype._clientSupportsTXPv2 = function() {
|
|
||||||
var version = this._parseClientVersion();
|
|
||||||
if (!version) return false;
|
|
||||||
if (version.agent != 'bwc') return true; // Asume 3rd party clients are up-to-date
|
|
||||||
if (version.major == 0 && version.minor == 0) return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
WalletService.prototype._clientSupportsTXPv3 = function() {
|
|
||||||
var version = this._parseClientVersion();
|
|
||||||
if (!version) return false;
|
|
||||||
if (version.agent != 'bwc') return true; // Asume 3rd party clients are up-to-date
|
|
||||||
if (version.major < 2) return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
WalletService.prototype._clientSupportsPayProRefund = function() {
|
WalletService.prototype._clientSupportsPayProRefund = function() {
|
||||||
var version = this._parseClientVersion();
|
var version = this._parseClientVersion();
|
||||||
if (!version) return false;
|
if (!version) return false;
|
||||||
|
@ -1021,37 +1005,6 @@ WalletService.prototype._totalizeUtxos = function(utxos) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
WalletService.prototype._computeBytesToSendMax = function(utxos, cb) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var size = {
|
|
||||||
all: 0,
|
|
||||||
confirmed: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
var unlockedUtxos = _.reject(utxos, 'locked');
|
|
||||||
if (_.isEmpty(unlockedUtxos)) return cb(null, size);
|
|
||||||
|
|
||||||
self.getWallet({}, function(err, wallet) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
|
|
||||||
var txp = Model.TxProposalLegacy.create({
|
|
||||||
walletId: self.walletId,
|
|
||||||
requiredSignatures: wallet.m,
|
|
||||||
walletN: wallet.n,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
txp.inputs = unlockedUtxos;
|
|
||||||
size.all = txp.getEstimatedSize();
|
|
||||||
|
|
||||||
txp.inputs = _.filter(unlockedUtxos, 'confirmations');
|
|
||||||
size.confirmed = txp.getEstimatedSize();
|
|
||||||
|
|
||||||
return cb(null, size);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
WalletService.prototype._getBalanceFromAddresses = function(addresses, cb) {
|
WalletService.prototype._getBalanceFromAddresses = function(addresses, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -1076,15 +1029,8 @@ WalletService.prototype._getBalanceFromAddresses = function(addresses, cb) {
|
||||||
|
|
||||||
balance.byAddress = _.values(byAddress);
|
balance.byAddress = _.values(byAddress);
|
||||||
|
|
||||||
self._computeBytesToSendMax(utxos, function(err, size) {
|
|
||||||
if (err) {
|
|
||||||
log.error('Could not compute size of send max transaction', err);
|
|
||||||
}
|
|
||||||
balance.totalBytesToSendMax = _.isNumber(size.all) ? size.all : null;
|
|
||||||
balance.totalBytesToSendConfirmedMax = _.isNumber(size.confirmed) ? size.confirmed : null;
|
|
||||||
return cb(null, balance);
|
return cb(null, balance);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService.prototype._getBalanceOneStep = function(opts, cb) {
|
WalletService.prototype._getBalanceOneStep = function(opts, cb) {
|
||||||
|
@ -1722,153 +1668,6 @@ WalletService.prototype._validateOutputs = function(opts, wallet, cb) {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService._getProposalHash = function(proposalHeader) {
|
|
||||||
function getOldHash(toAddress, amount, message, payProUrl) {
|
|
||||||
return [toAddress, amount, (message || ''), (payProUrl || '')].join('|');
|
|
||||||
};
|
|
||||||
|
|
||||||
// For backwards compatibility
|
|
||||||
if (arguments.length > 1) {
|
|
||||||
return getOldHash.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Stringify(proposalHeader);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new transaction proposal.
|
|
||||||
* @param {Object} opts
|
|
||||||
* @param {string} opts.type - Proposal type.
|
|
||||||
* @param {string} opts.toAddress || opts.outputs[].toAddress - Destination address.
|
|
||||||
* @param {number} opts.amount || opts.outputs[].amount - Amount to transfer in satoshi.
|
|
||||||
* @param {string} opts.outputs[].message - A message to attach to this output.
|
|
||||||
* @param {string} opts.message - A message to attach to this transaction.
|
|
||||||
* @param {string} opts.proposalSignature - S(toAddress|amount|message|payProUrl). Used by other copayers to verify the proposal.
|
|
||||||
* @param {string} opts.inputs - Optional. Inputs for this TX
|
|
||||||
* @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX
|
|
||||||
* @param {string} opts.payProUrl - Optional: Paypro URL for peers to verify TX
|
|
||||||
* @param {string} opts.excludeUnconfirmedUtxos - Optional: Do not use UTXOs of unconfirmed transactions as inputs
|
|
||||||
* @returns {TxProposal} Transaction proposal.
|
|
||||||
*/
|
|
||||||
WalletService.prototype.createTxLegacy = function(opts, cb) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (!opts.outputs) {
|
|
||||||
opts.outputs = _.pick(opts, ['amount', 'toAddress']);
|
|
||||||
}
|
|
||||||
opts.outputs = [].concat(opts.outputs);
|
|
||||||
|
|
||||||
if (!checkRequired(opts, ['outputs', 'proposalSignature'], cb)) return;
|
|
||||||
|
|
||||||
var type = opts.type || Model.TxProposalLegacy.Types.SIMPLE;
|
|
||||||
if (!Model.TxProposalLegacy.isTypeSupported(type))
|
|
||||||
return cb(new ClientError('Invalid proposal type'));
|
|
||||||
|
|
||||||
var feePerKb = opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB;
|
|
||||||
if (feePerKb < Defaults.MIN_FEE_PER_KB || feePerKb > Defaults.MAX_FEE_PER_KB)
|
|
||||||
return cb(new ClientError('Invalid fee per KB value'));
|
|
||||||
|
|
||||||
self._runLocked(cb, function(cb) {
|
|
||||||
self.getWallet({}, function(err, wallet) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
|
|
||||||
|
|
||||||
if (wallet.singleAddress) return cb(new ClientError('Not compatible with single-address wallets'));
|
|
||||||
|
|
||||||
if (opts.payProUrl) {
|
|
||||||
if (wallet.addressType == Constants.SCRIPT_TYPES.P2PKH && !self._clientSupportsPayProRefund()) {
|
|
||||||
return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To sign this spend proposal you need to upgrade your client app.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var copayer = wallet.getCopayer(self.copayerId);
|
|
||||||
var hash;
|
|
||||||
if (!opts.type || opts.type == Model.TxProposalLegacy.Types.SIMPLE) {
|
|
||||||
hash = WalletService._getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl);
|
|
||||||
} else {
|
|
||||||
// should match bwc api _computeProposalSignature
|
|
||||||
var header = {
|
|
||||||
outputs: _.map(opts.outputs, function(output) {
|
|
||||||
return _.pick(output, ['toAddress', 'amount', 'message']);
|
|
||||||
}),
|
|
||||||
message: opts.message,
|
|
||||||
payProUrl: opts.payProUrl
|
|
||||||
};
|
|
||||||
hash = WalletService._getProposalHash(header)
|
|
||||||
}
|
|
||||||
|
|
||||||
var signingKey = self._getSigningKey(hash, opts.proposalSignature, copayer.requestPubKeys)
|
|
||||||
if (!signingKey)
|
|
||||||
return cb(new ClientError('Invalid proposal signature'));
|
|
||||||
|
|
||||||
self._canCreateTx(function(err, canCreate) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
if (!canCreate) return cb(Errors.TX_CANNOT_CREATE);
|
|
||||||
|
|
||||||
if (type != Model.TxProposalLegacy.Types.EXTERNAL) {
|
|
||||||
var validationError = self._validateOutputs(opts, wallet, cb);
|
|
||||||
if (validationError) {
|
|
||||||
return cb(validationError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var txOpts = {
|
|
||||||
type: type,
|
|
||||||
walletId: self.walletId,
|
|
||||||
creatorId: self.copayerId,
|
|
||||||
outputs: opts.outputs,
|
|
||||||
inputs: opts.inputs,
|
|
||||||
toAddress: opts.toAddress,
|
|
||||||
amount: opts.amount,
|
|
||||||
message: opts.message,
|
|
||||||
proposalSignature: opts.proposalSignature,
|
|
||||||
changeAddress: wallet.createAddress(true),
|
|
||||||
feePerKb: feePerKb,
|
|
||||||
payProUrl: opts.payProUrl,
|
|
||||||
requiredSignatures: wallet.m,
|
|
||||||
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1),
|
|
||||||
walletN: wallet.n,
|
|
||||||
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
|
|
||||||
addressType: wallet.addressType,
|
|
||||||
derivationStrategy: wallet.derivationStrategy,
|
|
||||||
customData: opts.customData,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (signingKey.selfSigned) {
|
|
||||||
txOpts.proposalSignaturePubKey = signingKey.key;
|
|
||||||
txOpts.proposalSignaturePubKeySig = signingKey.signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
var txp = Model.TxProposalLegacy.create(txOpts);
|
|
||||||
|
|
||||||
if (!self._clientSupportsTXPv2()) {
|
|
||||||
txp.version = '1.0.1';
|
|
||||||
}
|
|
||||||
|
|
||||||
self._selectTxInputs(txp, opts.utxosToExclude, function(err) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
|
|
||||||
$.checkState(txp.inputs);
|
|
||||||
|
|
||||||
self.storage.storeAddressAndWallet(wallet, txp.changeAddress, function(err) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
|
|
||||||
self.storage.storeTx(wallet.id, txp, function(err) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
|
|
||||||
self._notify('NewTxProposal', {
|
|
||||||
amount: txp.getTotalAmount()
|
|
||||||
}, function() {
|
|
||||||
return cb(null, txp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) {
|
WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -2328,12 +2127,6 @@ WalletService.prototype.signTx = function(opts, cb) {
|
||||||
}, function(err, txp) {
|
}, function(err, txp) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
if (!self._clientSupportsTXPv2()) {
|
|
||||||
if (!_.startsWith(txp.version, '1.')) {
|
|
||||||
return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To sign this spend proposal you need to upgrade your client app.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var action = _.find(txp.actions, {
|
var action = _.find(txp.actions, {
|
||||||
copayerId: self.copayerId
|
copayerId: self.copayerId
|
||||||
});
|
});
|
||||||
|
@ -2528,13 +2321,6 @@ WalletService.prototype.getPendingTxs = function(opts, cb) {
|
||||||
self.storage.fetchPendingTxs(self.walletId, function(err, txps) {
|
self.storage.fetchPendingTxs(self.walletId, function(err, txps) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
var v3Txps = _.any(txps, function(txp) {
|
|
||||||
return txp.version >= 3;
|
|
||||||
});
|
|
||||||
if (v3Txps && !self._clientSupportsTXPv3()) {
|
|
||||||
return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To view some of the pending proposals you need to upgrade your client app.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
_.each(txps, function(txp) {
|
_.each(txps, function(txp) {
|
||||||
txp.deleteLockTime = self.getRemainingDeleteLockTime(txp);
|
txp.deleteLockTime = self.getRemainingDeleteLockTime(txp);
|
||||||
});
|
});
|
||||||
|
|
|
@ -80,11 +80,14 @@ describe('Email notifications', function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
var txOpts = {
|
||||||
message: 'some message'
|
outputs: [{
|
||||||
});
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
server.createTxLegacy(txOpts, function(err, tx) {
|
amount: 0.8e8
|
||||||
should.not.exist(err);
|
}],
|
||||||
|
feePerKb: 100e2
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
var calls = mailerStub.sendMail.getCalls();
|
var calls = mailerStub.sendMail.getCalls();
|
||||||
calls.length.should.equal(2);
|
calls.length.should.equal(2);
|
||||||
|
@ -117,11 +120,14 @@ describe('Email notifications', function() {
|
||||||
_applyTemplate_old.call(emailService, template, undefined, cb);
|
_applyTemplate_old.call(emailService, template, undefined, cb);
|
||||||
};
|
};
|
||||||
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
var txOpts = {
|
||||||
message: 'some message'
|
outputs: [{
|
||||||
});
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
server.createTxLegacy(txOpts, function(err, tx) {
|
amount: 0.8e8
|
||||||
should.not.exist(err);
|
}],
|
||||||
|
feePerKb: 100e2
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
var calls = mailerStub.sendMail.getCalls();
|
var calls = mailerStub.sendMail.getCalls();
|
||||||
calls.length.should.equal(0);
|
calls.length.should.equal(0);
|
||||||
|
@ -146,15 +152,21 @@ describe('Email notifications', function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
var txOpts = {
|
||||||
message: 'some message'
|
outputs: [{
|
||||||
});
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.8e8
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2
|
||||||
|
};
|
||||||
|
|
||||||
var txp;
|
var txp;
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
|
||||||
function(next) {
|
function(next) {
|
||||||
server.createTxLegacy(txOpts, next);
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
next(null, tx);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
function(t, next) {
|
function(t, next) {
|
||||||
txp = t;
|
txp = t;
|
||||||
|
@ -207,15 +219,21 @@ describe('Email notifications', function() {
|
||||||
|
|
||||||
it('should notify copayers a tx has been finally rejected', function(done) {
|
it('should notify copayers a tx has been finally rejected', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
var txOpts = {
|
||||||
message: 'some message'
|
outputs: [{
|
||||||
});
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.8e8
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2
|
||||||
|
};
|
||||||
|
|
||||||
var txpId;
|
var txpId;
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
|
||||||
function(next) {
|
function(next) {
|
||||||
server.createTxLegacy(txOpts, next);
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
next(null, tx);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
function(txp, next) {
|
function(txp, next) {
|
||||||
txpId = txp.id;
|
txpId = txp.id;
|
||||||
|
@ -378,11 +396,14 @@ describe('Email notifications', function() {
|
||||||
},
|
},
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
var txOpts = {
|
||||||
message: 'some message'
|
outputs: [{
|
||||||
});
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
server.createTxLegacy(txOpts, function(err, tx) {
|
amount: 0.8e8
|
||||||
should.not.exist(err);
|
}],
|
||||||
|
feePerKb: 100e2
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
var calls = mailerStub.sendMail.getCalls();
|
var calls = mailerStub.sendMail.getCalls();
|
||||||
calls.length.should.equal(2);
|
calls.length.should.equal(2);
|
||||||
|
@ -444,11 +465,14 @@ describe('Email notifications', function() {
|
||||||
|
|
||||||
it('should NOT notify copayers a new tx proposal has been created', function(done) {
|
it('should NOT notify copayers a new tx proposal has been created', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
var txOpts = {
|
||||||
message: 'some message'
|
outputs: [{
|
||||||
});
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
server.createTxLegacy(txOpts, function(err, tx) {
|
amount: 0.8e8
|
||||||
should.not.exist(err);
|
}],
|
||||||
|
feePerKb: 100e2
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
var calls = mailerStub.sendMail.getCalls();
|
var calls = mailerStub.sendMail.getCalls();
|
||||||
calls.length.should.equal(0);
|
calls.length.should.equal(0);
|
||||||
|
|
|
@ -391,46 +391,6 @@ helpers.clientSign = function(txp, derivedXPrivKey) {
|
||||||
return signatures;
|
return signatures;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
helpers.createProposalOptsLegacy = function(toAddress, amount, message, signingKey, feePerKb) {
|
|
||||||
var opts = {
|
|
||||||
toAddress: toAddress,
|
|
||||||
amount: helpers.toSatoshi(amount),
|
|
||||||
message: message,
|
|
||||||
proposalSignature: null,
|
|
||||||
};
|
|
||||||
if (feePerKb) opts.feePerKb = feePerKb;
|
|
||||||
|
|
||||||
var hash = WalletService._getProposalHash(toAddress, opts.amount, message);
|
|
||||||
|
|
||||||
try {
|
|
||||||
opts.proposalSignature = helpers.signMessage(hash, signingKey);
|
|
||||||
} catch (ex) {}
|
|
||||||
|
|
||||||
return opts;
|
|
||||||
};
|
|
||||||
|
|
||||||
helpers.createSimpleProposalOpts = function(toAddress, amount, signingKey, opts) {
|
|
||||||
var outputs = [{
|
|
||||||
toAddress: toAddress,
|
|
||||||
amount: amount,
|
|
||||||
}];
|
|
||||||
return helpers.createProposalOpts(Model.TxProposalLegacy.Types.SIMPLE, outputs, signingKey, opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
helpers.createExternalProposalOpts = function(toAddress, amount, signingKey, moreOpts, inputs) {
|
|
||||||
var outputs = [{
|
|
||||||
toAddress: toAddress,
|
|
||||||
amount: amount,
|
|
||||||
}];
|
|
||||||
if (_.isArray(moreOpts)) {
|
|
||||||
inputs = moreOpts;
|
|
||||||
moreOpts = null;
|
|
||||||
}
|
|
||||||
return helpers.createProposalOpts(Model.TxProposalLegacy.Types.EXTERNAL, outputs, signingKey, moreOpts, inputs);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
helpers.getProposalSignatureOpts = function(txp, signingKey) {
|
helpers.getProposalSignatureOpts = function(txp, signingKey) {
|
||||||
var raw = txp.getRawTx();
|
var raw = txp.getRawTx();
|
||||||
var proposalSignature = helpers.signMessage(raw, signingKey);
|
var proposalSignature = helpers.signMessage(raw, signingKey);
|
||||||
|
@ -442,48 +402,6 @@ helpers.getProposalSignatureOpts = function(txp, signingKey) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts, inputs) {
|
|
||||||
_.each(outputs, function(output) {
|
|
||||||
output.amount = helpers.toSatoshi(output.amount);
|
|
||||||
});
|
|
||||||
|
|
||||||
var opts = {
|
|
||||||
type: type,
|
|
||||||
proposalSignature: null,
|
|
||||||
inputs: inputs || []
|
|
||||||
};
|
|
||||||
|
|
||||||
if (moreOpts) {
|
|
||||||
moreOpts = _.pick(moreOpts, ['feePerKb', 'customData', 'message', 'payProUrl']);
|
|
||||||
opts = _.assign(opts, moreOpts);
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = _.defaults(opts, {
|
|
||||||
message: null
|
|
||||||
});
|
|
||||||
|
|
||||||
var hash;
|
|
||||||
if (type == Model.TxProposalLegacy.Types.SIMPLE) {
|
|
||||||
opts.toAddress = outputs[0].toAddress;
|
|
||||||
opts.amount = outputs[0].amount;
|
|
||||||
hash = WalletService._getProposalHash(opts.toAddress, opts.amount,
|
|
||||||
opts.message, opts.payProUrl);
|
|
||||||
} else if (type == Model.TxProposalLegacy.Types.MULTIPLEOUTPUTS || type == Model.TxProposalLegacy.Types.EXTERNAL) {
|
|
||||||
opts.outputs = outputs;
|
|
||||||
var header = {
|
|
||||||
outputs: outputs,
|
|
||||||
message: opts.message,
|
|
||||||
payProUrl: opts.payProUrl
|
|
||||||
};
|
|
||||||
hash = WalletService._getProposalHash(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
opts.proposalSignature = helpers.signMessage(hash, signingKey);
|
|
||||||
} catch (ex) {}
|
|
||||||
|
|
||||||
return opts;
|
|
||||||
};
|
|
||||||
helpers.createAddresses = function(server, wallet, main, change, cb) {
|
helpers.createAddresses = function(server, wallet, main, change, cb) {
|
||||||
// var clock = sinon.useFakeTimers('Date');
|
// var clock = sinon.useFakeTimers('Date');
|
||||||
async.mapSeries(_.range(main + change), function(i, next) {
|
async.mapSeries(_.range(main + change), function(i, next) {
|
||||||
|
|
|
@ -275,9 +275,6 @@ describe('Push notifications', function() {
|
||||||
|
|
||||||
it('should notify copayers a new tx proposal has been created', function(done) {
|
it('should notify copayers a new tx proposal has been created', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
|
||||||
message: 'some message'
|
|
||||||
});
|
|
||||||
server.createAddress({}, function(err, address) {
|
server.createAddress({}, function(err, address) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
server._notify('NewTxProposal', {
|
server._notify('NewTxProposal', {
|
||||||
|
@ -300,15 +297,21 @@ describe('Push notifications', function() {
|
||||||
|
|
||||||
it('should notify copayers a tx has been finally rejected', function(done) {
|
it('should notify copayers a tx has been finally rejected', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
var txOpts = {
|
||||||
message: 'some message'
|
outputs: [{
|
||||||
});
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.8e8
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2
|
||||||
|
};
|
||||||
|
|
||||||
var txpId;
|
var txpId;
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
|
||||||
function(next) {
|
function(next) {
|
||||||
server.createTxLegacy(txOpts, next);
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
next(null, tx);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
function(txp, next) {
|
function(txp, next) {
|
||||||
txpId = txp.id;
|
txpId = txp.id;
|
||||||
|
@ -341,15 +344,21 @@ describe('Push notifications', function() {
|
||||||
|
|
||||||
it('should notify copayers a new outgoing tx has been created', function(done) {
|
it('should notify copayers a new outgoing tx has been created', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
|
var txOpts = {
|
||||||
message: 'some message'
|
outputs: [{
|
||||||
});
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.8e8
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2
|
||||||
|
};
|
||||||
|
|
||||||
var txp;
|
var txp;
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
|
||||||
function(next) {
|
function(next) {
|
||||||
server.createTxLegacy(txOpts, next);
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
next(null, tx);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
function(t, next) {
|
function(t, next) {
|
||||||
txp = t;
|
txp = t;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,237 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var _ = require('lodash');
|
|
||||||
var chai = require('chai');
|
|
||||||
var sinon = require('sinon');
|
|
||||||
var should = chai.should();
|
|
||||||
var TxProposal = require('../../lib/model/txproposal_legacy');
|
|
||||||
var Bitcore = require('bitcore-lib');
|
|
||||||
|
|
||||||
describe('TXProposal legacy', function() {
|
|
||||||
|
|
||||||
describe('#create', function() {
|
|
||||||
it('should create a TxProposal', function() {
|
|
||||||
var txp = TxProposal.create(aTxpOpts());
|
|
||||||
should.exist(txp);
|
|
||||||
should.exist(txp.toAddress);
|
|
||||||
should.not.exist(txp.outputs);
|
|
||||||
});
|
|
||||||
it('should create a multiple-outputs TxProposal', function() {
|
|
||||||
var txp = TxProposal.create(aTxpOpts(TxProposal.Types.MULTIPLEOUTPUTS));
|
|
||||||
should.exist(txp);
|
|
||||||
should.not.exist(txp.toAddress);
|
|
||||||
should.exist(txp.outputs);
|
|
||||||
});
|
|
||||||
it('should create an external TxProposal', function() {
|
|
||||||
var txp = TxProposal.create(aTxpOpts(TxProposal.Types.EXTERNAL));
|
|
||||||
should.exist(txp);
|
|
||||||
should.not.exist(txp.toAddress);
|
|
||||||
should.exist(txp.outputs);
|
|
||||||
should.exist(txp.inputs);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#fromObj', function() {
|
|
||||||
it('should copy a TxProposal', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP());
|
|
||||||
should.exist(txp);
|
|
||||||
txp.toAddress.should.equal(aTXP().toAddress);
|
|
||||||
});
|
|
||||||
it('should copy a multiple-outputs TxProposal', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP(TxProposal.Types.MULTIPLEOUTPUTS));
|
|
||||||
should.exist(txp);
|
|
||||||
txp.outputs.should.deep.equal(aTXP(TxProposal.Types.MULTIPLEOUTPUTS).outputs);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getBitcoreTx', function() {
|
|
||||||
it('should create a valid bitcore TX', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP());
|
|
||||||
var t = txp.getBitcoreTx();
|
|
||||||
should.exist(t);
|
|
||||||
});
|
|
||||||
it('should order outputs as specified by outputOrder', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP());
|
|
||||||
|
|
||||||
txp.outputOrder = [0, 1];
|
|
||||||
var t = txp.getBitcoreTx();
|
|
||||||
t.getChangeOutput().should.deep.equal(t.outputs[1]);
|
|
||||||
|
|
||||||
txp.outputOrder = [1, 0];
|
|
||||||
var t = txp.getBitcoreTx();
|
|
||||||
t.getChangeOutput().should.deep.equal(t.outputs[0]);
|
|
||||||
});
|
|
||||||
it('should create a bitcore TX with multiple outputs', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP(TxProposal.Types.MULTIPLEOUTPUTS));
|
|
||||||
txp.outputOrder = [0, 1, 2];
|
|
||||||
var t = txp.getBitcoreTx();
|
|
||||||
t.getChangeOutput().should.deep.equal(t.outputs[2]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getTotalAmount', function() {
|
|
||||||
it('should be compatible with simple proposal legacy amount', function() {
|
|
||||||
var x = TxProposal.fromObj(aTXP());
|
|
||||||
var total = x.getTotalAmount();
|
|
||||||
total.should.equal(x.amount);
|
|
||||||
});
|
|
||||||
it('should handle multiple-outputs', function() {
|
|
||||||
var x = TxProposal.fromObj(aTXP(TxProposal.Types.MULTIPLEOUTPUTS));
|
|
||||||
var totalOutput = 0;
|
|
||||||
_.each(x.outputs, function(o) {
|
|
||||||
totalOutput += o.amount
|
|
||||||
});
|
|
||||||
x.getTotalAmount().should.equal(totalOutput);
|
|
||||||
});
|
|
||||||
it('should handle external', function() {
|
|
||||||
var x = TxProposal.fromObj(aTXP(TxProposal.Types.EXTERNAL));
|
|
||||||
var totalOutput = 0;
|
|
||||||
_.each(x.outputs, function(o) {
|
|
||||||
totalOutput += o.amount
|
|
||||||
});
|
|
||||||
x.getTotalAmount().should.equal(totalOutput);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#sign', function() {
|
|
||||||
it('should sign 2-2', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP());
|
|
||||||
txp.sign('1', theSignatures, theXPub);
|
|
||||||
txp.isAccepted().should.equal(false);
|
|
||||||
txp.isRejected().should.equal(false);
|
|
||||||
txp.sign('2', theSignatures, theXPub);
|
|
||||||
txp.isAccepted().should.equal(true);
|
|
||||||
txp.isRejected().should.equal(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getRawTx', function() {
|
|
||||||
it('should generate correct raw transaction for signed 2-2', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP());
|
|
||||||
txp.sign('1', theSignatures, theXPub);
|
|
||||||
txp.getRawTx().should.equal('0100000001ab069f7073be9b491bb1ad4233a45d2e383082ccc7206df905662d6d8499e66e080000009200483045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9014752210319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f2103b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d10130152aeffffffff0280f0fa02000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac70c9fa020000000017a914778192003f0e9e1d865c082179cc3dae5464b03d8700000000');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
describe('#reject', function() {
|
|
||||||
it('should reject 2-2', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP());
|
|
||||||
txp.reject('1');
|
|
||||||
txp.isAccepted().should.equal(false);
|
|
||||||
txp.isRejected().should.equal(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('#reject & #sign', function() {
|
|
||||||
it('should finally reject', function() {
|
|
||||||
var txp = TxProposal.fromObj(aTXP());
|
|
||||||
txp.sign('1', theSignatures);
|
|
||||||
txp.isAccepted().should.equal(false);
|
|
||||||
txp.isRejected().should.equal(false);
|
|
||||||
txp.reject('2');
|
|
||||||
txp.isAccepted().should.equal(false);
|
|
||||||
txp.isRejected().should.equal(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
var theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e';
|
|
||||||
var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9';
|
|
||||||
var theSignatures = ['3045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9'];
|
|
||||||
|
|
||||||
var aTxpOpts = function(type) {
|
|
||||||
var opts = {
|
|
||||||
type: type,
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 50000000,
|
|
||||||
message: 'some message'
|
|
||||||
};
|
|
||||||
if (type == TxProposal.Types.MULTIPLEOUTPUTS || type == TxProposal.Types.EXTERNAL) {
|
|
||||||
opts.outputs = [{
|
|
||||||
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
|
||||||
amount: 10000000,
|
|
||||||
message: "first message"
|
|
||||||
}, {
|
|
||||||
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
|
||||||
amount: 20000000,
|
|
||||||
message: "second message"
|
|
||||||
}, ];
|
|
||||||
delete opts.toAddress;
|
|
||||||
delete opts.amount;
|
|
||||||
}
|
|
||||||
if (type == TxProposal.Types.EXTERNAL) {
|
|
||||||
opts.inputs = [{
|
|
||||||
"txid": "6ee699846d2d6605f96d20c7cc8230382e5da43342adb11b499bbe73709f06ab",
|
|
||||||
"vout": 8,
|
|
||||||
"satoshis": 100000000,
|
|
||||||
"scriptPubKey": "a914a8a9648754fbda1b6c208ac9d4e252075447f36887",
|
|
||||||
"address": "3H4pNP6J4PW4NnvdrTg37VvZ7h2QWuAwtA",
|
|
||||||
"path": "m/2147483647/0/1",
|
|
||||||
"publicKeys": ["0319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f", "03b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d101301"]
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
return opts;
|
|
||||||
};
|
|
||||||
|
|
||||||
var aTXP = function(type) {
|
|
||||||
var txp = {
|
|
||||||
"version": '2.0.0',
|
|
||||||
"type": type,
|
|
||||||
"createdOn": 1423146231,
|
|
||||||
"id": "75c34f49-1ed6-255f-e9fd-0c71ae75ed1e",
|
|
||||||
"walletId": "1",
|
|
||||||
"creatorId": "1",
|
|
||||||
"toAddress": "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
|
||||||
"network": "livenet",
|
|
||||||
"amount": 50000000,
|
|
||||||
"message": 'some message',
|
|
||||||
"proposalSignature": '7035022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9',
|
|
||||||
"changeAddress": {
|
|
||||||
"version": '1.0.0',
|
|
||||||
"createdOn": 1424372337,
|
|
||||||
"address": '3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH',
|
|
||||||
"path": 'm/2147483647/1/0',
|
|
||||||
"publicKeys": ['030562cb099e6043dc499eb359dd97c9d500a3586498e4bcf0228a178cc20e6f16',
|
|
||||||
'0367027d17dbdfc27b5e31f8ed70e14d47949f0fa392261e977db0851c8b0d6fac',
|
|
||||||
'0315ae1e8aa866794ae603389fb2b8549153ebf04e7cdf74501dadde5c75ddad11'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"inputs": [{
|
|
||||||
"txid": "6ee699846d2d6605f96d20c7cc8230382e5da43342adb11b499bbe73709f06ab",
|
|
||||||
"vout": 8,
|
|
||||||
"satoshis": 100000000,
|
|
||||||
"scriptPubKey": "a914a8a9648754fbda1b6c208ac9d4e252075447f36887",
|
|
||||||
"address": "3H4pNP6J4PW4NnvdrTg37VvZ7h2QWuAwtA",
|
|
||||||
"path": "m/2147483647/0/1",
|
|
||||||
"publicKeys": ["0319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f", "03b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d101301"]
|
|
||||||
}],
|
|
||||||
"inputPaths": ["m/2147483647/0/1"],
|
|
||||||
"requiredSignatures": 2,
|
|
||||||
"requiredRejections": 1,
|
|
||||||
"walletN": 2,
|
|
||||||
"status": "pending",
|
|
||||||
"actions": [],
|
|
||||||
"outputOrder": [0, 1],
|
|
||||||
"fee": 10000,
|
|
||||||
};
|
|
||||||
if (type == TxProposal.Types.MULTIPLEOUTPUTS) {
|
|
||||||
txp.outputs = [{
|
|
||||||
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
|
||||||
amount: 10000000,
|
|
||||||
message: "first message"
|
|
||||||
}, {
|
|
||||||
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
|
||||||
amount: 20000000,
|
|
||||||
message: "second message"
|
|
||||||
}, ];
|
|
||||||
txp.outputOrder = [0, 1, 2];
|
|
||||||
delete txp.toAddress;
|
|
||||||
delete txp.amount;
|
|
||||||
}
|
|
||||||
return txp;
|
|
||||||
};
|
|
|
@ -142,13 +142,19 @@ describe('Storage', function() {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
|
|
||||||
proposals = _.map(_.range(4), function(i) {
|
proposals = _.map(_.range(4), function(i) {
|
||||||
var tx = Model.TxProposalLegacy.create({
|
var tx = Model.TxProposal.create({
|
||||||
walletId: '123',
|
walletId: '123',
|
||||||
|
outputs: [{
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
creatorId: wallet.copayers[0].id,
|
|
||||||
amount: i + 100,
|
amount: i + 100,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
creatorId: wallet.copayers[0].id,
|
||||||
});
|
});
|
||||||
if (i % 2 == 0) {
|
if (i % 2 == 0) {
|
||||||
|
tx.status = 'pending';
|
||||||
|
tx.isPending().should.be.true;
|
||||||
|
} else {
|
||||||
tx.status = 'rejected';
|
tx.status = 'rejected';
|
||||||
tx.isPending().should.be.false;
|
tx.isPending().should.be.false;
|
||||||
}
|
}
|
||||||
|
@ -190,8 +196,8 @@ describe('Storage', function() {
|
||||||
should.exist(txs);
|
should.exist(txs);
|
||||||
txs.length.should.equal(2);
|
txs.length.should.equal(2);
|
||||||
txs = _.sortBy(txs, 'amount');
|
txs = _.sortBy(txs, 'amount');
|
||||||
txs[0].amount.should.equal(101);
|
txs[0].amount.should.equal(100);
|
||||||
txs[1].amount.should.equal(103);
|
txs[1].amount.should.equal(102);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue