Ui 2 shell (#5510)

* Split application into ~/shell

* reset.css back to index
This commit is contained in:
Jaco Greeff
2017-04-26 10:56:31 +02:00
committed by GitHub
parent cdab1ebc04
commit 2f0ce06cc1
111 changed files with 211 additions and 168 deletions

View File

@@ -0,0 +1,48 @@
// 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, { Component, PropTypes } from 'react';
import { FirstRun, UpgradeParity } from '~/modals';
import { Errors, Tooltips } from '~/ui';
import styles from '../application.css';
export default class Container extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
onCloseFirstRun: PropTypes.func,
showFirstRun: PropTypes.bool,
upgradeStore: PropTypes.object.isRequired
};
render () {
const { children, onCloseFirstRun, showFirstRun, upgradeStore } = this.props;
return (
<div className={ styles.container }>
<FirstRun
onClose={ onCloseFirstRun }
visible={ showFirstRun }
/>
<Tooltips />
<UpgradeParity upgradeStore={ upgradeStore } />
<Errors />
{ children }
</div>
);
}
}

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 './container';

View File

@@ -0,0 +1,38 @@
// 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, { Component, PropTypes } from 'react';
import { Errors } from '~/ui';
import styles from '../application.css';
export default class DappContainer extends Component {
static propTypes = {
children: PropTypes.node.isRequired
};
render () {
const { children } = this.props;
return (
<div className={ styles.container }>
<Errors />
{ children }
</div>
);
}
}

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 './dappContainer';

View File

@@ -0,0 +1,52 @@
/* 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/>.
*/
.body {
background: #f80;
color: white;
opacity: 1;
max-width: 500px;
padding: 1em 4em 1em 2em;
position: fixed;
right: 1.5em;
top: 1.5em;
z-index: 1000;
.button {
background: rgba(0, 0, 0, 0.5);
color: white !important;
svg {
fill: white !important;
}
}
.buttonrow {
text-align: right;
}
p {
color: white;
}
.close {
cursor: pointer;
position: absolute;
right: 1em;
top: 1em;
}
}

View File

@@ -0,0 +1,74 @@
// 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 { observer } from 'mobx-react';
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button } from '~/ui';
import { CloseIcon, CheckIcon } from '~/ui/Icons';
import Store from './store';
import styles from './extension.css';
@observer
export default class Extension extends Component {
store = Store.get();
render () {
const { showWarning } = this.store;
if (!showWarning) {
return null;
}
return (
<div className={ styles.body }>
<CloseIcon
className={ styles.close }
onClick={ this.onClose }
/>
<p>
<FormattedMessage
id='extension.intro'
defaultMessage='Parity now has an extension available for Chrome that allows safe browsing of Ethereum-enabled distributed applications. It is highly recommended that you install this extension to further enhance your Parity experience.'
/>
</p>
<p className={ styles.buttonrow }>
<Button
className={ styles.button }
icon={ <CheckIcon /> }
label={
<FormattedMessage
id='extension.install'
defaultMessage='Install the extension now'
/>
}
onClick={ this.onInstallClick }
/>
</p>
</div>
);
}
onClose = () => {
this.store.snoozeWarning();
}
onInstallClick = () => {
this.store.installExtension();
}
}

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 './extension';

View File

@@ -0,0 +1,116 @@
// 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 chrome */
import { action, computed, observable } from 'mobx';
import store from 'store';
import browser from 'useragent.js/lib/browser';
import { DOMAIN } from '~/util/constants';
const A_DAY = 24 * 60 * 60 * 1000;
const NEXT_DISPLAY = '_parity::extensionWarning::nextDisplay';
// 'https://chrome.google.com/webstore/detail/parity-ethereum-integrati/himekenlppkgeaoeddcliojfddemadig';
const EXTENSION_PAGE = 'https://chrome.google.com/webstore/detail/himekenlppkgeaoeddcliojfddemadig';
let instance;
export default class Store {
@observable hasExtension = false;
@observable isInstalling = false;
@observable nextDisplay = 0;
@observable shouldInstall = false;
constructor () {
this.nextDisplay = store.get(NEXT_DISPLAY) || 0;
this.testInstall();
}
@computed get showWarning () {
return !this.isInstalling && this.shouldInstall && (Date.now() > this.nextDisplay);
}
@action setExtensionActive = () => {
this.hasExtension = true;
}
@action setInstalling = (isInstalling) => {
this.isInstalling = isInstalling;
}
@action snoozeWarning = (sleep = A_DAY) => {
this.nextDisplay = Date.now() + sleep;
store.set(NEXT_DISPLAY, this.nextDisplay);
}
@action testInstall = () => {
this.shouldInstall = this.readStatus();
}
readStatus = () => {
const hasExtension = Symbol.for('parity.extension') in window;
const ua = browser.analyze(navigator.userAgent || '');
if (hasExtension) {
this.setExtensionActive();
return false;
}
return (ua || {}).name.toLowerCase() === 'chrome';
}
installExtension = () => {
this.setInstalling(true);
if (window.location.hostname.toString().endsWith(DOMAIN)) {
return this.inlineInstall()
.catch((error) => {
console.warn('Unable to perform direct install', error);
window.open(EXTENSION_PAGE, '_blank');
});
}
window.open(EXTENSION_PAGE, '_blank');
return Promise.resolve(true);
}
inlineInstall = () => {
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.setAttribute('rel', 'chrome-webstore-item');
link.setAttribute('href', EXTENSION_PAGE);
document.querySelector('head').appendChild(link);
if (chrome && chrome.webstore && chrome.webstore.install) {
chrome.webstore.install(EXTENSION_PAGE, resolve, reject);
} else {
reject(new Error('Direct installation failed.'));
}
});
}
static get () {
if (!instance) {
instance = new Store();
}
return instance;
}
}

View File

@@ -0,0 +1,21 @@
/* 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/>.
*/
.error {
padding: 2em;
background: red;
color: white;
}

View File

@@ -0,0 +1,33 @@
// 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, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import styles from './frameError.css';
export default class FrameError extends Component {
render () {
return (
<div className={ styles.error }>
<FormattedMessage
id='application.frame.error'
defaultMessage='ERROR: This application cannot and should not be loaded in an embedded iFrame'
/>
</div>
);
}
}

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 './frameError';

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 './requests';

View File

@@ -0,0 +1,126 @@
/* 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/>.
*/
$baseColor: 18;
$baseOpacity: 0.95;
$borderColor: rgba($baseColor, $baseColor, $baseColor, 0.25);
.requests {
align-items: flex-end;
bottom: 2em;
display: flex;
flex-direction: column;
position: fixed;
right: 0.175em;
width: 24em;
z-index: 750;
* {
font-size: 0.85rem !important;
}
}
.request {
animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
background-color: rgba($baseColor, $baseColor, $baseColor, $baseOpacity);
border: 4px solid $borderColor;
cursor: pointer;
margin-top: 0.25em;
opacity: 1;
width: 100%;
&.hide {
animation-duration: 0.5s;
animation-name: fadeout;
}
.status {
padding: 0.5em;
&.error {
background-color: rgba(200, 40, 40, 0.95);
color: white;
}
}
.container {
display: flex;
flex-direction: row;
padding: 1em 1em 0.5em 1em;
}
&:hover .container {
background-color: rgba($baseColor, $baseColor, $baseColor, 1);
}
p {
margin: 0;
}
}
@keyframes fadeout {
from {
display: block;
height: inherit;
opacity: 1;
}
49% {
display: block;
height: inherit;
opacity: 0;
}
98% {
display: block;
height: 0;
opacity: 0;
}
to {
display: none;
height: 0;
opacity: 0;
}
}
.identity {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 1em;
.icon {
margin-bottom: 0.5rem;
}
}
.inline {
align-items: center;
display: flex;
flex-direction: row;
.fill {
flex: 1 0 auto;
}
}
.hash {
margin-left: 0.25em;
}

View File

@@ -0,0 +1,243 @@
// 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 { LinearProgress } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hideRequest } from '~/redux/providers/requestsActions';
import { MethodDecoding, IdentityIcon, ScrollableText, ShortenedHash } from '~/ui';
import styles from './requests.css';
const ERROR_STATE = 'ERROR_STATE';
const DONE_STATE = 'DONE_STATE';
const WAITING_STATE = 'WAITING_STATE';
class Requests extends Component {
static propTypes = {
requests: PropTypes.object.isRequired,
onHideRequest: PropTypes.func.isRequired
};
state = {
extras: {}
};
render () {
const { requests } = this.props;
const { extras } = this.state;
return (
<div className={ styles.requests }>
{
Object
.values(requests)
.map((request) => this.renderRequest(request, extras[request.requestId]))
}
</div>
);
}
renderRequest (request, extras = {}) {
const { show, transaction } = request;
const state = this.getTransactionState(request);
const displayedTransaction = { ...transaction };
// Don't show gas and gasPrice
delete displayedTransaction.gas;
delete displayedTransaction.gasPrice;
const requestClasses = [ styles.request ];
const statusClasses = [ styles.status ];
const requestStyle = {};
const handleHideRequest = () => {
this.handleHideRequest(request.requestId);
};
if (state.type === ERROR_STATE) {
statusClasses.push(styles.error);
}
if (!show) {
requestClasses.push(styles.hide);
}
// Set the Request height (for animation) if found
if (extras.height) {
requestStyle.height = extras.height;
}
return (
<div
className={ requestClasses.join(' ') }
key={ request.requestId }
ref={ `request_${request.requestId}` }
onClick={ handleHideRequest }
style={ requestStyle }
>
<div className={ statusClasses.join(' ') }>
{ this.renderStatus(request) }
</div>
{
state.type === ERROR_STATE
? null
: (
<LinearProgress
max={ 6 }
mode={ state.type === WAITING_STATE ? 'indeterminate' : 'determinate' }
value={ state.type === DONE_STATE ? request.blockHeight.toNumber() : 6 }
/>
)
}
<div className={ styles.container }>
<div
className={ styles.identity }
title={ transaction.from }
>
<IdentityIcon
address={ transaction.from }
inline
center
className={ styles.icon }
/>
</div>
<MethodDecoding
address={ transaction.from }
compact
historic={ state.type === DONE_STATE }
transaction={ displayedTransaction }
/>
</div>
</div>
);
}
renderStatus (request) {
const { error, transactionHash, transactionReceipt } = request;
if (error) {
return (
<div
className={ styles.inline }
title={ error.message }
>
<FormattedMessage
id='requests.status.error'
defaultMessage='An error occured:'
/>
<div className={ styles.fill }>
<ScrollableText
text={ error.text || error.message || error.toString() }
/>
</div>
</div>
);
}
if (transactionReceipt) {
return (
<FormattedMessage
id='requests.status.transactionMined'
defaultMessage='Transaction mined at block #{blockNumber} ({blockHeight} blocks ago)'
values={ {
blockHeight: request.blockHeight.toNumber(),
blockNumber: transactionReceipt.blockNumber.toFormat()
} }
/>
);
}
if (transactionHash) {
return (
<div className={ styles.inline }>
<FormattedMessage
id='requests.status.transactionSent'
defaultMessage='Transaction sent to network with hash'
/>
<div className={ [ styles.fill, styles.hash ].join(' ') }>
<ShortenedHash data={ transactionHash } />
</div>
</div>
);
}
return (
<FormattedMessage
id='requests.status.waitingForSigner'
defaultMessage='Waiting for authorization in the Parity Signer'
/>
);
}
getTransactionState (request) {
const { error, transactionReceipt } = request;
if (error) {
return { type: ERROR_STATE };
}
if (transactionReceipt) {
return { type: DONE_STATE };
}
return { type: WAITING_STATE };
}
handleHideRequest = (requestId) => {
const requestElement = ReactDOM.findDOMNode(this.refs[`request_${requestId}`]);
// Try to get the request element height, to have a nice transition effect
if (requestElement) {
const { height } = requestElement.getBoundingClientRect();
const prevExtras = this.state.extras;
const nextExtras = {
...prevExtras,
[ requestId ]: {
...prevExtras[requestId],
height
}
};
return this.setState({ extras: nextExtras }, () => {
return this.props.onHideRequest(requestId);
});
}
return this.props.onHideRequest(requestId);
}
}
const mapStateToProps = (state) => {
const { requests } = state;
return { requests };
};
function mapDispatchToProps (dispatch) {
return bindActionCreators({
onHideRequest: hideRequest
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Requests);

View File

@@ -0,0 +1,118 @@
// 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 store from 'store';
import { ERROR_CODES } from '@parity/api/transport/error';
export const LS_REQUESTS_KEY = '_parity::requests';
export default class SavedRequests {
network = null;
/**
* Load the network version, and then the related requests
*/
load (api) {
return api.net.version()
.then((network) => {
this.network = network;
return this.loadRequests(api);
})
.catch((error) => {
console.error(error);
return [];
});
}
/**
* Load the requests of the current network
*/
loadRequests (api) {
const requests = this._get();
const promises = Object.values(requests).map((request) => {
const { requestId, transactionHash } = request;
// The request hasn't been signed yet
if (transactionHash) {
return request;
}
return this._requestExists(api, requestId)
.then((exists) => {
if (!exists) {
return null;
}
return request;
})
.catch(() => {
this.remove(requestId);
});
});
return Promise.all(promises).then((requests) => requests.filter((request) => request));
}
save (requestId, requestData) {
const requests = this._get();
requests[requestId] = {
...(requests[requestId] || {}),
...requestData
};
this._set(requests);
}
remove (requestId) {
const requests = this._get();
delete requests[requestId];
this._set(requests);
}
_get () {
const allRequests = store.get(LS_REQUESTS_KEY) || {};
return allRequests[this.network] || {};
}
_set (requests = {}) {
const allRequests = store.get(LS_REQUESTS_KEY) || {};
if (Object.keys(requests).length > 0) {
allRequests[this.network] = requests;
} else {
delete allRequests[this.network];
}
return store.set(LS_REQUESTS_KEY, allRequests);
}
_requestExists (api, requestId) {
return api.parity
.checkRequest(requestId)
.then(() => true)
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_NOT_FOUND) {
return false;
}
throw error;
});
}
}

View File

@@ -0,0 +1,105 @@
// 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 sinon from 'sinon';
import store from 'store';
import SavedRequests, { LS_REQUESTS_KEY } from './savedRequests';
const NETWORK_ID = 42;
const DEFAULT_REQUEST = {
requestId: '0x1',
transaction: {}
};
const api = createApi(NETWORK_ID);
const api2 = createApi(1);
const savedRequests = new SavedRequests();
function createApi (networkVersion) {
return {
parity: {
checkRequest: sinon.stub().resolves()
},
net: {
version: sinon.stub().resolves(networkVersion)
}
};
}
describe('views/Application/Requests/savedRequests', () => {
beforeEach((done) => {
store.set(LS_REQUESTS_KEY, {
[NETWORK_ID]: {
[DEFAULT_REQUEST.requestId]: DEFAULT_REQUEST
}
});
savedRequests.load(api)
.then(() => done());
});
afterEach(() => {
store.set(LS_REQUESTS_KEY, {});
});
it('gets requests from local storage', () => {
const requests = savedRequests._get();
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal(DEFAULT_REQUEST);
});
it('sets requests to local storage', () => {
savedRequests._set({});
const requests = savedRequests._get();
expect(requests).to.deep.equal({});
});
it('removes requests', () => {
savedRequests.remove(DEFAULT_REQUEST.requestId);
const requests = savedRequests._get();
expect(requests).to.deep.equal({});
});
it('saves new requests', () => {
savedRequests.save(DEFAULT_REQUEST.requestId, { extraData: true });
const requests = savedRequests._get();
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal({
...DEFAULT_REQUEST,
extraData: true
});
});
it('loads requests', () => {
return savedRequests.load(api)
.then((requests) => {
expect(requests[0]).to.deep.equal(DEFAULT_REQUEST);
});
});
it('loads requests from the right network', () => {
return savedRequests.load(api2)
.then((requests) => {
expect(requests).to.deep.equal([]);
});
});
});

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 './snackbar';

View File

@@ -0,0 +1,76 @@
// 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, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Snackbar as SnackbarMUI } from 'material-ui';
import { darkBlack, grey800 } from 'material-ui/styles/colors';
import { closeSnackbar } from '~/redux/providers/snackbarActions';
const bodyStyle = {
backgroundColor: darkBlack,
borderStyle: 'solid',
borderColor: grey800,
borderWidth: '1px 1px 0 1px'
};
class Snackbar extends Component {
static propTypes = {
closeSnackbar: PropTypes.func.isRequired,
open: PropTypes.bool,
cooldown: PropTypes.number,
message: PropTypes.any
};
render () {
const { open, message, cooldown } = this.props;
return (
<SnackbarMUI
open={ open }
message={ message }
autoHideDuration={ cooldown }
bodyStyle={ bodyStyle }
onRequestClose={ this.handleClose }
/>
);
}
handleClose = () => {
this.props.closeSnackbar();
}
}
function mapStateToProps (state) {
const { open, message, cooldown } = state.snackbar;
return { open, message, cooldown };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
closeSnackbar
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Snackbar);

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 './status';

View File

@@ -0,0 +1,65 @@
/* 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/>.
*/
.status {
align-items: center;
background-color: rgba(0, 0, 0, 0.8);
bottom: 0;
color: #ccc;
display: flex;
font-size: 0.75em;
left: 0;
padding: .4em .5em;
position: fixed;
right: 0;
z-index: 1000;
}
.netinfo {
color: #ddd;
flex-grow: 1;
text-align: right;
vertical-align: middle;
text-align: right;
div {
display: inline-block;
margin-left: 1em;
}
}
.network {
border-radius: 0.4em;
line-height: 1.2;
padding: 0.25em 0.5em;
text-transform: uppercase;
&.live {
background: rgb(0, 136, 0);
}
&.test {
background: rgb(136, 0, 0);
}
}
.upgrade {
div {
display: inline-block;
margin-left: 1em;
}
}

