From 2121565070cccc65baa4a868090ab1faec0539e2 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 16 Feb 2015 20:23:25 -0300 Subject: [PATCH] async storage --- lib/client/api.js | 334 ++++++++++++++++++---------------- lib/client/filestorage.js | 17 +- test/integration/clientApi.js | 30 ++- 3 files changed, 217 insertions(+), 164 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 1bfdd22..f256b1c 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -62,28 +62,24 @@ function API(opts) { }; -API.prototype._loadAndCheck = function(opts) { - var data = this.storage.load(); - if (!data) { - log.error('Wallet file not found.'); - process.exit(1); - } - - if (data.verified == 'corrupt') { - throw new Error('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); - } - if (data.n > 1) { - var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; +API.prototype._loadAndCheck = function(opts, cb) { + this.storage.load(function(err, data) { + if (err || !data) { + return cb(err || 'Wallet file not found.'); + } - if (opts.requireCompletePKR && !pkrComplete) { - throw new Error('Wallet Incomplete, cannot derive address.'); + if (data.verified == 'corrupt') { + return cb('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); } + if (data.n > 1) { + var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; - if (!pkrComplete) { - log.warn('The file ' + this.filename + ' is incomplete. It will allow you to operate with the wallet but it should not be trusted as a backup. Please wait for all copayers to join the wallet and run the tool with -export flag.') + if (opts.requireCompletePKR && !pkrComplete) { + return cb('Wallet Incomplete, cannot derive address'); + } } - } - return data; + return cb(null, data); + }); }; API.prototype._doRequest = function(method, url, args, data, cb) { @@ -130,44 +126,48 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); - var data = this.storage.load(); - if (data) return cb('File ' + this.filename + ' already contains a wallet'); - - // Generate wallet key pair to verify copayers - var privKey = new Bitcore.PrivateKey(null, network); - var pubKey = privKey.toPublicKey(); - - data = { - m: m, - n: n, - walletPrivKey: privKey.toWIF(), - network: network, - }; - - var args = { - name: walletName, - m: m, - n: n, - pubKey: pubKey.toString(), - network: network, - }; - var url = '/v1/wallets/'; - - this._doPostRequest(url, args, data, function(err, body) { - if (err) return cb(err); + this.storage.load(function(err, data) { + if (data) + return cb('Storage already contains a wallet'); + + console.log('[API.js.132]'); //TODO + // Generate wallet key pair to verify copayers + var privKey = new Bitcore.PrivateKey(null, network); + var pubKey = privKey.toPublicKey(); + + data = { + m: m, + n: n, + walletPrivKey: privKey.toWIF(), + network: network, + }; + + var args = { + name: walletName, + m: m, + n: n, + pubKey: pubKey.toString(), + network: network, + }; + var url = '/v1/wallets/'; + + self._doPostRequest(url, args, data, function(err, body) { + if (err) return cb(err); - var walletId = body.walletId; - var secret = walletId + ':' + privKey.toString() + ':' + (network == 'testnet' ? 'T' : 'L'); - var ret; + var walletId = body.walletId; + var secret = walletId + ':' + privKey.toString() + ':' + (network == 'testnet' ? 'T' : 'L'); + var ret; - if (n > 1) - ret = data.secret = secret; + if (n > 1) + ret = data.secret = secret; - self.storage.save(data); - self._joinWallet(data, secret, copayerName, function(err) { - if (err) return cb(err); + self.storage.save(data, function(err) { + if (err) return cb(err); + self._joinWallet(data, secret, copayerName, function(err) { + return cb(err, ret); + }); - return cb(null, ret); + }); }); }); }; @@ -204,54 +204,58 @@ API.prototype._joinWallet = function(data, secret, copayerName, cb) { data.n = wallet.n; data.publicKeyRing = wallet.publicKeyRing; data.network = wallet.network, - self.storage.save(data); - - return cb(); + self.storage.save(data, cb); }); }; API.prototype.joinWallet = function(secret, copayerName, cb) { var self = this; - var data = this.storage.load(); - if (data) return cb('File ' + this.filename + ' already contains a wallet'); + this.storage.load(function(err, data) { + if (data) + return cb('Storage already contains a wallet'); - self._joinWallet(data, secret, copayerName, cb); + self._joinWallet(data, secret, copayerName, cb); + }); }; API.prototype.getStatus = function(cb) { var self = this; - var data = this._loadAndCheck(); - - var url = '/v1/wallets/'; - this._doGetRequest(url, data, function(err, body) { + this._loadAndCheck({}, function(err, data) { if (err) return cb(err); - var wallet = body; - if (wallet.n > 0 && wallet.status === 'complete' && !data.verified) { - var pubKey = Bitcore.PrivateKey.fromString(data.walletPrivKey).toPublicKey().toString(); - var fake = []; - _.each(wallet.copayers, function(copayer) { - - - console.log('[clilib.js.224]', copayer.xPubKey, copayer.xPubKeySignature, pubKey); //TODO - if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, pubKey)) { + var url = '/v1/wallets/'; + self._doGetRequest(url, data, function(err, body) { + if (err) return cb(err); - console.log('[clilib.js.227] FAKE'); //TODO - fake.push(copayer); + var wallet = body; + if (wallet.n > 0 && wallet.status === 'complete' && !data.verified) { + var pubKey = Bitcore.PrivateKey.fromString(data.walletPrivKey).toPublicKey().toString(); + var fake = []; + _.each(wallet.copayers, function(copayer) { + + + console.log('[clilib.js.224]', copayer.xPubKey, copayer.xPubKeySignature, pubKey); //TODO + if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, pubKey)) { + + console.log('[clilib.js.227] FAKE'); //TODO + fake.push(copayer); + } + }); + if (fake.length > 0) { + log.error('Some copayers in the wallet could not be verified to have known the wallet secret'); + data.verified = 'corrupt'; + } else { + data.verified = 'ok'; } - }); - if (fake.length > 0) { - log.error('Some copayers in the wallet could not be verified to have known the wallet secret'); - data.verified = 'corrupt'; - } else { - data.verified = 'ok'; + self.storage.save(data, function(err) { + return cb(err, wallet); + }); } - self.storage.save(data); - } - return cb(null, wallet); + return cb(null, wallet); + }); }); }; @@ -266,38 +270,35 @@ API.prototype.getStatus = function(cb) { API.prototype.sendTxProposal = function(inArgs, cb) { var self = this; - var data = this._loadAndCheck(); - var args = _createProposalOpts(inArgs, data.signingPrivKey); - - var url = '/v1/txproposals/'; - this._doPostRequest(url, args, data, cb); -}; - -// Get addresses -API.prototype.getAddresses = function(cb) { - var self = this; + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { + if (err) return cb(err); - var data = this._loadAndCheck(); + var args = _createProposalOpts(inArgs, data.signingPrivKey); - var url = '/v1/addresses/'; - this._doGetRequest(url, data, cb); + var url = '/v1/txproposals/'; + self._doPostRequest(url, args, data, cb); + }); }; - -// Creates a new address -// TODO: verify derivation!! API.prototype.createAddress = function(cb) { var self = this; - var data = this._loadAndCheck({requireCompletePKR: true}); - var url = '/v1/addresses/'; - this._doPostRequest(url, {}, data, function(err, address) { + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { if (err) return cb(err); - if (!Verifier.checkAddress(data, address)) { - return cb(new ServerCompromisedError('Server sent fake address')); - } - return cb(null, address); + var url = '/v1/addresses/'; + self._doPostRequest(url, {}, data, function(err, address) { + if (err) return cb(err); + if (!Verifier.checkAddress(data, address)) { + return cb(new ServerCompromisedError('Server sent fake address')); + } + + return cb(null, address); + }); }); }; @@ -308,92 +309,113 @@ API.prototype.history = function(limit, cb) { API.prototype.getBalance = function(cb) { var self = this; - var data = this._loadAndCheck(); - - var url = '/v1/balance/'; - this._doGetRequest(url, data, cb); + this._loadAndCheck({}, function(err, data) { + if (err) return cb(err); + var url = '/v1/balance/'; + self._doGetRequest(url, data, cb); + }); }; API.prototype.getTxProposals = function(opts, cb) { var self = this; - var data = this._loadAndCheck(); - - var url = '/v1/txproposals/'; - this._doGetRequest(url, data, cb); + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { + if (err) return cb(err); + var url = '/v1/txproposals/'; + self._doGetRequest(url, data, cb); + }); }; API.prototype.signTxProposal = function(txp, cb) { var self = this; - var data = this._loadAndCheck(); + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { + if (err) return cb(err); - //Derive proper key to sign, for each input - var privs = [], - derived = {}; - var network = new Bitcore.Address(txp.toAddress).network.name; - var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey, network); + //Derive proper key to sign, for each input + var privs = [], + derived = {}; - _.each(txp.inputs, function(i) { - if (!derived[i.path]) { - derived[i.path] = xpriv.derive(i.path).privateKey; - } - privs.push(derived[i.path]); - }); + var network = new Bitcore.Address(txp.toAddress).network.name; + var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey, network); - var t = new Bitcore.Transaction(); - _.each(txp.inputs, function(i) { - t.from(i, i.publicKeys, txp.requiredSignatures); - }); + _.each(txp.inputs, function(i) { + if (!derived[i.path]) { + derived[i.path] = xpriv.derive(i.path).privateKey; + } + privs.push(derived[i.path]); + }); - t.to(txp.toAddress, txp.amount) - .change(txp.changeAddress) - .sign(privs); + var t = new Bitcore.Transaction(); + _.each(txp.inputs, function(i) { + t.from(i, i.publicKeys, txp.requiredSignatures); + }); - var signatures = []; - _.each(privs, function(p) { - var s = t.getSignatures(p)[0].signature.toDER().toString('hex'); - signatures.push(s); - }); + t.to(txp.toAddress, txp.amount) + .change(txp.changeAddress) + .sign(privs); - var url = '/v1/txproposals/' + txp.id + '/signatures/'; - var args = { - signatures: signatures - }; + var signatures = []; + _.each(privs, function(p) { + var s = t.getSignatures(p)[0].signature.toDER().toString('hex'); + signatures.push(s); + }); - this._doPostRequest(url, args, data, cb); + var url = '/v1/txproposals/' + txp.id + '/signatures/'; + var args = { + signatures: signatures + }; + + self._doPostRequest(url, args, data, cb); + }); }; API.prototype.rejectTxProposal = function(txp, reason, cb) { var self = this; - var data = this._loadAndCheck(); - var url = '/v1/txproposals/' + txp.id + '/rejections/'; - var args = { - reason: reason || '', - }; - this._doPostRequest(url, args, data, cb); + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { + if (err) return cb(err); + + var url = '/v1/txproposals/' + txp.id + '/rejections/'; + var args = { + reason: reason || '', + }; + self._doPostRequest(url, args, data, cb); + }); }; API.prototype.broadcastTxProposal = function(txp, cb) { var self = this; - var data = this._loadAndCheck(); - var url = '/v1/txproposals/' + txp.id + '/broadcast/'; - this._doPostRequest(url, {}, data, cb); + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { + if (err) return cb(err); + + var url = '/v1/txproposals/' + txp.id + '/broadcast/'; + self._doPostRequest(url, {}, data, cb); + }); }; API.prototype.removeTxProposal = function(txp, cb) { var self = this; - var data = this._loadAndCheck(); - - var url = '/v1/txproposals/' + txp.id; - - this._doRequest('delete', url, {}, data, cb); + this._loadAndCheck({ + requireCompletePKR: true + }, function(err, data) { + if (err) return cb(err); + var url = '/v1/txproposals/' + txp.id; + self._doRequest('delete', url, {}, data, cb); + }); }; module.exports = API; diff --git a/lib/client/filestorage.js b/lib/client/filestorage.js index 257cf76..35e6e69 100644 --- a/lib/client/filestorage.js +++ b/lib/client/filestorage.js @@ -10,14 +10,19 @@ function FileStorage(opts) { }; -FileStorage.prototype.save = function(data) { - this.fs.writeFileSync(this.filename, JSON.stringify(data)); +FileStorage.prototype.save = function(data, cb) { + this.fs.writeFile(this.filename, JSON.stringify(data), cb); }; -FileStorage.prototype.load = function() { - try { - return JSON.parse(this.fs.readFileSync(this.filename)); - } catch (ex) {}; +FileStorage.prototype.load = function(cb) { + this.fs.readFile(this.filename, 'utf8', function(err,data) { + if (err) return cb(err); + try { + data = JSON.parse(data); + } catch (e) { + } + return cb(null, data); + }); }; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index ba8d8f6..12fe14b 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -19,6 +19,17 @@ var wallet11 = { "publicKeyRing": ["tpubD6NzVbkrYhZ4YBumFxZEowDuA8iirsny2nmmFUkuxBkkZoGdPyf61Waei3tDYvVa1yqW82Xhmmd6oiibeDyM1MS3zTiky7Yg75UEV9oQhFJ"] }; +var incompleteWallet22 = { + "m": 2, + "n": 2, + "walletPrivKey": "L3XSE3KNjQM1XRP1h5yMCSKsN4hs3D6eK7Vwn5M88Bs6jpCnXR3R", + "network": "testnet", + "secret": "d9cf45a1-6793-4df4-94df-c99d2c2e1fe9:bc2488c1b83e455a4b908a0d0aeaf70351efc48fbcaa454bffefdef419a5ee6a:T", + "xPrivKey": "tprv8ZgxMBicQKsPdoC5DGtnXx7fp7YnUtGv8b7fU2oDQfDpHFQh1QCgpKc8GHpdsBN5THaHYMV5LgD5cP5NYaacGVr786p3mVLSZff9berTV8h", + "copayerId": "c3a33ca0-37cf-4e80-b745-71272683835c", + "signingPrivKey": "6e129c4996666e5ecdf78aed626c01977fa19eacce6659738ebe065f86523e9b", + "publicKeyRing": [] +}; describe('client API', function() { @@ -28,8 +39,8 @@ describe('client API', function() { beforeEach(function() { var fsmock = {};; - fsmock.readFileSync = sinon.mock().returns(JSON.stringify(wallet11)); - fsmock.writeFileSync = sinon.mock(); + fsmock.readFile = sinon.mock().yields(null, JSON.stringify(wallet11)); + fsmock.writeFile = sinon.mock().yields(); var storage = new Client.FileStorage({ filename: 'dummy', fs: fsmock, @@ -77,5 +88,20 @@ describe('client API', function() { done(); }); }) + + it('should complain wallet is not complete', function(done) { + var request = sinon.mock().yields(null, { + statusCode: 200 + }, { + dummy: true + }); + client.request = request; + client.storage.fs.readFile = sinon.mock().yields(null, JSON.stringify(incompleteWallet22)); + client.createAddress(function(err, x) { + err.should.contain('Incomplete'); + done(); + }); + }) + }); });