Browse Source

Merge pull request #688 from isocolsky/feat/cash

Bitcoin Cash support
feat/estimateFee-limit
Matias Alejo Garcia 8 years ago
committed by GitHub
parent
commit
e2edfd394f
  1. 2
      .travis.yml
  2. 30
      config.js
  3. 20
      lib/blockchainexplorer.js
  4. 10
      lib/blockchainexplorers/insight.js
  5. 63
      lib/blockchainmonitor.js
  6. 5
      lib/common/constants.js
  7. 55
      lib/common/defaults.js
  8. 10
      lib/common/utils.js
  9. 3
      lib/expressapp.js
  10. 31
      lib/model/address.js
  11. 4
      lib/model/addressmanager.js
  12. 18
      lib/model/copayer.js
  13. 40
      lib/model/txproposal.js
  14. 5
      lib/model/txproposal_legacy.js
  15. 16
      lib/model/wallet.js
  16. 226
      lib/server.js
  17. 62
      lib/storage.js
  18. 25
      package.json
  19. 6
      test/integration/bcmonitor.js
  20. 4
      test/integration/emailnotifications.js
  21. 33
      test/integration/helpers.js
  22. 4
      test/integration/pushNotifications.js
  23. 160
      test/integration/server.js
  24. 8
      test/model/address.js
  25. 3
      test/model/copayer.js
  26. 11
      test/model/txproposal.js
  27. 10
      test/storage.js
  28. 37
      test/testdata.js

2
.travis.yml

@ -12,7 +12,7 @@ addons:
- g++-4.8
- clang
node_js:
- '4'
- '8'
before_install:
- export CXX="g++-4.8" CC="gcc-4.8"
install:

30
config.js

@ -38,16 +38,28 @@ var config = {
},
},
blockchainExplorerOpts: {
livenet: {
provider: 'insight',
url: 'https://insight.bitpay.com:443',
btc: {
livenet: {
provider: 'insight',
url: 'https://insight.bitpay.com:443',
},
testnet: {
provider: 'insight',
url: 'https://test-insight.bitpay.com:443',
// url: 'http://localhost:3001',
// Multiple servers (in priority order)
// url: ['http://a.b.c', 'https://test-insight.bitpay.com:443'],
},
},
testnet: {
provider: 'insight',
url: 'https://test-insight.bitpay.com:443',
// url: 'http://localhost:3001',
// Multiple servers (in priority order)
// url: ['http://a.b.c', 'https://test-insight.bitpay.com:443'],
bch: {
livenet: {
provider: 'insight',
url: 'https://cashexplorer.bitcoin.com',
},
testnet: {
provider: 'insight',
url: '',
},
},
},
pushNotificationsOpts: {

20
lib/blockchainexplorer.js

@ -6,11 +6,20 @@ var log = require('npmlog');
log.debug = log.verbose;
var Insight = require('./blockchainexplorers/insight');
var Common = require('./common');
var Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
var PROVIDERS = {
'insight': {
'livenet': 'https://insight.bitpay.com:443',
'testnet': 'https://test-insight.bitpay.com:443',
'btc': {
'livenet': 'https://insight.bitpay.com:443',
'testnet': 'https://test-insight.bitpay.com:443',
},
'bch': {
'livenet': 'https://insight.bitpay.com:443',
},
},
};
@ -18,16 +27,19 @@ function BlockChainExplorer(opts) {
$.checkArgument(opts);
var provider = opts.provider || 'insight';
var coin = opts.coin || Defaults.COIN;
var network = opts.network || 'livenet';
$.checkState(PROVIDERS[provider], 'Provider ' + provider + ' not supported');
$.checkState(_.contains(_.keys(PROVIDERS[provider]), network), 'Network ' + network + ' not supported by this provider');
$.checkState(_.contains(_.keys(PROVIDERS[provider]), coin), 'Coin ' + coin + ' not supported by this provider');
$.checkState(_.contains(_.keys(PROVIDERS[provider][coin]), network), 'Network ' + network + ' not supported by this provider for coin ' + coin);
var url = opts.url || PROVIDERS[provider][network];
var url = opts.url || PROVIDERS[provider][coin][network];
switch (provider) {
case 'insight':
return new Insight({
coin: coin,
network: network,
url: url,
apiPrefix: opts.apiPrefix,

10
lib/blockchainexplorers/insight.js

@ -6,13 +6,19 @@ var log = require('npmlog');
log.debug = log.verbose;
var io = require('socket.io-client');
var requestList = require('./request-list');
var Common = require('../common');
var Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
function Insight(opts) {
$.checkArgument(opts);
$.checkArgument(_.contains(['livenet', 'testnet'], opts.network));
$.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS));
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
$.checkArgument(opts.url);
this.apiPrefix = opts.apiPrefix || '/api';
this.coin = opts.coin || Defaults.COIN;
this.network = opts.network || 'livenet';
this.hosts = opts.url;
this.userAgent = opts.userAgent || 'bws';
@ -39,7 +45,7 @@ Insight.prototype._doRequest = function(args, cb) {
};
Insight.prototype.getConnectionInfo = function() {
return 'Insight (' + this.network + ') @ ' + this.hosts;
return 'Insight (' + this.coin + '/' + this.network + ') @ ' + this.hosts;
};
/**

63
lib/blockchainmonitor.js

@ -14,6 +14,7 @@ var Lock = require('./lock');
var Notification = require('./model/notification');
var WalletService = require('./server');
var Constants = require('./common/constants');
function BlockchainMonitor() {};
@ -25,26 +26,42 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
async.parallel([
function(done) {
self.explorers = {};
_.map(['livenet', 'testnet'], function(network) {
self.explorers = {
btc: {},
bch: {},
};
var coinNetworkPairs = [];
_.each(_.values(Constants.COINS), function(coin) {
_.each(_.values(Constants.NETWORKS), function(network) {
coinNetworkPairs.push({
coin: coin,
network: network
});
});
});
_.each(coinNetworkPairs, function(pair) {
var explorer;
if (opts.blockchainExplorers) {
explorer = opts.blockchainExplorers[network];
if (opts.blockchainExplorers && opts.blockchainExplorers[pair.coin] && opts.blockchainExplorers[pair.coin][pair.network]) {
explorer = opts.blockchainExplorers[pair.coin][pair.network];
} else {
var config = {}
if (opts.blockchainExplorerOpts && opts.blockchainExplorerOpts[network]) {
config = opts.blockchainExplorerOpts[network];
if (opts.blockchainExplorerOpts && opts.blockchainExplorerOpts[pair.coin] && opts.blockchainExplorerOpts[pair.coin][pair.network]) {
config = opts.blockchainExplorerOpts[pair.coin][pair.network];
} else {
return;
}
var explorer = new BlockchainExplorer({
provider: config.provider,
network: network,
coin: pair.coin,
network: pair.network,
url: config.url,
userAgent: WalletService.getServiceVersion(),
});
}
$.checkState(explorer);
self._initExplorer(network, explorer);
self.explorers[network] = explorer;
self._initExplorer(pair.coin, pair.network, explorer);
self.explorers[pair.coin][pair.network] = explorer;
});
done();
},
@ -73,7 +90,7 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
});
};
BlockchainMonitor.prototype._initExplorer = function(network, explorer) {
BlockchainMonitor.prototype._initExplorer = function(coin, network, explorer) {
var self = this;
var socket = explorer.initSocket();
@ -85,8 +102,8 @@ BlockchainMonitor.prototype._initExplorer = function(network, explorer) {
socket.on('connect_error', function() {
log.error('Error connecting to ' + explorer.getConnectionInfo());
});
socket.on('tx', _.bind(self._handleIncomingTx, self));
socket.on('block', _.bind(self._handleNewBlock, self, network));
socket.on('tx', _.bind(self._handleIncomingTx, self, coin, network));
socket.on('block', _.bind(self._handleNewBlock, self, coin, network));
};
BlockchainMonitor.prototype._handleThirdPartyBroadcasts = function(data, processIt) {
@ -133,7 +150,7 @@ BlockchainMonitor.prototype._handleThirdPartyBroadcasts = function(data, process
});
};
BlockchainMonitor.prototype._handleIncomingPayments = function(data) {
BlockchainMonitor.prototype._handleIncomingPayments = function(coin, network, data) {
var self = this;
if (!data || !data.vout) return;
@ -149,7 +166,7 @@ BlockchainMonitor.prototype._handleIncomingPayments = function(data) {
if (_.isEmpty(outs)) return;
async.each(outs, function(out, next) {
self.storage.fetchAddress(out.address, function(err, address) {
self.storage.fetchAddressByCoin(coin, out.address, function(err, address) {
if (err) {
log.error('Could not fetch addresses from the db');
return next(err);
@ -202,12 +219,12 @@ BlockchainMonitor.prototype._updateActiveAddresses = function(address, cb) {
});
};
BlockchainMonitor.prototype._handleIncomingTx = function(data) {
BlockchainMonitor.prototype._handleIncomingTx = function(coin, network, data) {
this._handleThirdPartyBroadcasts(data);
this._handleIncomingPayments(data);
this._handleIncomingPayments(coin, network, data);
};
BlockchainMonitor.prototype._notifyNewBlock = function(network, hash) {
BlockchainMonitor.prototype._notifyNewBlock = function(coin, network, hash) {
var self = this;
log.info('New ' + network + ' block: ' + hash);
@ -216,6 +233,7 @@ BlockchainMonitor.prototype._notifyNewBlock = function(network, hash) {
walletId: network, // use network name as wallet id for global notifications
data: {
hash: hash,
coin: coin,
network: network,
},
});
@ -227,7 +245,7 @@ BlockchainMonitor.prototype._notifyNewBlock = function(network, hash) {
});
};
BlockchainMonitor.prototype._handleTxConfirmations = function(network, hash) {
BlockchainMonitor.prototype._handleTxConfirmations = function(coin, network, hash) {
var self = this;
function processTriggeredSubs(subs, cb) {
@ -243,6 +261,7 @@ BlockchainMonitor.prototype._handleTxConfirmations = function(network, hash) {
creatorId: sub.copayerId,
data: {
txid: sub.txid,
coin: coin,
network: network,
// TODO: amount
},
@ -252,7 +271,7 @@ BlockchainMonitor.prototype._handleTxConfirmations = function(network, hash) {
});
};
var explorer = self.explorers[network];
var explorer = self.explorers[coin][network];
if (!explorer) return;
explorer.getTxidsInBlock(hash, function(err, txids) {
@ -279,9 +298,9 @@ BlockchainMonitor.prototype._handleTxConfirmations = function(network, hash) {
});
};
BlockchainMonitor.prototype._handleNewBlock = function(network, hash) {
this._notifyNewBlock(network, hash);
this._handleTxConfirmations(network, hash);
BlockchainMonitor.prototype._handleNewBlock = function(coin, network, hash) {
this._notifyNewBlock(coin, network, hash);
this._handleTxConfirmations(coin, network, hash);
};
BlockchainMonitor.prototype._storeAndBroadcastNotification = function(notification, cb) {

5
lib/common/constants.js

@ -2,6 +2,11 @@
var Constants = {};
Constants.COINS = {
BTC: 'btc',
BCH: 'bch',
};
Constants.NETWORKS = {
LIVENET: 'livenet',
TESTNET: 'testnet',

55
lib/common/defaults.js

@ -24,30 +24,35 @@ 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 + 20;
Defaults.FEE_LEVELS = [{
name: 'urgent',
nbBlocks: 2,
multiplier: 1.5,
defaultValue: 150000,
}, {
name: 'priority',
nbBlocks: 2,
defaultValue: 100000
}, {
name: 'normal',
nbBlocks: 3,
defaultValue: 80000
}, {
name: 'economy',
nbBlocks: 6,
defaultValue: 50000
}, {
name: 'superEconomy',
nbBlocks: 24,
defaultValue: 20000
}];
Defaults.DEFAULT_FEE_PER_KB = Defaults.FEE_LEVELS[1].defaultValue;
Defaults.FEE_LEVELS = {
btc: [{
name: 'urgent',
nbBlocks: 2,
multiplier: 1.5,
defaultValue: 150000,
}, {
name: 'priority',
nbBlocks: 2,
defaultValue: 100000
}, {
name: 'normal',
nbBlocks: 3,
defaultValue: 80000
}, {
name: 'economy',
nbBlocks: 6,
defaultValue: 50000
}, {
name: 'superEconomy',
nbBlocks: 24,
defaultValue: 20000
}],
bch: [{
name: 'normal',
nbBlocks: 2,
defaultValue: 2000,
}]
};
// How many levels to fallback to if the value returned by the network for a given nbBlocks is -1
Defaults.FEE_LEVELS_FALLBACK = 2;
@ -109,4 +114,6 @@ Defaults.RateLimit = {
// },
};
Defaults.COIN = 'btc';
module.exports = Defaults;

10
lib/common/utils.js

@ -64,7 +64,7 @@ Utils._tryImportPublicKey = function(publicKey) {
publicKeyBuffer = new Buffer(publicKey, 'hex');
}
return publicKeyBuffer;
} catch(e) {
} catch (e) {
return false;
}
};
@ -76,7 +76,7 @@ Utils._tryImportSignature = function(signature) {
signatureBuffer = new Buffer(signature, 'hex');
}
return secp256k1.signatureImport(signatureBuffer);
} catch(e) {
} catch (e) {
return false;
}
};
@ -84,7 +84,7 @@ Utils._tryImportSignature = function(signature) {
Utils._tryVerifyMessage = function(hash, sig, publicKeyBuffer) {
try {
return secp256k1.verify(hash, sig, publicKeyBuffer);
} catch(e) {
} catch (e) {
return false;
}
};
@ -176,5 +176,9 @@ Utils.parseVersion = function(version) {
return v;
};
Utils.checkValueInCollection = function(value, collection) {
if (!value || !_.isString(value)) return false;
return _.contains(_.values(collection), value);
};
module.exports = Utils;

3
lib/expressapp.js

@ -201,7 +201,6 @@ ExpressApp.prototype.start = function(opts, cb) {
}
// DEPRECATED
router.post('/v1/wallets/', createWalletLimiter, function(req, res) {
logDeprecated(req);
var server;
@ -454,7 +453,9 @@ ExpressApp.prototype.start = function(opts, cb) {
router.get('/v2/feelevels/', function(req, res) {
var opts = {};
if (req.query.coin) opts.coin = req.query.coin;
if (req.query.network) opts.network = req.query.network;
var server;
try {
server = getServer(req, res);

31
lib/model/address.js

@ -3,8 +3,14 @@
var $ = require('preconditions').singleton();
var _ = require('lodash');
var Bitcore = require('bitcore-lib');
var Constants = require('../common/constants');
var Bitcore = {
'btc': require('bitcore-lib'),
'bch': require('bitcore-lib-cash'),
};
var Common = require('../common');
var Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
function Address() {};
@ -13,6 +19,8 @@ Address.create = function(opts) {
var x = new Address();
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
x.version = '1.0.0';
x.createdOn = Math.floor(Date.now() / 1000);
x.address = opts.address;
@ -20,7 +28,8 @@ Address.create = function(opts) {
x.isChange = opts.isChange;
x.path = opts.path;
x.publicKeys = opts.publicKeys;
x.network = Bitcore.Address(x.address).toObject().network;
x.coin = opts.coin;
x.network = Bitcore[opts.coin].Address(x.address).toObject().network;
x.type = opts.type || Constants.SCRIPT_TYPES.P2SH;
x.hasActivity = undefined;
return x;
@ -33,6 +42,7 @@ Address.fromObj = function(obj) {
x.createdOn = obj.createdOn;
x.address = obj.address;
x.walletId = obj.walletId;
x.coin = obj.coin || Defaults.COIN;
x.network = obj.network;
x.isChange = obj.isChange;
x.path = obj.path;
@ -42,22 +52,22 @@ Address.fromObj = function(obj) {
return x;
};
Address._deriveAddress = function(scriptType, publicKeyRing, path, m, network) {
$.checkArgument(_.contains(_.values(Constants.SCRIPT_TYPES), scriptType));
Address._deriveAddress = function(scriptType, publicKeyRing, path, m, coin, network) {
$.checkArgument(Utils.checkValueInCollection(scriptType, Constants.SCRIPT_TYPES));
var publicKeys = _.map(publicKeyRing, function(item) {
var xpub = new Bitcore.HDPublicKey(item.xPubKey);
var xpub = new Bitcore[coin].HDPublicKey(item.xPubKey);
return xpub.deriveChild(path).publicKey;
});
var bitcoreAddress;
switch (scriptType) {
case Constants.SCRIPT_TYPES.P2SH:
bitcoreAddress = Bitcore.Address.createMultisig(publicKeys, m, network);
bitcoreAddress = Bitcore[coin].Address.createMultisig(publicKeys, m, network);
break;
case Constants.SCRIPT_TYPES.P2PKH:
$.checkState(_.isArray(publicKeys) && publicKeys.length == 1);
bitcoreAddress = Bitcore.Address.fromPublicKey(publicKeys[0], network);
bitcoreAddress = Bitcore[coin].Address.fromPublicKey(publicKeys[0], network);
break;
}
@ -68,9 +78,10 @@ Address._deriveAddress = function(scriptType, publicKeyRing, path, m, network) {
};
};
Address.derive = function(walletId, scriptType, publicKeyRing, path, m, network, isChange) {
var raw = Address._deriveAddress(scriptType, publicKeyRing, path, m, network);
Address.derive = function(walletId, scriptType, publicKeyRing, path, m, coin, network, isChange) {
var raw = Address._deriveAddress(scriptType, publicKeyRing, path, m, coin, network);
return Address.create(_.extend(raw, {
coin: coin,
walletId: walletId,
type: scriptType,
isChange: isChange,

4
lib/model/addressmanager.js

@ -1,8 +1,8 @@
var _ = require('lodash');
var $ = require('preconditions').singleton();
var Bitcore = require('bitcore-lib');
var Constants = require('../common/constants');
var Utils = require('../common/utils');
function AddressManager() {};
@ -13,7 +13,7 @@ AddressManager.create = function(opts) {
x.version = 2;
x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
$.checkState(_.contains(_.values(Constants.DERIVATION_STRATEGIES), x.derivationStrategy));
$.checkState(Utils.checkValueInCollection(x.derivationStrategy, Constants.DERIVATION_STRATEGIES));
x.receiveAddressIndex = 0;
x.changeAddressIndex = 0;

18
lib/model/copayer.js

@ -10,12 +10,16 @@ var Address = require('./address');
var AddressManager = require('./addressmanager');
var Bitcore = require('bitcore-lib');
var Constants = require('../common/constants');
var Common = require('../common');
var Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
function Copayer() {};
Copayer._xPubToCopayerId = function(xpub) {
var hash = sjcl.hash.sha256.hash(xpub);
Copayer._xPubToCopayerId = function(coin, xpub) {
var str = coin == Defaults.COIN ? xpub : coin + xpub;
var hash = sjcl.hash.sha256.hash(str);
return sjcl.codec.hex.fromBits(hash);
};
@ -25,14 +29,17 @@ Copayer.create = function(opts) {
.checkArgument(opts.requestPubKey, 'Missing copayer request public key')
.checkArgument(opts.signature, 'Missing copayer request public key signature');
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
opts.copayerIndex = opts.copayerIndex || 0;
var x = new Copayer();
x.version = 2;
x.createdOn = Math.floor(Date.now() / 1000);
x.coin = opts.coin;
x.xPubKey = opts.xPubKey;
x.id = Copayer._xPubToCopayerId(x.xPubKey);
x.id = Copayer._xPubToCopayerId(opts.coin, x.xPubKey);
x.name = opts.name;
x.requestPubKey = opts.requestPubKey;
x.signature = opts.signature;
@ -59,6 +66,7 @@ Copayer.fromObj = function(obj) {
x.version = obj.version;
x.createdOn = obj.createdOn;
x.coin = obj.coin || Defaults.COIN;
x.id = obj.id;
x.name = obj.name;
x.xPubKey = obj.xPubKey;
@ -87,7 +95,7 @@ Copayer.prototype.createAddress = function(wallet, isChange) {
$.checkState(wallet.isComplete());
var path = this.addressManager.getNewAddressPath(isChange);
var address = Address.derive(wallet.id, wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.network, isChange);
var address = Address.derive(wallet.id, wallet.addressType, wallet.publicKeyRing, path, wallet.m, wallet.coin, wallet.network, isChange);
return address;
};

40
lib/model/txproposal.js

@ -7,11 +7,15 @@ var log = require('npmlog');
log.debug = log.verbose;
log.disableColor();
var Bitcore = require('bitcore-lib');
var Bitcore = {
'btc': require('bitcore-lib'),
'bch': require('bitcore-lib-cash'),
};
var Common = require('../common');
var Constants = Common.Constants;
var Defaults = Common.Defaults;
var Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
var TxProposalLegacy = require('./txproposal_legacy');
var TxProposalAction = require('./txproposalaction');
@ -21,6 +25,9 @@ function TxProposal() {};
TxProposal.create = function(opts) {
opts = opts || {};
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
$.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS));
var x = new TxProposal();
x.version = 3;
@ -30,6 +37,8 @@ TxProposal.create = function(opts) {
x.id = opts.id || Uuid.v4();
x.walletId = opts.walletId;
x.creatorId = opts.creatorId;
x.coin = opts.coin;
x.network = opts.network;
x.message = opts.message;
x.payProUrl = opts.payProUrl;
x.changeAddress = opts.changeAddress;
@ -51,15 +60,11 @@ TxProposal.create = function(opts) {
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
x.addressType = opts.addressType || (x.walletN > 1 ? Constants.SCRIPT_TYPES.P2SH : Constants.SCRIPT_TYPES.P2PKH);
$.checkState(_.contains(_.values(Constants.SCRIPT_TYPES), x.addressType));
$.checkState(Utils.checkValueInCollection(x.addressType, Constants.SCRIPT_TYPES));
x.customData = opts.customData;
x.amount = x.getTotalAmount();
try {
x.network = opts.network || Bitcore.Address(x.outputs[0].toAddress).toObject().network;
} catch (ex) {}
$.checkState(_.contains(_.values(Constants.NETWORKS), x.network));
x.setInputs(opts.inputs);
x.fee = opts.fee;
@ -79,6 +84,7 @@ TxProposal.fromObj = function(obj) {
x.id = obj.id;
x.walletId = obj.walletId;
x.creatorId = obj.creatorId;
x.coin = obj.coin || Defaults.COIN;
x.network = obj.network;
x.outputs = obj.outputs;
x.amount = obj.amount;
@ -136,9 +142,9 @@ TxProposal.prototype._updateStatus = function() {
TxProposal.prototype._buildTx = function() {
var self = this;
var t = new Bitcore.Transaction();
var t = new Bitcore[self.coin].Transaction();
$.checkState(_.contains(_.values(Constants.SCRIPT_TYPES), self.addressType));
$.checkState(Utils.checkValueInCollection(self.addressType, Constants.SCRIPT_TYPES));
switch (self.addressType) {
case Constants.SCRIPT_TYPES.P2SH:
@ -155,7 +161,7 @@ TxProposal.prototype._buildTx = function() {
_.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({
t.addOutput(new Bitcore[self.coin].Transaction.Output({
script: o.script,
satoshis: o.amount
}));
@ -220,10 +226,6 @@ TxProposal.prototype.getBitcoreTx = function() {
return t;
};
TxProposal.prototype.getNetworkName = function() {
return this.network;
};
TxProposal.prototype.getRawTx = function() {
var t = this.getBitcoreTx();
@ -323,21 +325,23 @@ TxProposal.prototype.addAction = function(copayerId, type, comment, signatures,
TxProposal.prototype._addSignaturesToBitcoreTx = function(tx, signatures, xpub) {
var self = this;
var bitcore = Bitcore[self.coin];
if (signatures.length != this.inputs.length)
throw new Error('Number of signatures does not match number of inputs');
var i = 0,
x = new Bitcore.HDPublicKey(xpub);
x = new bitcore.HDPublicKey(xpub);
_.each(signatures, function(signatureHex) {
var input = self.inputs[i];
try {
var signature = Bitcore.crypto.Signature.fromString(signatureHex);
var signature = bitcore.crypto.Signature.fromString(signatureHex);
var pub = x.deriveChild(self.inputPaths[i]).publicKey;
var s = {
inputIndex: i,
signature: signature,
sigtype: Bitcore.crypto.Signature.SIGHASH_ALL,
sigtype: bitcore.crypto.Signature.SIGHASH_ALL | bitcore.crypto.Signature.SIGHASH_FORKID,
publicKey: pub,
};
tx.inputs[i].addSignature(tx, s);

5
lib/model/txproposal_legacy.js

@ -54,6 +54,7 @@ TxProposal.fromObj = function(obj) {
return TxProposalAction.fromObj(action);
});
x.outputOrder = obj.outputOrder;
x.coin = obj.coin || Defaults.COIN;
x.network = obj.network;
x.fee = obj.fee;
x.feePerKb = obj.feePerKb;
@ -93,10 +94,6 @@ TxProposal.prototype.getBitcoreTx = function() {
throwUnsupportedError();
};
TxProposal.prototype.getNetworkName = function() {
return Bitcore.Address(this.changeAddress.address).toObject().network;
};
TxProposal.prototype.getRawTx = function() {
throwUnsupportedError();
};

16
lib/model/wallet.js

@ -9,7 +9,10 @@ var Address = require('./address');
var Copayer = require('./copayer');
var AddressManager = require('./addressmanager');
var Constants = require('../common/constants');
var Common = require('../common');
var Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
function Wallet() {};
@ -20,6 +23,8 @@ Wallet.create = function(opts) {
$.shouldBeNumber(opts.m);
$.shouldBeNumber(opts.n);
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
$.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS));
x.version = '1.0.0';
x.createdOn = Math.floor(Date.now() / 1000);
@ -33,6 +38,7 @@ Wallet.create = function(opts) {
x.addressIndex = 0;
x.copayers = [];
x.pubKey = opts.pubKey;
x.coin = opts.coin;
x.network = opts.network;
x.derivationStrategy = opts.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
x.addressType = opts.addressType || Constants.SCRIPT_TYPES.P2SH;
@ -64,6 +70,7 @@ Wallet.fromObj = function(obj) {
return Copayer.fromObj(copayer);
});
x.pubKey = obj.pubKey;
x.coin = obj.coin || Defaults.COIN;
x.network = obj.network;
x.derivationStrategy = obj.derivationStrategy || Constants.DERIVATION_STRATEGIES.BIP45;
x.addressType = obj.addressType || Constants.SCRIPT_TYPES.P2SH;
@ -105,6 +112,7 @@ Wallet.prototype._updatePublicKeyRing = function() {
};
Wallet.prototype.addCopayer = function(copayer) {
$.checkState(copayer.coin == this.coin);
this.copayers.push(copayer);
if (this.copayers.length < this.n) return;
@ -134,10 +142,6 @@ Wallet.prototype.getCopayer = function(copayerId) {
});
};
Wallet.prototype.getNetworkName = function() {
return this.network;
};
Wallet.prototype.isComplete = function() {
return this.status == 'complete';
};
@ -152,7 +156,7 @@ Wallet.prototype.createAddress = function(isChange) {
var self = this;
var path = this.addressManager.getNewAddressPath(isChange);
var address = Address.derive(self.id, this.addressType, this.publicKeyRing, path, this.m, this.network, isChange);
var address = Address.derive(self.id, this.addressType, this.publicKeyRing, path, this.m, this.coin, this.network, isChange);
return address;
};

226
lib/server.js

@ -12,6 +12,10 @@ var EmailValidator = require('email-validator');
var Stringify = require('json-stable-stringify');
var Bitcore = require('bitcore-lib');
var Bitcore_ = {
btc: Bitcore,
bch: require('bitcore-lib-cash')
};
var Common = require('./common');
var Utils = Common.Utils;
@ -158,7 +162,7 @@ WalletService.handleIncomingNotification = function(notification, cb) {
if (!notification || notification.type != 'NewBlock') return cb();
WalletService._clearBlockchainHeightCache(notification.data.network);
WalletService._clearBlockchainHeightCache(notification.data.coin, notification.data.network);
return cb();
};
@ -318,6 +322,7 @@ WalletService.prototype.logout = function(opts, cb) {
* @param {number} opts.n - Total copayers.
* @param {string} opts.pubKey - Public key to verify copayers joining have access to the wallet secret.
* @param {string} opts.singleAddress[=false] - The wallet will only ever have one address.
* @param {string} opts.coin[='btc'] - The coin for this wallet (btc, bch).
* @param {string} opts.network[='livenet'] - The Bitcoin network for this wallet.
* @param {string} opts.supportBIP44AndP2PKH[=true] - Client supports BIP44 & P2PKH for new wallets.
*/
@ -331,8 +336,12 @@ WalletService.prototype.createWallet = function(opts, cb) {
if (!Wallet.verifyCopayerLimits(opts.m, opts.n))
return cb(new ClientError('Invalid combination of required copayers / total copayers'));
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
opts.network = opts.network || 'livenet';
if (!_.contains(['livenet', 'testnet'], opts.network))
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS))
return cb(new ClientError('Invalid network'));
opts.supportBIP44AndP2PKH = _.isBoolean(opts.supportBIP44AndP2PKH) ? opts.supportBIP44AndP2PKH : true;
@ -364,6 +373,7 @@ WalletService.prototype.createWallet = function(opts, cb) {
name: opts.name,
m: opts.m,
n: opts.n,
coin: opts.coin,
network: opts.network,
pubKey: pubKey.toString(),
singleAddress: !!opts.singleAddress,
@ -417,7 +427,7 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) {
});
},
function(done) {
self.storage.fetchAddress(opts.identifier, function(err, address) {
self.storage.fetchAddressByCoin(Defaults.COIN, opts.identifier, function(err, address) {
if (address) walletId = address.walletId;
return done(err);
});
@ -435,20 +445,29 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) {
}
// Is identifier a txid form an incomming tx?
async.detectSeries(_.values(Constants.NETWORKS), function(network, nextNetwork) {
var bc = self._getBlockchainExplorer(network);
var coinNetworkPairs = [];
_.each(_.values(Constants.COINS), function(coin) {
_.each(_.values(Constants.NETWORKS), function(network) {
coinNetworkPairs.push({
coin: coin,
network: network
});
});
});
async.detectSeries(coinNetworkPairs, function(coinNetwork, nextCoinNetwork) {
var bc = self._getBlockchainExplorer(coinNetwork.coin, coinNetwork.network);
bc.getTransaction(opts.identifier, function(err, tx) {
if (err || !tx) return nextNetwork(err, false);
if (err || !tx) return nextCoinNetwork(err, false);
var outputs = _.first(self._normalizeTxHistory(tx)).outputs;
var toAddresses = _.pluck(outputs, 'address');
async.detect(toAddresses, function(addressStr, nextAddress) {
self.storage.fetchAddress(addressStr, function(err, address) {
self.storage.fetchAddressByCoin(coinNetwork.coin, addressStr, function(err, address) {
if (err || !address) return nextAddress(err, false);
walletId = address.walletId;
nextAddress(null, true);
});
}, function(err) {
nextNetwork(err, !!walletId);
nextCoinNetwork(err, !!walletId);
});
});
}, function(err) {
@ -622,6 +641,7 @@ WalletService.prototype._addCopayerToWallet = function(wallet, opts, cb) {
var self = this;
var copayer = Model.Copayer.create({
coin: wallet.coin,
name: opts.name,
copayerIndex: wallet.copayers.length,
xPubKey: opts.xPubKey,
@ -630,6 +650,7 @@ WalletService.prototype._addCopayerToWallet = function(wallet, opts, cb) {
customData: opts.customData,
derivationStrategy: wallet.derivationStrategy,
});
self.storage.fetchCopayerLookup(copayer.id, function(err, res) {
if (err) return cb(err);
if (res) return cb(Errors.COPAYER_REGISTERED);
@ -755,6 +776,7 @@ WalletService._getCopayerHash = function(name, xPubKey, requestPubKey) {
* Joins a wallet in creation.
* @param {Object} opts
* @param {string} opts.walletId - The wallet id.
* @param {string} opts.coin[='btc'] - The expected coin for this wallet (btc, bch).
* @param {string} opts.name - The copayer name.
* @param {string} opts.xPubKey - Extended Public Key for this copayer.
* @param {string} opts.requestPubKey - Public Key used to check requests from this copayer.
@ -771,6 +793,10 @@ WalletService.prototype.joinWallet = function(opts, cb) {
if (_.isEmpty(opts.name))
return cb(new ClientError('Invalid copayer name'));
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
try {
Bitcore.HDPublicKey(opts.xPubKey);
} catch (ex) {
@ -785,6 +811,10 @@ WalletService.prototype.joinWallet = function(opts, cb) {
if (err) return cb(err);
if (!wallet) return cb(Errors.WALLET_NOT_FOUND);
if (opts.coin != wallet.coin) {
return cb(new ClientError('The wallet you are trying to join was created for a different coin'));
}
if (opts.supportBIP44AndP2PKH) {
// New client trying to join legacy wallet
if (wallet.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45) {
@ -900,7 +930,7 @@ WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) {
hasActivity: true
})) return cb(null, true);
var bc = self._getBlockchainExplorer(latestAddresses[0].network);
var bc = self._getBlockchainExplorer(latestAddresses[0].coin, latestAddresses[0].network);
var activityFound = false;
var i = latestAddresses.length;
async.whilst(function() {
@ -1020,27 +1050,28 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) {
};
WalletService.prototype._getBlockchainExplorer = function(network) {
WalletService.prototype._getBlockchainExplorer = function(coin, network) {
var opts = {};
if (this.blockchainExplorer) return this.blockchainExplorer;
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) {
opts = this.blockchainExplorerOpts[network];
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[coin] && this.blockchainExplorerOpts[coin][network]) {
opts = this.blockchainExplorerOpts[coin][network];
}
// TODO: provider should be configurable
opts.provider = 'insight';
opts.coin = coin;
opts.network = network;
opts.userAgent = WalletService.getServiceVersion();
return new BlockchainExplorer(opts);
};
WalletService.prototype._getUtxos = function(addresses, cb) {
WalletService.prototype._getUtxos = function(coin, addresses, cb) {
var self = this;
if (addresses.length == 0) return cb(null, []);
var networkName = Bitcore.Address(addresses[0]).toObject().network;
var bc = self._getBlockchainExplorer(networkName);
var bc = self._getBlockchainExplorer(coin, networkName);
bc.getUtxos(addresses, function(err, utxos) {
if (err) return cb(err);
@ -1064,10 +1095,16 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) {
return utxo.txid + '|' + utxo.vout
};
var allAddresses, allUtxos, utxoIndex;
var coin, allAddresses, allUtxos, utxoIndex;
async.series([
function(next) {
self.getWallet({}, function(err, wallet) {
coin = wallet.coin;
return next();
});
},
function(next) {
if (_.isArray(addresses)) {
allAddresses = addresses;
@ -1082,7 +1119,7 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) {
if (allAddresses.length == 0) return cb(null, []);
var addressStrs = _.pluck(allAddresses, 'address');
self._getUtxos(addressStrs, function(err, utxos) {
self._getUtxos(coin, addressStrs, function(err, utxos) {
if (err) return next(err);
if (utxos.length == 0) return cb(null, []);
@ -1144,6 +1181,7 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) {
/**
* Returns list of UTXOs
* @param {Object} opts
* @param {String} [opts.coin='btc'] (optional)
* @param {Array} opts.addresses (optional) - List of addresses from where to fetch UTXOs.
* @returns {Array} utxos - List of UTXOs.
*/
@ -1152,10 +1190,14 @@ WalletService.prototype.getUtxos = function(opts, cb) {
opts = opts || {};
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
if (_.isUndefined(opts.addresses)) {
self._getUtxosForCurrentWallet(null, cb);
} else {
self._getUtxos(opts.addresses, cb);
self._getUtxos(opts.coin, opts.addresses, cb);
}
};
@ -1321,29 +1363,30 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) {
opts = opts || {};
var feeArgs = !!opts.feeLevel + _.isNumber(opts.feePerKb);
if (feeArgs > 1)
return cb(new ClientError('Only one of feeLevel/feePerKb can be specified'));
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
if (feeArgs == 0) {
log.debug('No fee provided, using "normal" fee level');
opts.feeLevel = 'normal';
}
var feeArgs = !!opts.feeLevel + _.isNumber(opts.feePerKb);
if (feeArgs > 1)
return cb(new ClientError('Only one of feeLevel/feePerKb can be specified'));
if (opts.feeLevel) {
if (!_.any(Defaults.FEE_LEVELS, {
name: opts.feeLevel
}))
return cb(new ClientError('Invalid fee level. Valid values are ' + _.pluck(Defaults.FEE_LEVELS, 'name').join(', ')));
}
if (feeArgs == 0) {
log.debug('No fee provided, using "normal" fee level');
opts.feeLevel = 'normal';
}
if (_.isNumber(opts.feePerKb)) {
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
return cb(new ClientError('Invalid fee per KB'));
}
var feeLevels = Defaults.FEE_LEVELS[wallet.coin];
if (opts.feeLevel) {
if (!_.any(feeLevels, {
name: opts.feeLevel
}))
return cb(new ClientError('Invalid fee level. Valid values are ' + _.pluck(feeLevels, 'name').join(', ')));
}
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
if (_.isNumber(opts.feePerKb)) {
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
return cb(new ClientError('Invalid fee per KB'));
}
self._getUtxosForCurrentWallet(null, function(err, utxos) {
if (err) return cb(err);
@ -1377,6 +1420,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) {
var txp = Model.TxProposal.create({
walletId: self.walletId,
coin: wallet.coin,
network: wallet.network,
walletM: wallet.m,
walletN: wallet.n,
@ -1427,10 +1471,10 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) {
});
};
WalletService.prototype._sampleFeeLevels = function(network, points, cb) {
WalletService.prototype._sampleFeeLevels = function(coin, network, points, cb) {
var self = this;
var bc = self._getBlockchainExplorer(network);
var bc = self._getBlockchainExplorer(coin, network);
bc.estimateFee(points, function(err, result) {
if (err) {
log.error('Error estimating fee', err);
@ -1458,6 +1502,7 @@ WalletService.prototype._sampleFeeLevels = function(network, points, cb) {
/**
* Returns fee levels for the current state of the network.
* @param {Object} opts
* @param {string} [opts.coin = 'btc'] - The coin to estimate fee levels from.
* @param {string} [opts.network = 'livenet'] - The Bitcoin network to estimate fee levels from.
* @returns {Object} feeLevels - A list of fee levels & associated amount per kB in satoshi.
*/
@ -1466,8 +1511,18 @@ WalletService.prototype.getFeeLevels = function(opts, cb) {
opts = opts || {};
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
opts.network = opts.network || 'livenet';
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS))
return cb(new ClientError('Invalid network'));
var feeLevels = Defaults.FEE_LEVELS[opts.coin];
function samplePoints() {
var definedPoints = _.uniq(_.pluck(Defaults.FEE_LEVELS, 'nbBlocks'));
var definedPoints = _.uniq(_.pluck(feeLevels, 'nbBlocks'));
return _.uniq(_.flatten(_.map(definedPoints, function(p) {
return _.range(p, p + Defaults.FEE_LEVELS_FALLBACK + 1);
})));
@ -1494,12 +1549,8 @@ WalletService.prototype.getFeeLevels = function(opts, cb) {
return result;
};
var network = opts.network || 'livenet';
if (network != 'livenet' && network != 'testnet')
return cb(new ClientError('Invalid network'));
self._sampleFeeLevels(network, samplePoints(), function(err, feeSamples) {
var values = _.map(Defaults.FEE_LEVELS, function(level) {
self._sampleFeeLevels(opts.coin, opts.network, samplePoints(), function(err, feeSamples) {
var values = _.map(feeLevels, function(level) {
var result = {
level: level.name,
};
@ -1550,10 +1601,10 @@ WalletService.prototype._checkTx = function(txp) {
return ex;
}
if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError)
if (bitcoreError instanceof Bitcore_[txp.coin].errors.Transaction.FeeError)
return Errors.INSUFFICIENT_FUNDS_FOR_FEE;
if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs)
if (bitcoreError instanceof Bitcore_[txp.coin].errors.Transaction.DustOutputs)
return Errors.DUST_AMOUNT;
return bitcoreError;
};
@ -1682,7 +1733,7 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) {
var changeAmount = Math.round(total - txpAmount - fee);
log.debug('Tx change: ', Utils.formatAmountInBtc(changeAmount));
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore.Transaction.DUST_AMOUNT);
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[txp.coin].Transaction.DUST_AMOUNT);
if (changeAmount > 0 && changeAmount <= dustThreshold) {
log.debug('Change below dust threshold (' + Utils.formatAmountInBtc(dustThreshold) + '). Incrementing fee to remove change.');
// Remove dust change by incrementing fee
@ -1835,7 +1886,7 @@ WalletService.prototype._canCreateTx = function(cb) {
};
WalletService.prototype._validateOutputs = function(opts, wallet, cb) {
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore.Transaction.DUST_AMOUNT);
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[wallet.coin].Transaction.DUST_AMOUNT);
if (_.isEmpty(opts.outputs)) return new ClientError('No outputs were specified');
@ -1853,7 +1904,7 @@ WalletService.prototype._validateOutputs = function(opts, wallet, cb) {
} catch (ex) {
return Errors.INVALID_ADDRESS;
}
if (toAddress.network != wallet.getNetworkName()) {
if (toAddress.network != wallet.network) {
return Errors.INCORRECT_ADDRESS_NETWORK;
}
@ -1884,11 +1935,12 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb)
opts.feeLevel = 'normal';
}
var feeLevels = Defaults.FEE_LEVELS[wallet.coin];
if (opts.feeLevel) {
if (!_.any(Defaults.FEE_LEVELS, {
if (!_.any(feeLevels, {
name: opts.feeLevel
}))
return next(new ClientError('Invalid fee level. Valid values are ' + _.pluck(Defaults.FEE_LEVELS, 'name').join(', ')));
return next(new ClientError('Invalid fee level. Valid values are ' + _.pluck(feeLevels, 'name').join(', ')));
}
if (_.isNumber(opts.feePerKb)) {
@ -1916,7 +1968,7 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb)
return next(new ClientError('Fee is not allowed when sendMax is specified (use feeLevel/feePerKb instead)'));
self.getSendMaxInfo({
feePerKb: opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB,
feePerKb: opts.feePerKb,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
returnInputs: true,
}, function(err, info) {
@ -1942,6 +1994,7 @@ WalletService.prototype._getFeePerKb = function(wallet, opts, cb) {
if (_.isNumber(opts.feePerKb)) return cb(null, opts.feePerKb);
self.getFeeLevels({
coin: wallet.coin,
network: wallet.network
}, function(err, levels) {
if (err) return cb(err);
@ -1994,7 +2047,7 @@ WalletService.prototype.createTx = function(opts, cb) {
});
} else {
if (opts.changeAddress) {
self.storage.fetchAddress(opts.changeAddress, function(err, address) {
self.storage.fetchAddressByWalletId(wallet.id, opts.changeAddress, function(err, address) {
if (err) return cb(Errors.INVALID_CHANGE_ADDRESS);
return cb(null, address);
});
@ -2051,6 +2104,8 @@ WalletService.prototype.createTx = function(opts, cb) {
id: opts.txProposalId,
walletId: self.walletId,
creatorId: self.copayerId,
coin: wallet.coin,
network: wallet.network,
outputs: opts.outputs,
message: opts.message,
changeAddress: changeAddress,
@ -2315,8 +2370,8 @@ WalletService.prototype.removePendingTx = function(opts, cb) {
});
};
WalletService.prototype._broadcastRawTx = function(network, raw, cb) {
var bc = this._getBlockchainExplorer(network);
WalletService.prototype._broadcastRawTx = function(coin, network, raw, cb) {
var bc = this._getBlockchainExplorer(coin, network);
bc.broadcast(raw, function(err, txid) {
if (err) return cb(err);
return cb(null, txid);
@ -2326,6 +2381,7 @@ WalletService.prototype._broadcastRawTx = function(network, raw, cb) {
/**
* Broadcast a raw transaction.
* @param {Object} opts
* @param {string} [opts.coin = 'btc'] - The coin for this transaction.
* @param {string} [opts.network = 'livenet'] - The Bitcoin network for this transaction.
* @param {string} opts.rawTx - Raw tx data.
*/
@ -2334,17 +2390,21 @@ WalletService.prototype.broadcastRawTx = function(opts, cb) {
if (!checkRequired(opts, ['network', 'rawTx'], cb)) return;
var network = opts.network || 'livenet';
if (network != 'livenet' && network != 'testnet')
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
opts.network = opts.network || 'livenet';
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS))
return cb(new ClientError('Invalid network'));
self._broadcastRawTx(network, opts.rawTx, cb);
self._broadcastRawTx(opts.coin, opts.network, opts.rawTx, cb);
};
WalletService.prototype._checkTxInBlockchain = function(txp, cb) {
if (!txp.txid) return cb();
var bc = this._getBlockchainExplorer(txp.getNetworkName());
var bc = this._getBlockchainExplorer(txp.coin, txp.network);
bc.getTransaction(txp.txid, function(err, tx) {
if (err) return cb(err);
return cb(null, !!tx);
@ -2472,7 +2532,7 @@ WalletService.prototype.broadcastTx = function(opts, cb) {
} catch (ex) {
return cb(ex);
}
self._broadcastRawTx(txp.getNetworkName(), raw, function(err, txid) {
self._broadcastRawTx(wallet.coin, wallet.network, raw, function(err, txid) {
if (err) {
var broadcastErr = err;
// Check if tx already in blockchain
@ -2678,29 +2738,35 @@ WalletService._cachedBlockheight;
WalletService._initBlockchainHeightCache = function() {
if (WalletService._cachedBlockheight) return;
WalletService._cachedBlockheight = {
livenet: {},
testnet: {}
btc: {
livenet: {},
testnet: {}
},
bch: {
livenet: {},
testnet: {}
},
};
};
WalletService._clearBlockchainHeightCache = function(network) {
WalletService._clearBlockchainHeightCache = function(coin, network) {
WalletService._initBlockchainHeightCache();
if (!_.contains(['livenet', 'testnet'], network)) {
log.error('Incorrect network in new block: ' + network);
if (!Utils.checkValueInCollection(network, Constants.NETWORKS)) {
log.error('Incorrect network in new block: ' + coin + '/' + network);
return;
}
WalletService._cachedBlockheight[network].current = null;
WalletService._cachedBlockheight[coin][network].current = null;
};
WalletService.prototype._getBlockchainHeight = function(network, cb) {
WalletService.prototype._getBlockchainHeight = function(coin, network, cb) {
var self = this;
var now = Date.now();
WalletService._initBlockchainHeightCache();
var cache = WalletService._cachedBlockheight[network];
var cache = WalletService._cachedBlockheight[coin][network];
function fetchFromBlockchain(cb) {
var bc = self._getBlockchainExplorer(network);
var bc = self._getBlockchainExplorer(coin, network);
bc.getBlockchainHeight(function(err, height) {
if (!err && height > 0) {
cache.current = height;
@ -2868,10 +2934,9 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
});
};
function getNormalizedTxs(addresses, from, to, cb) {
function getNormalizedTxs(wallet, addresses, from, to, cb) {
var txs, fromCache, totalItems;
var useCache = addresses.length >= Defaults.HISTORY_CACHE_ADDRESS_THRESOLD;
var network = Bitcore.Address(addresses[0].address).toObject().network;
async.series([
@ -2892,7 +2957,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
if (txs) return next();
var addressStrs = _.pluck(addresses, 'address');
var bc = self._getBlockchainExplorer(network);
var bc = self._getBlockchainExplorer(wallet.coin, wallet.network);
bc.getTransactions(addressStrs, from, to, function(err, rawTxs, total) {
if (err) return next(err);
@ -2919,7 +2984,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
if (!txs) return next();
// Fix tx confirmations for cached txs
self._getBlockchainHeight(network, function(err, height) {
self._getBlockchainHeight(wallet.coin, wallet.network, function(err, height) {
if (err || !height) return next(err);
_.each(txs, function(tx) {
if (tx.blockheight >= 0) {
@ -2946,6 +3011,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
if (_.isEmpty(unconfirmed)) return cb();
self.getFeeLevels({
coin: wallet.coin,
network: wallet.network
}, function(err, levels) {
if (err) {
@ -2982,7 +3048,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
async.waterfall([
function(next) {
getNormalizedTxs(addresses, from, to, next);
getNormalizedTxs(wallet, addresses, from, to, next);
},
function(txs, next) {
// Fetch all proposals in [t - 7 days, t + 1 day]
@ -3041,12 +3107,12 @@ WalletService.prototype.scan = function(opts, cb) {
opts = opts || {};
function checkActivity(address, network, cb) {
var bc = self._getBlockchainExplorer(network);
function checkActivity(wallet, address, cb) {
var bc = self._getBlockchainExplorer(wallet.coin, wallet.network);
bc.getAddressActivity(address, cb);
};
function scanBranch(derivator, cb) {
function scanBranch(wallet, derivator, cb) {
var inactiveCounter = 0;
var allAddresses = [];
var gap = Defaults.SCAN_ADDRESS_GAP;
@ -3055,7 +3121,7 @@ WalletService.prototype.scan = function(opts, cb) {
return inactiveCounter < gap;
}, function(next) {
var address = derivator.derive();
checkActivity(address.address, address.network, function(err, activity) {
checkActivity(wallet, address.address, function(err, activity) {
if (err) return next(err);
allAddresses.push(address);
@ -3099,7 +3165,7 @@ WalletService.prototype.scan = function(opts, cb) {
});
async.eachSeries(derivators, function(derivator, next) {
scanBranch(derivator, function(err, addresses) {
scanBranch(wallet, derivator, function(err, addresses) {
if (err) return next(err);
self.storage.storeAddressAndWallet(wallet, addresses, next);
});

62
lib/storage.js

@ -461,6 +461,7 @@ Storage.prototype.storeAddress = function(address, cb) {
var self = this;
self.db.collection(collections.ADDRESSES).update({
walletId: address.walletId,
address: address.address
}, address, {
w: 1,
@ -471,45 +472,22 @@ Storage.prototype.storeAddress = function(address, cb) {
Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) {
var self = this;
function saveAddresses(addresses, cb) {
if (_.isEmpty(addresses)) return cb();
self.db.collection(collections.ADDRESSES).insert(addresses, {
w: 1
}, cb);
};
var addresses = [].concat(addresses);
if (addresses.length == 0) return cb();
if (_.isEmpty(addresses)) return cb();
async.filter(addresses, function(address, next) {
self.db.collection(collections.ADDRESSES).findOne({
address: address.address,
}, {
walletId: true,
}, function(err, result) {
if (err || !result) return next(true);
if (result.walletId != wallet.id) {
log.warn('Address ' + address.address + ' exists in more than one wallet.');
return next(true);
}
// Ignore if address was already in wallet
return next(false);
});
}, function(newAddresses) {
if (newAddresses.length < addresses.length) {
log.warn('Attempted to store already existing addresses on wallet ' + wallet.id);
}
saveAddresses(newAddresses, function(err) {
if (err) return cb(err);
self.storeWallet(wallet, cb);
});
self.db.collection(collections.ADDRESSES).insert(addresses, {
w: 1
}, function(err) {
if (err) return cb(err);
self.storeWallet(wallet, cb);
});
};
Storage.prototype.fetchAddress = function(address, cb) {
Storage.prototype.fetchAddressByWalletId = function(walletId, address, cb) {
var self = this;
this.db.collection(collections.ADDRESSES).findOne({
walletId: walletId,
address: address,
}, function(err, result) {
if (err) return cb(err);
@ -519,6 +497,27 @@ Storage.prototype.fetchAddress = function(address, cb) {
});
};
Storage.prototype.fetchAddressByCoin = function(coin, address, cb) {
var self = this;
this.db.collection(collections.ADDRESSES).find({
address: address,
}).toArray(function(err, result) {
if (err) return cb(err);
if (!result || _.isEmpty(result)) return cb();
if (result.length > 1) {
result = _.find(result, function(address) {
return coin == (address.coin || Defaults.COIN);
});
} else {
result = _.first(result);
}
if (!result) return cb();
return cb(null, Model.Address.fromObj(result));
});
};
Storage.prototype.fetchPreferences = function(walletId, copayerId, cb) {
this.db.collection(collections.PREFERENCES).find({
walletId: walletId,
@ -610,6 +609,7 @@ Storage.prototype.cleanActiveAddresses = function(walletId, cb) {
Storage.prototype.storeActiveAddresses = function(walletId, addresses, cb) {
var self = this;
if (_.isEmpty(addresses)) return cb();
async.each(addresses, function(address, next) {
var record = {
walletId: walletId,

25
package.json

@ -22,6 +22,7 @@
"dependencies": {
"async": "^0.9.2",
"bitcore-lib": "^0.14.0",
"bitcore-lib-cash": "https://github.com/bitpay/bitcore-lib.git#cash",
"body-parser": "^1.11.0",
"compression": "^1.6.2",
"coveralls": "^2.11.2",
@ -70,14 +71,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"
}
]
}

6
test/integration/bcmonitor.js

@ -49,8 +49,10 @@ describe('Blockchain monitor', function() {
messageBroker: server.messageBroker,
storage: storage,
blockchainExplorers: {
'testnet': blockchainExplorer,
'livenet': blockchainExplorer
'btc': {
'testnet': blockchainExplorer,
'livenet': blockchainExplorer
}
},
}, function(err) {
should.not.exist(err);

4
test/integration/emailnotifications.js

@ -169,7 +169,7 @@ describe('Email notifications', function() {
txp = t;
async.eachSeries(_.range(2), function(i, next) {
var copayer = TestData.copayers[i];
helpers.getAuthServer(copayer.id44, function(server) {
helpers.getAuthServer(copayer.id44btc, function(server) {
var signatures = helpers.clientSign(txp, copayer.xPrivKey_44H_0H_0H);
server.signTx({
txProposalId: txp.id,
@ -235,7 +235,7 @@ describe('Email notifications', function() {
txpId = txp.id;
async.eachSeries(_.range(1, 3), function(i, next) {
var copayer = TestData.copayers[i];
helpers.getAuthServer(copayer.id44, function(server) {
helpers.getAuthServer(copayer.id44btc, function(server) {
server.rejectTx({
txProposalId: txp.id,
}, next);

33
test/integration/helpers.js

@ -13,6 +13,10 @@ var tingodb = require('tingodb')({
});
var Bitcore = require('bitcore-lib');
var Bitcore_ = {
btc: Bitcore,
bch: require('bitcore-lib-cash')
};
var Common = require('../../lib/common');
var Utils = Common.Utils;
@ -95,6 +99,7 @@ helpers.signRequestPubKey = function(requestPubKey, xPrivKey) {
helpers.getAuthServer = function(copayerId, cb) {
var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature');
verifyStub.returns(true);
WalletService.getInstanceWithAuth({
copayerId: copayerId,
message: 'dummy',
@ -107,26 +112,40 @@ helpers.getAuthServer = function(copayerId, cb) {
});
};
helpers._generateCopayersTestData = function(n) {
helpers._generateCopayersTestData = function() {
var xPrivKeys = ['xprv9s21ZrQH143K2n4rV4AtAJFptEmd1tNMKCcSyQBCSuN5eq1dCUhcv6KQJS49joRxu8NNdFxy8yuwTtzCPNYUZvVGC7EPRm2st2cvE7oyTbB',
'xprv9s21ZrQH143K3BwkLceWNLUsgES15JoZuv8BZfnmDRcCGtDooUAPhY8KovhCWcRLXUun5AYL5vVtUNRrmPEibtfk9ongxAGLXZzEHifpvwZ',
'xprv9s21ZrQH143K3xgLzxd6SuWqG5Zp1iUmyGgSsJVhdQNeTzAqBFvXXLZqZzFZqocTx4HD9vUVYU27At5i8q46LmBXXL97fo4H9C3tHm4BnjY',
'xprv9s21ZrQH143K48nfuK14gKJtML7eQzV2dAH1RaqAMj8v2zs79uaavA9UTWMxpBdgbMH2mhJLeKGq8AFA6GDnFyWP4rLmknqZAfgFFV718vo',
'xprv9s21ZrQH143K44Bb9G3EVNmLfAUKjTBAA2YtKxF4zc8SLV1o15JBoddhGHE9PGLXePMbEsSjCCvTvP3fUv6yMXZrnHigBboRBn2DmNoJkJg',
'xprv9s21ZrQH143K48PpVxrh71KdViTFhAaiDSVtNFkmbWNYjwwwPbTrcqoVXsgBfue3Gq9b71hQeEbk67JgtTBcpYgKLF8pTwVnGz56f1BaCYt',
'xprv9s21ZrQH143K3pgRcRBRnmcxNkNNLmJrpneMkEXY6o5TWBuJLMfdRpAWdb2cG3yxbL4DxfpUnQpjfQUmwPdVrRGoDJmtAf5u8cyqKCoDV97',
'xprv9s21ZrQH143K3nvcmdjDDDZbDJHpfWZCUiunwraZdcamYcafHvUnZfV51fivH9FPyfo12NyKH5JDxGLsQePyWKtTiJx3pkEaiwxsMLkVapp',
'xprv9s21ZrQH143K2uYgqtYtphEQkFAgiWSqahFUWjgCdKykJagiNDz6Lf7xRVQdtZ7MvkhX9V3pEcK3xTAWZ6Y6ecJqrXnCpzrH9GSHn8wyrT5',
'xprv9s21ZrQH143K2wcRMP75tAEL5JnUx4xU2AbUBQzVVUDP7DHZJkjF3kaRE7tcnPLLLL9PGjYTWTJmCQPaQ4GGzgWEUFJ6snwJG9YnQHBFRNR'
];
console.log('var copayers = [');
_.each(_.range(n), function(c) {
var xpriv = new Bitcore.HDPrivateKey();
_.each(xPrivKeys, function(xPrivKeyStr, c) {
var xpriv = Bitcore.HDPrivateKey(xPrivKeyStr);
var xpub = Bitcore.HDPublicKey(xpriv);
var xpriv_45H = xpriv.deriveChild(45, true);
var xpub_45H = Bitcore.HDPublicKey(xpriv_45H);
var id45 = Copayer._xPubToCopayerId(xpub_45H.toString());
var id45 = Model.Copayer._xPubToCopayerId('btc', xpub_45H.toString());
var xpriv_44H_0H_0H = xpriv.deriveChild(44, true).deriveChild(0, true).deriveChild(0, true);
var xpub_44H_0H_0H = Bitcore.HDPublicKey(xpriv_44H_0H_0H);
var id44 = Copayer._xPubToCopayerId(xpub_44H_0H_0H.toString());
var id44btc = Model.Copayer._xPubToCopayerId('btc', xpub_44H_0H_0H.toString());
var id44bch = Model.Copayer._xPubToCopayerId('bch', xpub_44H_0H_0H.toString());
var xpriv_1H = xpriv.deriveChild(1, true);
var xpub_1H = Bitcore.HDPublicKey(xpriv_1H);
var priv = xpriv_1H.deriveChild(0).privateKey;
var pub = xpub_1H.deriveChild(0).publicKey;
console.log('{id44: ', "'" + id44 + "',");
console.log('{id44btc: ', "'" + id44btc + "',");
console.log('id44bch: ', "'" + id44bch + "',");
console.log('id45: ', "'" + id45 + "',");
console.log('xPrivKey: ', "'" + xpriv.toString() + "',");
console.log('xPubKey: ', "'" + xpub.toString() + "',");
@ -165,6 +184,7 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) {
n: n,
pubKey: TestData.keyPair.pub,
singleAddress: !!opts.singleAddress,
coin: opts.coin || 'btc',
};
if (_.isBoolean(opts.supportBIP44AndP2PKH))
walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH;
@ -176,6 +196,7 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) {
var copayerData = TestData.copayers[i + offset];
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
coin: opts.coin,
name: 'copayer ' + (i + 1),
xPubKey: (_.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H,
requestPubKey: copayerData.pubKey_1H_0,

4
test/integration/pushNotifications.js

@ -368,7 +368,7 @@ describe('Push notifications', function() {
txpId = txp.id;
async.eachSeries(_.range(1, 3), function(i, next) {
var copayer = TestData.copayers[i];
helpers.getAuthServer(copayer.id44, function(server) {
helpers.getAuthServer(copayer.id44btc, function(server) {
server.rejectTx({
txProposalId: txp.id,
}, next);
@ -413,7 +413,7 @@ describe('Push notifications', function() {
txp = t;
async.eachSeries(_.range(1, 3), function(i, next) {
var copayer = TestData.copayers[i];
helpers.getAuthServer(copayer.id44, function(s) {
helpers.getAuthServer(copayer.id44btc, function(s) {
server = s;
var signatures = helpers.clientSign(txp, copayer.xPrivKey_44H_0H_0H);
server.signTx({

160
test/integration/server.js

@ -433,6 +433,24 @@ describe('Wallet service', function() {
});
});
it('should create wallet for another coin', function(done) {
var opts = {
coin: 'bch',
name: 'my wallet',
m: 2,
n: 3,
pubKey: TestData.keyPair.pub,
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(err);
server.storage.fetchWallet(walletId, function(err, wallet) {
should.not.exist(err);
wallet.coin.should.equal('bch');
done();
});
});
});
describe('Address derivation strategy', function() {
var server;
beforeEach(function() {
@ -617,6 +635,21 @@ describe('Wallet service', function() {
});
});
it('should fail to join wallet for different coin', function(done) {
var copayerOpts = helpers.getSignedCopayerOpts({
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey_44H_0H_0H,
requestPubKey: TestData.copayers[0].pubKey_1H_0,
coin: 'bch',
});
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.message.should.contain('different coin');
done();
});
});
it('should return copayer in wallet error before full wallet', function(done) {
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
var copayerOpts = helpers.getSignedCopayerOpts({
@ -1155,22 +1188,6 @@ describe('Wallet service', function() {
});
});
it('should protect against storing same address multiple times', function(done) {
server.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
delete address._id;
server.storage.storeAddressAndWallet(wallet, address, function(err) {
should.not.exist(err);
server.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
done();
});
});
});
});
it('should create many addresses on simultaneous requests', function(done) {
var N = 5;
async.mapSeries(_.range(N), function(i, cb) {
@ -1259,7 +1276,7 @@ describe('Wallet service', function() {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
w.copayers[0].id.should.equal(TestData.copayers[0].id44);
w.copayers[0].id.should.equal(TestData.copayers[0].id44btc);
done();
});
});
@ -1638,7 +1655,7 @@ describe('Wallet service', function() {
var requestPubKeyStr = requestPubKey.toString();
var sig = helpers.signRequestPubKey(requestPubKeyStr, xPrivKey);
var copayerId = Model.Copayer._xPubToCopayerId(TestData.copayers[0].xPubKey_44H_0H_0H);
var copayerId = Model.Copayer._xPubToCopayerId('btc', TestData.copayers[0].xPubKey_44H_0H_0H);
opts = {
copayerId: copayerId,
requestPubKey: requestPubKeyStr,
@ -2178,28 +2195,30 @@ describe('Wallet service', function() {
var server, wallet, levels;
before(function() {
levels = Defaults.FEE_LEVELS;
Defaults.FEE_LEVELS = [{
name: 'urgent',
nbBlocks: 1,
multiplier: 1.5,
defaultValue: 50000,
}, {
name: 'priority',
nbBlocks: 1,
defaultValue: 50000
}, {
name: 'normal',
nbBlocks: 2,
defaultValue: 40000
}, {
name: 'economy',
nbBlocks: 6,
defaultValue: 25000
}, {
name: 'superEconomy',
nbBlocks: 24,
defaultValue: 10000
}];
Defaults.FEE_LEVELS = {
btc: [{
name: 'urgent',
nbBlocks: 1,
multiplier: 1.5,
defaultValue: 50000,
}, {
name: 'priority',
nbBlocks: 1,
defaultValue: 50000
}, {
name: 'normal',
nbBlocks: 2,
defaultValue: 40000
}, {
name: 'economy',
nbBlocks: 6,
defaultValue: 25000
}, {
name: 'superEconomy',
nbBlocks: 24,
defaultValue: 10000
}]
};
});
after(function() {
Defaults.FEE_LEVELS = levels;
@ -2248,7 +2267,7 @@ describe('Wallet service', function() {
fees = _.zipObject(_.map(fees, function(item) {
return [item.level, item.feePerKb];
}));
var defaults = _.zipObject(_.map(Defaults.FEE_LEVELS, function(item) {
var defaults = _.zipObject(_.map(Defaults.FEE_LEVELS['btc'], function(item) {
return [item.name, item.defaultValue];
}));
fees.priority.should.equal(defaults.priority);
@ -2311,7 +2330,7 @@ describe('Wallet service', function() {
});
});
it('should get monotonically decreasing fee values', function(done) {
_.find(Defaults.FEE_LEVELS, {
_.find(Defaults.FEE_LEVELS['btc'], {
nbBlocks: 6
}).defaultValue.should.equal(25000);
helpers.stubFeeLevels({
@ -3280,7 +3299,7 @@ describe('Wallet service', function() {
var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H;
var accessOpts = {
copayerId: TestData.copayers[0].id44,
copayerId: TestData.copayers[0].id44btc,
requestPubKey: reqPubKey,
signature: helpers.signRequestPubKey(reqPubKey, xPrivKey),
};
@ -6603,6 +6622,7 @@ describe('Wallet service', function() {
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 2000);
server._notify('NewBlock', {
coin: 'btc',
network: 'livenet',
hash: 'dummy hash',
}, {
@ -7578,4 +7598,58 @@ describe('Wallet service', function() {
});
});
});
describe('BTC & BCH wallets with same seed', function() {
var server = {},
wallet = {};
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server.btc = s;
wallet.btc = w;
w.copayers[0].id.should.equal(TestData.copayers[0].id44btc);
helpers.createAndJoinWallet(1, 1, {
coin: 'bch'
}, function(s, w) {
server.bch = s;
wallet.bch = w;
w.copayers[0].id.should.equal(TestData.copayers[0].id44bch);
done();
});
});
});
it('should create address', function(done) {
server.btc.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.btc.id);
address.coin.should.equal('btc');
address.network.should.equal('livenet');
address.address.should.equal('1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG');
server.bch.createAddress({}, function(err, address) {
should.not.exist(err);
should.exist(address);
address.walletId.should.equal(wallet.bch.id);
address.coin.should.equal('bch');
address.network.should.equal('livenet');
address.address.should.equal('1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG');
server.btc.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
addresses[0].coin.should.equal('btc');
addresses[0].walletId.should.equal(wallet.btc.id);
addresses[0].address.should.equal('1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG');
server.bch.getMainAddresses({}, function(err, addresses) {
should.not.exist(err);
addresses.length.should.equal(1);
addresses[0].coin.should.equal('bch');
addresses[0].walletId.should.equal(wallet.bch.id);
addresses[0].address.should.equal('1L3z9LPd861FWQhf3vDn89Fnc9dkdBo2CG');
done();
});
});
});
});
});
});
});

8
test/model/address.js

@ -12,6 +12,7 @@ describe('Address', function() {
it('should create livenet address', function() {
var x = Address.create({
address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg',
coin: 'btc',
walletId: '123',
isChange: false,
path: 'm/0/1',
@ -23,6 +24,7 @@ describe('Address', function() {
it('should create testnet address', function() {
var x = Address.create({
address: 'mp5xaa4uBj16DJt1fuA3D9fejHuCzeb7hj',
coin: 'btc',
walletId: '123',
isChange: false,
path: 'm/0/1',
@ -39,7 +41,7 @@ describe('Address', function() {
}, {
xPubKey: 'xpub68tpbrfk747AvDUCdtEUgK2yDPmtGKf7YXzEcUUqnF3jmAMeZgcpoZqgXwwoi8CpwDkyzVX6wxUktTw2wh9EhhVjh5S71MLL3FkZDGF5GeY'
// PubKey(xPubKey/0/0) -> 03162179906dbe6a67979d4f8f46ee1db6ff81715f465e6615a4f5969478ad2171
}], 'm/0/0', 1, 'livenet', false);
}], 'm/0/0', 1, 'btc', 'livenet', false);
should.exist(address);
address.walletId.should.equal('wallet-id');
address.address.should.equal('3QN2CiSxcUsFuRxZJwXMNDQ2esnr5RXTvw');
@ -52,7 +54,7 @@ describe('Address', function() {
var address = Address.derive('wallet-id', 'P2SH', [{
xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1'
// PubKey(xPubKey/0/0) -> 03fe466ea829aa4c9a1c289f9ba61ebc26a61816500860c8d23f94aad9af152ecd
}], 'm/0/0', 1, 'livenet', false);
}], 'm/0/0', 1, 'btc', 'livenet', false);
should.exist(address);
address.walletId.should.equal('wallet-id');
address.address.should.equal('3BY4K8dfsHryhWh2MJ6XHxxsRfcvPAyseH');
@ -65,7 +67,7 @@ describe('Address', function() {
var address = Address.derive('wallet-id', 'P2PKH', [{
xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1'
// PubKey(xPubKey/1/2) -> 0232c09a6edd8e2189628132d530c038e0b15b414cf3984e532358cbcfb83a7bd7
}], 'm/1/2', 1, 'livenet', true);
}], 'm/1/2', 1, 'btc', 'livenet', true);
should.exist(address);
address.walletId.should.equal('wallet-id');
address.address.should.equal('1G4wgi9YzmSSwQaQVLXQ5HUVquQDgJf8oT');

3
test/model/copayer.js

@ -17,7 +17,7 @@ describe('Copayer', function() {
});
});
describe('#createAddress', function() {
it('create an address', function() {
it('should create an address', function() {
var w = Wallet.fromObj(testWallet);
var c = Copayer.fromObj(testWallet.copayers[2]);
should.exist(c.requestPubKeys);
@ -42,6 +42,7 @@ var testWallet = {
createdOn: 1422904188,
id: '123',
name: '123 wallet',
network: 'livenet',
m: 2,
n: 3,
status: 'complete',

11
test/model/txproposal.js

@ -24,6 +24,11 @@ describe('TxProposal', function() {
should.exist(txp);
txp.amount.should.equal(aTXP().amount);
});
it('should default to BTC coin', function() {
var txp = TxProposal.fromObj(aTXP());
should.exist(txp);
txp.coin.should.equal('btc');
});
});
describe('#getBitcoreTx', function() {
@ -108,8 +113,10 @@ var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2
var theSignatures = ['304402201d210f731fa8cb8473ce49554382ad5d950c963d48b173a0591f13ed8cee10ce022027b30dc3a55c46b1f977a72491d338fc14b6d13a7b1a7c5a35950d8543c1ced6'];
var theRawTx = '0100000001ab069f7073be9b491bb1ad4233a45d2e383082ccc7206df905662d6d8499e66e08000000910047304402201d210f731fa8cb8473ce49554382ad5d950c963d48b173a0591f13ed8cee10ce022027b30dc3a55c46b1f977a72491d338fc14b6d13a7b1a7c5a35950d8543c1ced6014752210319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f2103b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d10130152aeffffffff0380969800000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac002d3101000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac70f62b040000000017a914778192003f0e9e1d865c082179cc3dae5464b03d8700000000';
var aTxpOpts = function(type) {
var aTxpOpts = function() {
var opts = {
coin: 'btc',
network: 'livenet',
message: 'some message'
};
opts.outputs = [{
@ -125,7 +132,7 @@ var aTxpOpts = function(type) {
return opts;
};
var aTXP = function(type) {
var aTXP = function() {
var txp = {
"version": 3,
"createdOn": 1423146231,

10
test/storage.js

@ -54,6 +54,8 @@ describe('Storage', function() {
name: 'my wallet',
m: 2,
n: 3,
coin: 'btc',
network: 'livenet',
});
should.exist(wallet);
storage.storeWallet(wallet, function(err) {
@ -85,9 +87,12 @@ describe('Storage', function() {
name: 'my wallet',
m: 2,
n: 3,
coin: 'btc',
network: 'livenet',
});
_.each(_.range(3), function(i) {
var copayer = Model.Copayer.create({
coin: 'btc',
name: 'copayer ' + i,
xPubKey: 'xPubKey ' + i,
requestPubKey: 'requestPubKey ' + i,
@ -127,9 +132,12 @@ describe('Storage', function() {
name: 'my wallet',
m: 2,
n: 3,
coin: 'btc',
network: 'livenet',
});
_.each(_.range(3), function(i) {
var copayer = Model.Copayer.create({
coin: 'btc',
name: 'copayer ' + i,
xPubKey: 'xPubKey ' + i,
requestPubKey: 'requestPubKey ' + i,
@ -144,6 +152,8 @@ describe('Storage', function() {
proposals = _.map(_.range(4), function(i) {
var tx = Model.TxProposal.create({
walletId: '123',
coin: 'btc',
network: 'livenet',
outputs: [{
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: i + 100,

37
test/testdata.js

@ -4,7 +4,8 @@ var keyPair = {
};
var copayers = [{
id44: '626452e5e0e35df4d9ae4d3e60653c9ae9a814f00c84dc40f5887069b18e2110',
id44btc: '626452e5e0e35df4d9ae4d3e60653c9ae9a814f00c84dc40f5887069b18e2110',
id44bch: '671fee02a6c1c4de2e2609f9f9a6180dc03acfff6b759fe0b13a616ed4880065',
id45: 'e7467366d5754be2b7d386c9737ab87214c26314bdc3489702e09c719be1bdb7',
xPrivKey: 'xprv9s21ZrQH143K2n4rV4AtAJFptEmd1tNMKCcSyQBCSuN5eq1dCUhcv6KQJS49joRxu8NNdFxy8yuwTtzCPNYUZvVGC7EPRm2st2cvE7oyTbB',
xPubKey: 'xpub661MyMwAqRbcFG9Kb5htXSCZSGc7RM6CgRY3mnap1Eu4XdLmk21sTtdt9iWAiL64KazU3QWrYEYSRAKgLvkMRe8JMxffDvt4AhCDzyMsnsT',
@ -17,7 +18,8 @@ var copayers = [{
privKey_1H_0: 'a710be25950738a7d13637e2e09affd7f579a3479fd7cc024bd9459f8fba6659',
pubKey_1H_0: '026e3020913420a5b9425952627f0a074c9235e7a329869b322061f786e997ae0d'
}, {
id44: '842c048066e7d10ae1bbf67edccf69f2e5ff9a754d0c2b5524f0d01a87d6acbb',
id44btc: '842c048066e7d10ae1bbf67edccf69f2e5ff9a754d0c2b5524f0d01a87d6acbb',
id44bch: '0d8f0c0ebfb11ad589002fd4539075c6fb625fb1725406ca442726c6bc6746b1',
id45: 'ee75154b646277c8d0d256fc1a0aa0470e4c3435497f208092c865737040b55b',
xPrivKey: 'xprv9s21ZrQH143K3BwkLceWNLUsgES15JoZuv8BZfnmDRcCGtDooUAPhY8KovhCWcRLXUun5AYL5vVtUNRrmPEibtfk9ongxAGLXZzEHifpvwZ',
xPubKey: 'xpub661MyMwAqRbcFg2DSeBWjURcEGGVUmXRH93nN4CNmm9B9gYxM1UeFLSofD6gtMnRYeucgPjfrWNxaAEhiT4di6HLty8Un6aheCKev4REvhZ',
@ -30,7 +32,8 @@ var copayers = [{
privKey_1H_0: 'ee062ce6dc5ece50e8110646b5e858c98dba9315cdfdd19da85ab0d33dcac74a',
pubKey_1H_0: '02c679bf169233a273dec87fae5a1830481866c4e96a350d56346ac267808c905d'
}, {
id44: '719f4ee61c691fbf0ebefa34e2151a1a3dbe39cf2fa4a498cb6af53600d30d1a',
id44btc: '719f4ee61c691fbf0ebefa34e2151a1a3dbe39cf2fa4a498cb6af53600d30d1a',
id44bch: '56ed2c8d04c4aa29e9d6408724197c27d1fa0b71e2c2a6b91a4cf9710f09eb0a',
id45: 'acd666d7c677d9f2c85b55a5fad1610fe272eac46ef7a577c7aeeab0b1474e43',
xPrivKey: 'xprv9s21ZrQH143K3xgLzxd6SuWqG5Zp1iUmyGgSsJVhdQNeTzAqBFvXXLZqZzFZqocTx4HD9vUVYU27At5i8q46LmBXXL97fo4H9C3tHm4BnjY',
xPubKey: 'xpub661MyMwAqRbcGSkp6zA6p3TZp7QJRBCdLVc3fguKBjudLnVyioEn58tKRFPmGMkdGJWMX69mgZWHKrKmpQ3fwBXeFjLc5Sd2rnxcQthSW42',
@ -43,7 +46,8 @@ var copayers = [{
privKey_1H_0: '5009c8488e9a364fc24a999d99a81ae955271de1d06d46c2f2f09e20c6281b04',
pubKey_1H_0: '03338a3b7c08e9d9832e1baff0758e08f9cc691497dd6e91d4c191cd960fb2f043'
}, {
id44: 'e225a29864060823df67b98432b070a40aad1bf9af517005b0b5fe09c96e29c9',
id44btc: 'e225a29864060823df67b98432b070a40aad1bf9af517005b0b5fe09c96e29c9',
id44bch: '2baf290be693407fd9c32597608b6fd90ba60f65b2b81b58e9fe9c960938de11',
id45: 'c65a89f64794cb7e1886c7010a32dd6fa362d3e81710bac32e97e325b9109fd8',
xPrivKey: 'xprv9s21ZrQH143K48nfuK14gKJtML7eQzV2dAH1RaqAMj8v2zs79uaavA9UTWMxpBdgbMH2mhJLeKGq8AFA6GDnFyWP4rLmknqZAfgFFV718vo',
xPubKey: 'xpub661MyMwAqRbcGcs91LY53TFcuMx8pTCszPCcDyEmv4ftuoCFhStqTxTxJoy35yjp2H3qQtxDYGe1gtkZu4T7mR7ARK1MLYte2fptZVt6hkD',
@ -56,7 +60,8 @@ var copayers = [{
privKey_1H_0: '460ee692f05de66b5d8e2fa1d005a8b6bdb1442e2ce6b3facfcee2f9012c9474',
pubKey_1H_0: '03d0e0c526619b158aac9a8de8082f439df43d389ec50cb54386c3d87cfde4c99b'
}, {
id44: '120416cd4c427a7e4d94213cebe242f56a06bc6dd5c5c6cae27dc920a0ddf1fb',
id44btc: '120416cd4c427a7e4d94213cebe242f56a06bc6dd5c5c6cae27dc920a0ddf1fb',
id44bch: '4abc36e7731c08e0a93483691c3cb451013463ccee1b676e4a20d98cd1de8af3',
id45: '65ae087eb9efdc7e0ada3a7ef954285e9e5ba4b8c7ab2d36747ddd286f7a334f',
xPrivKey: 'xprv9s21ZrQH143K44Bb9G3EVNmLfAUKjTBAA2YtKxF4zc8SLV1o15JBoddhGHE9PGLXePMbEsSjCCvTvP3fUv6yMXZrnHigBboRBn2DmNoJkJg',
xPubKey: 'xpub661MyMwAqRbcGYG4FHaErWi5DCJp8uu1XFUV8LegYwfRDHLwYccSMRxB7Z3L1NgKychKdXQvbVEyDhSwNnNnnNKh9mBEAdQ5tv2guK8ywKU',
@ -69,7 +74,8 @@ var copayers = [{
privKey_1H_0: '7a5158b92d9ed4cb9644ddbd472b43428832a5f3bb91a481532a081908e62b2e',
pubKey_1H_0: '02b47d5c977c93c883f369165ebc2b564d14a52712ec6892f7097fa99e0d36ca20'
}, {
id44: '85de9f025ee190fab7cb1bd9b6772c64df26188ce705d4f258c5adaf7bc610f9',
id44btc: '85de9f025ee190fab7cb1bd9b6772c64df26188ce705d4f258c5adaf7bc610f9',
id44bch: '0845739e508fb8f7b28e10bed9d827968a12d2dbd6ecbac3303305fcaf535bfe',
id45: 'dacc5c350cef4449a3ca12939711c7449d0d6189e5e7f33cff60095a7a29b0f9',
xPrivKey: 'xprv9s21ZrQH143K48PpVxrh71KdViTFhAaiDSVtNFkmbWNYjwwwPbTrcqoVXsgBfue3Gq9b71hQeEbk67JgtTBcpYgKLF8pTwVnGz56f1BaCYt',
xPubKey: 'xpub661MyMwAqRbcGcUHbzPhU9GN3kHk6dJZafRVAeAP9quXckH5w8n7Ae7yP8e2Zh6SPPKFn2K6oE3GBpcz9QzfJTNRWXbY7w1L3nGLE5beZL1',
@ -82,7 +88,8 @@ var copayers = [{
privKey_1H_0: '3c49816d4e83d8758f89e8e104e3566a8a61426a9b7d4945b34212fbbb8e8290',
pubKey_1H_0: '0307ab8c0d8eea1fe3c3781050a69e71f9e7c8cc8476a77103e08a461506a0e780'
}, {
id44: '4d0c1eaab0aafc08aea7328f9ed1d3fc2812791ad2ebb9cbc1a8537b51b18afa',
id44btc: '4d0c1eaab0aafc08aea7328f9ed1d3fc2812791ad2ebb9cbc1a8537b51b18afa',
id44bch: '63ed91d8b7c4f06028d4a795cbb30d91772d93c99e7cc612d9f0b33a4fa215de',
id45: '9129a0454adcf659f4f9d65a9b4dc4f9793bd1f59664268b56a7ef73f29f1b8a',
xPrivKey: 'xprv9s21ZrQH143K3pgRcRBRnmcxNkNNLmJrpneMkEXY6o5TWBuJLMfdRpAWdb2cG3yxbL4DxfpUnQpjfQUmwPdVrRGoDJmtAf5u8cyqKCoDV97',
xPubKey: 'xpub661MyMwAqRbcGJktiSiS9uZgvnCrkE2iC1ZxYcw9f8cSNzESstysycUzUsDCU6KnnjR29VZ1eRAXDgEXfYxGw1B9E7VLSAcHa9UuifSozmy',
@ -95,7 +102,8 @@ var copayers = [{
privKey_1H_0: '87f8a2b92dd04d2782c3d40a34f09f2ab42076bd02b81fbe4a4a72f87ad2e6df',
pubKey_1H_0: '02a0370d6f1213ab3390ac666585614ad71146f3f28ec326e2e779f999c1a497eb'
}, {
id44: '5ae7b75deb3b4d7e251f1fc5613904c9ef8548af7601d93ef668299be4f75ddd',
id44btc: '5ae7b75deb3b4d7e251f1fc5613904c9ef8548af7601d93ef668299be4f75ddd',
id44bch: '375a87b5614473ad359fee0385e9ffcb01d78c7880b34987e59da06eeac8029a',
id45: '37b81e2544b43ce7f37a132a748426e1566ecbb758564d4d7d07b716fbe1b368',
xPrivKey: 'xprv9s21ZrQH143K3nvcmdjDDDZbDJHpfWZCUiunwraZdcamYcafHvUnZfV51fivH9FPyfo12NyKH5JDxGLsQePyWKtTiJx3pkEaiwxsMLkVapp',
xPubKey: 'xpub661MyMwAqRbcGH15sfGDaMWKmL8K4yH3qwqPkEzBBx7kRQuoqTo37ToYrvLJh7JpV5FQSverERMcdF4HcP1UCiie2ayeMXRq67zr75PzMKs',
@ -108,7 +116,8 @@ var copayers = [{
privKey_1H_0: '66230b6b8b65725162ea43313fcc233f4f0dd135cea00d04b73a84d3f681ef25',
pubKey_1H_0: '03f148bde0784c80051acd159b28a30022e685aca56418f8f50100d9f8a0192c37'
}, {
id44: '98e78a9cb2ab340a245c5082897eadb28c367319f97b93e7b51b4d5ca5cdc68e',
id44btc: '98e78a9cb2ab340a245c5082897eadb28c367319f97b93e7b51b4d5ca5cdc68e',
id44bch: 'f390e03140593c0c724e0d3a2a9cf39d63319edc833024a149a72efacb368737',
id45: 'e1557d3421a8884fe007674f3f0b6f0feafa76289a0edcc5ec736161b4d02257',
xPrivKey: 'xprv9s21ZrQH143K2uYgqtYtphEQkFAgiWSqahFUWjgCdKykJagiNDz6Lf7xRVQdtZ7MvkhX9V3pEcK3xTAWZ6Y6ecJqrXnCpzrH9GSHn8wyrT5',
xPubKey: 'xpub661MyMwAqRbcFPd9wv5uBqB9JH1B7yAgwvB5K85pBfWjBP1rumJLtTSSGnCdsJSXfwmTyexsRjbUhzB4J6LWfL8mC2Ka117JrnXetyCzk3r',
@ -121,7 +130,8 @@ var copayers = [{
privKey_1H_0: '9e215580c8e5876215ad101ded325bcacc5ab9d97b26e8fdfab89ef5bb6e0ab7',
pubKey_1H_0: '0265d33caaa128a77cc38ab8751c7d730e0274a212f1f65b73f637eddb3a3fb151'
}, {
id44: 'f716dbeec58e44c698b34c2d81bae4699ed5a5a522281733ec50aa03caf76a19',
id44btc: 'f716dbeec58e44c698b34c2d81bae4699ed5a5a522281733ec50aa03caf76a19',
id44bch: 'da39b3d560d2d99d9557a5a70ca3dc4561c3930e2850748fa80bdcecb650a9bf',
id45: '8a6d840580549a34422c9b150dbd1e96e369c5db69ee736caab95616f8abb22b',
xPrivKey: 'xprv9s21ZrQH143K2wcRMP75tAEL5JnUx4xU2AbUBQzVVUDP7DHZJkjF3kaRE7tcnPLLLL9PGjYTWTJmCQPaQ4GGzgWEUFJ6snwJG9YnQHBFRNR',
xPubKey: 'xpub661MyMwAqRbcFRgtTQe6FJB4dLcyMXgKPPX4yoQ73okMz1chrJ3VbYtu5PRTxMBGuXt6eyqwAuG2BEBzQPLc1x8gnSQiATS3GRzKi1BuQAR',
@ -135,8 +145,8 @@ var copayers = [{
pubKey_1H_0: '0266cdb57b8a4d7c1b5b20ddeea43705420c6e3aef2c2979a3768b7b585839a0d3'
}, ];
var history = [
{
var history = [{
txid: "0279ef7b21630f859deb723e28beac9e7011660bd1346c2da40321d2f7e34f04",
vin: [{
txid: "c8e221141e8bb60977896561b77fa59d6dacfcc10db82bf6f5f923048b11c70d",
@ -175,8 +185,7 @@ var history = [
valueOut: 0.01345753,
valueIn: 0.01371235,
fees: 0.00025482
},
{
}, {
txid: "0279ef7b21630f859deb723e28beac9e7011660bd1346c2da40321d2f7e34f04",
vin: [{
txid: "c8e221141e8bb60977896561b77fa59d6dacfcc10db82bf6f5f923048b11c70d",

Loading…
Cancel
Save