diff --git a/lib/client/api.js b/lib/client/api.js index 63d84cb..db61edb 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -11,11 +11,12 @@ log.debug = log.verbose; var Bitcore = require('bitcore') var WalletUtils = require('../walletutils'); var Verifier = require('./verifier'); -var ServerCompromisedError = require('./servercompromisederror') +var ServerCompromisedError = require('./servercompromisederror'); +var ClientError = require('../clienterror'); var BASE_URL = 'http://localhost:3001/copay/api'; -var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'publicKeyRing', 'sharedEncryptingKey']; +var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'n', 'publicKeyRing', 'sharedEncryptingKey']; var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey']; var WALLET_AIRGAPPED_TOCOMPLETE = ['publicKeyRing', 'm', 'n', 'sharedEncryptingKey']; @@ -54,13 +55,17 @@ function _parseError(body) { }; } } - var code = body.code || 'ERROR'; - var message = body.error || 'There was an unknown error processing the request'; - log.error(code, message); - return { - message: message, - code: code - }; + var ret; + if (body.code) { + ret = new ClientError(body.code, body.message); + } else { + ret = { + code: 'ERROR', + error: body.error || 'There was an unknown error processing the request', + }; + } + log.error(ret); + return ret; }; function _signRequest(method, url, args, privKey) { @@ -68,6 +73,34 @@ function _signRequest(method, url, args, privKey) { return WalletUtils.signMessage(message, privKey); }; +function _initWcd(network) { + $.checkArgument(network); + var xPrivKey = new Bitcore.HDPrivateKey(network); + var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); + var roPrivKey = xPrivKey.derive('m/1/0').privateKey; + var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; + var copayerId = WalletUtils.xPubToCopayerId(xPubKey); + + + return { + copayerId: copayerId, + xPrivKey: xPrivKey.toString(), + publicKeyRing: [xPubKey], + network: network, + roPrivKey: roPrivKey.toWIF(), + rwPrivKey: rwPrivKey.toWIF(), + }; +}; + +function _addWalletToWcd(wcd, walletPrivKey, m, n) { + $.checkArgument(wcd); + var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); + + wcd.walletPrivKey = walletPrivKey.toWIF(); + wcd.sharedEncryptingKey = sharedEncryptingKey; + wcd.m = m; + wcd.n = n; +}; function API(opts) { if (!opts.storage) { @@ -85,57 +118,60 @@ function API(opts) { } }; -API.prototype._tryToCompleteFromServer = function(data, cb) { - var self = this; +API.prototype._tryToCompleteFromServer = function(wcd, cb) { + if (!wcd.walletPrivKey) + return cb('Could not perform that action. Wallet Incomplete'); + + var self = this; var url = '/v1/wallets/'; - self._doGetRequest(url, data, function(err, ret) { + self._doGetRequest(url, wcd, function(err, ret) { if (err) return cb(err); var wallet = ret.wallet; if (wallet.status != 'complete') return cb('Wallet Incomplete'); - if (!Verifier.checkCopayers(wallet.copayers, data.walletPrivKey, - data.xPrivKey, data.n)) { + if (!Verifier.checkCopayers(wallet.copayers, wcd.walletPrivKey, + wcd.xPrivKey, wcd.n)) { return cb(new ServerCompromisedError( 'Copayers in the wallet could not be verified to have known the wallet secret')); } - data.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') + wcd.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') - self.storage.save(data, function(err) { - return cb(err, data); + self.storage.save(wcd, function(err) { + return cb(err, wcd); }); }); }; -API.prototype._tryToCompleteFromData = function(data, toComplete, cb) { +API.prototype._tryToCompleteFromData = function(wcd, toComplete, cb) { var inData = _decryptMessage(toComplete, - WalletUtils.privateKeyToAESKey(data.roPrivKey)); + WalletUtils.privateKeyToAESKey(wcd.roPrivKey)); if (!inData) return cb('Could not complete wallet'); try { inData = JSON.parse(inData); - _.extend(data, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); + _.extend(wcd, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); } catch (ex) { return cb(ex); } - this.storage.save(data, function(err) { - return cb(err, data); + this.storage.save(wcd, function(err) { + return cb(err, wcd); }); }; -API.prototype._tryToComplete = function(opts, data, cb) { +API.prototype._tryToComplete = function(opts, wcd, cb) { if (opts.toComplete) { - this._tryToCompleteFromData(data, opts.toComplete, cb); + this._tryToCompleteFromData(wcd, opts.toComplete, cb); } else { - this._tryToCompleteFromServer(data, cb); + this._tryToCompleteFromServer(wcd, cb); } }; @@ -144,11 +180,11 @@ API.prototype._tryToComplete = function(opts, data, cb) { API.prototype._load = function(cb) { var self = this; - this.storage.load(function(err, data) { - if (err || !data) { - return cb(err || 'Wallet file not found.'); + this.storage.load(function(err, wcd) { + if (err || !wcd) { + return cb(err || 'wcd file not found.'); } - return cb(null, data); + return cb(null, wcd); }); }; @@ -161,26 +197,27 @@ API.prototype._load = function(cb) { API.prototype._loadAndCheck = function(opts, cb) { var self = this; - this._load(function(err, data) { + this._load(function(err, wcd) { if (err) return cb(err); - if (!data.n || (data.n > 1 && data.publicKeyRing.length != data.n)) - return self._tryToComplete(opts, data, cb); + if (!wcd.n || (wcd.n > 1 && wcd.publicKeyRing.length != wcd.n)) { + return self._tryToComplete(opts, wcd, cb); + } - return cb(null, data); + return cb(null, wcd); }); }; -API.prototype._doRequest = function(method, url, args, data, cb) { +API.prototype._doRequest = function(method, url, args, wcd, cb) { var reqSignature; - data = data || {}; + wcd = wcd || {}; if (method == 'get') { - if (data.roPrivKey) - reqSignature = _signRequest(method, url, args, data.roPrivKey); + if (wcd.roPrivKey) + reqSignature = _signRequest(method, url, args, wcd.roPrivKey); } else { - if (data.rwPrivKey) - reqSignature = _signRequest(method, url, args, data.rwPrivKey); + if (wcd.rwPrivKey) + reqSignature = _signRequest(method, url, args, wcd.rwPrivKey); } var absUrl = this.baseUrl + url; @@ -188,7 +225,7 @@ API.prototype._doRequest = function(method, url, args, data, cb) { // relUrl: only for testing with `supertest` relUrl: this.basePath + url, headers: { - 'x-identity': data.copayerId, + 'x-identity': wcd.copayerId, 'x-signature': reqSignature, }, method: method, @@ -214,44 +251,15 @@ API.prototype._doRequest = function(method, url, args, data, cb) { }; -API.prototype._doPostRequest = function(url, args, data, cb) { - return this._doRequest('post', url, args, data, cb); +API.prototype._doPostRequest = function(url, args, wcd, cb) { + return this._doRequest('post', url, args, wcd, cb); }; -API.prototype._doGetRequest = function(url, data, cb) { - return this._doRequest('get', url, {}, data, cb); +API.prototype._doGetRequest = function(url, wcd, cb) { + return this._doRequest('get', url, {}, wcd, cb); }; -API.prototype._initData = function(network, walletPrivKey, m, n) { - var xPrivKey = new Bitcore.HDPrivateKey(network); - var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); - var roPrivKey = xPrivKey.derive('m/1/0').privateKey; - var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; - var copayerId = WalletUtils.xPubToCopayerId(xPubKey); - - - var data = { - copayerId: copayerId, - xPrivKey: xPrivKey.toString(), - publicKeyRing: [xPubKey], - network: network, - roPrivKey: roPrivKey.toWIF(), - rwPrivKey: rwPrivKey.toWIF(), - }; - - if (walletPrivKey) { - var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); - data.walletPrivKey = walletPrivKey.toWIF(); - data.sharedEncryptingKey = sharedEncryptingKey; - } - - if (m) data.m = m; - if (n) data.n = n; - - return data; -}; - API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) { var args = { walletId: walletId, @@ -272,12 +280,12 @@ API.prototype.generateKey = function(network, cb) { if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); - this.storage.load(function(err, data) { - if (data) + this.storage.load(function(err, wcd) { + if (wcd) return cb(self.storage.getName() + ' already contains a wallet'); - var data = self._initData(network); - self.storage.save(data, function(err) { + var wcd = _initWcd(network); + self.storage.save(wcd, function(err) { return cb(err, null); }); }); @@ -289,10 +297,13 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); - this.storage.load(function(err, data) { - if (data) + this.storage.load(function(err, wcd) { + if (wcd && wcd.n) return cb(self.storage.getName() + ' already contains a wallet'); + if (wcd && wcd.network && wcd.network != network) + return cb('Storage ' + self.storage.getName() + ' is set to network:' + wcd.network); + var walletPrivKey = new Bitcore.PrivateKey(); var args = { name: walletName, @@ -308,11 +319,14 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb var walletId = body.walletId; var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); - var data = self._initData(network, walletPrivKey, m, n); - self._doJoinWallet(walletId, walletPrivKey, data.publicKeyRing[0], copayerName, + + wcd = wcd || _initWcd(network); + _addWalletToWcd(wcd, walletPrivKey, m, n) + + self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[0], copayerName, function(err, wallet) { if (err) return cb(err); - self.storage.save(data, function(err) { + self.storage.save(wcd, function(err) { return cb(err, n > 1 ? secret : null); }); }); @@ -323,16 +337,16 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb API.prototype.reCreateWallet = function(walletName, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var walletPrivKey = new Bitcore.PrivateKey(); var args = { name: walletName, - m: data.m, - n: data.n, + m: wcd.m, + n: wcd.n, pubKey: walletPrivKey.toPublicKey().toString(), - network: data.network, + network: wcd.network, }; var url = '/v1/wallets/'; self._doPostRequest(url, args, {}, function(err, body) { @@ -340,11 +354,11 @@ API.prototype.reCreateWallet = function(walletName, cb) { var walletId = body.walletId; - var secret = WalletUtils.toSecret(walletId, walletPrivKey, data.network); + var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network); var i = 0; - async.each(data.publicKeyRing, function(xpub, next) { + async.each(wcd.publicKeyRing, function(xpub, next) { var copayerName = 'recovered Copayer #' + i; - self._doJoinWallet(walletId, walletPrivKey, data.publicKeyRing[i++], copayerName, next); + self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next); }, function(err) { return cb(err); }); @@ -356,22 +370,22 @@ API.prototype.reCreateWallet = function(walletName, cb) { API.prototype.joinWallet = function(secret, copayerName, cb) { var self = this; - this.storage.load(function(err, data) { - if (data) - return cb('Storage already contains a wallet'); + this.storage.load(function(err, wcd) { + if (wcd && wcd.n) + return cb(self.storage.getName() + ' already contains a wallet'); try { var secretData = WalletUtils.fromSecret(secret); } catch (ex) { return cb(ex); } - var data = self._initData(secretData.network, secretData.walletPrivKey); - self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, data.publicKeyRing[0], copayerName, - function(err, wallet) { + wcd = wcd || _initWcd(secretData.network); + + self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, wcd.publicKeyRing[0], copayerName, + function(err, joinedWallet) { if (err) return cb(err); - data.m = wallet.m; - data.n = wallet.n; - self.storage.save(data, cb); + _addWalletToWcd(wcd, secretData.walletPrivKey, joinedWallet.m, joinedWallet.n); + self.storage.save(wcd, cb); }); }); }; @@ -379,13 +393,13 @@ API.prototype.joinWallet = function(secret, copayerName, cb) { API.prototype.getStatus = function(cb) { var self = this; - this._load(function(err, data) { + this._load(function(err, wcd) { if (err) return cb(err); var url = '/v1/wallets/'; - self._doGetRequest(url, data, function(err, result) { - _processTxps(result.pendingTxps, data.sharedEncryptingKey); - return cb(err, result, data.copayerId); + self._doGetRequest(url, wcd, function(err, result) { + _processTxps(result.pendingTxps, wcd.sharedEncryptingKey); + return cb(err, result, wcd.copayerId); }); }); }; @@ -404,36 +418,36 @@ API.prototype.sendTxProposal = function(opts, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); - if (!data.rwPrivKey) + if (!wcd.rwPrivKey) return cb('No key to generate proposals'); var args = { toAddress: opts.toAddress, amount: opts.amount, - message: _encryptMessage(opts.message, data.sharedEncryptingKey), + message: _encryptMessage(opts.message, wcd.sharedEncryptingKey), }; var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); - args.proposalSignature = WalletUtils.signMessage(hash, data.rwPrivKey); + args.proposalSignature = WalletUtils.signMessage(hash, wcd.rwPrivKey); log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); var url = '/v1/txproposals/'; - self._doPostRequest(url, args, data, cb); + self._doPostRequest(url, args, wcd, cb); }); }; API.prototype.createAddress = function(cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var url = '/v1/addresses/'; - self._doPostRequest(url, {}, data, function(err, address) { + self._doPostRequest(url, {}, wcd, function(err, address) { if (err) return cb(err); - if (!Verifier.checkAddress(data, address)) { + if (!Verifier.checkAddress(wcd, address)) { return cb(new ServerCompromisedError('Server sent fake address')); } @@ -449,16 +463,16 @@ API.prototype.createAddress = function(cb) { API.prototype.getMainAddresses = function(opts, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var url = '/v1/addresses/'; - self._doGetRequest(url, data, function(err, addresses) { + self._doGetRequest(url, wcd, function(err, addresses) { if (err) return cb(err); if (!opts.doNotVerify) { var fake = _.any(addresses, function(address) { - return !Verifier.checkAddress(data, address); + return !Verifier.checkAddress(wcd, address); }); if (fake) return cb(new ServerCompromisedError('Server sent fake address')); @@ -475,15 +489,16 @@ API.prototype.history = function(limit, cb) { API.prototype.getBalance = function(cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var url = '/v1/balance/'; - self._doGetRequest(url, data, cb); + self._doGetRequest(url, wcd, cb); }); }; /** - * export + * Export does not try to complete the wallet from the server. Exports the + * wallet as it is now. * * @param opts.access =['full', 'readonly', 'readwrite'] */ @@ -493,11 +508,11 @@ API.prototype.export = function(opts, cb) { opts = opts || {}; var access = opts.access || 'full'; - this._load(function(err, data) { + this._load(function(err, wcd) { if (err) return cb(err); var v = []; - var myXPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString(); + var myXPubKey = (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString(); _.each(WALLET_CRITICAL_DATA, function(k) { var d; @@ -509,18 +524,18 @@ API.prototype.export = function(opts, cb) { // Skips own pub key IF priv key is exported if (access == 'full' && k === 'publicKeyRing') { - d = _.without(data[k], myXPubKey); + d = _.without(wcd[k], myXPubKey); } else { - d = data[k]; + d = wcd[k]; } v.push(d); }); if (access != 'full') { - v.push(data.copayerId); - v.push(data.roPrivKey); + v.push(wcd.copayerId); + v.push(wcd.roPrivKey); if (access == 'readwrite') { - v.push(data.rwPrivKey); + v.push(wcd.rwPrivKey); } } @@ -532,39 +547,34 @@ API.prototype.export = function(opts, cb) { API.prototype.import = function(str, cb) { var self = this; - this.storage.load(function(err, data) { - if (data) + this.storage.load(function(err, wcd) { + if (wcd) return cb('Storage already contains a wallet'); - data = {}; + wcd = {}; var inData = JSON.parse(str); var i = 0; _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { - data[k] = inData[i++]; + wcd[k] = inData[i++]; }); - if (data.xPrivKey) { - var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey); + if (wcd.xPrivKey) { + var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey); var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); - data.publicKeyRing.unshift(xPubKey); - data.copayerId = WalletUtils.xPubToCopayerId(xPubKey); - data.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); - data.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); + wcd.publicKeyRing.unshift(xPubKey); + wcd.copayerId = WalletUtils.xPubToCopayerId(xPubKey); + wcd.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); + wcd.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); } - var dataIsComplete = !!data.m; - - if (dataIsComplete) - data.n = data.publicKeyRing.length; - - if (!data.publicKeyRing) - return cb('Invalid source data'); + if (!wcd.publicKeyRing) + return cb('Invalid source wallet'); - data.network = data.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; - self.storage.save(data, function(err) { - return cb(err, WalletUtils.accessFromData(data)); + wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; + self.storage.save(wcd, function(err) { + return cb(err, WalletUtils.accessFromData(wcd)); }); }); }; @@ -578,14 +588,14 @@ API.prototype.parseTxProposals = function(txData, cb) { this._loadAndCheck({ toComplete: txData.toComplete - }, function(err, data) { + }, function(err, wcd) { if (err) return cb(err); var txps = txData.txps; - _processTxps(txps, data.sharedEncryptingKey); + _processTxps(txps, wcd.sharedEncryptingKey); var fake = _.any(txps, function(txp) { - return (!Verifier.checkTxProposal(data, txp)); + return (!Verifier.checkTxProposal(wcd, txp)); }); if (fake) @@ -607,20 +617,20 @@ API.prototype.parseTxProposals = function(txData, cb) { API.prototype.getTxProposals = function(opts, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); var url = '/v1/txproposals/'; - self._doGetRequest(url, data, function(err, txps) { + self._doGetRequest(url, wcd, function(err, txps) { if (err) return cb(err); var rawTxps; if (opts.getRawTxps) rawTxps = JSON.parse(JSON.stringify(txps)); - _processTxps(txps, data.sharedEncryptingKey); + _processTxps(txps, wcd.sharedEncryptingKey); var fake = _.any(txps, function(txp) { - return (!opts.doNotVerify && !Verifier.checkTxProposal(data, txp)); + return (!opts.doNotVerify && !Verifier.checkTxProposal(wcd, txp)); }); if (fake) @@ -631,14 +641,14 @@ API.prototype.getTxProposals = function(opts, cb) { }); }; -API.prototype._getSignaturesFor = function(txp, data) { +API.prototype._getSignaturesFor = function(txp, wcd) { //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); + var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey, network); _.each(txp.inputs, function(i) { if (!derived[i.path]) { @@ -671,24 +681,24 @@ API.prototype.getSignatures = function(txp, cb) { $.checkArgument(txp.creatorId); var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); - if (!Verifier.checkTxProposal(data, txp)) { + if (!Verifier.checkTxProposal(wcd, txp)) { return cb(new ServerCompromisedError('Transaction proposal is invalid')); } - return cb(null, self._getSignaturesFor(txp, data)); + return cb(null, self._getSignaturesFor(txp, wcd)); }); }; API.prototype.getEncryptedWalletData = function(cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); - var toComplete = JSON.stringify(_.pick(data, WALLET_AIRGAPPED_TOCOMPLETE)); - return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(data.roPrivKey))); + var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE)); + return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey))); }); }; @@ -699,21 +709,21 @@ API.prototype.signTxProposal = function(txp, cb) { var self = this; - this._loadAndCheck({}, function(err, data) { + this._loadAndCheck({}, function(err, wcd) { if (err) return cb(err); - if (!Verifier.checkTxProposal(data, txp)) { + if (!Verifier.checkTxProposal(wcd, txp)) { return cb(new ServerCompromisedError('Server sent fake transaction proposal')); } - var signatures = txp.signatures || self._getSignaturesFor(txp, data); + var signatures = txp.signatures || self._getSignaturesFor(txp, wcd); var url = '/v1/txproposals/' + txp.id + '/signatures/'; var args = { signatures: signatures }; - self._doPostRequest(url, args, data, cb); + self._doPostRequest(url, args, wcd, cb); }); }; @@ -723,14 +733,14 @@ API.prototype.rejectTxProposal = function(txp, reason, cb) { var self = this; this._loadAndCheck({}, - function(err, data) { + function(err, wcd) { if (err) return cb(err); var url = '/v1/txproposals/' + txp.id + '/rejections/'; var args = { - reason: _encryptMessage(reason, data.sharedEncryptingKey) || '', + reason: _encryptMessage(reason, wcd.sharedEncryptingKey) || '', }; - self._doPostRequest(url, args, data, cb); + self._doPostRequest(url, args, wcd, cb); }); }; @@ -738,11 +748,11 @@ API.prototype.broadcastTxProposal = function(txp, cb) { var self = this; this._loadAndCheck({}, - function(err, data) { + function(err, wcd) { if (err) return cb(err); var url = '/v1/txproposals/' + txp.id + '/broadcast/'; - self._doPostRequest(url, {}, data, cb); + self._doPostRequest(url, {}, wcd, cb); }); }; @@ -751,10 +761,10 @@ API.prototype.broadcastTxProposal = function(txp, cb) { API.prototype.removeTxProposal = function(txp, cb) { var self = this; this._loadAndCheck({}, - function(err, data) { + function(err, wcd) { if (err) return cb(err); var url = '/v1/txproposals/' + txp.id; - self._doRequest('delete', url, {}, data, cb); + self._doRequest('delete', url, {}, wcd, cb); }); }; diff --git a/lib/clienterror.js b/lib/clienterror.js index 2e6fd84..f0f4015 100644 --- a/lib/clienterror.js +++ b/lib/clienterror.js @@ -18,4 +18,8 @@ function ClientError() { } }; +ClientError.prototype.toString = function() { + return ''; +}; + module.exports = ClientError; diff --git a/lib/expressapp.js b/lib/expressapp.js index e253af5..083ab5b 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -64,11 +64,11 @@ ExpressApp.start = function(opts) { var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400; if (!opts.disableLogs) - log.error('Err: ' + status + ':' + req.url + ' :' + err.code + ':' + err.message); + log.info('Client Err: ' + status + ' ' + req.url + ' ' + err); res.status(status).json({ code: err.code, - error: err.message, + message: err.message, }).end(); } else { var code, message; diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 6d09aef..d9d828a 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -377,6 +377,8 @@ describe('client API ', function() { should.not.exist(err); clients[1].import(str, function(err, wallet) { should.not.exist(err); + + console.log('[clientApi.js.380]'); //TODO clients[1].createAddress(function(err, x0) { err.code.should.equal('NOTAUTHORIZED'); clients[0].createAddress(function(err, x0) { @@ -439,7 +441,7 @@ describe('client API ', function() { }); }); }); - describe('Air gapped flows', function() { + describe('Air gapped related flows', function() { it('should be able get Tx proposals from a file', function(done) { helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { should.not.exist(err); @@ -505,66 +507,65 @@ describe('client API ', function() { }); }); - it('should complete public key ring from file', function(done) { - helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { + it('should create from proxy from airgapped', function(done) { + // client0 -> airgapped + // client1 -> proxy + clients[0].generateKey('testnet', function(err) { should.not.exist(err); - - clients[1].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 1, 1); - var opts = { - amount: 10000000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: 'hello 1-1', - }; - clients[1].sendTxProposal(opts, function(err, x) { + clients[0].export({ + access: 'readwrite' + }, function(err, str) { + clients[1].import(str, function(err) { should.not.exist(err); - // Create the proxy, ro, connected, device (2) - clients[0].export({ - access: 'readonly' - }, function(err, str) { - should.not.exist(err); - clients[2].import(str, function(err, wallet) { + clients[1].createWallet('1', '2', 1, 1, 'testnet', + function(err) { should.not.exist(err); - - clients[2].getTxProposals({ - getRawTxps: true - }, function(err, txs, rawTxps) { - should.not.exist(err); - - - clients[2].getEncryptedWalletData(function(err, toComplete) { - should.not.exist(err); - - // Disable networking - clients[0].request = sinon.stub().yields('no network'); - - // Make client incomplete - var data = JSON.parse(fsmock._get('client0')); - delete data.n; - fsmock._set('client0', JSON.stringify(data)); - - // Back to the air gapped - // - // Will trigger _tryToComplete and use pkr - // then, needs pkr to verify the txps - - clients[0].parseTxProposals({ - txps: rawTxps, - toComplete: toComplete, - }, function(err, txs2) { - should.not.exist(err); - done(); - }); - }); + // should keep cpub + var c0 = JSON.parse(fsmock._get('client0')); + var c1 = JSON.parse(fsmock._get('client1')); + _.each(['copayerId', 'network', 'publicKeyRing', + 'roPrivKey', 'rwPrivKey' + ], function(k) { + c0[k].should.deep.equal(c1[k]); }); + done(); }); + }); + }); + }); + }); + + it('should join from proxy from airgapped', function(done) { + // client0 -> airgapped + // client1 -> proxy + clients[0].generateKey('testnet', function(err) { + should.not.exist(err); + clients[0].export({ + access: 'readwrite' + }, function(err, str) { + clients[1].import(str, function(err) { + should.not.exist(err); + clients[2].createWallet('1', '2', 1, 2, 'testnet', function(err, secret) { + should.not.exist(err); + clients[1].joinWallet(secret, 'john', function(err) { + should.not.exist(err); + // should keep cpub + var c0 = JSON.parse(fsmock._get('client0')); + var c1 = JSON.parse(fsmock._get('client1')); + _.each(['copayerId', 'network', 'publicKeyRing', + 'roPrivKey', 'rwPrivKey' + ], function(k) { + c0[k].should.deep.equal(c1[k]); + }); + done(); + }) }); }); }); }); }); + it('should be able export signatures and sign later from a ro client', function(done) { helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { @@ -704,9 +705,9 @@ describe('client API ', function() { it('round trip #import #export', function(done) { helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { should.not.exist(err); - clients[0].export({}, function(err, str) { + clients[1].export({}, function(err, str) { should.not.exist(err); - var original = JSON.parse(fsmock._get('client0')); + var original = JSON.parse(fsmock._get('client1')); clients[2].import(str, function(err, wallet) { should.not.exist(err); var clone = JSON.parse(fsmock._get('client2')); @@ -719,7 +720,7 @@ describe('client API ', function() { }); }); it('should recreate a wallet, create addresses and receive money', function(done) { - var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; + var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]'; clients[0].import(backup, function(err, wallet) { should.not.exist(err); clients[0].reCreateWallet('pepe', function(err, wallet) {