diff --git a/js/src/api/api.js b/js/src/api/api.js index 5be20371e..2c102086f 100644 --- a/js/src/api/api.js +++ b/js/src/api/api.js @@ -55,7 +55,7 @@ export default class Api extends EventEmitter { .nodeKind() .then((nodeKind) => { if (nodeKind.availability === 'public') { - return new LocalAccountsMiddleware(transport); + return LocalAccountsMiddleware; } return null; diff --git a/js/src/api/local/ethkey/dummy.js b/js/src/api/local/ethkey/dummy.js deleted file mode 100644 index 38f7c84de..000000000 --- a/js/src/api/local/ethkey/dummy.js +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2015-2017 Parity Technologies (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -export default function () { - // empty file included while building parity.js (don't include local keygen) -} diff --git a/js/src/api/local/ethkey/index.spec.js b/js/src/api/local/ethkey/index.spec.js index c727e1d51..781c49b5c 100644 --- a/js/src/api/local/ethkey/index.spec.js +++ b/js/src/api/local/ethkey/index.spec.js @@ -18,8 +18,8 @@ import { randomPhrase } from '@parity/wordlist'; import { phraseToAddress, phraseToWallet } from './'; describe('api/local/ethkey', () => { - describe.skip('phraseToAddress', function () { - this.timeout(10000); + describe('phraseToAddress', function () { + this.timeout(30000); it('generates a valid address', () => { const phrase = randomPhrase(12); @@ -37,8 +37,8 @@ describe('api/local/ethkey', () => { }); }); - describe.skip('phraseToWallet', function () { - this.timeout(10000); + describe('phraseToWallet', function () { + this.timeout(30000); it('generates a valid wallet object', () => { const phrase = randomPhrase(12); diff --git a/js/src/api/local/ethkey/worker.js b/js/src/api/local/ethkey/worker.js index 00f4a0bed..ffb99d0e8 100644 --- a/js/src/api/local/ethkey/worker.js +++ b/js/src/api/local/ethkey/worker.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import secp256k1 from 'secp256k1/js'; +import secp256k1 from 'secp256k1'; import { keccak_256 as keccak256 } from 'js-sha3'; import { bytesToHex } from '~/api/util/format'; @@ -28,11 +28,9 @@ if (!isWorker) { } // keythereum should never be used outside of the browser -let keythereum = null; +let keythereum = require('keythereum'); if (isWorker) { - require('keythereum/dist/keythereum'); - keythereum = self.keythereum; } @@ -109,9 +107,13 @@ const actions = { }; self.onmessage = function ({ data }) { - const result = route(data); + try { + const result = route(data); - postMessage(result); + postMessage([null, result]); + } catch (err) { + postMessage([err, null]); + } }; // Emulate a web worker in Node.js @@ -119,9 +121,13 @@ class KeyWorker { postMessage (data) { // Force async setTimeout(() => { - const result = route(data); + try { + const result = route(data); - this.onmessage({ data: result }); + this.onmessage({ data: [null, result] }); + } catch (err) { + this.onmessage({ data: [err, null] }); + } }, 0); } diff --git a/js/src/api/local/ethkey/workerPool.js b/js/src/api/local/ethkey/workerPool.js index ff5315898..e4e4a5134 100644 --- a/js/src/api/local/ethkey/workerPool.js +++ b/js/src/api/local/ethkey/workerPool.js @@ -33,8 +33,15 @@ class WorkerContainer { return new Promise((resolve, reject) => { this._worker.postMessage({ action, payload }); this._worker.onmessage = ({ data }) => { + const [err, result] = data; + this.busy = false; - resolve(data); + + if (err) { + reject(err); + } else { + resolve(result); + } }; }); } diff --git a/js/src/api/local/index.js b/js/src/api/local/index.js index c1d4b60ca..1000fa330 100644 --- a/js/src/api/local/index.js +++ b/js/src/api/local/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export LocalAccountsMiddleware from './middleware'; +export LocalAccountsMiddleware from './localAccountsMiddleware'; diff --git a/js/src/api/local/middleware.js b/js/src/api/local/localAccountsMiddleware.js similarity index 94% rename from js/src/api/local/middleware.js rename to js/src/api/local/localAccountsMiddleware.js index 36a8cd2cf..05eefb3ca 100644 --- a/js/src/api/local/middleware.js +++ b/js/src/api/local/localAccountsMiddleware.js @@ -23,15 +23,6 @@ import { phraseToWallet, phraseToAddress, verifySecret } from './ethkey'; import { randomPhrase } from '@parity/wordlist'; export default class LocalAccountsMiddleware extends Middleware { - // Maps transaction requests to transaction hashes. - // This allows the locally-signed transactions to emulate the signer. - transactionHashes = {}; - transactions = {}; - - // Current transaction id. This doesn't need to be stored, as it's - // only relevant for the current the session. - transactionId = 1; - constructor (transport) { super(transport); @@ -170,6 +161,8 @@ export default class LocalAccountsMiddleware extends Middleware { data } = Object.assign(transactions.get(id), modify); + transactions.lock(id); + const account = accounts.get(from); return Promise.all([ diff --git a/js/src/api/local/localAccountsMiddleware.spec.js b/js/src/api/local/localAccountsMiddleware.spec.js new file mode 100644 index 000000000..7408b4b1e --- /dev/null +++ b/js/src/api/local/localAccountsMiddleware.spec.js @@ -0,0 +1,154 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import LocalAccountsMiddleware from './localAccountsMiddleware'; +import JsonRpcBase from '../transport/jsonRpcBase'; + +const RPC_RESPONSE = Symbol('RPC response'); +const ADDRESS = '0x00a329c0648769a73afac7f9381e08fb43dbea72'; +const SECRET = '0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7'; +const PASSWORD = 'password'; + +const FOO_PHRASE = 'foobar'; +const FOO_PASSWORD = 'foopass'; +const FOO_ADDRESS = '0x007ef7ac1058e5955e366ab9d6b6c4ebcc937e7e'; + +class MockedTransport extends JsonRpcBase { + _execute (method, params) { + return RPC_RESPONSE; + } +} + +describe('api/local/LocalAccountsMiddleware', function () { + this.timeout(30000); + + let transport; + + beforeEach(() => { + transport = new MockedTransport(); + transport.addMiddleware(LocalAccountsMiddleware); + + // Same as `parity_newAccountFromPhrase` with empty phrase + return transport + .execute('parity_newAccountFromSecret', SECRET, PASSWORD) + .catch((_err) => { + // Ignore the error - all instances of LocalAccountsMiddleware + // share account storage + }); + }); + + it('registers all necessary methods', () => { + return Promise + .all([ + 'eth_accounts', + 'eth_coinbase', + 'parity_accountsInfo', + 'parity_allAccountsInfo', + 'parity_changePassword', + 'parity_checkRequest', + 'parity_defaultAccount', + 'parity_generateSecretPhrase', + 'parity_getNewDappsAddresses', + 'parity_hardwareAccountsInfo', + 'parity_newAccountFromPhrase', + 'parity_newAccountFromSecret', + 'parity_setAccountMeta', + 'parity_setAccountName', + 'parity_postTransaction', + 'parity_phraseToAddress', + 'parity_useLocalAccounts', + 'parity_listGethAccounts', + 'parity_listRecentDapps', + 'parity_killAccount', + 'parity_testPassword', + 'signer_confirmRequest', + 'signer_rejectRequest', + 'signer_requestsToConfirm' + ].map((method) => { + return transport + .execute(method) + .then((result) => { + expect(result).not.to.be.equal(RPC_RESPONSE); + }) + // Some errors are expected here since we are calling methods + // without parameters. + .catch((_) => {}); + })); + }); + + it('allows non-registered methods through', () => { + return transport + .execute('eth_getBalance', '0x407d73d8a49eeb85d32cf465507dd71d507100c1') + .then((result) => { + expect(result).to.be.equal(RPC_RESPONSE); + }); + }); + + it('can handle `eth_accounts`', () => { + return transport + .execute('eth_accounts') + .then((accounts) => { + expect(accounts.length).to.be.equal(1); + expect(accounts[0]).to.be.equal(ADDRESS); + }); + }); + + it('can handle `parity_defaultAccount`', () => { + return transport + .execute('parity_defaultAccount') + .then((address) => { + expect(address).to.be.equal(ADDRESS); + }); + }); + + it('can handle `parity_phraseToAddress`', () => { + return transport + .execute('parity_phraseToAddress', '') + .then((address) => { + expect(address).to.be.equal(ADDRESS); + + return transport.execute('parity_phraseToAddress', FOO_PHRASE); + }) + .then((address) => { + expect(address).to.be.equal(FOO_ADDRESS); + }); + }); + + it('can create and kill an account', () => { + return transport + .execute('parity_newAccountFromPhrase', FOO_PHRASE, FOO_PASSWORD) + .then((address) => { + expect(address).to.be.equal(FOO_ADDRESS); + + return transport.execute('eth_accounts'); + }) + .then((accounts) => { + expect(accounts.length).to.be.equal(2); + expect(accounts.includes(FOO_ADDRESS)).to.be.true; + + return transport.execute('parity_killAccount', FOO_ADDRESS, FOO_PASSWORD); + }) + .then((result) => { + expect(result).to.be.true; + + return transport.execute('eth_accounts'); + }) + .then((accounts) => { + expect(accounts.length).to.be.equal(1); + expect(accounts.includes(FOO_ADDRESS)).to.be.false; + }); + }); +}); diff --git a/js/src/api/local/transactions.js b/js/src/api/local/transactions.js index 57d1eee62..757255b4a 100644 --- a/js/src/api/local/transactions.js +++ b/js/src/api/local/transactions.js @@ -18,6 +18,7 @@ import { toHex } from '../util/format'; import { TransportError } from '../transport'; const AWAITING = Symbol('awaiting'); +const LOCKED = Symbol('locked'); const CONFIRMED = Symbol('confirmed'); const REJECTED = Symbol('rejected'); @@ -57,6 +58,16 @@ class Transactions { return state.transaction; } + lock (id) { + const state = this._states[id]; + + if (!state || state.status !== AWAITING) { + throw new Error('Trying to lock an invalid transaction'); + } + + state.status = LOCKED; + } + hash (id) { const state = this._states[id]; @@ -76,9 +87,12 @@ class Transactions { confirm (id, hash) { const state = this._states[id]; + const status = state ? state.status : null; - if (!state || state.status !== AWAITING) { - throw new Error('Trying to confirm an invalid transaction'); + switch (status) { + case AWAITING: break; + case LOCKED: break; + default: throw new Error('Trying to confirm an invalid transaction'); } state.hash = hash; diff --git a/js/src/api/local/transactions.spec.js b/js/src/api/local/transactions.spec.js index 84482ff57..65f2d8ddc 100644 --- a/js/src/api/local/transactions.spec.js +++ b/js/src/api/local/transactions.spec.js @@ -65,4 +65,21 @@ describe('api/local/transactions', () => { expect(requests.length).to.be.equal(0); expect(() => transactions.hash(id)).to.throw(TransportError); }); + + it('can lock and confirm transactions', () => { + const id = transactions.add(DUMMY_TX); + const hash = '0x1111111111111111111111111111111111111111'; + + transactions.lock(id); + + const requests = transactions.requestsToConfirm(); + + expect(requests.length).to.be.equal(0); + expect(transactions.get(id)).to.be.null; + expect(transactions.hash(id)).to.be.null; + + transactions.confirm(id, hash); + + expect(transactions.hash(id)).to.be.equal(hash); + }); }); diff --git a/js/src/api/transport/jsonRpcBase.js b/js/src/api/transport/jsonRpcBase.js index 573204c3e..819e1f496 100644 --- a/js/src/api/transport/jsonRpcBase.js +++ b/js/src/api/transport/jsonRpcBase.js @@ -38,20 +38,20 @@ export default class JsonRpcBase extends EventEmitter { return json; } - addMiddleware (middleware) { + addMiddleware (Middleware) { this._middlewareList = Promise .all([ - middleware, + Middleware, this._middlewareList ]) - .then(([middleware, middlewareList]) => { + .then(([Middleware, middlewareList]) => { // Do nothing if `handlerPromise` resolves to a null-y value. - if (middleware == null) { + if (Middleware == null) { return middlewareList; } // don't mutate the original array - return middlewareList.concat([middleware]); + return middlewareList.concat([new Middleware(this)]); }); } @@ -80,8 +80,8 @@ export default class JsonRpcBase extends EventEmitter { const res = middleware.handle(method, params); if (res != null) { - // If `res` isn't a promise, we need to wrap it - return Promise.resolve(res) + return Promise + .resolve(res) .then((res) => { const result = this._wrapSuccessResult(res); const json = this.encode(method, params); diff --git a/js/src/api/transport/middleware.js b/js/src/api/transport/middleware.js index 5a4945e7e..7d7199f95 100644 --- a/js/src/api/transport/middleware.js +++ b/js/src/api/transport/middleware.js @@ -28,9 +28,7 @@ export default class Middleware { const handler = this._handlers[method]; if (handler != null) { - const response = handler(params); - - return response; + return handler(params); } return null; diff --git a/js/src/api/transport/middleware.spec.js b/js/src/api/transport/middleware.spec.js index 27b81c49f..4ae894135 100644 --- a/js/src/api/transport/middleware.spec.js +++ b/js/src/api/transport/middleware.spec.js @@ -25,17 +25,21 @@ class MockTransport extends JsonRpcBase { } } +class MockMiddleware extends Middleware { + constructor (transport) { + super(transport); + + this.register('mock_rpc', ([num]) => num); + this.register('mock_null', () => null); + } +} + describe('api/transport/Middleware', () => { - let middleware; let transport; beforeEach(() => { transport = new MockTransport(); - middleware = new Middleware(transport); - - middleware.register('mock_rpc', ([num]) => num); - middleware.register('mock_null', () => null); - transport.addMiddleware(middleware); + transport.addMiddleware(MockMiddleware); }); it('Routes requests to middleware', () => { diff --git a/js/webpack/app.js b/js/webpack/app.js index d121b6518..ded5c4468 100644 --- a/js/webpack/app.js +++ b/js/webpack/app.js @@ -138,7 +138,9 @@ module.exports = { resolve: { alias: { - '~': path.resolve(__dirname, '../src') + '~': path.resolve(__dirname, '../src'), + 'secp256k1': path.resolve(__dirname, '../node_modules/secp256k1/js'), + 'keythereum': path.resolve(__dirname, '../node_modules/keythereum/dist/keythereum') }, modules: [ path.join(__dirname, '../node_modules') diff --git a/js/webpack/libraries.js b/js/webpack/libraries.js index 3ddd45f2c..1fcb39eba 100644 --- a/js/webpack/libraries.js +++ b/js/webpack/libraries.js @@ -41,7 +41,9 @@ module.exports = { resolve: { alias: { - '~': path.resolve(__dirname, '../src') + '~': path.resolve(__dirname, '../src'), + 'secp256k1': path.resolve(__dirname, '../node_modules/secp256k1/js'), + 'keythereum': path.resolve(__dirname, '../node_modules/keythereum/dist/keythereum') } }, diff --git a/js/webpack/npm.js b/js/webpack/npm.js index b526b2f0f..2230bf90f 100644 --- a/js/webpack/npm.js +++ b/js/webpack/npm.js @@ -76,7 +76,6 @@ module.exports = { resolve: { alias: { - 'secp256k1/js': path.resolve(__dirname, '../src/api/local/ethkey/dummy.js'), '~': path.resolve(__dirname, '../src') }, modules: [