Browse Source

Merge pull request #393 from isocolsky/remove-bwu-dep

Remove BWU dependency
activeAddress
Matias Alejo Garcia 9 years ago
parent
commit
a1f58a6c8c
  1. 22
      lib/common/constants.js
  2. 40
      lib/common/defaults.js
  3. 7
      lib/common/index.js
  4. 97
      lib/common/utils.js
  5. 4
      lib/emailservice.js
  6. 47
      lib/model/address.js
  7. 17
      lib/model/addressmanager.js
  8. 25
      lib/model/copayer.js
  9. 82
      lib/model/txproposal.js
  10. 18
      lib/model/wallet.js
  11. 118
      lib/server.js
  12. 25
      lib/utils.js
  13. 14
      package.json
  14. 462
      test/integration/emailnotifications.js
  15. 419
      test/integration/helpers.js
  16. 914
      test/integration/server.js
  17. 46
      test/models/address.js
  18. 3
      test/models/txproposal.js
  19. 85
      test/utils.js

22
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;

40
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;

7
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;

97
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;

4
lib/emailservice.js

@ -10,7 +10,7 @@ var fs = require('fs');
var path = require('path'); var path = require('path');
var nodemailer = require('nodemailer'); var nodemailer = require('nodemailer');
var WalletUtils = require('bitcore-wallet-utils'); var Utils = require('./common/utils');
var Storage = require('./storage'); var Storage = require('./storage');
var MessageBroker = require('./messagebroker'); var MessageBroker = require('./messagebroker');
var Lock = require('./lock'); var Lock = require('./lock');
@ -227,7 +227,7 @@ EmailService.prototype._getDataForTemplate = function(notification, recipient, c
if (data.amount) { if (data.amount) {
try { try {
var unit = recipient.unit.toLowerCase(); 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) { } catch (ex) {
return cb(new Error('Could not format amount', ex)); return cb(new Error('Could not format amount', ex));
} }

47
lib/model/address.js

@ -1,7 +1,10 @@
'use strict'; 'use strict';
var WalletUtils = require('bitcore-wallet-utils'); var $ = require('preconditions').singleton();
var Bitcore = WalletUtils.Bitcore; var _ = require('lodash');
var Bitcore = require('bitcore-lib');
var Constants = require('../common/constants');
function Address() {}; function Address() {};
@ -18,7 +21,7 @@ Address.create = function(opts) {
x.path = opts.path; x.path = opts.path;
x.publicKeys = opts.publicKeys; x.publicKeys = opts.publicKeys;
x.network = Bitcore.Address(x.address).toObject().network; 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; x.hasActivity = undefined;
return x; return x;
}; };
@ -34,9 +37,45 @@ Address.fromObj = function(obj) {
x.isChange = obj.isChange; x.isChange = obj.isChange;
x.path = obj.path; x.path = obj.path;
x.publicKeys = obj.publicKeys; x.publicKeys = obj.publicKeys;
x.type = obj.type || WalletUtils.SCRIPT_TYPES.P2SH; x.type = obj.type || Constants.SCRIPT_TYPES.P2SH;
x.hasActivity = obj.hasActivity; x.hasActivity = obj.hasActivity;
return x; 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; module.exports = Address;

17
lib/model/addressmanager.js

@ -1,9 +1,8 @@
var _ = require('lodash'); var _ = require('lodash');
var $ = require('preconditions').singleton(); var $ = require('preconditions').singleton();
var WalletUtils = require('bitcore-wallet-utils'); var Bitcore = require('bitcore-lib');
var Constants = require('../common/constants');
var BIP45_SHARED_INDEX = 0x80000000 - 1;
function AddressManager() {}; function AddressManager() {};
@ -13,12 +12,12 @@ AddressManager.create = function(opts) {
var x = new AddressManager(); var x = new AddressManager();
x.version = 2; x.version = 2;
x.derivationStrategy = opts.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
$.checkState(_.contains(_.values(WalletUtils.DERIVATION_STRATEGIES), x.derivationStrategy)); $.checkState(_.contains(_.values(Constants.DERIVATION_STRATEGIES), x.derivationStrategy));
x.receiveAddressIndex = 0; x.receiveAddressIndex = 0;
x.changeAddressIndex = 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; return x;
}; };
@ -27,7 +26,7 @@ AddressManager.fromObj = function(obj) {
var x = new AddressManager(); var x = new AddressManager();
x.version = obj.version; 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.receiveAddressIndex = obj.receiveAddressIndex;
x.changeAddressIndex = obj.changeAddressIndex; x.changeAddressIndex = obj.changeAddressIndex;
x.copayerIndex = obj.copayerIndex; x.copayerIndex = obj.copayerIndex;
@ -36,7 +35,7 @@ AddressManager.fromObj = function(obj) {
}; };
AddressManager.supportsCopayerBranches = function(derivationStrategy) { AddressManager.supportsCopayerBranches = function(derivationStrategy) {
return derivationStrategy == WalletUtils.DERIVATION_STRATEGIES.BIP45; return derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45;
}; };
AddressManager.prototype._incrementIndex = function(isChange) { AddressManager.prototype._incrementIndex = function(isChange) {
@ -58,7 +57,7 @@ AddressManager.prototype.rewindIndex = function(isChange, n) {
AddressManager.prototype.getCurrentAddressPath = function(isChange) { AddressManager.prototype.getCurrentAddressPath = function(isChange) {
return 'm/' + return 'm/' +
(this.derivationStrategy == WalletUtils.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') + (this.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45 ? this.copayerIndex + '/' : '') +
(isChange ? 1 : 0) + '/' + (isChange ? 1 : 0) + '/' +
(isChange ? this.changeAddressIndex : this.receiveAddressIndex); (isChange ? this.changeAddressIndex : this.receiveAddressIndex);
}; };

25
lib/model/copayer.js

@ -3,17 +3,22 @@
var $ = require('preconditions').singleton(); var $ = require('preconditions').singleton();
var _ = require('lodash'); var _ = require('lodash');
var util = require('util'); var util = require('util');
var Uuid = require('uuid'); var Uuid = require('uuid');
var sjcl = require('sjcl');
var Address = require('./address'); var Address = require('./address');
var AddressManager = require('./addressmanager'); var AddressManager = require('./addressmanager');
var WalletUtils = require('bitcore-wallet-utils'); var Bitcore = require('bitcore-lib');
var Bitcore = WalletUtils.Bitcore;
var HDPublicKey = Bitcore.HDPublicKey; var Constants = require('../common/constants');
function Copayer() {}; function Copayer() {};
Copayer._xPubToCopayerId = function(xpub) {
var hash = sjcl.hash.sha256.hash(xpub);
return sjcl.codec.hex.fromBits(hash);
};
Copayer.create = function(opts) { Copayer.create = function(opts) {
opts = opts || {}; opts = opts || {};
$.checkArgument(opts.xPubKey, 'Missing copayer extended public key') $.checkArgument(opts.xPubKey, 'Missing copayer extended public key')
@ -27,7 +32,7 @@ Copayer.create = function(opts) {
x.version = 2; x.version = 2;
x.createdOn = Math.floor(Date.now() / 1000); x.createdOn = Math.floor(Date.now() / 1000);
x.xPubKey = opts.xPubKey; x.xPubKey = opts.xPubKey;
x.id = WalletUtils.xPubToCopayerId(x.xPubKey); x.id = Copayer._xPubToCopayerId(x.xPubKey);
x.name = opts.name; x.name = opts.name;
x.requestPubKey = opts.requestPubKey; x.requestPubKey = opts.requestPubKey;
x.signature = opts.signature; x.signature = opts.signature;
@ -36,7 +41,7 @@ Copayer.create = function(opts) {
signature: opts.signature, signature: opts.signature,
}]; }];
var derivationStrategy = opts.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; var derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
if (AddressManager.supportsCopayerBranches(derivationStrategy)) { if (AddressManager.supportsCopayerBranches(derivationStrategy)) {
x.addressManager = AddressManager.create({ x.addressManager = AddressManager.create({
derivationStrategy: derivationStrategy, derivationStrategy: derivationStrategy,
@ -82,13 +87,7 @@ Copayer.prototype.createAddress = function(wallet, isChange) {
$.checkState(wallet.isComplete()); $.checkState(wallet.isComplete());
var path = this.addressManager.getNewAddressPath(isChange); var path = this.addressManager.getNewAddressPath(isChange);
var raw = Address.create(WalletUtils.deriveAddress(wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.network)); var address = Address.derive(wallet.id, wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.network, isChange);
var address = Address.create(_.extend(raw, {
walletId: wallet.id,
type: wallet.addressType,
}));
address.isChange = isChange;
return address; return address;
}; };

82
lib/model/txproposal.js

@ -7,9 +7,11 @@ var log = require('npmlog');
log.debug = log.verbose; log.debug = log.verbose;
log.disableColor(); log.disableColor();
var WalletUtils = require('bitcore-wallet-utils'); var Bitcore = require('bitcore-lib');
var Bitcore = WalletUtils.Bitcore;
var Address = Bitcore.Address; var Common = require('../common');
var Constants = Common.Constants;
var Defaults = Common.Defaults;
var TxProposalAction = require('./txproposalaction'); var TxProposalAction = require('./txproposalaction');
@ -76,8 +78,7 @@ TxProposal.create = function(opts) {
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos; x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
x.proposalSignaturePubKey = opts.proposalSignaturePubKey; x.proposalSignaturePubKey = opts.proposalSignaturePubKey;
x.proposalSignaturePubKeySig = opts.proposalSignaturePubKeySig; x.proposalSignaturePubKeySig = opts.proposalSignaturePubKeySig;
x.derivationStrategy = opts.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH;
x.addressType = opts.addressType || WalletUtils.SCRIPT_TYPES.P2SH;
x.customData = opts.customData; x.customData = opts.customData;
if (_.isFunction(TxProposal._create[x.type])) { if (_.isFunction(TxProposal._create[x.type])) {
@ -125,8 +126,7 @@ TxProposal.fromObj = function(obj) {
x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos; x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos;
x.proposalSignaturePubKey = obj.proposalSignaturePubKey; x.proposalSignaturePubKey = obj.proposalSignaturePubKey;
x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig; x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig;
x.derivationStrategy = obj.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH;
x.addressType = obj.addressType || WalletUtils.SCRIPT_TYPES.P2SH;
x.customData = obj.customData; x.customData = obj.customData;
return x; 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() { TxProposal.prototype._getCurrentSignatures = function() {
var acceptedActions = _.filter(this.actions, { var acceptedActions = _.filter(this.actions, {
@ -164,7 +230,7 @@ TxProposal.prototype._getCurrentSignatures = function() {
TxProposal.prototype.getBitcoreTx = function() { TxProposal.prototype.getBitcoreTx = function() {
var self = this; var self = this;
var t = WalletUtils.buildTx(this); var t = this._buildTx();
var sigs = this._getCurrentSignatures(); var sigs = this._getCurrentSignatures();
_.each(sigs, function(x) { _.each(sigs, function(x) {

18
lib/model/wallet.js

@ -8,7 +8,8 @@ var Uuid = require('uuid');
var Address = require('./address'); var Address = require('./address');
var Copayer = require('./copayer'); var Copayer = require('./copayer');
var AddressManager = require('./addressmanager'); var AddressManager = require('./addressmanager');
var WalletUtils = require('bitcore-wallet-utils');
var Constants = require('../common/constants');
function Wallet() {}; function Wallet() {};
@ -29,8 +30,8 @@ Wallet.create = function(opts) {
x.copayers = []; x.copayers = [];
x.pubKey = opts.pubKey; x.pubKey = opts.pubKey;
x.network = opts.network; x.network = opts.network;
x.derivationStrategy = opts.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
x.addressType = opts.addressType || WalletUtils.SCRIPT_TYPES.P2SH; x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH;
x.addressManager = AddressManager.create({ x.addressManager = AddressManager.create({
derivationStrategy: x.derivationStrategy, derivationStrategy: x.derivationStrategy,
@ -56,8 +57,8 @@ Wallet.fromObj = function(obj) {
}); });
x.pubKey = obj.pubKey; x.pubKey = obj.pubKey;
x.network = obj.network; x.network = obj.network;
x.derivationStrategy = obj.derivationStrategy || WalletUtils.DERIVATION_STRATEGIES.BIP45; x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
x.addressType = obj.addressType || WalletUtils.SCRIPT_TYPES.P2SH; x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH;
x.addressManager = AddressManager.fromObj(obj.addressManager); x.addressManager = AddressManager.fromObj(obj.addressManager);
x.scanStatus = obj.scanStatus; x.scanStatus = obj.scanStatus;
@ -153,12 +154,7 @@ Wallet.prototype.createAddress = function(isChange) {
var self = this; var self = this;
var path = this.addressManager.getNewAddressPath(isChange); var path = this.addressManager.getNewAddressPath(isChange);
var raw = WalletUtils.deriveAddress(this.addressType, this.publicKeyRing, path, this.m, this.network); var address = Address.derive(self.id, this.addressType, this.publicKeyRing, path, this.m, this.network, isChange);
var address = Address.create(_.extend(raw, {
walletId: self.id,
type: self.addressType,
}));
address.isChange = isChange;
return address; return address;
}; };

118
lib/server.js

@ -6,17 +6,18 @@ var log = require('npmlog');
log.debug = log.verbose; log.debug = log.verbose;
log.disableColor(); log.disableColor();
var EmailValidator = require('email-validator'); var EmailValidator = require('email-validator');
var Stringify = require('json-stable-stringify');
var WalletUtils = require('bitcore-wallet-utils'); var Bitcore = require('bitcore-lib');
var Bitcore = WalletUtils.Bitcore;
var PublicKey = Bitcore.PublicKey; var Common = require('./common');
var HDPublicKey = Bitcore.HDPublicKey; var Utils = Common.Utils;
var Address = Bitcore.Address; var Constants = Common.Constants;
var Defaults = Common.Defaults;
var ClientError = require('./errors/clienterror'); var ClientError = require('./errors/clienterror');
var Errors = require('./errors/errordefinitions'); var Errors = require('./errors/errordefinitions');
var Utils = require('./utils');
var Lock = require('./lock'); var Lock = require('./lock');
var Storage = require('./storage'); var Storage = require('./storage');
var MessageBroker = require('./messagebroker'); 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 * Gets the current version of BWS
*/ */
@ -234,11 +202,11 @@ WalletService.prototype.createWallet = function(opts, cb) {
opts.supportBIP44AndP2PKH = _.isBoolean(opts.supportBIP44AndP2PKH) ? opts.supportBIP44AndP2PKH : true; opts.supportBIP44AndP2PKH = _.isBoolean(opts.supportBIP44AndP2PKH) ? opts.supportBIP44AndP2PKH : true;
var derivationStrategy = opts.supportBIP44AndP2PKH ? WalletUtils.DERIVATION_STRATEGIES.BIP44 : WalletUtils.DERIVATION_STRATEGIES.BIP45; var derivationStrategy = opts.supportBIP44AndP2PKH ? Constants.DERIVATION_STRATEGIES.BIP44 : Constants.DERIVATION_STRATEGIES.BIP45;
var addressType = (opts.n == 1 && opts.supportBIP44AndP2PKH) ? WalletUtils.SCRIPT_TYPES.P2PKH : WalletUtils.SCRIPT_TYPES.P2SH; var addressType = (opts.n == 1 && opts.supportBIP44AndP2PKH) ? Constants.SCRIPT_TYPES.P2PKH : Constants.SCRIPT_TYPES.P2SH;
try { try {
pubKey = new PublicKey.fromString(opts.pubKey); pubKey = new Bitcore.PublicKey.fromString(opts.pubKey);
} catch (ex) { } catch (ex) {
return cb(new ClientError('Invalid public key')); return cb(new ClientError('Invalid public key'));
}; };
@ -354,6 +322,7 @@ WalletService.prototype.getStatus = function(opts, cb) {
}); });
}; };
/* /*
* Verifies a signature * Verifies a signature
* @param text * @param text
@ -361,10 +330,21 @@ WalletService.prototype.getStatus = function(opts, cb) {
* @param pubKeys * @param pubKeys
*/ */
WalletService.prototype._verifySignature = function(text, signature, pubkey) { 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 * Verifies signature againt a collection of pubkeys
* @param text * @param text
@ -517,11 +497,11 @@ WalletService.prototype.addAccess = function(opts, cb) {
id: opts.copayerId id: opts.copayerId
}).xPubKey; }).xPubKey;
if (!WalletUtils.verifyRequestPubKey(opts.requestPubKey, opts.signature, xPubKey)) { if (!self._verifyRequestPubKey(opts.requestPubKey, opts.signature, xPubKey)) {
return cb(Errors.NOT_AUTHORIZED); 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); return cb(Errors.TOO_MANY_KEYS);
self._addKeyToCopayer(wallet, copayer, opts, cb); self._addKeyToCopayer(wallet, copayer, opts, cb);
@ -563,6 +543,10 @@ WalletService.prototype._clientSupportsTXPv2 = function() {
return true; return true;
}; };
WalletService._getCopayerHash = function(name, xPubKey, requestPubKey) {
return [name, xPubKey, requestPubKey].join('|');
};
/** /**
* Joins a wallet in creation. * Joins a wallet in creation.
* @param {Object} opts * @param {Object} opts
@ -593,17 +577,17 @@ WalletService.prototype.joinWallet = function(opts, cb) {
if (opts.supportBIP44AndP2PKH) { if (opts.supportBIP44AndP2PKH) {
// New client trying to join legacy wallet // 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.')); return cb(new ClientError('The wallet you are trying to join was created with an older version of the client app.'));
} }
} else { } else {
// Legacy client trying to join new wallet // 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.')); 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)) { if (!self._verifySignature(hash, opts.copayerSignature, wallet.pubKey)) {
return cb(new ClientError()); return cb(new ClientError());
} }
@ -701,8 +685,8 @@ WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) {
if (err) return cb(err); if (err) return cb(err);
var latestAddresses = _.takeRight(_.reject(addresses, { var latestAddresses = _.takeRight(_.reject(addresses, {
isChange: true isChange: true
}), WalletService.MAX_MAIN_ADDRESS_GAP); }), Defaults.MAX_MAIN_ADDRESS_GAP);
if (latestAddresses.length < WalletService.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, { if (latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, {
hasActivity: true hasActivity: true
})) return cb(null, true); })) return cb(null, true);
@ -1021,7 +1005,7 @@ WalletService.prototype.getFeeLevels = function(opts, cb) {
if (network != 'livenet' && network != 'testnet') if (network != 'livenet' && network != 'testnet')
return cb(new ClientError('Invalid network')); return cb(new ClientError('Invalid network'));
var levels = WalletService.FEE_LEVELS; var levels = Defaults.FEE_LEVELS;
var samplePoints = _.uniq(_.pluck(levels, 'nbBlocks')); var samplePoints = _.uniq(_.pluck(levels, 'nbBlocks'));
self._sampleFeeLevels(network, samplePoints, function(err, feeSamples) { self._sampleFeeLevels(network, samplePoints, function(err, feeSamples) {
var values = _.map(levels, function(level) { var values = _.map(levels, function(level) {
@ -1144,7 +1128,7 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) {
WalletService.prototype._canCreateTx = function(copayerId, cb) { WalletService.prototype._canCreateTx = function(copayerId, cb) {
var self = this; 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 (err) return cb(err);
if (!txs.length) if (!txs.length)
@ -1154,7 +1138,7 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) {
status: 'rejected' status: 'rejected'
}); });
var exceededRejections = lastRejections.length - WalletService.BACKOFF_OFFSET; var exceededRejections = lastRejections.length - Defaults.BACKOFF_OFFSET;
if (exceededRejections <= 0) if (exceededRejections <= 0)
return cb(null, true); return cb(null, true);
@ -1162,7 +1146,7 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) {
var lastTxTs = txs[0].createdOn; var lastTxTs = txs[0].createdOn;
var now = Math.floor(Date.now() / 1000); var now = Math.floor(Date.now() / 1000);
var timeSinceLastRejection = now - lastTxTs; 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) if (timeSinceLastRejection <= backoffTime)
log.debug('Not allowing to create TX: timeSinceLastRejection/backoffTime', 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. * Creates a new transaction proposal.
* @param {Object} opts * @param {Object} opts
@ -1212,8 +1209,8 @@ WalletService.prototype.createTx = function(opts, cb) {
valid: false valid: false
})) return; })) return;
var feePerKb = opts.feePerKb || WalletUtils.DEFAULT_FEE_PER_KB; var feePerKb = opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB;
if (feePerKb < WalletUtils.MIN_FEE_PER_KB || feePerKb > WalletUtils.MAX_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')); return cb(new ClientError('Invalid fee per KB value'));
self._runLocked(cb, function(cb) { self._runLocked(cb, function(cb) {
@ -1224,7 +1221,7 @@ WalletService.prototype.createTx = function(opts, cb) {
var copayer = wallet.getCopayer(self.copayerId); var copayer = wallet.getCopayer(self.copayerId);
var hash; var hash;
if (!opts.type || opts.type == Model.TxProposal.Types.SIMPLE) { 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 { } else {
// should match bwc api _computeProposalSignature // should match bwc api _computeProposalSignature
var header = { var header = {
@ -1234,7 +1231,7 @@ WalletService.prototype.createTx = function(opts, cb) {
message: opts.message, message: opts.message,
payProUrl: opts.payProUrl payProUrl: opts.payProUrl
}; };
hash = WalletUtils.getProposalHash(header) hash = WalletService._getProposalHash(header)
} }
var signingKey = self._getSigningKey(hash, opts.proposalSignature, copayer.requestPubKeys) 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), requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1),
walletN: wallet.n, walletN: wallet.n,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos, excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
derivationStrategy: wallet.derivationStrategy,
addressType: wallet.addressType, addressType: wallet.addressType,
customData: opts.customData, customData: opts.customData,
}; };
@ -1363,7 +1359,7 @@ WalletService.prototype.removeWallet = function(opts, cb) {
WalletService.prototype.getRemainingDeleteLockTime = function(txp) { WalletService.prototype.getRemainingDeleteLockTime = function(txp) {
var now = Math.floor(Date.now() / 1000); 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) if (lockTimeRemaining < 0)
return 0; return 0;
@ -1943,7 +1939,7 @@ WalletService.prototype.scan = function(opts, cb) {
function scanBranch(derivator, cb) { function scanBranch(derivator, cb) {
var inactiveCounter = 0; var inactiveCounter = 0;
var allAddresses = []; var allAddresses = [];
var gap = WalletService.SCAN_CONFIG.maxGap; var gap = Defaults.SCAN_ADDRESS_GAP;
async.whilst(function() { async.whilst(function() {
return inactiveCounter < gap; return inactiveCounter < gap;

25
lib/utils.js

@ -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;

14
package.json

@ -20,12 +20,12 @@
"dependencies": { "dependencies": {
"async": "^0.9.0", "async": "^0.9.0",
"bitcore-lib": "^0.13.7", "bitcore-lib": "^0.13.7",
"bitcore-wallet-utils": "~1.0.0",
"body-parser": "^1.11.0", "body-parser": "^1.11.0",
"coveralls": "^2.11.2", "coveralls": "^2.11.2",
"email-validator": "^1.0.1", "email-validator": "^1.0.1",
"express": "^4.10.0", "express": "^4.10.0",
"inherits": "^2.0.1", "inherits": "^2.0.1",
"json-stable-stringify": "^1.0.0",
"locker": "^0.1.0", "locker": "^0.1.0",
"locker-server": "^0.1.3", "locker-server": "^0.1.3",
"lodash": "^3.10.1", "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" "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", "bitcoreNode": "./bitcorenode",
"contributors": [{ "contributors": [
{
"name": "Braydon Fuller", "name": "Braydon Fuller",
"email": "braydon@bitpay.com" "email": "braydon@bitpay.com"
}, { },
{
"name": "Ivan Socolsky", "name": "Ivan Socolsky",
"email": "ivan@bitpay.com" "email": "ivan@bitpay.com"
}, { },
{
"name": "Matias Alejo Garcia", "name": "Matias Alejo Garcia",
"email": "ematiu@gmail.com" "email": "ematiu@gmail.com"
}] }
]
} }

462
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, '<html><body>{{walletName}}</body></html>');
} 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('<html>').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, '<html>{{&urlForTx}}<html>');
} 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);
});
});
});
});
});
});

419
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;

914
test/integration/server.js

File diff suppressed because it is too large

46
test/models/address.js

@ -8,6 +8,7 @@ var should = chai.should();
var Address = require('../../lib/model/address'); var Address = require('../../lib/model/address');
describe('Address', function() { describe('Address', function() {
describe('#create', function() {
it('should create livenet address', function() { it('should create livenet address', function() {
var x = Address.create({ var x = Address.create({
address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg', address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg',
@ -30,3 +31,48 @@ describe('Address', function() {
x.network.should.equal('testnet'); x.network.should.equal('testnet');
}); });
}); });
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');
});
});
});

3
test/models/txproposal.js

@ -5,8 +5,7 @@ var chai = require('chai');
var sinon = require('sinon'); var sinon = require('sinon');
var should = chai.should(); var should = chai.should();
var TxProposal = require('../../lib/model/txproposal'); var TxProposal = require('../../lib/model/txproposal');
var Bitcore = require('bitcore-wallet-utils').Bitcore; var Bitcore = require('bitcore-lib');
var WalletUtils = require('bitcore-wallet-utils');
describe('TXProposal', function() { describe('TXProposal', function() {

85
test/utils.js

@ -4,7 +4,7 @@ var _ = require('lodash');
var chai = require('chai'); var chai = require('chai');
var sinon = require('sinon'); var sinon = require('sinon');
var should = chai.should(); var should = chai.should();
var Utils = require('../lib/utils'); var Utils = require('../lib/common/utils');
describe('Utils', function() { describe('Utils', function() {
describe('#checkRequired', function() { describe('#checkRequired', function() {
@ -48,4 +48,87 @@ describe('Utils', function() {
Utils.checkRequired(obj, 'name').should.be.false; 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);
});
});
});
}); });

Loading…
Cancel
Save