diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index de24a35..4998ed9 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -13,6 +13,7 @@ var Common = require('../common'); var Constants = Common.Constants; var Defaults = Common.Defaults; +var TxProposalLegacy = require('./txproposal_legacy'); var TxProposalAction = require('./txproposalaction'); function TxProposal() {}; @@ -67,7 +68,7 @@ TxProposal.create = function(opts) { TxProposal.fromObj = function(obj) { if (!(obj.version >= 3)) { - throw new Error('TxProposal < v3 no longer supported.') + return TxProposalLegacy.fromObj(obj); } var x = new TxProposal(); diff --git a/lib/model/txproposal_legacy.js b/lib/model/txproposal_legacy.js new file mode 100644 index 0000000..697c029 --- /dev/null +++ b/lib/model/txproposal_legacy.js @@ -0,0 +1,176 @@ +'use strict'; + +var _ = require('lodash'); +var $ = require('preconditions').singleton(); +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.fromObj = function(obj) { + var x = new TxProposal(); + + x.version = obj.version; + if (obj.version === '1.0.0') { + x.type = TxProposal.Types.SIMPLE; + } else { + x.type = obj.type; + } + x.createdOn = obj.createdOn; + x.id = obj.id; + x.walletId = obj.walletId; + x.creatorId = obj.creatorId; + x.outputs = obj.outputs; + x.toAddress = obj.toAddress; + x.amount = obj.amount; + x.message = obj.message; + x.payProUrl = obj.payProUrl; + x.proposalSignature = obj.proposalSignature; + x.changeAddress = obj.changeAddress; + x.inputs = obj.inputs; + x.requiredSignatures = obj.requiredSignatures; + x.requiredRejections = obj.requiredRejections; + x.walletN = obj.walletN; + x.status = obj.status; + x.txid = obj.txid; + x.broadcastedOn = obj.broadcastedOn; + x.inputPaths = obj.inputPaths; + x.actions = _.map(obj.actions, function(action) { + return TxProposalAction.fromObj(action); + }); + x.outputOrder = obj.outputOrder; + x.network = obj.network; + x.fee = obj.fee; + x.feePerKb = obj.feePerKb; + x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos; + x.proposalSignaturePubKey = obj.proposalSignaturePubKey; + x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig; + x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH; + x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; + x.customData = obj.customData; + + return x; +}; + +function throwUnsupportedError() { + var msg = 'Unsupported operation on this transaction proposal'; + log.warn('DEPRECATED: ' + msg); + throw new Error(msg); +}; + +TxProposal.prototype.toObject = function() { + var x = _.cloneDeep(this); + x.isPending = this.isPending(); + return x; +}; + +TxProposal.prototype._updateStatus = function() { + if (this.status != 'pending') return; + + if (this.isRejected()) { + this.status = 'rejected'; + } else if (this.isAccepted()) { + this.status = 'accepted'; + } +}; + +TxProposal.prototype.getBitcoreTx = function() { + throwUnsupportedError(); +}; + +TxProposal.prototype.getNetworkName = function() { + return Bitcore.Address(this.changeAddress.address).toObject().network; +}; + +TxProposal.prototype.getRawTx = function() { + throwUnsupportedError(); +}; + +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; + } +}; + +TxProposal.prototype.getActors = function() { + return _.pluck(this.actions, 'copayerId'); +}; + +TxProposal.prototype.getApprovers = function() { + return _.pluck( + _.filter(this.actions, { + type: 'accept' + }), 'copayerId'); +}; + +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.sign = function() { + throwUnsupportedError(); +}; + +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; diff --git a/lib/server.js b/lib/server.js index b6dcaba..0a9a703 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1338,12 +1338,10 @@ WalletService.prototype._checkTx = function(txp) { var bitcoreError; var serializationOpts = { - disableIsFullySigned: true + disableIsFullySigned: true, + disableSmallFees: true, + disableLargeFees: true, }; - if (!_.startsWith(txp.version, '1.')) { - serializationOpts.disableSmallFees = true; - serializationOpts.disableLargeFees = true; - } if (txp.getEstimatedSize() / 1000 > Defaults.MAX_TX_SIZE_IN_KB) return Errors.TX_MAX_SIZE_EXCEEDED; @@ -1894,7 +1892,12 @@ WalletService.prototype.publishTx = function(opts, cb) { var copayer = wallet.getCopayer(self.copayerId); - var raw = txp.getRawTx(); + var raw; + try { + raw = txp.getRawTx(); + } catch (ex) { + return cb(ex); + } var signingKey = self._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys); if (!signingKey) { return cb(new ClientError('Invalid proposal signature')); @@ -2143,16 +2146,21 @@ WalletService.prototype.signTx = function(opts, cb) { var copayer = wallet.getCopayer(self.copayerId); - if (!txp.sign(self.copayerId, opts.signatures, copayer.xPubKey)) { - log.warn('Error signing transaction (BAD_SIGNATURES)'); - log.warn('Wallet id:', self.walletId); - log.warn('Copayer id:', self.copayerId); - log.warn('Client version:', self.clientVersion); - log.warn('Arguments:', JSON.stringify(opts)); - log.warn('Transaction proposal:', JSON.stringify(txp)); - var raw = txp.getBitcoreTx().uncheckedSerialize(); - log.warn('Raw tx:', raw); - return cb(Errors.BAD_SIGNATURES); + try { + if (!txp.sign(self.copayerId, opts.signatures, copayer.xPubKey)) { + log.warn('Error signing transaction (BAD_SIGNATURES)'); + log.warn('Wallet id:', self.walletId); + log.warn('Copayer id:', self.copayerId); + log.warn('Client version:', self.clientVersion); + log.warn('Arguments:', JSON.stringify(opts)); + log.warn('Transaction proposal:', JSON.stringify(txp)); + var raw = txp.getBitcoreTx().uncheckedSerialize(); + log.warn('Raw tx:', raw); + return cb(Errors.BAD_SIGNATURES); + } + } catch (ex) { + log.error('Error signing transaction proposal', ex); + return cb(ex); } self.storage.storeTx(self.walletId, txp, function(err) {