Merge pull request #523 from isocolsky/feat/single-addr-wallet

Feat/single-address wallet
This commit is contained in:
Matias Alejo Garcia 2016-06-06 22:58:43 -03:00
commit 7d18934f3b
5 changed files with 164 additions and 34 deletions

View File

@ -27,6 +27,7 @@ Wallet.create = function(opts) {
x.name = opts.name;
x.m = opts.m;
x.n = opts.n;
x.singleAddress = !!opts.singleAddress;
x.status = 'pending';
x.publicKeyRing = [];
x.addressIndex = 0;
@ -56,6 +57,7 @@ Wallet.fromObj = function(obj) {
x.name = obj.name;
x.m = obj.m;
x.n = obj.n;
x.singleAddress = !!obj.singleAddress;
x.status = obj.status;
x.publicKeyRing = obj.publicKeyRing;
x.copayers = _.map(obj.copayers, function(copayer) {

View File

@ -62,7 +62,8 @@ function WalletService() {
function checkRequired(obj, args, cb) {
var missing = Utils.getMissingFields(obj, args);
if (_.isEmpty(missing)) return true;
cb(new ClientError('Required argument ' + _.first(missing) + ' missing.'));
if (_.isFunction(cb))
cb(new ClientError('Required argument ' + _.first(missing) + ' missing.'));
return false;
};
@ -215,8 +216,9 @@ WalletService.prototype._runLocked = function(cb, task) {
* @param {number} opts.m - Required 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.network = 'livenet'] - The Bitcoin network for this wallet.
* @param {string} [opts.supportBIP44AndP2PKH = true] - Client supports BIP44 & P2PKH for new wallets.
* @param {string} opts.singleAddress[=false] - The wallet will only ever have one address.
* @param {string} opts.network[='livenet'] - The Bitcoin network for this wallet.
* @param {string} opts.supportBIP44AndP2PKH[=true] - Client supports BIP44 & P2PKH for new wallets.
*/
WalletService.prototype.createWallet = function(opts, cb) {
var self = this,
@ -263,6 +265,7 @@ WalletService.prototype.createWallet = function(opts, cb) {
n: opts.n,
network: opts.network,
pubKey: pubKey.toString(),
singleAddress: !!opts.singleAddress,
derivationStrategy: derivationStrategy,
addressType: addressType,
});
@ -784,27 +787,40 @@ WalletService.prototype.createAddress = function(opts, cb) {
opts = opts || {};
function createNewAddress(wallet, cb) {
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) {
if (err) return cb(err);
self._notify('NewAddress', {
address: address.address,
}, function() {
return cb(null, address);
});
});
});
};
function getFirstAddress(wallet, cb) {
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
if (!_.isEmpty(addresses)) return cb(null, _.first(addresses))
return createNewAddress(wallet, cb);
});
};
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) {
if (err) return cb(err);
self._notify('NewAddress', {
address: address.address,
}, function() {
return cb(null, address);
});
});
});
var createFn = wallet.singleAddress ? getFirstAddress : createNewAddress;
return createFn(wallet, cb);
});
});
};
@ -1669,7 +1685,9 @@ WalletService.prototype._validateOutputs = function(opts, wallet, cb) {
var output = opts.outputs[i];
output.valid = false;
if (!checkRequired(output, ['toAddress', 'amount'], cb)) return;
if (!checkRequired(output, ['toAddress', 'amount'])) {
return new ClientError('Argument missing in output #' + (i + 1) + '.');
}
var toAddress = {};
try {
@ -1744,6 +1762,8 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
if (err) return cb(err);
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
if (wallet.singleAddress) return cb(new ClientError('Not compatible with single-address wallets'));
if (opts.payProUrl) {
if (wallet.addressType == Constants.SCRIPT_TYPES.P2PKH && !self._clientSupportsPayProRefund()) {
return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To sign this spend proposal you need to upgrade your client app.'));
@ -1858,6 +1878,10 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb)
}
next();
},
function(next) {
if (wallet.singleAddress && opts.changeAddress) return next(new ClientError('Cannot specify change address on single-address wallet'));
next();
},
function(next) {
if (!opts.sendMax) return next();
if (!_.isArray(opts.outputs) || opts.outputs.length > 1) {
@ -1899,7 +1923,7 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb)
* @param {string} opts.outputs[].message - A message to attach to this output.
* @param {string} opts.message - A message to attach to this transaction.
* @param {number} opts.feePerKb - Use an alternative fee per KB for this TX.
* @param {string} opts.changeAddress - Optional. Use this address as the change address for the tx. The address should belong to the wallet.
* @param {string} opts.changeAddress - Optional. Use this address as the change address for the tx. The address should belong to the wallet. In the case of singleAddress wallets, the first main address will be used.
* @param {Boolean} opts.sendMax - Optional. Send maximum amount of funds that make sense under the specified fee/feePerKb conditions. (defaults to false).
* @param {string} opts.payProUrl - Optional. Paypro URL for peers to verify TX
* @param {Boolean} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs
@ -1913,6 +1937,25 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb)
WalletService.prototype.createTx = function(opts, cb) {
var self = this;
function getChangeAddress(wallet, cb) {
if (wallet.singleAddress) {
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
if (_.isEmpty(addresses)) return cb(new ClientError('The wallet has no addresses'));
return cb(null, _.first(addresses));
});
} else {
if (opts.changeAddress) {
self.storage.fetchAddress(opts.changeAddress, function(err, address) {
if (err) return cb(Errors.INVALID_CHANGE_ADDRESS);
return cb(null, address);
});
} else {
return cb(null, wallet.createAddress(true));
}
}
};
self._runLocked(cb, function(cb) {
var wallet, txp, changeAddress;
async.series([
@ -1937,16 +1980,11 @@ WalletService.prototype.createTx = function(opts, cb) {
},
function(next) {
if (opts.sendMax) return next();
if (opts.changeAddress) {
self.storage.fetchAddress(opts.changeAddress, function(err, address) {
if (err) return next(Errors.INVALID_CHANGE_ADDRESS);
changeAddress = address;
return next();
});
} else {
changeAddress = wallet.createAddress(true);
return next();
}
getChangeAddress(wallet, function(err, address) {
if (err) return next(err);
changeAddress = address;
next();
});
},
function(next) {
var txOpts = {

View File

@ -2,7 +2,7 @@
"name": "bitcore-wallet-service",
"description": "A service for Mutisig HD Bitcoin Wallets",
"author": "BitPay Inc",
"version": "1.8.2",
"version": "1.9.0",
"keywords": [
"bitcoin",
"copay",

View File

@ -164,6 +164,7 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) {
m: m,
n: n,
pubKey: TestData.keyPair.pub,
singleAddress: !!opts.singleAddress,
};
if (_.isBoolean(opts.supportBIP44AndP2PKH))
walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH;

View File

@ -2793,7 +2793,7 @@ describe('Wallet service', function() {
});
server.createTxLegacy(txOpts, function(err, tx) {
should.exist(err);
err.message.should.contain('outputs argument missing');
err.message.should.contain('Argument missing in output #1.');
done();
});
});
@ -3707,7 +3707,7 @@ describe('Wallet service', function() {
should.not.exist(err);
should.exist(txp);
txp.inputs.length.should.equal(3);
txOpts.feePerKb = 120e2;
txOpts.feePerKb = 160e2;
server.createTx(txOpts, function(err, txp) {
should.exist(err);
should.not.exist(txp);
@ -4209,6 +4209,95 @@ describe('Wallet service', function() {
});
});
describe('Single-address wallet', function() {
var server, wallet, firstAddress;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 2, {
singleAddress: true,
}, function(s, w) {
server = s;
wallet = w;
server.createAddress({}, function(err, a) {
should.not.exist(err);
should.exist(a.address);
firstAddress = a;
done();
});
});
});
it('should include singleAddress property', function(done) {
server.getWallet({}, function(err, wallet) {
should.not.exist(err);
wallet.singleAddress.should.be.true;
done();
});
});
it('should always return same address', function(done) {
firstAddress.path.should.equal('m/0/0');
server.createAddress({}, function(err, x) {
should.not.exist(err);
should.exist(x);
x.path.should.equal('m/0/0');
x.address.should.equal(firstAddress.address);
server.getMainAddresses({}, function(err, addr) {
should.not.exist(err);
addr.length.should.equal(1);
done();
});
});
});
it('should reuse address as change address on tx proposal creation', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var toAddress = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7';
var opts = {
outputs: [{
amount: 1e8,
toAddress: toAddress,
}],
feePerKb: 100e2,
};
server.createTx(opts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
should.exist(txp.changeAddress);
txp.changeAddress.address.should.equal(firstAddress.address);
txp.changeAddress.path.should.equal(firstAddress.path);
done();
});
});
});
it('should not allow legacy txs', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var toAddress = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7';
var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 1, TestData.copayers[0].privKey_1H_0);
server.createTxLegacy(txOpts, function(err, tx) {
should.exist(err);
err.message.should.contain('single-address');
done();
});
});
});
it('should not be able to specify custom changeAddress', function(done) {
helpers.stubUtxos(server, wallet, 2, function() {
var toAddress = '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7';
var opts = {
outputs: [{
amount: 1e8,
toAddress: toAddress,
}],
feePerKb: 100e2,
changeAddress: firstAddress.address,
};
server.createTx(opts, function(err, txp) {
should.exist(err);
err.message.should.contain('single-address');
done();
});
});
});
});
describe('#getSendMaxInfo', function() {
var server, wallet;
beforeEach(function(done) {