Merge pull request #163 from isocolsky/feat/scan

Feat/scan
This commit is contained in:
Matias Alejo Garcia 2015-04-01 18:47:16 -03:00
commit d7964a8239
7 changed files with 340 additions and 11 deletions

View File

@ -29,6 +29,7 @@ function BlockChainExplorer(opts) {
}
var explorer = new Explorers.Insight(url, network);
explorer.getTransactions = _.bind(getTransactionsInsight, explorer, url);
explorer.getAddressActivity = _.bind(getAddressActivityInsight, explorer, url);
explorer.initSocket = _.bind(initSocketInsight, explorer, url);
return explorer;
default:
@ -36,10 +37,16 @@ function BlockChainExplorer(opts) {
};
};
function getTransactionsInsight(url, addresses, cb) {
function getTransactionsInsight(url, addresses, from, to, cb) {
var qs = [];
if (_.isNumber(from)) qs.push('from=' + from);
if (_.isNumber(to)) qs.push('to=' + to);
var url = url + '/api/addrs/txs' + (qs.length > 0 ? '?' + qs.join('&') : '');
request({
method: "POST",
url: url + '/api/addrs/txs',
url: url,
json: {
addrs: [].concat(addresses).join(',')
}
@ -49,6 +56,13 @@ function getTransactionsInsight(url, addresses, cb) {
});
};
function getAddressActivityInsight(url, addresses, cb) {
getTransactionsInsight(url, addresses, 0, 0, function(err, result) {
if (err) return cb(err);
return cb(null, result.totalItems > 0);
});
};
function initSocketInsight(url) {
var socket = io.connect(url, {});
return socket;

View File

@ -5,6 +5,8 @@ var _ = require('lodash');
var util = require('util');
var Uuid = require('uuid');
var Address = require('./address');
var AddressManager = require('./addressmanager');
var WalletUtils = require('bitcore-wallet-utils');
var Bitcore = WalletUtils.Bitcore;
@ -51,5 +53,13 @@ Copayer.fromObj = function(obj) {
return x;
};
Copayer.prototype.createAddress = function(wallet, isChange) {
$.checkState(wallet.isComplete());
var path = this.addressManager.getNewAddressPath(isChange);
var address = Address.create(WalletUtils.deriveAddress(wallet.publicKeyRing, path, wallet.m, wallet.network));
address.isChange = isChange;
return address;
};
module.exports = Copayer;

View File

@ -1024,7 +1024,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
});
},
function(next) {
bc.getTransactions(addressStrs, function(err, txs) {
bc.getTransactions(addressStrs, null, null, function(err, txs) {
if (err) return next(err);
next(null, self._normalizeTxHistory(txs));
@ -1044,5 +1044,83 @@ WalletService.prototype.getTxHistory = function(opts, cb) {
};
WalletService.scanConfig = {
SCAN_WINDOW: 20,
DERIVATION_DELAY: 10, // in milliseconds
};
/**
* Scan the blockchain looking for addresses having some activity
*
* @param {Object} opts
* @param {Boolean} opts.includeCopayerBranches (defaults to false)
*/
WalletService.prototype.scan = function(opts, cb) {
var self = this;
opts = opts || {};
var allAddresses = [];
function deriveAddresses(size, derivator, cb) {
async.mapSeries(_.range(size), function(i, next) {
setTimeout(function() {
next(null, derivator());
}, WalletService.scanConfig.DERIVATION_DELAY)
}, cb);
};
function checkActivity(addresses, cb) {
var bc = self._getBlockchainExplorer();
bc.getAddressActivity(addresses, cb);
};
function scanBranch(derivator, cb) {
var activity = true;
async.whilst(function() {
return activity;
}, function(next) {
deriveAddresses(WalletService.scanConfig.SCAN_WINDOW, derivator, function(err, addresses) {
if (err) return next(err);
allAddresses.push(addresses);
checkActivity(_.pluck(addresses, 'address'), function(err, thereIsActivity) {
if (err) return next(err);
activity = thereIsActivity;
next();
});
});
}, cb);
};
Utils.runLocked(self.walletId, cb, function(cb) {
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
if (!wallet.isComplete())
return cb(new ClientError('Wallet is not complete'));
var derivators = [];
_.each([false, true], function(isChange) {
derivators.push(_.bind(wallet.createAddress, wallet, isChange));
if (opts.includeCopayerBranches) {
_.each(wallet.copayers, function(copayer) {
derivators.push(_.bind(copayer.createAddress, copayer, wallet, isChange));
});
}
});
async.eachSeries(derivators, function(derivator, next) {
scanBranch(derivator, next);
}, function(err) {
if (err) return cb(err);
self.storage.storeAddressAndWallet(wallet, _.flatten(allAddresses), function(err) {
return cb(err);
});
});
});
});
};
module.exports = WalletService;
module.exports.ClientError = ClientError;

View File

@ -343,16 +343,20 @@ Storage.prototype.fetchAddresses = function(walletId, cb) {
});
};
Storage.prototype.storeAddressAndWallet = function(wallet, address, cb) {
var ops = [{
Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) {
var ops = _.map([].concat(addresses), function(address) {
return {
type: 'put',
key: KEY.ADDRESS(wallet.id, address.address),
value: address,
};
});
ops.unshift({
type: 'put',
key: KEY.WALLET(wallet.id),
value: wallet,
}, {
type: 'put',
key: KEY.ADDRESS(wallet.id, address.address),
value: address,
}, ];
});
this.db.batch(ops, cb);
};

View File

@ -16,6 +16,7 @@ describe('Blockchain explorer', function() {
should.exist(exp);
exp.should.respondTo('broadcast');
exp.should.respondTo('getTransactions');
exp.should.respondTo('getAddressActivity');
exp.should.respondTo('getUnspentUtxos');
exp.should.respondTo('initSocket');
var exp = BlockchainExplorer({

View File

@ -166,7 +166,13 @@ helpers.stubBroadcastFail = function() {
};
helpers.stubHistory = function(txs) {
blockchainExplorer.getTransactions = sinon.stub().callsArgWith(1, null, txs);
blockchainExplorer.getTransactions = sinon.stub().callsArgWith(3, null, txs);
};
helpers.stubAddressActivity = function(activeAddresses) {
blockchainExplorer.getAddressActivity = function(addresses, cb) {
return cb(null, _.intersection(activeAddresses, addresses).length > 0);
};
};
helpers.clientSign = WalletUtils.signTxp;
@ -2473,6 +2479,123 @@ describe('Wallet service', function() {
}, done);
});
});
describe('#scan', function() {
var scanConfigOld = WalletService.scanConfig;
beforeEach(function() {
this.timeout(5000);
WalletService.scanConfig.SCAN_WINDOW = 2;
WalletService.scanConfig.DERIVATION_DELAY = 0;
});
afterEach(function() {
WalletService.scanConfig = scanConfigOld;
});
it('should scan main addresses', function(done) {
helpers.stubAddressActivity(['3K2VWMXheGZ4qG35DyGjA2dLeKfaSr534A']);
helpers.createAndJoinWallet(1, 2, function(server, wallet) {
var expectedPaths = [
'm/2147483647/0/0',
'm/2147483647/0/1',
'm/2147483647/0/2',
'm/2147483647/0/3',
'm/2147483647/1/0',
'm/2147483647/1/1',
];
server.scan({}, function(err) {
should.not.exist(err);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
addresses.length.should.equal(expectedPaths.length);
var paths = _.pluck(addresses, 'path');
_.difference(paths, expectedPaths).length.should.equal(0);
server.createAddress({}, function(err, address) {
should.not.exist(err);
address.path.should.equal('m/2147483647/0/4');
done();
});
})
});
});
});
it('should scan main addresses & copayer addresses', function(done) {
helpers.stubAddressActivity(['3K2VWMXheGZ4qG35DyGjA2dLeKfaSr534A']);
helpers.createAndJoinWallet(1, 2, function(server, wallet) {
var expectedPaths = [
'm/2147483647/0/0',
'm/2147483647/0/1',
'm/2147483647/0/2',
'm/2147483647/0/3',
'm/2147483647/1/0',
'm/2147483647/1/1',
'm/0/0/0',
'm/0/0/1',
'm/0/1/0',
'm/0/1/1',
'm/1/0/0',
'm/1/0/1',
'm/1/1/0',
'm/1/1/1',
];
server.scan({
includeCopayerBranches: true
}, function(err) {
should.not.exist(err);
server.storage.fetchAddresses(wallet.id, function(err, addresses) {
should.exist(addresses);
addresses.length.should.equal(expectedPaths.length);
var paths = _.pluck(addresses, 'path');
_.difference(paths, expectedPaths).length.should.equal(0);
done();
})
});
});
});
it('should restore wallet balance', function(done) {
async.waterfall([
function(next) {
helpers.createAndJoinWallet(1, 2, function(server, wallet) {
helpers.stubUtxos(server, wallet, [1, 2, 3], function(utxos) {
should.exist(utxos);
helpers.stubAddressActivity(_.pluck(utxos, 'address'));
server.getBalance({}, function(err, balance) {
balance.totalAmount.should.equal(helpers.toSatoshi(6));
next(null, server, wallet);
});
});
});
},
function(server, wallet, next) {
server.removeWallet({}, function(err) {
next(err);
});
},
function(next) {
// NOTE: this works because it creates the exact same wallet!
helpers.createAndJoinWallet(1, 2, function(server, wallet) {
server.getBalance({}, function(err, balance) {
balance.totalAmount.should.equal(0);
next(null, server, wallet);
});
});
},
function(server, wallet, next) {
server.scan({}, function(err) {
should.not.exist(err);
server.getBalance(wallet.id, function(err, balance) {
balance.totalAmount.should.equal(helpers.toSatoshi(6));
next();
})
});
},
], function(err) {
should.not.exist(err);
done();
});
});
it.skip('should abort scan if there is an error checking address activity', function(done) {});
});
});

