Merge pull request #407 from isocolsky/ref/proposal-flow

WIP: Ref/proposal flow
This commit is contained in:
Matias Alejo Garcia 2015-12-09 10:25:11 -03:00
commit a1596e4dd6
14 changed files with 1928 additions and 856 deletions

View File

@ -2,6 +2,11 @@
var Constants = {};
Constants.NETWORKS = {
LIVENET: 'livenet',
TESTNET: 'testnet',
};
Constants.SCRIPT_TYPES = {
P2SH: 'P2SH',
P2PKH: 'P2PKH',

View File

@ -26,6 +26,7 @@ var errors = {
TX_NOT_ACCEPTED: 'The transaction proposal is not accepted',
TX_NOT_FOUND: 'Transaction proposal not found',
TX_NOT_PENDING: 'The transaction proposal is not pending',
UNAVAILABLE_UTXOS: 'Unavailable unspent outputs',
UPGRADE_NEEDED: 'Client app needs to be upgraded',
WALLET_ALREADY_EXISTS: 'Wallet already exists',
WALLET_FULL: 'Wallet full',

View File

@ -251,6 +251,15 @@ ExpressApp.prototype.start = function(opts, cb) {
router.post('/v1/txproposals/', function(req, res) {
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) {
getServerWithAuth(req, res, function(server) {
server.createTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
@ -360,6 +369,17 @@ ExpressApp.prototype.start = function(opts, cb) {
});
});
router.post('/v1/txproposals/:id/publish/', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.publishTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
res.end();
});
});
});
// TODO Check HTTP verb and URL name
router.post('/v1/txproposals/:id/broadcast/', function(req, res) {
getServerWithAuth(req, res, function(server) {

View File

@ -2,6 +2,7 @@ var Model = {};
Model.Wallet = require('./wallet');
Model.Copayer = require('./copayer');
Model.TxProposalLegacy = require('./txproposal_legacy');
Model.TxProposal = require('./txproposal');
Model.Address = require('./address');
Model.Notification = require('./notification');

View File

@ -13,60 +13,17 @@ var Common = require('../common');
var Constants = Common.Constants;
var Defaults = Common.Defaults;
var TxProposalLegacy = require('./txproposal_legacy');
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));
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;
x.version = 3;
var now = Date.now();
x.createdOn = Math.floor(now / 1000);
@ -75,55 +32,58 @@ TxProposal.create = function(opts) {
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.setInputs(opts.inputs);
x.outputs = _.map(opts.outputs, function(output) {
return _.pick(output, ['amount', 'toAddress', 'message']);
});
x.outputOrder = _.shuffle(_.range(x.outputs.length + 1));
x.walletM = opts.walletM;
x.walletN = opts.walletN;
x.status = 'pending';
x.requiredSignatures = x.walletM;
x.requiredRejections = Math.min(x.walletM, x.walletN - x.walletM + 1),
x.status = 'temporary';
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.addressType = opts.addressType || (x.walletN > 1 ? Constants.SCRIPT_TYPES.P2SH : Constants.SCRIPT_TYPES.P2PKH);
$.checkState(_.contains(_.values(Constants.SCRIPT_TYPES), x.addressType));
x.customData = opts.customData;
if (_.isFunction(TxProposal._create[x.type])) {
TxProposal._create[x.type](x, opts);
}
x.amount = x.getTotalAmount();
try {
x.network = opts.network || Bitcore.Address(x.outputs[0].toAddress).toObject().network;
} catch (ex) {}
$.checkState(_.contains(_.values(Constants.NETWORKS), x.network));
return x;
};
TxProposal.fromObj = function(obj) {
if (!(obj.version >= 3)) {
return TxProposalLegacy.fromObj(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.walletM = obj.walletM;
x.walletN = obj.walletN;
x.requiredSignatures = obj.requiredSignatures;
x.requiredRejections = obj.requiredRejections;
x.walletN = obj.walletN;
x.status = obj.status;
x.txid = obj.txid;
x.broadcastedOn = obj.broadcastedOn;
@ -136,11 +96,12 @@ TxProposal.fromObj = function(obj) {
x.network = obj.network;
x.feePerKb = obj.feePerKb;
x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos;
x.addressType = obj.addressType;
x.customData = obj.customData;
x.proposalSignature = obj.proposalSignature;
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;
};
@ -152,8 +113,8 @@ TxProposal.prototype.toObject = function() {
};
TxProposal.prototype.setInputs = function(inputs) {
this.inputs = inputs;
this.inputPaths = _.pluck(inputs, 'path');
this.inputs = inputs || [];
this.inputPaths = _.pluck(inputs, 'path') || [];
};
TxProposal.prototype._updateStatus = function() {
@ -184,29 +145,19 @@ TxProposal.prototype._buildTx = function() {
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);
}
_.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);
}
});
t.fee(self.fee);
t.change(self.changeAddress.address);
// Shuffle outputs for improved privacy
@ -223,12 +174,8 @@ TxProposal.prototype._buildTx = function() {
}
// 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);
var totalInputs = _.sum(self.inputs, 'satoshis');
var totalOutputs = _.sum(t.outputs, 'satoshis');
$.checkState(totalInputs - totalOutputs <= Defaults.MAX_TX_FEE);
@ -262,10 +209,6 @@ TxProposal.prototype.getBitcoreTx = function() {
return t;
};
TxProposal.prototype.getNetworkName = function() {
return Bitcore.Address(this.changeAddress.address).toObject().network;
};
TxProposal.prototype.getRawTx = function() {
var t = this.getBitcoreTx();
@ -281,7 +224,7 @@ TxProposal.prototype.getEstimatedSize = function() {
var inputSize = walletM * 72 + this.walletN * 36 + 44;
var outputSize = 34;
var nbInputs = this.inputs.length;
var nbOutputs = (_.isArray(this.outputs) ? this.outputs.length : 1) + 1;
var nbOutputs = (_.isArray(this.outputs) ? Math.max(1, this.outputs.length) : 1) + 1;
var size = overhead + inputSize * nbInputs + outputSize * nbOutputs;
@ -290,7 +233,6 @@ TxProposal.prototype.getEstimatedSize = function() {
TxProposal.prototype.estimateFee = function() {
var fee = this.feePerKb * this.getEstimatedSize() / 1000;
this.fee = parseInt(fee.toFixed(0));
};
@ -300,14 +242,7 @@ TxProposal.prototype.estimateFee = function() {
* @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;
}
return _.sum(this.outputs, 'amount');
};
/**
@ -410,8 +345,12 @@ TxProposal.prototype.reject = function(copayerId, reason) {
this.addAction(copayerId, 'reject', reason);
};
TxProposal.prototype.isTemporary = function() {
return this.status == 'temporary';
};
TxProposal.prototype.isPending = function() {
return !_.contains(['broadcasted', 'rejected'], this.status);
return !_.contains(['temporary', 'broadcasted', 'rejected'], this.status);
};
TxProposal.prototype.isAccepted = function() {

View File

@ -0,0 +1,437 @@
'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));
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.fee = obj.fee;
x.network = obj.network;
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.getEstimatedSize = function() {
// Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits.
var safetyMargin = 0.05;
var walletM = this.requiredSignatures;
var overhead = 4 + 4 + 9 + 9;
var inputSize = walletM * 72 + this.walletN * 36 + 44;
var outputSize = 34;
var nbInputs = this.inputs.length;
var nbOutputs = (_.isArray(this.outputs) ? 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;

View File

@ -136,7 +136,7 @@ WalletService.shutDown = function(cb) {
WalletService.getInstance = function(opts) {
opts = opts || {};
var server = new WalletService();
server.clientVersion = opts.clientVersion;
server._setClientVersion(opts.clientVersion);
return server;
};
@ -163,7 +163,7 @@ WalletService.getInstanceWithAuth = function(opts, cb) {
server.copayerId = opts.copayerId;
server.walletId = copayer.walletId;
server.clientVersion = opts.clientVersion;
server._setClientVersion(opts.clientVersion);
return cb(null, server);
});
};
@ -509,6 +509,11 @@ WalletService.prototype.addAccess = function(opts, cb) {
});
};
WalletService.prototype._setClientVersion = function(version) {
delete this.parsedClientVersion;
this.clientVersion = version;
};
WalletService.prototype._parseClientVersion = function() {
function parse(version) {
var v = {};
@ -543,6 +548,14 @@ WalletService.prototype._clientSupportsTXPv2 = function() {
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._getCopayerHash = function(name, xPubKey, requestPubKey) {
return [name, xPubKey, requestPubKey].join('|');
};
@ -923,7 +936,7 @@ WalletService.prototype._computeBytesToSendMax = function(utxos, cb) {
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
var txp = Model.TxProposal.create({
var txp = Model.TxProposalLegacy.create({
walletId: self.walletId,
requiredSignatures: wallet.m,
walletN: wallet.n,
@ -1146,9 +1159,9 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) {
});
};
WalletService.prototype._canCreateTx = function(copayerId, cb) {
WalletService.prototype._canCreateTx = function(cb) {
var self = this;
self.storage.fetchLastTxs(self.walletId, copayerId, 5 + Defaults.BACKOFF_OFFSET, function(err, txs) {
self.storage.fetchLastTxs(self.walletId, self.copayerId, 5 + Defaults.BACKOFF_OFFSET, function(err, txs) {
if (err) return cb(err);
if (!txs.length)
@ -1232,7 +1245,7 @@ WalletService._getProposalHash = function(proposalHeader) {
* @param {string} opts.excludeUnconfirmedUtxos - Optional: Do not use UTXOs of unconfirmed transactions as inputs
* @returns {TxProposal} Transaction proposal.
*/
WalletService.prototype.createTx = function(opts, cb) {
WalletService.prototype.createTxLegacy = function(opts, cb) {
var self = this;
if (!opts.outputs) {
@ -1243,8 +1256,8 @@ WalletService.prototype.createTx = function(opts, cb) {
if (!Utils.checkRequired(opts, ['outputs', 'proposalSignature']))
return cb(new ClientError('Required argument missing'));
var type = opts.type || Model.TxProposal.Types.SIMPLE;
if (!Model.TxProposal.isTypeSupported(type))
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;
@ -1258,7 +1271,7 @@ WalletService.prototype.createTx = function(opts, cb) {
var copayer = wallet.getCopayer(self.copayerId);
var hash;
if (!opts.type || opts.type == Model.TxProposal.Types.SIMPLE) {
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
@ -1276,11 +1289,11 @@ WalletService.prototype.createTx = function(opts, cb) {
if (!signingKey)
return cb(new ClientError('Invalid proposal signature'));
self._canCreateTx(self.copayerId, function(err, canCreate) {
self._canCreateTx(function(err, canCreate) {
if (err) return cb(err);
if (!canCreate) return cb(Errors.TX_CANNOT_CREATE);
if (type != Model.TxProposal.Types.EXTERNAL) {
if (type != Model.TxProposalLegacy.Types.EXTERNAL) {
var validationError = self._validateOutputs(opts, wallet);
if (validationError) {
return cb(validationError);
@ -1314,7 +1327,7 @@ WalletService.prototype.createTx = function(opts, cb) {
txOpts.proposalSignaturePubKeySig = signingKey.signature;
}
var txp = Model.TxProposal.create(txOpts);
var txp = Model.TxProposalLegacy.create(txOpts);
if (!self._clientSupportsTXPv2()) {
txp.version = '1.0.1';
@ -1344,6 +1357,160 @@ WalletService.prototype.createTx = function(opts, cb) {
});
};
/**
* Creates a new transaction proposal.
* @param {Object} opts
* @param {Array} opts.outputs - List of outputs.
* @param {string} opts.outputs[].toAddress - Destination address.
* @param {number} 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 {Array} 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[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs
* @param {string} opts.validateOutputs[=true] - Optional. Perform validation on outputs.
* @returns {TxProposal} Transaction proposal.
*/
WalletService.prototype.createTx = function(opts, cb) {
var self = this;
if (!Utils.checkRequired(opts, ['outputs']))
return cb(new ClientError('Required argument missing'));
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);
self._canCreateTx(function(err, canCreate) {
if (err) return cb(err);
if (!canCreate) return cb(Errors.TX_CANNOT_CREATE);
if (opts.validateOutputs !== false) {
var validationError = self._validateOutputs(opts, wallet);
if (validationError) {
return cb(validationError);
}
}
var txOpts = {
walletId: self.walletId,
creatorId: self.copayerId,
inputs: opts.inputs,
outputs: opts.outputs,
message: opts.message,
changeAddress: wallet.createAddress(true),
feePerKb: feePerKb,
payProUrl: opts.payProUrl,
walletM: wallet.m,
walletN: wallet.n,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
validateOutputs: !opts.validateOutputs,
addressType: wallet.addressType,
customData: opts.customData,
};
var txp = Model.TxProposal.create(txOpts);
self._selectTxInputs(txp, opts.utxosToExclude, function(err) {
if (err) return cb(err);
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._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
var pub = (new Bitcore.HDPublicKey(xPubKey)).derive(Constants.PATHS.REQUEST_KEY_AUTH).publicKey;
return Utils.verifyMessage(requestPubKey, signature, pub.toString());
};
/**
* Publish an already created tx proposal so inputs are locked and other copayers in the wallet can see it.
* @param {Object} opts
* @param {string} opts.txProposalId - The tx id.
* @param {string} opts.proposalSignature - S(raw tx). Used by other copayers to verify the proposal.
* @param {string} opts.proposalSignaturePubKey - (Optional) An alternative public key used to verify the proposal signature.
* @param {string} opts.proposalSignaturePubKeySig - (Optional) A signature used to validate the opts.proposalSignaturePubKey.
*/
WalletService.prototype.publishTx = function(opts, cb) {
var self = this;
function utxoKey(utxo) {
return utxo.txid + '|' + utxo.vout
};
if (!Utils.checkRequired(opts, ['txProposalId', 'proposalSignature']))
return cb(new ClientError('Required argument missing'));
self._runLocked(cb, function(cb) {
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
self.storage.fetchTx(self.walletId, opts.txProposalId, function(err, txp) {
if (err) return cb(err);
if (!txp) return cb(Errors.TX_NOT_FOUND);
if (!txp.isTemporary()) return cb();
var copayer = wallet.getCopayer(self.copayerId);
var raw = txp.getRawTx();
var signingKey = self._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys);
if (!signingKey) {
return cb(new ClientError('Invalid proposal signature'));
}
// Verify signingKey
if (opts.proposalSignaturePubKey) {
if (opts.proposalSignaturePubKey != signingKey ||
!self._verifyRequestPubKey(opts.proposalSignaturePubKey, opts.proposalSignaturePubKeySig, copayer.xPubKey)
) {
return cb(new ClientError('Invalid proposal signing key'));
}
}
// Verify UTXOs are still available
self.getUtxos({}, function(err, utxos) {
if (err) return cb(err);
var txpInputs = _.map(txp.inputs, utxoKey);
var lockedUtxoIndex = _.indexBy(_.filter(utxos, 'locked'), utxoKey);
var unavailable = _.any(txpInputs, function(i) {
return lockedUtxoIndex[i];
});
if (unavailable) return cb(Errors.UNAVAILABLE_UTXOS);
txp.status = 'pending';
self.storage.storeTx(self.walletId, txp, function(err) {
if (err) return cb(err);
return cb();
});
});
});
});
});
};
/**
* Retrieves a tx from storage.
* @param {Object} opts
@ -1678,6 +1845,13 @@ WalletService.prototype.getPendingTxs = function(opts, cb) {
self.storage.fetchPendingTxs(self.walletId, function(err, txps) {
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) {
txp.deleteLockTime = self.getRemainingDeleteLockTime(txp);
});

View File

@ -227,7 +227,7 @@ Storage.prototype.fetchPendingTxs = function(walletId, cb) {
this.db.collection(collections.TXS).find({
walletId: walletId,
isPending: true
isPending: true,
}).sort({
createdOn: -1
}).toArray(function(err, result) {

View File

@ -83,7 +83,7 @@ describe('Email notifications', function() {
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
message: 'some message'
});
server.createTx(txOpts, function(err, tx) {
server.createTxLegacy(txOpts, function(err, tx) {
should.not.exist(err);
setTimeout(function() {
var calls = mailerStub.sendMail.getCalls();
@ -120,7 +120,7 @@ describe('Email notifications', function() {
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
message: 'some message'
});
server.createTx(txOpts, function(err, tx) {
server.createTxLegacy(txOpts, function(err, tx) {
should.not.exist(err);
setTimeout(function() {
var calls = mailerStub.sendMail.getCalls();
@ -154,7 +154,7 @@ describe('Email notifications', function() {
async.waterfall([
function(next) {
server.createTx(txOpts, next);
server.createTxLegacy(txOpts, next);
},
function(t, next) {
txp = t;
@ -215,7 +215,7 @@ describe('Email notifications', function() {
async.waterfall([
function(next) {
server.createTx(txOpts, next);
server.createTxLegacy(txOpts, next);
},
function(txp, next) {
txpId = txp.id;
@ -381,7 +381,7 @@ describe('Email notifications', function() {
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
message: 'some message'
});
server.createTx(txOpts, function(err, tx) {
server.createTxLegacy(txOpts, function(err, tx) {
should.not.exist(err);
setTimeout(function() {
var calls = mailerStub.sendMail.getCalls();
@ -447,7 +447,7 @@ describe('Email notifications', function() {
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, {
message: 'some message'
});
server.createTx(txOpts, function(err, tx) {
server.createTxLegacy(txOpts, function(err, tx) {
should.not.exist(err);
setTimeout(function() {
var calls = mailerStub.sendMail.getCalls();

View File

@ -26,9 +26,11 @@ var TestData = require('../testdata');
var storage, blockchainExplorer;
var useMongoDb = !!process.env.USE_MONGO_DB;
var helpers = {};
var useMongoDb = !!process.env.USE_MONGO_DB;
helpers.CLIENT_VERSION = 'bwc-2.0.0';
helpers.before = function(cb) {
function getDb(cb) {
@ -96,7 +98,7 @@ helpers.getAuthServer = function(copayerId, cb) {
copayerId: copayerId,
message: 'dummy',
signature: 'dummy',
clientVersion: 'bwc-0.1.0',
clientVersion: helpers.CLIENT_VERSION,
}, function(err, server) {
verifyStub.restore();
if (err || !server) throw new Error('Could not login as copayerId ' + copayerId);
@ -356,7 +358,7 @@ helpers.createSimpleProposalOpts = function(toAddress, amount, signingKey, opts)
toAddress: toAddress,
amount: amount,
}];
return helpers.createProposalOpts(Model.TxProposal.Types.SIMPLE, outputs, signingKey, opts);
return helpers.createProposalOpts(Model.TxProposalLegacy.Types.SIMPLE, outputs, signingKey, opts);
};
helpers.createExternalProposalOpts = function(toAddress, amount, signingKey, moreOpts, inputs) {
@ -368,9 +370,43 @@ helpers.createExternalProposalOpts = function(toAddress, amount, signingKey, mor
inputs = moreOpts;
moreOpts = null;
}
return helpers.createProposalOpts(Model.TxProposal.Types.EXTERNAL, outputs, signingKey, moreOpts, inputs);
return helpers.createProposalOpts(Model.TxProposalLegacy.Types.EXTERNAL, outputs, signingKey, moreOpts, inputs);
};
helpers.createProposalOpts2 = function(outputs, moreOpts, inputs) {
_.each(outputs, function(output) {
output.amount = helpers.toSatoshi(output.amount);
});
var opts = {
outputs: outputs,
inputs: inputs || [],
};
if (moreOpts) {
moreOpts = _.pick(moreOpts, ['feePerKb', 'customData', 'message']);
opts = _.assign(opts, moreOpts);
}
opts = _.defaults(opts, {
message: null
});
return opts;
};
helpers.getProposalSignatureOpts = function(txp, signingKey) {
var raw = txp.getRawTx();
var proposalSignature = helpers.signMessage(raw, signingKey);
return {
txProposalId: txp.id,
proposalSignature: proposalSignature,
}
};
helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts, inputs) {
_.each(outputs, function(output) {
output.amount = helpers.toSatoshi(output.amount);
@ -383,9 +419,7 @@ helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts, input
};
if (moreOpts) {
moreOpts = _.chain(moreOpts)
.pick(['feePerKb', 'customData', 'message'])
.value();
moreOpts = _.pick(moreOpts, ['feePerKb', 'customData', 'message']);
opts = _.assign(opts, moreOpts);
}
@ -394,12 +428,12 @@ helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts, input
});
var hash;
if (type == Model.TxProposal.Types.SIMPLE) {
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.TxProposal.Types.MULTIPLEOUTPUTS || type == Model.TxProposal.Types.EXTERNAL) {
} else if (type == Model.TxProposalLegacy.Types.MULTIPLEOUTPUTS || type == Model.TxProposalLegacy.Types.EXTERNAL) {
opts.outputs = outputs;
var header = {
outputs: outputs,
@ -415,7 +449,6 @@ helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts, input
return opts;
};
helpers.createAddresses = function(server, wallet, main, change, cb) {
var clock = sinon.useFakeTimers(Date.now(), 'Date');
async.map(_.range(main + change), function(i, next) {

File diff suppressed because it is too large Load Diff

View File

@ -7,27 +7,14 @@ var should = chai.should();
var TxProposal = require('../../lib/model/txproposal');
var Bitcore = require('bitcore-lib');
describe('TXProposal', function() {
describe('TxProposal', 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);
txp.outputs.length.should.equal(2);
txp.amount.should.equal(30000000);
txp.network.should.equal('livenet');
});
});
@ -35,12 +22,7 @@ describe('TXProposal', 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);
txp.amount.should.equal(aTXP().amount);
});
});
@ -53,45 +35,29 @@ describe('TXProposal', function() {
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]);
txp.outputOrder = [2, 0, 1];
var t = txp.getBitcoreTx();
t.getChangeOutput().should.deep.equal(t.outputs[0]);
});
});
describe('#getTotalAmount', function() {
it('should be compatible with simple proposal legacy amount', function() {
it('should compute total 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('#getEstimatedSize', function() {
it('should return estimated size in bytes', function() {
var x = TxProposal.fromObj(aTXP());
x.getEstimatedSize().should.equal(407);
});
});
describe('#sign', function() {
@ -110,12 +76,10 @@ describe('TXProposal', 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');
txp.getRawTx().should.equal(theRawTx);
});
});
describe('#reject', function() {
it('should reject 2-2', function() {
var txp = TxProposal.fromObj(aTXP());
@ -125,7 +89,6 @@ describe('TXProposal', function() {
});
});
describe('#reject & #sign', function() {
it('should finally reject', function() {
var txp = TxProposal.fromObj(aTXP());
@ -142,53 +105,35 @@ describe('TXProposal', function() {
var theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e';
var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9';
var theSignatures = ['3045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9'];
var theSignatures = ['304402201d210f731fa8cb8473ce49554382ad5d950c963d48b173a0591f13ed8cee10ce022027b30dc3a55c46b1f977a72491d338fc14b6d13a7b1a7c5a35950d8543c1ced6'];
var theRawTx = '0100000001ab069f7073be9b491bb1ad4233a45d2e383082ccc7206df905662d6d8499e66e08000000910047304402201d210f731fa8cb8473ce49554382ad5d950c963d48b173a0591f13ed8cee10ce022027b30dc3a55c46b1f977a72491d338fc14b6d13a7b1a7c5a35950d8543c1ced6014752210319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f2103b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d10130152aeffffffff0380969800000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac002d3101000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac70f62b040000000017a914778192003f0e9e1d865c082179cc3dae5464b03d8700000000';
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"]
}];
}
opts.outputs = [{
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
amount: 10000000,
message: "first message"
}, {
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
amount: 20000000,
message: "second message"
}, ];
return opts;
};
var aTXP = function(type) {
var txp = {
"version": '2.0.0',
"type": type,
"version": 3,
"createdOn": 1423146231,
"id": "75c34f49-1ed6-255f-e9fd-0c71ae75ed1e",
"walletId": "1",
"creatorId": "1",
"toAddress": "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
"network": "livenet",
"amount": 50000000,
"amount": 30000000,
"message": 'some message',
"proposalSignature": '7035022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9',
"changeAddress": {
@ -214,24 +159,21 @@ var aTXP = function(type) {
"requiredSignatures": 2,
"requiredRejections": 1,
"walletN": 2,
"addressType": "P2SH",
"status": "pending",
"actions": [],
"outputOrder": [0, 1],
"fee": 10000,
};
if (type == TxProposal.Types.MULTIPLEOUTPUTS) {
txp.outputs = [{
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
amount: 10000000,
message: "first message"
"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;
}
"toAddress": "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
"amount": 20000000,
"message": "second message"
}, ],
"outputOrder": [0, 1, 2]
};
return txp;
};

View File

@ -0,0 +1,237 @@
'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;
};

View File

@ -141,7 +141,7 @@ describe('Storage', function() {
should.not.exist(err);
proposals = _.map(_.range(4), function(i) {
var tx = Model.TxProposal.create({
var tx = Model.TxProposalLegacy.create({
walletId: '123',
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
creatorId: wallet.copayers[0].id,
@ -182,7 +182,7 @@ describe('Storage', function() {
done();
});
});
it('should fetch all pending txs', function(done) {
storage.fetchPendingTxs('123', function(err, txs) {
should.not.exist(err);