diff --git a/lib/common/utils.js b/lib/common/utils.js index 0d82aa1..0332a89 100644 --- a/lib/common/utils.js +++ b/lib/common/utils.js @@ -122,4 +122,24 @@ Utils.formatSize = function(size) { return (size / 1000.).toFixed(4) + 'kB'; }; +Utils.parseVersion = function(version) { + var v = {}; + + if (!version) return null; + + var x = version.split('-'); + if (x.length != 2) { + v.agent = version; + return v; + } + v.agent = _.contains(['bwc', 'bws'], x[0]) ? 'bwc' : x[0]; + x = x[1].split('.'); + v.major = parseInt(x[0]); + v.minor = parseInt(x[1]); + v.patch = parseInt(x[2]); + + return v; +}; + + module.exports = Utils; diff --git a/lib/expressapp.js b/lib/expressapp.js index 9d70ebd..da766cb 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -151,7 +151,12 @@ ExpressApp.prototype.start = function(opts, cb) { // DEPRECATED router.post('/v1/wallets/', function(req, res) { logDeprecated(req); - var server = getServer(req, res); + var server; + try { + server = getServer(req, res); + } catch (ex) { + return returnError(ex, res, req); + } req.body.supportBIP44AndP2PKH = false; server.createWallet(req.body, function(err, walletId) { if (err) return returnError(err, res, req); @@ -162,7 +167,12 @@ ExpressApp.prototype.start = function(opts, cb) { }); router.post('/v2/wallets/', function(req, res) { - var server = getServer(req, res); + var server; + try { + server = getServer(req, res); + } catch (ex) { + return returnError(ex, res, req); + } server.createWallet(req.body, function(err, walletId) { if (err) return returnError(err, res, req); res.json({ @@ -173,7 +183,12 @@ ExpressApp.prototype.start = function(opts, cb) { router.put('/v1/copayers/:id/', function(req, res) { req.body.copayerId = req.params['id']; - var server = getServer(req, res); + var server; + try { + server = getServer(req, res); + } catch (ex) { + return returnError(ex, res, req); + } server.addAccess(req.body, function(err, result) { if (err) return returnError(err, res, req); res.json(result); @@ -185,7 +200,12 @@ ExpressApp.prototype.start = function(opts, cb) { logDeprecated(req); req.body.walletId = req.params['id']; req.body.supportBIP44AndP2PKH = false; - var server = getServer(req, res); + var server; + try { + server = getServer(req, res); + } catch (ex) { + return returnError(ex, res, req); + } server.joinWallet(req.body, function(err, result) { if (err) return returnError(err, res, req); @@ -195,7 +215,12 @@ ExpressApp.prototype.start = function(opts, cb) { router.post('/v2/wallets/:id/copayers/', function(req, res) { req.body.walletId = req.params['id']; - var server = getServer(req, res); + var server; + try { + server = getServer(req, res); + } catch (ex) { + return returnError(ex, res, req); + } server.joinWallet(req.body, function(err, result) { if (err) return returnError(err, res, req); @@ -256,17 +281,6 @@ ExpressApp.prototype.start = function(opts, cb) { }); }); - - router.post('/v1/txproposals/', function(req, res) { - log.warn('DEPRECATED v1/txproposals'); - getServerWithAuth(req, res, function(server) { - server.createTxLegacy(req.body, function(err, txp) { - if (err) return returnError(err, res, req); - res.json(txp); - }); - }); - }); - router.post('/v2/txproposals/', function(req, res) { getServerWithAuth(req, res, function(server) { server.createTx(req.body, function(err, txp) { @@ -340,7 +354,12 @@ ExpressApp.prototype.start = function(opts, cb) { logDeprecated(req); var opts = {}; if (req.query.network) opts.network = req.query.network; - var server = getServer(req, res); + var server; + try { + server = getServer(req, res); + } catch (ex) { + return returnError(ex, res, req); + } server.getFeeLevels(opts, function(err, feeLevels) { if (err) return returnError(err, res, req); _.each(feeLevels, function(feeLevel) { @@ -354,7 +373,12 @@ ExpressApp.prototype.start = function(opts, cb) { router.get('/v2/feelevels/', function(req, res) { var opts = {}; if (req.query.network) opts.network = req.query.network; - var server = getServer(req, res); + var server; + try { + server = getServer(req, res); + } catch (ex) { + return returnError(ex, res, req); + } server.getFeeLevels(opts, function(err, feeLevels) { if (err) return returnError(err, res, req); res.json(feeLevels); diff --git a/lib/model/index.js b/lib/model/index.js index bcf5e3e..f463e88 100644 --- a/lib/model/index.js +++ b/lib/model/index.js @@ -2,7 +2,6 @@ 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'); diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index 4998ed9..de24a35 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -13,7 +13,6 @@ var Common = require('../common'); var Constants = Common.Constants; var Defaults = Common.Defaults; -var TxProposalLegacy = require('./txproposal_legacy'); var TxProposalAction = require('./txproposalaction'); function TxProposal() {}; @@ -68,7 +67,7 @@ TxProposal.create = function(opts) { TxProposal.fromObj = function(obj) { if (!(obj.version >= 3)) { - return TxProposalLegacy.fromObj(obj); + throw new Error('TxProposal < v3 no longer supported.') } var x = new TxProposal(); diff --git a/lib/model/txproposal_legacy.js b/lib/model/txproposal_legacy.js deleted file mode 100644 index da39d8f..0000000 --- a/lib/model/txproposal_legacy.js +++ /dev/null @@ -1,441 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var $ = require('preconditions').singleton(); -var Uuid = require('uuid'); -var log = require('npmlog'); -log.debug = log.verbose; -log.disableColor(); - -var Bitcore = require('bitcore-lib'); - -var Common = require('../common'); -var Constants = Common.Constants; -var Defaults = Common.Defaults; - -var TxProposalAction = require('./txproposalaction'); - -function TxProposal() {}; - -TxProposal.Types = { - SIMPLE: 'simple', - MULTIPLEOUTPUTS: 'multiple_outputs', - EXTERNAL: 'external' -}; - -TxProposal.isTypeSupported = function(type) { - return _.contains(_.values(TxProposal.Types), type); -}; - -TxProposal._create = {}; - -TxProposal._create.simple = function(txp, opts) { - txp.toAddress = opts.toAddress; - txp.amount = opts.amount; - txp.outputOrder = _.shuffle(_.range(2)); - try { - txp.network = Bitcore.Address(txp.toAddress).toObject().network; - } catch (ex) {} -}; - -TxProposal._create.undefined = TxProposal._create.simple; - -TxProposal._create.multiple_outputs = function(txp, opts) { - txp.outputs = _.map(opts.outputs, function(output) { - return _.pick(output, ['amount', 'toAddress', 'message']); - }); - txp.outputOrder = _.shuffle(_.range(txp.outputs.length + 1)); - txp.amount = txp.getTotalAmount(); - try { - txp.network = Bitcore.Address(txp.outputs[0].toAddress).toObject().network; - } catch (ex) {} -}; - -TxProposal._create.external = function(txp, opts) { - txp.setInputs(opts.inputs || []); - txp.outputs = opts.outputs; - txp.outputOrder = _.range(txp.outputs.length + 1); - txp.amount = txp.getTotalAmount(); - try { - txp.network = Bitcore.Address(txp.outputs[0].toAddress).toObject().network; - } catch (ex) {} -}; - -TxProposal.create = function(opts) { - opts = opts || {}; - - var x = new TxProposal(); - - x.version = '2.0.0'; - x.type = opts.type || TxProposal.Types.SIMPLE; - - var now = Date.now(); - x.createdOn = Math.floor(now / 1000); - x.id = _.padLeft(now, 14, '0') + Uuid.v4(); - x.walletId = opts.walletId; - x.creatorId = opts.creatorId; - x.message = opts.message; - x.payProUrl = opts.payProUrl; - x.proposalSignature = opts.proposalSignature; - x.changeAddress = opts.changeAddress; - x.inputs = []; - x.inputPaths = []; - x.requiredSignatures = opts.requiredSignatures; - x.requiredRejections = opts.requiredRejections; - x.walletN = opts.walletN; - x.status = 'pending'; - x.actions = []; - x.fee = null; - x.feePerKb = opts.feePerKb; - x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos; - x.proposalSignaturePubKey = opts.proposalSignaturePubKey; - x.proposalSignaturePubKeySig = opts.proposalSignaturePubKeySig; - x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH; - x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; - x.customData = opts.customData; - - if (_.isFunction(TxProposal._create[x.type])) { - TxProposal._create[x.type](x, opts); - } - - return x; -}; - -TxProposal.fromObj = function(obj) { - var x = new TxProposal(); - - x.version = obj.version; - if (obj.version === '1.0.0') { - x.type = TxProposal.Types.SIMPLE; - } else { - x.type = obj.type; - } - x.createdOn = obj.createdOn; - x.id = obj.id; - x.walletId = obj.walletId; - x.creatorId = obj.creatorId; - x.outputs = obj.outputs; - x.toAddress = obj.toAddress; - x.amount = obj.amount; - x.message = obj.message; - x.payProUrl = obj.payProUrl; - x.proposalSignature = obj.proposalSignature; - x.changeAddress = obj.changeAddress; - x.inputs = obj.inputs; - x.requiredSignatures = obj.requiredSignatures; - x.requiredRejections = obj.requiredRejections; - x.walletN = obj.walletN; - x.status = obj.status; - x.txid = obj.txid; - x.broadcastedOn = obj.broadcastedOn; - x.inputPaths = obj.inputPaths; - x.actions = _.map(obj.actions, function(action) { - return TxProposalAction.fromObj(action); - }); - x.outputOrder = obj.outputOrder; - x.network = obj.network; - x.fee = obj.fee; - x.feePerKb = obj.feePerKb; - x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos; - x.proposalSignaturePubKey = obj.proposalSignaturePubKey; - x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig; - x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH; - x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; - x.customData = obj.customData; - - return x; -}; - -TxProposal.prototype.toObject = function() { - var x = _.cloneDeep(this); - x.isPending = this.isPending(); - return x; -}; - -TxProposal.prototype.setInputs = function(inputs) { - this.inputs = inputs; - this.inputPaths = _.pluck(inputs, 'path'); -}; - -TxProposal.prototype._updateStatus = function() { - if (this.status != 'pending') return; - - if (this.isRejected()) { - this.status = 'rejected'; - } else if (this.isAccepted()) { - this.status = 'accepted'; - } -}; - -TxProposal.prototype._buildTx = function() { - var self = this; - - var t = new Bitcore.Transaction(); - - $.checkState(_.contains(_.values(Constants.SCRIPT_TYPES), self.addressType)); - - switch (self.addressType) { - case Constants.SCRIPT_TYPES.P2SH: - _.each(self.inputs, function(i) { - t.from(i, i.publicKeys, self.requiredSignatures); - }); - break; - case Constants.SCRIPT_TYPES.P2PKH: - t.from(self.inputs); - break; - } - - if (self.toAddress && self.amount && !self.outputs) { - t.to(self.toAddress, self.amount); - } else if (self.outputs) { - _.each(self.outputs, function(o) { - $.checkState(o.script || o.toAddress, 'Output should have either toAddress or script specified'); - if (o.script) { - t.addOutput(new Bitcore.Transaction.Output({ - script: o.script, - satoshis: o.amount - })); - } else { - t.to(o.toAddress, o.amount); - } - }); - } - - if (_.startsWith(self.version, '1.')) { - Bitcore.Transaction.FEE_SECURITY_MARGIN = 1; - t.feePerKb(self.feePerKb); - } else { - t.fee(self.fee); - } - - t.change(self.changeAddress.address); - - // Shuffle outputs for improved privacy - if (t.outputs.length > 1) { - var outputOrder = _.reject(self.outputOrder, function(order) { - return order >= t.outputs.length; - }); - $.checkState(t.outputs.length == outputOrder.length); - t.sortOutputs(function(outputs) { - return _.map(outputOrder, function(i) { - return outputs[i]; - }); - }); - } - - // Validate inputs vs outputs independently of Bitcore - var totalInputs = _.reduce(self.inputs, function(memo, i) { - return +i.satoshis + memo; - }, 0); - var totalOutputs = _.reduce(t.outputs, function(memo, o) { - return +o.satoshis + memo; - }, 0); - - $.checkState(totalInputs - totalOutputs <= Defaults.MAX_TX_FEE); - - return t; -}; - - -TxProposal.prototype._getCurrentSignatures = function() { - var acceptedActions = _.filter(this.actions, { - type: 'accept' - }); - - return _.map(acceptedActions, function(x) { - return { - signatures: x.signatures, - xpub: x.xpub, - }; - }); -}; - -TxProposal.prototype.getBitcoreTx = function() { - var self = this; - - var t = this._buildTx(); - - var sigs = this._getCurrentSignatures(); - _.each(sigs, function(x) { - self._addSignaturesToBitcoreTx(t, x.signatures, x.xpub); - }); - - return t; -}; - -TxProposal.prototype.getNetworkName = function() { - return Bitcore.Address(this.changeAddress.address).toObject().network; -}; - -TxProposal.prototype.getRawTx = function() { - var t = this.getBitcoreTx(); - - return t.uncheckedSerialize(); -}; - -TxProposal.prototype.getEstimatedSizeForSingleInput = function() { - return this.requiredSignatures * 72 + this.walletN * 36 + 44; -}; - -TxProposal.prototype.getEstimatedSize = function() { - // Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits. - var safetyMargin = 0.05; - - var overhead = 4 + 4 + 9 + 9; - var inputSize = this.getEstimatedSizeForSingleInput(); - var outputSize = 34; - var nbInputs = this.inputs.length; - var nbOutputs = (_.isArray(this.outputs) ? Math.max(1, this.outputs.length) : 1) + 1; - - var size = overhead + inputSize * nbInputs + outputSize * nbOutputs; - - return parseInt((size * (1 + safetyMargin)).toFixed(0)); -}; - -TxProposal.prototype.estimateFee = function() { - var fee = this.feePerKb * this.getEstimatedSize() / 1000; - - this.fee = parseInt(fee.toFixed(0)); -}; - -/** - * getTotalAmount - * - * @return {Number} total amount of all outputs excluding change output - */ -TxProposal.prototype.getTotalAmount = function() { - if (this.type == TxProposal.Types.MULTIPLEOUTPUTS || this.type == TxProposal.Types.EXTERNAL) { - return _.pluck(this.outputs, 'amount') - .reduce(function(total, n) { - return total + n; - }, 0); - } else { - return this.amount; - } -}; - -/** - * getActors - * - * @return {String[]} copayerIds that performed actions in this proposal (accept / reject) - */ -TxProposal.prototype.getActors = function() { - return _.pluck(this.actions, 'copayerId'); -}; - - -/** - * getApprovers - * - * @return {String[]} copayerIds that approved the tx proposal (accept) - */ -TxProposal.prototype.getApprovers = function() { - return _.pluck( - _.filter(this.actions, { - type: 'accept' - }), 'copayerId'); -}; - -/** - * getActionBy - * - * @param {String} copayerId - * @return {Object} type / createdOn - */ -TxProposal.prototype.getActionBy = function(copayerId) { - return _.find(this.actions, { - copayerId: copayerId - }); -}; - -TxProposal.prototype.addAction = function(copayerId, type, comment, signatures, xpub) { - var action = TxProposalAction.create({ - copayerId: copayerId, - type: type, - signatures: signatures, - xpub: xpub, - comment: comment, - }); - this.actions.push(action); - this._updateStatus(); -}; - -TxProposal.prototype._addSignaturesToBitcoreTx = function(tx, signatures, xpub) { - var self = this; - - if (signatures.length != this.inputs.length) - throw new Error('Number of signatures does not match number of inputs'); - - var i = 0, - x = new Bitcore.HDPublicKey(xpub); - - _.each(signatures, function(signatureHex) { - var input = self.inputs[i]; - try { - var signature = Bitcore.crypto.Signature.fromString(signatureHex); - var pub = x.derive(self.inputPaths[i]).publicKey; - var s = { - inputIndex: i, - signature: signature, - sigtype: Bitcore.crypto.Signature.SIGHASH_ALL, - publicKey: pub, - }; - tx.inputs[i].addSignature(tx, s); - i++; - } catch (e) {}; - }); - - if (i != tx.inputs.length) - throw new Error('Wrong signatures'); -}; - - -TxProposal.prototype.sign = function(copayerId, signatures, xpub) { - try { - // Tests signatures are OK - var tx = this.getBitcoreTx(); - this._addSignaturesToBitcoreTx(tx, signatures, xpub); - - this.addAction(copayerId, 'accept', null, signatures, xpub); - - if (this.status == 'accepted') { - this.raw = tx.uncheckedSerialize(); - this.txid = tx.id; - } - - return true; - } catch (e) { - log.debug(e); - return false; - } -}; - -TxProposal.prototype.reject = function(copayerId, reason) { - this.addAction(copayerId, 'reject', reason); -}; - -TxProposal.prototype.isPending = function() { - return !_.contains(['broadcasted', 'rejected'], this.status); -}; - -TxProposal.prototype.isAccepted = function() { - var votes = _.countBy(this.actions, 'type'); - return votes['accept'] >= this.requiredSignatures; -}; - -TxProposal.prototype.isRejected = function() { - var votes = _.countBy(this.actions, 'type'); - return votes['reject'] >= this.requiredRejections; -}; - -TxProposal.prototype.isBroadcasted = function() { - return this.status == 'broadcasted'; -}; - -TxProposal.prototype.setBroadcasted = function() { - $.checkState(this.txid); - this.status = 'broadcasted'; - this.broadcastedOn = Math.floor(Date.now() / 1000); -}; - -module.exports = TxProposal; diff --git a/lib/server.js b/lib/server.js index 7f674fa..4aedd09 100644 --- a/lib/server.js +++ b/lib/server.js @@ -170,6 +170,14 @@ WalletService.shutDown = function(cb) { */ WalletService.getInstance = function(opts) { opts = opts || {}; + + var version = Utils.parseVersion(opts.clientVersion); + if (version && version.agent == 'bwc') { + if (version.major == 0 || (version.major == 1 && version.minor < 2)) { + throw new ClientError(Errors.codes.UPGRADE_NEEDED, 'BWC clients < 1.2 are no longer supported.'); + } + } + var server = new WalletService(); server._setClientVersion(opts.clientVersion); return server; @@ -186,7 +194,13 @@ WalletService.getInstance = function(opts) { WalletService.getInstanceWithAuth = function(opts, cb) { if (!checkRequired(opts, ['copayerId', 'message', 'signature'], cb)) return; - var server = new WalletService(); + var server; + try { + server = WalletService.getInstance(opts); + } catch (ex) { + return cb(ex); + } + server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) { if (err) return cb(err); if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found')); @@ -197,7 +211,6 @@ WalletService.getInstanceWithAuth = function(opts, cb) { server.copayerId = opts.copayerId; server.walletId = copayer.walletId; - server._setClientVersion(opts.clientVersion); return cb(null, server); }); }; @@ -550,47 +563,12 @@ WalletService.prototype._setClientVersion = function(version) { }; WalletService.prototype._parseClientVersion = function() { - function parse(version) { - var v = {}; - - if (!version) return null; - - var x = version.split('-'); - if (x.length != 2) { - v.agent = version; - return v; - } - v.agent = _.contains(['bwc', 'bws'], x[0]) ? 'bwc' : x[0]; - x = x[1].split('.'); - v.major = parseInt(x[0]); - v.minor = parseInt(x[1]); - v.patch = parseInt(x[2]); - - return v; - }; - if (_.isUndefined(this.parsedClientVersion)) { - this.parsedClientVersion = parse(this.clientVersion); + this.parsedClientVersion = Utils.parseVersion(this.clientVersion); } return this.parsedClientVersion; }; -WalletService.prototype._clientSupportsTXPv2 = function() { - var version = this._parseClientVersion(); - if (!version) return false; - if (version.agent != 'bwc') return true; // Asume 3rd party clients are up-to-date - if (version.major == 0 && version.minor == 0) return false; - return true; -}; - -WalletService.prototype._clientSupportsTXPv3 = function() { - var version = this._parseClientVersion(); - if (!version) return false; - if (version.agent != 'bwc') return true; // Asume 3rd party clients are up-to-date - if (version.major < 2) return false; - return true; -}; - WalletService.prototype._clientSupportsPayProRefund = function() { var version = this._parseClientVersion(); if (!version) return false; @@ -1027,37 +1005,6 @@ WalletService.prototype._totalizeUtxos = function(utxos) { }; -WalletService.prototype._computeBytesToSendMax = function(utxos, cb) { - var self = this; - - var size = { - all: 0, - confirmed: 0 - }; - - var unlockedUtxos = _.reject(utxos, 'locked'); - if (_.isEmpty(unlockedUtxos)) return cb(null, size); - - self.getWallet({}, function(err, wallet) { - if (err) return cb(err); - - var txp = Model.TxProposalLegacy.create({ - walletId: self.walletId, - requiredSignatures: wallet.m, - walletN: wallet.n, - }); - - - txp.inputs = unlockedUtxos; - size.all = txp.getEstimatedSize(); - - txp.inputs = _.filter(unlockedUtxos, 'confirmations'); - size.confirmed = txp.getEstimatedSize(); - - return cb(null, size); - }); -}; - WalletService.prototype._getBalanceFromAddresses = function(addresses, cb) { var self = this; @@ -1082,14 +1029,7 @@ WalletService.prototype._getBalanceFromAddresses = function(addresses, cb) { balance.byAddress = _.values(byAddress); - self._computeBytesToSendMax(utxos, function(err, size) { - if (err) { - log.error('Could not compute size of send max transaction', err); - } - balance.totalBytesToSendMax = _.isNumber(size.all) ? size.all : null; - balance.totalBytesToSendConfirmedMax = _.isNumber(size.confirmed) ? size.confirmed : null; - return cb(null, balance); - }); + return cb(null, balance); }); }; @@ -1728,153 +1668,6 @@ WalletService.prototype._validateOutputs = function(opts, wallet, cb) { return null; }; -WalletService._getProposalHash = function(proposalHeader) { - function getOldHash(toAddress, amount, message, payProUrl) { - return [toAddress, amount, (message || ''), (payProUrl || '')].join('|'); - }; - - // For backwards compatibility - if (arguments.length > 1) { - return getOldHash.apply(this, arguments); - } - - return Stringify(proposalHeader); -}; - -/** - * Creates a new transaction proposal. - * @param {Object} opts - * @param {string} opts.type - Proposal type. - * @param {string} opts.toAddress || opts.outputs[].toAddress - Destination address. - * @param {number} opts.amount || opts.outputs[].amount - Amount to transfer in satoshi. - * @param {string} opts.outputs[].message - A message to attach to this output. - * @param {string} opts.message - A message to attach to this transaction. - * @param {string} opts.proposalSignature - S(toAddress|amount|message|payProUrl). Used by other copayers to verify the proposal. - * @param {string} opts.inputs - Optional. Inputs for this TX - * @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX - * @param {string} opts.payProUrl - Optional: Paypro URL for peers to verify TX - * @param {string} opts.excludeUnconfirmedUtxos - Optional: Do not use UTXOs of unconfirmed transactions as inputs - * @returns {TxProposal} Transaction proposal. - */ -WalletService.prototype.createTxLegacy = function(opts, cb) { - var self = this; - - if (!opts.outputs) { - opts.outputs = _.pick(opts, ['amount', 'toAddress']); - } - opts.outputs = [].concat(opts.outputs); - - if (!checkRequired(opts, ['outputs', 'proposalSignature'], cb)) return; - - var type = opts.type || Model.TxProposalLegacy.Types.SIMPLE; - if (!Model.TxProposalLegacy.isTypeSupported(type)) - return cb(new ClientError('Invalid proposal type')); - - var feePerKb = opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB; - if (feePerKb < Defaults.MIN_FEE_PER_KB || feePerKb > Defaults.MAX_FEE_PER_KB) - return cb(new ClientError('Invalid fee per KB value')); - - self._runLocked(cb, function(cb) { - self.getWallet({}, function(err, wallet) { - if (err) return cb(err); - if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE); - - if (wallet.singleAddress) return cb(new ClientError('Not compatible with single-address wallets')); - - if (opts.payProUrl) { - if (wallet.addressType == Constants.SCRIPT_TYPES.P2PKH && !self._clientSupportsPayProRefund()) { - return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To sign this spend proposal you need to upgrade your client app.')); - } - } - - var copayer = wallet.getCopayer(self.copayerId); - var hash; - if (!opts.type || opts.type == Model.TxProposalLegacy.Types.SIMPLE) { - hash = WalletService._getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); - } else { - // should match bwc api _computeProposalSignature - var header = { - outputs: _.map(opts.outputs, function(output) { - return _.pick(output, ['toAddress', 'amount', 'message']); - }), - message: opts.message, - payProUrl: opts.payProUrl - }; - hash = WalletService._getProposalHash(header) - } - - var signingKey = self._getSigningKey(hash, opts.proposalSignature, copayer.requestPubKeys) - if (!signingKey) - return cb(new ClientError('Invalid proposal signature')); - - self._canCreateTx(function(err, canCreate) { - if (err) return cb(err); - if (!canCreate) return cb(Errors.TX_CANNOT_CREATE); - - if (type != Model.TxProposalLegacy.Types.EXTERNAL) { - var validationError = self._validateOutputs(opts, wallet, cb); - if (validationError) { - return cb(validationError); - } - } - - var txOpts = { - type: type, - walletId: self.walletId, - creatorId: self.copayerId, - outputs: opts.outputs, - inputs: opts.inputs, - toAddress: opts.toAddress, - amount: opts.amount, - message: opts.message, - proposalSignature: opts.proposalSignature, - changeAddress: wallet.createAddress(true), - feePerKb: feePerKb, - payProUrl: opts.payProUrl, - requiredSignatures: wallet.m, - requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), - walletN: wallet.n, - excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, - addressType: wallet.addressType, - derivationStrategy: wallet.derivationStrategy, - customData: opts.customData, - }; - - if (signingKey.selfSigned) { - txOpts.proposalSignaturePubKey = signingKey.key; - txOpts.proposalSignaturePubKeySig = signingKey.signature; - } - - var txp = Model.TxProposalLegacy.create(txOpts); - - if (!self._clientSupportsTXPv2()) { - txp.version = '1.0.1'; - } - - self._selectTxInputs(txp, opts.utxosToExclude, function(err) { - if (err) return cb(err); - - $.checkState(txp.inputs); - - self.storage.storeAddressAndWallet(wallet, txp.changeAddress, function(err) { - if (err) return cb(err); - - self.storage.storeTx(wallet.id, txp, function(err) { - if (err) return cb(err); - - self._notify('NewTxProposal', { - amount: txp.getTotalAmount() - }, function() { - return cb(null, txp); - }); - }); - }); - }); - }); - }); - }); -}; - WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) { var self = this; @@ -2334,12 +2127,6 @@ WalletService.prototype.signTx = function(opts, cb) { }, function(err, txp) { if (err) return cb(err); - if (!self._clientSupportsTXPv2()) { - if (!_.startsWith(txp.version, '1.')) { - return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To sign this spend proposal you need to upgrade your client app.')); - } - } - var action = _.find(txp.actions, { copayerId: self.copayerId }); @@ -2534,13 +2321,6 @@ 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); }); diff --git a/test/integration/emailnotifications.js b/test/integration/emailnotifications.js index 0bfddc4..8558814 100644 --- a/test/integration/emailnotifications.js +++ b/test/integration/emailnotifications.js @@ -80,11 +80,14 @@ describe('Email notifications', function() { } }; helpers.stubUtxos(server, wallet, [1, 1], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { setTimeout(function() { var calls = mailerStub.sendMail.getCalls(); calls.length.should.equal(2); @@ -117,11 +120,14 @@ describe('Email notifications', function() { _applyTemplate_old.call(emailService, template, undefined, cb); }; helpers.stubUtxos(server, wallet, [1, 1], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { setTimeout(function() { var calls = mailerStub.sendMail.getCalls(); calls.length.should.equal(0); @@ -146,15 +152,21 @@ describe('Email notifications', function() { } }; helpers.stubUtxos(server, wallet, [1, 1], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; var txp; async.waterfall([ function(next) { - server.createTxLegacy(txOpts, next); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + next(null, tx); + }); }, function(t, next) { txp = t; @@ -207,15 +219,21 @@ describe('Email notifications', function() { it('should notify copayers a tx has been finally rejected', function(done) { helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; var txpId; async.waterfall([ function(next) { - server.createTxLegacy(txOpts, next); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + next(null, tx); + }); }, function(txp, next) { txpId = txp.id; @@ -378,11 +396,14 @@ describe('Email notifications', function() { }, }, function(err) { helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { setTimeout(function() { var calls = mailerStub.sendMail.getCalls(); calls.length.should.equal(2); @@ -444,11 +465,14 @@ describe('Email notifications', function() { it('should NOT notify copayers a new tx proposal has been created', function(done) { helpers.stubUtxos(server, wallet, [1, 1], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { setTimeout(function() { var calls = mailerStub.sendMail.getCalls(); calls.length.should.equal(0); diff --git a/test/integration/helpers.js b/test/integration/helpers.js index f0d0cac..60cee8f 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -391,46 +391,6 @@ helpers.clientSign = function(txp, derivedXPrivKey) { return signatures; }; - -helpers.createProposalOptsLegacy = function(toAddress, amount, message, signingKey, feePerKb) { - var opts = { - toAddress: toAddress, - amount: helpers.toSatoshi(amount), - message: message, - proposalSignature: null, - }; - if (feePerKb) opts.feePerKb = feePerKb; - - var hash = WalletService._getProposalHash(toAddress, opts.amount, message); - - try { - opts.proposalSignature = helpers.signMessage(hash, signingKey); - } catch (ex) {} - - return opts; -}; - -helpers.createSimpleProposalOpts = function(toAddress, amount, signingKey, opts) { - var outputs = [{ - toAddress: toAddress, - amount: amount, - }]; - return helpers.createProposalOpts(Model.TxProposalLegacy.Types.SIMPLE, outputs, signingKey, opts); -}; - -helpers.createExternalProposalOpts = function(toAddress, amount, signingKey, moreOpts, inputs) { - var outputs = [{ - toAddress: toAddress, - amount: amount, - }]; - if (_.isArray(moreOpts)) { - inputs = moreOpts; - moreOpts = null; - } - return helpers.createProposalOpts(Model.TxProposalLegacy.Types.EXTERNAL, outputs, signingKey, moreOpts, inputs); -}; - - helpers.getProposalSignatureOpts = function(txp, signingKey) { var raw = txp.getRawTx(); var proposalSignature = helpers.signMessage(raw, signingKey); @@ -442,48 +402,6 @@ helpers.getProposalSignatureOpts = function(txp, signingKey) { }; -helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts, inputs) { - _.each(outputs, function(output) { - output.amount = helpers.toSatoshi(output.amount); - }); - - var opts = { - type: type, - proposalSignature: null, - inputs: inputs || [] - }; - - if (moreOpts) { - moreOpts = _.pick(moreOpts, ['feePerKb', 'customData', 'message', 'payProUrl']); - opts = _.assign(opts, moreOpts); - } - - opts = _.defaults(opts, { - message: null - }); - - var hash; - if (type == Model.TxProposalLegacy.Types.SIMPLE) { - opts.toAddress = outputs[0].toAddress; - opts.amount = outputs[0].amount; - hash = WalletService._getProposalHash(opts.toAddress, opts.amount, - opts.message, opts.payProUrl); - } else if (type == Model.TxProposalLegacy.Types.MULTIPLEOUTPUTS || type == Model.TxProposalLegacy.Types.EXTERNAL) { - opts.outputs = outputs; - var header = { - outputs: outputs, - message: opts.message, - payProUrl: opts.payProUrl - }; - hash = WalletService._getProposalHash(header); - } - - try { - opts.proposalSignature = helpers.signMessage(hash, signingKey); - } catch (ex) {} - - return opts; -}; helpers.createAddresses = function(server, wallet, main, change, cb) { // var clock = sinon.useFakeTimers('Date'); async.mapSeries(_.range(main + change), function(i, next) { diff --git a/test/integration/pushNotifications.js b/test/integration/pushNotifications.js index c71d837..8ff62d1 100644 --- a/test/integration/pushNotifications.js +++ b/test/integration/pushNotifications.js @@ -275,9 +275,6 @@ describe('Push notifications', function() { it('should notify copayers a new tx proposal has been created', function(done) { helpers.stubUtxos(server, wallet, [1, 1], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); server.createAddress({}, function(err, address) { should.not.exist(err); server._notify('NewTxProposal', { @@ -300,15 +297,21 @@ describe('Push notifications', function() { it('should notify copayers a tx has been finally rejected', function(done) { helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; var txpId; async.waterfall([ function(next) { - server.createTxLegacy(txOpts, next); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + next(null, tx); + }); }, function(txp, next) { txpId = txp.id; @@ -341,15 +344,21 @@ describe('Push notifications', function() { it('should notify copayers a new outgoing tx has been created', function(done) { helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; var txp; async.waterfall([ function(next) { - server.createTxLegacy(txOpts, next); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + next(null, tx); + }); }, function(t, next) { txp = t; diff --git a/test/integration/server.js b/test/integration/server.js index 6a568eb..8a73a3f 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -51,13 +51,46 @@ describe('Wallet service', function() { describe('#getInstance', function() { it('should get server instance', function() { var server = WalletService.getInstance({ - clientVersion: 'bwc-0.0.1', + clientVersion: 'bwc-2.9.0', }); - server.clientVersion.should.equal('bwc-0.0.1'); + server.clientVersion.should.equal('bwc-2.9.0'); + }); + it('should not get server instance for BWC lower than v1.2', function() { + var err; + try { + var server = WalletService.getInstance({ + clientVersion: 'bwc-1.1.99', + }); + } catch (ex) { + err = ex; + } + should.exist(err); + err.code.should.equal('UPGRADE_NEEDED'); + }); + it('should get server instance for non-BWC clients', function() { + var server = WalletService.getInstance({ + clientVersion: 'dummy-1.0.0', + }); + server.clientVersion.should.equal('dummy-1.0.0'); + server = WalletService.getInstance({}); + (server.clientVersion == null).should.be.true; }); }); describe('#getInstanceWithAuth', function() { + it('should not get server instance for BWC lower than v1.2', function(done) { + var server = WalletService.getInstanceWithAuth({ + copayerId: '1234', + message: 'hello world', + signature: 'xxx', + clientVersion: 'bwc-1.1.99', + }, function(err, server) { + should.exist(err); + should.not.exist(server); + err.code.should.equal('UPGRADE_NEEDED'); + done(); + }); + }); it('should get server instance for existing copayer', function(done) { helpers.createAndJoinWallet(1, 2, function(s, wallet) { var xpriv = TestData.copayers[0].xPrivKey; @@ -69,12 +102,12 @@ describe('Wallet service', function() { copayerId: wallet.copayers[0].id, message: 'hello world', signature: sig, - clientVersion: 'bwc-0.0.1', + clientVersion: 'bwc-2.0.0', }, function(err, server) { should.not.exist(err); server.walletId.should.equal(wallet.id); server.copayerId.should.equal(wallet.copayers[0].id); - server.clientVersion.should.equal('bwc-0.0.1'); + server.clientVersion.should.equal('bwc-2.0.0'); done(); }); }); @@ -704,13 +737,16 @@ describe('Wallet service', function() { server = s; wallet = w; - helpers.stubUtxos(server, wallet, _.range(2), function() { + helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: helpers.toSatoshi(0.1), + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.1e8, + }], + feePerKb: 100e2, }; async.eachSeries(_.range(2), function(i, next) { - server.createTxLegacy(txOpts, function(err, tx) { + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function() { next(); }); }, done); @@ -765,14 +801,17 @@ describe('Wallet service', function() { server2 = s; wallet2 = w; - helpers.stubUtxos(server2, wallet2, _.range(1, 3), function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.1, TestData.copayers[1].privKey_1H_0, { - message: 'some message' - }); + helpers.stubUtxos(server2, wallet2, [1, 2, 3], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.1e8, + }], + feePerKb: 100e2, + }; async.eachSeries(_.range(2), function(i, next) { - server2.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - next(err); + helpers.createAndPublishTx(server2, txOpts, TestData.copayers[1].privKey_1H_0, function() { + next(); }); }, next); }); @@ -887,18 +926,21 @@ describe('Wallet service', function() { }); }); it('should get status after tx creation', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { should.exist(tx); server.getStatus({}, function(err, status) { should.not.exist(err); status.pendingTxps.length.should.equal(1); var balance = status.balance; - balance.totalAmount.should.equal(helpers.toSatoshi(300)); + balance.totalAmount.should.equal(3e8); balance.lockedAmount.should.equal(tx.inputs[0].satoshis); balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); done(); @@ -1531,8 +1573,14 @@ describe('Wallet service', function() { ws.addAccess(opts, function(err, res) { should.not.exist(err); getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, reqPrivKey); - server2.createTxLegacy(txOpts, function(err, tx) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; + server2.createTx(txOpts, function(err, tx) { should.not.exist(err); done(); }); @@ -1573,9 +1621,14 @@ describe('Wallet service', function() { ws.addAccess(opts, function(err, res) { should.not.exist(err); getAuthServer(opts.copayerId, reqPrivKey, function(err, server2) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, reqPrivKey); - server2.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, reqPrivKey, function() { server2.getPendingTxs({}, function(err, txs) { should.not.exist(err); should.exist(txs[0].proposalSignaturePubKey); @@ -1607,8 +1660,6 @@ describe('Wallet service', function() { balance.totalAmount.should.equal(helpers.toSatoshi(6)); balance.lockedAmount.should.equal(0); balance.availableAmount.should.equal(helpers.toSatoshi(6)); - balance.totalBytesToSendMax.should.equal(578); - balance.totalBytesToSendConfirmedMax.should.equal(418); balance.totalConfirmedAmount.should.equal(helpers.toSatoshi(4)); balance.lockedConfirmedAmount.should.equal(0); @@ -1634,7 +1685,6 @@ describe('Wallet service', function() { balance.totalAmount.should.equal(0); balance.lockedAmount.should.equal(0); balance.availableAmount.should.equal(0); - balance.totalBytesToSendMax.should.equal(0); should.exist(balance.byAddress); balance.byAddress.length.should.equal(0); done(); @@ -1650,7 +1700,6 @@ describe('Wallet service', function() { balance.totalAmount.should.equal(0); balance.lockedAmount.should.equal(0); balance.availableAmount.should.equal(0); - balance.totalBytesToSendMax.should.equal(0); should.exist(balance.byAddress); balance.byAddress.length.should.equal(0); done(); @@ -1671,18 +1720,6 @@ describe('Wallet service', function() { }); }); }); - it('should return correct kb to send max', function(done) { - helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { - server.getBalance({}, function(err, balance) { - should.not.exist(err); - should.exist(balance); - balance.totalAmount.should.equal(helpers.toSatoshi(9)); - balance.lockedAmount.should.equal(0); - balance.totalBytesToSendMax.should.equal(1535); - done(); - }); - }); - }); it('should fail gracefully when blockchain is unreachable', function(done) { blockchainExplorer.getUtxos = sinon.stub().callsArgWith(1, 'dummy error'); server.createAddress({}, function(err, address) { @@ -1724,7 +1761,6 @@ describe('Wallet service', function() { balance.totalAmount.should.equal(helpers.toSatoshi(6)); balance.lockedAmount.should.equal(0); balance.availableAmount.should.equal(helpers.toSatoshi(6)); - balance.totalBytesToSendMax.should.equal(578); balance.totalConfirmedAmount.should.equal(helpers.toSatoshi(4)); balance.lockedConfirmedAmount.should.equal(0); @@ -2196,8 +2232,14 @@ describe('Wallet service', function() { server.joinWallet(copayerOpts, function(err, result) { should.not.exist(err); helpers.getAuthServer(result.copayerId, function(server, wallet) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey); - server.createTxLegacy(txOpts, function(err, tx) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2 + }; + server.createTx(txOpts, function(err, tx) { should.not.exist(tx); should.exist(err); err.code.should.equal('WALLET_NOT_COMPLETE'); @@ -2210,737 +2252,7 @@ describe('Wallet service', function() { }); describe('#createTx', function() { - describe('Legacy', 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, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message', - customData: 'some custom data', - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.walletId.should.equal(wallet.id); - tx.network.should.equal('livenet'); - tx.creatorId.should.equal(wallet.copayers[0].id); - tx.message.should.equal('some message'); - tx.customData.should.equal('some custom data'); - tx.isAccepted().should.equal.false; - tx.isRejected().should.equal.false; - tx.amount.should.equal(helpers.toSatoshi(80)); - var estimatedFee = Defaults.DEFAULT_FEE_PER_KB * 400 / 1000; // fully signed tx should have about 400 bytes - tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(1); - // creator - txs[0].deleteLockTime.should.equal(0); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(helpers.toSatoshi(300)); - balance.lockedAmount.should.equal(tx.inputs[0].satoshis); - balance.lockedAmount.should.be.below(balance.totalAmount); - balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); - server.storage.fetchAddresses(wallet.id, function(err, addresses) { - should.not.exist(err); - var change = _.filter(addresses, { - isChange: true - }); - change.length.should.equal(1); - done(); - }); - }); - }); - }); - }); - }); - it('should generate new change address for each created tx', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx1) { - should.not.exist(err); - should.exist(tx1); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx2) { - should.not.exist(err); - should.exist(tx2); - tx1.changeAddress.address.should.not.equal(tx2.changeAddress.address); - done(); - }); - }); - }); - }); - it('should create a tx with legacy signature', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createProposalOptsLegacy('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, 'some message', TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - done(); - }); - }); - }); - it('should assume default feePerKb for "normal" level when none is specified', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createProposalOptsLegacy('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, 'some message', TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.feePerKb.should.equal(_.find(Defaults.FEE_LEVELS, { - name: 'normal' - }).defaultValue); - done(); - }); - }); - }); - it('should support creating a tx with no change address', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var max = 3 - (7200 / 1e8); // Fees for this tx at 100bits/kB = 7200 sat - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', max, TestData.copayers[0].privKey_1H_0, { - feePerKb: 100e2 - }); - server.createTxLegacy(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - var t = txp.getBitcoreTx().toObject(); - t.outputs.length.should.equal(1); - t.outputs[0].satoshis.should.equal(max * 1e8); - done(); - }); - }); - }); - it('should create a tx using confirmed utxos first', function(done) { - helpers.stubUtxos(server, wallet, [1.3, 'u0.5', 'u0.1', 1.2], function(utxos) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1.5, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.inputs.length.should.equal(2); - _.difference(_.pluck(tx.inputs, 'txid'), [utxos[0].txid, utxos[3].txid]).length.should.equal(0); - done(); - }); - }); - }); - it('should use unconfirmed utxos only when no more confirmed utxos are available', function(done) { - helpers.stubUtxos(server, wallet, [1.3, 'u0.5', 'u0.1', 1.2], function(utxos) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2.55, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.inputs.length.should.equal(3); - var txids = _.pluck(tx.inputs, 'txid'); - txids.should.contain(utxos[0].txid); - txids.should.contain(utxos[3].txid); - done(); - }); - }); - }); - it('should exclude unconfirmed utxos if specified', function(done) { - helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 3, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - txOpts.excludeUnconfirmedUtxos = true; - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('INSUFFICIENT_FUNDS'); - err.message.should.equal('Insufficient funds'); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2.5, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - txOpts.excludeUnconfirmedUtxos = true; - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE'); - err.message.should.equal('Insufficient funds for fee'); - done(); - }); - }); - }); - }); - it('should use non-locked confirmed utxos when specified', function(done) { - helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1.4, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - txOpts.excludeUnconfirmedUtxos = true; - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.inputs.length.should.equal(2); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.lockedConfirmedAmount.should.equal(helpers.toSatoshi(2.5)); - balance.availableConfirmedAmount.should.equal(0); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.01, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - txOpts.excludeUnconfirmedUtxos = true; - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('LOCKED_FUNDS'); - done(); - }); - }); - }); - }); - }); - it('should fail gracefully if unable to reach the blockchain', function(done) { - blockchainExplorer.getUtxos = sinon.stub().callsArgWith(1, 'dummy error'); - server.createAddress({}, function(err, address) { - should.not.exist(err); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.toString().should.equal('dummy error'); - done(); - }); - }); - }); - it('should fail to create tx with invalid proposal signature', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, 'dummy'); - - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(tx); - should.exist(err); - err.message.should.equal('Invalid proposal signature'); - done(); - }); - }); - }); - it('should fail to create tx with proposal signed by another copayer', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[1].privKey_1H_0); - - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(tx); - should.exist(err); - err.message.should.equal('Invalid proposal signature'); - done(); - }); - }); - }); - it('should fail to create tx for invalid address', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('invalid address', 80, TestData.copayers[0].privKey_1H_0); - - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - should.not.exist(tx); - // may fail due to Non-base58 character, or Checksum mismatch, or other - done(); - }); - }); - }); - it('should fail to create tx for address of different network', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD', 80, TestData.copayers[0].privKey_1H_0); - - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(tx); - should.exist(err); - err.code.should.equal('INCORRECT_ADDRESS_NETWORK'); - err.message.should.equal('Incorrect address network'); - done(); - }); - }); - }); - it('should fail to create tx for invalid amount', function(done) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(tx); - should.exist(err); - err.message.should.equal('Invalid amount'); - done(); - }); - }); - it('should fail to create tx when insufficient funds', function(done) { - helpers.stubUtxos(server, wallet, [100], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 120, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('INSUFFICIENT_FUNDS'); - err.message.should.equal('Insufficient funds'); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(0); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.lockedAmount.should.equal(0); - balance.totalAmount.should.equal(10000000000); - done(); - }); - }); - }); - }); - }); - it('should fail to create tx when insufficient funds for fee', function(done) { - helpers.stubUtxos(server, wallet, 0.048222, function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.048200, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE'); - err.message.should.equal('Insufficient funds for fee'); - done(); - }); - }); - }); - it('should scale fees according to tx size', function(done) { - helpers.stubUtxos(server, wallet, [1, 1, 1, 1], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 3.5, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - var estimatedFee = Defaults.DEFAULT_FEE_PER_KB * 1300 / 1000; // fully signed tx should have about 1300 bytes - tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee); - done(); - }); - }); - }); - it('should be possible to use a smaller fee', function(done) { - helpers.stubUtxos(server, wallet, 1, function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.9999, TestData.copayers[0].privKey_1H_0, { - feePerKb: 80000 - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE'); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.9999, TestData.copayers[0].privKey_1H_0, { - feePerKb: 5000 - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - var estimatedFee = 5000 * 410 / 1000; // fully signed tx should have about 410 bytes - tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee); - - // Sign it to make sure Bitcore doesn't complain about the fees - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); - server.signTx({ - txProposalId: tx.id, - signatures: signatures, - }, function(err) { - should.not.exist(err); - done(); - }); - }); - }); - }); - }); - it('should fail to create a tx exceeding max size in kb', function(done) { - var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB; - Defaults.MAX_TX_SIZE_IN_KB = 1; - helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 8, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('TX_MAX_SIZE_EXCEEDED'); - Defaults.MAX_TX_SIZE_IN_KB = _oldDefault; - done(); - }); - }); - }); - it('should fail to create tx for dust amount', function(done) { - helpers.stubUtxos(server, wallet, [1], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.00000001, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('DUST_AMOUNT'); - err.message.should.equal('Amount below dust threshold'); - done(); - }); - }); - }); - it('should modify fee if tx would return change for dust amount', function(done) { - helpers.stubUtxos(server, wallet, [1], function() { - var fee = 4095; // The exact fee of the resulting tx (based exclusively on feePerKB && size) - var change = 100; // Below dust - var amount = (1e8 - fee - change) / 1e8; - - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount, TestData.copayers[0].privKey_1H_0, { - feePerKb: 10000 - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - tx.fee.should.equal(fee + change); - done(); - }); - }); - }); - it('should fail with different error for insufficient funds and locked funds', function(done) { - helpers.stubUtxos(server, wallet, [10, 10], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 11, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(helpers.toSatoshi(20)); - balance.lockedAmount.should.equal(helpers.toSatoshi(20)); - txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 8, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('LOCKED_FUNDS'); - err.message.should.equal('Funds are locked by pending transaction proposals'); - done(); - }); - }); - }); - }); - }); - it('should create tx with 0 change output', function(done) { - helpers.stubUtxos(server, wallet, [1], function() { - var fee = 4100 / 1e8; // The exact fee of the resulting tx - var amount = 1 - fee; - - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount, TestData.copayers[0].privKey_1H_0, { - feePerKb: 100e2 - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - var bitcoreTx = tx.getBitcoreTx(); - bitcoreTx.outputs.length.should.equal(1); - bitcoreTx.outputs[0].satoshis.should.equal(tx.amount); - done(); - }); - }); - }); - it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) { - helpers.stubUtxos(server, wallet, [10], function() { - var bitcoreStub = sinon.stub(Bitcore, 'Transaction'); - bitcoreStub.throws({ - name: 'dummy', - message: 'dummy exception' - }); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.message.should.equal('dummy exception'); - bitcoreStub.restore(); - done(); - }); - }); - }); - it('should create tx when there is a pending tx and enough UTXOs', function(done) { - helpers.stubUtxos(server, wallet, [10.1, 10.2, 10.3], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 12, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - var txOpts2 = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 8, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts2, function(err, tx) { - should.not.exist(err); - should.exist(tx); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(2); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(3060000000); - balance.lockedAmount.should.equal(3060000000); - done(); - }); - }); - }); - }); - }); - }); - it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) { - helpers.stubUtxos(server, wallet, [10.1, 10.2, 10.3], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 12, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - var txOpts2 = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 24, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts2, function(err, tx) { - err.code.should.equal('LOCKED_FUNDS'); - should.not.exist(tx); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(1); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(helpers.toSatoshi(30.6)); - var amountInputs = _.sum(txs[0].inputs, 'satoshis'); - balance.lockedAmount.should.equal(amountInputs); - balance.lockedAmount.should.be.below(balance.totalAmount); - balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); - done(); - }); - }); - }); - }); - }); - }); - it('should create tx using different UTXOs for simultaneous requests', function(done) { - var N = 5; - helpers.stubUtxos(server, wallet, _.range(100, 100 + N, 0), function(utxos) { - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(helpers.toSatoshi(N * 100)); - balance.lockedAmount.should.equal(0); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0); - async.map(_.range(N), function(i, cb) { - server.createTxLegacy(txOpts, function(err, tx) { - cb(err, tx); - }); - }, function(err) { - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(N); - _.uniq(_.pluck(txs, 'changeAddress')).length.should.equal(N); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(helpers.toSatoshi(N * 100)); - balance.lockedAmount.should.equal(balance.totalAmount); - done(); - }); - }); - }); - }); - }); - }); - it('should create tx for type multiple_outputs', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var outputs = [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 75, - message: 'message #1' - }, { - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 75, - message: 'message #2' - }]; - var txOpts = helpers.createProposalOpts(Model.TxProposalLegacy.Types.MULTIPLEOUTPUTS, outputs, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.amount.should.equal(helpers.toSatoshi(150)); - done(); - }); - }); - }); - it('should support creating a multiple output tx with no change address', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var max = 3 - (7560 / 1e8); // Fees for this tx at 100bits/kB = 7560 sat - var outputs = [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1, - message: 'message #1' - }, { - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: max - 1, - message: 'message #2' - }]; - var txOpts = helpers.createProposalOpts(Model.TxProposalLegacy.Types.MULTIPLEOUTPUTS, outputs, TestData.copayers[0].privKey_1H_0, { - message: 'some message', - feePerKb: 100e2, - }); - server.createTxLegacy(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - - var t = txp.getBitcoreTx().toObject(); - t.outputs.length.should.equal(2); - _.sum(t.outputs, 'satoshis').should.equal(max * 1e8); - done(); - }); - }); - }); - it('should fail to create tx for type multiple_outputs with missing output argument', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var outputs = [{ - amount: 80, - message: 'message #1', - }, { - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 90, - message: 'message #2' - }]; - var txOpts = helpers.createProposalOpts(Model.TxProposalLegacy.Types.MULTIPLEOUTPUTS, outputs, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.message.should.contain('Argument missing in output #1.'); - done(); - }); - }); - }); - it('should fail to create tx for unsupported proposal type', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - txOpts.type = 'bogus'; - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.message.should.contain('Invalid proposal type'); - done(); - }); - }); - }); - it('should be able to create tx with inputs argument', function(done) { - helpers.stubUtxos(server, wallet, [1, 3, 2], function(utxos) { - server.getUtxos({}, function(err, utxos) { - should.not.exist(err); - var inputs = [utxos[0], utxos[2]]; - var txOpts = helpers.createExternalProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2.5, - TestData.copayers[0].privKey_1H_0, inputs); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.inputs.length.should.equal(2); - var txids = _.pluck(tx.inputs, 'txid'); - txids.should.contain(utxos[0].txid); - txids.should.contain(utxos[2].txid); - done(); - }); - }); - }); - }); - it('should be able to send max amount', function(done) { - helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(helpers.toSatoshi(9)); - balance.lockedAmount.should.equal(0); - balance.availableAmount.should.equal(helpers.toSatoshi(9)); - balance.totalBytesToSendMax.should.equal(2896); - balance.totalBytesToSendConfirmedMax.should.equal(2896); - var fee = parseInt((balance.totalBytesToSendMax * 10000 / 1000).toFixed(0)); - var max = balance.availableAmount - fee; - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', max / 1e8, TestData.copayers[0].privKey_1H_0, { - feePerKb: 100e2, - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.amount.should.equal(max); - var estimatedFee = 2896 * 10000 / 1000; - tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.lockedAmount.should.equal(helpers.toSatoshi(9)); - balance.availableAmount.should.equal(0); - done(); - }); - }); - }); - }); - }); - it('should be able to send max non-locked amount', function(done) { - helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 3.5, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(helpers.toSatoshi(9)); - balance.lockedAmount.should.equal(helpers.toSatoshi(4)); - balance.availableAmount.should.equal(helpers.toSatoshi(5)); - balance.totalBytesToSendMax.should.equal(1653); - balance.totalBytesToSendConfirmedMax.should.equal(1653); - var fee = parseInt((balance.totalBytesToSendMax * 2000 / 1000).toFixed(0)); - var max = balance.availableAmount - fee; - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', max / 1e8, TestData.copayers[0].privKey_1H_0, { - feePerKb: 2000 - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.amount.should.equal(max); - var estimatedFee = 1653 * 2000 / 1000; - tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.lockedAmount.should.equal(helpers.toSatoshi(9)); - done(); - }); - }); - }); - }); - }); - }); - it('should be able to send max confirmed', function(done) { - helpers.stubUtxos(server, wallet, [1, 1, 'u1', 'u1'], function() { - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.totalAmount.should.equal(helpers.toSatoshi(4)); - balance.totalConfirmedAmount.should.equal(helpers.toSatoshi(2)); - balance.lockedAmount.should.equal(0); - balance.availableAmount.should.equal(helpers.toSatoshi(4)); - balance.availableConfirmedAmount.should.equal(helpers.toSatoshi(2)); - balance.totalBytesToSendMax.should.equal(1342); - balance.totalBytesToSendConfirmedMax.should.equal(720); - var fee = parseInt((balance.totalBytesToSendConfirmedMax * 10000 / 1000).toFixed(0)); - var max = balance.availableConfirmedAmount - fee; - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', max / 1e8, TestData.copayers[0].privKey_1H_0, { - feePerKb: 100e2, - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.amount.should.equal(max); - var estimatedFee = 720 * 10000 / 1000; - tx.fee.should.be.within(0.9 * estimatedFee, 1.1 * estimatedFee); - server.getBalance({}, function(err, balance) { - should.not.exist(err); - balance.lockedAmount.should.equal(helpers.toSatoshi(2)); - balance.availableConfirmedAmount.should.equal(0); - balance.availableAmount.should.equal(helpers.toSatoshi(2)); - done(); - }); - }); - }); - }); - }); - it('should not use UTXO provided in utxosToExclude option', function(done) { - helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 4.5, TestData.copayers[0].privKey_1H_0); - txOpts.utxosToExclude = [utxos[1].txid + ':' + utxos[1].vout]; - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('INSUFFICIENT_FUNDS'); - err.message.should.equal('Insufficient funds'); - done(); - }); - }); - }); - it('should use non-excluded UTXOs', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function(utxos) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.5, TestData.copayers[0].privKey_1H_0); - txOpts.utxosToExclude = [utxos[0].txid + ':' + utxos[0].vout]; - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - tx.inputs.length.should.equal(1); - tx.inputs[0].txid.should.equal(utxos[1].txid); - tx.inputs[0].vout.should.equal(utxos[1].vout); - done(); - }); - }); - }); - }); - - describe('New', function() { + describe('Tx proposal creation & publishing', function() { var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(2, 3, function(s, w) { @@ -2982,94 +2294,6 @@ describe('Wallet service', function() { }); }); }); - it('should create a tx with foreign ID', function(done) { - helpers.stubUtxos(server, wallet, 2, function() { - var txOpts = { - txProposalId: '123', - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.id.should.equal('123'); - done(); - }); - }); - }); - it('should return already created tx if same foreign ID is specified and tx still unpublished', function(done) { - helpers.stubUtxos(server, wallet, 2, function() { - var txOpts = { - txProposalId: '123', - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.id.should.equal('123'); - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.id.should.equal('123'); - server.storage.fetchTxs(wallet.id, {}, function(err, txs) { - should.not.exist(err); - should.exist(txs); - txs.length.should.equal(1); - done(); - }); - }); - }); - }); - }); - it('should fail to create tx if same foreign ID is specified and tx already published', function(done) { - helpers.stubUtxos(server, wallet, [2, 2, 2], function() { - var txOpts = { - txProposalId: '123', - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 1e8, - }], - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.id.should.equal('123'); - var publishOpts = helpers.getProposalSignatureOpts(tx, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, function(err) { - should.not.exist(err); - server.createTx(txOpts, function(err, tx) { - should.exist(err); - should.not.exist(tx); - err.code.should.equal('TX_ALREADY_EXISTS'); - server.storage.fetchTxs(wallet.id, {}, function(err, txs) { - should.not.exist(err); - should.exist(txs); - txs.length.should.equal(1); - txOpts.txProposalId = null; - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.id.should.not.equal('123'); - server.storage.fetchTxs(wallet.id, {}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(2); - done(); - }); - }); - }); - }); - }); - }); - }); - }); it('should be able to publish a temporary tx proposal', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { @@ -3123,6 +2347,288 @@ describe('Wallet service', function() { }); }); }); + it('should generate new change address for each created tx', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx1) { + should.not.exist(err); + should.exist(tx1); + server.createTx(txOpts, function(err, tx2) { + should.not.exist(err); + should.exist(tx2); + tx1.changeAddress.address.should.not.equal(tx2.changeAddress.address); + done(); + }); + }); + }); + }); + it('should support creating a tx with no change address', function(done) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var max = 3e8 - 7000; // Fees for this tx at 100bits/kB = 7000 sat + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: max, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, txp) { + should.not.exist(err); + should.exist(txp); + var t = txp.getBitcoreTx().toObject(); + t.outputs.length.should.equal(1); + t.outputs[0].satoshis.should.equal(max); + done(); + }); + }); + }); + it('should fail gracefully if unable to reach the blockchain', function(done) { + blockchainExplorer.getUtxos = sinon.stub().callsArgWith(1, 'dummy error'); + server.createAddress({}, function(err, address) { + should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 1e8 + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.toString().should.equal('dummy error'); + done(); + }); + }); + }); + it('should fail to create tx for invalid address', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = { + outputs: [{ + toAddress: 'invalid address', + amount: 0.5e8 + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + should.not.exist(tx); + // may fail due to Non-base58 character, or Checksum mismatch, or other + done(); + }); + }); + }); + it('should fail to create tx for address of different network', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = { + outputs: [{ + toAddress: 'myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD', + amount: 0.5e8 + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(tx); + should.exist(err); + err.code.should.equal('INCORRECT_ADDRESS_NETWORK'); + err.message.should.equal('Incorrect address network'); + done(); + }); + }); + }); + it('should fail to create tx for invalid amount', function(done) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(tx); + should.exist(err); + err.message.should.equal('Invalid amount'); + done(); + }); + }); + it('should fail to create a tx exceeding max size in kb', function(done) { + var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB; + Defaults.MAX_TX_SIZE_IN_KB = 1; + helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 8e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('TX_MAX_SIZE_EXCEEDED'); + Defaults.MAX_TX_SIZE_IN_KB = _oldDefault; + done(); + }); + }); + }); + it('should fail with different error for insufficient funds and locked funds', function(done) { + helpers.stubUtxos(server, wallet, [1, 1], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 1.1e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.totalAmount.should.equal(2e8); + balance.lockedAmount.should.equal(2e8); + txOpts.outputs[0].amount = 0.8e8; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('LOCKED_FUNDS'); + err.message.should.equal('Funds are locked by pending transaction proposals'); + done(); + }); + }); + }); + }); + }); + it('should create tx with 0 change output', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var fee = 4100; // The exact fee of the resulting tx + var amount = 1e8 - fee; + + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: amount, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + var bitcoreTx = tx.getBitcoreTx(); + bitcoreTx.outputs.length.should.equal(1); + bitcoreTx.outputs[0].satoshis.should.equal(tx.amount); + done(); + }); + }); + }); + it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) { + helpers.stubUtxos(server, wallet, 1, function() { + var bitcoreStub = sinon.stub(Bitcore, 'Transaction'); + bitcoreStub.throws({ + name: 'dummy', + message: 'dummy exception' + }); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.5e8, + }], + feePerKb: 100e2, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.message.should.equal('dummy exception'); + bitcoreStub.restore(); + done(); + }); + }); + }); + it('should create tx when there is a pending tx and enough UTXOs', function(done) { + helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 1.5e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + should.exist(tx); + txOpts.outputs[0].amount = 0.8e8; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + should.exist(tx); + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(2); + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.totalAmount.should.equal(3.6e8); + balance.lockedAmount.should.equal(3.6e8); + done(); + }); + }); + }); + }); + }); + }); + it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) { + helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 1.5e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + should.exist(tx); + txOpts.outputs[0].amount = 1.8e8; + server.createTx(txOpts, function(err, tx) { + err.code.should.equal('LOCKED_FUNDS'); + should.not.exist(tx); + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(1); + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.totalAmount.should.equal(3.6e8); + var amountInputs = _.sum(txs[0].inputs, 'satoshis'); + balance.lockedAmount.should.equal(amountInputs); + balance.lockedAmount.should.be.below(balance.totalAmount); + balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount); + done(); + }); + }); + }); + }); + }); + }); + it('should be able to create tx with inputs argument', function(done) { + helpers.stubUtxos(server, wallet, [1, 3, 2], function(utxos) { + server.getUtxos({}, function(err, utxos) { + should.not.exist(err); + var inputs = [utxos[0], utxos[2]]; + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 2.5e8, + }], + feePerKb: 100e2, + inputs: inputs, + }; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.inputs.length.should.equal(2); + var txids = _.pluck(tx.inputs, 'txid'); + txids.should.contain(utxos[0].txid); + txids.should.contain(utxos[2].txid); + done(); + }); + }); + }); + }); it('should delay NewTxProposal notification until published', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function() { var txOpts = { @@ -3333,38 +2839,6 @@ describe('Wallet service', function() { done(); }); }); - it('should fail to list pending proposals from legacy client', function(done) { - helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 0.8 * 1e8, - }], - message: 'some message', - customData: 'some custom data', - feePerKb: 100e2, - }; - server.createTx(txOpts, function(err, txp) { - should.not.exist(err); - should.exist(txp); - var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0); - server.publishTx(publishOpts, function(err) { - should.not.exist(err); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(1); - - server._setClientVersion('bwc-1.1.8'); - server.getPendingTxs({}, function(err, txs) { - should.exist(err); - err.code.should.equal('UPGRADE_NEEDED'); - done(); - }); - }); - }); - }); - }); - }); it('should be able to specify change address', function(done) { helpers.stubUtxos(server, wallet, [1, 2], function(utxos) { var txOpts = { @@ -3483,7 +2957,6 @@ describe('Wallet service', function() { }); }); }); - }); describe('Backoff time', function(done) { @@ -3507,13 +2980,18 @@ describe('Wallet service', function() { it('should follow backoff time after consecutive rejections', function(done) { clock = sinon.useFakeTimers(Date.now(), 'Date'); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 1e8, + }], + feePerKb: 100e2, + }; async.series([ function(next) { async.each(_.range(3), function(i, next) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { server.rejectTx({ txProposalId: tx.id, reason: 'some reason', @@ -3524,8 +3002,7 @@ describe('Wallet service', function() { }, function(next) { // Allow a 4th tx - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { server.rejectTx({ txProposalId: tx.id, reason: 'some reason', @@ -3534,8 +3011,7 @@ describe('Wallet service', function() { }, function(next) { // Do not allow before backoff time - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { + server.createTx(txOpts, function(err, tx) { should.exist(err); err.code.should.equal('TX_CANNOT_CREATE'); next(); @@ -3543,9 +3019,7 @@ describe('Wallet service', function() { }, function(next) { clock.tick((Defaults.BACKOFF_TIME + 1) * 1000); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { server.rejectTx({ txProposalId: tx.id, reason: 'some reason', @@ -3555,8 +3029,7 @@ describe('Wallet service', function() { function(next) { // Do not allow a 5th tx before backoff time clock.tick((Defaults.BACKOFF_TIME - 1) * 1000); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { + server.createTx(txOpts, function(err, tx) { should.exist(err); err.code.should.equal('TX_CANNOT_CREATE'); next(); @@ -3564,9 +3037,7 @@ describe('Wallet service', function() { }, function(next) { clock.tick(2000); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { server.rejectTx({ txProposalId: tx.id, reason: 'some reason', @@ -3594,6 +3065,80 @@ describe('Wallet service', function() { log.level = 'info'; }); + it('should exclude unconfirmed utxos if specified', function(done) { + helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 3e8 + }], + feePerKb: 100e2, + excludeUnconfirmedUtxos: true, + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('INSUFFICIENT_FUNDS'); + err.message.should.equal('Insufficient funds'); + txOpts.outputs[0].amount = 2.5e8; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE'); + err.message.should.equal('Insufficient funds for fee'); + done(); + }); + }); + }); + }); + it('should use non-locked confirmed utxos when specified', function(done) { + helpers.stubUtxos(server, wallet, [1.3, 'u2', 'u0.1', 1.2], function(utxos) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 1.4e8 + }], + feePerKb: 100e2, + excludeUnconfirmedUtxos: true, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { + should.exist(tx); + tx.inputs.length.should.equal(2); + server.getBalance({}, function(err, balance) { + should.not.exist(err); + balance.lockedConfirmedAmount.should.equal(helpers.toSatoshi(2.5)); + balance.availableConfirmedAmount.should.equal(0); + txOpts.outputs[0].amount = 0.01e8; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('LOCKED_FUNDS'); + done(); + }); + }); + }); + }); + }); + it('should not use UTXO provided in utxosToExclude option', function(done) { + helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 3.5e8, + }], + feePerKb: 100e2, + utxosToExclude: [utxos[2].txid + ':' + utxos[2].vout], + }; + server.createTx(txOpts, function(err, tx) { + should.exist(err); + err.code.should.equal('INSUFFICIENT_FUNDS'); + err.message.should.equal('Insufficient funds'); + txOpts.utxosToExclude = [utxos[0].txid + ':' + utxos[0].vout]; + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + should.exist(tx); + done(); + }); + }); + }); + }); it('should select a single utxo if within thresholds relative to tx amount', function(done) { helpers.stubUtxos(server, wallet, [1, '350bit', '100bit', '100bit', '100bit'], function() { var txOpts = { @@ -4361,17 +3906,6 @@ describe('Wallet service', function() { }); }); }); - it('should not allow legacy txs', function(done) { - helpers.stubUtxos(server, wallet, 2, function() { - var toAddress = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7'; - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.message.should.contain('single-address'); - done(); - }); - }); - }); it('should not be able to specify custom changeAddress', function(done) { helpers.stubUtxos(server, wallet, 2, function() { var toAddress = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7'; @@ -4602,9 +4136,14 @@ describe('Wallet service', function() { server = s; wallet = w; helpers.stubUtxos(server, wallet, _.range(1, 9), function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 10e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { should.exist(tx); txid = tx.id; done(); @@ -4641,7 +4180,6 @@ describe('Wallet service', function() { }); }); }); - it('should fail to reject non-pending TX', function(done) { async.waterfall([ @@ -4693,9 +4231,14 @@ describe('Wallet service', function() { server = s; wallet = w; helpers.stubUtxos(server, wallet, [1, 2], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2.5, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 2.5e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { should.exist(tx); tx.addressType.should.equal('P2PKH'); txid = tx.id; @@ -4742,9 +4285,14 @@ describe('Wallet service', function() { server = s; wallet = w; helpers.stubUtxos(server, wallet, _.range(1, 9), function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 20, TestData.copayers[0].privKey_1H_0); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 20e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { should.exist(tx); txid = tx.id; done(); @@ -4943,264 +4491,207 @@ describe('Wallet service', function() { describe('#broadcastTx & #broadcastRawTx', function() { var server, wallet, txpid, txid; - describe('Legacy', function() { - - beforeEach(function(done) { - helpers.createAndJoinWallet(1, 1, function(s, w) { - server = s; - wallet = w; - helpers.stubUtxos(server, wallet, [10, 10], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, txp) { + beforeEach(function(done) { + helpers.createAndJoinWallet(1, 1, function(s, w) { + server = s; + wallet = w; + helpers.stubUtxos(server, wallet, [10, 10], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 9e8, + }], + message: 'some message', + feePerKb: 100e2, + }; + 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); - 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(); - }); - }); - }); - }); - }); - - it('should broadcast a tx', function(done) { - var clock = sinon.useFakeTimers(1234000, 'Date'); - 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({ - txProposalId: txpid - }, function(err) { - should.exist(err); - err.code.should.equal('TX_ALREADY_BROADCASTED'); - done(); - }); - }); - }); - - it('should auto process already broadcasted txs', function(done) { - helpers.stubBroadcast(); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(1); - blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, { - txid: 999 - }); - server.getPendingTxs({}, function(err, txs) { - should.not.exist(err); - txs.length.should.equal(0); - done(); - }); - }); - }); - - 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); + txp.isAccepted().should.be.true; + txp.isBroadcasted().should.be.false; + txid = txp.txid; + txpid = txp.id; 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) { + it('should broadcast a tx', function(done) { + var clock = sinon.useFakeTimers(1234000, 'Date'); + helpers.stubBroadcast(); + server.broadcastTx({ + txProposalId: txpid + }, function(err) { + should.not.exist(err); + server.getTx({ + txProposalId: txpid + }, 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(); - }); + 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 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); + }); + 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({ 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(); - }); + err.code.should.equal('TX_ALREADY_BROADCASTED'); + done(); }); }); - - it('should mark tx as broadcasted if accepted but already in blockchain', function(done) { - blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error'); + }); + it('should auto process already broadcasted txs', function(done) { + helpers.stubBroadcast(); + server.getPendingTxs({}, function(err, txs) { + should.not.exist(err); + txs.length.should.equal(1); blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, { - txid: '999' + txid: 999 }); - server.broadcastTx({ - txProposalId: txpid - }, function(err) { + server.getPendingTxs({}, function(err, txs) { should.not.exist(err); - server.getTx({ - txProposalId: txpid - }, function(err, txp) { + txs.length.should.equal(0); + done(); + }); + }); + }); + it('should process only broadcasted txs', function(done) { + helpers.stubBroadcast(); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 9e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { + 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); - should.exist(txp.txid); - txp.isBroadcasted().should.be.true; - should.exist(txp.broadcastedOn); + txs.length.should.equal(1); + txs[0].status.should.equal('pending'); + should.not.exist(txs[0].txid); 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'); + }); + it('should fail to brodcast a not yet accepted tx', function(done) { + helpers.stubBroadcast(); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 9e8, + }], + feePerKb: 100e2, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { + should.exist(txp); server.broadcastTx({ - txProposalId: txpid + txProposalId: txp.id }, 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(); - }); + err.code.should.equal('TX_NOT_ACCEPTED'); + done(); }); }); }); - - describe('New', function() { - beforeEach(function(done) { - helpers.createAndJoinWallet(1, 1, function(s, w) { - server = s; - wallet = w; - helpers.stubUtxos(server, wallet, [10, 10], function() { - var txOpts = { - outputs: [{ - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 9e8, - }], - message: 'some message', - feePerKb: 100e2, - }; - 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(); - }); - }); - }); - }); - }); - - it('should broadcast a tx', function(done) { - var clock = sinon.useFakeTimers(1234000, 'Date'); - helpers.stubBroadcast(); - server.broadcastTx({ + 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) { + }, function(err, txp) { 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(); - }); + 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(); + }); + }); + }); + }); describe('Tx proposal workflow', function() { @@ -5217,11 +4708,15 @@ describe('Wallet service', function() { }); it('other copayers should see pending proposal created by one copayer', function(done) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, txp) { - should.not.exist(err); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 10e8 + }], + feePerKb: 100e2, + message: 'some message', + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { should.exist(txp); helpers.getAuthServer(wallet.copayers[1].id, function(server2, wallet) { server2.getPendingTxs({}, function(err, txps) { @@ -5234,18 +4729,21 @@ describe('Wallet service', function() { }); }); }); - it('tx proposals should not be finally accepted until quorum is reached', function(done) { var txpId; async.waterfall([ function(next) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, txp) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 10e8 + }], + feePerKb: 100e2, + message: 'some message', + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { txpId = txp.id; - should.not.exist(err); should.exist(txp); next(); }); @@ -5324,18 +4822,21 @@ describe('Wallet service', function() { }, ]); }); - it('tx proposals should accept as many rejections as possible without finally rejecting', function(done) { var txpId; async.waterfall([ function(next) { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, txp) { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 10e8 + }], + feePerKb: 100e2, + message: 'some message', + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { txpId = txp.id; - should.not.exist(err); should.exist(txp); next(); }); @@ -5413,12 +4914,16 @@ describe('Wallet service', function() { helpers.createAndJoinWallet(2, 3, function(s, w) { server = s; wallet = w; - helpers.stubUtxos(server, wallet, 10, function() { - 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); + helpers.stubUtxos(server, wallet, 1, function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.5e8 + }], + feePerKb: 100e2, + message: 'some message', + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { should.exist(txp); txpid = txp.id; done(); @@ -5474,11 +4979,17 @@ describe('Wallet service', function() { server = s; wallet = w; helpers.stubUtxos(server, wallet, _.range(1, 11), function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.1, TestData.copayers[0].privKey_1H_0); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.1e8 + }], + feePerKb: 100e2, + message: 'some message', + }; async.eachSeries(_.range(10), function(i, next) { clock.tick(10 * 1000); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { next(); }); }, function(err) { @@ -5491,7 +5002,6 @@ describe('Wallet service', function() { afterEach(function() { clock.restore(); }); - it('should pull 4 txs, down to to time 60', function(done) { server.getTxs({ minTs: 60, @@ -5503,7 +5013,6 @@ describe('Wallet service', function() { done(); }); }); - it('should pull the first 5 txs', function(done) { server.getTxs({ maxTs: 50, @@ -5515,7 +5024,6 @@ describe('Wallet service', function() { done(); }); }); - it('should pull the last 4 txs', function(done) { server.getTxs({ limit: 4 @@ -5526,7 +5034,6 @@ describe('Wallet service', function() { done(); }); }); - it('should pull all txs', function(done) { server.getTxs({}, function(err, txps) { should.not.exist(err); @@ -5535,8 +5042,6 @@ describe('Wallet service', function() { done(); }); }); - - it('should txs from times 50 to 70', function(done) { server.getTxs({ @@ -5561,11 +5066,17 @@ describe('Wallet service', function() { server = s; wallet = w; helpers.stubUtxos(server, wallet, _.range(4), function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.01, TestData.copayers[0].privKey_1H_0); + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.1e8 + }], + feePerKb: 100e2, + message: 'some message', + }; async.eachSeries(_.range(3), function(i, next) { clock.tick(25 * 1000); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) { next(); }); }, function(err) { @@ -5578,7 +5089,6 @@ describe('Wallet service', function() { afterEach(function() { clock.restore(); }); - it('should pull all notifications', function(done) { server.getNotifications({}, function(err, notifications) { should.not.exist(err); @@ -5593,7 +5103,6 @@ describe('Wallet service', function() { done(); }); }); - it('should pull new block notifications along with wallet notifications in the last 60 seconds', function(done) { // Simulate new block notification server.walletId = 'livenet'; @@ -5625,7 +5134,6 @@ describe('Wallet service', function() { }); }); }); - it('should pull notifications in the last 60 seconds', function(done) { server.getNotifications({ minTs: +Date.now() - (60 * 1000), @@ -5636,7 +5144,6 @@ describe('Wallet service', function() { done(); }); }); - it('should pull notifications after a given notification id', function(done) { server.getNotifications({}, function(err, notifications) { should.not.exist(err); @@ -5652,7 +5159,6 @@ describe('Wallet service', function() { }); }); }); - it('should return empty if no notifications found after a given id', function(done) { server.getNotifications({}, function(err, notifications) { should.not.exist(err); @@ -5666,7 +5172,6 @@ describe('Wallet service', function() { }); }); }); - it('should return empty if no notifications exist in the given timespan', function(done) { clock.tick(100 * 1000); server.getNotifications({ @@ -5677,7 +5182,6 @@ describe('Wallet service', function() { done(); }); }); - it('should contain walletId & creatorId on NewCopayer', function(done) { server.getNotifications({}, function(err, notifications) { should.not.exist(err); @@ -5688,7 +5192,6 @@ describe('Wallet service', function() { done(); }); }); - it('should notify sign and acceptance', function(done) { server.getPendingTxs({}, function(err, txs) { blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error'); @@ -5710,7 +5213,6 @@ describe('Wallet service', function() { }); }); }); - it('should notify rejection', function(done) { server.getPendingTxs({}, function(err, txs) { var tx = txs[1]; @@ -5730,8 +5232,6 @@ describe('Wallet service', function() { }); }); }); - - it('should notify sign, acceptance, and broadcast, and emit', function(done) { server.getPendingTxs({}, function(err, txs) { var tx = txs[2]; @@ -5759,8 +5259,6 @@ describe('Wallet service', function() { }); }); }); - - it('should notify sign, acceptance, and broadcast, and emit (with 3rd party broadcast', function(done) { server.getPendingTxs({}, function(err, txs) { var tx = txs[2]; @@ -5799,11 +5297,16 @@ describe('Wallet service', function() { helpers.createAndJoinWallet(2, 3, function(s, w) { server = s; wallet = w; - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2, + message: 'some message', + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function() { server.getPendingTxs({}, function(err, txs) { txp = txs[0]; done(); @@ -5812,8 +5315,6 @@ describe('Wallet service', function() { }); }); }); - - it('should allow creator to remove an unsigned TX', function(done) { server.removePendingTx({ txProposalId: txp.id @@ -5825,7 +5326,6 @@ describe('Wallet service', function() { }); }); }); - it('should allow creator to remove a signed TX by himself', function(done) { var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ @@ -5844,7 +5344,6 @@ describe('Wallet service', function() { }); }); }); - it('should fail to remove non-pending TX', function(done) { async.waterfall([ @@ -5896,7 +5395,6 @@ describe('Wallet service', function() { }, ]); }); - it('should not allow non-creator copayer to remove an unsigned TX ', function(done) { helpers.getAuthServer(wallet.copayers[1].id, function(server2) { server2.removePendingTx({ @@ -5911,7 +5409,6 @@ describe('Wallet service', function() { }); }); }); - it('should not allow creator copayer to remove a TX signed by other copayer, in less than 24hrs', function(done) { helpers.getAuthServer(wallet.copayers[1].id, function(server2) { var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); @@ -5930,7 +5427,6 @@ describe('Wallet service', function() { }); }); }); - it('should allow creator copayer to remove a TX rejected by other copayer, in less than 24hrs', function(done) { helpers.getAuthServer(wallet.copayers[1].id, function(server2) { var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); @@ -5948,9 +5444,6 @@ describe('Wallet service', function() { }); }); }); - - - it('should allow creator copayer to remove a TX signed by other copayer, after 24hrs', function(done) { helpers.getAuthServer(wallet.copayers[1].id, function(server2) { var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); @@ -5976,8 +5469,6 @@ describe('Wallet service', function() { }); }); }); - - it('should allow other copayer to remove a TX signed, after 24hrs', function(done) { helpers.getAuthServer(wallet.copayers[1].id, function(server2) { var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); @@ -6123,24 +5614,24 @@ describe('Wallet service', function() { server._normalizeTxHistory = sinon.stub().returnsArg(0); var external = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7'; - helpers.stubUtxos(server, wallet, [100, 200], function(utxos) { - var outputs = [{ - toAddress: external, - amount: 50, - message: undefined // no message - }, { - toAddress: external, - amount: 30, - message: 'message #2' - }]; - var txOpts = helpers.createProposalOpts(Model.TxProposalLegacy.Types.MULTIPLEOUTPUTS, outputs, TestData.copayers[0].privKey_1H_0, { + helpers.stubUtxos(server, wallet, [1, 2], function(utxos) { + var txOpts = { + outputs: [{ + toAddress: external, + amount: 0.5e8, + message: undefined // no message + }, { + toAddress: external, + amount: 0.3e8, + message: 'message #2' + }], + feePerKb: 100e2, message: 'some message', customData: { "test": true - } - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); + }, + }; + helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) { should.exist(tx); var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); @@ -6166,13 +5657,13 @@ describe('Wallet service', function() { }], outputs: [{ address: changeAddresses[0].address, - amount: helpers.toSatoshi(20) - 5460, + amount: 0.2e8 - 5460, }, { address: external, - amount: helpers.toSatoshi(50) + amount: 0.5e8, }, { address: external, - amount: helpers.toSatoshi(30) + amount: 0.3e8, }] }]; helpers.stubHistory(txs); @@ -6183,20 +5674,19 @@ describe('Wallet service', function() { txs.length.should.equal(1); var tx = txs[0]; tx.action.should.equal('sent'); - tx.amount.should.equal(helpers.toSatoshi(80)); + tx.amount.should.equal(0.8e8); tx.message.should.equal('some message'); tx.addressTo.should.equal(external); tx.actions.length.should.equal(1); tx.actions[0].type.should.equal('accept'); tx.actions[0].copayerName.should.equal('copayer 1'); - tx.proposalType.should.equal(Model.TxProposalLegacy.Types.MULTIPLEOUTPUTS); tx.outputs[0].address.should.equal(external); - tx.outputs[0].amount.should.equal(helpers.toSatoshi(50)); + tx.outputs[0].amount.should.equal(0.5e8); should.not.exist(tx.outputs[0].message); should.not.exist(tx.outputs[0]['isMine']); should.not.exist(tx.outputs[0]['isChange']); tx.outputs[1].address.should.equal(external); - tx.outputs[1].amount.should.equal(helpers.toSatoshi(30)); + tx.outputs[1].amount.should.equal(0.3e8); should.exist(tx.outputs[1].message); tx.outputs[1].message.should.equal('message #2'); should.exist(tx.customData); @@ -6710,185 +6200,6 @@ describe('Wallet service', function() { }); }); - describe('Legacy', function() { - describe('Fees', function() { - var server, wallet; - beforeEach(function(done) { - helpers.createAndJoinWallet(2, 3, function(s, w) { - server = s; - wallet = w; - done(); - }); - }); - - it('should create a tx from legacy (bwc-0.0.*) client', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - - var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); - verifyStub.returns(true); - WalletService.getInstanceWithAuth({ - copayerId: wallet.copayers[0].id, - message: 'dummy', - signature: 'dummy', - clientVersion: 'bwc-0.0.40', - }, function(err, server) { - should.not.exist(err); - should.exist(server); - verifyStub.restore(); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.amount.should.equal(helpers.toSatoshi(80)); - tx.fee.should.equal(Defaults.DEFAULT_FEE_PER_KB); - done(); - }); - }); - }); - }); - - it('should not return error when fetching new txps from legacy (bwc-0.0.*) client', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - - var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); - verifyStub.returns(true); - WalletService.getInstanceWithAuth({ - copayerId: wallet.copayers[0].id, - message: 'dummy', - signature: 'dummy', - clientVersion: 'bwc-0.0.40', - }, function(err, server) { - verifyStub.restore(); - should.not.exist(err); - should.exist(server); - server.getPendingTxs({}, function(err, txps) { - should.not.exist(err); - should.exist(txps); - done(); - }); - }); - }); - }); - }); - it('should fail to sign tx from legacy (bwc-0.0.*) client', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - _.startsWith(tx.version, '1.').should.be.false; - - var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); - verifyStub.returns(true); - WalletService.getInstanceWithAuth({ - copayerId: wallet.copayers[0].id, - message: 'dummy', - signature: 'dummy', - clientVersion: 'bwc-0.0.40', - }, function(err, server) { - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); - server.signTx({ - txProposalId: tx.id, - signatures: signatures, - }, function(err) { - verifyStub.restore(); - should.exist(err); - err.code.should.equal('UPGRADE_NEEDED'); - err.message.should.contain('sign this spend proposal'); - done(); - }); - }); - }); - }); - }); - it('should create a tx from legacy (bwc-0.0.*) client and sign it from newer client', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - - var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); - verifyStub.returns(true); - WalletService.getInstanceWithAuth({ - copayerId: wallet.copayers[0].id, - message: 'dummy', - signature: 'dummy', - clientVersion: 'bwc-0.0.40', - }, function(err, server) { - should.not.exist(err); - should.exist(server); - verifyStub.restore(); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - should.exist(tx); - tx.amount.should.equal(helpers.toSatoshi(80)); - tx.fee.should.equal(Defaults.DEFAULT_FEE_PER_KB); - helpers.getAuthServer(wallet.copayers[0].id, function(server) { - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); - server.signTx({ - txProposalId: tx.id, - signatures: signatures, - }, function(err) { - should.not.exist(err); - done(); - }); - }); - }); - }); - }); - }); - it('should fail with insufficient fee when invoked from legacy (bwc-0.0.*) client', function(done) { - helpers.stubUtxos(server, wallet, 1, function() { - var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); - verifyStub.returns(true); - WalletService.getInstanceWithAuth({ - copayerId: wallet.copayers[0].id, - message: 'dummy', - signature: 'dummy', - clientVersion: 'bwc-0.0.40', - }, function(err, server) { - should.not.exist(err); - should.exist(server); - verifyStub.restore(); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.99995, TestData.copayers[0].privKey_1H_0); - - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE'); - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.99995, TestData.copayers[0].privKey_1H_0, { - feePerKb: 5000 - }); - server.createTxLegacy(txOpts, function(err, tx) { - should.not.exist(err); - tx.fee.should.equal(5000); - - // Sign it to make sure Bitcore doesn't complain about the fees - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); - server.signTx({ - txProposalId: tx.id, - signatures: signatures, - }, function(err) { - should.not.exist(err); - done(); - }); - }); - }); - }); - }); - }); - }); - }); - describe('PayPro', function() { var server, wallet; @@ -6901,13 +6212,18 @@ describe('Wallet service', function() { }); it('should create a paypro tx', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { + helpers.stubUtxos(server, wallet, [1, 2], function() { + var txOpts = { + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: 0.8e8 + }], + feePerKb: 100e2, message: 'some message', customData: 'some custom data', payProUrl: 'http:/fakeurl.com', - }); - server.createTxLegacy(txOpts, function(err, tx) { + }; + server.createTx(txOpts, function(err, tx) { should.not.exist(err); should.exist(tx); tx.payProUrl.should.equal('http:/fakeurl.com'); @@ -6915,22 +6231,6 @@ describe('Wallet service', function() { }); }); }); - it('should fail to create a paypro tx for a P2PKH wallet from an old client (bwc < 1.2.0)', function(done) { - helpers.stubUtxos(server, wallet, [100, 200], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, TestData.copayers[0].privKey_1H_0, { - message: 'some message', - customData: 'some custom data', - payProUrl: 'http:/fakeurl.com', - }); - server._setClientVersion('bwc-1.1.99'); - server.createTxLegacy(txOpts, function(err, tx) { - should.exist(err); - should.not.exist(tx); - err.code.should.equal('UPGRADE_NEEDED'); - done(); - }); - }); - }); }); describe('Push notifications', function() { diff --git a/test/model/txproposal_legacy.js b/test/model/txproposal_legacy.js deleted file mode 100644 index 2f1eda7..0000000 --- a/test/model/txproposal_legacy.js +++ /dev/null @@ -1,237 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var chai = require('chai'); -var sinon = require('sinon'); -var should = chai.should(); -var TxProposal = require('../../lib/model/txproposal_legacy'); -var Bitcore = require('bitcore-lib'); - -describe('TXProposal legacy', function() { - - describe('#create', function() { - it('should create a TxProposal', function() { - var txp = TxProposal.create(aTxpOpts()); - should.exist(txp); - should.exist(txp.toAddress); - should.not.exist(txp.outputs); - }); - it('should create a multiple-outputs TxProposal', function() { - var txp = TxProposal.create(aTxpOpts(TxProposal.Types.MULTIPLEOUTPUTS)); - should.exist(txp); - should.not.exist(txp.toAddress); - should.exist(txp.outputs); - }); - it('should create an external TxProposal', function() { - var txp = TxProposal.create(aTxpOpts(TxProposal.Types.EXTERNAL)); - should.exist(txp); - should.not.exist(txp.toAddress); - should.exist(txp.outputs); - should.exist(txp.inputs); - }); - }); - - describe('#fromObj', function() { - it('should copy a TxProposal', function() { - var txp = TxProposal.fromObj(aTXP()); - should.exist(txp); - txp.toAddress.should.equal(aTXP().toAddress); - }); - it('should copy a multiple-outputs TxProposal', function() { - var txp = TxProposal.fromObj(aTXP(TxProposal.Types.MULTIPLEOUTPUTS)); - should.exist(txp); - txp.outputs.should.deep.equal(aTXP(TxProposal.Types.MULTIPLEOUTPUTS).outputs); - }); - }); - - describe('#getBitcoreTx', function() { - it('should create a valid bitcore TX', function() { - var txp = TxProposal.fromObj(aTXP()); - var t = txp.getBitcoreTx(); - should.exist(t); - }); - it('should order outputs as specified by outputOrder', function() { - var txp = TxProposal.fromObj(aTXP()); - - txp.outputOrder = [0, 1]; - var t = txp.getBitcoreTx(); - t.getChangeOutput().should.deep.equal(t.outputs[1]); - - txp.outputOrder = [1, 0]; - var t = txp.getBitcoreTx(); - t.getChangeOutput().should.deep.equal(t.outputs[0]); - }); - it('should create a bitcore TX with multiple outputs', function() { - var txp = TxProposal.fromObj(aTXP(TxProposal.Types.MULTIPLEOUTPUTS)); - txp.outputOrder = [0, 1, 2]; - var t = txp.getBitcoreTx(); - t.getChangeOutput().should.deep.equal(t.outputs[2]); - }); - }); - - describe('#getTotalAmount', function() { - it('should be compatible with simple proposal legacy amount', function() { - var x = TxProposal.fromObj(aTXP()); - var total = x.getTotalAmount(); - total.should.equal(x.amount); - }); - it('should handle multiple-outputs', function() { - var x = TxProposal.fromObj(aTXP(TxProposal.Types.MULTIPLEOUTPUTS)); - var totalOutput = 0; - _.each(x.outputs, function(o) { - totalOutput += o.amount - }); - x.getTotalAmount().should.equal(totalOutput); - }); - it('should handle external', function() { - var x = TxProposal.fromObj(aTXP(TxProposal.Types.EXTERNAL)); - var totalOutput = 0; - _.each(x.outputs, function(o) { - totalOutput += o.amount - }); - x.getTotalAmount().should.equal(totalOutput); - }); - - }); - - describe('#sign', function() { - it('should sign 2-2', function() { - var txp = TxProposal.fromObj(aTXP()); - txp.sign('1', theSignatures, theXPub); - txp.isAccepted().should.equal(false); - txp.isRejected().should.equal(false); - txp.sign('2', theSignatures, theXPub); - txp.isAccepted().should.equal(true); - txp.isRejected().should.equal(false); - }); - }); - - describe('#getRawTx', function() { - it('should generate correct raw transaction for signed 2-2', function() { - var txp = TxProposal.fromObj(aTXP()); - txp.sign('1', theSignatures, theXPub); - txp.getRawTx().should.equal('0100000001ab069f7073be9b491bb1ad4233a45d2e383082ccc7206df905662d6d8499e66e080000009200483045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9014752210319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f2103b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d10130152aeffffffff0280f0fa02000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac70c9fa020000000017a914778192003f0e9e1d865c082179cc3dae5464b03d8700000000'); - }); - }); - - - - describe('#reject', function() { - it('should reject 2-2', function() { - var txp = TxProposal.fromObj(aTXP()); - txp.reject('1'); - txp.isAccepted().should.equal(false); - txp.isRejected().should.equal(true); - }); - }); - - - describe('#reject & #sign', function() { - it('should finally reject', function() { - var txp = TxProposal.fromObj(aTXP()); - txp.sign('1', theSignatures); - txp.isAccepted().should.equal(false); - txp.isRejected().should.equal(false); - txp.reject('2'); - txp.isAccepted().should.equal(false); - txp.isRejected().should.equal(true); - }); - }); - -}); - -var theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e'; -var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9'; -var theSignatures = ['3045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9']; - -var aTxpOpts = function(type) { - var opts = { - type: type, - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: 50000000, - message: 'some message' - }; - if (type == TxProposal.Types.MULTIPLEOUTPUTS || type == TxProposal.Types.EXTERNAL) { - opts.outputs = [{ - toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", - amount: 10000000, - message: "first message" - }, { - toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", - amount: 20000000, - message: "second message" - }, ]; - delete opts.toAddress; - delete opts.amount; - } - if (type == TxProposal.Types.EXTERNAL) { - opts.inputs = [{ - "txid": "6ee699846d2d6605f96d20c7cc8230382e5da43342adb11b499bbe73709f06ab", - "vout": 8, - "satoshis": 100000000, - "scriptPubKey": "a914a8a9648754fbda1b6c208ac9d4e252075447f36887", - "address": "3H4pNP6J4PW4NnvdrTg37VvZ7h2QWuAwtA", - "path": "m/2147483647/0/1", - "publicKeys": ["0319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f", "03b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d101301"] - }]; - } - return opts; -}; - -var aTXP = function(type) { - var txp = { - "version": '2.0.0', - "type": type, - "createdOn": 1423146231, - "id": "75c34f49-1ed6-255f-e9fd-0c71ae75ed1e", - "walletId": "1", - "creatorId": "1", - "toAddress": "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", - "network": "livenet", - "amount": 50000000, - "message": 'some message', - "proposalSignature": '7035022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9', - "changeAddress": { - "version": '1.0.0', - "createdOn": 1424372337, - "address": '3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH', - "path": 'm/2147483647/1/0', - "publicKeys": ['030562cb099e6043dc499eb359dd97c9d500a3586498e4bcf0228a178cc20e6f16', - '0367027d17dbdfc27b5e31f8ed70e14d47949f0fa392261e977db0851c8b0d6fac', - '0315ae1e8aa866794ae603389fb2b8549153ebf04e7cdf74501dadde5c75ddad11' - ] - }, - "inputs": [{ - "txid": "6ee699846d2d6605f96d20c7cc8230382e5da43342adb11b499bbe73709f06ab", - "vout": 8, - "satoshis": 100000000, - "scriptPubKey": "a914a8a9648754fbda1b6c208ac9d4e252075447f36887", - "address": "3H4pNP6J4PW4NnvdrTg37VvZ7h2QWuAwtA", - "path": "m/2147483647/0/1", - "publicKeys": ["0319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f", "03b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d101301"] - }], - "inputPaths": ["m/2147483647/0/1"], - "requiredSignatures": 2, - "requiredRejections": 1, - "walletN": 2, - "status": "pending", - "actions": [], - "outputOrder": [0, 1], - "fee": 10000, - }; - if (type == TxProposal.Types.MULTIPLEOUTPUTS) { - txp.outputs = [{ - toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", - amount: 10000000, - message: "first message" - }, { - toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7", - amount: 20000000, - message: "second message" - }, ]; - txp.outputOrder = [0, 1, 2]; - delete txp.toAddress; - delete txp.amount; - } - return txp; -}; diff --git a/test/storage.js b/test/storage.js index 6b336a7..7d0250b 100644 --- a/test/storage.js +++ b/test/storage.js @@ -142,13 +142,19 @@ describe('Storage', function() { should.not.exist(err); proposals = _.map(_.range(4), function(i) { - var tx = Model.TxProposalLegacy.create({ + var tx = Model.TxProposal.create({ walletId: '123', - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + outputs: [{ + toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', + amount: i + 100, + }], + feePerKb: 100e2, creatorId: wallet.copayers[0].id, - amount: i + 100, }); if (i % 2 == 0) { + tx.status = 'pending'; + tx.isPending().should.be.true; + } else { tx.status = 'rejected'; tx.isPending().should.be.false; } @@ -190,8 +196,8 @@ describe('Storage', function() { should.exist(txs); txs.length.should.equal(2); txs = _.sortBy(txs, 'amount'); - txs[0].amount.should.equal(101); - txs[1].amount.should.equal(103); + txs[0].amount.should.equal(100); + txs[1].amount.should.equal(102); done(); }); });