diff --git a/lib/common/constants.js b/lib/common/constants.js new file mode 100644 index 0000000..ef15049 --- /dev/null +++ b/lib/common/constants.js @@ -0,0 +1,22 @@ +'use strict'; + +var Constants = {}; + +Constants.SCRIPT_TYPES = { + P2SH: 'P2SH', + P2PKH: 'P2PKH', +}; +Constants.DERIVATION_STRATEGIES = { + BIP44: 'BIP44', + BIP45: 'BIP45', +}; + +Constants.PATHS = { + REQUEST_KEY: "m/1'/0", + TXPROPOSAL_KEY: "m/1'/1", + REQUEST_KEY_AUTH: "m/2", // relative to BASE +}; + +Constants.BIP45_SHARED_INDEX = 0x80000000 - 1; + +module.exports = Constants; diff --git a/lib/common/defaults.js b/lib/common/defaults.js new file mode 100644 index 0000000..cc8a4ba --- /dev/null +++ b/lib/common/defaults.js @@ -0,0 +1,40 @@ +'use strict'; + +var Defaults = {}; + +Defaults.DEFAULT_FEE_PER_KB = 10000; +Defaults.MIN_FEE_PER_KB = 0; +Defaults.MAX_FEE_PER_KB = 1000000; +Defaults.MAX_TX_FEE = 1 * 1e8; + +Defaults.MAX_KEYS = 100; + +// Time after which a Tx proposal can be erased by any copayer. in seconds +Defaults.DELETE_LOCKTIME = 24 * 3600; + +// Allowed consecutive txp rejections before backoff is applied. +Defaults.BACKOFF_OFFSET = 3; + +// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in Minutes. +Defaults.BACKOFF_TIME = 2; + +Defaults.MAX_MAIN_ADDRESS_GAP = 20; + +// TODO: should allow different gap sizes for external/internal chains +Defaults.SCAN_ADDRESS_GAP = Defaults.MAX_MAIN_ADDRESS_GAP; + +Defaults.FEE_LEVELS = [{ + name: 'priority', + nbBlocks: 1, + defaultValue: 50000 +}, { + name: 'normal', + nbBlocks: 2, + defaultValue: 20000 +}, { + name: 'economy', + nbBlocks: 6, + defaultValue: 10000 +}]; + +module.exports = Defaults; diff --git a/lib/common/index.js b/lib/common/index.js new file mode 100644 index 0000000..ccb2c53 --- /dev/null +++ b/lib/common/index.js @@ -0,0 +1,7 @@ +var Common = {}; + +Common.Constants = require('./constants'); +Common.Defaults = require('./defaults'); +Common.Utils = require('./utils'); + +module.exports = Common; diff --git a/lib/common/utils.js b/lib/common/utils.js new file mode 100644 index 0000000..01f1420 --- /dev/null +++ b/lib/common/utils.js @@ -0,0 +1,97 @@ +var $ = require('preconditions').singleton(); +var _ = require('lodash'); + +var Bitcore = require('bitcore-lib'); +var crypto = Bitcore.crypto; +var encoding = Bitcore.encoding; + +var Utils = {}; + +Utils.checkRequired = function(obj, args) { + args = [].concat(args); + if (!_.isObject(obj)) return false; + for (var i = 0; i < args.length; i++) { + if (!obj.hasOwnProperty(args[i])) return false; + } + return true; +}; + +/** + * + * @desc rounds a JAvascript number + * @param number + * @return {number} + */ +Utils.strip = function(number) { + return (parseFloat(number.toPrecision(12))); +} + +/* TODO: It would be nice to be compatible with bitcoind signmessage. How + * the hash is calculated there? */ +Utils.hashMessage = function(text) { + $.checkArgument(text); + var buf = new Buffer(text); + var ret = crypto.Hash.sha256sha256(buf); + ret = new Bitcore.encoding.BufferReader(ret).readReverse(); + return ret; +}; + +Utils.verifyMessage = function(text, signature, pubKey) { + $.checkArgument(text); + $.checkArgument(pubKey); + + if (!signature) + return false; + + var pub = new Bitcore.PublicKey(pubKey); + var hash = Utils.hashMessage(text); + + try { + var sig = new crypto.Signature.fromString(signature); + return crypto.ECDSA.verify(hash, sig, pub, 'little'); + } catch (e) { + return false; + } +}; + +Utils.formatAmount = function(satoshis, unit, opts) { + var UNITS = { + btc: { + toSatoshis: 100000000, + maxDecimals: 6, + minDecimals: 2, + }, + bit: { + toSatoshis: 100, + maxDecimals: 0, + minDecimals: 0, + }, + }; + + $.shouldBeNumber(satoshis); + $.checkArgument(_.contains(_.keys(UNITS), unit)); + + function addSeparators(nStr, thousands, decimal, minDecimals) { + nStr = nStr.replace('.', decimal); + var x = nStr.split(decimal); + var x0 = x[0]; + var x1 = x[1]; + + x1 = _.dropRightWhile(x1, function(n, i) { + return n == '0' && i >= minDecimals; + }).join(''); + var x2 = x.length > 1 ? decimal + x1 : ''; + + x0 = x0.replace(/\B(?=(\d{3})+(?!\d))/g, thousands); + return x0 + x2; + } + + opts = opts || {}; + + var u = UNITS[unit]; + var amount = (satoshis / u.toSatoshis).toFixed(u.maxDecimals); + return addSeparators(amount, opts.thousandsSeparator || ',', opts.decimalSeparator || '.', u.minDecimals); +}; + + +module.exports = Utils; diff --git a/lib/emailservice.js b/lib/emailservice.js index 8181f4e..bb4c5b0 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -10,7 +10,7 @@ var fs = require('fs'); var path = require('path'); var nodemailer = require('nodemailer'); -var WalletUtils = require('bitcore-wallet-utils'); +var Utils = require('./common/utils'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); var Lock = require('./lock'); @@ -227,7 +227,7 @@ EmailService.prototype._getDataForTemplate = function(notification, recipient, c if (data.amount) { try { var unit = recipient.unit.toLowerCase(); - data.amount = WalletUtils.formatAmount(+data.amount, unit) + ' ' + UNIT_LABELS[unit]; + data.amount = Utils.formatAmount(+data.amount, unit) + ' ' + UNIT_LABELS[unit]; } catch (ex) { return cb(new Error('Could not format amount', ex)); } diff --git a/lib/model/address.js b/lib/model/address.js index c6af424..9b024ab 100644 --- a/lib/model/address.js +++ b/lib/model/address.js @@ -1,7 +1,10 @@ 'use strict'; -var WalletUtils = require('bitcore-wallet-utils'); -var Bitcore = WalletUtils.Bitcore; +var $ = require('preconditions').singleton(); +var _ = require('lodash'); + +var Bitcore = require('bitcore-lib'); +var Constants = require('../common/constants'); function Address() {}; @@ -18,7 +21,7 @@ Address.create = function(opts) { x.path = opts.path; x.publicKeys = opts.publicKeys; x.network = Bitcore.Address(x.address).toObject().network; - x.type = opts.type || WalletUtils.SCRIPT_TYPES.P2SH; + x.type = opts.type || Constants.SCRIPT_TYPES.P2SH; x.hasActivity = undefined; return x; }; @@ -34,9 +37,45 @@ Address.fromObj = function(obj) { x.isChange = obj.isChange; x.path = obj.path; x.publicKeys = obj.publicKeys; - x.type = obj.type || WalletUtils.SCRIPT_TYPES.P2SH; + x.type = obj.type || Constants.SCRIPT_TYPES.P2SH; x.hasActivity = obj.hasActivity; return x; }; +Address._deriveAddress = function(scriptType, publicKeyRing, path, m, network) { + $.checkArgument(_.contains(_.values(Constants.SCRIPT_TYPES), scriptType)); + + var publicKeys = _.map(publicKeyRing, function(item) { + var xpub = new Bitcore.HDPublicKey(item.xPubKey); + return xpub.derive(path).publicKey; + }); + + var bitcoreAddress; + switch (scriptType) { + case Constants.SCRIPT_TYPES.P2SH: + bitcoreAddress = Bitcore.Address.createMultisig(publicKeys, m, network); + break; + case Constants.SCRIPT_TYPES.P2PKH: + $.checkState(_.isArray(publicKeys) && publicKeys.length == 1); + bitcoreAddress = Bitcore.Address.fromPublicKey(publicKeys[0], network); + break; + } + + return { + address: bitcoreAddress.toString(), + path: path, + publicKeys: _.invoke(publicKeys, 'toString'), + }; +}; + +Address.derive = function(walletId, scriptType, publicKeyRing, path, m, network, isChange) { + var raw = Address._deriveAddress(scriptType, publicKeyRing, path, m, network); + return Address.create(_.extend(raw, { + walletId: walletId, + type: scriptType, + isChange: isChange, + })); +}; + + module.exports = Address; diff --git a/lib/model/addressmanager.js b/lib/model/addressmanager.js index 78bcc61..d164fbc 100644 --- a/lib/model/addressmanager.js +++ b/lib/model/addressmanager.js @@ -1,9 +1,8 @@ var _ = require('lodash'); var $ = require('preconditions').singleton(); -var WalletUtils = require('bitcore-wallet-utils'); - -var BIP45_SHARED_INDEX = 0x80000000 - 1; +var Bitcore = require('bitcore-lib'); +var Constants = require('../common/constants'); function AddressManager() {}; @@ -13,12 +12,12 @@ AddressManager.create = function(opts) { var x = new AddressManager(); x.version = 2; - x.derivationStrategy = opts.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; - $.checkState(_.contains(_.values(WalletUtils.DERIVATION_STRATEGIES), x.derivationStrategy)); + x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; + $.checkState(_.contains(_.values(Constants.DERIVATION_STRATEGIES), x.derivationStrategy)); x.receiveAddressIndex = 0; x.changeAddressIndex = 0; - x.copayerIndex = _.isNumber(opts.copayerIndex) ? opts.copayerIndex : BIP45_SHARED_INDEX; + x.copayerIndex = _.isNumber(opts.copayerIndex) ? opts.copayerIndex : Constants.BIP45_SHARED_INDEX; return x; }; @@ -27,7 +26,7 @@ AddressManager.fromObj = function(obj) { var x = new AddressManager(); x.version = obj.version; - x.derivationStrategy = obj.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; + x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; x.receiveAddressIndex = obj.receiveAddressIndex; x.changeAddressIndex = obj.changeAddressIndex; x.copayerIndex = obj.copayerIndex; @@ -36,7 +35,7 @@ AddressManager.fromObj = function(obj) { }; AddressManager.supportsCopayerBranches = function(derivationStrategy) { - return derivationStrategy == WalletUtils.DERIVATION_STRATEGIES.BIP45; + return derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45; }; AddressManager.prototype._incrementIndex = function(isChange) { @@ -58,7 +57,7 @@ AddressManager.prototype.rewindIndex = function(isChange, n) { AddressManager.prototype.getCurrentAddressPath = function(isChange) { return 'm/' + - (this.derivationStrategy == WalletUtils.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') + + (this.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') + (isChange ? 1 : 0) + '/' + (isChange ? this.changeAddressIndex : this.receiveAddressIndex); }; diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 4fdbb45..433bb57 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -3,17 +3,22 @@ var $ = require('preconditions').singleton(); var _ = require('lodash'); var util = require('util'); - var Uuid = require('uuid'); +var sjcl = require('sjcl'); var Address = require('./address'); var AddressManager = require('./addressmanager'); -var WalletUtils = require('bitcore-wallet-utils'); -var Bitcore = WalletUtils.Bitcore; -var HDPublicKey = Bitcore.HDPublicKey; +var Bitcore = require('bitcore-lib'); + +var Constants = require('../common/constants'); function Copayer() {}; +Copayer._xPubToCopayerId = function(xpub) { + var hash = sjcl.hash.sha256.hash(xpub); + return sjcl.codec.hex.fromBits(hash); +}; + Copayer.create = function(opts) { opts = opts || {}; $.checkArgument(opts.xPubKey, 'Missing copayer extended public key') @@ -27,7 +32,7 @@ Copayer.create = function(opts) { x.version = 2; x.createdOn = Math.floor(Date.now() / 1000); x.xPubKey = opts.xPubKey; - x.id = WalletUtils.xPubToCopayerId(x.xPubKey); + x.id = Copayer._xPubToCopayerId(x.xPubKey); x.name = opts.name; x.requestPubKey = opts.requestPubKey; x.signature = opts.signature; @@ -36,7 +41,7 @@ Copayer.create = function(opts) { signature: opts.signature, }]; - var derivationStrategy = opts.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; + var derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; if (AddressManager.supportsCopayerBranches(derivationStrategy)) { x.addressManager = AddressManager.create({ derivationStrategy: derivationStrategy, @@ -82,13 +87,7 @@ Copayer.prototype.createAddress = function(wallet, isChange) { $.checkState(wallet.isComplete()); var path = this.addressManager.getNewAddressPath(isChange); - var raw = Address.create(WalletUtils.deriveAddress(wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.network)); - var address = Address.create(_.extend(raw, { - walletId: wallet.id, - type: wallet.addressType, - })); - - address.isChange = isChange; + var address = Address.derive(wallet.id, wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.network, isChange); return address; }; diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index b05d466..f698292 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -7,9 +7,11 @@ var log = require('npmlog'); log.debug = log.verbose; log.disableColor(); -var WalletUtils = require('bitcore-wallet-utils'); -var Bitcore = WalletUtils.Bitcore; -var Address = Bitcore.Address; +var Bitcore = require('bitcore-lib'); + +var Common = require('../common'); +var Constants = Common.Constants; +var Defaults = Common.Defaults; var TxProposalAction = require('./txproposalaction'); @@ -76,8 +78,7 @@ TxProposal.create = function(opts) { x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos; x.proposalSignaturePubKey = opts.proposalSignaturePubKey; x.proposalSignaturePubKeySig = opts.proposalSignaturePubKeySig; - x.derivationStrategy = opts.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; - x.addressType = opts.addressType || WalletUtils.SCRIPT_TYPES.P2SH; + x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH; x.customData = opts.customData; if (_.isFunction(TxProposal._create[x.type])) { @@ -125,8 +126,7 @@ TxProposal.fromObj = function(obj) { x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos; x.proposalSignaturePubKey = obj.proposalSignaturePubKey; x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig; - x.derivationStrategy = obj.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; - x.addressType = obj.addressType || WalletUtils.SCRIPT_TYPES.P2SH; + x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH; x.customData = obj.customData; return x; @@ -147,6 +147,72 @@ TxProposal.prototype._updateStatus = function() { } }; +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) { + $.checkState(t.outputs.length == self.outputOrder.length); + t.sortOutputs(function(outputs) { + return _.map(self.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, { @@ -164,7 +230,7 @@ TxProposal.prototype._getCurrentSignatures = function() { TxProposal.prototype.getBitcoreTx = function() { var self = this; - var t = WalletUtils.buildTx(this); + var t = this._buildTx(); var sigs = this._getCurrentSignatures(); _.each(sigs, function(x) { diff --git a/lib/model/wallet.js b/lib/model/wallet.js index a85ae68..afe2a15 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -8,7 +8,8 @@ var Uuid = require('uuid'); var Address = require('./address'); var Copayer = require('./copayer'); var AddressManager = require('./addressmanager'); -var WalletUtils = require('bitcore-wallet-utils'); + +var Constants = require('../common/constants'); function Wallet() {}; @@ -29,8 +30,8 @@ Wallet.create = function(opts) { x.copayers = []; x.pubKey = opts.pubKey; x.network = opts.network; - x.derivationStrategy = opts.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; - x.addressType = opts.addressType || WalletUtils.SCRIPT_TYPES.P2SH; + x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; + x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH; x.addressManager = AddressManager.create({ derivationStrategy: x.derivationStrategy, @@ -56,8 +57,8 @@ Wallet.fromObj = function(obj) { }); x.pubKey = obj.pubKey; x.network = obj.network; - x.derivationStrategy = obj.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; - x.addressType = obj.addressType || WalletUtils.SCRIPT_TYPES.P2SH; + x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45; + x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH; x.addressManager = AddressManager.fromObj(obj.addressManager); x.scanStatus = obj.scanStatus; @@ -153,12 +154,7 @@ Wallet.prototype.createAddress = function(isChange) { var self = this; var path = this.addressManager.getNewAddressPath(isChange); - var raw = WalletUtils.deriveAddress(this.addressType, this.publicKeyRing, path, this.m, this.network); - var address = Address.create(_.extend(raw, { - walletId: self.id, - type: self.addressType, - })); - address.isChange = isChange; + var address = Address.derive(self.id, this.addressType, this.publicKeyRing, path, this.m, this.network, isChange); return address; }; diff --git a/lib/server.js b/lib/server.js index e9805f4..d56518e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -6,17 +6,18 @@ var log = require('npmlog'); log.debug = log.verbose; log.disableColor(); var EmailValidator = require('email-validator'); +var Stringify = require('json-stable-stringify'); -var WalletUtils = require('bitcore-wallet-utils'); -var Bitcore = WalletUtils.Bitcore; -var PublicKey = Bitcore.PublicKey; -var HDPublicKey = Bitcore.HDPublicKey; -var Address = Bitcore.Address; +var Bitcore = require('bitcore-lib'); + +var Common = require('./common'); +var Utils = Common.Utils; +var Constants = Common.Constants; +var Defaults = Common.Defaults; var ClientError = require('./errors/clienterror'); var Errors = require('./errors/errordefinitions'); -var Utils = require('./utils'); var Lock = require('./lock'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); @@ -52,39 +53,6 @@ function WalletService() { }; -WalletService.MAX_KEYS = 100; - -// Time after which a Tx proposal can be erased by any copayer. in seconds -WalletService.DELETE_LOCKTIME = 24 * 3600; - -// Allowed consecutive txp rejections before backoff is applied. -WalletService.BACKOFF_OFFSET = 3; - -// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in Minutes. -WalletService.BACKOFF_TIME = 2; - -WalletService.MAX_MAIN_ADDRESS_GAP = 20; - -// Fund scanning parameters -WalletService.SCAN_CONFIG = { - maxGap: WalletService.MAX_MAIN_ADDRESS_GAP, -}; - -WalletService.FEE_LEVELS = [{ - name: 'priority', - nbBlocks: 1, - defaultValue: 50000 -}, { - name: 'normal', - nbBlocks: 2, - defaultValue: 20000 -}, { - name: 'economy', - nbBlocks: 6, - defaultValue: 10000 -}]; - - /** * Gets the current version of BWS */ @@ -234,11 +202,11 @@ WalletService.prototype.createWallet = function(opts, cb) { opts.supportBIP44AndP2PKH = _.isBoolean(opts.supportBIP44AndP2PKH) ? opts.supportBIP44AndP2PKH : true; - var derivationStrategy = opts.supportBIP44AndP2PKH ? WalletUtils.DERIVATION_STRATEGIES.BIP44 : WalletUtils.DERIVATION_STRATEGIES.BIP45; - var addressType = (opts.n == 1 && opts.supportBIP44AndP2PKH) ? WalletUtils.SCRIPT_TYPES.P2PKH : WalletUtils.SCRIPT_TYPES.P2SH; + var derivationStrategy = opts.supportBIP44AndP2PKH ? Constants.DERIVATION_STRATEGIES.BIP44 : Constants.DERIVATION_STRATEGIES.BIP45; + var addressType = (opts.n == 1 && opts.supportBIP44AndP2PKH) ? Constants.SCRIPT_TYPES.P2PKH : Constants.SCRIPT_TYPES.P2SH; try { - pubKey = new PublicKey.fromString(opts.pubKey); + pubKey = new Bitcore.PublicKey.fromString(opts.pubKey); } catch (ex) { return cb(new ClientError('Invalid public key')); }; @@ -354,6 +322,7 @@ WalletService.prototype.getStatus = function(opts, cb) { }); }; + /* * Verifies a signature * @param text @@ -361,10 +330,21 @@ WalletService.prototype.getStatus = function(opts, cb) { * @param pubKeys */ WalletService.prototype._verifySignature = function(text, signature, pubkey) { - return WalletUtils.verifyMessage(text, signature, pubkey); + return Utils.verifyMessage(text, signature, pubkey); }; +/* + * Verifies a request public key + * @param requestPubKey + * @param signature + * @param xPubKey + */ +WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) { + var pub = (new Bitcore.HDPublicKey(xPubKey)).derive(Constants.PATHS.REQUEST_KEY_AUTH).publicKey; + return Utils.verifyMessage(requestPubKey, signature, pub.toString()); +}; + /* * Verifies signature againt a collection of pubkeys * @param text @@ -517,11 +497,11 @@ WalletService.prototype.addAccess = function(opts, cb) { id: opts.copayerId }).xPubKey; - if (!WalletUtils.verifyRequestPubKey(opts.requestPubKey, opts.signature, xPubKey)) { + if (!self._verifyRequestPubKey(opts.requestPubKey, opts.signature, xPubKey)) { return cb(Errors.NOT_AUTHORIZED); } - if (copayer.requestPubKeys.length > WalletService.MAX_KEYS) + if (copayer.requestPubKeys.length > Defaults.MAX_KEYS) return cb(Errors.TOO_MANY_KEYS); self._addKeyToCopayer(wallet, copayer, opts, cb); @@ -563,6 +543,10 @@ WalletService.prototype._clientSupportsTXPv2 = function() { return true; }; +WalletService._getCopayerHash = function(name, xPubKey, requestPubKey) { + return [name, xPubKey, requestPubKey].join('|'); +}; + /** * Joins a wallet in creation. * @param {Object} opts @@ -593,17 +577,17 @@ WalletService.prototype.joinWallet = function(opts, cb) { if (opts.supportBIP44AndP2PKH) { // New client trying to join legacy wallet - if (wallet.derivationStrategy == WalletUtils.DERIVATION_STRATEGIES.BIP45) { + if (wallet.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45) { return cb(new ClientError('The wallet you are trying to join was created with an older version of the client app.')); } } else { // Legacy client trying to join new wallet - if (wallet.derivationStrategy == WalletUtils.DERIVATION_STRATEGIES.BIP44) { + if (wallet.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP44) { return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To join this wallet you need to upgrade your client app.')); } } - var hash = WalletUtils.getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); + var hash = WalletService._getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); if (!self._verifySignature(hash, opts.copayerSignature, wallet.pubKey)) { return cb(new ClientError()); } @@ -701,8 +685,8 @@ WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) { if (err) return cb(err); var latestAddresses = _.takeRight(_.reject(addresses, { isChange: true - }), WalletService.MAX_MAIN_ADDRESS_GAP); - if (latestAddresses.length < WalletService.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, { + }), Defaults.MAX_MAIN_ADDRESS_GAP); + if (latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, { hasActivity: true })) return cb(null, true); @@ -1021,7 +1005,7 @@ WalletService.prototype.getFeeLevels = function(opts, cb) { if (network != 'livenet' && network != 'testnet') return cb(new ClientError('Invalid network')); - var levels = WalletService.FEE_LEVELS; + var levels = Defaults.FEE_LEVELS; var samplePoints = _.uniq(_.pluck(levels, 'nbBlocks')); self._sampleFeeLevels(network, samplePoints, function(err, feeSamples) { var values = _.map(levels, function(level) { @@ -1144,7 +1128,7 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) { WalletService.prototype._canCreateTx = function(copayerId, cb) { var self = this; - self.storage.fetchLastTxs(self.walletId, copayerId, 5 + WalletService.BACKOFF_OFFSET, function(err, txs) { + self.storage.fetchLastTxs(self.walletId, copayerId, 5 + Defaults.BACKOFF_OFFSET, function(err, txs) { if (err) return cb(err); if (!txs.length) @@ -1154,7 +1138,7 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { status: 'rejected' }); - var exceededRejections = lastRejections.length - WalletService.BACKOFF_OFFSET; + var exceededRejections = lastRejections.length - Defaults.BACKOFF_OFFSET; if (exceededRejections <= 0) return cb(null, true); @@ -1162,7 +1146,7 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { var lastTxTs = txs[0].createdOn; var now = Math.floor(Date.now() / 1000); var timeSinceLastRejection = now - lastTxTs; - var backoffTime = 60 * Math.pow(WalletService.BACKOFF_TIME, exceededRejections); + var backoffTime = 60 * Math.pow(Defaults.BACKOFF_TIME, exceededRejections); if (timeSinceLastRejection <= backoffTime) log.debug('Not allowing to create TX: timeSinceLastRejection/backoffTime', timeSinceLastRejection, backoffTime); @@ -1172,6 +1156,19 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { }; +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 @@ -1212,8 +1209,8 @@ WalletService.prototype.createTx = function(opts, cb) { valid: false })) return; - var feePerKb = opts.feePerKb || WalletUtils.DEFAULT_FEE_PER_KB; - if (feePerKb < WalletUtils.MIN_FEE_PER_KB || feePerKb > WalletUtils.MAX_FEE_PER_KB) + 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) { @@ -1224,7 +1221,7 @@ WalletService.prototype.createTx = function(opts, cb) { var copayer = wallet.getCopayer(self.copayerId); var hash; if (!opts.type || opts.type == Model.TxProposal.Types.SIMPLE) { - hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); + hash = WalletService._getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); } else { // should match bwc api _computeProposalSignature var header = { @@ -1234,7 +1231,7 @@ WalletService.prototype.createTx = function(opts, cb) { message: opts.message, payProUrl: opts.payProUrl }; - hash = WalletUtils.getProposalHash(header) + hash = WalletService._getProposalHash(header) } var signingKey = self._getSigningKey(hash, opts.proposalSignature, copayer.requestPubKeys) @@ -1288,7 +1285,6 @@ WalletService.prototype.createTx = function(opts, cb) { requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), walletN: wallet.n, excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, - derivationStrategy: wallet.derivationStrategy, addressType: wallet.addressType, customData: opts.customData, }; @@ -1363,7 +1359,7 @@ WalletService.prototype.removeWallet = function(opts, cb) { WalletService.prototype.getRemainingDeleteLockTime = function(txp) { var now = Math.floor(Date.now() / 1000); - var lockTimeRemaining = txp.createdOn + WalletService.DELETE_LOCKTIME - now; + var lockTimeRemaining = txp.createdOn + Defaults.DELETE_LOCKTIME - now; if (lockTimeRemaining < 0) return 0; @@ -1943,7 +1939,7 @@ WalletService.prototype.scan = function(opts, cb) { function scanBranch(derivator, cb) { var inactiveCounter = 0; var allAddresses = []; - var gap = WalletService.SCAN_CONFIG.maxGap; + var gap = Defaults.SCAN_ADDRESS_GAP; async.whilst(function() { return inactiveCounter < gap; diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index aadc018..0000000 --- a/lib/utils.js +++ /dev/null @@ -1,25 +0,0 @@ -var $ = require('preconditions').singleton(); -var _ = require('lodash'); - -var Utils = {}; - -Utils.checkRequired = function(obj, args) { - args = [].concat(args); - if (!_.isObject(obj)) return false; - for (var i = 0; i < args.length; i++) { - if (!obj.hasOwnProperty(args[i])) return false; - } - return true; -}; - -/** - * - * @desc rounds a JAvascript number - * @param number - * @return {number} - */ -Utils.strip = function(number) { - return (parseFloat(number.toPrecision(12))); -} - -module.exports = Utils; diff --git a/package.json b/package.json index 01d6fe3..b8fe7c8 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,12 @@ "dependencies": { "async": "^0.9.0", "bitcore-lib": "^0.13.7", - "bitcore-wallet-utils": "~1.0.0", "body-parser": "^1.11.0", "coveralls": "^2.11.2", "email-validator": "^1.0.1", "express": "^4.10.0", "inherits": "^2.0.1", + "json-stable-stringify": "^1.0.0", "locker": "^0.1.0", "locker-server": "^0.1.3", "lodash": "^3.10.1", @@ -64,14 +64,18 @@ "coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, "bitcoreNode": "./bitcorenode", - "contributors": [{ - "name": "Braydon Fuller", - "email": "braydon@bitpay.com" - }, { - "name": "Ivan Socolsky", - "email": "ivan@bitpay.com" - }, { - "name": "Matias Alejo Garcia", - "email": "ematiu@gmail.com" - }] + "contributors": [ + { + "name": "Braydon Fuller", + "email": "braydon@bitpay.com" + }, + { + "name": "Ivan Socolsky", + "email": "ivan@bitpay.com" + }, + { + "name": "Matias Alejo Garcia", + "email": "ematiu@gmail.com" + } + ] } diff --git a/test/integration/emailnotifications.js b/test/integration/emailnotifications.js new file mode 100644 index 0000000..86e2548 --- /dev/null +++ b/test/integration/emailnotifications.js @@ -0,0 +1,462 @@ +'use strict'; + +var _ = require('lodash'); +var async = require('async'); + +var chai = require('chai'); +var sinon = require('sinon'); +var should = chai.should(); +var log = require('npmlog'); +log.debug = log.verbose; +log.level = 'info'; + +var WalletService = require('../../lib/server'); +var EmailService = require('../../lib/emailservice'); + +var TestData = require('../testdata'); +var helpers = require('./helpers'); + +describe('Email notifications', function() { + var server, wallet, mailerStub, emailService; + + before(function(done) { + helpers.before(done); + }); + after(function(done) { + helpers.after(done); + }); + describe('Shared wallet', function() { + beforeEach(function(done) { + helpers.beforeEach(function(res) { + helpers.createAndJoinWallet(2, 3, function(s, w) { + server = s; + wallet = w; + + var i = 0; + async.eachSeries(w.copayers, function(copayer, next) { + helpers.getAuthServer(copayer.id, function(server) { + server.savePreferences({ + email: 'copayer' + (++i) + '@domain.com', + unit: 'bit', + }, next); + }); + }, function(err) { + should.not.exist(err); + + mailerStub = sinon.stub(); + mailerStub.sendMail = sinon.stub(); + mailerStub.sendMail.yields(); + + emailService = new EmailService(); + emailService.start({ + lockOpts: {}, + messageBroker: server.messageBroker, + storage: helpers.getStorage(), + mailer: mailerStub, + emailOpts: { + from: 'bws@dummy.net', + subjectPrefix: '[test wallet]', + publicTxUrlTemplate: { + livenet: 'https://insight.bitpay.com/tx/{{txid}}', + testnet: 'https://test-insight.bitpay.com/tx/{{txid}}', + }, + }, + }, function(err) { + should.not.exist(err); + done(); + }); + }); + }); + }); + }); + + it('should notify copayers a new tx proposal has been created', function(done) { + var _readTemplateFile_old = emailService._readTemplateFile; + emailService._readTemplateFile = function(language, filename, cb) { + if (_.endsWith(filename, '.html')) { + return cb(null, '{{walletName}}'); + } else { + _readTemplateFile_old.call(emailService, language, filename, 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.createTx(txOpts, function(err, tx) { + should.not.exist(err); + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(2); + var emails = _.map(calls, function(c) { + return c.args[0]; + }); + _.difference(['copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; + var one = emails[0]; + one.from.should.equal('bws@dummy.net'); + one.subject.should.contain('New payment proposal'); + one.text.should.contain(wallet.name); + one.text.should.contain(wallet.copayers[0].name); + should.exist(one.html); + one.html.indexOf('').should.equal(0); + one.html.should.contain(wallet.name); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + emailService._readTemplateFile = _readTemplateFile_old; + done(); + }); + }, 100); + }); + }); + }); + + it('should not send email if unable to apply template to notification', function(done) { + var _applyTemplate_old = emailService._applyTemplate; + emailService._applyTemplate = function(template, data, cb) { + _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.createTx(txOpts, function(err, tx) { + should.not.exist(err); + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(0); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + emailService._applyTemplate = _applyTemplate_old; + done(); + }); + }, 100); + }); + }); + }); + + it('should notify copayers a new outgoing tx has been created', function(done) { + var _readTemplateFile_old = emailService._readTemplateFile; + emailService._readTemplateFile = function(language, filename, cb) { + if (_.endsWith(filename, '.html')) { + return cb(null, '{{&urlForTx}}'); + } else { + _readTemplateFile_old.call(emailService, language, filename, cb); + } + }; + helpers.stubUtxos(server, wallet, [1, 1], function() { + var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { + message: 'some message' + }); + + var txp; + async.waterfall([ + + function(next) { + server.createTx(txOpts, next); + }, + function(t, next) { + txp = t; + async.eachSeries(_.range(2), function(i, next) { + var copayer = TestData.copayers[i]; + helpers.getAuthServer(copayer.id44, function(server) { + var signatures = helpers.clientSign(txp, copayer.xPrivKey_44H_0H_0H); + server.signTx({ + txProposalId: txp.id, + signatures: signatures, + }, function(err, t) { + txp = t; + next(); + }); + }); + }, next); + }, + function(next) { + helpers.stubBroadcast(); + server.broadcastTx({ + txProposalId: txp.id, + }, next); + }, + ], function(err) { + should.not.exist(err); + + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + var emails = _.map(_.takeRight(calls, 3), function(c) { + return c.args[0]; + }); + _.difference(['copayer1@domain.com', 'copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; + var one = emails[0]; + one.from.should.equal('bws@dummy.net'); + one.subject.should.contain('Payment sent'); + one.text.should.contain(wallet.name); + one.text.should.contain('800,000'); + should.exist(one.html); + one.html.should.contain('https://insight.bitpay.com/tx/' + txp.txid); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + emailService._readTemplateFile = _readTemplateFile_old; + done(); + }); + }, 100); + }); + }); + }); + + 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 txpId; + async.waterfall([ + + function(next) { + server.createTx(txOpts, next); + }, + function(txp, next) { + txpId = txp.id; + async.eachSeries(_.range(1, 3), function(i, next) { + var copayer = TestData.copayers[i]; + helpers.getAuthServer(copayer.id44, function(server) { + server.rejectTx({ + txProposalId: txp.id, + }, next); + }); + }, next); + }, + ], function(err) { + should.not.exist(err); + + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + var emails = _.map(_.takeRight(calls, 2), function(c) { + return c.args[0]; + }); + _.difference(['copayer1@domain.com', 'copayer2@domain.com'], _.pluck(emails, 'to')).should.be.empty; + var one = emails[0]; + one.from.should.equal('bws@dummy.net'); + one.subject.should.contain('Payment proposal rejected'); + one.text.should.contain(wallet.name); + one.text.should.contain('copayer 2, copayer 3'); + one.text.should.not.contain('copayer 1'); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + done(); + }); + }, 100); + }); + }); + }); + + it('should notify copayers of incoming txs', function(done) { + server.createAddress({}, function(err, address) { + should.not.exist(err); + + // Simulate incoming tx notification + server._notify('NewIncomingTx', { + txid: '999', + address: address, + amount: 12300000, + }, function(err) { + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(3); + var emails = _.map(calls, function(c) { + return c.args[0]; + }); + _.difference(['copayer1@domain.com', 'copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; + var one = emails[0]; + one.from.should.equal('bws@dummy.net'); + one.subject.should.contain('New payment received'); + one.text.should.contain(wallet.name); + one.text.should.contain('123,000'); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + done(); + }); + }, 100); + }); + }); + }); + + it('should notify each email address only once', function(done) { + // Set same email address for copayer1 and copayer2 + server.savePreferences({ + email: 'copayer2@domain.com', + }, function(err) { + server.createAddress({}, function(err, address) { + should.not.exist(err); + + // Simulate incoming tx notification + server._notify('NewIncomingTx', { + txid: '999', + address: address, + amount: 12300000, + }, function(err) { + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(2); + var emails = _.map(calls, function(c) { + return c.args[0]; + }); + _.difference(['copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; + var one = emails[0]; + one.from.should.equal('bws@dummy.net'); + one.subject.should.contain('New payment received'); + one.text.should.contain(wallet.name); + one.text.should.contain('123,000'); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + done(); + }); + }, 100); + }); + }); + }); + }); + + it('should build each email using preferences of the copayers', function(done) { + // Set same email address for copayer1 and copayer2 + server.savePreferences({ + email: 'copayer1@domain.com', + language: 'es', + unit: 'btc', + }, function(err) { + server.createAddress({}, function(err, address) { + should.not.exist(err); + + // Simulate incoming tx notification + server._notify('NewIncomingTx', { + txid: '999', + address: address, + amount: 12300000, + }, function(err) { + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(3); + var emails = _.map(calls, function(c) { + return c.args[0]; + }); + var spanish = _.find(emails, { + to: 'copayer1@domain.com' + }); + spanish.from.should.equal('bws@dummy.net'); + spanish.subject.should.contain('Nuevo pago recibido'); + spanish.text.should.contain(wallet.name); + spanish.text.should.contain('0.123 BTC'); + var english = _.find(emails, { + to: 'copayer2@domain.com' + }); + english.from.should.equal('bws@dummy.net'); + english.subject.should.contain('New payment received'); + english.text.should.contain(wallet.name); + english.text.should.contain('123,000 bits'); + done(); + }, 100); + }); + }); + }); + }); + + it('should support multiple emailservice instances running concurrently', function(done) { + var emailService2 = new EmailService(); + emailService2.start({ + lock: emailService.lock, // Use same locker service + messageBroker: server.messageBroker, + storage: helpers.getStorage(), + mailer: mailerStub, + emailOpts: { + from: 'bws2@dummy.net', + subjectPrefix: '[test wallet 2]', + }, + }, 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.createTx(txOpts, function(err, tx) { + should.not.exist(err); + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(2); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + done(); + }); + }, 100); + }); + }); + }); + }); + }); + + describe('1-of-N wallet', function() { + beforeEach(function(done) { + helpers.beforeEach(function(res) { + helpers.createAndJoinWallet(1, 2, function(s, w) { + server = s; + wallet = w; + + var i = 0; + async.eachSeries(w.copayers, function(copayer, next) { + helpers.getAuthServer(copayer.id, function(server) { + server.savePreferences({ + email: 'copayer' + (++i) + '@domain.com', + unit: 'bit', + }, next); + }); + }, function(err) { + should.not.exist(err); + + mailerStub = sinon.stub(); + mailerStub.sendMail = sinon.stub(); + mailerStub.sendMail.yields(); + + emailService = new EmailService(); + emailService.start({ + lockOpts: {}, + messageBroker: server.messageBroker, + storage: helpers.getStorage(), + mailer: mailerStub, + emailOpts: { + from: 'bws@dummy.net', + subjectPrefix: '[test wallet]', + publicTxUrlTemplate: { + livenet: 'https://insight.bitpay.com/tx/{{txid}}', + testnet: 'https://test-insight.bitpay.com/tx/{{txid}}', + }, + }, + }, function(err) { + should.not.exist(err); + done(); + }); + }); + }); + }); + + 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.createTx(txOpts, function(err, tx) { + should.not.exist(err); + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(0); + done(); + }, 100); + }); + }); + }); + }); + }); +}); diff --git a/test/integration/helpers.js b/test/integration/helpers.js new file mode 100644 index 0000000..9adf561 --- /dev/null +++ b/test/integration/helpers.js @@ -0,0 +1,419 @@ +'use strict'; + +var _ = require('lodash'); +var async = require('async'); + +var chai = require('chai'); +var sinon = require('sinon'); +var should = chai.should(); +var log = require('npmlog'); +log.debug = log.verbose; +var tingodb = require('tingodb')({ + memStore: true +}); + +var Bitcore = require('bitcore-lib'); + +var Common = require('../../lib/common'); +var Utils = Common.Utils; +var Constants = Common.Constants; +var Defaults = Common.Defaults; + +var Storage = require('../../lib/storage'); +var Model = require('../../lib/model'); +var WalletService = require('../../lib/server'); +var TestData = require('../testdata'); + +var storage, blockchainExplorer; + +var helpers = {}; + +var useMongoDb = !!process.env.USE_MONGO_DB; + +helpers.before = function(cb) { + function getDb(cb) { + if (useMongoDb) { + var mongodb = require('mongodb'); + mongodb.MongoClient.connect('mongodb://localhost:27017/bws_test', function(err, db) { + if (err) throw err; + return cb(db); + }); + } else { + var db = new tingodb.Db('./db/test', {}); + return cb(db); + } + } + getDb(function(db) { + storage = new Storage({ + db: db + }); + return cb(); + }); +}; + +helpers.beforeEach = function(cb) { + if (!storage.db) return cb(); + storage.db.dropDatabase(function(err) { + if (err) return cb(err); + blockchainExplorer = sinon.stub(); + var opts = { + storage: storage, + blockchainExplorer: blockchainExplorer + }; + WalletService.initialize(opts, function() { + return cb(opts); + }); + }); +}; + +helpers.after = function(cb) { + WalletService.shutDown(cb); +}; + +helpers.getBlockchainExplorer = function() { + return blockchainExplorer; +}; + +helpers.getStorage = function() { + return storage; +}; + +helpers.signMessage = function(text, privKey) { + var priv = new Bitcore.PrivateKey(privKey); + var hash = Utils.hashMessage(text); + return Bitcore.crypto.ECDSA.sign(hash, priv, 'little').toString(); +}; + +helpers.signRequestPubKey = function(requestPubKey, xPrivKey) { + var priv = new Bitcore.HDPrivateKey(xPrivKey).derive(Constants.PATHS.REQUEST_KEY_AUTH).privateKey; + return helpers.signMessage(requestPubKey, priv); +}; + +helpers.getAuthServer = function(copayerId, cb) { + var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); + verifyStub.returns(true); + WalletService.getInstanceWithAuth({ + copayerId: copayerId, + message: 'dummy', + signature: 'dummy', + clientVersion: 'bwc-0.1.0', + }, function(err, server) { + verifyStub.restore(); + if (err || !server) throw new Error('Could not login as copayerId ' + copayerId); + return cb(server); + }); +}; + +helpers._generateCopayersTestData = function(n) { + console.log('var copayers = ['); + _.each(_.range(n), function(c) { + var xpriv = new Bitcore.HDPrivateKey(); + var xpub = Bitcore.HDPublicKey(xpriv); + + var xpriv_45H = xpriv.derive(45, true); + var xpub_45H = Bitcore.HDPublicKey(xpriv_45H); + var id45 = Copayer._xPubToCopayerId(xpub_45H.toString()); + + var xpriv_44H_0H_0H = xpriv.derive(44, true).derive(0, true).derive(0, true); + var xpub_44H_0H_0H = Bitcore.HDPublicKey(xpriv_44H_0H_0H); + var id44 = Copayer._xPubToCopayerId(xpub_44H_0H_0H.toString()); + + var xpriv_1H = xpriv.derive(1, true); + var xpub_1H = Bitcore.HDPublicKey(xpriv_1H); + var priv = xpriv_1H.derive(0).privateKey; + var pub = xpub_1H.derive(0).publicKey; + + console.log('{id44: ', "'" + id44 + "',"); + console.log('id45: ', "'" + id45 + "',"); + console.log('xPrivKey: ', "'" + xpriv.toString() + "',"); + console.log('xPubKey: ', "'" + xpub.toString() + "',"); + console.log('xPrivKey_45H: ', "'" + xpriv_45H.toString() + "',"); + console.log('xPubKey_45H: ', "'" + xpub_45H.toString() + "',"); + console.log('xPrivKey_44H_0H_0H: ', "'" + xpriv_44H_0H_0H.toString() + "',"); + console.log('xPubKey_44H_0H_0H: ', "'" + xpub_44H_0H_0H.toString() + "',"); + console.log('xPrivKey_1H: ', "'" + xpriv_1H.toString() + "',"); + console.log('xPubKey_1H: ', "'" + xpub_1H.toString() + "',"); + console.log('privKey_1H_0: ', "'" + priv.toString() + "',"); + console.log('pubKey_1H_0: ', "'" + pub.toString() + "'},"); + }); + console.log('];'); +}; + +helpers.getSignedCopayerOpts = function(opts) { + var hash = WalletService._getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); + opts.copayerSignature = helpers.signMessage(hash, TestData.keyPair.priv); + return opts; +}; + +helpers.createAndJoinWallet = function(m, n, opts, cb) { + if (_.isFunction(opts)) { + cb = opts; + opts = {}; + } + opts = opts || {}; + + var server = new WalletService(); + var copayerIds = []; + var offset = opts.offset || 0; + + var walletOpts = { + name: 'a wallet', + m: m, + n: n, + pubKey: TestData.keyPair.pub, + }; + if (_.isBoolean(opts.supportBIP44AndP2PKH)) + walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; + + server.createWallet(walletOpts, function(err, walletId) { + if (err) return cb(err); + + async.each(_.range(n), function(i, cb) { + var copayerData = TestData.copayers[i + offset]; + var copayerOpts = helpers.getSignedCopayerOpts({ + walletId: walletId, + name: 'copayer ' + (i + 1), + xPubKey: (_.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H, + requestPubKey: copayerData.pubKey_1H_0, + customData: 'custom data ' + (i + 1), + }); + if (_.isBoolean(opts.supportBIP44AndP2PKH)) + copayerOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; + + server.joinWallet(copayerOpts, function(err, result) { + should.not.exist(err); + copayerIds.push(result.copayerId); + return cb(err); + }); + }, function(err) { + if (err) return new Error('Could not generate wallet'); + helpers.getAuthServer(copayerIds[0], function(s) { + s.getWallet({}, function(err, w) { + cb(s, w); + }); + }); + }); + }); +}; + + +helpers.randomTXID = function() { + return Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex');; +}; + +helpers.toSatoshi = function(btc) { + if (_.isArray(btc)) { + return _.map(btc, helpers.toSatoshi); + } else { + return Utils.strip(btc * 1e8); + } +}; + +helpers.stubUtxos = function(server, wallet, amounts, cb) { + async.mapSeries(_.range(0, amounts.length > 2 ? 2 : 1), function(i, next) { + server.createAddress({}, next); + }, function(err, addresses) { + should.not.exist(err); + addresses.should.not.be.empty; + var utxos = _.map([].concat(amounts), function(amount, i) { + var address = addresses[i % addresses.length]; + var confirmations; + if (_.isString(amount) && _.startsWith(amount, 'u')) { + amount = parseFloat(amount.substring(1)); + confirmations = 0; + } else { + confirmations = Math.floor(Math.random() * 100 + 1); + } + + var scriptPubKey; + switch (wallet.addressType) { + case Constants.SCRIPT_TYPES.P2SH: + scriptPubKey = Bitcore.Script.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut(); + break; + case Constants.SCRIPT_TYPES.P2PKH: + scriptPubKey = Bitcore.Script.buildPublicKeyHashOut(address.address); + break; + } + should.exist(scriptPubKey); + + return { + txid: helpers.randomTXID(), + vout: Math.floor(Math.random() * 10 + 1), + satoshis: helpers.toSatoshi(amount).toString(), + scriptPubKey: scriptPubKey.toBuffer().toString('hex'), + address: address.address, + confirmations: confirmations, + }; + }); + blockchainExplorer.getUnspentUtxos = function(addresses, cb) { + var selected = _.filter(utxos, function(utxo) { + return _.contains(addresses, utxo.address); + }); + return cb(null, selected); + }; + + return cb(utxos); + }); +}; + +helpers.stubBroadcast = function(thirdPartyBroadcast) { + blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, null, '112233'); + blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null); +}; + +helpers.stubHistory = function(txs) { + blockchainExplorer.getTransactions = function(addresses, from, to, cb) { + var MAX_BATCH_SIZE = 100; + var nbTxs = txs.length; + + if (_.isUndefined(from) && _.isUndefined(to)) { + from = 0; + to = MAX_BATCH_SIZE; + } + if (!_.isUndefined(from) && _.isUndefined(to)) + to = from + MAX_BATCH_SIZE; + + if (!_.isUndefined(from) && !_.isUndefined(to) && to - from > MAX_BATCH_SIZE) + to = from + MAX_BATCH_SIZE; + + if (from < 0) from = 0; + if (to < 0) to = 0; + if (from > nbTxs) from = nbTxs; + if (to > nbTxs) to = nbTxs; + + var page = txs.slice(from, to); + return cb(null, page); + }; +}; + +helpers.stubFeeLevels = function(levels) { + blockchainExplorer.estimateFee = function(nbBlocks, cb) { + var result = _.zipObject(_.map(_.pick(levels, nbBlocks), function(fee, n) { + return [+n, fee > 0 ? fee / 1e8 : fee]; + })); + return cb(null, result); + }; +}; + +helpers.stubAddressActivity = function(activeAddresses) { + blockchainExplorer.getAddressActivity = function(address, cb) { + return cb(null, _.contains(activeAddresses, address)); + }; +}; + +helpers.clientSign = function(txp, derivedXPrivKey) { + var self = this; + + //Derive proper key to sign, for each input + var privs = []; + var derived = {}; + + var xpriv = new Bitcore.HDPrivateKey(derivedXPrivKey, txp.network); + + _.each(txp.inputs, function(i) { + if (!derived[i.path]) { + derived[i.path] = xpriv.derive(i.path).privateKey; + privs.push(derived[i.path]); + } + }); + + var t = txp.getBitcoreTx(); + + var signatures = _.map(privs, function(priv, i) { + return t.getSignatures(priv); + }); + + signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) { + return s.signature.toDER().toString('hex'); + }); + + 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.TxProposal.Types.SIMPLE, outputs, signingKey, opts); +}; + +helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts) { + _.each(outputs, function(output) { + output.amount = helpers.toSatoshi(output.amount); + }); + + var opts = { + type: type, + proposalSignature: null + }; + + if (moreOpts) { + moreOpts = _.chain(moreOpts) + .pick(['feePerKb', 'customData', 'message']) + .value(); + opts = _.assign(opts, moreOpts); + } + + opts = _.defaults(opts, { + message: null + }); + + var hash; + if (type == Model.TxProposal.Types.SIMPLE) { + opts.toAddress = outputs[0].toAddress; + opts.amount = outputs[0].amount; + hash = WalletService._getProposalHash(opts.toAddress, opts.amount, + opts.message, opts.payProUrl); + } else if (type == Model.TxProposal.Types.MULTIPLEOUTPUTS) { + 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.now(), 'Date'); + async.map(_.range(main + change), function(i, next) { + clock.tick(1000); + var address = wallet.createAddress(i >= main); + server.storage.storeAddressAndWallet(wallet, address, function(err) { + next(err, address); + }); + }, function(err, addresses) { + if (err) throw new Error('Could not generate addresses'); + clock.restore(); + return cb(_.take(addresses, main), _.takeRight(addresses, change)); + }); +}; + +module.exports = helpers; diff --git a/test/integration/server.js b/test/integration/server.js index 3c29d84..4db9d13 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -2,817 +2,44 @@ var _ = require('lodash'); var async = require('async'); -var inspect = require('util').inspect; var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); var log = require('npmlog'); log.debug = log.verbose; +log.level = 'info'; -var fs = require('fs'); -var tingodb = require('tingodb')({ - memStore: true -}); +var Bitcore = require('bitcore-lib'); -var Utils = require('../../lib/utils'); -var WalletUtils = require('bitcore-wallet-utils'); -var Bitcore = WalletUtils.Bitcore; -var Storage = require('../../lib/storage'); +var Common = require('../../lib/common'); +var Utils = Common.Utils; +var Constants = Common.Constants; +var Defaults = Common.Defaults; var Model = require('../../lib/model'); var WalletService = require('../../lib/server'); -var EmailService = require('../../lib/emailservice'); var TestData = require('../testdata'); -var CLIENT_VERSION = 'bwc-0.1.1'; - -var helpers = {}; -helpers.getAuthServer = function(copayerId, cb) { - var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); - verifyStub.returns(true); - WalletService.getInstanceWithAuth({ - copayerId: copayerId, - message: 'dummy', - signature: 'dummy', - clientVersion: 'bwc-0.1.0', - }, function(err, server) { - verifyStub.restore(); - if (err || !server) throw new Error('Could not login as copayerId ' + copayerId); - return cb(server); - }); -}; - -helpers._generateCopayersTestData = function(n) { - console.log('var copayers = ['); - _.each(_.range(n), function(c) { - var xpriv = new Bitcore.HDPrivateKey(); - var xpub = Bitcore.HDPublicKey(xpriv); - - var xpriv_45H = xpriv.derive(45, true); - var xpub_45H = Bitcore.HDPublicKey(xpriv_45H); - var id45 = WalletUtils.xPubToCopayerId(xpub_45H.toString()); - - var xpriv_44H_0H_0H = xpriv.derive(44, true).derive(0, true).derive(0, true); - var xpub_44H_0H_0H = Bitcore.HDPublicKey(xpriv_44H_0H_0H); - var id44 = WalletUtils.xPubToCopayerId(xpub_44H_0H_0H.toString()); - - var xpriv_1H = xpriv.derive(1, true); - var xpub_1H = Bitcore.HDPublicKey(xpriv_1H); - var priv = xpriv_1H.derive(0).privateKey; - var pub = xpub_1H.derive(0).publicKey; - - console.log('{id44: ', "'" + id44 + "',"); - console.log('id45: ', "'" + id45 + "',"); - console.log('xPrivKey: ', "'" + xpriv.toString() + "',"); - console.log('xPubKey: ', "'" + xpub.toString() + "',"); - console.log('xPrivKey_45H: ', "'" + xpriv_45H.toString() + "',"); - console.log('xPubKey_45H: ', "'" + xpub_45H.toString() + "',"); - console.log('xPrivKey_44H_0H_0H: ', "'" + xpriv_44H_0H_0H.toString() + "',"); - console.log('xPubKey_44H_0H_0H: ', "'" + xpub_44H_0H_0H.toString() + "',"); - console.log('xPrivKey_1H: ', "'" + xpriv_1H.toString() + "',"); - console.log('xPubKey_1H: ', "'" + xpub_1H.toString() + "',"); - console.log('privKey_1H_0: ', "'" + priv.toString() + "',"); - console.log('pubKey_1H_0: ', "'" + pub.toString() + "'},"); - }); - console.log('];'); -}; - -helpers.getSignedCopayerOpts = function(opts) { - var hash = WalletUtils.getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); - opts.copayerSignature = WalletUtils.signMessage(hash, TestData.keyPair.priv); - return opts; -}; - -helpers.createAndJoinWallet = function(m, n, opts, cb) { - if (_.isFunction(opts)) { - cb = opts; - opts = {}; - } - opts = opts || {}; - - var server = new WalletService(); - var copayerIds = []; - var offset = opts.offset || 0; - - var walletOpts = { - name: 'a wallet', - m: m, - n: n, - pubKey: TestData.keyPair.pub, - }; - if (_.isBoolean(opts.supportBIP44AndP2PKH)) - walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; - - server.createWallet(walletOpts, function(err, walletId) { - if (err) return cb(err); - - async.each(_.range(n), function(i, cb) { - var copayerData = TestData.copayers[i + offset]; - var copayerOpts = helpers.getSignedCopayerOpts({ - walletId: walletId, - name: 'copayer ' + (i + 1), - xPubKey: (_.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H, - requestPubKey: copayerData.pubKey_1H_0, - customData: 'custom data ' + (i + 1), - }); - if (_.isBoolean(opts.supportBIP44AndP2PKH)) - copayerOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; - - server.joinWallet(copayerOpts, function(err, result) { - should.not.exist(err); - copayerIds.push(result.copayerId); - return cb(err); - }); - }, function(err) { - if (err) return new Error('Could not generate wallet'); - helpers.getAuthServer(copayerIds[0], function(s) { - s.getWallet({}, function(err, w) { - cb(s, w); - }); - }); - }); - }); -}; - - -helpers.randomTXID = function() { - return Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex');; -}; - - -helpers.toSatoshi = function(btc) { - if (_.isArray(btc)) { - return _.map(btc, helpers.toSatoshi); - } else { - return Utils.strip(btc * 1e8); - } -}; - -helpers.stubUtxos = function(server, wallet, amounts, cb) { - async.mapSeries(_.range(0, amounts.length > 2 ? 2 : 1), function(i, next) { - server.createAddress({}, next); - }, function(err, addresses) { - should.not.exist(err); - addresses.should.not.be.empty; - var utxos = _.map([].concat(amounts), function(amount, i) { - var address = addresses[i % addresses.length]; - var confirmations; - if (_.isString(amount) && _.startsWith(amount, 'u')) { - amount = parseFloat(amount.substring(1)); - confirmations = 0; - } else { - confirmations = Math.floor(Math.random() * 100 + 1); - } - - var scriptPubKey; - switch (wallet.addressType) { - case WalletUtils.SCRIPT_TYPES.P2SH: - scriptPubKey = Bitcore.Script.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut(); - break; - case WalletUtils.SCRIPT_TYPES.P2PKH: - scriptPubKey = Bitcore.Script.buildPublicKeyHashOut(address.address); - break; - } - should.exist(scriptPubKey); - - return { - txid: helpers.randomTXID(), - vout: Math.floor(Math.random() * 10 + 1), - satoshis: helpers.toSatoshi(amount).toString(), - scriptPubKey: scriptPubKey.toBuffer().toString('hex'), - address: address.address, - confirmations: confirmations, - }; - }); - blockchainExplorer.getUnspentUtxos = function(addresses, cb) { - var selected = _.filter(utxos, function(utxo) { - return _.contains(addresses, utxo.address); - }); - return cb(null, selected); - }; - - return cb(utxos); - }); -}; - -helpers.stubBroadcast = function(thirdPartyBroadcast) { - blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, null, '112233'); - blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null); -}; - -helpers.stubHistory = function(txs) { - blockchainExplorer.getTransactions = function(addresses, from, to, cb) { - var MAX_BATCH_SIZE = 100; - var nbTxs = txs.length; - - if (_.isUndefined(from) && _.isUndefined(to)) { - from = 0; - to = MAX_BATCH_SIZE; - } - if (!_.isUndefined(from) && _.isUndefined(to)) - to = from + MAX_BATCH_SIZE; - - if (!_.isUndefined(from) && !_.isUndefined(to) && to - from > MAX_BATCH_SIZE) - to = from + MAX_BATCH_SIZE; - - if (from < 0) from = 0; - if (to < 0) to = 0; - if (from > nbTxs) from = nbTxs; - if (to > nbTxs) to = nbTxs; - - var page = txs.slice(from, to); - return cb(null, page); - }; -}; - -helpers.stubFeeLevels = function(levels) { - blockchainExplorer.estimateFee = function(nbBlocks, cb) { - var result = _.zipObject(_.map(_.pick(levels, nbBlocks), function(fee, n) { - return [+n, fee > 0 ? fee / 1e8 : fee]; - })); - return cb(null, result); - }; -}; - -helpers.stubAddressActivity = function(activeAddresses) { - blockchainExplorer.getAddressActivity = function(address, cb) { - return cb(null, _.contains(activeAddresses, address)); - }; -}; - -helpers.clientSign = WalletUtils.signTxp; - -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 = WalletUtils.getProposalHash(toAddress, opts.amount, message); - - try { - opts.proposalSignature = WalletUtils.signMessage(hash, signingKey); - } catch (ex) {} - - return opts; -}; - -helpers.createSimpleProposalOpts = function(toAddress, amount, signingKey, opts) { - var outputs = [{ - toAddress: toAddress, - amount: amount, - }]; - return helpers.createProposalOpts(Model.TxProposal.Types.SIMPLE, outputs, signingKey, opts); -}; - -helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts) { - _.each(outputs, function(output) { - output.amount = helpers.toSatoshi(output.amount); - }); - - var opts = { - type: type, - proposalSignature: null - }; - - if (moreOpts) { - moreOpts = _.chain(moreOpts) - .pick(['feePerKb', 'customData', 'message']) - .value(); - opts = _.assign(opts, moreOpts); - } - - opts = _.defaults(opts, { - message: null - }); - - var hash; - if (type == Model.TxProposal.Types.SIMPLE) { - opts.toAddress = outputs[0].toAddress; - opts.amount = outputs[0].amount; - hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, - opts.message, opts.payProUrl); - } else if (type == Model.TxProposal.Types.MULTIPLEOUTPUTS) { - opts.outputs = outputs; - var header = { - outputs: outputs, - message: opts.message, - payProUrl: opts.payProUrl - }; - hash = WalletUtils.getProposalHash(header); - } - - try { - opts.proposalSignature = WalletUtils.signMessage(hash, signingKey); - } catch (ex) {} - - return opts; -}; - -helpers.createAddresses = function(server, wallet, main, change, cb) { - var clock = sinon.useFakeTimers(Date.now(), 'Date'); - async.map(_.range(main + change), function(i, next) { - clock.tick(1000); - var address = wallet.createAddress(i >= main); - server.storage.storeAddressAndWallet(wallet, address, function(err) { - next(err, address); - }); - }, function(err, addresses) { - if (err) throw new Error('Could not generate addresses'); - clock.restore(); - return cb(_.take(addresses, main), _.takeRight(addresses, change)); - }); -}; - +var helpers = require('./helpers'); var storage, blockchainExplorer; -var useMongoDb = !!process.env.USE_MONGO_DB; - -function initStorage(cb) { - function getDb(cb) { - if (useMongoDb) { - var mongodb = require('mongodb'); - mongodb.MongoClient.connect('mongodb://localhost:27017/bws_test', function(err, db) { - if (err) throw err; - return cb(db); - }); - } else { - var db = new tingodb.Db('./db/test', {}); - return cb(db); - } - } - getDb(function(db) { - storage = new Storage({ - db: db - }); - return cb(); - }); -}; - -function resetStorage(cb) { - if (!storage.db) return cb(); - storage.db.dropDatabase(function(err) { - return cb(); - }); -}; - +var CLIENT_VERSION = 'bwc-0.1.1'; describe('Wallet service', function() { before(function(done) { - initStorage(done); + helpers.before(done); }); beforeEach(function(done) { - resetStorage(function() { - blockchainExplorer = sinon.stub(); - WalletService.initialize({ - storage: storage, - blockchainExplorer: blockchainExplorer, - }, done); + helpers.beforeEach(function(res) { + storage = res.storage; + blockchainExplorer = res.blockchainExplorer; + done(); }); }); after(function(done) { - WalletService.shutDown(done); - }); - - describe('Email notifications', function() { - var server, wallet, mailerStub, emailService; - - describe('Shared wallet', function() { - beforeEach(function(done) { - helpers.createAndJoinWallet(2, 3, function(s, w) { - server = s; - wallet = w; - - var i = 0; - async.eachSeries(w.copayers, function(copayer, next) { - helpers.getAuthServer(copayer.id, function(server) { - server.savePreferences({ - email: 'copayer' + (++i) + '@domain.com', - unit: 'bit', - }, next); - }); - }, function(err) { - should.not.exist(err); - - mailerStub = sinon.stub(); - mailerStub.sendMail = sinon.stub(); - mailerStub.sendMail.yields(); - - emailService = new EmailService(); - emailService.start({ - lockOpts: {}, - messageBroker: server.messageBroker, - storage: storage, - mailer: mailerStub, - emailOpts: { - from: 'bws@dummy.net', - subjectPrefix: '[test wallet]', - publicTxUrlTemplate: { - livenet: 'https://insight.bitpay.com/tx/{{txid}}', - testnet: 'https://test-insight.bitpay.com/tx/{{txid}}', - }, - }, - }, function(err) { - should.not.exist(err); - done(); - }); - }); - }); - }); - - it('should notify copayers a new tx proposal has been created', function(done) { - var _readTemplateFile_old = emailService._readTemplateFile; - emailService._readTemplateFile = function(language, filename, cb) { - if (_.endsWith(filename, '.html')) { - return cb(null, '{{walletName}}'); - } else { - _readTemplateFile_old.call(emailService, language, filename, 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.createTx(txOpts, function(err, tx) { - should.not.exist(err); - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - calls.length.should.equal(2); - var emails = _.map(calls, function(c) { - return c.args[0]; - }); - _.difference(['copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; - var one = emails[0]; - one.from.should.equal('bws@dummy.net'); - one.subject.should.contain('New payment proposal'); - one.text.should.contain(wallet.name); - one.text.should.contain(wallet.copayers[0].name); - should.exist(one.html); - one.html.indexOf('').should.equal(0); - one.html.should.contain(wallet.name); - server.storage.fetchUnsentEmails(function(err, unsent) { - should.not.exist(err); - unsent.should.be.empty; - emailService._readTemplateFile = _readTemplateFile_old; - done(); - }); - }, 100); - }); - }); - }); - - it('should not send email if unable to apply template to notification', function(done) { - var _applyTemplate_old = emailService._applyTemplate; - emailService._applyTemplate = function(template, data, cb) { - _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.createTx(txOpts, function(err, tx) { - should.not.exist(err); - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - calls.length.should.equal(0); - server.storage.fetchUnsentEmails(function(err, unsent) { - should.not.exist(err); - unsent.should.be.empty; - emailService._applyTemplate = _applyTemplate_old; - done(); - }); - }, 100); - }); - }); - }); - - it('should notify copayers a new outgoing tx has been created', function(done) { - var _readTemplateFile_old = emailService._readTemplateFile; - emailService._readTemplateFile = function(language, filename, cb) { - if (_.endsWith(filename, '.html')) { - return cb(null, '{{&urlForTx}}'); - } else { - _readTemplateFile_old.call(emailService, language, filename, cb); - } - }; - helpers.stubUtxos(server, wallet, [1, 1], function() { - var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, TestData.copayers[0].privKey_1H_0, { - message: 'some message' - }); - - var txp; - async.waterfall([ - - function(next) { - server.createTx(txOpts, next); - }, - function(t, next) { - txp = t; - async.eachSeries(_.range(2), function(i, next) { - var copayer = TestData.copayers[i]; - helpers.getAuthServer(copayer.id44, function(server) { - var signatures = helpers.clientSign(txp, copayer.xPrivKey); - server.signTx({ - txProposalId: txp.id, - signatures: signatures, - }, function(err, t) { - txp = t; - next(); - }); - }); - }, next); - }, - function(next) { - helpers.stubBroadcast(); - server.broadcastTx({ - txProposalId: txp.id, - }, next); - }, - ], function(err) { - should.not.exist(err); - - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - var emails = _.map(_.takeRight(calls, 3), function(c) { - return c.args[0]; - }); - _.difference(['copayer1@domain.com', 'copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; - var one = emails[0]; - one.from.should.equal('bws@dummy.net'); - one.subject.should.contain('Payment sent'); - one.text.should.contain(wallet.name); - one.text.should.contain('800,000'); - should.exist(one.html); - one.html.should.contain('https://insight.bitpay.com/tx/' + txp.txid); - server.storage.fetchUnsentEmails(function(err, unsent) { - should.not.exist(err); - unsent.should.be.empty; - emailService._readTemplateFile = _readTemplateFile_old; - done(); - }); - }, 100); - }); - }); - }); - - 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 txpId; - async.waterfall([ - - function(next) { - server.createTx(txOpts, next); - }, - function(txp, next) { - txpId = txp.id; - async.eachSeries(_.range(1, 3), function(i, next) { - var copayer = TestData.copayers[i]; - helpers.getAuthServer(copayer.id44, function(server) { - server.rejectTx({ - txProposalId: txp.id, - }, next); - }); - }, next); - }, - ], function(err) { - should.not.exist(err); - - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - var emails = _.map(_.takeRight(calls, 2), function(c) { - return c.args[0]; - }); - _.difference(['copayer1@domain.com', 'copayer2@domain.com'], _.pluck(emails, 'to')).should.be.empty; - var one = emails[0]; - one.from.should.equal('bws@dummy.net'); - one.subject.should.contain('Payment proposal rejected'); - one.text.should.contain(wallet.name); - one.text.should.contain('copayer 2, copayer 3'); - one.text.should.not.contain('copayer 1'); - server.storage.fetchUnsentEmails(function(err, unsent) { - should.not.exist(err); - unsent.should.be.empty; - done(); - }); - }, 100); - }); - }); - }); - - it('should notify copayers of incoming txs', function(done) { - server.createAddress({}, function(err, address) { - should.not.exist(err); - - // Simulate incoming tx notification - server._notify('NewIncomingTx', { - txid: '999', - address: address, - amount: 12300000, - }, function(err) { - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - calls.length.should.equal(3); - var emails = _.map(calls, function(c) { - return c.args[0]; - }); - _.difference(['copayer1@domain.com', 'copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; - var one = emails[0]; - one.from.should.equal('bws@dummy.net'); - one.subject.should.contain('New payment received'); - one.text.should.contain(wallet.name); - one.text.should.contain('123,000'); - server.storage.fetchUnsentEmails(function(err, unsent) { - should.not.exist(err); - unsent.should.be.empty; - done(); - }); - }, 100); - }); - }); - }); - - it('should notify each email address only once', function(done) { - // Set same email address for copayer1 and copayer2 - server.savePreferences({ - email: 'copayer2@domain.com', - }, function(err) { - server.createAddress({}, function(err, address) { - should.not.exist(err); - - // Simulate incoming tx notification - server._notify('NewIncomingTx', { - txid: '999', - address: address, - amount: 12300000, - }, function(err) { - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - calls.length.should.equal(2); - var emails = _.map(calls, function(c) { - return c.args[0]; - }); - _.difference(['copayer2@domain.com', 'copayer3@domain.com'], _.pluck(emails, 'to')).should.be.empty; - var one = emails[0]; - one.from.should.equal('bws@dummy.net'); - one.subject.should.contain('New payment received'); - one.text.should.contain(wallet.name); - one.text.should.contain('123,000'); - server.storage.fetchUnsentEmails(function(err, unsent) { - should.not.exist(err); - unsent.should.be.empty; - done(); - }); - }, 100); - }); - }); - }); - }); - - it('should build each email using preferences of the copayers', function(done) { - // Set same email address for copayer1 and copayer2 - server.savePreferences({ - email: 'copayer1@domain.com', - language: 'es', - unit: 'btc', - }, function(err) { - server.createAddress({}, function(err, address) { - should.not.exist(err); - - // Simulate incoming tx notification - server._notify('NewIncomingTx', { - txid: '999', - address: address, - amount: 12300000, - }, function(err) { - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - calls.length.should.equal(3); - var emails = _.map(calls, function(c) { - return c.args[0]; - }); - var spanish = _.find(emails, { - to: 'copayer1@domain.com' - }); - spanish.from.should.equal('bws@dummy.net'); - spanish.subject.should.contain('Nuevo pago recibido'); - spanish.text.should.contain(wallet.name); - spanish.text.should.contain('0.123 BTC'); - var english = _.find(emails, { - to: 'copayer2@domain.com' - }); - english.from.should.equal('bws@dummy.net'); - english.subject.should.contain('New payment received'); - english.text.should.contain(wallet.name); - english.text.should.contain('123,000 bits'); - done(); - }, 100); - }); - }); - }); - }); - - it('should support multiple emailservice instances running concurrently', function(done) { - var emailService2 = new EmailService(); - emailService2.start({ - lock: emailService.lock, // Use same locker service - messageBroker: server.messageBroker, - storage: storage, - mailer: mailerStub, - emailOpts: { - from: 'bws2@dummy.net', - subjectPrefix: '[test wallet 2]', - }, - }, 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.createTx(txOpts, function(err, tx) { - should.not.exist(err); - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - calls.length.should.equal(2); - server.storage.fetchUnsentEmails(function(err, unsent) { - should.not.exist(err); - unsent.should.be.empty; - done(); - }); - }, 100); - }); - }); - }); - }); - }); - - describe('1-of-N wallet', function() { - beforeEach(function(done) { - helpers.createAndJoinWallet(1, 2, function(s, w) { - server = s; - wallet = w; - - var i = 0; - async.eachSeries(w.copayers, function(copayer, next) { - helpers.getAuthServer(copayer.id, function(server) { - server.savePreferences({ - email: 'copayer' + (++i) + '@domain.com', - unit: 'bit', - }, next); - }); - }, function(err) { - should.not.exist(err); - - mailerStub = sinon.stub(); - mailerStub.sendMail = sinon.stub(); - mailerStub.sendMail.yields(); - - emailService = new EmailService(); - emailService.start({ - lockOpts: {}, - messageBroker: server.messageBroker, - storage: storage, - mailer: mailerStub, - emailOpts: { - from: 'bws@dummy.net', - subjectPrefix: '[test wallet]', - publicTxUrlTemplate: { - livenet: 'https://insight.bitpay.com/tx/{{txid}}', - testnet: 'https://test-insight.bitpay.com/tx/{{txid}}', - }, - }, - }, function(err) { - should.not.exist(err); - done(); - }); - }); - }); - }); - - 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.createTx(txOpts, function(err, tx) { - should.not.exist(err); - setTimeout(function() { - var calls = mailerStub.sendMail.getCalls(); - calls.length.should.equal(0); - done(); - }, 100); - }); - }); - }); - }); - + helpers.after(done); }); describe('#getServiceVersion', function() { @@ -836,7 +63,7 @@ describe('Wallet service', function() { var xpriv = TestData.copayers[0].xPrivKey; var priv = TestData.copayers[0].privKey_1H_0; - var sig = WalletUtils.signMessage('hello world', priv); + var sig = helpers.signMessage('hello world', priv); WalletService.getInstanceWithAuth({ copayerId: wallet.copayers[0].id, @@ -858,7 +85,7 @@ describe('Wallet service', function() { var opts = { copayerId: 'dummy', message: message, - signature: WalletUtils.signMessage(message, TestData.copayers[0].privKey_1H_0), + signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0), }; WalletService.getInstanceWithAuth(opts, function(err, server) { err.code.should.equal('NOT_AUTHORIZED'); @@ -1512,7 +739,7 @@ describe('Wallet service', function() { var message = 'hello world'; var opts = { message: message, - signature: WalletUtils.signMessage(message, TestData.copayers[0].privKey_1H_0), + signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0), }; server.verifyMessageSignature(opts, function(err, isValid) { should.not.exist(err); @@ -1525,7 +752,7 @@ describe('Wallet service', function() { var message = 'hello world'; var opts = { message: message, - signature: WalletUtils.signMessage(message, TestData.copayers[0].privKey_1H_0), + signature: helpers.signMessage(message, TestData.copayers[0].privKey_1H_0), }; helpers.getAuthServer(wallet.copayers[1].id, function(server) { server.verifyMessageSignature(opts, function(err, isValid) { @@ -1719,8 +946,8 @@ describe('Wallet service', function() { }); it('should fail to create more consecutive addresses with no activity than allowed', function(done) { - var MAX_MAIN_ADDRESS_GAP_old = WalletService.MAX_MAIN_ADDRESS_GAP; - WalletService.MAX_MAIN_ADDRESS_GAP = 2; + var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP; + Defaults.MAX_MAIN_ADDRESS_GAP = 2; helpers.stubAddressActivity([]); async.map(_.range(2), function(i, next) { server.createAddress({}, next); @@ -1746,7 +973,7 @@ describe('Wallet service', function() { should.exist(address); address.path.should.equal('m/0/3'); - WalletService.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old; + Defaults.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old; done(); }); }); @@ -1755,8 +982,8 @@ describe('Wallet service', function() { }); it('should cache address activity', function(done) { - var MAX_MAIN_ADDRESS_GAP_old = WalletService.MAX_MAIN_ADDRESS_GAP; - WalletService.MAX_MAIN_ADDRESS_GAP = 2; + var MAX_MAIN_ADDRESS_GAP_old = Defaults.MAX_MAIN_ADDRESS_GAP; + Defaults.MAX_MAIN_ADDRESS_GAP = 2; helpers.stubAddressActivity([]); async.map(_.range(2), function(i, next) { server.createAddress({}, next); @@ -1770,7 +997,7 @@ describe('Wallet service', function() { server.createAddress({}, function(err, address) { should.not.exist(err); getAddressActivitySpy.callCount.should.equal(1); - WalletService.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old; + Defaults.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old; done(); }); }); @@ -1826,7 +1053,6 @@ describe('Wallet service', function() { }); }); - describe('Preferences', function() { var server, wallet; beforeEach(function(done) { @@ -2014,7 +1240,7 @@ describe('Wallet service', function() { var opts, reqPrivKey, ws; var getAuthServer = function(copayerId, privKey, cb) { var msg = 'dummy'; - var sig = WalletUtils.signMessage(msg, privKey); + var sig = helpers.signMessage(msg, privKey); WalletService.getInstanceWithAuth({ copayerId: copayerId, message: msg, @@ -2030,9 +1256,9 @@ describe('Wallet service', function() { var requestPubKey = reqPrivKey.toPublicKey(); var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H; - var sig = WalletUtils.signRequestPubKey(requestPubKey, xPrivKey); + var sig = helpers.signRequestPubKey(requestPubKey, xPrivKey); - var copayerId = WalletUtils.xPubToCopayerId(TestData.copayers[0].xPubKey_44H_0H_0H); + var copayerId = Model.Copayer._xPubToCopayerId(TestData.copayers[0].xPubKey_44H_0H_0H); opts = { copayerId: copayerId, requestPubKey: requestPubKey, @@ -2424,7 +1650,7 @@ describe('Wallet service', function() { tx.isAccepted().should.equal.false; tx.isRejected().should.equal.false; tx.amount.should.equal(helpers.toSatoshi(80)); - var estimatedFee = WalletUtils.DEFAULT_FEE_PER_KB * 400 / 1000; // fully signed tx should have about 400 bytes + 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); @@ -2679,7 +1905,7 @@ describe('Wallet service', function() { var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 3.5, TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { should.not.exist(err); - var estimatedFee = WalletUtils.DEFAULT_FEE_PER_KB * 1300 / 1000; // fully signed tx should have about 1300 bytes + 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(); }); @@ -2703,7 +1929,7 @@ describe('Wallet service', function() { 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); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: tx.id, signatures: signatures, @@ -3075,7 +2301,7 @@ describe('Wallet service', function() { }); }, function(next) { - var clock = sinon.useFakeTimers(Date.now() + (WalletService.BACKOFF_TIME + 2) * 60 * 1000, 'Date'); + var clock = sinon.useFakeTimers(Date.now() + (Defaults.BACKOFF_TIME + 2) * 60 * 1000, 'Date'); var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { clock.restore(); @@ -3087,7 +2313,7 @@ describe('Wallet service', function() { }, function(next) { // Do not allow a 5th tx before backoff time - var clock = sinon.useFakeTimers(Date.now() + (WalletService.BACKOFF_TIME + 2) * 60 * 1000 + 1, 'Date'); + var clock = sinon.useFakeTimers(Date.now() + (Defaults.BACKOFF_TIME + 2) * 60 * 1000 + 1, 'Date'); var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { clock.restore(); @@ -3206,7 +2432,6 @@ describe('Wallet service', function() { server.createTx(txOpts, function(err, tx) { should.not.exist(err); should.exist(tx); - tx.derivationStrategy.should.equal('BIP44'); tx.addressType.should.equal('P2PKH'); txid = tx.id; done(); @@ -3220,7 +2445,7 @@ describe('Wallet service', function() { server.getPendingTxs({}, function(err, txs) { var tx = txs[0]; tx.id.should.equal(txid); - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); should.not.exist(tx.raw); server.signTx({ txProposalId: txid, @@ -3268,7 +2493,7 @@ describe('Wallet service', function() { var tx = txs[0]; tx.id.should.equal(txid); - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txid, signatures: signatures, @@ -3295,7 +2520,7 @@ describe('Wallet service', function() { server.getPendingTxs({}, function(err, txs) { var tx = txs[0]; tx.id.should.equal(txid); - var signatures = helpers.clientSign(tx, TestData.copayers[1].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[1].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txid, signatures: signatures, @@ -3311,7 +2536,7 @@ describe('Wallet service', function() { var tx = txs[0]; tx.id.should.equal(txid); - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); signatures[0] = 1; server.signTx({ @@ -3346,7 +2571,7 @@ describe('Wallet service', function() { var tx = txs[0]; tx.id.should.equal(txid); - var signatures = _.take(helpers.clientSign(tx, TestData.copayers[0].xPrivKey), tx.inputs.length - 1); + var signatures = _.take(helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H), tx.inputs.length - 1); server.signTx({ txProposalId: txid, signatures: signatures, @@ -3363,7 +2588,7 @@ describe('Wallet service', function() { var tx = txs[0]; tx.id.should.equal(txid); - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txid, signatures: signatures, @@ -3386,7 +2611,7 @@ describe('Wallet service', function() { server.rejectTx({ txProposalId: txid, }, function(err) { - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txid, signatures: signatures, @@ -3434,7 +2659,7 @@ describe('Wallet service', function() { txProposalId: txid }, function(err, tx) { should.not.exist(err); - var signatures = helpers.clientSign(tx, TestData.copayers[2].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[2].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txid, signatures: signatures, @@ -3464,7 +2689,7 @@ describe('Wallet service', function() { server.createTx(txOpts, function(err, txp) { should.not.exist(err); should.exist(txp); - var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txp.id, signatures: signatures, @@ -3714,7 +2939,7 @@ describe('Wallet service', function() { }); }, function(txp, next) { - var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txpId, signatures: signatures, @@ -3745,7 +2970,7 @@ describe('Wallet service', function() { }, function(txp, next) { helpers.getAuthServer(wallet.copayers[1].id, function(server, wallet) { - var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey); + var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txpId, signatures: signatures, @@ -4147,7 +3372,7 @@ describe('Wallet service', function() { server.getPendingTxs({}, function(err, txs) { blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, 'broadcast error'); var tx = txs[0]; - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: tx.id, signatures: signatures, @@ -4189,7 +3414,7 @@ describe('Wallet service', function() { it('should notify sign, acceptance, and broadcast, and emit', function(done) { server.getPendingTxs({}, function(err, txs) { var tx = txs[2]; - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: tx.id, signatures: signatures, @@ -4218,7 +3443,7 @@ 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]; - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: tx.id, signatures: signatures, @@ -4412,7 +3637,7 @@ 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); + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txp.id, signatures: signatures, @@ -4434,7 +3659,7 @@ describe('Wallet service', function() { async.waterfall([ function(next) { - var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: txp.id, signatures: signatures, @@ -4499,7 +3724,7 @@ 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); + var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); server2.signTx({ txProposalId: txp.id, signatures: signatures, @@ -4518,7 +3743,7 @@ 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); + var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); server2.rejectTx({ txProposalId: txp.id, signatures: signatures, @@ -4538,7 +3763,7 @@ 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); + var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); server2.signTx({ txProposalId: txp.id, signatures: signatures, @@ -4547,7 +3772,7 @@ describe('Wallet service', function() { server.getPendingTxs({}, function(err, txs) { should.not.exist(err); - txs[0].deleteLockTime.should.be.above(WalletService.DELETE_LOCKTIME - 10); + txs[0].deleteLockTime.should.be.above(Defaults.DELETE_LOCKTIME - 10); var clock = sinon.useFakeTimers(Date.now() + 1 + 24 * 3600 * 1000, 'Date'); server.removePendingTx({ @@ -4565,14 +3790,14 @@ 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); + var signatures = helpers.clientSign(txp, TestData.copayers[1].xPrivKey_44H_0H_0H); server2.signTx({ txProposalId: txp.id, signatures: signatures, }, function(err) { should.not.exist(err); - var clock = sinon.useFakeTimers(Date.now() + 2000 + WalletService.DELETE_LOCKTIME * 1000, 'Date'); + var clock = sinon.useFakeTimers(Date.now() + 2000 + Defaults.DELETE_LOCKTIME * 1000, 'Date'); server2.removePendingTx({ txProposalId: txp.id }, function(err) { @@ -4725,7 +3950,7 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(tx); - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: tx.id, signatures: signatures, @@ -4897,12 +4122,11 @@ describe('Wallet service', function() { describe('#scan', function() { var server, wallet; - var scanConfigOld = WalletService.SCAN_CONFIG; describe('1-of-1 wallet (BIP44 & P2PKH)', function() { beforeEach(function(done) { this.timeout(5000); - WalletService.SCAN_CONFIG.maxGap = 2; + Defaults.SCAN_ADDRESS_GAP = 2; helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; @@ -4910,9 +4134,7 @@ describe('Wallet service', function() { done(); }); }); - afterEach(function() { - WalletService.SCAN_CONFIG = scanConfigOld; - }); + afterEach(function() {}); it('should scan main addresses', function(done) { helpers.stubAddressActivity( @@ -5094,7 +4316,7 @@ describe('Wallet service', function() { beforeEach(function(done) { this.timeout(5000); - WalletService.SCAN_CONFIG.maxGap = 2; + Defaults.SCAN_ADDRESS_GAP = 2; helpers.createAndJoinWallet(1, 2, { supportBIP44AndP2PKH: false @@ -5104,9 +4326,7 @@ describe('Wallet service', function() { done(); }); }); - afterEach(function() { - WalletService.SCAN_CONFIG = scanConfigOld; - }); + afterEach(function() {}); it('should scan main addresses', function(done) { helpers.stubAddressActivity( @@ -5173,10 +4393,9 @@ describe('Wallet service', function() { describe('#startScan', function() { var server, wallet; - var scanConfigOld = WalletService.SCAN_CONFIG; beforeEach(function(done) { this.timeout(5000); - WalletService.SCAN_CONFIG.maxGap = 2; + Defaults.SCAN_ADDRESS_GAP = 2; helpers.createAndJoinWallet(1, 1, { supportBIP44AndP2PKH: false @@ -5187,7 +4406,6 @@ describe('Wallet service', function() { }); }); afterEach(function() { - WalletService.SCAN_CONFIG = scanConfigOld; server.messageBroker.removeAllListeners(); }); @@ -5245,7 +4463,7 @@ describe('Wallet service', function() { }); it('should start multiple asynchronous scans for different wallets', function(done) { helpers.stubAddressActivity(['3K2VWMXheGZ4qG35DyGjA2dLeKfaSr534A']); - WalletService.SCAN_CONFIG.scanWindow = 1; + Defaults.SCAN_ADDRESS_GAP = 1; var scans = 0; server.messageBroker.onMessage(function(n) { @@ -5321,7 +4539,7 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(tx); tx.amount.should.equal(helpers.toSatoshi(80)); - tx.fee.should.equal(WalletUtils.DEFAULT_FEE_PER_KB); + tx.fee.should.equal(Defaults.DEFAULT_FEE_PER_KB); done(); }); }); @@ -5375,7 +4593,7 @@ describe('Wallet service', function() { signature: 'dummy', clientVersion: 'bwc-0.0.40', }, function(err, server) { - var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: tx.id, signatures: signatures, @@ -5411,9 +4629,9 @@ describe('Wallet service', function() { should.not.exist(err); should.exist(tx); tx.amount.should.equal(helpers.toSatoshi(80)); - tx.fee.should.equal(WalletUtils.DEFAULT_FEE_PER_KB); + 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); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: tx.id, signatures: signatures, @@ -5452,7 +4670,7 @@ describe('Wallet service', function() { 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); + var signatures = helpers.clientSign(tx, TestData.copayers[0].xPrivKey_44H_0H_0H); server.signTx({ txProposalId: tx.id, signatures: signatures, diff --git a/test/models/address.js b/test/models/address.js index 7a87ae1..23bcbfc 100644 --- a/test/models/address.js +++ b/test/models/address.js @@ -8,25 +8,71 @@ var should = chai.should(); var Address = require('../../lib/model/address'); describe('Address', function() { - it('should create livenet address', function() { - var x = Address.create({ - address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg', - walletId: '123', - isChange: false, - path: 'm/0/1', - publicKeys: ['123', '456'], + describe('#create', function() { + it('should create livenet address', function() { + var x = Address.create({ + address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg', + walletId: '123', + isChange: false, + path: 'm/0/1', + publicKeys: ['123', '456'], + }); + should.exist(x.createdOn); + x.network.should.equal('livenet'); + }); + it('should create testnet address', function() { + var x = Address.create({ + address: 'mp5xaa4uBj16DJt1fuA3D9fejHuCzeb7hj', + walletId: '123', + isChange: false, + path: 'm/0/1', + publicKeys: ['123', '456'], + }); + x.network.should.equal('testnet'); }); - should.exist(x.createdOn); - x.network.should.equal('livenet'); }); - it('should create testnet address', function() { - var x = Address.create({ - address: 'mp5xaa4uBj16DJt1fuA3D9fejHuCzeb7hj', - walletId: '123', - isChange: false, - path: 'm/0/1', - publicKeys: ['123', '456'], + describe('#derive', function() { + it('should derive multi-sig P2SH address', function() { + var address = Address.derive('wallet-id', 'P2SH', [{ + xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1' + // PubKey(xPubKey/0/0) -> 03fe466ea829aa4c9a1c289f9ba61ebc26a61816500860c8d23f94aad9af152ecd + }, { + xPubKey: 'xpub68tpbrfk747AvDUCdtEUgK2yDPmtGKf7YXzEcUUqnF3jmAMeZgcpoZqgXwwoi8CpwDkyzVX6wxUktTw2wh9EhhVjh5S71MLL3FkZDGF5GeY' + // PubKey(xPubKey/0/0) -> 03162179906dbe6a67979d4f8f46ee1db6ff81715f465e6615a4f5969478ad2171 + }], 'm/0/0', 1, 'livenet', false); + should.exist(address); + address.walletId.should.equal('wallet-id'); + address.address.should.equal('3QN2CiSxcUsFuRxZJwXMNDQ2esnr5RXTvw'); + address.network.should.equal('livenet'); + address.isChange.should.be.false; + address.path.should.equal('m/0/0'); + address.type.should.equal('P2SH'); + }); + it('should derive 1-of-1 P2SH address', function() { + var address = Address.derive('wallet-id', 'P2SH', [{ + xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1' + // PubKey(xPubKey/0/0) -> 03fe466ea829aa4c9a1c289f9ba61ebc26a61816500860c8d23f94aad9af152ecd + }], 'm/0/0', 1, 'livenet', false); + should.exist(address); + address.walletId.should.equal('wallet-id'); + address.address.should.equal('3BY4K8dfsHryhWh2MJ6XHxxsRfcvPAyseH'); + address.network.should.equal('livenet'); + address.isChange.should.be.false; + address.path.should.equal('m/0/0'); + address.type.should.equal('P2SH'); + }); + it('should derive 1-of-1 P2PKH address', function() { + var address = Address.derive('wallet-id', 'P2PKH', [{ + xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1' + // PubKey(xPubKey/1/2) -> 0232c09a6edd8e2189628132d530c038e0b15b414cf3984e532358cbcfb83a7bd7 + }], 'm/1/2', 1, 'livenet', true); + should.exist(address); + address.walletId.should.equal('wallet-id'); + address.address.should.equal('1G4wgi9YzmSSwQaQVLXQ5HUVquQDgJf8oT'); + address.network.should.equal('livenet'); + address.isChange.should.be.true; + address.path.should.equal('m/1/2'); + address.type.should.equal('P2PKH'); }); - x.network.should.equal('testnet'); }); }); diff --git a/test/models/txproposal.js b/test/models/txproposal.js index bc9cf14..525cc0c 100644 --- a/test/models/txproposal.js +++ b/test/models/txproposal.js @@ -5,8 +5,7 @@ var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); var TxProposal = require('../../lib/model/txproposal'); -var Bitcore = require('bitcore-wallet-utils').Bitcore; -var WalletUtils = require('bitcore-wallet-utils'); +var Bitcore = require('bitcore-lib'); describe('TXProposal', function() { diff --git a/test/utils.js b/test/utils.js index a309281..c35d4d9 100644 --- a/test/utils.js +++ b/test/utils.js @@ -4,7 +4,7 @@ var _ = require('lodash'); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); -var Utils = require('../lib/utils'); +var Utils = require('../lib/common/utils'); describe('Utils', function() { describe('#checkRequired', function() { @@ -48,4 +48,87 @@ describe('Utils', function() { Utils.checkRequired(obj, 'name').should.be.false; }); }); + + describe('#hashMessage', function() { + it('should create a hash', function() { + var res = Utils.hashMessage('hola'); + res.toString('hex').should.equal('4102b8a140ec642feaa1c645345f714bc7132d4fd2f7f6202db8db305a96172f'); + }); + }); + + describe('#verifyMessage', function() { + it('should fail to verify a malformed signature', function() { + var res = Utils.verifyMessage('hola', 'badsignature', '02555a2d45e309c00cc8c5090b6ec533c6880ab2d3bc970b3943def989b3373f16'); + should.exist(res); + res.should.equal(false); + }); + it('should fail to verify a null signature', function() { + var res = Utils.verifyMessage('hola', null, '02555a2d45e309c00cc8c5090b6ec533c6880ab2d3bc970b3943def989b3373f16'); + should.exist(res); + res.should.equal(false); + }); + it('should fail to verify with wrong pubkey', function() { + var res = Utils.verifyMessage('hola', '3045022100d6186930e4cd9984e3168e15535e2297988555838ad10126d6c20d4ac0e74eb502201095a6319ea0a0de1f1e5fb50f7bf10b8069de10e0083e23dbbf8de9b8e02785', '02555a2d45e309c00cc8c5090b6ec533c6880ab2d3bc970b3943def989b3373f16'); + should.exist(res); + res.should.equal(false); + }); + it('should verify', function() { + var res = Utils.verifyMessage('hola', '3045022100d6186930e4cd9984e3168e15535e2297988555838ad10126d6c20d4ac0e74eb502201095a6319ea0a0de1f1e5fb50f7bf10b8069de10e0083e23dbbf8de9b8e02785', '03bec86ad4a8a91fe7c11ec06af27246ec55094db3d86098b7d8b2f12afe47627f'); + should.exist(res); + res.should.equal(true); + }); + }); + + describe('#formatAmount', function() { + it('should successfully format amount', function() { + var cases = [{ + args: [1, 'bit'], + expected: '0', + }, { + args: [1, 'btc'], + expected: '0.00', + }, { + args: [0, 'bit'], + expected: '0', + }, { + args: [12345678, 'bit'], + expected: '123,457', + }, { + args: [12345678, 'btc'], + expected: '0.123457', + }, { + args: [12345611, 'btc'], + expected: '0.123456', + }, { + args: [1234, 'btc'], + expected: '0.000012', + }, { + args: [1299, 'btc'], + expected: '0.000013', + }, { + args: [1234567899999, 'btc'], + expected: '12,345.679', + }, { + args: [12345678, 'bit', { + thousandsSeparator: '.' + }], + expected: '123.457', + }, { + args: [12345678, 'btc', { + decimalSeparator: ',' + }], + expected: '0,123457', + }, { + args: [1234567899999, 'btc', { + thousandsSeparator: ' ', + decimalSeparator: ',' + }], + expected: '12 345,679', + }, ]; + + _.each(cases, function(testCase) { + Utils.formatAmount.apply(this, testCase.args).should.equal(testCase.expected); + }); + }); + }); });