Public node WASM, performance and fixes (#5734)

This commit is contained in:
Maciej Hirsz 2017-06-12 15:57:16 +02:00 committed by Arkadiy Paronyan
parent edea41d35e
commit b2a42f03eb
32 changed files with 739 additions and 129 deletions

View File

@ -159,6 +159,7 @@
}, },
"dependencies": { "dependencies": {
"@parity/wordlist": "1.0.1", "@parity/wordlist": "1.0.1",
"arraybuffer-loader": "0.2.2",
"babel-runtime": "6.23.0", "babel-runtime": "6.23.0",
"base32.js": "0.1.0", "base32.js": "0.1.0",
"bignumber.js": "3.0.1", "bignumber.js": "3.0.1",
@ -218,7 +219,6 @@
"redux-thunk": "2.1.0", "redux-thunk": "2.1.0",
"rlp": "2.0.0", "rlp": "2.0.0",
"scryptsy": "2.0.0", "scryptsy": "2.0.0",
"secp256k1": "3.2.5",
"solc": "ngotchac/solc-js", "solc": "ngotchac/solc-js",
"store": "1.3.20", "store": "1.3.20",
"sw-toolbox": "^3.6.0", "sw-toolbox": "^3.6.0",

View File

@ -23,7 +23,7 @@ import { Db, Eth, Parity, Net, Personal, Shh, Signer, Trace, Web3 } from './rpc'
import Subscriptions from './subscriptions'; import Subscriptions from './subscriptions';
import util from './util'; import util from './util';
import { isFunction } from './util/types'; import { isFunction } from './util/types';
// import { LocalAccountsMiddleware } from './local'; import { LocalAccountsMiddleware } from './local';
export default class Api extends EventEmitter { export default class Api extends EventEmitter {
constructor (transport, allowSubscriptions = true) { constructor (transport, allowSubscriptions = true) {
@ -54,9 +54,9 @@ export default class Api extends EventEmitter {
const middleware = this.parity const middleware = this.parity
.nodeKind() .nodeKind()
.then((nodeKind) => { .then((nodeKind) => {
// if (nodeKind.availability === 'public') { if (nodeKind.availability === 'public') {
// return LocalAccountsMiddleware; return LocalAccountsMiddleware;
// } }
return null; return null;
}) })

View File

@ -17,9 +17,9 @@
import { createKeyObject, decryptPrivateKey } from '../ethkey'; import { createKeyObject, decryptPrivateKey } from '../ethkey';
export default class Account { export default class Account {
constructor (persist, data) { constructor (persist, data = {}) {
const { const {
keyObject, keyObject = null,
meta = {}, meta = {},
name = '' name = ''
} = data; } = data;
@ -41,6 +41,15 @@ export default class Account {
}); });
} }
export () {
const exported = Object.assign({}, this._keyObject);
exported.meta = JSON.stringify(this._meta);
exported.name = this._name;
return exported;
}
get address () { get address () {
return `0x${this._keyObject.address.toLowerCase()}`; return `0x${this._keyObject.address.toLowerCase()}`;
} }
@ -66,6 +75,10 @@ export default class Account {
} }
get uuid () { get uuid () {
if (!this._keyObject) {
return null;
}
return this._keyObject.id; return this._keyObject.id;
} }

View File

@ -17,23 +17,74 @@
import Account from './account'; import Account from './account';
import localStore from 'store'; import localStore from 'store';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { decryptPrivateKey } from '../ethkey';
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
const LS_STORE_KEY = '_parity::localAccounts'; const LS_STORE_KEY = '_parity::localAccounts';
export default class Accounts { export default class Accounts {
constructor (data = localStore.get(LS_STORE_KEY) || {}) { persist = debounce(() => {
const { this._lastState = JSON.stringify(this);
last = NULL_ADDRESS,
store = []
} = data;
this.persist = debounce(() => {
localStore.set(LS_STORE_KEY, this); localStore.set(LS_STORE_KEY, this);
}, 100); }, 100);
constructor (data = localStore.get(LS_STORE_KEY) || {}) {
this._lastState = JSON.stringify(data);
window.addEventListener('storage', ({ key, newValue }) => {
if (key !== LS_STORE_KEY) {
return;
}
if (newValue !== this._lastState) {
console.log('Data changed in a second tab, syncing state');
this.restore(JSON.parse(newValue));
}
});
this.restore(data);
}
restore (data) {
const {
last = NULL_ADDRESS,
dappsDefault = NULL_ADDRESS,
store = {}
} = data;
this._last = last; this._last = last;
this._store = store.map((data) => new Account(this.persist, data)); this._dappsDefaultAddress = dappsDefault;
this._store = {};
if (Array.isArray(store)) {
// Recover older version that stored accounts as an array
store.forEach((data) => {
const account = new Account(this.persist, data);
this._store[account.address] = account;
});
} else {
Object.keys(store).forEach((key) => {
this._store[key] = new Account(this.persist, store[key]);
});
}
}
_addAccount = (account) => {
const { address } = account;
if (address in this._store && this._store[address].uuid) {
throw new Error(`Account ${address} already exists!`);
}
this._store[address] = account;
this.lastAddress = address;
this.persist();
return account.address;
} }
create (secret, password) { create (secret, password) {
@ -41,20 +92,19 @@ export default class Accounts {
return Account return Account
.fromPrivateKey(this.persist, privateKey, password) .fromPrivateKey(this.persist, privateKey, password)
.then((account) => { .then(this._addAccount);
const { address } = account;
if (this._store.find((account) => account.address === address)) {
throw new Error(`Account ${address} already exists!`);
} }
this._store.push(account); restoreFromWallet (wallet, password) {
this.lastAddress = address; return decryptPrivateKey(wallet, password)
.then((privateKey) => {
if (!privateKey) {
throw new Error('Invalid password');
}
this.persist(); return Account.fromPrivateKey(this.persist, privateKey, password);
})
return account.address; .then(this._addAccount);
});
} }
set lastAddress (value) { set lastAddress (value) {
@ -65,20 +115,48 @@ export default class Accounts {
return this._last; return this._last;
} }
get dappsDefaultAddress () {
if (this._dappsDefaultAddress === NULL_ADDRESS) {
return this._last;
}
if (this._dappsDefaultAddress in this._store) {
return this._dappsDefaultAddress;
}
return NULL_ADDRESS;
}
set dappsDefaultAddress (value) {
this._dappsDefaultAddress = value.toLowerCase();
}
get (address) { get (address) {
address = address.toLowerCase(); address = address.toLowerCase();
this.lastAddress = address; const account = this._store[address];
const account = this._store.find((account) => account.address === address);
if (!account) { if (!account) {
throw new Error(`Account not found: ${address}`); throw new Error(`Account not found: ${address}`);
} }
this.lastAddress = address;
return account; return account;
} }
getLazyCreate (address) {
address = address.toLowerCase();
this.lastAddress = address;
if (!(address in this._store)) {
this._store[address] = new Account(this.persist);
}
return this._store[address];
}
remove (address, password) { remove (address, password) {
address = address.toLowerCase(); address = address.toLowerCase();
@ -108,26 +186,20 @@ export default class Accounts {
removeUnsafe (address) { removeUnsafe (address) {
address = address.toLowerCase(); address = address.toLowerCase();
const index = this._store.findIndex((account) => account.address === address); delete this._store[address];
if (index === -1) {
return;
}
this._store.splice(index, 1);
this.persist(); this.persist();
} }
mapArray (mapper) { addresses () {
return this._store.map(mapper); return Object.keys(this._store);
} }
mapObject (mapper) { map (mapper) {
const result = {}; const result = {};
this._store.forEach((account) => { Object.keys(this._store).forEach((key) => {
result[account.address] = mapper(account); result[key] = mapper(this._store[key]);
}); });
return result; return result;
@ -136,6 +208,7 @@ export default class Accounts {
toJSON () { toJSON () {
return { return {
last: this._last, last: this._last,
dappsDefault: this._dappsDefaultAddress,
store: this._store store: this._store
}; };
} }

View File

@ -0,0 +1,147 @@
// 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 <http://www.gnu.org/licenses/>.
/* global WebAssembly */
import wasmBuffer from './ethkey.wasm.js';
const NOOP = () => {};
// WASM memory setup
const WASM_PAGE_SIZE = 65536;
const STATIC_BASE = 1024;
const STATICTOP = STATIC_BASE + WASM_PAGE_SIZE * 2;
const STACK_BASE = align(STATICTOP + 16);
const STACKTOP = STACK_BASE;
const TOTAL_STACK = 5 * 1024 * 1024;
const TOTAL_MEMORY = 16777216;
const STACK_MAX = STACK_BASE + TOTAL_STACK;
const DYNAMIC_BASE = STACK_MAX + 64;
const DYNAMICTOP_PTR = STACK_MAX;
function mockWebAssembly () {
function throwWasmError () {
throw new Error('Missing WebAssembly support');
}
// Simple mock replacement
return {
Memory: class { buffer = new ArrayBuffer(2048) },
Table: class {},
Module: class {},
Instance: class {
exports = {
'_input_ptr': () => 0,
'_secret_ptr': () => 0,
'_public_ptr': () => 0,
'_address_ptr': () => 0,
'_ecpointg': NOOP,
'_brain': throwWasmError,
'_verify_secret': throwWasmError
}
}
};
}
const { Memory, Table, Module, Instance } = typeof WebAssembly !== 'undefined' ? WebAssembly : mockWebAssembly();
const wasmMemory = new Memory({
initial: TOTAL_MEMORY / WASM_PAGE_SIZE,
maximum: TOTAL_MEMORY / WASM_PAGE_SIZE
});
const wasmTable = new Table({
initial: 8,
maximum: 8,
element: 'anyfunc'
});
// TypedArray views into the memory
const wasmMemoryU8 = new Uint8Array(wasmMemory.buffer);
const wasmMemoryU32 = new Uint32Array(wasmMemory.buffer);
// Keep DYNAMIC_BASE in memory
wasmMemoryU32[DYNAMICTOP_PTR >> 2] = align(DYNAMIC_BASE);
function align (mem) {
const ALIGN_SIZE = 16;
return (Math.ceil(mem / ALIGN_SIZE) * ALIGN_SIZE) | 0;
}
export function slice (ptr, len) {
return wasmMemoryU8.subarray(ptr, ptr + len);
}
// Required by emscripten
function abort (what) {
throw new Error(what || 'WASM abort');
}
// Required by emscripten
function abortOnCannotGrowMemory () {
abort(`Cannot enlarge memory arrays.`);
}
// Required by emscripten
function enlargeMemory () {
abortOnCannotGrowMemory();
}
// Required by emscripten
function getTotalMemory () {
return TOTAL_MEMORY;
}
// Required by emscripten - used to perform memcpy on large data
function memcpy (dest, src, len) {
wasmMemoryU8.set(wasmMemoryU8.subarray(src, src + len), dest);
return dest;
}
// Synchronously compile WASM from the buffer
const module = new Module(wasmBuffer);
// Instantiated WASM module
const instance = new Instance(module, {
global: {},
env: {
DYNAMICTOP_PTR,
STACKTOP,
STACK_MAX,
abort,
enlargeMemory,
getTotalMemory,
abortOnCannotGrowMemory,
___lock: NOOP,
___syscall6: () => 0,
___setErrNo: (no) => no,
_abort: abort,
___syscall140: () => 0,
_emscripten_memcpy_big: memcpy,
___syscall54: () => 0,
___unlock: NOOP,
_llvm_trap: abort,
___syscall146: () => 0,
'memory': wasmMemory,
'table': wasmTable,
tableBase: 0,
memoryBase: STATIC_BASE
}
});
export const extern = instance.exports;

File diff suppressed because one or more lines are too long

View File

@ -17,13 +17,12 @@
import workerPool from './workerPool'; import workerPool from './workerPool';
export function createKeyObject (key, password) { export function createKeyObject (key, password) {
return workerPool.getWorker().action('createKeyObject', { key, password }) return workerPool.action('createKeyObject', { key, password })
.then((obj) => JSON.parse(obj)); .then((obj) => JSON.parse(obj));
} }
export function decryptPrivateKey (keyObject, password) { export function decryptPrivateKey (keyObject, password) {
return workerPool return workerPool
.getWorker()
.action('decryptPrivateKey', { keyObject, password }) .action('decryptPrivateKey', { keyObject, password })
.then((privateKey) => { .then((privateKey) => {
if (privateKey) { if (privateKey) {
@ -40,9 +39,9 @@ export function phraseToAddress (phrase) {
} }
export function phraseToWallet (phrase) { export function phraseToWallet (phrase) {
return workerPool.getWorker().action('phraseToWallet', phrase); return workerPool.action('phraseToWallet', phrase);
} }
export function verifySecret (secret) { export function verifySecret (secret) {
return workerPool.getWorker().action('verifySecret', secret); return workerPool.action('verifySecret', secret);
} }

View File

@ -17,7 +17,8 @@
import { randomPhrase } from '@parity/wordlist'; import { randomPhrase } from '@parity/wordlist';
import { phraseToAddress, phraseToWallet } from './'; import { phraseToAddress, phraseToWallet } from './';
describe('api/local/ethkey', () => { // TODO: Skipping until Node.js 8.0 comes out and we can test WebAssembly
describe.skip('api/local/ethkey', () => {
describe('phraseToAddress', function () { describe('phraseToAddress', function () {
this.timeout(30000); this.timeout(30000);

View File

@ -14,9 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import secp256k1 from 'secp256k1';
import { keccak_256 as keccak256 } from 'js-sha3';
import { bytesToHex } from '~/api/util/format'; import { bytesToHex } from '~/api/util/format';
import { extern, slice } from './ethkey.js';
const isWorker = typeof self !== 'undefined'; const isWorker = typeof self !== 'undefined';
@ -42,43 +41,40 @@ function route ({ action, payload }) {
return null; return null;
} }
const input = slice(extern._input_ptr(), 1024);
const secret = slice(extern._secret_ptr(), 32);
const publicKey = slice(extern._public_ptr(), 64);
const address = slice(extern._address_ptr(), 20);
extern._ecpointg();
const actions = { const actions = {
phraseToWallet (phrase) { phraseToWallet (phrase) {
let secret = keccak256.array(phrase); const phraseUtf8 = Buffer.from(phrase, 'utf8');
for (let i = 0; i < 16384; i++) { if (phraseUtf8.length > input.length) {
secret = keccak256.array(secret); throw new Error('Phrase is too long!');
} }
while (true) { input.set(phraseUtf8);
secret = keccak256.array(secret);
const secretBuf = Buffer.from(secret); extern._brain(phraseUtf8.length);
if (secp256k1.privateKeyVerify(secretBuf)) {
// No compression, slice out last 64 bytes
const publicBuf = secp256k1.publicKeyCreate(secretBuf, false).slice(-64);
const address = keccak256.array(publicBuf).slice(12);
if (address[0] !== 0) {
continue;
}
const wallet = { const wallet = {
secret: bytesToHex(secretBuf), secret: bytesToHex(secret),
public: bytesToHex(publicBuf), public: bytesToHex(publicKey),
address: bytesToHex(address) address: bytesToHex(address)
}; };
return wallet; return wallet;
}
}
}, },
verifySecret (secret) { verifySecret (key) {
const key = Buffer.from(secret.slice(2), 'hex'); const keyBuf = Buffer.from(key.slice(2), 'hex');
return secp256k1.privateKeyVerify(key); secret.set(keyBuf);
return extern._verify_secret();
}, },
createKeyObject ({ key, password }) { createKeyObject ({ key, password }) {
@ -112,7 +108,8 @@ self.onmessage = function ({ data }) {
postMessage([null, result]); postMessage([null, result]);
} catch (err) { } catch (err) {
postMessage([err, null]); console.error(err);
postMessage([err.toString(), null]);
} }
}; };

View File

@ -38,7 +38,8 @@ class WorkerContainer {
this.busy = false; this.busy = false;
if (err) { if (err) {
reject(err); // `err` ought to be a String
reject(new Error(err));
} else { } else {
resolve(result); resolve(result);
} }
@ -48,20 +49,56 @@ class WorkerContainer {
} }
class WorkerPool { class WorkerPool {
pool = []; pool = [
new WorkerContainer(),
new WorkerContainer()
];
getWorker () { queue = [];
let container = this.pool.find((container) => !container.busy);
if (container) { _getContainer () {
return container; return this.pool.find((container) => !container.busy);
} }
container = new WorkerContainer(); action (action, payload) {
let container = this.pool.find((container) => !container.busy);
this.pool.push(container); let promise;
return container; // const start = Date.now();
if (container) {
promise = container.action(action, payload);
} else {
promise = new Promise((resolve, reject) => {
this.queue.push([action, payload, resolve]);
});
}
return promise
.catch((err) => {
this.processQueue();
throw err;
})
.then((result) => {
this.processQueue();
// console.log('Work done in ', Date.now() - start);
return result;
});
}
processQueue () {
let container = this._getContainer();
while (container && this.queue.length > 0) {
const [action, payload, resolve] = this.queue.shift();
resolve(container.action(action, payload));
container = this._getContainer();
}
} }
} }

View File

@ -29,7 +29,7 @@ export default class LocalAccountsMiddleware extends Middleware {
const register = this.register.bind(this); const register = this.register.bind(this);
register('eth_accounts', () => { register('eth_accounts', () => {
return accounts.mapArray((account) => account.address); return accounts.addresses();
}); });
register('eth_coinbase', () => { register('eth_coinbase', () => {
@ -37,13 +37,13 @@ export default class LocalAccountsMiddleware extends Middleware {
}); });
register('parity_accountsInfo', () => { register('parity_accountsInfo', () => {
return accounts.mapObject(({ name }) => { return accounts.map(({ name }) => {
return { name }; return { name };
}); });
}); });
register('parity_allAccountsInfo', () => { register('parity_allAccountsInfo', () => {
return accounts.mapObject(({ name, meta, uuid }) => { return accounts.map(({ name, meta, uuid }) => {
return { name, meta, uuid }; return { name, meta, uuid };
}); });
}); });
@ -68,10 +68,31 @@ export default class LocalAccountsMiddleware extends Middleware {
return transactions.hash(id) || Promise.resolve(null); return transactions.hash(id) || Promise.resolve(null);
}); });
register('parity_dappsList', () => {
return [];
});
register('parity_defaultAccount', () => { register('parity_defaultAccount', () => {
return accounts.lastAddress; return accounts.lastAddress;
}); });
register('parity_exportAccount', ([address, password]) => {
const account = accounts.get(address);
if (!password) {
password = '';
}
return account.isValidPassword(password)
.then((isValid) => {
if (!isValid) {
throw new Error('Invalid password');
}
return account.export();
});
});
register('parity_generateSecretPhrase', () => { register('parity_generateSecretPhrase', () => {
return randomPhrase(12); return randomPhrase(12);
}); });
@ -80,6 +101,10 @@ export default class LocalAccountsMiddleware extends Middleware {
return []; return [];
}); });
register('parity_getNewDappsDefaultAddress', () => {
return accounts.lastAddress;
});
register('parity_hardwareAccountsInfo', () => { register('parity_hardwareAccountsInfo', () => {
return {}; return {};
}); });
@ -102,18 +127,30 @@ export default class LocalAccountsMiddleware extends Middleware {
}); });
}); });
register('parity_newAccountFromWallet', ([json, password]) => {
if (!password) {
password = '';
}
return accounts.restoreFromWallet(JSON.parse(json), password);
});
register('parity_setAccountMeta', ([address, meta]) => { register('parity_setAccountMeta', ([address, meta]) => {
accounts.get(address).meta = meta; accounts.getLazyCreate(address).meta = meta;
return true; return true;
}); });
register('parity_setAccountName', ([address, name]) => { register('parity_setAccountName', ([address, name]) => {
accounts.get(address).name = name; accounts.getLazyCreate(address).name = name;
return true; return true;
}); });
register('parity_setNewDappsDefaultAddress', ([address]) => {
accounts.dappsDefaultAddress = address;
});
register('parity_postTransaction', ([tx]) => { register('parity_postTransaction', ([tx]) => {
if (!tx.from) { if (!tx.from) {
tx.from = accounts.lastAddress; tx.from = accounts.lastAddress;
@ -137,10 +174,32 @@ export default class LocalAccountsMiddleware extends Middleware {
return []; return [];
}); });
register('parity_listOpenedVaults', () => {
return [];
});
register('parity_listRecentDapps', () => { register('parity_listRecentDapps', () => {
return {}; return {};
}); });
register('parity_listVaults', () => {
return [];
});
register('parity_wsUrl', () => {
// This is a hack, will be replaced by a `hostname` setting on the node itself
return `${window.location.hostname}:8546`;
});
register('parity_dappsUrl', () => {
// This is a hack, will be replaced by a `hostname` setting on the node itself
return `${window.location.hostname}:8545`;
});
register('parity_hashContent', () => {
throw new Error('Functionality unavailable on a public wallet.');
});
register('parity_killAccount', ([address, password]) => { register('parity_killAccount', ([address, password]) => {
return accounts.remove(address, password); return accounts.remove(address, password);
}); });
@ -204,6 +263,10 @@ export default class LocalAccountsMiddleware extends Middleware {
}); });
}); });
register('signer_generateAuthorizationToken', () => {
return '';
});
register('signer_rejectRequest', ([id]) => { register('signer_rejectRequest', ([id]) => {
return transactions.reject(id); return transactions.reject(id);
}); });

View File

@ -71,7 +71,9 @@ describe('api/local/LocalAccountsMiddleware', function () {
'parity_phraseToAddress', 'parity_phraseToAddress',
'parity_useLocalAccounts', 'parity_useLocalAccounts',
'parity_listGethAccounts', 'parity_listGethAccounts',
'parity_listOpenedVaults',
'parity_listRecentDapps', 'parity_listRecentDapps',
'parity_listVaults',
'parity_killAccount', 'parity_killAccount',
'parity_testPassword', 'parity_testPassword',
'signer_confirmRequest', 'signer_confirmRequest',

View File

@ -41,6 +41,7 @@ class Accounts extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
accountsInfo: PropTypes.object.isRequired, accountsInfo: PropTypes.object.isRequired,
availability: PropTypes.string.isRequired,
hasAccounts: PropTypes.bool.isRequired, hasAccounts: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired setVisibleAccounts: PropTypes.func.isRequired
} }
@ -249,21 +250,7 @@ class Accounts extends Component {
renderActionbar () { renderActionbar () {
const buttons = [ const buttons = [
<Link this.renderVaultsButton(),
to='/vaults'
key='vaults'
>
<Button
icon={ <KeyIcon /> }
label={
<FormattedMessage
id='accounts.button.vaults'
defaultMessage='vaults'
/>
}
onClick={ this.onVaultsClick }
/>
</Link>,
<Button <Button
key='newAccount' key='newAccount'
icon={ <AddIcon /> } icon={ <AddIcon /> }
@ -275,17 +262,7 @@ class Accounts extends Component {
} }
onClick={ this.onNewAccountClick } onClick={ this.onNewAccountClick }
/>, />,
<Button this.renderNewWalletButton(),
key='newWallet'
icon={ <AddIcon /> }
label={
<FormattedMessage
id='accounts.button.newWallet'
defaultMessage='wallet'
/>
}
onClick={ this.onNewWalletClick }
/>,
<Button <Button
key='restoreAccount' key='restoreAccount'
icon={ <AddIcon /> } icon={ <AddIcon /> }
@ -370,6 +347,50 @@ class Accounts extends Component {
); );
} }
renderVaultsButton () {
if (this.props.availability !== 'personal') {
return null;
}
return (
<Link
to='/vaults'
key='vaults'
>
<Button
icon={ <KeyIcon /> }
label={
<FormattedMessage
id='accounts.button.vaults'
defaultMessage='vaults'
/>
}
onClick={ this.onVaultsClick }
/>
</Link>
);
}
renderNewWalletButton () {
if (this.props.availability !== 'personal') {
return null;
}
return (
<Button
key='newWallet'
icon={ <AddIcon /> }
label={
<FormattedMessage
id='accounts.button.newWallet'
defaultMessage='wallet'
/>
}
onClick={ this.onNewWalletClick }
/>
);
}
renderNewWalletDialog () { renderNewWalletDialog () {
const { accounts } = this.props; const { accounts } = this.props;
const { newWalletDialog } = this.state; const { newWalletDialog } = this.state;
@ -474,10 +495,12 @@ class Accounts extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts, accountsInfo, hasAccounts } = state.personal; const { accounts, accountsInfo, hasAccounts } = state.personal;
const { availability = 'unknown' } = state.nodeStatus.nodeKind || {};
return { return {
accounts, accounts,
accountsInfo, accountsInfo,
availability,
hasAccounts hasAccounts
}; };
} }

View File

@ -99,11 +99,19 @@ function mapStateToProps (initState) {
})); }));
return (state) => { return (state) => {
const { availability = 'unknown' } = state.nodeStatus.nodeKind || {};
const { views } = state.settings; const { views } = state.settings;
const viewIds = Object const viewIds = Object
.keys(views) .keys(views)
.filter((id) => views[id].fixed || views[id].active); .filter((id) => {
const view = views[id];
const isEnabled = view.fixed || view.active;
const isAllowed = !view.onlyPersonal || availability === 'personal';
return isEnabled && isAllowed;
});
if (isEqual(viewIds, filteredViewIds)) { if (isEqual(viewIds, filteredViewIds)) {
return { views: filteredViews }; return { views: filteredViews };

View File

@ -63,7 +63,8 @@
"author": "Parity Team <admin@parity.io>", "author": "Parity Team <admin@parity.io>",
"version": "1.0.0", "version": "1.0.0",
"visible": false, "visible": false,
"secure": true "secure": true,
"onlyPersonal": true
}, },
{ {
"id": "0xae74ad174b95cdbd01c88ac5b73a296d33e9088fc2a200e76bcedf3a94a7815d", "id": "0xae74ad174b95cdbd01c88ac5b73a296d33e9088fc2a200e76bcedf3a94a7815d",
@ -94,6 +95,7 @@
"version": "1.0.0", "version": "1.0.0",
"visible": true, "visible": true,
"skipBuild": true, "skipBuild": true,
"skipHistory": true "skipHistory": true,
"onlyPersonal": true
} }
] ]

View File

@ -34,10 +34,11 @@ import styles from './dapps.css';
class Dapps extends Component { class Dapps extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} };
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired accounts: PropTypes.object.isRequired,
availability: PropTypes.string.isRequired
}; };
store = DappsStore.get(this.context.api); store = DappsStore.get(this.context.api);
@ -133,6 +134,10 @@ class Dapps extends Component {
} }
renderApp = (app) => { renderApp = (app) => {
if (app.onlyPersonal && this.props.availability !== 'personal') {
return null;
}
return ( return (
<DappCard <DappCard
app={ app } app={ app }
@ -156,6 +161,7 @@ class Dapps extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts } = state.personal; const { accounts } = state.personal;
const { availability = 'unknown' } = state.nodeStatus.nodeKind || {};
/** /**
* Do not show the Wallet Accounts in the Dapps * Do not show the Wallet Accounts in the Dapps
@ -165,7 +171,8 @@ function mapStateToProps (state) {
const _accounts = omitBy(accounts, (account) => account.wallet); const _accounts = omitBy(accounts, (account) => account.wallet);
return { return {
accounts: _accounts accounts: _accounts,
availability
}; };
} }

View File

@ -16,6 +16,7 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import HistoryStore from '~/mobx/historyStore'; import HistoryStore from '~/mobx/historyStore';
@ -32,11 +33,15 @@ import Urls from './Urls';
import styles from './home.css'; import styles from './home.css';
@observer @observer
export default class Home extends Component { class Home extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
}; };
static propTypes = {
availability: PropTypes.string.isRequired
};
dappsStore = DappsStore.get(this.context.api); dappsStore = DappsStore.get(this.context.api);
extensionStore = ExtensionStore.get(); extensionStore = ExtensionStore.get();
webStore = WebStore.get(this.context.api); webStore = WebStore.get(this.context.api);
@ -49,6 +54,13 @@ export default class Home extends Component {
} }
render () { render () {
const urls = this.props.availability !== 'personal' ? null : (
<Urls
extensionStore={ this.extensionStore }
store={ this.webStore }
/>
);
return ( return (
<Page <Page
className={ styles.body } className={ styles.body }
@ -60,10 +72,7 @@ export default class Home extends Component {
} }
> >
<News /> <News />
<Urls { urls }
extensionStore={ this.extensionStore }
store={ this.webStore }
/>
<div className={ styles.row }> <div className={ styles.row }>
<div className={ styles.column }> <div className={ styles.column }>
<Dapps <Dapps
@ -79,3 +88,16 @@ export default class Home extends Component {
); );
} }
} }
function mapStateToProps (initState) {
return (state) => {
const { availability = 'unknown' } = state.nodeStatus.nodeKind || {};
return { availability };
};
}
export default connect(
mapStateToProps,
null
)(Home);

View File

@ -59,6 +59,7 @@ const defaultViews = {
contracts: { contracts: {
active: false, active: false,
onlyPersonal: true,
icon: <ContactsIcon />, icon: <ContactsIcon />,
route: '/contracts', route: '/contracts',
value: 'contract' value: 'contract'
@ -66,6 +67,7 @@ const defaultViews = {
status: { status: {
active: false, active: false,
onlyPersonal: true,
icon: <StatusIcon />, icon: <StatusIcon />,
route: '/status', route: '/status',
value: 'status' value: 'status'

View File

@ -30,7 +30,8 @@ import styles from './views.css';
class Views extends Component { class Views extends Component {
static propTypes = { static propTypes = {
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,
toggleView: PropTypes.func.isRequired toggleView: PropTypes.func.isRequired,
availability: PropTypes.string.isRequired
} }
render () { render () {
@ -168,6 +169,10 @@ class Views extends Component {
const toggle = () => toggleView(id); const toggle = () => toggleView(id);
const view = settings.views[id]; const view = settings.views[id];
if (view.onlyPersonal && this.props.availability !== 'personal') {
return null;
}
return ( return (
<div className={ styles.view } key={ id }> <div className={ styles.view } key={ id }>
<Checkbox <Checkbox
@ -196,8 +201,9 @@ class Views extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { settings } = state; const { settings } = state;
const { availability = 'unknown' } = state.nodeStatus.nodeKind || {};
return { settings }; return { settings, availability };
} }
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {

3
js/wasm/README.md Normal file
View File

@ -0,0 +1,3 @@
# WASM modules
- `ethkey` -> `/js/src/api/local/ethkey/ethkey.wasm`

2
js/wasm/ethkey/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target
Cargo.lock

17
js/wasm/ethkey/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
description = "Parity ethkey WASM module."
name = "parity-ethkey-wasm"
version = "1.7.0"
license = "GPL-3.0"
authors = ["Parity Technologies <admin@parity.io>"]
[workspace]
members = []
[dependencies]
tiny-keccak = "1.0"
tiny-secp256k1 = "0.1"
libc = { version = "0.2.14", default-features = false }
[profile.release]
panic = "abort"

View File

@ -0,0 +1,5 @@
const fs = require('fs');
const file = fs.readFileSync('./ethkey.opt.wasm', { encoding: 'base64' });
fs.writeFileSync('../../src/api/local/ethkey/ethkey.wasm.js', `module.exports = new Buffer('${file}', 'base64');\n`);

16
js/wasm/ethkey/build.sh Executable file
View File

@ -0,0 +1,16 @@
# Remove previous build to avoid name conflicts
rm -rf target/wasm32-unknown-emscripten/*
# Build using nightly rustc + emscripten
rustup run nightly cargo build --release --target=wasm32-unknown-emscripten
# Copy final WASM file over
cp ./target/wasm32-unknown-emscripten/release/deps/parity_ethkey_wasm-*.wasm ./ethkey.wasm
# Create a Base64-encoded JS version of the wasm file for easy inclusion in Webpack
node base64ify
# Copy Base64-encoded JS version to src
cp ./ethkey.wasm.js ../../src/api/local/ethkey/ethkey.wasm.js
# rm -f ./ethkey.wasm ./ethkey.opt.wasm ./ethkey.wasm.js

Binary file not shown.

BIN
js/wasm/ethkey/ethkey.wasm Normal file

Binary file not shown.

153
js/wasm/ethkey/src/main.rs Normal file
View File

@ -0,0 +1,153 @@
// 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 <http://www.gnu.org/licenses/>.
#![feature(lang_items, core_intrinsics)]
#![feature(start)]
#![feature(link_args)]
#![no_std]
use core::intrinsics;
// Pull in the system libc library for what crt0.o likely requires.
extern crate libc;
extern crate tiny_keccak;
extern crate tiny_secp256k1;
use tiny_secp256k1::{is_valid_secret, create_public_key, ECPointG};
// #[link_args = "-s EXPORTED_FUNCTIONS=['_input_ptr','_secret_ptr','_public_ptr','_address_ptr','_ecpointg','_verify_secret','_brain']"]
// extern {}
use tiny_keccak::Keccak;
pub trait Keccak256<T: Sized> {
fn keccak256(&self) -> T;
}
impl Keccak256<[u8; 32]> for [u8] {
#[inline]
fn keccak256(&self) -> [u8; 32] {
let mut keccak = Keccak::new_keccak256();
let mut result = [0u8; 32];
keccak.update(self);
keccak.finalize(&mut result);
result
}
}
static mut INPUT: [u8; 1024] = [0; 1024];
static mut SECRET: [u8; 32] = [0; 32];
static mut PUBLIC: [u8; 64] = [0; 64];
static mut ADDRESS: [u8; 20] = [0; 20];
static mut G: Option<ECPointG> = None;
#[no_mangle]
pub extern "C" fn ecpointg() -> &'static ECPointG {
let g = unsafe { &G };
if let Some(ref g) = *g {
return g;
}
unsafe { G = Some(ECPointG::new()) };
g.as_ref().expect("value set above; qed")
}
#[no_mangle]
pub extern "C" fn input_ptr() -> *const u8 {
unsafe { INPUT.as_ptr() }
}
#[no_mangle]
pub extern "C" fn secret_ptr() -> *const u8 {
unsafe { SECRET.as_ptr() }
}
#[no_mangle]
pub extern "C" fn public_ptr() -> *const u8 {
unsafe { PUBLIC.as_ptr() }
}
#[no_mangle]
pub extern "C" fn address_ptr() -> *const u8 {
unsafe { ADDRESS.as_ptr() }
}
#[no_mangle]
pub extern "C" fn verify_secret() -> bool {
is_valid_secret(unsafe { &SECRET })
}
#[no_mangle]
pub extern "C" fn brain(input_len: usize) {
let data = unsafe { &INPUT[..input_len] };
let mut secret_out = unsafe { &mut SECRET };
let mut public_out = unsafe { &mut PUBLIC };
let mut address_out = unsafe { &mut ADDRESS };
let g = ecpointg();
let mut secret = data.keccak256();
let mut i = 0;
loop {
secret = secret.keccak256();
match i > 16384 {
false => i += 1,
true => {
if let Some(public) = create_public_key(g, &secret) {
let public = &public[1..];
let hash = public.keccak256();
address_out.copy_from_slice(&hash[12..]);
if address_out[0] == 0 {
public_out.copy_from_slice(&public);
secret_out.copy_from_slice(&secret);
return;
}
}
}
}
}
}
// Entry point for this program.
#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
0
}
// These functions are used by the compiler, but not
// for a bare-bones hello world. These are normally
// provided by libstd.
#[lang = "eh_personality"]
#[no_mangle]
pub extern fn rust_eh_personality() {
}
// This function may be needed based on the compilation target.
#[lang = "eh_unwind_resume"]
#[no_mangle]
pub extern fn rust_eh_unwind_resume() {
}
#[lang = "panic_fmt"]
#[no_mangle]
pub extern fn rust_begin_panic(_msg: core::fmt::Arguments,
_file: &'static str,
_line: u32) -> ! {
unsafe { intrinsics::abort() }
}

View File

@ -139,7 +139,6 @@ module.exports = {
resolve: { resolve: {
alias: { 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') 'keythereum': path.resolve(__dirname, '../node_modules/keythereum/dist/keythereum')
}, },
modules: [ modules: [

View File

@ -42,7 +42,6 @@ module.exports = {
resolve: { resolve: {
alias: { 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') 'keythereum': path.resolve(__dirname, '../node_modules/keythereum/dist/keythereum')
} }
}, },

View File

@ -840,6 +840,7 @@ impl Configuration {
hosts: self.ws_hosts(), hosts: self.ws_hosts(),
origins: self.ws_origins(), origins: self.ws_origins(),
signer_path: self.directories().signer.into(), signer_path: self.directories().signer.into(),
support_token_api: !self.args.flag_public_node,
ui_address: ui.address(), ui_address: ui.address(),
}; };

View File

@ -141,6 +141,7 @@ pub struct WsConfiguration {
pub origins: Option<Vec<String>>, pub origins: Option<Vec<String>>,
pub hosts: Option<Vec<String>>, pub hosts: Option<Vec<String>>,
pub signer_path: PathBuf, pub signer_path: PathBuf,
pub support_token_api: bool,
pub ui_address: Option<(String, u16)>, pub ui_address: Option<(String, u16)>,
} }
@ -155,6 +156,7 @@ impl Default for WsConfiguration {
origins: Some(vec!["chrome-extension://*".into()]), origins: Some(vec!["chrome-extension://*".into()]),
hosts: Some(Vec::new()), hosts: Some(Vec::new()),
signer_path: replace_home(&data_dir, "$BASE/signer").into(), signer_path: replace_home(&data_dir, "$BASE/signer").into(),
support_token_api: true,
ui_address: Some(("127.0.0.1".to_owned(), 8180)), ui_address: Some(("127.0.0.1".to_owned(), 8180)),
} }
} }
@ -207,9 +209,14 @@ pub fn new_ws<D: rpc_apis::Dependencies>(
let allowed_origins = into_domains(with_domain(conf.origins, domain, &[ui_address])); let allowed_origins = into_domains(with_domain(conf.origins, domain, &[ui_address]));
let allowed_hosts = into_domains(with_domain(conf.hosts, domain, &[Some(ws_address)])); let allowed_hosts = into_domains(with_domain(conf.hosts, domain, &[Some(ws_address)]));
let signer_path = conf.signer_path; let signer_path;
let signer_path = conf.ui_address.map(move |_| ::signer::codes_path(&signer_path)); let path = match conf.support_token_api && conf.ui_address.is_some() {
let path = signer_path.as_ref().map(|p| p.as_path()); true => {
signer_path = ::signer::codes_path(&conf.signer_path);
Some(signer_path.as_path())
},
false => None
};
let start_result = rpc::start_ws( let start_result = rpc::start_ws(
&addr, &addr,
handler, handler,

View File

@ -307,6 +307,11 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
} }
fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error> { fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error> {
// Return nothing if accounts are disabled (running as public node)
if self.accounts.is_none() {
return Ok(BTreeMap::new());
}
let transactions = self.miner.local_transactions(); let transactions = self.miner.local_transactions();
let block_number = self.client.chain_info().best_block_number; let block_number = self.client.chain_info().best_block_number;
Ok(transactions Ok(transactions