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

Fix & improve new proposal flow
This commit is contained in:
Matias Alejo Garcia 2016-01-29 14:37:34 -03:00
commit f7afe89c4f
5 changed files with 405 additions and 275 deletions

View File

@ -5,7 +5,8 @@ var Defaults = {};
Defaults.DEFAULT_FEE_PER_KB = 10000; Defaults.DEFAULT_FEE_PER_KB = 10000;
Defaults.MIN_FEE_PER_KB = 0; Defaults.MIN_FEE_PER_KB = 0;
Defaults.MAX_FEE_PER_KB = 1000000; Defaults.MAX_FEE_PER_KB = 1000000;
Defaults.MAX_TX_FEE = 1 * 1e8; Defaults.MIN_TX_FEE = 0;
Defaults.MAX_TX_FEE = 0.1 * 1e8;
Defaults.MAX_KEYS = 100; Defaults.MAX_KEYS = 100;

View File

@ -44,7 +44,7 @@ TxProposal.create = function(opts) {
x.requiredRejections = Math.min(x.walletM, x.walletN - x.walletM + 1), x.requiredRejections = Math.min(x.walletM, x.walletN - x.walletM + 1),
x.status = 'temporary'; x.status = 'temporary';
x.actions = []; x.actions = [];
x.fee = null; x.fee = opts.fee;
x.feePerKb = opts.feePerKb; x.feePerKb = opts.feePerKb;
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos; x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
@ -74,6 +74,7 @@ TxProposal.fromObj = function(obj) {
x.id = obj.id; x.id = obj.id;
x.walletId = obj.walletId; x.walletId = obj.walletId;
x.creatorId = obj.creatorId; x.creatorId = obj.creatorId;
x.network = obj.network;
x.outputs = obj.outputs; x.outputs = obj.outputs;
x.amount = obj.amount; x.amount = obj.amount;
x.message = obj.message; x.message = obj.message;
@ -93,7 +94,6 @@ TxProposal.fromObj = function(obj) {
}); });
x.outputOrder = obj.outputOrder; x.outputOrder = obj.outputOrder;
x.fee = obj.fee; x.fee = obj.fee;
x.network = obj.network;
x.feePerKb = obj.feePerKb; x.feePerKb = obj.feePerKb;
x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos; x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos;
x.addressType = obj.addressType; x.addressType = obj.addressType;
@ -209,6 +209,10 @@ TxProposal.prototype.getBitcoreTx = function() {
return t; return t;
}; };
TxProposal.prototype.getNetworkName = function() {
return this.network;
};
TxProposal.prototype.getRawTx = function() { TxProposal.prototype.getRawTx = function() {
var t = this.getBitcoreTx(); var t = this.getBitcoreTx();
@ -232,6 +236,7 @@ TxProposal.prototype.getEstimatedSize = function() {
}; };
TxProposal.prototype.estimateFee = function() { TxProposal.prototype.estimateFee = function() {
$.checkState(_.isNumber(this.feePerKb));
var fee = this.feePerKb * this.getEstimatedSize() / 1000; var fee = this.feePerKb * this.getEstimatedSize() / 1000;
this.fee = parseInt(fee.toFixed(0)); this.fee = parseInt(fee.toFixed(0));
}; };

View File

@ -1,4 +1,5 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var $ = require('preconditions').singleton(); var $ = require('preconditions').singleton();
var async = require('async'); var async = require('async');
@ -650,8 +651,8 @@ WalletService.prototype.joinWallet = function(opts, cb) {
} }
if (_.find(wallet.copayers, { if (_.find(wallet.copayers, {
xPubKey: opts.xPubKey xPubKey: opts.xPubKey
})) return cb(Errors.COPAYER_IN_WALLET); })) return cb(Errors.COPAYER_IN_WALLET);
if (wallet.copayers.length == wallet.n) return cb(Errors.WALLET_FULL); if (wallet.copayers.length == wallet.n) return cb(Errors.WALLET_FULL);
@ -744,8 +745,8 @@ WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) {
isChange: true isChange: true
}), Defaults.MAX_MAIN_ADDRESS_GAP); }), Defaults.MAX_MAIN_ADDRESS_GAP);
if (latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, { if (latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, {
hasActivity: true hasActivity: true
})) return cb(null, true); })) return cb(null, true);
var bc = self._getBlockchainExplorer(latestAddresses[0].network); var bc = self._getBlockchainExplorer(latestAddresses[0].network);
var activityFound = false; var activityFound = false;
@ -1219,8 +1220,11 @@ WalletService.prototype._checkTxAndEstimateFee = function(txp) {
serializationOpts.disableLargeFees = true; serializationOpts.disableLargeFees = true;
} }
try { if (_.isNumber(txp.feePerKb)) {
txp.estimateFee(); txp.estimateFee();
}
try {
var bitcoreTx = txp.getBitcoreTx(); var bitcoreTx = txp.getBitcoreTx();
bitcoreError = bitcoreTx.getSerializationError(serializationOpts); bitcoreError = bitcoreTx.getSerializationError(serializationOpts);
if (!bitcoreError) { if (!bitcoreError) {
@ -1535,7 +1539,8 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
* @param {string} opts.outputs[].message - A message to attach to this output. * @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.message - A message to attach to this transaction.
* @param {Array} opts.inputs - Optional. Inputs for this TX * @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.fee - Optional. Use an alternative fee for this TX (mutually exclusive with feePerKb)
* @param {string} opts.feePerKb - Optional. Use an alternative fee per KB for this TX (mutually exclusive with fee)
* @param {string} opts.payProUrl - Optional. Paypro URL for peers to verify 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.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs
* @param {string} opts.validateOutputs[=true] - Optional. Perform validation on outputs. * @param {string} opts.validateOutputs[=true] - Optional. Perform validation on outputs.
@ -1547,9 +1552,16 @@ WalletService.prototype.createTx = function(opts, cb) {
if (!Utils.checkRequired(opts, ['outputs'])) if (!Utils.checkRequired(opts, ['outputs']))
return cb(new ClientError('Required argument missing')); return cb(new ClientError('Required argument missing'));
var feePerKb = opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB; if (_.isNumber(opts.fee)) {
if (feePerKb < Defaults.MIN_FEE_PER_KB || feePerKb > Defaults.MAX_FEE_PER_KB) opts.feePerKb = null;
return cb(new ClientError('Invalid fee per KB value')); if (opts.fee < Defaults.MIN_TX_FEE || opts.fee > Defaults.MAX_TX_FEE)
return cb(new ClientError('Invalid fee'));
} else {
opts.fee = null;
opts.feePerKb = opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB;
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
return cb(new ClientError('Invalid fee per KB'));
}
self._runLocked(cb, function(cb) { self._runLocked(cb, function(cb) {
self.getWallet({}, function(err, wallet) { self.getWallet({}, function(err, wallet) {
@ -1574,7 +1586,8 @@ WalletService.prototype.createTx = function(opts, cb) {
outputs: opts.outputs, outputs: opts.outputs,
message: opts.message, message: opts.message,
changeAddress: wallet.createAddress(true), changeAddress: wallet.createAddress(true),
feePerKb: feePerKb, fee: opts.fee,
feePerKb: opts.feePerKb,
payProUrl: opts.payProUrl, payProUrl: opts.payProUrl,
walletM: wallet.m, walletM: wallet.m,
walletN: wallet.n, walletN: wallet.n,
@ -1594,12 +1607,7 @@ WalletService.prototype.createTx = function(opts, cb) {
self.storage.storeTx(wallet.id, txp, function(err) { self.storage.storeTx(wallet.id, txp, function(err) {
if (err) return cb(err); if (err) return cb(err);
return cb(null, txp);
self._notify('NewTxProposal', {
amount: txp.getTotalAmount()
}, function() {
return cb(null, txp);
});
}); });
}); });
}); });
@ -1618,8 +1626,6 @@ WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature
* @param {Object} opts * @param {Object} opts
* @param {string} opts.txProposalId - The tx id. * @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.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) { WalletService.prototype.publishTx = function(opts, cb) {
var self = this; var self = this;
@ -1648,13 +1654,11 @@ WalletService.prototype.publishTx = function(opts, cb) {
return cb(new ClientError('Invalid proposal signature')); return cb(new ClientError('Invalid proposal signature'));
} }
// Verify signingKey // Save signature info for other copayers to check
if (opts.proposalSignaturePubKey) { txp.proposalSignature = opts.proposalSignature;
if (opts.proposalSignaturePubKey != signingKey || if (signingKey.selfSigned) {
!self._verifyRequestPubKey(opts.proposalSignaturePubKey, opts.proposalSignaturePubKeySig, copayer.xPubKey) txp.proposalSignaturePubKey = signingKey.key;
) { txp.proposalSignaturePubKeySig = signingKey.signature;
return cb(new ClientError('Invalid proposal signing key'));
}
} }
// Verify UTXOs are still available // Verify UTXOs are still available
@ -1672,7 +1676,12 @@ WalletService.prototype.publishTx = function(opts, cb) {
txp.status = 'pending'; txp.status = 'pending';
self.storage.storeTx(self.walletId, txp, function(err) { self.storage.storeTx(self.walletId, txp, function(err) {
if (err) return cb(err); if (err) return cb(err);
return cb();
self._notify('NewTxProposal', {
amount: txp.getTotalAmount()
}, function() {
return cb(null, txp);
});
}); });
}); });
}); });

