Signer-plugin-based approach (plugins available)

This commit is contained in:
Jaco Greeff 2017-09-29 15:04:57 +02:00
parent 9daa884699
commit 39b5e5b98a
18 changed files with 843 additions and 395 deletions

703
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -147,6 +147,10 @@
"@parity/shapeshift": "^2", "@parity/shapeshift": "^2",
"@parity/shared": "^2", "@parity/shared": "^2",
"@parity/ui": "^2", "@parity/ui": "^2",
"@parity/plugin-signer-account": "paritytech/plugin-signer-account",
"@parity/plugin-signer-default": "paritytech/plugin-signer-default",
"@parity/plugin-signer-hardware": "paritytech/plugin-signer-hardware",
"@parity/plugin-signer-qr": "paritytech/plugin-signer-qr",
"@parity/dapp-account": "paritytech/dapp-account", "@parity/dapp-account": "paritytech/dapp-account",
"@parity/dapp-accounts": "paritytech/dapp-accounts", "@parity/dapp-accounts": "paritytech/dapp-accounts",
"@parity/dapp-address": "paritytech/dapp-address", "@parity/dapp-address": "paritytech/dapp-address",

View File

@ -170,6 +170,8 @@ export default class Store {
} }
this.middleware.push(middleware); this.middleware.push(middleware);
return true;
} }
hasValidToken = (method, appId, token) => { hasValidToken = (method, appId, token) => {

View File

@ -19,21 +19,23 @@ import SignerPluginStore from '../Signer/pluginStore';
import StatusPluginStore from '../Status/pluginStore'; import StatusPluginStore from '../Status/pluginStore';
function injectInterceptorPlugin (middleware) { function injectInterceptorPlugin (middleware) {
InterceptorStore.get().addMiddleware(middleware); return InterceptorStore.get().addMiddleware(middleware);
return true;
} }
function injectSignerPlugin (component) { function injectSignerPlugin (component, isHandler) {
SignerPluginStore.get().addComponent(component); let isDefault;
return true; try {
isDefault = isHandler(null, null, null) || false;
} catch (error) {
isDefault = false;
}
return SignerPluginStore.get().addComponent(component, isHandler, isDefault);
} }
function injectStatusPlugin (component) { function injectStatusPlugin (component) {
StatusPluginStore.get().addComponent(component); return StatusPluginStore.get().addComponent(component);
return true;
} }
export function extendShell (options) { export function extendShell (options) {
@ -42,7 +44,7 @@ export function extendShell (options) {
return injectInterceptorPlugin(options.middleware); return injectInterceptorPlugin(options.middleware);
case 'signer': case 'signer':
return injectSignerPlugin(options.component); return injectSignerPlugin(options.component, options.isHandler);
case 'status': case 'status':
return injectStatusPlugin(options.component); return injectStatusPlugin(options.component);

View File

@ -21,16 +21,6 @@
padding: 1em 0; padding: 1em 0;
} }
.none {
color: #aaa;
}
.request {
&:nth-child(even) {
background: rgba(255, 255, 255, 0.04);
}
}
.signer { .signer {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;

View File

@ -14,23 +14,24 @@
// 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 BigNumber from 'bignumber.js';
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as RequestsActions from '@parity/shared/redux/providers/signerActions'; import * as RequestsActions from '@parity/shared/redux/providers/signerActions';
import Container from '@parity/ui/Container'; import Container from '@parity/ui/Container';
import RequestPending from '@parity/ui/Signer/RequestPending';
import Store from '@parity/shared/mobx/signerStore';
import PluginStore from '../pluginStore'; import PendingList from '../PendingList';
import PendingStore from '../pendingStore';
import styles from './embedded.css'; import styles from './embedded.css';
const CONTAINER_STYLE = {
background: 'transparent'
};
@observer @observer
class Embedded extends Component { class Embedded extends Component {
static contextTypes = { static contextTypes = {
@ -43,113 +44,41 @@ class Embedded extends Component {
startConfirmRequest: PropTypes.func.isRequired, startConfirmRequest: PropTypes.func.isRequired,
startRejectRequest: PropTypes.func.isRequired startRejectRequest: PropTypes.func.isRequired
}).isRequired, }).isRequired,
externalLink: PropTypes.string,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired, netVersion: PropTypes.string.isRequired
signer: PropTypes.shape({
finished: PropTypes.array.isRequired,
pending: PropTypes.array.isRequired
}).isRequired
}; };
store = new Store(this.context.api, false, this.props.externalLink); pendingStore = PendingStore.get(this.context.api);
pluginStore = PluginStore.get();
render () { render () {
return ( const { accounts, actions, gasLimit, netVersion } = this.props;
<Container style={ { background: 'transparent' } }>
<div className={ styles.signer }>
{ this.renderPendingRequests() }
</div>
</Container>
);
}
renderPendingRequests () {
const { signer } = this.props;
const { pending } = signer;
if (!pending.length) {
return (
<div className={ styles.none }>
<FormattedMessage
id='signer.embedded.noPending'
defaultMessage='There are currently no pending requests awaiting your confirmation'
/>
</div>
);
}
return ( return (
<div> <Container style={ CONTAINER_STYLE }>
{ <PendingList
pending accounts={ accounts }
.sort(this._sortRequests) className={ styles.signer }
.map(this.renderPending)
}
</div>
);
}
findPluginHandler (data) {
const { accounts } = this.props;
const { payload } = data;
let account;
if (payload.decrypt) {
account = accounts[payload.decrypt.address];
} else if (payload.sign) {
account = accounts[payload.sign.address];
} else if (payload.sendTransaction) {
account = accounts[payload.sendTransaction.from];
} else if (payload.signTransaction) {
account = accounts[payload.signTransaction.from];
}
return this.pluginStore.findHandler(payload, account);
}
renderPending = (data, index) => {
const { actions, gasLimit, netVersion } = this.props;
const { date, id, isSending, payload, origin } = data;
return (
<RequestPending
className={ styles.request }
date={ date }
elementRequest={ this.findPluginHandler(data) }
focus={ index === 0 }
gasLimit={ gasLimit } gasLimit={ gasLimit }
id={ id }
isSending={ isSending }
netVersion={ netVersion } netVersion={ netVersion }
key={ id }
onConfirm={ actions.startConfirmRequest } onConfirm={ actions.startConfirmRequest }
onReject={ actions.startRejectRequest } onReject={ actions.startRejectRequest }
origin={ origin } pendingItems={ this.pendingStore.pending }
payload={ payload }
signerStore={ this.store }
/> />
</Container>
); );
} }
_sortRequests = (a, b) => {
return new BigNumber(a.id).cmp(b.id);
}
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { gasLimit, netVersion } = state.nodeStatus; const { gasLimit, netVersion } = state.nodeStatus;
const { accounts } = state.personal; const { accounts } = state.personal;
const { actions, signer } = state; const { actions } = state;
return { return {
accounts, accounts,
actions, actions,
gasLimit, gasLimit,
netVersion, netVersion
signer
}; };
} }

View File

@ -0,0 +1,15 @@
// 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/>.

View File

@ -0,0 +1,18 @@
/* 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/>.
*/
export default from './pendingItem';

View File

@ -0,0 +1,22 @@
/* 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/>.
*/
.request {
&:nth-child(even) {
background: rgba(0, 0, 0, 0.04);
}
}

View File

@ -0,0 +1,82 @@
// 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/>.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import { observer } from 'mobx-react';
import SignerLayout from '@parity/ui/Signer/Layout';
import PluginStore from '../pluginStore';
import styles from './pendingItem.css';
const pluginStore = PluginStore.get();
const DEFAULT_ORIGIN = {
type: 'unknown',
details: ''
};
function PendingItem ({ accounts, className, data: { date, id, isSending, payload, origin }, gasLimit, isFocussed, netVersion, onConfirm, onReject }) {
const Handler = pluginStore.findHandler(payload, accounts);
if (!Handler) {
console.error('No transaction handler found for', payload);
return (
<SignerLayout className={ `${styles.error} ${className}` }>
<FormattedMessage
id='shell.signer.error.noHandler'
defaultMessage='Unable to find a Signer handler for the specific transaction, no fallback available.'
/>
</SignerLayout>
);
}
const _onConfirm = (data) => onConfirm(Object.assign({ id, payload }, data));
const _onReject = () => onReject(id);
return (
<Handler
accounts={ accounts }
className={ `${styles.request} ${className}` }
date={ date }
gasLimit={ gasLimit }
id={ id }
isFocussed={ isFocussed || false }
isSending={ isSending || false }
netVersion={ netVersion }
onConfirm={ _onConfirm }
onReject={ _onReject }
origin={ origin || DEFAULT_ORIGIN }
payload={ payload }
/>
);
}
PendingItem.propTypes = {
accounts: PropTypes.object.isRequired,
className: PropTypes.string,
data: PropTypes.object.isRequired,
gasLimit: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired,
isFocussed: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired
};
export default observer(PendingItem);

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './pendingList';

View File

@ -0,0 +1,24 @@
/* 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/>.
*/
.list {
color: inherit;
}
.none {
color: #aaa;
}

View File

@ -0,0 +1,68 @@
// 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/>.
import BigNumber from 'bignumber.js';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import PendingItem from '../PendingItem';
import styles from './pendingList.css';
export default function PendingList ({ accounts, className, gasLimit, netVersion, onConfirm, onReject, pendingItems }) {
if (!pendingItems.length) {
return (
<div className={ `${styles.none} ${className}` }>
<FormattedMessage
id='signer.embedded.noPending'
defaultMessage='There are currently no pending requests awaiting your confirmation'
/>
</div>
);
}
return (
<div className={ `${styles.list} ${className}` }>
{
pendingItems
.sort((a, b) => new BigNumber(a.id).cmp(b.id))
.map((data, index) => (
<PendingItem
accounts={ accounts }
data={ data }
gasLimit={ gasLimit }
isFocussed={ index === 0 }
key={ data.id }
netVersion={ netVersion }
onConfirm={ onConfirm }
onReject={ onReject }
/>
))
}
</div>
);
}
PendingList.propTypes = {
accounts: PropTypes.object.isRequired,
className: PropTypes.string,
gasLimit: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
pendingItems: PropTypes.object.isRequired
};

View File

@ -0,0 +1,59 @@
// 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/>.
import { action, observable } from 'mobx';
let instance = null;
export default class PendingStore {
@observable pending = [];
constructor (api) {
this._api = api;
api.on('connected', this.subscribePending);
if (api.isConnected) {
this.subscribePending();
}
}
@action confirmRequest = (id, payload) => {
}
@action rejectRequest = (id) => {
}
@action setPending = (pending = []) => {
this.pending = pending;
}
subscribePending = () => {
this._api.subscribe('signer_requestsToConfirm', (error, pending) => {
if (!error) {
this.setPending(pending);
}
});
}
static get (api) {
if (!instance) {
instance = new PendingStore(api);
}
return instance;
}
}

View File

@ -19,20 +19,69 @@ import { action, observable } from 'mobx';
let instance = null; let instance = null;
export default class PluginStore { export default class PluginStore {
@observable components = []; @observable plugins = [];
@action addComponent (Component) { @action addComponent (Component, isHandler, isFallback) {
if (!Component || (typeof Component.isHandler !== 'function')) { if (!Component || (typeof isHandler !== 'function')) {
throw new Error(`Unable to attach Signer component, 'isHandler' function is not defined`); throw new Error(`Unable to attach Signer plugin, 'React Component' or 'isHandler' function is not defined`);
} }
this.components.push(Component); this.plugins.push({
} Component,
isHandler,
findHandler (payload, account) { isFallback
return this.components.find((component) => {
return component.isHandler(payload, account);
}); });
return true;
}
findPayloadAccount (payload, accounts) {
if (payload.decrypt) {
return accounts[payload.decrypt.address];
} else if (payload.sign) {
return accounts[payload.sign.address];
} else if (payload.sendTransaction) {
return accounts[payload.sendTransaction.from];
} else if (payload.signTransaction) {
return accounts[payload.signTransaction.from];
}
return null;
}
findFallback (payload, accounts, account) {
const plugin = this.plugins.find((p) => {
try {
return !!(
p.isFallback &&
p.isHandler(payload, accounts, account)
);
} catch (error) {
return false;
}
});
return plugin
? plugin.Component
: null;
}
findHandler (payload, accounts) {
const account = this.findPayloadAccount(payload, accounts);
const plugin = this.plugins.find((p) => {
try {
return !!(
!p.isFallback &&
p.isHandler(payload, accounts, account)
);
} catch (error) {
return false;
}
});
return plugin
? plugin.Component
: this.findFallback(payload, accounts, account);
} }
static get () { static get () {

View File

@ -27,6 +27,8 @@ export default class PluginStore {
} }
this.components.push(Component); this.components.push(Component);
return true;
} }
static get () { static get () {

View File

@ -43,7 +43,7 @@ function Status ({ className = '', upgradeStore }, { api }) {
const accountStore = AccountStore.get(api); const accountStore = AccountStore.get(api);
return ( return (
<GradientBg className={ [styles.status, className].join(' ') }> <GradientBg className={ `${styles.status} ${className}` }>
<ClientVersion className={ styles.version } /> <ClientVersion className={ styles.version } />
<div className={ styles.upgrade }> <div className={ styles.upgrade }>
<Consensus upgradeStore={ upgradeStore } /> <Consensus upgradeStore={ upgradeStore } />

View File

@ -81,5 +81,9 @@ ReactDOM.render(
// testing, priceTicker gist // testing, priceTicker gist
injectExternalScript('https://cdn.rawgit.com/jacogr/396fc583e81b9404e21195a48dc862ca/raw/33e5058a4c0028cf9acf4b0662d75298e41ca6fa/priceTicker.js'); injectExternalScript('https://cdn.rawgit.com/jacogr/396fc583e81b9404e21195a48dc862ca/raw/33e5058a4c0028cf9acf4b0662d75298e41ca6fa/priceTicker.js');
// testing, signer plugin
// injectExternalScript('https://rawgit.com/paritytech/plugin-sign-qr/master/dist.js'); // testing, signer plugins
import '@parity/plugin-signer-account';
import '@parity/plugin-signer-default';
import '@parity/plugin-signer-hardware';
import '@parity/plugin-signer-qr';