99
test/models/copayer.js Normal file
View File

@ -0,0 +1,99 @@
'use strict';
var _ = require('lodash');
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var Wallet = require('../../lib/model/wallet');
var Copayer = require('../../lib/model/copayer');
describe('Copayer', function() {
describe('#fromObj', function() {
it('read a copayer', function() {
var c = Copayer.fromObj(testWallet.copayers[0]);
c.name.should.equal('copayer 1');
});
});
describe('#createAddress', function() {
it('create an address', function() {
var w = Wallet.fromObj(testWallet);
var c = Copayer.fromObj(testWallet.copayers[2]);
var a1 = c.createAddress(w, true);
a1.address.should.equal('3AXmDe2FkWY9g5LpRaTs1U7pXKtkNm3NBf');
a1.path.should.equal('m/2/1/0');
a1.createdOn.should.be.above(1);
var a2 = c.createAddress(w, true);
a2.path.should.equal('m/2/1/1');
});
});
});
var testWallet = {
addressManager: {
receiveAddressIndex: 0,
changeAddressIndex: 0,
copayerIndex: 2147483647,
},
createdOn: 1422904188,
id: '123',
name: '123 wallet',
m: 2,
n: 3,
status: 'complete',
publicKeyRing: [{
xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9',
requestPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc'
}, {
xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o',
requestPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60'
}, {
xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd',
requestPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a'
}, ],
copayers: [{
addressManager: {
receiveAddressIndex: 0,
changeAddressIndex: 0,
copayerIndex: 0,
},
createdOn: 1422904189,
id: '1',
name: 'copayer 1',
xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9',
requestPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc',
signature: '30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46',
version: '1.0.0',
}, {
addressManager: {
receiveAddressIndex: 0,
changeAddressIndex: 0,
copayerIndex: 1,
},
createdOn: 1422904189,
id: '2',
name: 'copayer 2',
xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o',
requestPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60',
signature: '30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62',
version: '1.0.0',
}, {
addressManager: {
receiveAddressIndex: 0,
changeAddressIndex: 0,
copayerIndex: 2,
},
createdOn: 1422904189,
id: '3',
name: 'copayer 3',
xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd',
requestPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a',
signature: '304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c',
version: '1.0.0',
}],
version: '1.0.0',
pubKey: '{"x":"6092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80","y":"ba5e7082351115af6f8a9eb218979c7ed1f8aa94214f627ae624ab00048b8650","compressed":true}',
isTestnet: false
};