View File

@@ -0,0 +1,152 @@
// 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, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { BlockStatus } from '~/ui';
import styles from './status.css';
class Status extends Component {
static propTypes = {
clientVersion: PropTypes.string,
isTest: PropTypes.bool,
netChain: PropTypes.string,
netPeers: PropTypes.object,
upgradeStore: PropTypes.object.isRequired
}
render () {
const { clientVersion, isTest, netChain, netPeers } = this.props;
return (
<div className={ styles.status }>
<div className={ styles.version }>
{ clientVersion }
</div>
<div className={ styles.upgrade }>
{ this.renderConsensus() }
{ this.renderUpgradeButton() }
</div>
<div className={ styles.netinfo }>
<BlockStatus />
<div className={ `${styles.network} ${styles[isTest ? 'test' : 'live']}` }>
{ netChain }
</div>
<div className={ styles.peers }>
{ netPeers.active.toFormat() }/{ netPeers.connected.toFormat() }/{ netPeers.max.toFormat() } peers
</div>
</div>
</div>
);
}
renderConsensus () {
const { upgradeStore } = this.props;
if (!upgradeStore || !upgradeStore.consensusCapability) {
return null;
}
if (upgradeStore.consensusCapability === 'capable') {
return (
<div>
<FormattedMessage
id='application.status.consensus.capable'
defaultMessage='Capable'
/>
</div>
);
}
if (upgradeStore.consensusCapability.capableUntil) {
return (
<div>
<FormattedMessage
id='application.status.consensus.capableUntil'
defaultMessage='Capable until #{blockNumber}'
values={ {
blockNumber: upgradeStore.consensusCapability.capableUntil
} }
/>
</div>
);
}
if (upgradeStore.consensusCapability.incapableSince) {
return (
<div>
<FormattedMessage
id='application.status.consensus.incapableSince'
defaultMessage='Incapable since #{blockNumber}'
values={ {
blockNumber: upgradeStore.consensusCapability.incapableSince
} }
/>
</div>
);
}
return (
<div>
<FormattedMessage
id='application.status.consensus.unknown'
defaultMessage='Unknown capability'
/>
</div>
);
}
renderUpgradeButton () {
const { upgradeStore } = this.props;
if (!upgradeStore.available) {
return null;
}
return (
<div>
<a
href='javascript:void(0)'
onClick={ upgradeStore.openModal }
>
<FormattedMessage
id='application.status.upgrade'
defaultMessage='Upgrade'
/>
</a>
</div>
);
}
}
function mapStateToProps (state) {
const { clientVersion, netPeers, netChain, isTest } = state.nodeStatus;
return {
clientVersion,
netPeers,
netChain,
isTest
};
}
export default connect(
mapStateToProps,
null
)(Status);

View File

@@ -0,0 +1,26 @@
/* 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/>.
*/
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content {
padding-bottom: 1.25em;
}

View File

@@ -0,0 +1,134 @@
// 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 { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import UpgradeStore from '~/modals/UpgradeParity/store';
import Connection from '../Connection';
import ParityBar from '../ParityBar';
import Snackbar from './Snackbar';
import Container from './Container';
import DappContainer from './DappContainer';
import Extension from './Extension';
import FrameError from './FrameError';
import Status from './Status';
import Store from './store';
import Requests from './Requests';
import styles from './application.css';
const inFrame = window.parent !== window && window.parent.frames.length !== 0;
@observer
class Application extends Component {
static contextTypes = {
api: PropTypes.object.isRequired,
background: PropTypes.string
}
static propTypes = {
blockNumber: PropTypes.object,
children: PropTypes.node,
pending: PropTypes.array
}
store = new Store(this.context.api);
upgradeStore = UpgradeStore.get(this.context.api);
render () {
const [root] = (window.location.hash || '').replace('#/', '').split('/');
const isMinimized = root === 'app' || root === 'web';
if (process.env.NODE_ENV !== 'production' && root === 'playground') {
return (
<div>
{ this.props.children }
</div>
);
}
if (inFrame) {
return (
<FrameError />
);
}
return (
<div>
{
isMinimized
? this.renderMinimized()
: this.renderApp()
}
<Connection />
<Requests />
<ParityBar dapp={ isMinimized } />
</div>
);
}
renderApp () {
const { blockNumber, children } = this.props;
return (
<Container
upgradeStore={ this.upgradeStore }
onCloseFirstRun={ this.store.closeFirstrun }
showFirstRun={ this.store.firstrunVisible }
>
<div className={ styles.content }>
{ children }
</div>
{
blockNumber
? <Status upgradeStore={ this.upgradeStore } />
: null
}
<Extension />
<Snackbar />
</Container>
);
}
renderMinimized () {
const { children } = this.props;
return (
<DappContainer>
{ children }
</DappContainer>
);
}
}
function mapStateToProps (state) {
const { blockNumber } = state.nodeStatus;
const { hasAccounts } = state.personal;
return {
blockNumber,
hasAccounts
};
}
export default connect(
mapStateToProps,
null
)(Application);

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 './application';

View File

@@ -0,0 +1,84 @@
// 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';
import store from 'store';
const OLD_LS_FIRST_RUN_KEY = 'showFirstRun';
const LS_FIRST_RUN_KEY = '_parity::showFirstRun';
export default class Store {
@observable firstrunVisible = false;
constructor (api) {
// Migrate the old key to the new one
this._migrateStore();
this._api = api;
// Show the first run if it hasn't been shown before
// (thus an undefined value)
this.firstrunVisible = store.get(LS_FIRST_RUN_KEY) === undefined;
this._checkAccounts();
}
@action closeFirstrun = () => {
this.toggleFirstrun(false);
}
@action toggleFirstrun = (visible = false) => {
this.firstrunVisible = visible;
// There's no need to write to storage that the
// First Run should be visible
if (!visible) {
store.set(LS_FIRST_RUN_KEY, !!visible);
}
}
/**
* Migrate the old LocalStorage ket format
* to the new one
*/
_migrateStore () {
const oldValue = store.get(OLD_LS_FIRST_RUN_KEY);
const newValue = store.get(LS_FIRST_RUN_KEY);
if (newValue === undefined && oldValue !== undefined) {
store.set(LS_FIRST_RUN_KEY, oldValue);
store.remove(OLD_LS_FIRST_RUN_KEY);
}
}
_checkAccounts () {
return Promise
.all([
this._api.parity.listVaults(),
this._api.parity.allAccountsInfo()
])
.then(([ vaults, info ]) => {
const accounts = Object.keys(info).filter((address) => info[address].uuid);
// Has accounts if any vaults or accounts
const hasAccounts = (accounts && accounts.length > 0) || (vaults && vaults.length > 0);
// Show First Run if no accounts and no vaults
this.toggleFirstrun(this.firstrunVisible || !hasAccounts);
})
.catch((error) => {
console.error('checkAccounts', error);
});
}
}

View File

@@ -0,0 +1,114 @@
/* 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/>.
*/
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.75);
z-index: 20000
}
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 20001
}
.body {
margin: 0 auto;
padding: 2em 4em;
text-align: center;
max-width: 40em;
background: rgba(25, 25, 25, 0.75);
color: rgb(208, 208, 208);
box-shadow: rgba(0, 0, 0, 0.25) 0px 14px 45px, rgba(0, 0, 0, 0.22) 0px 10px 18px
}
.header {
fontSize: 1.25em
}
.info {
margin-top: 2em;
line-height: 1.618em
}
.form {
margin-top: 0.75em;
padding: 0 4em;
text-align: left;
}
.btnrow {
text-align: right;
padding: 0 4em;
}
.icons {
}
.icon,
.iconSmall {
display: inline-block;
padding: 1em;
vertical-align: middle;
}
.iconName {
}
.icon .svg {
width: 6em !important;
height: 6em !important;
}
.iconSmall .svg {
width: 3em !important;
height: 3em !important;
}
.console {
font-family: 'Roboto Mono', monospace;
background: rgba(0, 0, 0, 0.25);
font-size: 16px;
padding: 0 0.25em;
}
@keyframes pulse {
0% {
fill: rgb(0, 200, 0);
}
50% {
fill: rgb(150, 200, 150);
}
100% {
fill: rgb(0, 200, 0);
}
}
.pulse {
fill: red;
animation-name: pulse;
animation-duration: 1.5s;
animation-iteration-count: infinite;
}

View File

@@ -0,0 +1,199 @@
// 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, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Input } from '~/ui';
import { CompareIcon, ComputerIcon, DashboardIcon, VpnIcon } from '~/ui/Icons';
import styles from './connection.css';
class Connection extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
isConnected: PropTypes.bool,
isConnecting: PropTypes.bool,
needsToken: PropTypes.bool
}
state = {
loading: false,
token: '',
validToken: false
}
render () {
const { isConnecting, isConnected, needsToken } = this.props;
if (!isConnecting && isConnected) {
return null;
}
return (
<div>
<div className={ styles.overlay } />
<div className={ styles.modal }>
<div className={ styles.body }>
<div className={ styles.icons }>
<div className={ styles.icon }>
<ComputerIcon className={ styles.svg } />
</div>
<div className={ styles.iconSmall }>
<CompareIcon className={ `${styles.svg} ${styles.pulse}` } />
</div>
<div className={ styles.icon }>
{
needsToken
? <VpnIcon className={ styles.svg } />
: <DashboardIcon className={ styles.svg } />
}
</div>
</div>
{
needsToken
? this.renderSigner()
: this.renderPing()
}
</div>
</div>
</div>
);
}
renderSigner () {
const { loading, token, validToken } = this.state;
const { isConnecting, needsToken } = this.props;
if (needsToken && !isConnecting) {
return (
<div className={ styles.info }>
<div>
<FormattedMessage
id='connection.noConnection'
defaultMessage='Unable to make a connection to the Parity Secure API. To update your secure token or to generate a new one, run {newToken} and paste the generated token into the space below.'
values={ {
newToken: <span className={ styles.console }>parity signer new-token</span>
} }
/>
</div>
<div className={ styles.form }>
<Input
disabled={ loading }
error={
validToken || (!token || !token.length)
? null
: (
<FormattedMessage
id='connection.invalidToken'
defaultMessage='invalid signer token'
/>
)
}
hint={
<FormattedMessage
id='connection.token.hint'
defaultMessage='a generated token from Parity'
/>
}
label={
<FormattedMessage
id='connection.token.label'
defaultMessage='secure token'
/>
}
onChange={ this.onChangeToken }
value={ token }
/>
</div>
</div>
);
}
return (
<div className={ styles.info }>
<FormattedMessage
id='connection.connectingAPI'
defaultMessage='Connecting to the Parity Secure API.'
/>
</div>
);
}
renderPing () {
return (
<div className={ styles.info }>
<FormattedMessage
id='connection.connectingNode'
defaultMessage='Connecting to the Parity Node. If this informational message persists, please ensure that your Parity node is running and reachable on the network.'
/>
</div>
);
}
validateToken = (_token) => {
const token = _token.trim();
const validToken = /^[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}$/.test(token);
return {
token,
validToken
};
}
onChangeToken = (event, _token) => {
const { token, validToken } = this.validateToken(_token || event.target.value);
this.setState({ token, validToken }, () => {
validToken && this.setToken();
});
}
setToken = () => {
const { api } = this.context;
const { token } = this.state;
this.setState({ loading: true });
return api
.updateToken(token, 0)
.then((isValid) => {
this.setState({
loading: isValid || false,
validToken: isValid
});
});
}
}
function mapStateToProps (state) {
const { isConnected, isConnecting, needsToken } = state.nodeStatus;
return {
isConnected,
isConnecting,
needsToken
};
}
export default connect(
mapStateToProps,
null
)(Connection);

View File

@@ -0,0 +1,156 @@
// 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 { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import Connection from './';
let api;
let component;
let instance;
function createApi () {
return {
updateToken: sinon.stub().resolves()
};
}
function createRedux (isConnected = true, isConnecting = false, needsToken = false) {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
nodeStatus: {
isConnected,
isConnecting,
needsToken
}
};
}
};
}
function render (store) {
api = createApi();
component = shallow(
<Connection />,
{ context: { store: store || createRedux() } }
).find('Connection').shallow({ context: { api } });
instance = component.instance();
return component;
}
describe('views/Connection', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
it('does not render when connected', () => {
expect(render(createRedux(true)).find('div')).to.have.length(0);
});
describe('renderPing', () => {
it('renders the connecting to node message', () => {
render();
const ping = shallow(instance.renderPing());
expect(ping.find('FormattedMessage').props().id).to.equal('connection.connectingNode');
});
});
describe('renderSigner', () => {
it('renders the connecting to api message when isConnecting === true', () => {
render(createRedux(false, true));
const signer = shallow(instance.renderSigner());
expect(signer.find('FormattedMessage').props().id).to.equal('connection.connectingAPI');
});
it('renders token input when needsToken == true & isConnecting === false', () => {
render(createRedux(false, false, true));
const signer = shallow(instance.renderSigner());
expect(signer.find('FormattedMessage').first().props().id).to.equal('connection.noConnection');
});
});
describe('validateToken', () => {
beforeEach(() => {
render();
});
it('trims whitespace from passed tokens', () => {
expect(instance.validateToken(' \t test ing\t ').token).to.equal('test ing');
});
it('validates 4-4-4-4 format', () => {
expect(instance.validateToken('1234-5678-90ab-cdef').validToken).to.be.true;
});
it('validates 4-4-4-4 format (with trimmable whitespace)', () => {
expect(instance.validateToken(' \t 1234-5678-90ab-cdef \t ').validToken).to.be.true;
});
it('validates 4444 format', () => {
expect(instance.validateToken('1234567890abcdef').validToken).to.be.true;
});
it('validates 4444 format (with trimmable whitespace)', () => {
expect(instance.validateToken(' \t 1234567890abcdef \t ').validToken).to.be.true;
});
});
describe('onChangeToken', () => {
beforeEach(() => {
render();
sinon.spy(instance, 'setToken');
sinon.spy(instance, 'validateToken');
});
afterEach(() => {
instance.setToken.restore();
instance.validateToken.restore();
});
it('validates tokens passed', () => {
instance.onChangeToken({ target: { value: 'testing' } });
expect(instance.validateToken).to.have.been.calledWith('testing');
});
it('sets the token on the api when valid', () => {
instance.onChangeToken({ target: { value: '1234-5678-90ab-cdef' } });
expect(instance.setToken).to.have.been.called;
});
});
describe('setToken', () => {
beforeEach(() => {
render();
});
it('calls the api.updateToken', () => {
component.setState({ token: 'testing' });
return instance.setToken().then(() => {
expect(api.updateToken).to.have.been.calledWith('testing');
});
});
});
});

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 './connection';

View File

@@ -0,0 +1,43 @@
/* 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/>.
*/
.frame {
background: white;
border: 0;
position: absolute;
height: 100%;
width: 100%;
}
.full {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background: white;
font-family: 'Roboto', sans-serif;
font-size: 16px;
font-weight: 300;
.text {
text-align: center;
padding: 5em;
font-size: 2em;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
}
}

139
js/src/shell/Dapp/dapp.js Normal file
View File

@@ -0,0 +1,139 @@
// 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, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import { FormattedMessage } from 'react-intl';
import DappsStore from '../Dapps/dappsStore';
import styles from './dapp.css';
@observer
export default class Dapp extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
params: PropTypes.object
};
state = {
app: null,
loading: true
};
store = DappsStore.get(this.context.api);
componentWillMount () {
const { id } = this.props.params;
this.loadApp(id);
}
componentWillReceiveProps (nextProps) {
if (nextProps.params.id !== this.props.params.id) {
this.loadApp(nextProps.params.id);
}
}
loadApp (id) {
this.setState({ loading: true });
this.store
.loadApp(id)
.then((app) => {
this.setState({ loading: false, app });
})
.catch(() => {
this.setState({ loading: false });
});
}
render () {
const { dappsUrl } = this.context.api;
const { params } = this.props;
const { app, loading } = this.state;
if (loading) {
return (
<div className={ styles.full }>
<div className={ styles.text }>
<FormattedMessage
id='dapp.loading'
defaultMessage='Loading'
/>
</div>
</div>
);
}
if (!app) {
return (
<div className={ styles.full }>
<div className={ styles.text }>
<FormattedMessage
id='dapp.unavailable'
defaultMessage='The dapp cannot be reached'
/>
</div>
</div>
);
}
let src = null;
switch (app.type) {
case 'local':
src = `${dappsUrl}/${app.id}/`;
break;
case 'network':
src = `${dappsUrl}/${app.contentHash}/`;
break;
default:
let dapphost = process.env.DAPPS_URL || (
process.env.NODE_ENV === 'production' && !app.secure
? `${dappsUrl}/ui`
: ''
);
if (dapphost === '/') {
dapphost = '';
}
src = `${dapphost}/${app.url}.html`;
break;
}
let hash = '';
if (params.details) {
hash = `#/${params.details}`;
}
return (
<iframe
className={ styles.frame }
frameBorder={ 0 }
name={ name }
sandbox='allow-forms allow-popups allow-same-origin allow-scripts allow-top-navigation'
scrolling='auto'
src={ `${src}${hash}` }
/>
);
}
}

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 './dapp';

View File

@@ -0,0 +1,220 @@
// 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 sinon from 'sinon';
import localStore from 'store';
import Contracts from '~/contracts';
import Store, { LS_KEY_DISPLAY } from './dappsStore';
const APPID_DAPPREG = '0x7bbc4f1a27628781b96213e781a1b8eec6982c1db8fac739af6e4c5a55862c03';
const APPID_GHH = '0x058740ee9a5a3fb9f1cfa10752baec87e09cc45cd7027fd54708271aca300c75';
const APPID_LOCALTX = '0xae74ad174b95cdbd01c88ac5b73a296d33e9088fc2a200e76bcedf3a94a7815d';
const APPID_TOKENDEPLOY = '0xf9f2d620c2e08f83e45555247146c62185e4ab7cf82a4b9002a265a0d020348f';
const FETCH_OK = {
ok: true,
status: 200
};
let globalContractsGet;
let globalFetch;
function stubGlobals () {
globalContractsGet = Contracts.get;
globalFetch = global.fetch;
Contracts.get = () => {
return {
dappReg: {
at: sinon.stub().resolves([[0, 1, 2, 3], 'appOwner']),
count: sinon.stub().resolves(new BigNumber(1)),
getContract: sinon.stub().resolves({}),
getContent: sinon.stub().resolves([0, 1, 2, 3]),
getImage: sinon.stub().resolves([0, 1, 2, 3]),
getManifest: sinon.stub().resolves([0, 1, 2, 3])
}
};
};
global.fetch = (url) => {
switch (url) {
case '/api/apps':
return Promise.resolve(Object.assign({}, FETCH_OK, {
json: sinon.stub().resolves([]) // TODO: Local stubs in here
}));
default:
console.log('Unknown fetch stub endpoint', url);
return Promise.reject();
}
};
}
function restoreGlobals () {
Contracts.get = globalContractsGet;
global.fetch = globalFetch;
}
let api;
let store;
function create () {
api = {};
store = new Store(api);
return store;
}
describe('views/Dapps/DappStore', () => {
beforeEach(() => {
stubGlobals();
});
afterEach(() => {
restoreGlobals();
});
describe('@action', () => {
const defaultViews = {
[APPID_TOKENDEPLOY]: { visible: false },
[APPID_DAPPREG]: { visible: true }
};
describe('setDisplayApps', () => {
beforeEach(() => {
create();
store.setDisplayApps(defaultViews);
});
it('sets from empty start', () => {
expect(store.displayApps).to.deep.equal(defaultViews);
});
it('overrides single keys, keeping existing', () => {
store.setDisplayApps({ [APPID_TOKENDEPLOY]: { visible: true } });
expect(store.displayApps).to.deep.equal(
Object.assign({}, defaultViews, { [APPID_TOKENDEPLOY]: { visible: true } })
);
});
it('extends with new keys, keeping existing', () => {
store.setDisplayApps({ 'test': { visible: true } });
expect(store.displayApps).to.deep.equal(
Object.assign({}, defaultViews, { 'test': { visible: true } })
);
});
});
describe('hideApp/showApp', () => {
beforeEach(() => {
localStore.set(LS_KEY_DISPLAY, defaultViews);
create().readDisplayApps();
});
afterEach(() => {
localStore.set(LS_KEY_DISPLAY, {});
});
it('disables visibility', () => {
store.hideApp(APPID_DAPPREG);
expect(store.displayApps[APPID_DAPPREG].visible).to.be.false;
expect(localStore.get(LS_KEY_DISPLAY)).to.deep.equal(
Object.assign({}, defaultViews, { [APPID_DAPPREG]: { visible: false } })
);
});
it('enables visibility', () => {
store.showApp(APPID_TOKENDEPLOY);
expect(store.displayApps[APPID_TOKENDEPLOY].visible).to.be.true;
expect(localStore.get(LS_KEY_DISPLAY)).to.deep.equal(
Object.assign({}, defaultViews, { [APPID_TOKENDEPLOY]: { visible: true } })
);
});
it('keeps visibility state', () => {
store.hideApp(APPID_TOKENDEPLOY);
store.showApp(APPID_DAPPREG);
expect(store.displayApps[APPID_TOKENDEPLOY].visible).to.be.false;
expect(store.displayApps[APPID_DAPPREG].visible).to.be.true;
expect(localStore.get(LS_KEY_DISPLAY)).to.deep.equal(defaultViews);
});
});
describe('readDisplayApps/writeDisplayApps', () => {
beforeEach(() => {
localStore.set(LS_KEY_DISPLAY, defaultViews);
create().readDisplayApps();
});
afterEach(() => {
localStore.set(LS_KEY_DISPLAY, {});
});
it('loads visibility from storage', () => {
expect(store.displayApps).to.deep.equal(defaultViews);
});
it('saves visibility to storage', () => {
store.setDisplayApps({ [APPID_TOKENDEPLOY]: { visible: true } });
store.writeDisplayApps();
expect(localStore.get(LS_KEY_DISPLAY)).to.deep.equal(
Object.assign({}, defaultViews, { [APPID_TOKENDEPLOY]: { visible: true } })
);
});
});
});
describe('saved views', () => {
beforeEach(() => {
localStore.set(LS_KEY_DISPLAY, {
[APPID_TOKENDEPLOY]: { visible: false },
[APPID_DAPPREG]: { visible: true }
});
return create().loadAllApps();
});
afterEach(() => {
localStore.set(LS_KEY_DISPLAY, {});
});
it('disables based on saved keys', () => {
expect(store.displayApps[APPID_TOKENDEPLOY].visible).to.be.false;
});
it('enables based on saved keys', () => {
expect(store.displayApps[APPID_DAPPREG].visible).to.be.true;
});
it('keeps non-sepcified disabled keys', () => {
expect(store.displayApps[APPID_GHH].visible).to.be.false;
});
it('keeps non-specified enabled keys', () => {
expect(store.displayApps[APPID_LOCALTX].visible).to.be.true;
});
});
});

View File

@@ -0,0 +1,27 @@
/* 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/>.
*/
.overlay {
line-height: 1.5em;
margin: 0 auto;
text-align: left;
max-width: 980px;
&>div:first-child {
padding-bottom: 1em;
}
}

176
js/src/shell/Dapps/dapps.js Normal file
View File

@@ -0,0 +1,176 @@
// 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 { omitBy } from 'lodash';
import { Checkbox } from 'material-ui';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { DappPermissions, DappsVisible } from '~/modals';
import PermissionStore from '~/modals/DappPermissions/store';
import { Actionbar, Button, DappCard, Page, SectionList } from '~/ui';
import { LockedIcon, VisibleIcon } from '~/ui/Icons';
import DappsStore from './dappsStore';
import styles from './dapps.css';
@observer
class Dapps extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
accounts: PropTypes.object.isRequired
};
store = DappsStore.get(this.context.api);
permissionStore = new PermissionStore(this.context.api);
componentWillMount () {
this.store.loadAllApps();
}
render () {
let externalOverlay = null;
if (this.store.externalOverlayVisible) {
externalOverlay = (
<div className={ styles.overlay }>
<div>
<FormattedMessage
id='dapps.external.warning'
defaultMessage='Applications made available on the network by 3rd-party authors are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each before interacting.'
/>
</div>
<div>
<Checkbox
className={ styles.accept }
label={
<FormattedMessage
id='dapps.external.accept'
defaultMessage='I understand that these applications are not affiliated with Parity'
/>
}
checked={ false }
onCheck={ this.onClickAcceptExternal }
/>
</div>
</div>
);
}
return (
<div>
<DappPermissions permissionStore={ this.permissionStore } />
<DappsVisible store={ this.store } />
<Actionbar
className={ styles.toolbar }
title={
<FormattedMessage
id='dapps.label'
defaultMessage='Decentralized Applications'
/>
}
buttons={ [
<Button
icon={ <VisibleIcon /> }
key='edit'
label={
<FormattedMessage
id='dapps.button.edit'
defaultMessage='edit'
/>
}
onClick={ this.store.openModal }
/>,
<Button
icon={ <LockedIcon /> }
key='permissions'
label={
<FormattedMessage
id='dapps.button.permissions'
defaultMessage='permissions'
/>
}
onClick={ this.openPermissionsModal }
/>
] }
/>
<Page>
<div>{ this.renderList(this.store.visibleViews) }</div>
<div>{ this.renderList(this.store.visibleLocal) }</div>
<div>{ this.renderList(this.store.visibleBuiltin) }</div>
<div>{ this.renderList(this.store.visibleNetwork, externalOverlay) }</div>
</Page>
</div>
);
}
renderList (items, overlay) {
return (
<SectionList
items={ items }
overlay={ overlay }
renderItem={ this.renderApp }
/>
);
}
renderApp = (app) => {
return (
<DappCard
app={ app }
key={ app.id }
showLink
showTags
/>
);
}
onClickAcceptExternal = () => {
this.store.closeExternalOverlay();
}
openPermissionsModal = () => {
const { accounts } = this.props;
this.permissionStore.openModal(accounts);
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
/**
* Do not show the Wallet Accounts in the Dapps
* Permissions Modal. This will come in v1.6, but
* for now it would break dApps using Web3...
*/
const _accounts = omitBy(accounts, (account) => account.wallet);
return {
accounts: _accounts
};
}
export default connect(
mapStateToProps,
null
)(Dapps);

View File

@@ -0,0 +1,302 @@
// 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 EventEmitter from 'eventemitter3';
import { action, computed, observable, transaction } from 'mobx';
import store from 'store';
import Contracts from '~/contracts';
import {
fetchBuiltinApps, fetchLocalApps,
fetchRegistryAppIds, fetchRegistryApp,
subscribeToChanges
} from '~/util/dapps';
const LS_KEY_DISPLAY = 'displayApps';
const LS_KEY_EXTERNAL_ACCEPT = 'acceptExternal';
const BUILTIN_APPS_KEY = 'BUILTIN_APPS_KEY';
let instance = null;
export default class DappsStore extends EventEmitter {
@observable apps = [];
@observable displayApps = {};
@observable modalOpen = false;
@observable externalOverlayVisible = true;
_api = null;
_subscriptions = {};
_cachedApps = {};
_manifests = {};
_registryAppsIds = null;
constructor (api) {
super();
this._api = api;
this.readDisplayApps();
this.loadExternalOverlay();
this.subscribeToChanges();
}
static get (api) {
if (!instance) {
instance = new DappsStore(api);
}
return instance;
}
@computed get sortedBuiltin () {
return this.apps.filter((app) => app.type === 'builtin');
}
@computed get sortedLocal () {
return this.apps.filter((app) => app.type === 'local');
}
@computed get sortedNetwork () {
return this.apps.filter((app) => app.type === 'network');
}
@computed get visibleApps () {
return this.apps.filter((app) => this.displayApps[app.id] && this.displayApps[app.id].visible);
}
@computed get visibleBuiltin () {
return this.visibleApps.filter((app) => !app.noselect && app.type === 'builtin');
}
@computed get visibleLocal () {
return this.visibleApps.filter((app) => app.type === 'local');
}
@computed get visibleNetwork () {
return this.visibleApps.filter((app) => app.type === 'network');
}
@computed get visibleViews () {
return this.visibleApps.filter((app) => !app.noselect && app.type === 'view');
}
/**
* Try to find the app from the local (local or builtin)
* apps, else fetch from the node
*/
loadApp (id) {
const { dappReg } = Contracts.get(this._api);
return this
.loadLocalApps()
.then(() => {
const app = this.apps.find((app) => app.id === id);
if (app) {
return app;
}
return this.fetchRegistryApp(dappReg, id, true);
})
.then((app) => {
this.emit('loaded', app);
return app;
});
}
loadLocalApps () {
return Promise
.all([
this.fetchBuiltinApps().then((apps) => this.addApps(apps)),
this.fetchLocalApps().then((apps) => this.addApps(apps))
]);
}
loadAllApps () {
const { dappReg } = Contracts.get(this._api);
return Promise
.all([
this.loadLocalApps(),
this.fetchRegistryApps(dappReg).then((apps) => this.addApps(apps))
])
.then(this.writeDisplayApps);
}
subscribeToChanges () {
const { dappReg } = Contracts.get(this._api);
// Unsubscribe from previous subscriptions, if any
if (this._subscriptions.block) {
this._api.unsubscribe(this._subscriptions.block);
}
if (this._subscriptions.filter) {
this._api.eth.uninstallFilter(this._subscriptions.filter);
}
// Subscribe to dapps reg changes
subscribeToChanges(this._api, dappReg, (appIds) => {
const updates = appIds.map((appId) => {
return this.fetchRegistryApp(dappReg, appId, true);
});
Promise
.all(updates)
.then((apps) => {
this.addApps(apps);
});
}).then((subscriptions) => {
this._subscriptions = subscriptions;
});
}
fetchBuiltinApps (force = false) {
if (!force && this._cachedApps[BUILTIN_APPS_KEY] !== undefined) {
return Promise.resolve(this._cachedApps[BUILTIN_APPS_KEY]);
}
this._cachedApps[BUILTIN_APPS_KEY] = fetchBuiltinApps(this._api)
.then((apps) => {
this._cachedApps[BUILTIN_APPS_KEY] = apps;
return apps;
});
return Promise.resolve(this._cachedApps[BUILTIN_APPS_KEY]);
}
fetchLocalApps () {
return fetchLocalApps(this._api);
}
fetchRegistryAppIds (force = false) {
if (!force && this._registryAppsIds) {
return Promise.resolve(this._registryAppsIds);
}
this._registryAppsIds = fetchRegistryAppIds(this._api)
.then((appIds) => {
this._registryAppsIds = appIds;
return this._registryAppsIds;
});
return Promise.resolve(this._registryAppsIds);
}
fetchRegistryApp (dappReg, appId, force = false) {
if (!force && this._cachedApps[appId] !== undefined) {
return Promise.resolve(this._cachedApps[appId]);
}
this._cachedApps[appId] = fetchRegistryApp(this._api, dappReg, appId)
.then((dapp) => {
this._cachedApps[appId] = dapp;
return dapp;
});
return Promise.resolve(this._cachedApps[appId]);
}
fetchRegistryApps (dappReg) {
return this
.fetchRegistryAppIds()
.then((appIds) => {
const promises = appIds.map((appId) => {
// Fetch the Dapp and display it ASAP
return this
.fetchRegistryApp(dappReg, appId)
.then((app) => {
if (app) {
this.addApps([ app ]);
}
return app;
});
});
return Promise.all(promises);
});
}
@action openModal = () => {
this.modalOpen = true;
}
@action closeModal = () => {
this.modalOpen = false;
}
@action closeExternalOverlay = () => {
this.externalOverlayVisible = false;
store.set(LS_KEY_EXTERNAL_ACCEPT, true);
}
@action loadExternalOverlay () {
this.externalOverlayVisible = !(store.get(LS_KEY_EXTERNAL_ACCEPT) || false);
}
@action hideApp = (id) => {
this.setDisplayApps({ [id]: { visible: false } });
this.writeDisplayApps();
}
@action showApp = (id) => {
this.setDisplayApps({ [id]: { visible: true } });
this.writeDisplayApps();
}
@action readDisplayApps = () => {
this.displayApps = store.get(LS_KEY_DISPLAY) || {};
}
@action writeDisplayApps = () => {
store.set(LS_KEY_DISPLAY, this.displayApps);
}
@action setDisplayApps = (displayApps) => {
this.displayApps = Object.assign({}, this.displayApps, displayApps);
};
@action addApps = (_apps = []) => {
transaction(() => {
const apps = _apps.filter((app) => app);
// Get new apps IDs if available
const newAppsIds = apps
.map((app) => app.id)
.filter((id) => id);
this.apps = this.apps
.filter((app) => !app.id || !newAppsIds.includes(app.id))
.concat(apps || [])
.sort((a, b) => a.name.localeCompare(b.name));
const visibility = {};
apps.forEach((app) => {
if (!this.displayApps[app.id]) {
visibility[app.id] = { visible: app.visible };
}
});
this.setDisplayApps(visibility);
});
}
}
export {
LS_KEY_DISPLAY
};

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 './dapps';

View File

@@ -0,0 +1,129 @@
// 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, transaction } from 'mobx';
export default class AccountStore {
@observable accounts = [];
@observable defaultAccount = null;
@observable isLoading = false;
constructor (api) {
this._api = api;
this.loadDefaultAccount()
.then(() => this.loadAccounts());
this.subscribeDefaultAccount();
}
@action setAccounts = (accounts) => {
this.accounts = accounts;
}
@action setDefaultAccount = (defaultAccount) => {
transaction(() => {
this.accounts = this.accounts.map((account) => {
account.checked = account.address === defaultAccount;
return account;
});
this.defaultAccount = defaultAccount;
});
}
@action setLoading = (isLoading) => {
this.isLoading = isLoading;
}
makeDefaultAccount = (defaultAddress) => {
this.setDefaultAccount(defaultAddress);
return this._api.parity
.setNewDappsDefaultAddress(defaultAddress)
.catch((error) => {
console.warn('makeDefaultAccount', error);
});
}
loadDefaultAccount () {
return this._api.parity
.defaultAccount()
.then((address) => this.setDefaultAccount(address));
}
loadAccounts () {
this.setLoading(true);
return Promise
.all([
this._api.parity.getNewDappsAddresses(),
this._api.parity.allAccountsInfo()
])
.then(([whitelist, allAccounts]) => {
transaction(() => {
const accounts = Object
.keys(allAccounts)
.filter((address) => {
const account = allAccounts[address];
const isAccount = account.uuid;
const isExternal = account.meta && (account.meta.external || account.meta.hardware);
const isWhitelisted = !whitelist || whitelist.includes(address);
return (isAccount || isExternal) && isWhitelisted;
})
.map((address) => {
return {
...allAccounts[address],
checked: address === this.defaultAccount,
address
};
});
this.setLoading(false);
this.setAccounts(accounts);
});
})
.catch((error) => {
this.setLoading(false);
console.warn('loadAccounts', error);
});
}
subscribeDefaultAccount () {
const promiseDefaultAccount = this._api.subscribe('parity_defaultAccount', (error, defaultAccount) => {
if (!error) {
this.setDefaultAccount(defaultAccount);
}
});
const promiseEthAccounts = this._api.subscribe('eth_accounts', (error) => {
if (!error) {
this.loadAccounts();
}
});
const promiseAccountsInfo = this._api
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
if (!error) {
this.loadAccounts();
}
});
return Promise.all([ promiseDefaultAccount, promiseEthAccounts, promiseAccountsInfo ]);
}
}

View File

@@ -0,0 +1,112 @@
// 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 sinon from 'sinon';
import AccountStore from './accountStore';
import { ACCOUNT_DEFAULT, ACCOUNT_NEW, createApi } from './parityBar.test.js';
let api;
let store;
function create () {
api = createApi();
store = new AccountStore(api);
return store;
}
describe('views/ParityBar/AccountStore', () => {
beforeEach(() => {
create();
});
describe('constructor', () => {
it('subscribes to defaultAccount', () => {
expect(api.subscribe).to.have.been.calledWith('parity_defaultAccount');
});
});
describe('@action', () => {
describe('setAccounts', () => {
it('sets the accounts', () => {
store.setAccounts('testing');
expect(store.accounts).to.equal('testing');
});
});
describe('setDefaultAccount', () => {
it('sets the default account', () => {
store.setDefaultAccount('testing');
expect(store.defaultAccount).to.equal('testing');
});
});
describe('setLoading', () => {
it('sets the loading status', () => {
store.setLoading('testing');
expect(store.isLoading).to.equal('testing');
});
});
});
describe('operations', () => {
describe('loadAccounts', () => {
beforeEach(() => {
sinon.spy(store, 'setAccounts');
return store.loadAccounts();
});
afterEach(() => {
store.setAccounts.restore();
});
it('calls into parity_getNewDappsAddresses', () => {
expect(api.parity.getNewDappsAddresses).to.have.been.called;
});
it('calls into parity_allAccountsInfo', () => {
expect(api.parity.allAccountsInfo).to.have.been.called;
});
it('sets the accounts', () => {
expect(store.setAccounts).to.have.been.called;
});
});
describe('loadDefaultAccount', () => {
beforeEach(() => {
return store.loadDefaultAccount();
});
it('load and set the default account', () => {
expect(store.defaultAccount).to.equal(ACCOUNT_DEFAULT);
});
});
describe('makeDefaultAccount', () => {
beforeEach(() => {
return store.makeDefaultAccount(ACCOUNT_NEW);
});
it('calls into parity_setNewDappsDefaultAddress', () => {
expect(api.parity.setNewDappsDefaultAddress).to.have.been.calledWith(ACCOUNT_NEW);
});
});
});
});

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 './parityBar';

View File

@@ -0,0 +1,267 @@
/* 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/>.
*/
$overlayZ: 10000;
$modalZ: 10001;
.account {
width: 100%;
.selected,
.unselected {
margin: 0.125em 0;
&:focus {
outline: none;
}
}
.unselected {
background: rgba(0, 0, 0, 0.4) !important;
}
.selected {
background: rgba(255, 255, 255, 0.35) !important;
}
}
.container {
display: flex;
flex-direction: column;
width: 100%;
}
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.5);
z-index: $overlayZ;
user-select: none;
}
.bar,
.expanded {
position: fixed;
font-size: 16px;
font-family: 'Roboto', sans-serif;
z-index: $modalZ;
user-select: none;
}
.bar {
vertical-align: middle;
display: flex;
flex-wrap: wrap;
width: 100%;
top: 0;
left: 0;
&.moving {
bottom: 0;
right: 0;
&:hover {
cursor: move;
}
}
}
.parityBg {
position: fixed;
transition-property: left, top, right, bottom;
transition-duration: 0.25s;
transition-timing-function: ease;
&.moving {
transition-duration: 0.05s;
transition-timing-function: ease-in-out;
}
}
.accountsSection {
padding: 0 1em;
width: 920px;
}
.expanded {
border-radius: 4px 4px 0 0;
display: flex;
flex-direction: row;
min-height: 30vh;
max-height: 80vh;
max-width: calc(100vw - 2em);
width: 960px;
.content {
flex: 1;
overflow: auto;
display: flex;
background: rgba(255, 255, 255, 0.95);
max-width: calc(100vw - 2em);
}
}
.corner {
border-radius: 4px 4px 0 0;
}
.cornercolor {
background: rgba(0, 0, 0, 0.75);
padding: 0.5em 1em;
display: flex;
align-items: center;
}
.link {
white-space: nowrap;
border: none;
outline: none !important;
color: white !important;
display: inline-block;
img,
svg {
height: 24px !important;
width: 24px !important;
margin: 2px 0.5em 0 0;
}
}
.link+.link {
margin-left: 1em;
}
.button,
.iconButton,
.parityButton {
overflow: visible !important;
}
.parityButton img {
width: auto !important;
height: 24px;
}
.button svg {
fill: white !important;
}
.iconButton {
min-width: 2em !important;
img {
margin: 6px 0.5em 0;
}
}
.label {
position: relative;
display: inline-block;
vertical-align: top;
}
.labelText {
text-transform: uppercase;
vertical-align: top;
}
.labelBubble {
position: absolute;
top: 0;
right: -10px;
}
.header {
height: 2em;
padding: 0.5em 1em;
background: rgba(0, 0, 0, 0.25);
margin-bottom: 0;
&::after {
clear: both;
}
}
.header,
.corner {
button {
color: white !important;
}
svg {
fill: white !important;
}
}
.body {
padding: 1em;
}
.title {
float: left;
}
.actions {
float: right;
margin-top: -2px;
div {
margin-left: 1em;
display: inline-block;
cursor: pointer;
}
}
.parityIcon,
.signerIcon {
width: 24px;
height: 24px;
vertical-align: middle;
margin-left: 12px;
}
.moveIcon {
display: flex;
align-items: center;
&:hover {
cursor: move;
}
}
.dragButton {
width: 1em;
height: 1em;
margin-left: 0.5em;
background-color: white;
opacity: 0.25;
border-radius: 50%;
transition-property: opacity;
transition-duration: 0.1s;
transition-timing-function: ease-in-out;
&:hover {
opacity: 0.5;
}
&.moving {
opacity: 0.75;
}
}

View File

@@ -0,0 +1,709 @@
// 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 { throttle } from 'lodash';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import store from 'store';
import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg';
import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, SelectionList } from '~/ui';
import { CancelIcon, FingerprintIcon } from '~/ui/Icons';
import DappsStore from '~/shell/Dapps/dappsStore';
import Signer from '~/shell/Signer/Embedded';
import AccountStore from './accountStore';
import styles from './parityBar.css';
const LS_STORE_KEY = '_parity::parityBar';
const DEFAULT_POSITION = { right: '1em', bottom: 0 };
const DISPLAY_ACCOUNTS = 'accounts';
const DISPLAY_SIGNER = 'signer';
@observer
class ParityBar extends Component {
app = null;
measures = null;
moving = false;
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
dapp: PropTypes.bool,
externalLink: PropTypes.string,
pending: PropTypes.array
};
state = {
displayType: DISPLAY_SIGNER,
moving: false,
opened: false,
position: DEFAULT_POSITION
};
constructor (props) {
super(props);
this.debouncedMouseMove = throttle(
this._onMouseMove,
40,
{ leading: true, trailing: true }
);
}
componentWillMount () {
const { api } = this.context;
this.accountStore = new AccountStore(api);
// Hook to the dapp loaded event to position the
// Parity Bar accordingly
const dappsStore = DappsStore.get(api);
dappsStore
.on('loaded', (app) => {
this.app = app;
this.loadPosition();
});
if (this.props.dapp) {
this.loadPosition();
}
}
componentWillReceiveProps (nextProps) {
const count = this.props.pending.length;
const newCount = nextProps.pending.length;
// Replace to default position when leaving a dapp
if (this.props.dapp && !nextProps.dapp) {
this.loadPosition(true);
}
// Load position when dapp loads
if (!this.props.dapp && nextProps.dapp) {
this.loadPosition();
}
if (count === newCount) {
return;
}
if (count < newCount) {
this.setOpened(true, DISPLAY_SIGNER);
} else if (newCount === 0 && count === 1) {
this.setOpened(false);
}
}
setOpened (opened, displayType = DISPLAY_SIGNER) {
this.setState({ displayType, opened });
this.dispatchOpenEvent(opened);
}
dispatchOpenEvent (opened) {
if (!this.bar) {
return;
}
// Fire up custom even to support having parity bar iframed.
const event = new CustomEvent('parity.bar.visibility', {
bubbles: true,
detail: { opened }
});
this.bar.dispatchEvent(event);
}
onRef = (element) => {
this.bar = element;
}
render () {
const { moving, opened, position } = this.state;
const containerClassNames = opened
? [ styles.overlay ]
: [ styles.bar ];
if (!opened && moving) {
containerClassNames.push(styles.moving);
}
const parityBgClassNames = [
opened
? styles.expanded
: styles.corner,
styles.parityBg
];
if (moving) {
parityBgClassNames.push(styles.moving);
}
const parityBgStyle = {
...position
};
// Open the Signer at one of the four corners
// of the screen
if (opened) {
// Set at top or bottom of the screen
if (position.top !== undefined) {
parityBgStyle.top = 0;
} else {
parityBgStyle.bottom = 0;
}
// Set at left or right of the screen
if (position.left !== undefined) {
parityBgStyle.left = '1em';
} else {
parityBgStyle.right = '1em';
}
}
return (
<div
className={ containerClassNames.join(' ') }
onMouseEnter={ this.onMouseEnter }
onMouseLeave={ this.onMouseLeave }
onMouseMove={ this.onMouseMove }
onMouseUp={ this.onMouseUp }
ref={ this.onRef }
>
<div
className={ parityBgClassNames.join(' ') }
ref='container'
style={ parityBgStyle }
>
{
opened
? this.renderExpanded()
: this.renderBar()
}
</div>
</div>
);
}
renderBar () {
const { dapp } = this.props;
if (!dapp) {
return null;
}
return (
<div className={ styles.cornercolor }>
<Button
className={ styles.iconButton }
icon={
<IdentityIcon
address={ this.accountStore.defaultAccount }
button
center
inline
/>
}
onClick={ this.toggleAccountsDisplay }
/>
{
this.renderLink(
<Button
className={ styles.parityButton }
icon={
<img
className={ styles.parityIcon }
src={ imagesEthcoreBlock }
/>
}
label={
this.renderLabel(
<FormattedMessage
id='parityBar.label.parity'
defaultMessage='Parity'
/>
)
}
/>
)
}
<Button
className={ styles.button }
icon={ <FingerprintIcon /> }
label={ this.renderSignerLabel() }
onClick={ this.toggleSignerDisplay }
/>
{ this.renderDrag() }
</div>
);
}
renderDrag () {
const dragButtonClasses = [ styles.dragButton ];
if (this.state.moving) {
dragButtonClasses.push(styles.moving);
}
return (
<div
className={ styles.moveIcon }
onMouseDown={ this.onMouseDown }
>
<div
className={ dragButtonClasses.join(' ') }
ref='dragButton'
/>
</div>
);
}
renderLink (button) {
const { externalLink } = this.props;
if (!externalLink) {
return (
<Link to='/apps'>
{ button }
</Link>
);
}
return (
<a
href={ externalLink }
target='_parent'
>
{ button }
</a>
);
}
renderExpanded () {
const { externalLink } = this.props;
const { displayType } = this.state;
return (
<div className={ styles.container }>
<div className={ styles.header }>
<div className={ styles.title }>
<ContainerTitle
title={
displayType === DISPLAY_ACCOUNTS
? (
<FormattedMessage
id='parityBar.title.accounts'
defaultMessage='Default Account'
/>
)
: (
<FormattedMessage
id='parityBar.title.signer'
defaultMessage='Parity Signer: Pending'
/>
)
}
/>
</div>
<div className={ styles.actions }>
<Button
icon={ <CancelIcon /> }
label={
<FormattedMessage
id='parityBar.button.close'
defaultMessage='Close'
/>
}
onClick={ this.toggleSignerDisplay }
/>
</div>
</div>
<div className={ styles.content }>
{
displayType === DISPLAY_ACCOUNTS
? (
<SelectionList
className={ styles.accountsSection }
items={ this.accountStore.accounts }
noStretch
onSelectClick={ this.onMakeDefault }
renderItem={ this.renderAccount }
/>
)
: (
<Signer externalLink={ externalLink } />
)
}
</div>
</div>
);
}
onMakeDefault = (account) => {
this.toggleAccountsDisplay();
return this.accountStore
.makeDefaultAccount(account.address)
.then(() => this.accountStore.loadAccounts());
}
renderAccount = (account) => {
return (
<AccountCard
account={ account }
/>
);
}
renderLabel (name, bubble) {
return (
<div className={ styles.label }>
<div className={ styles.labelText }>
{ name }
</div>
{ bubble }
</div>
);
}
renderSignerLabel () {
const { pending } = this.props;
let bubble = null;
if (pending && pending.length) {
bubble = (
<Badge
color='red'
className={ styles.labelBubble }
value={ pending.length }
/>
);
}
return this.renderLabel(
<FormattedMessage
id='parityBar.label.signer'
defaultMessage='Signer'
/>,
bubble
);
}
getHorizontal (x) {
const { page, button, container } = this.measures;
const left = x - button.offset.left;
const centerX = left + container.width / 2;
// left part of the screen
if (centerX < page.width / 2) {
return { left: Math.max(0, left) };
}
const right = page.width - x - button.offset.right;
return { right: Math.max(0, right) };
}
getVertical (y) {
const STICKY_SIZE = 75;
const { page, button, container } = this.measures;
const top = y - button.offset.top;
const centerY = top + container.height / 2;
// top part of the screen
if (centerY < page.height / 2) {
// Add Sticky edges
const stickyTop = top < STICKY_SIZE
? 0
: top;
return { top: Math.max(0, stickyTop) };
}
const bottom = page.height - y - button.offset.bottom;
// Add Sticky edges
const stickyBottom = bottom < STICKY_SIZE
? 0
: bottom;
return { bottom: Math.max(0, stickyBottom) };
}
getPosition (x, y) {
if (!this.moving || !this.measures) {
return {};
}
const horizontal = this.getHorizontal(x);
const vertical = this.getVertical(y);
const position = {
...horizontal,
...vertical
};
return position;
}
onMouseDown = () => {
// Dispatch an open event in case in an iframe (get full w and h)
this.dispatchOpenEvent(true);
window.setTimeout(() => {
const containerElt = ReactDOM.findDOMNode(this.refs.container);
const dragButtonElt = ReactDOM.findDOMNode(this.refs.dragButton);
if (!containerElt || !dragButtonElt) {
console.warn(containerElt ? 'drag button' : 'container', 'not found...');
return;
}
const bodyRect = document.body.getBoundingClientRect();
const containerRect = containerElt.getBoundingClientRect();
const buttonRect = dragButtonElt.getBoundingClientRect();
const buttonOffset = {
top: (buttonRect.top + buttonRect.height / 2) - containerRect.top,
left: (buttonRect.left + buttonRect.width / 2) - containerRect.left
};
buttonOffset.bottom = containerRect.height - buttonOffset.top;
buttonOffset.right = containerRect.width - buttonOffset.left;
const button = {
offset: buttonOffset,
height: buttonRect.height,
width: buttonRect.width
};
const container = {
height: containerRect.height,
width: containerRect.width
};
const page = {
height: bodyRect.height,
width: bodyRect.width
};
this.moving = true;
this.measures = {
button,
container,
page
};
this.setMovingState(true);
}, 50);
}
onMouseEnter = (event) => {
if (!this.moving) {
return;
}
const { buttons } = event;
// If no left-click, stop move
if (buttons !== 1) {
this.onMouseUp(event);
}
}
onMouseLeave = (event) => {
if (!this.moving) {
return;
}
event.stopPropagation();
event.preventDefault();
}
onMouseMove = (event) => {
const { pageX, pageY } = event;
// this._onMouseMove({ pageX, pageY });
this.debouncedMouseMove({ pageX, pageY });
event.stopPropagation();
event.preventDefault();
}
_onMouseMove = (event) => {
if (!this.moving) {
return;
}
const { pageX, pageY } = event;
const position = this.getPosition(pageX, pageY);
this.setState({ position });
}
onMouseUp = (event) => {
if (!this.moving) {
return;
}
const { pageX, pageY } = event;
const position = this.getPosition(pageX, pageY);
// Stick to bottom or top
if (position.top !== undefined) {
position.top = 0;
} else {
position.bottom = 0;
}
// Stick to bottom or top
if (position.left !== undefined) {
position.left = '1em';
} else {
position.right = '1em';
}
this.moving = false;
this.setMovingState(false, { position });
this.savePosition(position);
}
toggleAccountsDisplay = () => {
const { opened } = this.state;
this.setOpened(!opened, DISPLAY_ACCOUNTS);
}
toggleSignerDisplay = () => {
const { opened } = this.state;
this.setOpened(!opened, DISPLAY_SIGNER);
}
get config () {
const config = store.get(LS_STORE_KEY);
if (typeof config === 'string') {
try {
return JSON.parse(config);
} catch (e) {
return {};
}
}
return config || {};
}
/**
* Return the config key for the current view.
* If inside a dapp, should be the dapp id.
* Otherwise, try to get the current hostname.
*/
getConfigKey () {
const { app } = this;
if (app && app.id) {
return app.id;
}
return window.location.hostname;
}
loadPosition (loadDefault = false) {
if (loadDefault) {
return this.setState({ position: DEFAULT_POSITION });
}
const { app, config } = this;
const configKey = this.getConfigKey();
if (config[configKey]) {
return this.setState({ position: config[configKey] });
}
if (app && app.position) {
const position = this.stringToPosition(app.position);
return this.setState({ position });
}
return this.setState({ position: DEFAULT_POSITION });
}
savePosition (position) {
const { config } = this;
const configKey = this.getConfigKey();
config[configKey] = position;
store.set(LS_STORE_KEY, config);
}
stringToPosition (value) {
switch (value) {
case 'top-left':
return {
left: '1em',
top: 0
};
case 'top-right':
return {
right: '1em',
top: 0
};
case 'bottom-left':
return {
bottom: 0,
left: '1em'
};
case 'bottom-right':
default:
return DEFAULT_POSITION;
}
}
setMovingState (moving, extras = {}) {
this.setState({ moving, ...extras });
// Trigger an open event if it's moving
this.dispatchOpenEvent(moving);
}
}
function mapStateToProps (state) {
const { pending } = state.signer;
return {
pending
};
}
export default connect(
mapStateToProps,
null
)(ParityBar);

View File

@@ -0,0 +1,170 @@
// 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 { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import ParityBar from './';
import { createApi } from './parityBar.test.js';
let api;
let component;
let instance;
let store;
function createRedux (state = {}) {
store = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => Object.assign({
balances: {
balances: {}
},
signer: {
pending: []
}
}, state)
};
return store;
}
function render (props = {}, state = {}) {
api = createApi();
component = shallow(
<ParityBar { ...props } />,
{
context: {
store: createRedux(state)
}
}
).find('ParityBar').shallow({ context: { api } });
instance = component.instance();
return component;
}
describe('views/ParityBar', () => {
beforeEach(() => {
render({ dapp: true });
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('renderBar', () => {
let bar;
beforeEach(() => {
bar = shallow(instance.renderBar());
});
it('renders nothing when not overlaying a dapp', () => {
render({ dapp: false });
expect(instance.renderBar()).to.be.null;
});
it('renders when overlaying a dapp', () => {
expect(bar.find('div')).not.to.have.length(0);
});
it('renders the Account selector button', () => {
const icon = bar.find('Button').first().props().icon;
expect(icon.type.displayName).to.equal('Connect(IdentityIcon)');
});
it('renders the Parity button', () => {
const label = shallow(bar.find('Button').at(1).props().label);
expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.parity');
});
it('renders the Signer button', () => {
const label = shallow(bar.find('Button').last().props().label);
expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.signer');
});
});
describe('renderExpanded', () => {
let expanded;
beforeEach(() => {
expanded = shallow(instance.renderExpanded());
});
it('includes the Signer', () => {
expect(expanded.find('Connect(Embedded)')).to.have.length(1);
});
});
describe('renderLabel', () => {
it('renders the label name', () => {
expect(shallow(instance.renderLabel('testing', null)).text()).to.equal('testing');
});
it('renders name and bubble', () => {
expect(shallow(instance.renderLabel('testing', '(bubble)')).text()).to.equal('testing(bubble)');
});
});
describe('renderSignerLabel', () => {
let label;
beforeEach(() => {
label = shallow(instance.renderSignerLabel());
});
it('renders the signer label', () => {
expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.signer');
});
it('does not render a badge when no pending requests', () => {
expect(label.find('Badge')).to.have.length(0);
});
it('renders a badge when pending requests', () => {
render({}, { signer: { pending: ['123', '456'] } });
expect(shallow(instance.renderSignerLabel()).find('Badge').props().value).to.equal(2);
});
});
describe('opened state', () => {
beforeEach(() => {
sinon.spy(instance, 'renderBar');
sinon.spy(instance, 'renderExpanded');
});
afterEach(() => {
instance.renderBar.restore();
instance.renderExpanded.restore();
});
it('renders the bar on with opened === false', () => {
expect(component.find('Link[to="/apps"]')).to.have.length(1);
});
it('renders expanded with opened === true', () => {
expect(instance.renderExpanded).not.to.have.been.called;
instance.setState({ opened: true });
expect(instance.renderExpanded).to.have.been.called;
});
});
});

View File

@@ -0,0 +1,56 @@
// 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 sinon from 'sinon';
const ACCOUNT_DEFAULT = '0x2345678901';
const ACCOUNT_FIRST = '0x1234567890';
const ACCOUNT_NEW = '0x0987654321';
const ACCOUNTS = {
[ACCOUNT_FIRST]: { uuid: 123 },
[ACCOUNT_DEFAULT]: { uuid: 234 },
'0x3456789012': {},
[ACCOUNT_NEW]: { uuid: 456 }
};
function createApi () {
const api = {
subscribe: (params, callback) => {
callback(null, ACCOUNT_DEFAULT);
return Promise.resolve(1);
},
parity: {
defaultAccount: sinon.stub().resolves(ACCOUNT_DEFAULT),
allAccountsInfo: sinon.stub().resolves(ACCOUNTS),
getNewDappsAddresses: sinon.stub().resolves(null),
setNewDappsAddresses: sinon.stub().resolves(true),
setNewDappsDefaultAddress: sinon.stub().resolves(true)
}
};
sinon.spy(api, 'subscribe');
return api;
}
export {
ACCOUNT_DEFAULT,
ACCOUNT_FIRST,
ACCOUNT_NEW,
ACCOUNTS,
createApi
};

View File

@@ -0,0 +1,38 @@
/* 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 '../_layout.css';
.info {
padding: 1em 0;
}
.none {
color: #aaa;
}
.request {
&:nth-child(even) {
background: rgba(255, 255, 255, 0.04);
}
}
.signer {
box-sizing: border-box;
padding: 0;
width: $embedWidth;
}

View File

@@ -0,0 +1,135 @@
// 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, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Store from '../store';
import * as RequestsActions from '~/redux/providers/signerActions';
import { Container } from '~/ui';
import RequestPending from '../components/RequestPending';
import styles from './embedded.css';
class Embedded extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
actions: PropTypes.shape({
startConfirmRequest: PropTypes.func.isRequired,
startRejectRequest: PropTypes.func.isRequired
}).isRequired,
externalLink: PropTypes.string,
gasLimit: PropTypes.object.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);
render () {
return (
<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>
);
}
const items = pending.sort(this._sortRequests).map(this.renderPending);
return (
<div>
{ items }
</div>
);
}
renderPending = (data, index) => {
const { actions, gasLimit, netVersion } = this.props;
const { date, id, isSending, payload, origin } = data;
return (
<RequestPending
className={ styles.request }
date={ date }
focus={ index === 0 }
gasLimit={ gasLimit }
id={ id }
isSending={ isSending }
netVersion={ netVersion }
key={ id }
onConfirm={ actions.startConfirmRequest }
onReject={ actions.startRejectRequest }
origin={ origin }
payload={ payload }
signerStore={ this.store }
/>
);
}
_sortRequests = (a, b) => {
return new BigNumber(a.id).cmp(b.id);
}
}
function mapStateToProps (state) {
const { gasLimit, netVersion } = state.nodeStatus;
const { actions, signer } = state;
return {
actions,
gasLimit,
netVersion,
signer
};
}
function mapDispatchToProps (dispatch) {
return {
actions: bindActionCreators(RequestsActions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Embedded);

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 './embedded';

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/>.
*/
$pendingHeight: 190px;
$finishedHeight: 120px;
$embedWidth: 920px;
$statusWidth: 270px;
$accountPadding: 75px;

View File

@@ -0,0 +1,20 @@
/* 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/>.
*/
.container {
text-decoration: none;
color: inherit;
}

View File

@@ -0,0 +1,105 @@
// 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, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import styles from './accountLink.css';
class AccountLink extends Component {
static propTypes = {
accountAddresses: PropTypes.array.isRequired,
address: PropTypes.string.isRequired,
className: PropTypes.string,
children: PropTypes.node,
externalLink: PropTypes.string.isRequired
}
state = {
link: null
};
componentWillMount () {
const { address, externalLink } = this.props;
this.updateLink(address, externalLink);
}
componentWillReceiveProps (nextProps) {
const { address, externalLink } = nextProps;
this.updateLink(address, externalLink);
}
render () {
const { children, address, className, externalLink } = this.props;
if (externalLink) {
return (
<a
href={ this.state.link }
target='_blank'
className={ `${styles.container} ${className}` }
>
{ children || address }
</a>
);
}
return (
<Link
className={ `${styles.container} ${className}` }
to={ this.state.link }
>
{ children || address }
</Link>
);
}
updateLink (address, externalLink) {
const { accountAddresses } = this.props;
const isAccount = accountAddresses.includes(address);
let link = isAccount
? `/accounts/${address}`
: `/addresses/${address}`;
if (externalLink) {
const path = externalLink.replace(/\/+$/, '');
link = `${path}/#${link}`;
}
this.setState({
link
});
}
}
function mapStateToProps (initState) {
const { accounts } = initState.personal;
const accountAddresses = Object.keys(accounts);
return () => {
return { accountAddresses };
};
}
export default connect(
mapStateToProps,
null
)(AccountLink);

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 './accountLink';

View File

@@ -0,0 +1,50 @@
/* 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/>.
*/
.acc {
text-align: center;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.acc > * {
display: block;
}
.address {
font-family: monospace;
}
.acc img {
width: 28px;
height: 28px;
vertical-align: middle;
margin-right: 3px;
}
.name {
white-space: nowrap;
display: block;
vertical-align: middle;
text-transform: uppercase;
span {
text-overflow: ellipsis;
overflow: hidden;
}
}

View File

@@ -0,0 +1,128 @@
// 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, { Component, PropTypes } from 'react';
import { IdentityIcon, IdentityName } from '~/ui';
import AccountLink from './AccountLink';
import styles from './account.css';
export default class Account extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
className: PropTypes.string,
disabled: PropTypes.bool,
externalLink: PropTypes.string.isRequired,
netVersion: PropTypes.string.isRequired,
balance: PropTypes.object // eth BigNumber, not required since it mght take time to fetch
};
state = {
balanceDisplay: '?'
};
componentWillMount () {
this.updateBalanceDisplay(this.props.balance);
}
componentWillReceiveProps (nextProps) {
if (nextProps.balance === this.props.balance) {
return;
}
this.updateBalanceDisplay(nextProps.balance);
}
updateBalanceDisplay (balance) {
this.setState({
balanceDisplay: balance ? balance.div(1e18).toFormat(3) : '?'
});
}
render () {
const { address, className, disabled, externalLink, netVersion } = this.props;
return (
<div className={ `${styles.acc} ${className}` }>
<AccountLink
address={ address }
externalLink={ externalLink }
netVersion={ netVersion }
>
<IdentityIcon
center
disabled={ disabled }
address={ address }
/>
</AccountLink>
{ this.renderName() }
{ this.renderBalance() }
</div>
);
}
renderBalance () {
const { balanceDisplay } = this.state;
return (
<span> <strong>{ balanceDisplay }</strong> <small>ETH</small></span>
);
}
renderName () {
const { address, externalLink, netVersion } = this.props;
const name = <IdentityName address={ address } empty />;
if (!name) {
return (
<AccountLink
address={ address }
externalLink={ externalLink }
netVersion={ netVersion }
>
[{ this.shortAddress(address) }]
</AccountLink>
);
}
return (
<AccountLink
address={ address }
externalLink={ externalLink }
netVersion={ netVersion }
>
<span>
<span className={ styles.name }>{ name }</span>
<span className={ styles.address }>[{ this.tinyAddress(address) }]</span>
</span>
</AccountLink>
);
}
tinyAddress () {
const { address } = this.props;
const len = address.length;
return address.slice(2, 4) + '..' + address.slice(len - 2);
}
shortAddress () {
const { address } = this.props;
const len = address.length;
return address.slice(2, 8) + '..' + address.slice(len - 7);
}
}

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 './account';

View File

@@ -0,0 +1,183 @@
// 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 { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import Account from '../Account';
import TransactionPendingForm from '../TransactionPendingForm';
import RequestOrigin from '../RequestOrigin';
import styles from '../SignRequest/signRequest.css';
@observer
class DecryptRequest extends Component {
static contextTypes = {
api: PropTypes.object
};
static propTypes = {
accounts: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
data: PropTypes.string.isRequired,
id: PropTypes.object.isRequired,
isFinished: PropTypes.bool.isRequired,
netVersion: PropTypes.string.isRequired,
signerStore: PropTypes.object.isRequired,
className: PropTypes.string,
focus: PropTypes.bool,
isSending: PropTypes.bool,
onConfirm: PropTypes.func,
onReject: PropTypes.func,
origin: PropTypes.any,
status: PropTypes.string
};
static defaultProps = {
focus: false,
origin: {
type: 'unknown',
details: ''
}
};
componentWillMount () {
const { address, signerStore } = this.props;
signerStore.fetchBalance(address);
}
render () {
const { className } = this.props;
return (
<div className={ `${styles.container} ${className}` }>
{ this.renderDetails() }
{ this.renderActions() }
</div>
);
}
renderDetails () {
const { api } = this.context;
const { address, data, netVersion, origin, signerStore } = this.props;
const { balances, externalLink } = signerStore;
const balance = balances[address];
if (!balance) {
return <div />;
}
return (
<div className={ styles.signDetails }>
<div className={ styles.address }>
<Account
address={ address }
balance={ balance }
className={ styles.account }
externalLink={ externalLink }
netVersion={ netVersion }
/>
<RequestOrigin origin={ origin } />
</div>
<div className={ styles.info } title={ api.util.sha3(data) }>
<p>
<FormattedMessage
id='signer.decryptRequest.request'
defaultMessage='A request to decrypt data using your account:'
/>
</p>
<div className={ styles.signData }>
<p>{ data }</p>
</div>
</div>
</div>
);
}
renderActions () {
const { accounts, address, focus, isFinished, status } = this.props;
const account = accounts[address];
if (isFinished) {
if (status === 'confirmed') {
return (
<div className={ styles.actions }>
<span className={ styles.isConfirmed }>
<FormattedMessage
id='signer.decryptRequest.state.confirmed'
defaultMessage='Confirmed'
/>
</span>
</div>
);
}
return (
<div className={ styles.actions }>
<span className={ styles.isRejected }>
<FormattedMessage
id='signer.decryptRequest.state.rejected'
defaultMessage='Rejected'
/>
</span>
</div>
);
}
return (
<TransactionPendingForm
account={ account }
address={ address }
focus={ focus }
isSending={ this.props.isSending }
netVersion={ this.props.netVersion }
onConfirm={ this.onConfirm }
onReject={ this.onReject }
className={ styles.actions }
/>
);
}
onConfirm = (data) => {
const { id } = this.props;
const { password } = data;
this.props.onConfirm({ id, password });
}
onReject = () => {
this.props.onReject(this.props.id);
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
return {
accounts
};
}
export default connect(
mapStateToProps,
null
)(DecryptRequest);

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 './decryptRequest';

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 './requestOrigin';

View File

@@ -0,0 +1,43 @@
/* 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/>.
*/
.container {
text-align: left;
margin: 3em .5em;
opacity: 0.6;
font-size: 0.8em;
.unknown {
color: #e44;
}
.url {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.hash {
margin-left: .5em;
}
.hash, .url {
margin-bottom: -.2em;
display: inline-block;
}
}

View File

@@ -0,0 +1,163 @@
// 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, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import IdentityIcon from '~/ui/IdentityIcon';
import styles from './requestOrigin.css';
export default class RequestOrigin extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
origin: PropTypes.shape({
type: PropTypes.oneOf(['unknown', 'dapp', 'rpc', 'ipc', 'signer']),
details: PropTypes.string.isRequired
}).isRequired
};
render () {
const { origin } = this.props;
return (
<div className={ styles.container }>
Requested { this.renderOrigin(origin) }
</div>
);
}
renderOrigin (origin) {
if (origin.type === 'unknown') {
return (
<span className={ styles.unknown }>
<FormattedMessage
id='signer.requestOrigin.unknownInterface'
defaultMessage='via unknown interface'
/>
</span>
);
}
if (origin.type === 'dapp') {
return (
<span>
<FormattedMessage
id='signer.requestOrigin.dapp'
defaultMessage='by a dapp at {url}'
values={ {
url: (
<span className={ styles.url }>
{
origin.details || (
<FormattedMessage
id='signer.requestOrigin.unknownUrl'
defaultMessage='unknown URL'
/>
)
}
</span>
)
} }
/>
</span>
);
}
if (origin.type === 'rpc') {
return (
<span>
<FormattedMessage
id='signer.requestOrigin.rpc'
defaultMessage='via RPC {rpc}'
values={ {
url: (
<span className={ styles.url }>
({
origin.details || (
<FormattedMessage
id='signer.requestOrigin.unknownRpc'
defaultMessage='unidentified'
/>
)
})
</span>
)
} }
/>
</span>
);
}
if (origin.type === 'ipc') {
return (
<span>
<FormattedMessage
id='signer.requestOrigin.ipc'
defaultMessage='via IPC session'
/>
<span
className={ styles.hash }
title={ origin.details }
>
<IdentityIcon
address={ origin.details }
tiny
/>
</span>
</span>
);
}
if (origin.type === 'signer') {
return this.renderSigner(origin.details);
}
}
renderSigner (session) {
if (session.substr(2) === this.context.api.transport.sessionHash) {
return (
<span title={ session }>
<FormattedMessage
id='signer.requestOrigin.signerCurrent'
defaultMessage='via current tab'
/>
</span>
);
}
return (
<span>
<FormattedMessage
id='signer.requestOrigin.signerUI'
defaultMessage='via UI session'
/>
<span
className={ styles.hash }
title={ `UI Session id: ${session}` }
>
<IdentityIcon
address={ session }
tiny
/>
</span>
</span>
);
}
}

View File

@@ -0,0 +1,72 @@
// 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 { shallow } from 'enzyme';
import React from 'react';
import RequestOrigin from './';
const context = {
context: {
api: {
transport: {
sessionHash: '1234'
}
}
}
};
describe('views/Signer/components/RequestOrigin', () => {
it('renders unknown', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'unknown', details: '' } } />,
context
).find('FormattedMessage').props().id).to.equal('signer.requestOrigin.unknownInterface');
});
it('renders dapps', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'dapp', details: 'http://parity.io' } } />,
context
).find('FormattedMessage').props().id).to.equal('signer.requestOrigin.dapp');
});
it('renders rpc', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'rpc', details: '' } } />,
context
).find('FormattedMessage').props().id).to.equal('signer.requestOrigin.rpc');
});
it('renders ipc', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'ipc', details: '0x1234' } } />,
context
).find('FormattedMessage').props().id).to.equal('signer.requestOrigin.ipc');
});
it('renders signer', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'signer', details: '0x12345' } } />,
context
).find('FormattedMessage').props().id).to.equal('signer.requestOrigin.signerUI');
expect(shallow(
<RequestOrigin origin={ { type: 'signer', details: '0x1234' } } />,
context
).find('FormattedMessage').props().id).to.equal('signer.requestOrigin.signerCurrent');
});
});

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 './requestPending';

View File

@@ -0,0 +1,125 @@
// 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, { Component, PropTypes } from 'react';
import DecryptRequest from '../DecryptRequest';
import SignRequest from '../SignRequest';
import TransactionPending from '../TransactionPending';
export default class RequestPending extends Component {
static propTypes = {
className: PropTypes.string,
date: PropTypes.instanceOf(Date).isRequired,
focus: PropTypes.bool,
gasLimit: PropTypes.object.isRequired,
id: PropTypes.object.isRequired,
isSending: PropTypes.bool.isRequired,
netVersion: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
origin: PropTypes.object.isRequired,
payload: PropTypes.oneOfType([
PropTypes.shape({ decrypt: PropTypes.object.isRequired }),
PropTypes.shape({ sendTransaction: PropTypes.object.isRequired }),
PropTypes.shape({ sign: PropTypes.object.isRequired }),
PropTypes.shape({ signTransaction: PropTypes.object.isRequired })
]).isRequired,
signerStore: PropTypes.object.isRequired
};
static defaultProps = {
focus: false,
isSending: false
};
render () {
const { className, date, focus, gasLimit, id, isSending, netVersion, onReject, payload, signerStore, origin } = this.props;
if (payload.sign) {
const { sign } = payload;
return (
<SignRequest
address={ sign.address }
className={ className }
focus={ focus }
data={ sign.data }
id={ id }
isFinished={ false }
isSending={ isSending }
netVersion={ netVersion }
onConfirm={ this.onConfirm }
onReject={ onReject }
origin={ origin }
signerStore={ signerStore }
/>
);
}
if (payload.decrypt) {
const { decrypt } = payload;
return (
<DecryptRequest
address={ decrypt.address }
className={ className }
focus={ focus }
data={ decrypt.msg }
id={ id }
isFinished={ false }
isSending={ isSending }
netVersion={ netVersion }
onConfirm={ this.onConfirm }
onReject={ onReject }
origin={ origin }
signerStore={ signerStore }
/>
);
}
const transaction = payload.sendTransaction || payload.signTransaction;
if (transaction) {
return (
<TransactionPending
className={ className }
date={ date }
focus={ focus }
gasLimit={ gasLimit }
id={ id }
isSending={ isSending }
netVersion={ netVersion }
onConfirm={ this.onConfirm }
onReject={ onReject }
origin={ origin }
signerStore={ signerStore }
transaction={ transaction }
/>
);
}
console.error('RequestPending: Unknown payload', payload);
return null;
}
onConfirm = (data) => {
const { onConfirm, payload } = this.props;
data.payload = payload;
onConfirm(data);
};
}

View File

@@ -0,0 +1,112 @@
// 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 { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import RequestPending from './';
const ADDRESS = '0x1234567890123456789012345678901234567890';
const TRANSACTION = {
from: ADDRESS,
gas: new BigNumber(21000),
gasPrice: new BigNumber(20000000),
value: new BigNumber(1)
};
const PAYLOAD_SENDTX = {
sendTransaction: TRANSACTION
};
const PAYLOAD_SIGN = {
sign: {
address: ADDRESS,
data: 'testing'
}
};
const PAYLOAD_SIGNTX = {
signTransaction: TRANSACTION
};
let component;
let onConfirm;
let onReject;
function render (payload) {
onConfirm = sinon.stub();
onReject = sinon.stub();
component = shallow(
<RequestPending
date={ new Date() }
gasLimit={ new BigNumber(100000) }
id={ new BigNumber(123) }
isSending={ false }
netVersion='42'
onConfirm={ onConfirm }
onReject={ onReject }
origin={ {} }
payload={ payload }
store={ {} }
/>
);
return component;
}
describe('views/Signer/RequestPending', () => {
describe('sendTransaction', () => {
beforeEach(() => {
render(PAYLOAD_SENDTX);
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders TransactionPending component', () => {
expect(component.find('Connect(TransactionPending)')).to.have.length(1);
});
});
describe('sign', () => {
beforeEach(() => {
render(PAYLOAD_SIGN);
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders SignRequest component', () => {
expect(component.find('Connect(SignRequest)')).to.have.length(1);
});
});
describe('signTransaction', () => {
beforeEach(() => {
render(PAYLOAD_SIGNTX);
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders TransactionPending component', () => {
expect(component.find('Connect(TransactionPending)')).to.have.length(1);
});
});
});

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 './signRequest';

View File

@@ -0,0 +1,91 @@
/* 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 '../../_layout.css';
.container {
display: flex;
padding: 1.5em 1em 1.5em 0;
}
.actions, .signDetails {
vertical-align: middle;
min-height: $pendingHeight;
}
.signData {
border: 0.25em solid red;
margin-left: 2em;
padding: 0.5em;
overflow: auto;
max-height: 6em;
max-width: calc(100% - 2em);
}
.signData > p {
color: white;
}
.signDetails {
flex: 1;
overflow: auto;
}
.account img {
display: inline-block;
height: 50px;
margin: 5px;
width: 50px;
}
.address, .info {
box-sizing: border-box;
display: inline-block;
vertical-align: top;
}
.address {
width: 40%;
}
.info {
color: #E53935;
width: 60%;
}
.info p:first-child {
margin-top: 0;
}
/* TODO [todr] copy&paste from transactions */
.isConfirmed {
color: green;
}
.isRejected {
opacity: 0.7;
}
.txHash {
display: block;
word-break: break-all;
}
.actions {
display: inline-block;
min-height: $finishedHeight;
}

View File

@@ -0,0 +1,224 @@
// 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 { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import Account from '../Account';
import TransactionPendingForm from '../TransactionPendingForm';
import RequestOrigin from '../RequestOrigin';
import styles from './signRequest.css';
function isAscii (data) {
for (var i = 2; i < data.length; i += 2) {
let n = parseInt(data.substr(i, 2), 16);
if (n < 32 || n >= 128) {
return false;
}
}
return true;
}
@observer
class SignRequest extends Component {
static contextTypes = {
api: PropTypes.object
};
static propTypes = {
accounts: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
data: PropTypes.string.isRequired,
id: PropTypes.object.isRequired,
isFinished: PropTypes.bool.isRequired,
netVersion: PropTypes.string.isRequired,
signerStore: PropTypes.object.isRequired,
className: PropTypes.string,
focus: PropTypes.bool,
isSending: PropTypes.bool,
onConfirm: PropTypes.func,
onReject: PropTypes.func,
origin: PropTypes.any,
status: PropTypes.string
};
static defaultProps = {
focus: false,
origin: {
type: 'unknown',
details: ''
}
};
componentWillMount () {
const { address, signerStore } = this.props;
signerStore.fetchBalance(address);
}
render () {
const { className } = this.props;
return (
<div className={ `${styles.container} ${className}` }>
{ this.renderDetails() }
{ this.renderActions() }
</div>
);
}
renderAsciiDetails (ascii) {
return (
<div className={ styles.signData }>
<p>{ascii}</p>
</div>
);
}
renderBinaryDetails (data) {
return (
<div className={ styles.signData }>
<p>
<FormattedMessage
id='signer.signRequest.unknownBinary'
defaultMessage='(Unknown binary data)'
/>
</p>
</div>
);
}
renderDetails () {
const { api } = this.context;
const { address, data, netVersion, origin, signerStore } = this.props;
const { balances, externalLink } = signerStore;
const balance = balances[address];
if (!balance) {
return <div />;
}
return (
<div className={ styles.signDetails }>
<div className={ styles.address }>
<Account
address={ address }
balance={ balance }
className={ styles.account }
externalLink={ externalLink }
netVersion={ netVersion }
/>
<RequestOrigin origin={ origin } />
</div>
<div className={ styles.info } title={ api.util.sha3(data) }>
<p>
<FormattedMessage
id='signer.signRequest.request'
defaultMessage='A request to sign data using your account:'
/>
</p>
{
isAscii(data)
? this.renderAsciiDetails(api.util.hexToAscii(data))
: this.renderBinaryDetails(data)
}
<p>
<strong>
<FormattedMessage
id='signer.signRequest.warning'
defaultMessage='WARNING: This consequences of doing this may be grave. Confirm the request only if you are sure.'
/>
</strong>
</p>
</div>
</div>
);
}
renderActions () {
const { accounts, address, focus, isFinished, status } = this.props;
const account = accounts[address];
if (isFinished) {
if (status === 'confirmed') {
return (
<div className={ styles.actions }>
<span className={ styles.isConfirmed }>
<FormattedMessage
id='signer.signRequest.state.confirmed'
defaultMessage='Confirmed'
/>
</span>
</div>
);
}
return (
<div className={ styles.actions }>
<span className={ styles.isRejected }>
<FormattedMessage
id='signer.signRequest.state.rejected'
defaultMessage='Rejected'
/>
</span>
</div>
);
}
return (
<TransactionPendingForm
account={ account }
address={ address }
focus={ focus }
isSending={ this.props.isSending }
netVersion={ this.props.netVersion }
onConfirm={ this.onConfirm }
onReject={ this.onReject }
className={ styles.actions }
/>
);
}
onConfirm = (data) => {
const { id } = this.props;
const { password } = data;
this.props.onConfirm({ id, password });
}
onReject = () => {
this.props.onReject(this.props.id);
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
return {
accounts
};
}
export default connect(
mapStateToProps,
null
)(SignRequest);

View File

@@ -0,0 +1,72 @@
// 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 { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import SignRequest from './';
let component;
let reduxStore;
let signerStore;
function createSignerStore () {
return {
balances: {},
fetchBalance: sinon.stub()
};
}
function createReduxStore () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
personal: {
accounts: {}
}
};
}
};
}
function render () {
reduxStore = createReduxStore();
signerStore = createSignerStore();
component = shallow(
<SignRequest signerStore={ signerStore } />,
{
context: {
store: reduxStore
}
}
).find('SignRequest').shallow();
return component;
}
describe('views/Signer/components/SignRequest', () => {
beforeEach(() => {
render();
});
it('renders', () => {
expect(component).to.be.ok;
});
});

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 './transactionMainDetails';

View File

@@ -0,0 +1,80 @@
/* 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 '../../_layout.css';
.account {
text-align: center;
}
.contractIcon {
background: #eee;
width: 50px !important;
height: 50px !important;
box-sizing: border-box;
border-radius: 50%;
padding: 13px;
}
.editButtonRow {
text-align: right;
}
.from {
display: inline-block;
width: 40%;
vertical-align: top;
.account {
img {
display: inline-block;
width: 50px;
height: 50px;
margin: 5px;
}
span {
display: block;
}
}
}
.method {
display: inline-block;
width: 60%;
vertical-align: top;
line-height: 1em;
}
.tx {
position: relative;
text-align: center;
margin: 0 -75px;
width: 150px;
top: -20px;
white-space: nowrap;
}
.total {
font-size: 0.6em;
opacity: .5;
}
.transaction {
flex: 1;
overflow: auto;
}

View File

@@ -0,0 +1,201 @@
// 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, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import ReactTooltip from 'react-tooltip';
import { Button, MethodDecoding } from '~/ui';
import { GasIcon } from '~/ui/Icons';
import * as tUtil from '../util/transaction';
import Account from '../Account';
import RequestOrigin from '../RequestOrigin';
import styles from './transactionMainDetails.css';
export default class TransactionMainDetails extends Component {
static propTypes = {
children: PropTypes.node,
disabled: PropTypes.bool,
externalLink: PropTypes.string.isRequired,
from: PropTypes.string.isRequired,
fromBalance: PropTypes.object,
gasStore: PropTypes.object,
id: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired,
origin: PropTypes.any,
totalValue: PropTypes.object.isRequired,
transaction: PropTypes.object.isRequired,
value: PropTypes.object.isRequired
};
static defaultProps = {
origin: {
type: 'unknown',
details: ''
}
};
componentWillMount () {
const { totalValue, value } = this.props;
this.updateDisplayValues(value, totalValue);
}
componentWillReceiveProps (nextProps) {
const { totalValue, value } = nextProps;
this.updateDisplayValues(value, totalValue);
}
render () {
const { children, disabled, externalLink, from, fromBalance, gasStore, netVersion, transaction, origin } = this.props;
return (
<div className={ styles.transaction }>
<div className={ styles.from }>
<div className={ styles.account }>
<Account
address={ from }
balance={ fromBalance }
disabled={ disabled }
externalLink={ externalLink }
netVersion={ netVersion }
/>
</div>
<RequestOrigin origin={ origin } />
</div>
<div className={ styles.method }>
<MethodDecoding
address={ from }
historic={ false }
transaction={
gasStore
? gasStore.overrideTransaction(transaction)
: transaction
}
/>
{ this.renderEditTx() }
</div>
{ children }
</div>
);
}
renderEditTx () {
const { gasStore } = this.props;
if (!gasStore) {
return null;
}
return (
<div className={ styles.editButtonRow }>
<Button
icon={ <GasIcon /> }
label={
<FormattedMessage
id='signer.mainDetails.editTx'
defaultMessage='Edit conditions/gas/gasPrice'
/>
}
onClick={ this.toggleGasEditor }
/>
</div>
);
}
renderTotalValue () {
const { id } = this.props;
const { feeEth, totalValueDisplay, totalValueDisplayWei } = this.state;
const labelId = `totalValue${id}`;
return (
<div>
<div
className={ styles.total }
data-effect='solid'
data-for={ labelId }
data-place='bottom'
data-tip
>
{ totalValueDisplay } <small>ETH</small>
</div>
<ReactTooltip id={ labelId }>
<FormattedMessage
id='signer.mainDetails.tooltips.total1'
defaultMessage='The value of the transaction including the mining fee is {total} {type}.'
values={ {
total: <strong>{ totalValueDisplayWei }</strong>,
type: <small>WEI</small>
} }
/>
<br />
<FormattedMessage
id='signer.mainDetails.tooltips.total2'
defaultMessage='(This includes a mining fee of {fee} {token})'
values={ {
fee: <strong>{ feeEth }</strong>,
token: <small>ETH</small>
} }
/>
</ReactTooltip>
</div>
);
}
renderValue () {
const { id } = this.props;
const { valueDisplay, valueDisplayWei } = this.state;
const labelId = `value${id}`;
return (
<div>
<div
data-effect='solid'
data-for={ labelId }
data-tip
>
<strong>{ valueDisplay } </strong>
<small>ETH</small>
</div>
<ReactTooltip id={ labelId }>
<FormattedMessage
id='signer.mainDetails.tooltips.value1'
defaultMessage='The value of the transaction.'
/>
<br />
<strong>{ valueDisplayWei }</strong> <small>WEI</small>
</ReactTooltip>
</div>
);
}
updateDisplayValues (value, totalValue) {
this.setState({
feeEth: tUtil.calcFeeInEth(totalValue, value),
totalValueDisplay: tUtil.getTotalValueDisplay(totalValue),
totalValueDisplayWei: tUtil.getTotalValueDisplayWei(totalValue),
valueDisplay: tUtil.getValueDisplay(value),
valueDisplayWei: tUtil.getValueDisplayWei(value)
});
}
toggleGasEditor = () => {
this.props.gasStore.setEditing(true);
}
}

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 './transactionPending';

View File

@@ -0,0 +1,27 @@
/* 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 '../../_layout.css';
.container {
display: flex;
padding: 1.5em 1em 1.5em 0;
& > * {
vertical-align: middle;
}
}

View File

@@ -0,0 +1,202 @@
// 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 { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import HardwareStore from '~/mobx/hardwareStore';
import { Button, GasPriceEditor } from '~/ui';
import TransactionMainDetails from '../TransactionMainDetails';
import TransactionPendingForm from '../TransactionPendingForm';
import styles from './transactionPending.css';
import * as tUtil from '../util/transaction';
@observer
class TransactionPending extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
className: PropTypes.string,
date: PropTypes.instanceOf(Date).isRequired,
focus: PropTypes.bool,
gasLimit: PropTypes.object,
id: PropTypes.object.isRequired,
isSending: PropTypes.bool.isRequired,
netVersion: PropTypes.string.isRequired,
nonce: PropTypes.number,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
origin: PropTypes.any,
signerStore: PropTypes.object.isRequired,
transaction: PropTypes.shape({
condition: PropTypes.object,
data: PropTypes.string,
from: PropTypes.string.isRequired,
gas: PropTypes.object.isRequired,
gasPrice: PropTypes.object.isRequired,
to: PropTypes.string,
value: PropTypes.object.isRequired
}).isRequired
};
static defaultProps = {
focus: false,
origin: {
type: 'unknown',
details: ''
}
};
gasStore = new GasPriceEditor.Store(this.context.api, {
condition: this.props.transaction.condition,
gas: this.props.transaction.gas.toFixed(),
gasLimit: this.props.gasLimit,
gasPrice: this.props.transaction.gasPrice.toFixed()
});
hardwareStore = HardwareStore.get(this.context.api);
componentWillMount () {
const { signerStore, transaction } = this.props;
const { from, gas, gasPrice, to, value } = transaction;
const fee = tUtil.getFee(gas, gasPrice); // BigNumber object
const gasPriceEthmDisplay = tUtil.getEthmFromWeiDisplay(gasPrice);
const gasToDisplay = tUtil.getGasDisplay(gas);
const totalValue = tUtil.getTotalValue(fee, value);
this.setState({ gasPriceEthmDisplay, totalValue, gasToDisplay });
this.gasStore.setEthValue(value);
signerStore.fetchBalances([from, to]);
}
render () {
return this.gasStore.isEditing
? this.renderTxEditor()
: this.renderTransaction();
}
renderTransaction () {
const { accounts, className, focus, id, isSending, netVersion, origin, signerStore, transaction } = this.props;
const { totalValue } = this.state;
const { balances, externalLink } = signerStore;
const { from, value } = transaction;
const fromBalance = balances[from];
const account = accounts[from] || {};
const disabled = account.hardware && !this.hardwareStore.isConnected(from);
return (
<div className={ `${styles.container} ${className}` }>
<TransactionMainDetails
className={ styles.transactionDetails }
disabled={ disabled }
externalLink={ externalLink }
from={ from }
fromBalance={ fromBalance }
gasStore={ this.gasStore }
id={ id }
netVersion={ netVersion }
origin={ origin }
totalValue={ totalValue }
transaction={ transaction }
value={ value }
/>
<TransactionPendingForm
account={ account }
address={ from }
disabled={ disabled }
focus={ focus }
gasStore={ this.gasStore }
isSending={ isSending }
netVersion={ netVersion }
onConfirm={ this.onConfirm }
onReject={ this.onReject }
transaction={ transaction }
/>
</div>
);
}
renderTxEditor () {
const { className } = this.props;
return (
<div className={ `${styles.container} ${className}` }>
<GasPriceEditor store={ this.gasStore }>
<Button
label={
<FormattedMessage
id='signer.txPending.buttons.viewToggle'
defaultMessage='view transaction'
/>
}
onClick={ this.toggleGasEditor }
/>
</GasPriceEditor>
</div>
);
}
onConfirm = (data) => {
const { id, transaction } = this.props;
const { password, txSigned, wallet } = data;
const { condition, gas, gasPrice } = this.gasStore.overrideTransaction(transaction);
const options = {
gas,
gasPrice,
id,
password,
txSigned,
wallet
};
if (condition && (condition.block || condition.time)) {
options.condition = condition;
}
this.props.onConfirm(options);
}
onReject = () => {
this.props.onReject(this.props.id);
}
toggleGasEditor = () => {
this.gasStore.setEditing(false);
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
return {
accounts
};
}
export default connect(
mapStateToProps,
null
)(TransactionPending);

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 './transactionPendingFormConfirm';

View File

@@ -0,0 +1,51 @@
/* 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/>.
*/
.confirmForm {
margin-top: -2em;
}
.confirmButton {
display: block !important;
margin-bottom: 10px;
white-space: nowrap;
}
.signerIcon {
width: 24px;
height: 24px;
vertical-align: middle;
margin-left: 12px;
}
.passwordHint {
font-size: 0.75em;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 0.75em;
}
.passwordHint span {
opacity: 0.85;
}
.fileInput input {
top: 22px;
}
.qr {
margin-bottom: 0.5em;
text-align: center;
}

View File

@@ -0,0 +1,547 @@
// 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 keycode from 'keycode';
import RaisedButton from 'material-ui/RaisedButton';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { FormattedMessage } from 'react-intl';
import ReactTooltip from 'react-tooltip';
import { Form, Input, IdentityIcon, QrCode, QrScan } from '~/ui';
import { generateTxQr } from '~/util/qrscan';
import styles from './transactionPendingFormConfirm.css';
const QR_VISIBLE = 1;
const QR_SCAN = 2;
const QR_COMPLETED = 3;
export default class TransactionPendingFormConfirm extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
account: PropTypes.object,
address: PropTypes.string.isRequired,
disabled: PropTypes.bool,
focus: PropTypes.bool,
gasStore: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired,
isSending: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
transaction: PropTypes.object.isRequired
};
static defaultProps = {
account: {},
focus: false
};
id = Math.random(); // for tooltip
state = {
password: '',
qrState: QR_VISIBLE,
qr: {},
wallet: null,
walletError: null
}
componentDidMount () {
this.focus();
}
componentWillMount () {
this.readNonce();
this.subscribeNonce();
}
componentWillUnmount () {
this.unsubscribeNonce();
}
componentWillReceiveProps (nextProps) {
if (!this.props.focus && nextProps.focus) {
this.focus(nextProps);
}
}
/**
* Properly focus on the input element when needed.
* This might be fixed some day in MaterialUI with
* an autoFocus prop.
*
* @see https://github.com/callemall/material-ui/issues/5632
*/
focus (props = this.props) {
if (props.focus) {
const textNode = ReactDOM.findDOMNode(this.refs.input);
if (!textNode) {
return;
}
const inputNode = textNode.querySelector('input');
inputNode && inputNode.focus();
}
}
getPasswordHint () {
const { account } = this.props;
const accountHint = account.meta && account.meta.passwordHint;
if (accountHint) {
return accountHint;
}
const { wallet } = this.state;
const walletHint = wallet && wallet.meta && wallet.meta.passwordHint;
return walletHint || null;
}
// TODO: Now that we have 3 types, it would make sense splitting each into their own
// sub-module and having the consistent bits combined (e.g. i18n, layouts)
render () {
const { account, address, disabled, isSending } = this.props;
const { wallet, walletError } = this.state;
const isAccount = account.external || account.hardware || account.uuid;
const isWalletOk = isAccount || (walletError === null && wallet !== null);
const confirmText = this.renderConfirmButton();
const confirmButton = confirmText
? (
<div
data-effect='solid'
data-for={ `transactionConfirmForm${this.id}` }
data-place='bottom'
data-tip
>
<RaisedButton
className={ styles.confirmButton }
disabled={ disabled || isSending || !isWalletOk }
fullWidth
icon={
<IdentityIcon
address={ address }
button
className={ styles.signerIcon }
/>
}
label={ confirmText }
onTouchTap={ this.onConfirm }
primary
/>
</div>
)
: null;
return (
<div className={ styles.confirmForm }>
<Form>
{ this.renderKeyInput() }
{ this.renderQrCode() }
{ this.renderQrScanner() }
{ this.renderPassword() }
{ this.renderHint() }
{ confirmButton }
{ this.renderTooltip() }
</Form>
</div>
);
}
renderConfirmButton () {
const { account, isSending } = this.props;
const { qrState } = this.state;
if (account.external) {
switch (qrState) {
case QR_VISIBLE:
return (
<FormattedMessage
id='signer.txPendingConfirm.buttons.scanSigned'
defaultMessage='Scan Signed QR'
/>
);
case QR_SCAN:
case QR_COMPLETED:
return null;
}
}
return isSending
? (
<FormattedMessage
id='signer.txPendingConfirm.buttons.confirmBusy'
defaultMessage='Confirming...'
/>
)
: (
<FormattedMessage
id='signer.txPendingConfirm.buttons.confirmRequest'
defaultMessage='Confirm Request'
/>
);
}
renderPassword () {
const { account } = this.props;
const { password } = this.state;
if (account.hardware || account.external) {
return null;
}
const isAccount = account.uuid;
return (
<Input
hint={
isAccount
? (
<FormattedMessage
id='signer.txPendingConfirm.password.unlock.hint'
defaultMessage='unlock the account'
/>
)
: (
<FormattedMessage
id='signer.txPendingConfirm.password.decrypt.hint'
defaultMessage='decrypt the key'
/>
)
}
label={
isAccount
? (
<FormattedMessage
id='signer.txPendingConfirm.password.unlock.label'
defaultMessage='Account Password'
/>
)
: (
<FormattedMessage
id='signer.txPendingConfirm.password.decrypt.label'
defaultMessage='Key Password'
/>
)
}
onChange={ this.onModifyPassword }
onKeyDown={ this.onKeyDown }
ref='input'
type='password'
value={ password }
/>
);
}
renderHint () {
const { account, disabled, isSending } = this.props;
const { qrState } = this.state;
if (account.external) {
switch (qrState) {
case QR_VISIBLE:
return (
<div className={ styles.passwordHint }>
<FormattedMessage
id='signer.sending.external.scanTx'
defaultMessage='Please scan the transaction QR on your external device'
/>
</div>
);
case QR_SCAN:
return (
<div className={ styles.passwordHint }>
<FormattedMessage
id='signer.sending.external.scanSigned'
defaultMessage='Scan the QR code of the signed transaction from your external device'
/>
</div>
);
case QR_COMPLETED:
return null;
}
}
if (account.hardware) {
if (isSending) {
return (
<div className={ styles.passwordHint }>
<FormattedMessage
id='signer.sending.hardware.confirm'
defaultMessage='Please confirm the transaction on your attached hardware device'
/>
</div>
);
} else if (disabled) {
return (
<div className={ styles.passwordHint }>
<FormattedMessage
id='signer.sending.hardware.connect'
defaultMessage='Please attach your hardware device before confirming the transaction'
/>
</div>
);
}
}
const passwordHint = this.getPasswordHint();
if (!passwordHint) {
return null;
}
return (
<div className={ styles.passwordHint }>
<FormattedMessage
id='signer.txPendingConfirm.passwordHint'
defaultMessage='(hint) {passwordHint}'
values={ {
passwordHint
} }
/>
</div>
);
}
// TODO: Split into sub-scomponent
renderQrCode () {
const { account } = this.props;
const { qrState, qr } = this.state;
if (!account.external || qrState !== QR_VISIBLE || !qr.value) {
return null;
}
return (
<QrCode
className={ styles.qr }
value={ qr.value }
/>
);
}
// TODO: Split into sub-scomponent
renderQrScanner () {
const { account } = this.props;
const { qrState } = this.state;
if (!account.external || qrState !== QR_SCAN) {
return null;
}
return (
<QrScan
className={ styles.camera }
onScan={ this.onScanTx }
/>
);
}
renderKeyInput () {
const { account } = this.props;
const { walletError } = this.state;
if (account.uuid || account.wallet || account.hardware || account.external) {
return null;
}
return (
<Input
className={ styles.fileInput }
error={ walletError }
hint={
<FormattedMessage
id='signer.txPendingConfirm.selectKey.hint'
defaultMessage='The keyfile to use for this account'
/>
}
label={
<FormattedMessage
id='signer.txPendingConfirm.selectKey.label'
defaultMessage='Select Local Key'
/>
}
onChange={ this.onKeySelect }
type='file'
/>
);
}
renderTooltip () {
const { account } = this.props;
if (this.state.password.length || account.hardware || account.external) {
return null;
}
return (
<ReactTooltip id={ `transactionConfirmForm${this.id}` }>
<FormattedMessage
id='signer.txPendingConfirm.tooltips.password'
defaultMessage='Please provide a password for this account'
/>
</ReactTooltip>
);
}
onScanTx = (signature) => {
const { chainId, rlp, tx } = this.state.qr;
if (signature && signature.substr(0, 2) !== '0x') {
signature = `0x${signature}`;
}
this.setState({ qrState: QR_COMPLETED });
this.props.onConfirm({
txSigned: {
chainId,
rlp,
signature,
tx
}
});
}
onKeySelect = (event) => {
// Check that file have been selected
if (event.target.files.length === 0) {
return this.setState({
wallet: null,
walletError: null
});
}
const fileReader = new FileReader();
fileReader.onload = (e) => {
try {
const wallet = JSON.parse(e.target.result);
try {
if (wallet && typeof wallet.meta === 'string') {
wallet.meta = JSON.parse(wallet.meta);
}
} catch (e) {}
this.setState({
wallet,
walletError: null
});
} catch (error) {
this.setState({
wallet: null,
walletError: (
<FormattedMessage
id='signer.txPendingConfirm.errors.invalidWallet'
defaultMessage='Given wallet file is invalid.'
/>
)
});
}
};
fileReader.readAsText(event.target.files[0]);
}
onModifyPassword = (event) => {
const password = event.target.value;
this.setState({
password
});
}
onConfirm = () => {
const { account } = this.props;
const { password, qrState, wallet } = this.state;
if (account.external && qrState === QR_VISIBLE) {
return this.setState({ qrState: QR_SCAN });
}
this.props.onConfirm({
password,
wallet
});
}
generateTxQr = () => {
const { api } = this.context;
const { netVersion, gasStore, transaction } = this.props;
generateTxQr(api, netVersion, gasStore, transaction).then((qr) => {
this.setState({ qr });
});
}
onKeyDown = (event) => {
const codeName = keycode(event);
if (codeName !== 'enter') {
return;
}
this.onConfirm();
}
// FIXME: Sadly the API subscription channels currently does not allow for specific values,
// rather it can only do general queries where parameters are not specified. Hence we are
// polling for the nonce here. Since we are moving to node-based subscriptions on the API layer,
// this can be optimised when the subscription mechanism is reworked to conform.
subscribeNonce () {
const nonceTimerId = setInterval(this.readNonce, 1000);
this.setState({ nonceTimerId });
}
unsubscribeNonce () {
const { nonceTimerId } = this.state;
if (!nonceTimerId) {
return;
}
clearInterval(nonceTimerId);
}
readNonce = () => {
const { api } = this.context;
const { account } = this.props;
if (!account || !account.external || !api.transport.isConnected) {
return;
}
return api.parity
.nextNonce(account.address)
.then((nonce) => {
const { qr } = this.state;
if (!qr.nonce || !nonce.eq(qr.nonce)) {
this.generateTxQr();
}
});
}
}

View File

@@ -0,0 +1,138 @@
// 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 { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import TransactionPendingFormConfirm from './';
const ADDR_NORMAL = '0x0123456789012345678901234567890123456789';
const ADDR_WALLET = '0x1234567890123456789012345678901234567890';
const ADDR_HARDWARE = '0x2345678901234567890123456789012345678901';
const ADDR_SIGN = '0x3456789012345678901234567890123456789012';
const ACCOUNTS = {
[ADDR_NORMAL]: {
address: ADDR_NORMAL,
uuid: ADDR_NORMAL
},
[ADDR_WALLET]: {
address: ADDR_WALLET,
wallet: true
},
[ADDR_HARDWARE]: {
address: ADDR_HARDWARE,
hardware: true
}
};
let component;
let instance;
let onConfirm;
function render (address) {
onConfirm = sinon.stub();
component = shallow(
<TransactionPendingFormConfirm
account={ ACCOUNTS[address] }
address={ address }
onConfirm={ onConfirm }
isSending={ false }
/>
);
instance = component.instance();
return component;
}
describe('views/Signer/TransactionPendingFormConfirm', () => {
describe('normal accounts', () => {
beforeEach(() => {
render(ADDR_NORMAL);
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('does not render the key input', () => {
expect(instance.renderKeyInput()).to.be.null;
});
it('renders the password', () => {
expect(instance.renderPassword()).not.to.be.null;
});
});
describe('hardware accounts', () => {
beforeEach(() => {
render(ADDR_HARDWARE);
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('does not render the key input', () => {
expect(instance.renderKeyInput()).to.be.null;
});
it('does not render the password', () => {
expect(instance.renderPassword()).to.be.null;
});
});
describe('wallet accounts', () => {
beforeEach(() => {
render(ADDR_WALLET);
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('does not render the key input', () => {
expect(instance.renderKeyInput()).to.be.null;
});
it('renders the password', () => {
expect(instance.renderPassword()).not.to.be.null;
});
});
describe('signing accounts', () => {
beforeEach(() => {
render(ADDR_SIGN);
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders the key input', () => {
expect(instance.renderKeyInput()).not.to.be.null;
});
it('renders the password', () => {
expect(instance.renderPassword()).not.to.be.null;
});
it('renders the hint', () => {
expect(instance.renderHint()).to.be.null;
});
});
});

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 './transactionPendingFormReject';

View File

@@ -0,0 +1,26 @@
/* 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/>.
*/
/* the rejection button itself, once .reject has been pressed */
.rejectButton {
display: block !important;
margin-bottom: 5px;
}
.rejectText {
margin-bottom: 10px;
}

View File

@@ -0,0 +1,62 @@
// 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, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import RaisedButton from 'material-ui/RaisedButton';
import styles from './transactionPendingFormReject.css';
export default class TransactionPendingFormReject extends Component {
static propTypes = {
onReject: PropTypes.func.isRequired,
className: PropTypes.string
};
render () {
const { onReject } = this.props;
return (
<div>
<div className={ styles.rejectText }>
<FormattedMessage
id='signer.txPendingReject.info'
defaultMessage='Are you sure you want to reject request?'
/>
<br />
<strong>
<FormattedMessage
id='signer.txPendingReject.undone'
defaultMessage='This cannot be undone'
/>
</strong>
</div>
<RaisedButton
onTouchTap={ onReject }
className={ styles.rejectButton }
fullWidth
label={
<FormattedMessage
id='signer.txPendingReject.buttons.reject'
defaultMessage='Reject Request'
/>
}
/>
</div>
);
}
}

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 './transactionPendingForm';

View File

@@ -0,0 +1,44 @@
/* 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 '../../_layout.css';
.container {
box-sizing: border-box;
padding: 1em 0 0 2em;
flex: 0 0 $statusWidth;
}
.rejectToggle {
display: block;
cursor: pointer;
color: #00e;
opacity: .7;
transition: opacity .5s;
}
.rejectToggle:hover {
opacity: 1;
text-decoration: underline;
}
.rejectToggle svg {
position: relative;
width: 18px !important;
height: 18px !important;
top: 3px;
}

View File

@@ -0,0 +1,127 @@
// 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, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { PrevIcon } from '~/ui/Icons';
import TransactionPendingFormConfirm from './TransactionPendingFormConfirm';
import TransactionPendingFormReject from './TransactionPendingFormReject';
import styles from './transactionPendingForm.css';
export default class TransactionPendingForm extends Component {
static propTypes = {
account: PropTypes.object,
address: PropTypes.string.isRequired,
className: PropTypes.string,
disabled: PropTypes.bool,
focus: PropTypes.bool,
gasStore: PropTypes.object.isRequired,
netVersion: PropTypes.string.isRequired,
isSending: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
transaction: PropTypes.object.isRequired
};
static defaultProps = {
account: {},
focus: false
};
state = {
isRejectOpen: false
};
render () {
const { className } = this.props;
return (
<div className={ `${styles.container} ${className}` }>
{ this.renderForm() }
{ this.renderRejectToggle() }
</div>
);
}
renderForm () {
const { account, address, disabled, focus, gasStore, isSending, netVersion, onConfirm, onReject, transaction } = this.props;
if (this.state.isRejectOpen) {
return (
<TransactionPendingFormReject onReject={ onReject } />
);
}
return (
<TransactionPendingFormConfirm
address={ address }
account={ account }
disabled={ disabled }
focus={ focus }
gasStore={ gasStore }
netVersion={ netVersion }
isSending={ isSending }
onConfirm={ onConfirm }
transaction={ transaction }
/>
);
}
renderRejectToggle () {
const { isRejectOpen } = this.state;
let html;
if (!isRejectOpen) {
html = (
<span>
<FormattedMessage
id='signer.txPendingForm.reject'
defaultMessage='reject request'
/>
</span>
);
} else {
html = (
<span>
<PrevIcon />
<FormattedMessage
id='signer.txPendingForm.changedMind'
defaultMessage="I've changed my mind"
/>
</span>
);
}
return (
<a
className={ styles.rejectToggle }
onClick={ this.onToggleReject }
>
{ html }
</a>
);
}
onToggleReject = () => {
const { isRejectOpen } = this.state;
this.setState({
isRejectOpen: !isRejectOpen
});
}
}

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 './txHashLink';

View File

@@ -0,0 +1,42 @@
// 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, { Component, PropTypes } from 'react';
import { txLink } from '~/3rdparty/etherscan/links';
export default class TxHashLink extends Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
netVersion: PropTypes.string.isRequired,
txHash: PropTypes.string.isRequired
}
render () {
const { children, className, netVersion, txHash } = this.props;
return (
<a
className={ className }
href={ txLink(txHash, false, netVersion) }
target='_blank'
>
{ children || txHash }
</a>
);
}
}

View File

@@ -0,0 +1,38 @@
// 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/>.
const isLogging = process.env.LOGGING;
export default logger();
function logger () {
return isLogging ? devLogger() : prodLogger();
}
function prodLogger () {
return {
log: noop,
info: noop,
error: noop,
warn: noop
};
}
function devLogger () {
return console;
}
function noop () {}

View File

@@ -0,0 +1,21 @@
// 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 { isValidElement } from 'react';
export function isReactComponent (componentOrElem) {
return isValidElement(componentOrElem) && typeof componentOrElem.type === 'function';
}

View File

@@ -0,0 +1,115 @@
// 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';
const WEI_TO_ETH_MULTIPLIER = 0.000000000000000001;
const WEI_TO_SZABU_MULTIPLIER = 0.000000000001;
export const getShortData = _getShortData;
// calculations
export const getFee = _getFee;
export const calcFeeInEth = _calcFeeInEth;
export const getTotalValue = _getTotalValue;
// displays
export const getSzaboFromWeiDisplay = _getSzaboFromWeiDisplay;
export const getValueDisplay = _getValueDisplay;
export const getValueDisplayWei = _getValueDisplayWei;
export const getTotalValueDisplay = _getTotalValueDisplay;
export const getTotalValueDisplayWei = _getTotalValueDisplayWei;
export const getEthmFromWeiDisplay = _getEthmFromWeiDisplay;
export const getGasDisplay = _getGasDisplay;
function _getShortData (data) {
if (data.length <= 3) {
return data;
}
return data.substr(0, 3) + '...';
}
/*
* @param {hex string} gas
* @param {wei hex string} gasPrice
* @return {BigNumber} fee in wei
*/
function _getFee (gas, gasPrice) {
gas = new BigNumber(gas);
gasPrice = new BigNumber(gasPrice);
return gasPrice.times(gas);
}
function _calcFeeInEth (totalValue, value) {
let fee = new BigNumber(totalValue).sub(new BigNumber(value));
return fee.times(WEI_TO_ETH_MULTIPLIER).toFormat(7);
}
/*
* @param {wei BigNumber} fee
* @param {wei hex string} value
* @return {BigNumber} total value in wei
*/
function _getTotalValue (fee, value) {
value = new BigNumber(value);
return fee.plus(value);
}
/*
* @param {wei hex string} gasPrice
* @return {string} szabo gas price with unit [szabo] i.e. 21,423 [szabo]
*/
function _getSzaboFromWeiDisplay (gasPrice) {
gasPrice = new BigNumber(gasPrice);
return gasPrice.times(WEI_TO_SZABU_MULTIPLIER).toPrecision(5);
}
/*
* @param {wei hex string} value
* @return {string} value in WEI nicely formatted
*/
function _getValueDisplay (value) {
value = new BigNumber(value);
return value.times(WEI_TO_ETH_MULTIPLIER).toFormat(5);
}
function _getValueDisplayWei (value) {
value = new BigNumber(value);
return value.toFormat(0);
}
/*
* @param {wei hex string} totalValue
* @return {string} total value (including fee) with units i.e. 1.32 [eth]
*/
function _getTotalValueDisplay (totalValue) {
totalValue = new BigNumber(totalValue);
return totalValue.times(WEI_TO_ETH_MULTIPLIER).toFormat(5);
}
function _getTotalValueDisplayWei (totalValue) {
totalValue = new BigNumber(totalValue);
return totalValue.toFormat(0);
}
function _getEthmFromWeiDisplay (weiHexString) {
const value = new BigNumber(weiHexString);
return value.times(WEI_TO_ETH_MULTIPLIER).times(1e7).toFixed(5);
}
function _getGasDisplay (gas) {
return new BigNumber(gas).times(1e-7).toFormat(4);
}

View File

@@ -0,0 +1,79 @@
// 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 { getShortData, getFee, getTotalValue } from './transaction';
describe('views/Signer/components/util/transaction', () => {
describe('getEstimatedMiningTime', () => {
it('should return estimated mining time', () => {
});
});
describe('getShortData', () => {
it('should return short data', () => {
// given
const data = '0xh87dY78';
// when
const res = getShortData(data);
// then
expect(res).to.equal('0xh...');
});
it('should return data as is', () => {
// given
const data = '0x0';
// when
const shortData = getShortData(data);
// then
expect(shortData).to.equal('0x0');
});
});
describe('getFee', () => {
it('should return wei BigNumber object equals to gas * gasPrice', () => {
// given
const gas = '0x76c0'; // 30400
const gasPrice = '0x9184e72a000'; // 10000000000000 wei
// when
const fee = getFee(gas, gasPrice);
// then
expect(fee).to.be.an.instanceOf(BigNumber);
expect(fee.toString()).to.be.equal('304000000000000000'); // converting to string due to https://github.com/MikeMcl/bignumber.js/issues/11
});
});
describe('getTotalValue', () => {
it('should return wei BigNumber totalValue equals to value + fee', () => {
// given
const fee = new BigNumber(304000000000000000); // wei
const value = '0x9184e72a'; // 2441406250 wei
// when
const totalValue = getTotalValue(fee, value);
// then
expect(totalValue).to.be.an.instanceOf(BigNumber);
expect(totalValue.toString()).to.be.equal('304000002441406250'); // converting to string due to https://github.com/MikeMcl/bignumber.js/issues/11
});
});
});

View File

@@ -0,0 +1,35 @@
// 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 function toPromise (fn) {
return new Promise((resolve, reject) => {
fn((err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
}
export function identity (x) {
return x;
}
export function capitalize (str) {
return str[0].toUpperCase() + str.slice(1).toLowerCase();
}

View File

@@ -0,0 +1,107 @@
// 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 { isEqual } from 'lodash';
import { action, observable } from 'mobx';
export default class SignerStore {
@observable balances = {};
@observable localHashes = [];
externalLink = '';
constructor (api, withLocalTransactions = false, externalLink = '') {
this._api = api;
this._timeoutId = 0;
this.externalLink = externalLink;
if (withLocalTransactions) {
this.fetchLocalTransactions();
}
}
@action setBalance = (address, balance) => {
this.setBalances({ [address]: balance });
}
@action setBalances = (balances) => {
this.balances = Object.assign({}, this.balances, balances);
}
@action setLocalHashes = (localHashes = []) => {
// Use slice to make sure they are both Arrays (MobX uses Objects for Observable Arrays)
if (!isEqual(localHashes.slice(), this.localHashes.slice())) {
this.localHashes = localHashes;
}
}
@action unsubscribe () {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
}
}
fetchBalance (address) {
this._api.eth
.getBalance(address)
.then((balance) => {
this.setBalance(address, balance);
})
.catch((error) => {
console.warn('Store:fetchBalance', error);
});
}
fetchBalances (_addresses) {
const addresses = _addresses.filter((address) => address) || [];
if (!addresses.length) {
return;
}
Promise
.all(addresses.map((address) => this._api.eth.getBalance(address)))
.then((_balances) => {
this.setBalances(
addresses.reduce((balances, address, index) => {
balances[address] = _balances[index];
return balances;
}, {})
);
})
.catch((error) => {
console.warn('Store:fetchBalances', error);
});
}
fetchLocalTransactions = () => {
const nextTimeout = () => {
this._timeoutId = setTimeout(this.fetchLocalTransactions, 1500);
};
this._api.parity
.localTransactions()
.then((localTransactions) => {
const keys = Object
.keys(localTransactions)
.filter((key) => localTransactions[key].status !== 'canceled');
this.setLocalHashes(keys);
})
.then(nextTimeout)
.catch(nextTimeout);
}
}

View File

@@ -0,0 +1,19 @@
// 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 const isExtension = () => {
return window.location.protocol.indexOf('chrome-extension:') > -1;
};

View File

@@ -0,0 +1,19 @@
// 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 function identity (x) {
return x;
}

108
js/src/shell/embed.js Normal file
View File

@@ -0,0 +1,108 @@
// 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 'babel-polyfill';
import 'whatwg-fetch';
import es6Promise from 'es6-promise';
es6Promise.polyfill();
import React from 'react';
import ReactDOM from 'react-dom';
import injectTapEventPlugin from 'react-tap-event-plugin';
import SecureApi from '~/secureApi';
import ContractInstances from '~/contracts';
import { initStore } from '~/redux';
import ContextProvider from '~/ui/ContextProvider';
import muiTheme from '~/ui/Theme';
import { patchApi } from '~/util/tx';
import '~/environment';
import '~/../assets/fonts/Roboto/font.css';
import '~/../assets/fonts/RobotoMono/font.css';
injectTapEventPlugin();
import ParityBar from '~/shell/ParityBar';
// Test transport (std transport should be provided as global object)
class FakeTransport {
constructor () {
console.warn('Secure Transport not provided. Falling back to FakeTransport');
}
execute (method, ...params) {
console.log('Calling', method, params);
return Promise.reject('not connected');
}
on () {
}
}
class FrameSecureApi extends SecureApi {
constructor (transport) {
super('', null, () => {
return transport;
});
}
connect () {
// Do nothing - this API does not need connecting
this.emit('connecting');
// Fire connected event with some delay.
setTimeout(() => {
this.emit('connected');
});
}
needsToken () {
return false;
}
isConnecting () {
return false;
}
isConnected () {
return true;
}
}
const api = new FrameSecureApi(window.secureTransport || new FakeTransport());
patchApi(api);
ContractInstances.get(api);
const store = initStore(api, null, true);
window.secureApi = api;
ReactDOM.render(
<ContextProvider
api={ api }
muiTheme={ muiTheme }
store={ store }
>
<ParityBar dapp externalLink={ 'http://127.0.0.1:8180' } />
</ContextProvider>,
document.querySelector('#container')
);

38
js/src/shell/index.ejs Normal file
View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title><%= htmlWebpackPlugin.options.title %></title>
<style>
html {
background: white;
background-repeat: round;
}
html, body, #container {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
font-size: 16px;
font-weight: 300;
}
.loading {
text-align: center;
padding-top: 5em;
font-size: 2em;
color: #999;
}
</style>
</head>
<body>
<div id="container">
<div class="loading">Loading</div>
</div>
<script src="vendor.js"></script>
</body>
</html>

103
js/src/shell/index.js Normal file
View File

@@ -0,0 +1,103 @@
// 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 'babel-polyfill';
import 'whatwg-fetch';
import es6Promise from 'es6-promise';
es6Promise.polyfill();
import React from 'react';
import ReactDOM from 'react-dom';
import injectTapEventPlugin from 'react-tap-event-plugin';
import { IndexRoute, Redirect, Route, Router, hashHistory } from 'react-router';
import qs from 'querystring';
import SecureApi from '~/secureApi';
import ContractInstances from '~/contracts';
import { initStore } from '~/redux';
import ContextProvider from '~/ui/ContextProvider';
import muiTheme from '~/ui/Theme';
import { patchApi } from '~/util/tx';
import Application from './Application';
import Dapp from './Dapp';
import Dapps from './Dapps';
import styles from '~/reset.css';
import '~/environment';
import '~/../assets/fonts/Roboto/font.css';
import '~/../assets/fonts/RobotoMono/font.css';
injectTapEventPlugin();
if (process.env.NODE_ENV === 'development') {
// Expose the React Performance Tools on the`window` object
const Perf = require('react-addons-perf');
window.Perf = Perf;
}
const AUTH_HASH = '#/auth?';
const parityUrl = process.env.PARITY_URL || window.location.host;
const urlScheme = window.location.href.match(/^https/) ? 'wss://' : 'ws://';
let token = null;
if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
token = qs.parse(window.location.hash.substr(AUTH_HASH.length)).token;
}
const api = new SecureApi(`${urlScheme}${parityUrl}`, token);
patchApi(api);
ContractInstances.get(api);
const store = initStore(api, hashHistory);
window.secureApi = api;
import HistoryStore from '~/mobx/historyStore';
import builtinDapps from '~/config/dappsBuiltin.json';
import viewsDapps from '~/config/dappsViews.json';
const dapps = [].concat(viewsDapps, builtinDapps);
const dappsHistory = HistoryStore.get('dapps');
function onEnterDapp ({ params }) {
if (!dapps[params.id] || !dapps[params.id].skipHistory) {
dappsHistory.add(params.id);
}
}
ReactDOM.render(
<ContextProvider api={ api } muiTheme={ muiTheme } store={ store }>
<Router className={ styles.reset } history={ hashHistory }>
<Route path='/' component={ Application }>
<Redirect from='/auth' to='/' />
<Route path='/:id' component={ Dapp } onEnter={ onEnterDapp } />
<Route path='/:id/:details' component={ Dapp } onEnter={ onEnterDapp } />
<IndexRoute component={ Dapps } />
</Route>
</Router>
</ContextProvider>,
document.querySelector('#container')
);

19
js/src/shell/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "@parity/shell",
"description": "Parity UI shell",
"version": "0.0.0",
"main": "index.js",
"author": "Parity Team <admin@parity.io>",
"maintainers": [],
"contributors": [],
"license": "GPL-3.0",
"repository": {
"type": "git",
"url": "git+https://github.com/paritytech/parity.git"
},
"keywords": [],
"scripts": {},
"devDependencies": {},
"dependencies": {},
"peerDependencies": {}
}