From 9db456550f2e807d3e9b11dfe790ee3c989213fd Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 26 Nov 2015 10:50:22 -0300 Subject: [PATCH] create temporary proposal + test --- lib/model/txproposal.js | 3 +- lib/server.js | 95 +++++++++++++++++++++++++++++++++++-- lib/storage.js | 2 +- test/integration/helpers.js | 29 +++++++++-- test/integration/server.js | 36 ++++++++++++++ 5 files changed, 155 insertions(+), 10 deletions(-) diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index a81d7e9..38446b2 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -129,7 +129,6 @@ TxProposal.fromObj = function(obj) { TxProposal.prototype.toObject = function() { var x = _.cloneDeep(this); x.isPending = this.isPending(); - x.isTemporary = this.isTemporary(); return x; }; @@ -375,7 +374,7 @@ TxProposal.prototype.isTemporary = function() { }; TxProposal.prototype.isPending = function() { - return !_.contains(['broadcasted', 'rejected'], this.status); + return !_.contains(['temporary', 'broadcasted', 'rejected'], this.status); }; TxProposal.prototype.isAccepted = function() { diff --git a/lib/server.js b/lib/server.js index 1dcf8fd..2a6e8f3 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1147,9 +1147,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) @@ -1277,7 +1277,7 @@ 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); @@ -1345,6 +1345,95 @@ WalletService.prototype.createTx = function(opts, cb) { }); }; +/** + * Creates a new transaction proposal. + * @param {Object} opts + * @param {string} opts.type - Proposal type. + * @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 - Optional. Do not use UTXOs of unconfirmed transactions as inputs (defaults to false) + * @returns {TxProposal} Transaction proposal. + */ +WalletService.prototype.createTx2 = function(opts, cb) { + var self = this; + + if (!Utils.checkRequired(opts, ['outputs'])) + return cb(new ClientError('Required argument missing')); + + var type = opts.type || Model.TxProposal.Types.STANDARD; + if (!Model.TxProposal.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); + + self._canCreateTx(function(err, canCreate) { + if (err) return cb(err); + if (!canCreate) return cb(Errors.TX_CANNOT_CREATE); + + if (type != Model.TxProposal.Types.EXTERNAL) { + var validationError = self._validateOutputs(opts, wallet); + if (validationError) { + return cb(validationError); + } + } + + var txOpts = { + version: 3, + type: type, + walletId: self.walletId, + creatorId: self.copayerId, + outputs: opts.outputs, + inputs: opts.inputs, + message: opts.message, + 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, + 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); + }); + }); + }); + }); + }); + }); + }); +}; + + /** * Retrieves a tx from storage. * @param {Object} opts diff --git a/lib/storage.js b/lib/storage.js index 884f307..7fe6dd0 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -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) { diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 7cfa22a..6223d23 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -371,6 +371,30 @@ helpers.createExternalProposalOpts = function(toAddress, amount, signingKey, mor return helpers.createProposalOpts(Model.TxProposalLegacy.Types.EXTERNAL, outputs, signingKey, moreOpts, inputs); }; + +helpers.createStandardProposalOpts = function(outputs, moreOpts, inputs) { + _.each(outputs, function(output) { + output.amount = helpers.toSatoshi(output.amount); + }); + + var opts = { + type: Model.TxProposal.Types.STANDARD, + outputs: outputs, + inputs: inputs || [], + }; + + if (moreOpts) { + moreOpts = _.pick(moreOpts, ['feePerKb', 'customData', 'message']); + opts = _.assign(opts, moreOpts); + } + + opts = _.defaults(opts, { + message: null + }); + + return opts; +}; + helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts, inputs) { _.each(outputs, function(output) { output.amount = helpers.toSatoshi(output.amount); @@ -383,9 +407,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); } @@ -415,7 +437,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) { diff --git a/test/integration/server.js b/test/integration/server.js index 2c1d111..fb21ba3 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -2330,6 +2330,42 @@ describe('Wallet service', function() { }); }); + describe('#createTx2', function() { + var server, wallet; + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 3, function(s, w) { + server = s; + wallet = w; + done(); + }); + }); + + it('should create a tx', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = helpers.createStandardProposalOpts([{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8 + }], { + message: 'some message', + customData: 'some custom data', + }); + server.createTx2(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.isAccepted().should.equal.false; + tx.isRejected().should.equal.false; + tx.isPending().should.equal.true; + tx.isTemporary().should.equal.true; + tx.amount.should.equal(helpers.toSatoshi(0.8)); + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.should.be.empty; + done(); + }); + }); + }); + }); + }); describe('#createTx backoff time', function(done) { var server, wallet, txid;