View File

@ -400,28 +400,6 @@ helpers.createExternalProposalOpts = function(toAddress, amount, signingKey, mor
}; };
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) { 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);
@ -490,4 +468,15 @@ helpers.createAddresses = function(server, wallet, main, change, cb) {
}); });
}; };
helpers.createAndPublishTx = function(server, txOpts, signingKey, cb) {
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
var publishOpts = helpers.getProposalSignatureOpts(txp, signingKey);
server.publishTx(publishOpts, function(err) {
should.not.exist(err);
return cb(txp);
});
});
};
module.exports = helpers; module.exports = helpers;

View File

@ -2693,13 +2693,14 @@ describe('Wallet service', function() {
it('should create a tx', function(done) { it('should create a tx', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() { helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = helpers.createProposalOpts2([{ var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', outputs: [{
amount: 0.8 toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
}], { amount: 0.8 * 1e8,
}],
message: 'some message', message: 'some message',
customData: 'some custom data', customData: 'some custom data',
}); };
server.createTx(txOpts, function(err, tx) { server.createTx(txOpts, function(err, tx) {
should.not.exist(err); should.not.exist(err);
should.exist(tx); should.exist(tx);
@ -2721,15 +2722,53 @@ describe('Wallet service', function() {
}); });
}); });
it('should be able to send a temporary tx proposal', function(done) { it('should be able to specify the final fee', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() { helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = helpers.createProposalOpts2([{ var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', outputs: [{
amount: 0.8 toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
}], { amount: 0.8 * 1e8,
}],
fee: 123400,
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
should.exist(tx);
tx.fee.should.equal(123400);
var t = tx.getBitcoreTx();
t.getFee().should.equal(123400);
done();
});
});
});
it('should check explicit fee to be below max', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8 * 1e8,
}],
fee: 1e8,
};
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.message.should.contain('fee');
done();
});
});
});
it('should be able to publish a temporary tx proposal', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8 * 1e8,
}],
message: 'some message', message: 'some message',
customData: 'some custom data', customData: 'some custom data',
}); };
server.createTx(txOpts, function(err, txp) { server.createTx(txOpts, function(err, txp) {
should.not.exist(err); should.not.exist(err);
should.exist(txp); should.exist(txp);
@ -2739,6 +2778,7 @@ describe('Wallet service', function() {
server.getPendingTxs({}, function(err, txs) { server.getPendingTxs({}, function(err, txs) {
should.not.exist(err); should.not.exist(err);
txs.length.should.equal(1); txs.length.should.equal(1);
should.exist(txs[0].proposalSignature);
done(); done();
}); });
}); });
@ -2746,7 +2786,36 @@ describe('Wallet service', function() {
}); });
}); });
it('should fail to send non-existent tx proposal', function(done) { it('should delay NewTxProposal notification until published', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = {
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 0.8 * 1e8,
}],
message: 'some message',
};
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
_.pluck(notifications, 'type').should.not.contain('NewTxProposal');
var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err) {
should.not.exist(err);
server.getNotifications({}, function(err, notifications) {
should.not.exist(err);
_.pluck(notifications, 'type').should.contain('NewTxProposal');
done();
});
});
});
});
});
});
it('should fail to publish non-existent tx proposal', function(done) {
server.publishTx({ server.publishTx({
txProposalId: 'wrong-id', txProposalId: 'wrong-id',
proposalSignature: 'dummy', proposalSignature: 'dummy',
@ -2760,14 +2829,15 @@ describe('Wallet service', function() {
}); });
}); });
it('should fail to send tx proposal with wrong signature', function(done) { it('should fail to publish tx proposal with wrong signature', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() { helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = helpers.createProposalOpts2([{ var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', outputs: [{
amount: 0.8 toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
}], { amount: 0.8 * 1e8,
}],
message: 'some message', message: 'some message',
}); };
server.createTx(txOpts, function(err, txp) { server.createTx(txOpts, function(err, txp) {
should.not.exist(err); should.not.exist(err);
should.exist(txp); should.exist(txp);
@ -2783,33 +2853,27 @@ describe('Wallet service', function() {
}); });
}); });
it('should fail to send tx proposal not signed by the creator', function(done) { it('should fail to publish tx proposal not signed by the creator', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() { helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = helpers.createProposalOpts2([{ var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', outputs: [{
amount: 0.8 toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
}], { amount: 0.8 * 1e8,
}],
message: 'some message', message: 'some message',
}); };
server.createTx(txOpts, function(err, txp) { server.createTx(txOpts, function(err, txp) {
should.not.exist(err); should.not.exist(err);
should.exist(txp); should.exist(txp);
var raw = txp.getRawTx();
var proposalSignature = helpers.signMessage(raw, TestData.copayers[0].privKey_1H_0);
var pubKey = new Bitcore.PrivateKey(TestData.copayers[0].privKey_1H_0).toPublicKey().toString();
var pubKeySig = helpers.signMessage(pubKey, TestData.copayers[1].privKey_1H_0);
var publishOpts = { var publishOpts = {
txProposalId: txp.id, txProposalId: txp.id,
proposalSignature: proposalSignature, proposalSignature: helpers.signMessage(txp.getRawTx(), TestData.copayers[1].privKey_1H_0),
proposalSignaturePubKey: pubKey,
proposalSignaturePubKeySig: pubKeySig,
} }
server.publishTx(publishOpts, function(err) { server.publishTx(publishOpts, function(err) {
should.exist(err); should.exist(err);
err.message.should.contain('Invalid proposal signing key'); err.message.should.contain('Invalid proposal signature');
done(); done();
}); });
}); });
@ -2818,27 +2882,27 @@ describe('Wallet service', function() {
it('should accept a tx proposal signed with a custom key', function(done) { it('should accept a tx proposal signed with a custom key', function(done) {
var reqPrivKey = new Bitcore.PrivateKey(); var reqPrivKey = new Bitcore.PrivateKey();
var reqPubKey = reqPrivKey.toPublicKey(); var reqPubKey = reqPrivKey.toPublicKey().toString();
var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H; var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H;
var sig = helpers.signRequestPubKey(reqPubKey, xPrivKey);
var opts = { var accessOpts = {
copayerId: TestData.copayers[0].id44, copayerId: TestData.copayers[0].id44,
requestPubKey: reqPubKey, requestPubKey: reqPubKey,
signature: sig, signature: helpers.signRequestPubKey(reqPubKey, xPrivKey),
}; };
server.addAccess(opts, function(err) { server.addAccess(accessOpts, function(err) {
should.not.exist(err); should.not.exist(err);
helpers.stubUtxos(server, wallet, [1, 2], function() { helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = helpers.createProposalOpts2([{ var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', outputs: [{
amount: 0.8 toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
}], { amount: 0.8 * 1e8,
}],
message: 'some message', message: 'some message',
}); };
server.createTx(txOpts, function(err, txp) { server.createTx(txOpts, function(err, txp) {
should.not.exist(err); should.not.exist(err);
should.exist(txp); should.exist(txp);
@ -2846,28 +2910,34 @@ describe('Wallet service', function() {
var publishOpts = { var publishOpts = {
txProposalId: txp.id, txProposalId: txp.id,
proposalSignature: helpers.signMessage(txp.getRawTx(), reqPrivKey), proposalSignature: helpers.signMessage(txp.getRawTx(), reqPrivKey),
proposalSignaturePubKey: reqPubKey,
proposalSignaturePubKeySig: sig,
} }
server.publishTx(publishOpts, function(err) { server.publishTx(publishOpts, function(err) {
should.exist(err); should.not.exist(err);
err.message.should.contain('Invalid proposal signing key'); server.getTx({
done(); txProposalId: txp.id
}, function(err, x) {
should.not.exist(err);
x.proposalSignature.should.equal(publishOpts.proposalSignature);
x.proposalSignaturePubKey.should.equal(accessOpts.requestPubKey);
x.proposalSignaturePubKeySig.should.equal(accessOpts.signature);
done();
});
}); });
}); });
}); });
}); });
}); });
it('should fail to send a temporary tx proposal if utxos are unavailable', function(done) { it('should fail to publish a temporary tx proposal if utxos are unavailable', function(done) {
var txp1, txp2; var txp1, txp2;
var txOpts = helpers.createProposalOpts2([{ var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', outputs: [{
amount: 0.8 toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
}], { amount: 0.8 * 1e8,
}],
message: 'some message', message: 'some message',
}); };
async.waterfall([ async.waterfall([
@ -2890,7 +2960,7 @@ describe('Wallet service', function() {
var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0); var publishOpts = helpers.getProposalSignatureOpts(txp1, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, next); server.publishTx(publishOpts, next);
}, },
function(next) { function(txp, next) {
var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0); var publishOpts = helpers.getProposalSignatureOpts(txp2, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, function(err) { server.publishTx(publishOpts, function(err) {
should.exist(err); should.exist(err);
@ -2914,7 +2984,7 @@ describe('Wallet service', function() {
var publishOpts = helpers.getProposalSignatureOpts(txp3, TestData.copayers[0].privKey_1H_0); var publishOpts = helpers.getProposalSignatureOpts(txp3, TestData.copayers[0].privKey_1H_0);
server.publishTx(publishOpts, next); server.publishTx(publishOpts, next);
}, },
function(next) { function(txp, next) {
server.getPendingTxs({}, function(err, txs) { server.getPendingTxs({}, function(err, txs) {
should.not.exist(err); should.not.exist(err);
txs.length.should.equal(2); txs.length.should.equal(2);
@ -2929,13 +2999,14 @@ describe('Wallet service', function() {
it('should fail to list pending proposals from legacy client', function(done) { it('should fail to list pending proposals from legacy client', function(done) {
helpers.stubUtxos(server, wallet, [1, 2], function() { helpers.stubUtxos(server, wallet, [1, 2], function() {
var txOpts = helpers.createProposalOpts2([{ var txOpts = {
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', outputs: [{
amount: 0.8 toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
}], { amount: 0.8 * 1e8,
}],
message: 'some message', message: 'some message',
customData: 'some custom data', customData: 'some custom data',
}); };
server.createTx(txOpts, function(err, txp) { server.createTx(txOpts, function(err, txp) {
should.not.exist(err); should.not.exist(err);
should.exist(txp); should.exist(txp);
@ -3387,207 +3458,262 @@ describe('Wallet service', function() {
describe('#broadcastTx & #broadcastRawTx', function() { describe('#broadcastTx & #broadcastRawTx', function() {
var server, wallet, txpid, txid; var server, wallet, txpid, txid;
beforeEach(function(done) { describe('Legacy', function() {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s; beforeEach(function(done) {
wallet = w; helpers.createAndJoinWallet(1, 1, function(s, w) {
helpers.stubUtxos(server, wallet, [10, 10], function() { server = s;
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, TestData.copayers[0].privKey_1H_0, { wallet = w;
message: 'some message' helpers.stubUtxos(server, wallet, [10, 10], function() {
}); var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, TestData.copayers[0].privKey_1H_0, {
server.createTxLegacy(txOpts, function(err, txp) { message: 'some message'
should.not.exist(err); });
should.exist(txp); server.createTxLegacy(txOpts, function(err, txp) {
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err); should.not.exist(err);
should.exist(txp); should.exist(txp);
txp.isAccepted().should.be.true; var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
txp.isBroadcasted().should.be.false; server.signTx({
txid = txp.txid; txProposalId: txp.id,
txpid = txp.id; signatures: signatures,
done(); }, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.isAccepted().should.be.true;
txp.isBroadcasted().should.be.false;
txid = txp.txid;
txpid = txp.id;
done();
});
}); });
}); });
}); });
}); });
});
it('should broadcast a tx', function(done) { it('should broadcast a tx', function(done) {
var clock = sinon.useFakeTimers(1234000, 'Date'); var clock = sinon.useFakeTimers(1234000, 'Date');
helpers.stubBroadcast(); helpers.stubBroadcast();
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.not.exist(txp.raw);
txp.txid.should.equal(txid);
txp.isBroadcasted().should.be.true;
txp.broadcastedOn.should.equal(1234);
clock.restore();
done();
});
});
});
it('should broadcast a raw tx', function(done) {
helpers.stubBroadcast();
server.broadcastRawTx({
network: 'testnet',
rawTx: 'raw tx',
}, function(err, txid) {
should.not.exist(err);
should.exist(txid);
done();
});
});
it('should fail to brodcast a tx already marked as broadcasted', function(done) {
helpers.stubBroadcast();
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.broadcastTx({ server.broadcastTx({
txProposalId: txpid txProposalId: txpid
}, function(err) { }, function(err) {
should.exist(err); should.not.exist(err);
err.code.should.equal('TX_ALREADY_BROADCASTED'); server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.not.exist(txp.raw);
txp.txid.should.equal(txid);
txp.isBroadcasted().should.be.true;
txp.broadcastedOn.should.equal(1234);
clock.restore();
done();
});
});
});
it('should broadcast a raw tx', function(done) {
helpers.stubBroadcast();
server.broadcastRawTx({
network: 'testnet',
rawTx: 'raw tx',
}, function(err, txid) {
should.not.exist(err);
should.exist(txid);
done(); done();
}); });
}); });
});
it('should auto process already broadcasted txs', function(done) { it('should fail to brodcast a tx already marked as broadcasted', function(done) {
helpers.stubBroadcast(); helpers.stubBroadcast();
server.getPendingTxs({}, function(err, txs) { server.broadcastTx({
should.not.exist(err); txProposalId: txpid
txs.length.should.equal(1); }, function(err) {
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: 999
});
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err); should.not.exist(err);
txs.length.should.equal(0); server.broadcastTx({
done(); txProposalId: txpid
}, function(err) {
should.exist(err);
err.code.should.equal('TX_ALREADY_BROADCASTED');
done();
});
}); });
}); });
});
it('should process only broadcasted txs', function(done) { it('should auto process already broadcasted txs', function(done) {
helpers.stubBroadcast(); helpers.stubBroadcast();
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, TestData.copayers[0].privKey_1H_0, {
message: 'some message 2'
});
server.createTxLegacy(txOpts, function(err, txp) {
should.not.exist(err);
server.getPendingTxs({}, function(err, txs) { server.getPendingTxs({}, function(err, txs) {
should.not.exist(err); should.not.exist(err);
txs.length.should.equal(2); txs.length.should.equal(1);
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, { blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: 999 txid: 999
}); });
server.getPendingTxs({}, function(err, txs) { server.getPendingTxs({}, function(err, txs) {
should.not.exist(err); should.not.exist(err);
txs.length.should.equal(1); txs.length.should.equal(0);
txs[0].status.should.equal('pending'); done();
should.not.exist(txs[0].txid); });
});
});
it('should process only broadcasted txs', function(done) {
helpers.stubBroadcast();
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, TestData.copayers[0].privKey_1H_0, {
message: 'some message 2'
});
server.createTxLegacy(txOpts, function(err, txp) {
should.not.exist(err);
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(2);
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: 999
});
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.length.should.equal(1);
txs[0].status.should.equal('pending');
should.not.exist(txs[0].txid);
done();
});
});
});
});
it('should fail to brodcast a not yet accepted tx', function(done) {
helpers.stubBroadcast();
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, TestData.copayers[0].privKey_1H_0, {
message: 'some message'
});
server.createTxLegacy(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
server.broadcastTx({
txProposalId: txp.id
}, function(err) {
should.exist(err);
err.code.should.equal('TX_NOT_ACCEPTED');
done();
});
});
});
it('should keep tx as accepted if unable to broadcast it', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.toString().should.equal('broadcast error');
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.isBroadcasted().should.be.false;
should.not.exist(txp.broadcastedOn);
txp.isAccepted().should.be.true;
done();
});
});
});
it('should mark tx as broadcasted if accepted but already in blockchain', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: '999'
});
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.isBroadcasted().should.be.true;
should.exist(txp.broadcastedOn);
done();
});
});
});
it('should keep tx as accepted if broadcast fails and cannot check tx in blockchain', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, 'bc check error');
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.toString().should.equal('bc check error');
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.isBroadcasted().should.be.false;
should.not.exist(txp.broadcastedOn);
txp.isAccepted().should.be.true;
done(); done();
}); });
}); });
}); });
}); });
describe('New', function() {
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
it('should fail to brodcast a not yet accepted tx', function(done) { wallet = w;
helpers.stubBroadcast(); helpers.stubUtxos(server, wallet, [10, 10], function() {
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, TestData.copayers[0].privKey_1H_0, { var txOpts = {
message: 'some message' outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 9e8,
}],
message: 'some message',
};
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
should.exist(txp);
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.isAccepted().should.be.true;
txp.isBroadcasted().should.be.false;
txid = txp.txid;
txpid = txp.id;
done();
});
});
});
});
}); });
server.createTxLegacy(txOpts, function(err, txp) {
should.not.exist(err); it('should broadcast a tx', function(done) {
should.exist(txp); var clock = sinon.useFakeTimers(1234000, 'Date');
helpers.stubBroadcast();
server.broadcastTx({ server.broadcastTx({
txProposalId: txp.id txProposalId: txpid
}, function(err) { }, function(err) {
should.exist(err);
err.code.should.equal('TX_NOT_ACCEPTED');
done();
});
});
});
it('should keep tx as accepted if unable to broadcast it', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.toString().should.equal('broadcast error');
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err); should.not.exist(err);
should.exist(txp.txid); server.getTx({
txp.isBroadcasted().should.be.false; txProposalId: txpid
should.not.exist(txp.broadcastedOn); }, function(err, txp) {
txp.isAccepted().should.be.true; should.not.exist(err);
done(); should.not.exist(txp.raw);
txp.txid.should.equal(txid);
txp.isBroadcasted().should.be.true;
txp.broadcastedOn.should.equal(1234);
clock.restore();
done();
});
}); });
}); });
});
it('should mark tx as broadcasted if accepted but already in blockchain', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
txid: '999'
});
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.isBroadcasted().should.be.true;
should.exist(txp.broadcastedOn);
done();
});
});
});
it('should keep tx as accepted if broadcast fails and cannot check tx in blockchain', function(done) {
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error');
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, 'bc check error');
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.toString().should.equal('bc check error');
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp.txid);
txp.isBroadcasted().should.be.false;
should.not.exist(txp.broadcastedOn);
txp.isAccepted().should.be.true;
done();
});
});
}); });
}); });