Merge pull request #523 from isocolsky/feat/single-addr-wallet
Feat/single-address wallet
This commit is contained in:
commit
7d18934f3b
|
@ -27,6 +27,7 @@ Wallet.create = function(opts) {
|
||||||
x.name = opts.name;
|
x.name = opts.name;
|
||||||
x.m = opts.m;
|
x.m = opts.m;
|
||||||
x.n = opts.n;
|
x.n = opts.n;
|
||||||
|
x.singleAddress = !!opts.singleAddress;
|
||||||
x.status = 'pending';
|
x.status = 'pending';
|
||||||
x.publicKeyRing = [];
|
x.publicKeyRing = [];
|
||||||
x.addressIndex = 0;
|
x.addressIndex = 0;
|
||||||
|
@ -56,6 +57,7 @@ Wallet.fromObj = function(obj) {
|
||||||
x.name = obj.name;
|
x.name = obj.name;
|
||||||
x.m = obj.m;
|
x.m = obj.m;
|
||||||
x.n = obj.n;
|
x.n = obj.n;
|
||||||
|
x.singleAddress = !!obj.singleAddress;
|
||||||
x.status = obj.status;
|
x.status = obj.status;
|
||||||
x.publicKeyRing = obj.publicKeyRing;
|
x.publicKeyRing = obj.publicKeyRing;
|
||||||
x.copayers = _.map(obj.copayers, function(copayer) {
|
x.copayers = _.map(obj.copayers, function(copayer) {
|
||||||
|
|
|
@ -62,6 +62,7 @@ function WalletService() {
|
||||||
function checkRequired(obj, args, cb) {
|
function checkRequired(obj, args, cb) {
|
||||||
var missing = Utils.getMissingFields(obj, args);
|
var missing = Utils.getMissingFields(obj, args);
|
||||||
if (_.isEmpty(missing)) return true;
|
if (_.isEmpty(missing)) return true;
|
||||||
|
if (_.isFunction(cb))
|
||||||
cb(new ClientError('Required argument ' + _.first(missing) + ' missing.'));
|
cb(new ClientError('Required argument ' + _.first(missing) + ' missing.'));
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
@ -215,8 +216,9 @@ WalletService.prototype._runLocked = function(cb, task) {
|
||||||
* @param {number} opts.m - Required copayers.
|
* @param {number} opts.m - Required copayers.
|
||||||
* @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.network = 'livenet'] - The Bitcoin network for this wallet.
|
* @param {string} opts.singleAddress[=false] - The wallet will only ever have one address.
|
||||||
* @param {string} [opts.supportBIP44AndP2PKH = true] - Client supports BIP44 & P2PKH for new wallets.
|
* @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) {
|
WalletService.prototype.createWallet = function(opts, cb) {
|
||||||
var self = this,
|
var self = this,
|
||||||
|
@ -263,6 +265,7 @@ WalletService.prototype.createWallet = function(opts, cb) {
|
||||||
n: opts.n,
|
n: opts.n,
|
||||||
network: opts.network,
|
network: opts.network,
|
||||||
pubKey: pubKey.toString(),
|
pubKey: pubKey.toString(),
|
||||||
|
singleAddress: !!opts.singleAddress,
|
||||||
derivationStrategy: derivationStrategy,
|
derivationStrategy: derivationStrategy,
|
||||||
addressType: addressType,
|
addressType: addressType,
|
||||||
});
|
});
|
||||||
|
@ -784,11 +787,7 @@ WalletService.prototype.createAddress = function(opts, cb) {
|
||||||
|
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
|
||||||
self._runLocked(cb, function(cb) {
|
function createNewAddress(wallet, 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) {
|
self._canCreateAddress(opts.ignoreMaxGap, function(err, canCreate) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
if (!canCreate) return cb(Errors.MAIN_ADDRESS_GAP_REACHED);
|
if (!canCreate) return cb(Errors.MAIN_ADDRESS_GAP_REACHED);
|
||||||
|
@ -805,6 +804,23 @@ WalletService.prototype.createAddress = function(opts, cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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];
|
var output = opts.outputs[i];
|
||||||
output.valid = false;
|
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 = {};
|
var toAddress = {};
|
||||||
try {
|
try {
|
||||||
|
@ -1744,6 +1762,8 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
|
||||||
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);
|
||||||
|
|
||||||
|
if (wallet.singleAddress) return cb(new ClientError('Not compatible with single-address wallets'));
|
||||||
|
|
||||||
if (opts.payProUrl) {
|
if (opts.payProUrl) {
|
||||||
if (wallet.addressType == Constants.SCRIPT_TYPES.P2PKH && !self._clientSupportsPayProRefund()) {
|
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.'));
|
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();
|
next();
|
||||||
},
|
},
|
||||||
|
function(next) {
|
||||||
|
if (wallet.singleAddress && opts.changeAddress) return next(new ClientError('Cannot specify change address on single-address wallet'));
|
||||||
|
next();
|
||||||
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
if (!opts.sendMax) return next();
|
if (!opts.sendMax) return next();
|
||||||
if (!_.isArray(opts.outputs) || opts.outputs.length > 1) {
|
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.outputs[].message - A message to attach to this output.
|
||||||
* @param {string} opts.message - A message to attach to this transaction.
|
* @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 {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 {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 {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
|
* @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) {
|
WalletService.prototype.createTx = function(opts, cb) {
|
||||||
var self = this;
|
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) {
|
self._runLocked(cb, function(cb) {
|
||||||
var wallet, txp, changeAddress;
|
var wallet, txp, changeAddress;
|
||||||
async.series([
|
async.series([
|
||||||
|
@ -1937,16 +1980,11 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
},
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
if (opts.sendMax) return next();
|
if (opts.sendMax) return next();
|
||||||
if (opts.changeAddress) {
|
getChangeAddress(wallet, function(err, address) {
|
||||||
self.storage.fetchAddress(opts.changeAddress, function(err, address) {
|
if (err) return next(err);
|
||||||
if (err) return next(Errors.INVALID_CHANGE_ADDRESS);
|
|
||||||
changeAddress = address;
|
changeAddress = address;
|
||||||
return next();
|
next();
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
changeAddress = wallet.createAddress(true);
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "bitcore-wallet-service",
|
"name": "bitcore-wallet-service",
|
||||||
"description": "A service for Mutisig HD Bitcoin Wallets",
|
"description": "A service for Mutisig HD Bitcoin Wallets",
|
||||||
"author": "BitPay Inc",
|
"author": "BitPay Inc",
|
||||||
"version": "1.8.2",
|
"version": "1.9.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"copay",
|
"copay",
|
||||||
|
|
|
@ -164,6 +164,7 @@ helpers.createAndJoinWallet = function(m, n, opts, cb) {
|
||||||
m: m,
|
m: m,
|
||||||
n: n,
|
n: n,
|
||||||
pubKey: TestData.keyPair.pub,
|
pubKey: TestData.keyPair.pub,
|
||||||
|
singleAddress: !!opts.singleAddress,
|
||||||
};
|
};
|
||||||
if (_.isBoolean(opts.supportBIP44AndP2PKH))
|
if (_.isBoolean(opts.supportBIP44AndP2PKH))
|
||||||
walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH;
|
walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH;
|
||||||
|
|
|
@ -2793,7 +2793,7 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
server.createTxLegacy(txOpts, function(err, tx) {
|
server.createTxLegacy(txOpts, function(err, tx) {
|
||||||
should.exist(err);
|
should.exist(err);
|
||||||
err.message.should.contain('outputs argument missing');
|
err.message.should.contain('Argument missing in output #1.');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3707,7 +3707,7 @@ describe('Wallet service', function() {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(txp);
|
should.exist(txp);
|
||||||
txp.inputs.length.should.equal(3);
|
txp.inputs.length.should.equal(3);
|
||||||
txOpts.feePerKb = 120e2;
|
txOpts.feePerKb = 160e2;
|
||||||
server.createTx(txOpts, function(err, txp) {
|
server.createTx(txOpts, function(err, txp) {
|
||||||
should.exist(err);
|
should.exist(err);
|
||||||
should.not.exist(txp);
|
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() {
|
describe('#getSendMaxInfo', function() {
|
||||||
var server, wallet;
|
var server, wallet;
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
|
|
Loading…
Reference in New Issue