Merge pull request #390 from isocolsky/limit-address-gap
Limit nb of consecutive addresses without activity
This commit is contained in:
commit
5edfe3d384
|
@ -114,7 +114,9 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) {
|
|||
};
|
||||
|
||||
Insight.prototype.getAddressActivity = function(address, cb) {
|
||||
var url = this.url + this.apiPrefix + '/addr/' + address;
|
||||
var self = this;
|
||||
|
||||
var url = self.url + self.apiPrefix + '/addr/' + address;
|
||||
var args = {
|
||||
method: 'GET',
|
||||
url: url,
|
||||
|
|
|
@ -17,6 +17,7 @@ var errors = {
|
|||
INVALID_ADDRESS: 'Invalid address',
|
||||
KEY_IN_COPAYER: 'Key already registered',
|
||||
LOCKED_FUNDS: 'Funds are locked by pending transaction proposals',
|
||||
MAIN_ADDRESS_GAP_REACHED: 'Maximum number of consecutive addresses without activity reached',
|
||||
NOT_AUTHORIZED: 'Not authorized',
|
||||
TOO_MANY_KEYS: 'Too many keys registered',
|
||||
TX_ALREADY_BROADCASTED: 'The transaction proposal is already broadcasted',
|
||||
|
|
|
@ -259,8 +259,19 @@ ExpressApp.prototype.start = function(opts, cb) {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
// DEPRECATED
|
||||
router.post('/v1/addresses/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
server.createAddress({
|
||||
ignoreMaxGap: true
|
||||
}, function(err, address) {
|
||||
if (err) return returnError(err, res, req);
|
||||
res.json(address);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/v2/addresses/', function(req, res) {
|
||||
getServerWithAuth(req, res, function(server) {
|
||||
server.createAddress(req.body, function(err, address) {
|
||||
if (err) return returnError(err, res, req);
|
||||
|
|
|
@ -19,6 +19,7 @@ Address.create = function(opts) {
|
|||
x.publicKeys = opts.publicKeys;
|
||||
x.network = Bitcore.Address(x.address).toObject().network;
|
||||
x.type = opts.type || WalletUtils.SCRIPT_TYPES.P2SH;
|
||||
x.hasActivity = undefined;
|
||||
return x;
|
||||
};
|
||||
|
||||
|
@ -34,6 +35,7 @@ Address.fromObj = function(obj) {
|
|||
x.path = obj.path;
|
||||
x.publicKeys = obj.publicKeys;
|
||||
x.type = obj.type || WalletUtils.SCRIPT_TYPES.P2SH;
|
||||
x.hasActivity = obj.hasActivity;
|
||||
return x;
|
||||
};
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ var blockchainExplorerOpts;
|
|||
var messageBroker;
|
||||
var serviceVersion;
|
||||
|
||||
var MAX_KEYS = 100;
|
||||
|
||||
/**
|
||||
* Creates an instance of the Bitcore Wallet Service.
|
||||
|
@ -53,6 +52,8 @@ function WalletService() {
|
|||
};
|
||||
|
||||
|
||||
WalletService.MAX_KEYS = 100;
|
||||
|
||||
// Time after which a Tx proposal can be erased by any copayer. in seconds
|
||||
WalletService.DELETE_LOCKTIME = 24 * 3600;
|
||||
|
||||
|
@ -62,9 +63,11 @@ WalletService.BACKOFF_OFFSET = 3;
|
|||
// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in Minutes.
|
||||
WalletService.BACKOFF_TIME = 2;
|
||||
|
||||
WalletService.MAX_MAIN_ADDRESS_GAP = 20;
|
||||
|
||||
// Fund scanning parameters
|
||||
WalletService.SCAN_CONFIG = {
|
||||
maxGap: 20,
|
||||
maxGap: WalletService.MAX_MAIN_ADDRESS_GAP,
|
||||
};
|
||||
|
||||
WalletService.FEE_LEVELS = [{
|
||||
|
@ -518,7 +521,7 @@ WalletService.prototype.addAccess = function(opts, cb) {
|
|||
return cb(Errors.NOT_AUTHORIZED);
|
||||
}
|
||||
|
||||
if (copayer.requestPubKeys.length > MAX_KEYS)
|
||||
if (copayer.requestPubKeys.length > WalletService.MAX_KEYS)
|
||||
return cb(Errors.TOO_MANY_KEYS);
|
||||
|
||||
self._addKeyToCopayer(wallet, copayer, opts, cb);
|
||||
|
@ -689,20 +692,64 @@ WalletService.prototype.getPreferences = function(opts, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) {
|
||||
var self = this;
|
||||
|
||||
if (ignoreMaxGap) return cb(null, true);
|
||||
|
||||
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
|
||||
if (err) return cb(err);
|
||||
var latestAddresses = _.takeRight(_.reject(addresses, {
|
||||
isChange: true
|
||||
}), WalletService.MAX_MAIN_ADDRESS_GAP);
|
||||
if (latestAddresses.length < WalletService.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, {
|
||||
hasActivity: true
|
||||
})) return cb(null, true);
|
||||
|
||||
var bc = self._getBlockchainExplorer(latestAddresses[0].network);
|
||||
var activityFound = false;
|
||||
var i = latestAddresses.length;
|
||||
async.whilst(function() {
|
||||
return i > 0 && !activityFound;
|
||||
}, function(next) {
|
||||
bc.getAddressActivity(latestAddresses[--i].address, function(err, res) {
|
||||
if (err) return next(err);
|
||||
activityFound = !!res;
|
||||
return next();
|
||||
});
|
||||
}, function(err) {
|
||||
if (err) return cb(err);
|
||||
if (!activityFound) return cb(null, false);
|
||||
|
||||
var address = latestAddresses[i];
|
||||
address.hasActivity = true;
|
||||
self.storage.storeAddress(address, function(err) {
|
||||
return cb(err, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new address.
|
||||
* @param {Object} opts
|
||||
* @param {Boolean} [opts.ignoreMaxGap=false] - Ignore constraint of maximum number of consecutive addresses without activity
|
||||
* @returns {Address} address
|
||||
*/
|
||||
WalletService.prototype.createAddress = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
self._runLocked(cb, function(cb) {
|
||||
self.getWallet({}, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
|
||||
|
||||
self._canCreateAddress(opts.ignoreMaxGap, function(err, canCreate) {
|
||||
if (err) return cb(err);
|
||||
if (!canCreate) return cb(Errors.MAIN_ADDRESS_GAP_REACHED);
|
||||
|
||||
var address = wallet.createAddress(false);
|
||||
|
||||
self.storage.storeAddressAndWallet(wallet, address, function(err) {
|
||||
|
@ -716,6 +763,7 @@ WalletService.prototype.createAddress = function(opts, cb) {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -383,6 +383,17 @@ Storage.prototype.fetchAddresses = function(walletId, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
Storage.prototype.storeAddress = function(address, cb) {
|
||||
var self = this;
|
||||
|
||||
self.db.collection(collections.ADDRESSES).update({
|
||||
address: address.address
|
||||
}, address, {
|
||||
w: 1,
|
||||
upsert: false,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) {
|
||||
var self = this;
|
||||
|
||||
|
|
|
@ -1715,6 +1715,65 @@ describe('Wallet service', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create more consecutive addresses with no activity than allowed', function(done) {
|
||||
var MAX_MAIN_ADDRESS_GAP_old = WalletService.MAX_MAIN_ADDRESS_GAP;
|
||||
WalletService.MAX_MAIN_ADDRESS_GAP = 2;
|
||||
helpers.stubAddressActivity([]);
|
||||
async.map(_.range(2), function(i, next) {
|
||||
server.createAddress({}, next);
|
||||
}, function(err, addresses) {
|
||||
addresses.length.should.equal(2);
|
||||
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.exist(err);
|
||||
should.not.exist(address);
|
||||
err.code.should.equal('MAIN_ADDRESS_GAP_REACHED');
|
||||
server.createAddress({
|
||||
ignoreMaxGap: true
|
||||
}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
should.exist(address);
|
||||
address.path.should.equal('m/0/2');
|
||||
|
||||
helpers.stubAddressActivity([
|
||||
'1GdXraZ1gtoVAvBh49D4hK9xLm6SKgesoE', // m/0/2
|
||||
]);
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
should.exist(address);
|
||||
address.path.should.equal('m/0/3');
|
||||
|
||||
WalletService.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should cache address activity', function(done) {
|
||||
var MAX_MAIN_ADDRESS_GAP_old = WalletService.MAX_MAIN_ADDRESS_GAP;
|
||||
WalletService.MAX_MAIN_ADDRESS_GAP = 2;
|
||||
helpers.stubAddressActivity([]);
|
||||
async.map(_.range(2), function(i, next) {
|
||||
server.createAddress({}, next);
|
||||
}, function(err, addresses) {
|
||||
addresses.length.should.equal(2);
|
||||
|
||||
helpers.stubAddressActivity([addresses[1].address]);
|
||||
var getAddressActivitySpy = sinon.spy(blockchainExplorer, 'getAddressActivity');
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
server.createAddress({}, function(err, address) {
|
||||
should.not.exist(err);
|
||||
getAddressActivitySpy.callCount.should.equal(1);
|
||||
WalletService.MAX_MAIN_ADDRESS_GAP = MAX_MAIN_ADDRESS_GAP_old;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue