Unverified Commit 4cdfca0a authored by Amaury Martiny's avatar Amaury Martiny Committed by GitHub
Browse files

Merge pull request #336 from paritytech/ac-signer

Integrate Parity Signer into Fether
parents c7a90537 bfc3894c
Pipeline #28374 passed with stages
in 12 minutes and 6 seconds
......@@ -35,14 +35,18 @@
},
"dependencies": {
"@craco/craco": "^3.2.3",
"@parity/api": "^3.0.11",
"@parity/contracts": "^3.0.11",
"@parity/light.js": "^3.0.11",
"@parity/light.js-react": "^3.0.11",
"@parity/abi": "^3.0.25",
"@parity/api": "^3.0.25",
"@parity/contracts": "^3.0.25",
"@parity/light.js": "^3.0.25",
"@parity/light.js-react": "^3.0.25",
"@parity/qr-signer": "^0.3.2",
"bignumber.js": "^8.0.1",
"bip39": "^2.5.0",
"debounce-promise": "^3.1.0",
"debug": "^4.1.0",
"ethereumjs-tx": "^1.3.7",
"ethereumjs-util": "^6.0.0",
"ethereumjs-wallet": "^0.6.2",
"fether-ui": "^0.2.0",
"file-saver": "^2.0.0",
......
......@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="default-src 'self';script-src file: http: 'unsafe-inline'; connect-src file: http: https: ws: wss:;img-src 'self' 'unsafe-inline' file: data: blob: http: https:;style-src 'unsafe-inline' file:;">
<meta http-equiv="Content-Security-Policy" content="default-src 'self';script-src file: http: blob: 'unsafe-inline'; connect-src file: http: https: ws: wss:;img-src 'self' 'unsafe-inline' file: data: blob: http: https:;style-src 'unsafe-inline' file:;">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
......
......@@ -5,17 +5,19 @@
import React, { Component } from 'react';
import { AccountCard, Clickable, Header } from 'fether-ui';
import { accountsInfo$, withoutLoading } from '@parity/light.js';
import { chainId$, withoutLoading } from '@parity/light.js';
import { inject, observer } from 'mobx-react';
import light from '@parity/light.js-react';
import Health from '../../Health';
import Feedback from './Feedback';
import withAccountsInfo from '../../utils/withAccountsInfo';
@withAccountsInfo
@inject('createAccountStore', 'parityStore')
@light({
accountsInfo: () => accountsInfo$().pipe(withoutLoading())
chainId: () => chainId$().pipe(withoutLoading())
})
@inject('createAccountStore', 'parityStore')
@observer
class AccountsList extends Component {
handleClick = ({
......@@ -34,9 +36,13 @@ class AccountsList extends Component {
};
render () {
const { accountsInfo } = this.props;
const { accountsInfo, chainId } = this.props;
const accountsList = Object.keys(accountsInfo);
const accountsList = Object.keys(accountsInfo).filter(
key =>
!accountsInfo[key].chainId ||
accountsInfo[key].chainId === parseInt(chainId, 10)
);
const accountsListLength = accountsList && accountsList.length;
return (
......@@ -64,13 +70,8 @@ class AccountsList extends Component {
<AccountCard
address={address}
className='-clickable'
name={
accountsInfo &&
accountsInfo[address] &&
(accountsInfo[address].name
? accountsInfo[address].name
: '(No name)')
}
type={accountsInfo[address].type}
name={accountsInfo[address].name || '(no name)'}
shortAddress
/>
</li>
......
......@@ -5,20 +5,20 @@
import React, { Component } from 'react';
import { addressShort, Card, Form as FetherForm } from 'fether-ui';
import { accounts$, withoutLoading } from '@parity/light.js';
import light from '@parity/light.js-react';
import { inject, observer } from 'mobx-react';
@light({
accounts: () => accounts$().pipe(withoutLoading())
})
import Scanner from '../../../Scanner';
import withAccountsInfo from '../../../utils/withAccountsInfo';
@withAccountsInfo
@inject('createAccountStore')
@observer
class AccountImportOptions extends Component {
state = {
error: '',
isLoading: false,
phrase: ''
phrase: '',
importingFromSigner: false
};
handleNextStep = async () => {
......@@ -87,11 +87,42 @@ class AccountImportOptions extends Component {
}
};
hasExistingAddressForImport = addressForImport => {
const { accounts } = this.props;
const isExistingAddress = accounts
.map(address => address && address.toLowerCase())
.includes(addressForImport.toLowerCase());
handleSignerImported = async ({ address, chainId: chainIdString }) => {
const {
createAccountStore: { importFromSigner }
} = this.props;
if (!address || !chainIdString) {
this.setState({ error: 'Invalid QR code.' });
return;
}
const chainId = parseInt(chainIdString);
if (this.hasExistingAddressForImport(address, chainId)) {
return;
}
await importFromSigner({ address, chainId });
this.handleNextStep();
};
handleSignerImport = () => {
this.setState({
importingFromSigner: true
});
};
hasExistingAddressForImport = (addressForImport, chainId) => {
const { accountsInfo } = this.props;
const isExistingAddress = Object.keys(accountsInfo).some(
key =>
key.toLowerCase() === addressForImport.toLowerCase() &&
(!accountsInfo[key].chainId ||
!chainId ||
accountsInfo[key].chainId === chainId)
);
if (isExistingAddress) {
this.setState({
......@@ -108,46 +139,76 @@ class AccountImportOptions extends Component {
history,
location: { pathname }
} = this.props;
const { error, phrase } = this.state;
const { error, importingFromSigner, phrase } = this.state;
const currentStep = pathname.slice(-1);
const jsonCard = (
<div key='createAccount'>
<div className='text -centered'>
<p> Recover from JSON Keyfile </p>
<FetherForm.InputFile
label='JSON Backup Keyfile'
onChangeFile={this.handleChangeFile}
required
/>
<Card>
<div key='createAccount'>
<div className='text -centered'>
<p>Recover from JSON Keyfile</p>
<FetherForm.InputFile
label='JSON Backup Keyfile'
onChangeFile={this.handleChangeFile}
required
/>
</div>
</div>
</div>
</Card>
);
const signerCard = (
<Card>
<div key='createAccount'>
<div className='text -centered'>
<p>Recover from Parity Signer</p>
{importingFromSigner ? (
<Scanner
onScan={this.handleSignerImported}
label='Please show the QR code of the account on the webcam.'
/>
) : (
<button
className='button -footer'
onClick={this.handleSignerImport}
>
Scan QR code
</button>
)}
</div>
</div>
</Card>
);
const phraseCard = (
<div key='importBackup'>
<div className='text -centered'>
<p>Recover from Seed Phrase</p>
<FetherForm.Field
as='textarea'
label='Recovery phrase'
onChange={this.handlePhraseChange}
required
phrase={phrase}
/>
{this.renderButton()}
<Card>
<div key='importBackup'>
<div className='text -centered'>
<p>Recover from Seed Phrase</p>
<FetherForm.Field
as='textarea'
label='Recovery phrase'
onChange={this.handlePhraseChange}
required
phrase={phrase}
/>
{this.renderButton()}
</div>
</div>
</div>
</Card>
);
return (
<div className='center-md'>
<Card> {jsonCard} </Card>
{!importingFromSigner && jsonCard}
<br />
{signerCard}
<br />
<Card> {phraseCard} </Card>
{!importingFromSigner && phraseCard}
<br />
<p>{error}</p>
<nav className='form-nav -space-around'>
......
......@@ -26,13 +26,32 @@ class AccountName extends Component {
handleSubmit = () => {
const {
createAccountStore,
history,
location: { pathname }
} = this.props;
const currentStep = pathname.slice(-1);
history.push(`/accounts/new/${+currentStep + 1}`);
if (createAccountStore.noPrivateKey) {
// Save Signer account to Parity without asking for a password
createAccountStore
.saveAccountToParity()
.then(res => {
createAccountStore.clear();
history.push('/accounts');
})
.catch(err => {
console.error(err);
this.setState({
error: err.text
});
});
} else {
// Ask for a password otherwise
history.push(`/accounts/new/${+currentStep + 1}`);
}
};
render () {
......@@ -45,12 +64,13 @@ class AccountName extends Component {
renderCardWhenImported = () => {
const {
createAccountStore: { address, name }
createAccountStore: { address, name, noPrivateKey }
} = this.props;
return (
<AccountCard
address={address}
type={noPrivateKey ? 'signer' : 'node'}
drawers={[this.renderDrawer()]}
name={name || '(no name)'}
/>
......@@ -89,6 +109,7 @@ class AccountName extends Component {
renderDrawer = () => {
const {
createAccountStore: { address, name },
error,
history,
location: { pathname }
} = this.props;
......@@ -107,6 +128,7 @@ class AccountName extends Component {
type='text'
value={name}
/>
{error && <p>{error}</p>}
<nav className='form-nav -space-around'>
{currentStep > 1 && (
<button
......
......@@ -29,7 +29,7 @@ class AccountPassword extends Component {
const { createAccountStore, history } = this.props;
const { confirm, password } = this.state;
event && event.preventDefault();
event.preventDefault();
if (!createAccountStore.jsonString && confirm !== password) {
this.setState({
......
......@@ -4,10 +4,8 @@
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import { accountsInfo$ } from '@parity/light.js';
import { Header } from 'fether-ui';
import { inject, observer } from 'mobx-react';
import light from '@parity/light.js-react';
import { Link, Route } from 'react-router-dom';
import AccountCopyPhrase from './AccountCopyPhrase';
......@@ -16,9 +14,10 @@ import AccountRewritePhrase from './AccountRewritePhrase';
import AccountName from './AccountName';
import AccountPassword from './AccountPassword';
import Health from '../../Health';
import withAccountsInfo from '../../utils/withAccountsInfo';
@light({ accountsInfo: accountsInfo$ })
@inject('createAccountStore')
@withAccountsInfo
@observer
class CreateAccount extends Component {
constructor (props) {
......@@ -45,7 +44,6 @@ class CreateAccount extends Component {
} = this.props;
createAccountStore.clear();
createAccountStore.setIsImport(!createAccountStore.isImport);
// If we were further in the account creation, go back to step 1
if (step > 1) {
history.push('/accounts/new/1');
......
......@@ -6,8 +6,6 @@
import React, { Component } from 'react';
import { AccountHeader, Card, Form as FetherForm } from 'fether-ui';
import { observer } from 'mobx-react';
import { accountsInfo$ } from '@parity/light.js';
import light from '@parity/light.js-react';
import { Link, withRouter } from 'react-router-dom';
import backupAccount from '../utils/backupAccount';
......@@ -15,9 +13,6 @@ import withAccount from '../utils/withAccount';
@withRouter
@withAccount
@light({
accountsInfo: accountsInfo$
})
@observer
class BackupAccount extends Component {
state = {
......@@ -31,14 +26,17 @@ class BackupAccount extends Component {
};
handleSubmit = event => {
const { accountAddress, history } = this.props;
const {
account: { address },
history
} = this.props;
const { password } = this.state;
event && event.preventDefault();
this.setState({ isLoading: true });
backupAccount(accountAddress, password)
backupAccount(address, password)
.then(res => {
/*
FIXME: this timeout is a placeholder for after the backup file is saved.
......@@ -58,23 +56,18 @@ class BackupAccount extends Component {
render () {
const {
accountsInfo,
history,
location: { pathname }
account: { name, address, type },
history
} = this.props;
const { isLoading, message, password } = this.state;
const accountAddress = pathname.slice(-42);
return (
<div>
<AccountHeader
address={accountAddress}
address={address}
copyAddress
name={
accountsInfo &&
accountsInfo[accountAddress] &&
accountsInfo[accountAddress].name
}
name={name}
type={type}
left={
<Link to='/accounts' className='icon -back'>
Back
......
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
import React from 'react';
import QrSigner from '@parity/qr-signer';
import loading from '../assets/img/icons/loading.svg';
export default class Scanner extends React.PureComponent {
state = {
webcamError: null,
isLoading: true
};
componentDidMount () {
this.checkForWebcam();
if (navigator.mediaDevices) {
navigator.mediaDevices.addEventListener(
'devicechange',
this.checkForWebcam
);
}
}
componentWillUnmount () {
if (navigator.mediaDevices && navigator.mediaDevices.ondevicechange) {
navigator.mediaDevices.removeEventListener(
'devicechange',
this.checkForWebcam
);
}
}
checkForWebcam = async () => {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
await navigator.mediaDevices.getUserMedia({ video: true });
this.setState({
webcamError: null,
isLoading: false
});
} catch (e) {
let errorMessage;
switch (e.name) {
case 'NotAllowedError':
case 'SecurityError':
errorMessage = 'Access to the webcam was refused.';
break;
case 'NotFoundError':
case 'OverconstrainedError':
errorMessage = 'No webcam found on the device.';
break;
case 'NotReadableError':
errorMessage =
'Webcam hardware error. Try restarting your computer';
break;
default:
errorMessage = 'Unknown error.';
}
this.setState({
webcamError: errorMessage,
isLoading: false
});
}
}
};
render () {
const { onScan, label } = this.props;
const { webcamError, isLoading } = this.state;
const size = 300;
return (
<div>
{isLoading ? (
<img alt='loading' src={loading} />
) : webcamError ? (
<p>{webcamError}</p>
) : (
<div>
<p>{label}</p>
<br />
<QrSigner scan onScan={onScan} size={size} />
</div>
)}
</div>
);
}
}
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
import Scanner from './Scanner';
export default Scanner;
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import { Card, Header } from 'fether-ui';
import { inject, observer } from 'mobx-react';
import { Link, Redirect } from 'react-router-dom';
import Scanner from '../../Scanner';
import { withProps } from 'recompose';
import RequireHealth from '../../RequireHealthOverlay';
import withAccount from '../../utils/withAccount.js';
import withTokens from '../../utils/withTokens';
@inject('sendStore')
@withAccount
@withTokens
@withProps(({ match: { params: { tokenAddress } }, tokens }) => ({
token: tokens[tokenAddress]
}))
@observer
class ScanSignedTx extends Component {
state = {
error: null
};
onScanSignedTx = signature => {
const {
account: { address: accountAddress },
history,
sendStore: { signRaw },
token
} = this.props;