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) {
|
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 = {
|
var args = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: url,
|
url: url,
|
||||||
|
|
|
@ -17,6 +17,7 @@ var errors = {
|
||||||
INVALID_ADDRESS: 'Invalid address',
|
INVALID_ADDRESS: 'Invalid address',
|
||||||
KEY_IN_COPAYER: 'Key already registered',
|
KEY_IN_COPAYER: 'Key already registered',
|
||||||
LOCKED_FUNDS: 'Funds are locked by pending transaction proposals',
|
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',
|
NOT_AUTHORIZED: 'Not authorized',
|
||||||
TOO_MANY_KEYS: 'Too many keys registered',
|
TOO_MANY_KEYS: 'Too many keys registered',
|
||||||
TX_ALREADY_BROADCASTED: 'The transaction proposal is already broadcasted',
|
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) {
|
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) {
|
getServerWithAuth(req, res, function(server) {
|
||||||
server.createAddress(req.body, function(err, address) {
|
server.createAddress(req.body, function(err, address) {
|
||||||
if (err) return returnError(err, res, req);
|
if (err) return returnError(err, res, req);
|
||||||
|
|
|
@ -19,6 +19,7 @@ Address.create = function(opts) {
|
||||||
x.publicKeys = opts.publicKeys;
|
x.publicKeys = opts.publicKeys;
|
||||||
x.network = Bitcore.Address(x.address).toObject().network;
|
x.network = Bitcore.Address(x.address).toObject().network;
|
||||||
x.type = opts.type || WalletUtils.SCRIPT_TYPES.P2SH;
|
x.type = opts.type || WalletUtils.SCRIPT_TYPES.P2SH;
|
||||||
|
x.hasActivity = undefined;
|
||||||
return x;
|
return x;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ Address.fromObj = function(obj) {
|
||||||
x.path = obj.path;
|
x.path = obj.path;
|
||||||
x.publicKeys = obj.publicKeys;
|
x.publicKeys = obj.publicKeys;
|
||||||
x.type = obj.type || WalletUtils.SCRIPT_TYPES.P2SH;
|
x.type = obj.type || WalletUtils.SCRIPT_TYPES.P2SH;
|
||||||
|
x.hasActivity = obj.hasActivity;
|
||||||
return x;
|
return x;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,6 @@ var blockchainExplorerOpts;
|
||||||
var messageBroker;
|
var messageBroker;
|
||||||
var serviceVersion;
|
var serviceVersion;
|
||||||
|
|
||||||
var MAX_KEYS = 100;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of the Bitcore Wallet Service.
|
* 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
|
// Time after which a Tx proposal can be erased by any copayer. in seconds
|
||||||
WalletService.DELETE_LOCKTIME = 24 * 3600;
|
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.
|
// 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.BACKOFF_TIME = 2;
|
||||||
|
|
||||||
|
WalletService.MAX_MAIN_ADDRESS_GAP = 20;
|
||||||
|
|
||||||
// Fund scanning parameters
|
// Fund scanning parameters
|
||||||
WalletService.SCAN_CONFIG = {
|
WalletService.SCAN_CONFIG = {
|
||||||
maxGap: 20,
|
maxGap: WalletService.MAX_MAIN_ADDRESS_GAP,
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService.FEE_LEVELS = [{
|
WalletService.FEE_LEVELS = [{
|
||||||
|
@ -518,7 +521,7 @@ WalletService.prototype.addAccess = function(opts, cb) {
|
||||||
return cb(Errors.NOT_AUTHORIZED);
|
return cb(Errors.NOT_AUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (copayer.requestPubKeys.length > MAX_KEYS)
|
if (copayer.requestPubKeys.length > WalletService.MAX_KEYS)
|
||||||
return cb(Errors.TOO_MANY_KEYS);
|
return cb(Errors.TOO_MANY_KEYS);
|
||||||
|
|
||||||
self._addKeyToCopayer(wallet, copayer, opts, cb);
|
self._addKeyToCopayer(wallet, copayer, opts, cb);
|
||||||
|
@ -689,29 +692,74 @@ 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.
|
* Creates a new address.
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
|
* @param {Boolean} [opts.ignoreMaxGap=false] - Ignore constraint of maximum number of consecutive addresses without activity
|
||||||
* @returns {Address} address
|
* @returns {Address} address
|
||||||
*/
|
*/
|
||||||
WalletService.prototype.createAddress = function(opts, cb) {
|
WalletService.prototype.createAddress = function(opts, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
self._runLocked(cb, function(cb) {
|
self._runLocked(cb, function(cb) {
|
||||||
self.getWallet({}, function(err, wallet) {
|
self.getWallet({}, function(err, wallet) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
|
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
|
||||||
|
|
||||||
var address = wallet.createAddress(false);
|
self._canCreateAddress(opts.ignoreMaxGap, function(err, canCreate) {
|
||||||
|
|
||||||
self.storage.storeAddressAndWallet(wallet, address, function(err) {
|
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
if (!canCreate) return cb(Errors.MAIN_ADDRESS_GAP_REACHED);
|
||||||
|
|
||||||
self._notify('NewAddress', {
|
var address = wallet.createAddress(false);
|
||||||
address: address.address,
|
|
||||||
}, function() {
|
self.storage.storeAddressAndWallet(wallet, address, function(err) {
|
||||||
return cb(null, address);
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
self._notify('NewAddress', {
|
||||||
|
address: address.address,
|
||||||
|
}, function() {
|
||||||
|
return cb(null, address);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
|
|
@ -1715,6 +1715,65 @@ describe('Wallet service', function() {
|
||||||
done();
|
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