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 - g++-4.8
- clang - clang
node_js: node_js:
- '4' - '8'
before_install: before_install:
- export CXX="g++-4.8" CC="gcc-4.8" - export CXX="g++-4.8" CC="gcc-4.8"
install: install:

30
config.js

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

20
lib/blockchainexplorer.js

@ -6,11 +6,20 @@ var log = require('npmlog');
log.debug = log.verbose; log.debug = log.verbose;
var Insight = require('./blockchainexplorers/insight'); var Insight = require('./blockchainexplorers/insight');
var Common = require('./common');
var Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
var PROVIDERS = { var PROVIDERS = {
'insight': { 'insight': {
'livenet': 'https://insight.bitpay.com:443', 'btc': {
'testnet': 'https://test-insight.bitpay.com:443', '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); $.checkArgument(opts);
var provider = opts.provider || 'insight'; var provider = opts.provider || 'insight';
var coin = opts.coin || Defaults.COIN;
var network = opts.network || 'livenet'; var network = opts.network || 'livenet';
$.checkState(PROVIDERS[provider], 'Provider ' + provider + ' not supported'); $.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) { switch (provider) {
case 'insight': case 'insight':
return new Insight({ return new Insight({
coin: coin,
network: network, network: network,
url: url, url: url,
apiPrefix: opts.apiPrefix, apiPrefix: opts.apiPrefix,

10
lib/blockchainexplorers/insight.js

@ -6,13 +6,19 @@ var log = require('npmlog');
log.debug = log.verbose; log.debug = log.verbose;
var io = require('socket.io-client'); var io = require('socket.io-client');
var requestList = require('./request-list'); var requestList = require('./request-list');
var Common = require('../common');
var Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
function Insight(opts) { function Insight(opts) {
$.checkArgument(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); $.checkArgument(opts.url);
this.apiPrefix = opts.apiPrefix || '/api'; this.apiPrefix = opts.apiPrefix || '/api';
this.coin = opts.coin || Defaults.COIN;
this.network = opts.network || 'livenet'; this.network = opts.network || 'livenet';
this.hosts = opts.url; this.hosts = opts.url;
this.userAgent = opts.userAgent || 'bws'; this.userAgent = opts.userAgent || 'bws';
@ -39,7 +45,7 @@ Insight.prototype._doRequest = function(args, cb) {
}; };
Insight.prototype.getConnectionInfo = function() { 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 Notification = require('./model/notification');
var WalletService = require('./server'); var WalletService = require('./server');
var Constants = require('./common/constants');
function BlockchainMonitor() {}; function BlockchainMonitor() {};
@ -25,26 +26,42 @@ BlockchainMonitor.prototype.start = function(opts, cb) {
async.parallel([ async.parallel([
function(done) { function(done) {
self.explorers = {}; self.explorers = {
_.map(['livenet', 'testnet'], function(network) { 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; var explorer;
if (opts.blockchainExplorers) { if (opts.blockchainExplorers && opts.blockchainExplorers[pair.coin] && opts.blockchainExplorers[pair.coin][pair.network]) {
explorer = opts.blockchainExplorers[network]; explorer = opts.blockchainExplorers[pair.coin][pair.network];
} else { } else {
var config = {} var config = {}
if (opts.blockchainExplorerOpts && opts.blockchainExplorerOpts[network]) { if (opts.blockchainExplorerOpts && opts.blockchainExplorerOpts[pair.coin] && opts.blockchainExplorerOpts[pair.coin][pair.network]) {
config = opts.blockchainExplorerOpts[network]; config = opts.blockchainExplorerOpts[pair.coin][pair.network];
} else {
return;
} }
var explorer = new BlockchainExplorer({ var explorer = new BlockchainExplorer({
provider: config.provider, provider: config.provider,
network: network, coin: pair.coin,
network: pair.network,
url: config.url, url: config.url,
userAgent: WalletService.getServiceVersion(), userAgent: WalletService.getServiceVersion(),
}); });
} }
$.checkState(explorer); $.checkState(explorer);
self._initExplorer(network, explorer); self._initExplorer(pair.coin, pair.network, explorer);
self.explorers[network] = explorer; self.explorers[pair.coin][pair.network] = explorer;
}); });
done(); 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 self = this;
var socket = explorer.initSocket(); var socket = explorer.initSocket();
@ -85,8 +102,8 @@ BlockchainMonitor.prototype._initExplorer = function(network, explorer) {
socket.on('connect_error', function() { socket.on('connect_error', function() {
log.error('Error connecting to ' + explorer.getConnectionInfo()); log.error('Error connecting to ' + explorer.getConnectionInfo());
}); });
socket.on('tx', _.bind(self._handleIncomingTx, self)); socket.on('tx', _.bind(self._handleIncomingTx, self, coin, network));
socket.on('block', _.bind(self._handleNewBlock, self, network)); socket.on('block', _.bind(self._handleNewBlock, self, coin, network));
}; };
BlockchainMonitor.prototype._handleThirdPartyBroadcasts = function(data, processIt) { 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; var self = this;
if (!data || !data.vout) return; if (!data || !data.vout) return;
@ -149,7 +166,7 @@ BlockchainMonitor.prototype._handleIncomingPayments = function(data) {
if (_.isEmpty(outs)) return; if (_.isEmpty(outs)) return;
async.each(outs, function(out, next) { 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) { if (err) {
log.error('Could not fetch addresses from the db'); log.error('Could not fetch addresses from the db');
return next(err); 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._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; var self = this;
log.info('New ' + network + ' block: ' + hash); 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 walletId: network, // use network name as wallet id for global notifications
data: { data: {
hash: hash, hash: hash,
coin: coin,
network: network, 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; var self = this;
function processTriggeredSubs(subs, cb) { function processTriggeredSubs(subs, cb) {
@ -243,6 +261,7 @@ BlockchainMonitor.prototype._handleTxConfirmations = function(network, hash) {
creatorId: sub.copayerId, creatorId: sub.copayerId,
data: { data: {
txid: sub.txid, txid: sub.txid,
coin: coin,
network: network, network: network,
// TODO: amount // 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; if (!explorer) return;
explorer.getTxidsInBlock(hash, function(err, txids) { explorer.getTxidsInBlock(hash, function(err, txids) {
@ -279,9 +298,9 @@ BlockchainMonitor.prototype._handleTxConfirmations = function(network, hash) {
}); });
}; };
BlockchainMonitor.prototype._handleNewBlock = function(network, hash) { BlockchainMonitor.prototype._handleNewBlock = function(coin, network, hash) {
this._notifyNewBlock(network, hash); this._notifyNewBlock(coin, network, hash);
this._handleTxConfirmations(network, hash); this._handleTxConfirmations(coin, network, hash);
}; };
BlockchainMonitor.prototype._storeAndBroadcastNotification = function(notification, cb) { BlockchainMonitor.prototype._storeAndBroadcastNotification = function(notification, cb) {

5
lib/common/constants.js

@ -2,6 +2,11 @@
var Constants = {}; var Constants = {};
Constants.COINS = {
BTC: 'btc',
BCH: 'bch',
};
Constants.NETWORKS = { Constants.NETWORKS = {
LIVENET: 'livenet', LIVENET: 'livenet',
TESTNET: 'testnet', 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 // TODO: should allow different gap sizes for external/internal chains
Defaults.SCAN_ADDRESS_GAP = Defaults.MAX_MAIN_ADDRESS_GAP + 20; Defaults.SCAN_ADDRESS_GAP = Defaults.MAX_MAIN_ADDRESS_GAP + 20;
Defaults.FEE_LEVELS = [{ Defaults.FEE_LEVELS = {
name: 'urgent', btc: [{
nbBlocks: 2, name: 'urgent',
multiplier: 1.5, nbBlocks: 2,
defaultValue: 150000, multiplier: 1.5,
}, { defaultValue: 150000,
name: 'priority', }, {
nbBlocks: 2, name: 'priority',
defaultValue: 100000 nbBlocks: 2,
}, { defaultValue: 100000
name: 'normal', }, {
nbBlocks: 3, name: 'normal',
defaultValue: 80000 nbBlocks: 3,
}, { defaultValue: 80000
name: 'economy', }, {
nbBlocks: 6, name: 'economy',
defaultValue: 50000 nbBlocks: 6,
}, { defaultValue: 50000
name: 'superEconomy', }, {
nbBlocks: 24, name: 'superEconomy',
defaultValue: 20000 nbBlocks: 24,
}]; defaultValue: 20000
}],
Defaults.DEFAULT_FEE_PER_KB = Defaults.FEE_LEVELS[1].defaultValue; 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 // How many levels to fallback to if the value returned by the network for a given nbBlocks is -1
Defaults.FEE_LEVELS_FALLBACK = 2; Defaults.FEE_LEVELS_FALLBACK = 2;
@ -109,4 +114,6 @@ Defaults.RateLimit = {
// }, // },
}; };
Defaults.COIN = 'btc';
module.exports = Defaults; module.exports = Defaults;

10
lib/common/utils.js

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

3
lib/expressapp.js

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

31
lib/model/address.js

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

4
lib/model/addressmanager.js

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

18
lib/model/copayer.js

@ -10,12 +10,16 @@ var Address = require('./address');
var AddressManager = require('./addressmanager'); var AddressManager = require('./addressmanager');
var Bitcore = require('bitcore-lib'); 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() {}; function Copayer() {};
Copayer._xPubToCopayerId = function(xpub) { Copayer._xPubToCopayerId = function(coin, xpub) {
var hash = sjcl.hash.sha256.hash(xpub); var str = coin == Defaults.COIN ? xpub : coin + xpub;
var hash = sjcl.hash.sha256.hash(str);
return sjcl.codec.hex.fromBits(hash); return sjcl.codec.hex.fromBits(hash);
}; };
@ -25,14 +29,17 @@ Copayer.create = function(opts) {
.checkArgument(opts.requestPubKey, 'Missing copayer request public key') .checkArgument(opts.requestPubKey, 'Missing copayer request public key')
.checkArgument(opts.signature, 'Missing copayer request public key signature'); .checkArgument(opts.signature, 'Missing copayer request public key signature');
$.checkArgument(Utils.checkValueInCollection(opts.coin, Constants.COINS));
opts.copayerIndex = opts.copayerIndex || 0; opts.copayerIndex = opts.copayerIndex || 0;
var x = new Copayer(); var x = new Copayer();
x.version = 2; x.version = 2;
x.createdOn = Math.floor(Date.now() / 1000); x.createdOn = Math.floor(Date.now() / 1000);
x.coin = opts.coin;
x.xPubKey = opts.xPubKey; x.xPubKey = opts.xPubKey;
x.id = Copayer._xPubToCopayerId(x.xPubKey); x.id = Copayer._xPubToCopayerId(opts.coin, 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;
@ -59,6 +66,7 @@ Copayer.fromObj = function(obj) {
x.version = obj.version; x.version = obj.version;
x.createdOn = obj.createdOn; x.createdOn = obj.createdOn;
x.coin = obj.coin || Defaults.COIN;
x.id = obj.id; x.id = obj.id;
x.name = obj.name; x.name = obj.name;
x.xPubKey = obj.xPubKey; x.xPubKey = obj.xPubKey;
@ -87,7 +95,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 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; return address;
}; };

40
lib/model/txproposal.js

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

5
lib/model/txproposal_legacy.js

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

16
lib/model/wallet.js

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

226
lib/server.js

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

62
lib/storage.js

@ -461,6 +461,7 @@ Storage.prototype.storeAddress = function(address, cb) {
var self = this; var self = this;
self.db.collection(collections.ADDRESSES).update({ self.db.collection(collections.ADDRESSES).update({
walletId: address.walletId,
address: address.address address: address.address
}, address, { }, address, {
w: 1, w: 1,
@ -471,45 +472,22 @@ Storage.prototype.storeAddress = function(address, cb) {
Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) { Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) {
var self = this; 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); 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).insert(addresses, {
self.db.collection(collections.ADDRESSES).findOne({ w: 1
address: address.address, }, function(err) {
}, { if (err) return cb(err);
walletId: true, self.storeWallet(wallet, cb);
}, 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);
});
}); });
}; };
Storage.prototype.fetchAddress = function(address, cb) { Storage.prototype.fetchAddressByWalletId = function(walletId, address, cb) {
var self = this; var self = this;
this.db.collection(collections.ADDRESSES).findOne({ this.db.collection(collections.ADDRESSES).findOne({
walletId: walletId,
address: address, address: address,
}, function(err, result) { }, function(err, result) {
if (err) return cb(err); 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) { Storage.prototype.fetchPreferences = function(walletId, copayerId, cb) {
this.db.collection(collections.PREFERENCES).find({ this.db.collection(collections.PREFERENCES).find({
walletId: walletId, walletId: walletId,
@ -610,6 +609,7 @@ Storage.prototype.cleanActiveAddresses = function(walletId, cb) {
Storage.prototype.storeActiveAddresses = function(walletId, addresses, cb) { Storage.prototype.storeActiveAddresses = function(walletId, addresses, cb) {
var self = this; var self = this;
if (_.isEmpty(addresses)) return cb();
async.each(addresses, function(address, next) { async.each(addresses, function(address, next) {
var record = { var record = {
walletId: walletId, walletId: walletId,

25
package.json

@ -22,6 +22,7 @@
"dependencies": { "dependencies": {
"async": "^0.9.2", "async": "^0.9.2",
"bitcore-lib": "^0.14.0", "bitcore-lib": "^0.14.0",
"bitcore-lib-cash": "https://github.com/bitpay/bitcore-lib.git#cash",
"body-parser": "^1.11.0", "body-parser": "^1.11.0",
"compression": "^1.6.2", "compression": "^1.6.2",
"coveralls": "^2.11.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" "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", {
"email": "braydon@bitpay.com" "name": "Braydon Fuller",
}, { "email": "braydon@bitpay.com"
"name": "Ivan Socolsky", },
"email": "ivan@bitpay.com" {
}, { "name": "Ivan Socolsky",
"name": "Matias Alejo Garcia", "email": "ivan@bitpay.com"
"email": "ematiu@gmail.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, messageBroker: server.messageBroker,
storage: storage, storage: storage,
blockchainExplorers: { blockchainExplorers: {
'testnet': blockchainExplorer, 'btc': {
'livenet': blockchainExplorer 'testnet': blockchainExplorer,
'livenet': blockchainExplorer
}
}, },
}, function(err) { }, function(err) {
should.not.exist(err); should.not.exist(err);

4
test/integration/emailnotifications.js

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

33
test/integration/helpers.js

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

4
test/integration/pushNotifications.js

@ -368,7 +368,7 @@ describe('Push notifications', function() {
txpId = txp.id; txpId = txp.id;
async.eachSeries(_.range(1, 3), function(i, next) { async.eachSeries(_.range(1, 3), function(i, next) {
var copayer = TestData.copayers[i]; var copayer = TestData.copayers[i];
helpers.getAuthServer(copayer.id44, function(server) { helpers.getAuthServer(copayer.id44btc, function(server) {
server.rejectTx({ server.rejectTx({
txProposalId: txp.id, txProposalId: txp.id,
}, next); }, next);
@ -413,7 +413,7 @@ describe('Push notifications', function() {
txp = t; txp = t;
async.eachSeries(_.range(1, 3), function(i, next) { async.eachSeries(_.range(1, 3), function(i, next) {
var copayer = TestData.copayers[i]; var copayer = TestData.copayers[i];
helpers.getAuthServer(copayer.id44, function(s) { helpers.getAuthServer(copayer.id44btc, function(s) {
server = s; server = s;
var signatures = helpers.clientSign(txp, copayer.xPrivKey_44H_0H_0H); var signatures = helpers.clientSign(txp, copayer.xPrivKey_44H_0H_0H);
server.signTx({ 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() { describe('Address derivation strategy', function() {
var server; var server;
beforeEach(function() { 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) { it('should return copayer in wallet error before full wallet', function(done) {
helpers.createAndJoinWallet(1, 1, function(s, wallet) { helpers.createAndJoinWallet(1, 1, function(s, wallet) {
var copayerOpts = helpers.getSignedCopayerOpts({ 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) { it('should create many addresses on simultaneous requests', function(done) {
var N = 5; var N = 5;
async.mapSeries(_.range(N), function(i, cb) { async.mapSeries(_.range(N), function(i, cb) {
@ -1259,7 +1276,7 @@ describe('Wallet service', function() {
helpers.createAndJoinWallet(1, 1, function(s, w) { helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s; server = s;
wallet = w; wallet = w;
w.copayers[0].id.should.equal(TestData.copayers[0].id44); w.copayers[0].id.should.equal(TestData.copayers[0].id44btc);
done(); done();
}); });
}); });
@ -1638,7 +1655,7 @@ describe('Wallet service', function() {
var requestPubKeyStr = requestPubKey.toString(); var requestPubKeyStr = requestPubKey.toString();
var sig = helpers.signRequestPubKey(requestPubKeyStr, xPrivKey); 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 = { opts = {
copayerId: copayerId, copayerId: copayerId,
requestPubKey: requestPubKeyStr, requestPubKey: requestPubKeyStr,
@ -2178,28 +2195,30 @@ describe('Wallet service', function() {
var server, wallet, levels; var server, wallet, levels;
before(function() { before(function() {
levels = Defaults.FEE_LEVELS; levels = Defaults.FEE_LEVELS;
Defaults.FEE_LEVELS = [{ Defaults.FEE_LEVELS = {
name: 'urgent', btc: [{
nbBlocks: 1, name: 'urgent',
multiplier: 1.5, nbBlocks: 1,
defaultValue: 50000, multiplier: 1.5,
}, { defaultValue: 50000,
name: 'priority', }, {
nbBlocks: 1, name: 'priority',
defaultValue: 50000 nbBlocks: 1,
}, { defaultValue: 50000
name: 'normal', }, {
nbBlocks: 2, name: 'normal',
defaultValue: 40000 nbBlocks: 2,
}, { defaultValue: 40000
name: 'economy', }, {
nbBlocks: 6, name: 'economy',
defaultValue: 25000 nbBlocks: 6,
}, { defaultValue: 25000
name: 'superEconomy', }, {
nbBlocks: 24, name: 'superEconomy',
defaultValue: 10000 nbBlocks: 24,
}]; defaultValue: 10000
}]
};
}); });
after(function() { after(function() {
Defaults.FEE_LEVELS = levels; Defaults.FEE_LEVELS = levels;
@ -2248,7 +2267,7 @@ describe('Wallet service', function() {
fees = _.zipObject(_.map(fees, function(item) { fees = _.zipObject(_.map(fees, function(item) {
return [item.level, item.feePerKb]; 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]; return [item.name, item.defaultValue];
})); }));
fees.priority.should.equal(defaults.priority); fees.priority.should.equal(defaults.priority);
@ -2311,7 +2330,7 @@ describe('Wallet service', function() {
}); });
}); });
it('should get monotonically decreasing fee values', function(done) { it('should get monotonically decreasing fee values', function(done) {
_.find(Defaults.FEE_LEVELS, { _.find(Defaults.FEE_LEVELS['btc'], {
nbBlocks: 6 nbBlocks: 6
}).defaultValue.should.equal(25000); }).defaultValue.should.equal(25000);
helpers.stubFeeLevels({ helpers.stubFeeLevels({
@ -3280,7 +3299,7 @@ describe('Wallet service', function() {
var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H; var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H;
var accessOpts = { var accessOpts = {
copayerId: TestData.copayers[0].id44, copayerId: TestData.copayers[0].id44btc,
requestPubKey: reqPubKey, requestPubKey: reqPubKey,
signature: helpers.signRequestPubKey(reqPubKey, xPrivKey), signature: helpers.signRequestPubKey(reqPubKey, xPrivKey),
}; };
@ -6603,6 +6622,7 @@ describe('Wallet service', function() {
blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 2000); blockchainExplorer.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 2000);
server._notify('NewBlock', { server._notify('NewBlock', {
coin: 'btc',
network: 'livenet', network: 'livenet',
hash: 'dummy hash', 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() { it('should create livenet address', function() {
var x = Address.create({ var x = Address.create({
address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg', address: '3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg',
coin: 'btc',
walletId: '123', walletId: '123',
isChange: false, isChange: false,
path: 'm/0/1', path: 'm/0/1',
@ -23,6 +24,7 @@ describe('Address', function() {
it('should create testnet address', function() { it('should create testnet address', function() {
var x = Address.create({ var x = Address.create({
address: 'mp5xaa4uBj16DJt1fuA3D9fejHuCzeb7hj', address: 'mp5xaa4uBj16DJt1fuA3D9fejHuCzeb7hj',
coin: 'btc',
walletId: '123', walletId: '123',
isChange: false, isChange: false,
path: 'm/0/1', path: 'm/0/1',
@ -39,7 +41,7 @@ describe('Address', function() {
}, { }, {
xPubKey: 'xpub68tpbrfk747AvDUCdtEUgK2yDPmtGKf7YXzEcUUqnF3jmAMeZgcpoZqgXwwoi8CpwDkyzVX6wxUktTw2wh9EhhVjh5S71MLL3FkZDGF5GeY' xPubKey: 'xpub68tpbrfk747AvDUCdtEUgK2yDPmtGKf7YXzEcUUqnF3jmAMeZgcpoZqgXwwoi8CpwDkyzVX6wxUktTw2wh9EhhVjh5S71MLL3FkZDGF5GeY'
// PubKey(xPubKey/0/0) -> 03162179906dbe6a67979d4f8f46ee1db6ff81715f465e6615a4f5969478ad2171 // PubKey(xPubKey/0/0) -> 03162179906dbe6a67979d4f8f46ee1db6ff81715f465e6615a4f5969478ad2171
}], 'm/0/0', 1, 'livenet', false); }], 'm/0/0', 1, 'btc', 'livenet', false);
should.exist(address); should.exist(address);
address.walletId.should.equal('wallet-id'); address.walletId.should.equal('wallet-id');
address.address.should.equal('3QN2CiSxcUsFuRxZJwXMNDQ2esnr5RXTvw'); address.address.should.equal('3QN2CiSxcUsFuRxZJwXMNDQ2esnr5RXTvw');
@ -52,7 +54,7 @@ describe('Address', function() {
var address = Address.derive('wallet-id', 'P2SH', [{ var address = Address.derive('wallet-id', 'P2SH', [{
xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1' xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1'
// PubKey(xPubKey/0/0) -> 03fe466ea829aa4c9a1c289f9ba61ebc26a61816500860c8d23f94aad9af152ecd // PubKey(xPubKey/0/0) -> 03fe466ea829aa4c9a1c289f9ba61ebc26a61816500860c8d23f94aad9af152ecd
}], 'm/0/0', 1, 'livenet', false); }], 'm/0/0', 1, 'btc', 'livenet', false);
should.exist(address); should.exist(address);
address.walletId.should.equal('wallet-id'); address.walletId.should.equal('wallet-id');
address.address.should.equal('3BY4K8dfsHryhWh2MJ6XHxxsRfcvPAyseH'); address.address.should.equal('3BY4K8dfsHryhWh2MJ6XHxxsRfcvPAyseH');
@ -65,7 +67,7 @@ describe('Address', function() {
var address = Address.derive('wallet-id', 'P2PKH', [{ var address = Address.derive('wallet-id', 'P2PKH', [{
xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1' xPubKey: 'xpub686v8eJUJEqxzAtkWPyQ9nvpBHfucVsB8Q8HQHw5mxYPQtBact2rmA8wRXFYaVESK8f7WrxeU4ayALaEhicdXCX5ZHktNeRFnvFeffztiY1'
// PubKey(xPubKey/1/2) -> 0232c09a6edd8e2189628132d530c038e0b15b414cf3984e532358cbcfb83a7bd7 // PubKey(xPubKey/1/2) -> 0232c09a6edd8e2189628132d530c038e0b15b414cf3984e532358cbcfb83a7bd7
}], 'm/1/2', 1, 'livenet', true); }], 'm/1/2', 1, 'btc', 'livenet', true);
should.exist(address); should.exist(address);
address.walletId.should.equal('wallet-id'); address.walletId.should.equal('wallet-id');
address.address.should.equal('1G4wgi9YzmSSwQaQVLXQ5HUVquQDgJf8oT'); address.address.should.equal('1G4wgi9YzmSSwQaQVLXQ5HUVquQDgJf8oT');

3
test/model/copayer.js

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

11
test/model/txproposal.js

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

10
test/storage.js

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

37
test/testdata.js

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

Loading…
Cancel
Save