Allow signing via Qr (#4881)
* QR code scanning in-place * QrScan component * Update tests * s/store/createStore/ in props * Create of actual accountsInfo entry * Exteral/hardware forget, no password change * Add external accounts to accounts list * Render external accounts in section (along with hw) * Manul test bugfixes * Display Qr code for tx signing * Align QR code * Hints for QR operations * Generate actual qr codes based on tx data * Add leading 0x if not found * Update tests for 0x leading addition * from & rpl without leading 0x * Auto-detect QR code size (input length) * Confirm raw * WIP (lots of logging) * WIP * Chain-replay protection * Readability * Re-add r: chainId * s = 0, r = 0, v = chainId * Update eth_signTransaction to send transaction object * And it actually works. * Externalise createUnsigned/createSigned * Check for nonce updates (future: subscriptions) * Allow gas overrides * Expose formatted condition * Extract calculation (cap at 40) * Remove debug log * Fix rename linting * Allow for signing hash & rlp (App support forthcoming) * WIP * User original qrcode-generator package * Complete hash + rlp signing * Accurate QR code size calculation * Simplify type calculation * R-eactivate current mobile interface (TODO for new) * Move napa to dep * Allow external accounts visibility in dapps * Allow napa install on CI * Allow new signTransaction & signTransactionHash functionality
This commit is contained in:
parent
cbaa7fdee6
commit
1987dad527
@ -26,6 +26,7 @@
|
||||
"Promise"
|
||||
],
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"analize": "npm run analize:lib && npm run analize:dll && npm run analize:app",
|
||||
"analize:app": "WPANALIZE=1 webpack --config webpack/app --json > .build/analize.app.json && cat .build/analize.app.json | webpack-bundle-size-analyzer",
|
||||
"analize:lib": "WPANALIZE=1 webpack --config webpack/libraries --json > .build/analize.lib.json && cat .build/analize.lib.json | webpack-bundle-size-analyzer",
|
||||
@ -63,6 +64,9 @@
|
||||
"test:npm": "(cd .npmjs && npm i) && node test/npmParity && node test/npmJsonRpc && (rm -rf .npmjs/node_modules)",
|
||||
"prepush": "npm run lint:cached"
|
||||
},
|
||||
"napa": {
|
||||
"qrcode-generator": "kazuhikoarase/qrcode-generator"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "6.23.0",
|
||||
"babel-core": "6.23.1",
|
||||
@ -182,10 +186,10 @@
|
||||
"mobx-react": "4.0.3",
|
||||
"mobx-react-devtools": "4.2.10",
|
||||
"moment": "2.17.0",
|
||||
"napa": "2.3.0",
|
||||
"phoneformat.js": "1.0.3",
|
||||
"promise-worker": "1.1.1",
|
||||
"push.js": "0.0.11",
|
||||
"qrcode-npm": "0.0.3",
|
||||
"qs": "6.3.0",
|
||||
"react": "15.4.2",
|
||||
"react-ace": "4.1.0",
|
||||
@ -198,6 +202,7 @@
|
||||
"react-intl": "2.1.5",
|
||||
"react-markdown": "2.4.4",
|
||||
"react-portal": "3.0.0",
|
||||
"react-qr-reader": "1.0.3",
|
||||
"react-redux": "4.4.6",
|
||||
"react-router": "3.0.0",
|
||||
"react-router-redux": "4.0.7",
|
||||
|
@ -288,9 +288,9 @@ export default class Eth {
|
||||
.execute('eth_sign', inAddress(address), inHash(hash));
|
||||
}
|
||||
|
||||
signTransaction () {
|
||||
signTransaction (options) {
|
||||
return this._transport
|
||||
.execute('eth_signTransaction');
|
||||
.execute('eth_signTransaction', inOptions(options));
|
||||
}
|
||||
|
||||
submitHashrate (hashrate, clientId) {
|
||||
|
@ -25,11 +25,11 @@ import styles from '../createAccount.css';
|
||||
@observer
|
||||
export default class AccountDetails extends Component {
|
||||
static propTypes = {
|
||||
store: PropTypes.object.isRequired
|
||||
createStore: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { address, description, name } = this.props.store;
|
||||
const { address, description, name } = this.props.createStore;
|
||||
|
||||
return (
|
||||
<div className={ styles.details }>
|
||||
@ -79,7 +79,7 @@ export default class AccountDetails extends Component {
|
||||
}
|
||||
|
||||
renderPhrase () {
|
||||
const { phrase } = this.props.store;
|
||||
const { phrase } = this.props.createStore;
|
||||
|
||||
if (!phrase) {
|
||||
return null;
|
||||
|
@ -28,7 +28,7 @@ function render () {
|
||||
store = createStore();
|
||||
component = shallow(
|
||||
<AccountDetails
|
||||
store={ store }
|
||||
createStore={ store }
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -26,11 +26,11 @@ import styles from '../createAccount.css';
|
||||
@observer
|
||||
export default class AccountDetailsGeth extends Component {
|
||||
static propTypes = {
|
||||
store: PropTypes.object.isRequired
|
||||
createStore: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { gethAccountsAvailable, gethImported } = this.props.store;
|
||||
const { gethAccountsAvailable, gethImported } = this.props.createStore;
|
||||
|
||||
const accounts = gethAccountsAvailable.filter((account) => gethImported.includes(account.address));
|
||||
|
||||
|
@ -28,7 +28,7 @@ function render () {
|
||||
store = createStore();
|
||||
component = shallow(
|
||||
<AccountDetailsGeth
|
||||
store={ store }
|
||||
createStore={ store }
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -22,13 +22,13 @@ import { VaultSelect } from '~/ui';
|
||||
@observer
|
||||
export default class ChangeVault extends Component {
|
||||
static propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
createStore: PropTypes.object.isRequired,
|
||||
vaultStore: PropTypes.object
|
||||
}
|
||||
|
||||
render () {
|
||||
const { store, vaultStore } = this.props;
|
||||
const { vaultName } = store;
|
||||
const { createStore, vaultStore } = this.props;
|
||||
const { vaultName } = createStore;
|
||||
|
||||
if (!vaultStore || vaultStore.vaultsOpened.length === 0) {
|
||||
return null;
|
||||
@ -44,8 +44,8 @@ export default class ChangeVault extends Component {
|
||||
}
|
||||
|
||||
onSelect = (vaultName) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setVaultName(vaultName);
|
||||
createStore.setVaultName(vaultName);
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ function createVaultStore () {
|
||||
function render () {
|
||||
component = shallow(
|
||||
<ChangeVault
|
||||
store={ createStore() }
|
||||
createStore={ createStore() }
|
||||
vaultStore={ createVaultStore() }
|
||||
/>
|
||||
);
|
||||
|
@ -54,6 +54,21 @@ const TYPES = [
|
||||
),
|
||||
key: 'fromPhrase'
|
||||
},
|
||||
{
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id='createAccount.creationType.fromQr.description'
|
||||
defaultMessage='Attach an externally managed account via QR code'
|
||||
/>
|
||||
),
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='createAccount.creationType.fromQr.label'
|
||||
defaultMessage='External Account'
|
||||
/>
|
||||
),
|
||||
key: 'fromQr'
|
||||
},
|
||||
{
|
||||
description: (
|
||||
<FormattedMessage
|
||||
@ -84,21 +99,6 @@ const TYPES = [
|
||||
),
|
||||
key: 'fromJSON'
|
||||
},
|
||||
{
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id='createAccount.creationType.fromPresale.description'
|
||||
defaultMessage='Import an Ethereum presale wallet file with the original password'
|
||||
/>
|
||||
),
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='createAccount.creationType.fromPresale.label'
|
||||
defaultMessage='Presale wallet'
|
||||
/>
|
||||
),
|
||||
key: 'fromPresale'
|
||||
},
|
||||
{
|
||||
description: (
|
||||
<FormattedMessage
|
||||
@ -113,17 +113,32 @@ const TYPES = [
|
||||
/>
|
||||
),
|
||||
key: 'fromRaw'
|
||||
},
|
||||
{
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id='createAccount.creationType.fromPresale.description'
|
||||
defaultMessage='Import an Ethereum presale wallet file with the original password'
|
||||
/>
|
||||
),
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='createAccount.creationType.fromPresale.label'
|
||||
defaultMessage='Presale wallet'
|
||||
/>
|
||||
),
|
||||
key: 'fromPresale'
|
||||
}
|
||||
];
|
||||
|
||||
@observer
|
||||
export default class CreationType extends Component {
|
||||
static propTypes = {
|
||||
store: PropTypes.object.isRequired
|
||||
createStore: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { createType } = this.props.store;
|
||||
const { createType } = this.props.createStore;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -157,7 +172,7 @@ export default class CreationType extends Component {
|
||||
<div className={ styles.selectItem }>
|
||||
<TypeIcon
|
||||
className={ styles.icon }
|
||||
store={ this.props.store }
|
||||
createStore={ this.props.createStore }
|
||||
type={ item.key }
|
||||
/>
|
||||
<Title
|
||||
@ -171,21 +186,21 @@ export default class CreationType extends Component {
|
||||
}
|
||||
|
||||
isSelected = (item) => {
|
||||
const { createType } = this.props.store;
|
||||
const { createType } = this.props.createStore;
|
||||
|
||||
return item.key === createType;
|
||||
}
|
||||
|
||||
onChange = (item) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setCreateType(item.key);
|
||||
createStore.setCreateType(item.key);
|
||||
}
|
||||
|
||||
onSelect = (item) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setCreateType(item.key);
|
||||
store.nextStage();
|
||||
createStore.setCreateType(item.key);
|
||||
createStore.nextStage();
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ function render () {
|
||||
store = createStore();
|
||||
component = shallow(
|
||||
<CreationType
|
||||
store={ store }
|
||||
createStore={ store }
|
||||
/>
|
||||
);
|
||||
instance = component.instance();
|
||||
|
@ -31,7 +31,7 @@ import styles from '../createAccount.css';
|
||||
export default class CreateAccount extends Component {
|
||||
static propTypes = {
|
||||
newError: PropTypes.func.isRequired,
|
||||
store: PropTypes.object.isRequired,
|
||||
createStore: PropTypes.object.isRequired,
|
||||
vaultStore: PropTypes.object
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ export default class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint } = this.props.store;
|
||||
const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint } = this.props.createStore;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@ -126,7 +126,7 @@ export default class CreateAccount extends Component {
|
||||
</div>
|
||||
<PasswordStrength input={ password } />
|
||||
<ChangeVault
|
||||
store={ this.props.store }
|
||||
createStore={ this.props.createStore }
|
||||
vaultStore={ this.props.vaultStore }
|
||||
/>
|
||||
{ this.renderIdentitySelector() }
|
||||
@ -203,16 +203,16 @@ export default class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
createIdentities = () => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
return store
|
||||
return createStore
|
||||
.createIdentities()
|
||||
.then((accounts) => {
|
||||
const selectedAddress = Object.keys(accounts)[0];
|
||||
const { phrase } = accounts[selectedAddress];
|
||||
|
||||
store.setAddress(selectedAddress);
|
||||
store.setPhrase(phrase);
|
||||
createStore.setAddress(selectedAddress);
|
||||
createStore.setPhrase(phrase);
|
||||
|
||||
this.setState({
|
||||
accounts,
|
||||
@ -225,7 +225,7 @@ export default class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
onChangeIdentity = (event) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
const selectedAddress = event.target.value || event.target.getAttribute('value');
|
||||
|
||||
if (!selectedAddress) {
|
||||
@ -235,32 +235,32 @@ export default class CreateAccount extends Component {
|
||||
this.setState({ selectedAddress }, () => {
|
||||
const { phrase } = this.state.accounts[selectedAddress];
|
||||
|
||||
store.setAddress(selectedAddress);
|
||||
store.setPhrase(phrase);
|
||||
createStore.setAddress(selectedAddress);
|
||||
createStore.setPhrase(phrase);
|
||||
});
|
||||
}
|
||||
|
||||
onEditPasswordHint = (event, passwordHint) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPasswordHint(passwordHint);
|
||||
createStore.setPasswordHint(passwordHint);
|
||||
}
|
||||
|
||||
onEditAccountName = (event, name) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setName(name);
|
||||
createStore.setName(name);
|
||||
}
|
||||
|
||||
onEditPassword = (event, password) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPassword(password);
|
||||
createStore.setPassword(password);
|
||||
}
|
||||
|
||||
onEditPasswordRepeat = (event, password) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPasswordRepeat(password);
|
||||
createStore.setPasswordRepeat(password);
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ function render () {
|
||||
store = createStore();
|
||||
component = shallow(
|
||||
<NewAccount
|
||||
store={ store }
|
||||
createStore={ store }
|
||||
/>,
|
||||
{
|
||||
context: { api }
|
||||
|
@ -30,11 +30,11 @@ export default class NewGeth extends Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
store: PropTypes.object.isRequired
|
||||
createStore: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { gethAccountsAvailable, gethAddresses } = this.props.store;
|
||||
const { gethAccountsAvailable, gethAddresses } = this.props.createStore;
|
||||
|
||||
return gethAccountsAvailable.length
|
||||
? (
|
||||
@ -84,14 +84,14 @@ export default class NewGeth extends Component {
|
||||
}
|
||||
|
||||
isSelected = (account) => {
|
||||
const { gethAddresses } = this.props.store;
|
||||
const { gethAddresses } = this.props.createStore;
|
||||
|
||||
return gethAddresses.includes(account.address);
|
||||
}
|
||||
|
||||
onSelect = (account) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.selectGethAccount(account.address);
|
||||
createStore.selectGethAccount(account.address);
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ function render () {
|
||||
store = createStore();
|
||||
component = shallow(
|
||||
<NewGeth
|
||||
store={ store }
|
||||
createStore={ store }
|
||||
/>
|
||||
);
|
||||
instance = component.instance();
|
||||
|
@ -26,13 +26,13 @@ import styles from '../createAccount.css';
|
||||
@observer
|
||||
export default class NewImport extends Component {
|
||||
static propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
createStore: PropTypes.object.isRequired,
|
||||
vaultStore: PropTypes.object
|
||||
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, nameError, password, passwordHint } = this.props.store;
|
||||
const { name, nameError, password, passwordHint } = this.props.createStore;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@ -92,7 +92,7 @@ export default class NewImport extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<ChangeVault
|
||||
store={ this.props.store }
|
||||
createStore={ this.props.createStore }
|
||||
vaultStore={ this.props.vaultStore }
|
||||
/>
|
||||
{ this.renderFileSelector() }
|
||||
@ -101,7 +101,7 @@ export default class NewImport extends Component {
|
||||
}
|
||||
|
||||
renderFileSelector () {
|
||||
const { walletFile, walletFileError } = this.props.store;
|
||||
const { walletFile, walletFileError } = this.props.createStore;
|
||||
|
||||
return walletFile
|
||||
? (
|
||||
@ -133,27 +133,27 @@ export default class NewImport extends Component {
|
||||
}
|
||||
|
||||
onFileSelect = (fileName, fileContent) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setWalletFile(fileName);
|
||||
store.setWalletJson(fileContent);
|
||||
createStore.setWalletFile(fileName);
|
||||
createStore.setWalletJson(fileContent);
|
||||
}
|
||||
|
||||
onEditName = (event, name) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setName(name);
|
||||
createStore.setName(name);
|
||||
}
|
||||
|
||||
onEditPassword = (event, password) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPassword(password);
|
||||
createStore.setPassword(password);
|
||||
}
|
||||
|
||||
onEditPasswordHint = (event, passwordHint) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPasswordHint(passwordHint);
|
||||
createStore.setPasswordHint(passwordHint);
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ function render () {
|
||||
store = createStore();
|
||||
component = shallow(
|
||||
<NewImport
|
||||
store={ store }
|
||||
createStore={ store }
|
||||
/>
|
||||
);
|
||||
instance = component.instance();
|
||||
|
17
js/src/modals/CreateAccount/NewQr/index.js
Normal file
17
js/src/modals/CreateAccount/NewQr/index.js
Normal 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 './newQr';
|
134
js/src/modals/CreateAccount/NewQr/newQr.js
Normal file
134
js/src/modals/CreateAccount/NewQr/newQr.js
Normal 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 React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Form, Input, InputAddress, QrScan } from '~/ui';
|
||||
|
||||
import ChangeVault from '../ChangeVault';
|
||||
|
||||
export default class NewQr extends Component {
|
||||
static propTypes = {
|
||||
createStore: PropTypes.object.isRequired,
|
||||
vaultStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
const { createStore } = this.props;
|
||||
|
||||
return createStore.qrAddressValid
|
||||
? this.renderInfo()
|
||||
: this.renderScanner();
|
||||
}
|
||||
|
||||
renderInfo () {
|
||||
const { createStore, vaultStore } = this.props;
|
||||
const { description, name, nameError, qrAddress } = createStore;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<InputAddress
|
||||
allowCopy
|
||||
readOnly
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='createAccount.newQr.address.hint'
|
||||
defaultMessage='the network address for the account'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='createAccount.newQr.address.label'
|
||||
defaultMessage='address'
|
||||
/>
|
||||
}
|
||||
value={ qrAddress }
|
||||
/>
|
||||
<Input
|
||||
autoFocus
|
||||
error={ nameError }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='createAccount.newQr.name.hint'
|
||||
defaultMessage='a descriptive name for the account'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='createAccount.newQr.name.label'
|
||||
defaultMessage='account name'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onEditAccountName }
|
||||
value={ name }
|
||||
/>
|
||||
<Input
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='createAccount.newQr.description.hint'
|
||||
defaultMessage='a description for the account'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='createAccount.newQr.description.label'
|
||||
defaultMessage='account description'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onEditAccountDescription }
|
||||
value={ description }
|
||||
/>
|
||||
<ChangeVault
|
||||
createStore={ createStore }
|
||||
vaultStore={ vaultStore }
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
renderScanner () {
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='createAccount.newQr.summary'
|
||||
defaultMessage='Use the built-in machine camera to scan to QR code of the account you wish to attach as an external account. External accounts are signed on the external device.'
|
||||
/>
|
||||
<QrScan onScan={ this.onScan } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onEditAccountDescription = (event, description) => {
|
||||
const { createStore } = this.props;
|
||||
|
||||
createStore.setDescription(description);
|
||||
}
|
||||
|
||||
onEditAccountName = (event, name) => {
|
||||
const { createStore } = this.props;
|
||||
|
||||
createStore.setName(name);
|
||||
}
|
||||
|
||||
onScan = (address) => {
|
||||
const { createStore } = this.props;
|
||||
|
||||
console.log('QR scan', address);
|
||||
|
||||
createStore.setQrAddress(address);
|
||||
}
|
||||
}
|
93
js/src/modals/CreateAccount/NewQr/newQr.spec.js
Normal file
93
js/src/modals/CreateAccount/NewQr/newQr.spec.js
Normal file
@ -0,0 +1,93 @@
|
||||
// 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 NewQr from './';
|
||||
|
||||
let component;
|
||||
let instance;
|
||||
let createStore;
|
||||
let vaultStore;
|
||||
|
||||
function createStores () {
|
||||
createStore = {
|
||||
qrAddressValid: false,
|
||||
setDescription: sinon.stub(),
|
||||
setName: sinon.stub(),
|
||||
setQrAddress: sinon.stub()
|
||||
};
|
||||
|
||||
vaultStore = {};
|
||||
}
|
||||
|
||||
function render (props = {}) {
|
||||
createStores();
|
||||
|
||||
component = shallow(
|
||||
<NewQr
|
||||
createStore={ createStore }
|
||||
vaultStore={ vaultStore }
|
||||
/>
|
||||
);
|
||||
instance = component.instance();
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('modals/CreateAccount/NewQr', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
describe('event methods', () => {
|
||||
describe('onEditAccountDescription', () => {
|
||||
beforeEach(() => {
|
||||
instance.onEditAccountDescription(null, 'testing');
|
||||
});
|
||||
|
||||
it('calls into createStore.setDescription', () => {
|
||||
expect(createStore.setDescription).to.have.been.calledWith('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onEditAccountName', () => {
|
||||
beforeEach(() => {
|
||||
instance.onEditAccountName(null, 'testing');
|
||||
});
|
||||
|
||||
it('calls into createStore.setName', () => {
|
||||
expect(createStore.setName).to.have.been.calledWith('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onScan', () => {
|
||||
beforeEach(() => {
|
||||
instance.onScan('testing');
|
||||
});
|
||||
|
||||
it('calls into createStore.setQrAddress', () => {
|
||||
expect(createStore.setQrAddress).to.have.been.calledWith('testing');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -31,12 +31,12 @@ export default class RawKey extends Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
createStore: PropTypes.object.isRequired,
|
||||
vaultStore: PropTypes.object
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint, rawKey, rawKeyError } = this.props.store;
|
||||
const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint, rawKey, rawKeyError } = this.props.createStore;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@ -134,7 +134,7 @@ export default class RawKey extends Component {
|
||||
</div>
|
||||
<PasswordStrength input={ password } />
|
||||
<ChangeVault
|
||||
store={ this.props.store }
|
||||
createStore={ this.props.createStore }
|
||||
vaultStore={ this.props.vaultStore }
|
||||
/>
|
||||
</Form>
|
||||
@ -142,32 +142,32 @@ export default class RawKey extends Component {
|
||||
}
|
||||
|
||||
onEditName = (event, name) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setName(name);
|
||||
createStore.setName(name);
|
||||
}
|
||||
|
||||
onEditPasswordHint = (event, passwordHint) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPasswordHint(passwordHint);
|
||||
createStore.setPasswordHint(passwordHint);
|
||||
}
|
||||
|
||||
onEditPassword = (event, password) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPassword(password);
|
||||
createStore.setPassword(password);
|
||||
}
|
||||
|
||||
onEditPasswordRepeat = (event, password) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPasswordRepeat(password);
|
||||
createStore.setPasswordRepeat(password);
|
||||
}
|
||||
|
||||
onEditKey = (event, rawKey) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setRawKey(rawKey);
|
||||
createStore.setRawKey(rawKey);
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ function render () {
|
||||
store = createStore();
|
||||
component = shallow(
|
||||
<RawKey
|
||||
store={ store }
|
||||
createStore={ store }
|
||||
/>
|
||||
);
|
||||
instance = component.instance();
|
||||
|
@ -28,12 +28,12 @@ import styles from '../createAccount.css';
|
||||
@observer
|
||||
export default class RecoveryPhrase extends Component {
|
||||
static propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
createStore: PropTypes.object.isRequired,
|
||||
vaultStore: PropTypes.object
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isWindowsPhrase, name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint, phrase } = this.props.store;
|
||||
const { isWindowsPhrase, name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint, phrase } = this.props.createStore;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@ -130,7 +130,7 @@ export default class RecoveryPhrase extends Component {
|
||||
</div>
|
||||
<PasswordStrength input={ password } />
|
||||
<ChangeVault
|
||||
store={ this.props.store }
|
||||
createStore={ this.props.createStore }
|
||||
vaultStore={ this.props.vaultStore }
|
||||
/>
|
||||
<Checkbox
|
||||
@ -149,38 +149,38 @@ export default class RecoveryPhrase extends Component {
|
||||
}
|
||||
|
||||
onToggleWindowsPhrase = (event) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setWindowsPhrase(!store.isWindowsPhrase);
|
||||
createStore.setWindowsPhrase(!createStore.isWindowsPhrase);
|
||||
}
|
||||
|
||||
onEditPhrase = (event, phrase) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPhrase(phrase);
|
||||
createStore.setPhrase(phrase);
|
||||
}
|
||||
|
||||
onEditName = (event, name) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setName(name);
|
||||
createStore.setName(name);
|
||||
}
|
||||
|
||||
onEditPassword = (event, password) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPassword(password);
|
||||
createStore.setPassword(password);
|
||||
}
|
||||
|
||||
onEditPasswordRepeat = (event, password) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPasswordRepeat(password);
|
||||
createStore.setPasswordRepeat(password);
|
||||
}
|
||||
|
||||
onEditPasswordHint = (event, passwordHint) => {
|
||||
const { store } = this.props;
|
||||
const { createStore } = this.props;
|
||||
|
||||
store.setPasswordHint(passwordHint);
|
||||
createStore.setPasswordHint(passwordHint);
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ function render () {
|
||||
store = createStore();
|
||||
component = shallow(
|
||||
<RecoveryPhrase
|
||||
store={ store }
|
||||
createStore={ store }
|
||||
/>
|
||||
);
|
||||
instance = component.instance();
|
||||
|
@ -16,20 +16,20 @@
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { AccountsIcon, DoneIcon, FileIcon, FileUploadIcon, KeyboardIcon, KeyIcon, MembershipIcon } from '~/ui/Icons';
|
||||
import { AccountsIcon, DoneIcon, FileIcon, FileUploadIcon, KeyboardIcon, KeyIcon, MembershipIcon, PhoneLock } from '~/ui/Icons';
|
||||
|
||||
import { STAGE_INFO } from '../store';
|
||||
|
||||
export default class TypeIcon extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
store: PropTypes.object.isRequired,
|
||||
createStore: PropTypes.object.isRequired,
|
||||
type: PropTypes.string
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, store, type } = this.props;
|
||||
const { createType, stage } = store;
|
||||
const { className, createStore, type } = this.props;
|
||||
const { createType, stage } = createStore;
|
||||
|
||||
if (stage === STAGE_INFO) {
|
||||
return <DoneIcon className={ className } />;
|
||||
@ -39,18 +39,21 @@ export default class TypeIcon extends Component {
|
||||
case 'fromGeth':
|
||||
return <FileUploadIcon className={ className } />;
|
||||
|
||||
case 'fromPhrase':
|
||||
return <KeyboardIcon className={ className } />;
|
||||
|
||||
case 'fromRaw':
|
||||
return <KeyIcon className={ className } />;
|
||||
|
||||
case 'fromJSON':
|
||||
return <FileIcon className={ className } />;
|
||||
|
||||
case 'fromPhrase':
|
||||
return <KeyboardIcon className={ className } />;
|
||||
|
||||
case 'fromPresale':
|
||||
return <MembershipIcon className={ className } />;
|
||||
|
||||
case 'fromQr':
|
||||
return <PhoneLock className={ className } />;
|
||||
|
||||
case 'fromRaw':
|
||||
return <KeyIcon className={ className } />;
|
||||
|
||||
case 'fromNew':
|
||||
default:
|
||||
return <AccountsIcon className={ className } />;
|
||||
|
@ -34,6 +34,7 @@ import CreationType from './CreationType';
|
||||
import NewAccount from './NewAccount';
|
||||
import NewGeth from './NewGeth';
|
||||
import NewImport from './NewImport';
|
||||
import NewQr from './NewQr';
|
||||
import RawKey from './RawKey';
|
||||
import RecoveryPhrase from './RecoveryPhrase';
|
||||
import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE } from './store';
|
||||
@ -65,10 +66,17 @@ const TITLES = {
|
||||
id='createAccount.title.importWallet'
|
||||
defaultMessage='import wallet'
|
||||
/>
|
||||
),
|
||||
qr: (
|
||||
<FormattedMessage
|
||||
id='createAccount.title.qr'
|
||||
defaultMessage='external account'
|
||||
/>
|
||||
)
|
||||
};
|
||||
const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info];
|
||||
const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info];
|
||||
const STAGE_QR = [TITLES.type, TITLES.qr, TITLES.info];
|
||||
|
||||
@observer
|
||||
class CreateAccount extends Component {
|
||||
@ -83,7 +91,7 @@ class CreateAccount extends Component {
|
||||
onUpdate: PropTypes.func
|
||||
}
|
||||
|
||||
store = new Store(this.context.api, this.props.accounts);
|
||||
createStore = new Store(this.context.api, this.props.accounts);
|
||||
vaultStore = VaultStore.get(this.context.api);
|
||||
|
||||
componentWillMount () {
|
||||
@ -91,7 +99,15 @@ class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isBusy, createType, stage } = this.store;
|
||||
const { isBusy, createType, stage } = this.createStore;
|
||||
|
||||
let steps = STAGE_IMPORT;
|
||||
|
||||
if (createType === 'fromNew') {
|
||||
steps = STAGE_NAMES;
|
||||
} else if (createType === 'fromQr') {
|
||||
steps = STAGE_QR;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal
|
||||
@ -100,13 +116,9 @@ class CreateAccount extends Component {
|
||||
activeStep={ stage }
|
||||
onClose={ this.onClose }
|
||||
open
|
||||
steps={
|
||||
createType === 'fromNew'
|
||||
? STAGE_NAMES
|
||||
: STAGE_IMPORT
|
||||
}
|
||||
steps={ steps }
|
||||
>
|
||||
<ModalBox icon={ <TypeIcon store={ this.store } /> }>
|
||||
<ModalBox icon={ <TypeIcon createStore={ this.createStore } /> }>
|
||||
{ this.renderPage() }
|
||||
</ModalBox>
|
||||
</Portal>
|
||||
@ -114,12 +126,12 @@ class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
renderPage () {
|
||||
const { createType, stage } = this.store;
|
||||
const { createType, stage } = this.createStore;
|
||||
|
||||
switch (stage) {
|
||||
case STAGE_SELECT_TYPE:
|
||||
return (
|
||||
<CreationType store={ this.store } />
|
||||
<CreationType createStore={ this.createStore } />
|
||||
);
|
||||
|
||||
case STAGE_CREATE:
|
||||
@ -127,7 +139,7 @@ class CreateAccount extends Component {
|
||||
return (
|
||||
<NewAccount
|
||||
newError={ this.props.newError }
|
||||
store={ this.store }
|
||||
createStore={ this.createStore }
|
||||
vaultStore={ this.vaultStore }
|
||||
/>
|
||||
);
|
||||
@ -135,14 +147,23 @@ class CreateAccount extends Component {
|
||||
|
||||
if (createType === 'fromGeth') {
|
||||
return (
|
||||
<NewGeth store={ this.store } />
|
||||
<NewGeth createStore={ this.createStore } />
|
||||
);
|
||||
}
|
||||
|
||||
if (createType === 'fromPhrase') {
|
||||
return (
|
||||
<RecoveryPhrase
|
||||
store={ this.store }
|
||||
createStore={ this.createStore }
|
||||
vaultStore={ this.vaultStore }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (createType === 'fromQr') {
|
||||
return (
|
||||
<NewQr
|
||||
createStore={ this.createStore }
|
||||
vaultStore={ this.vaultStore }
|
||||
/>
|
||||
);
|
||||
@ -151,7 +172,7 @@ class CreateAccount extends Component {
|
||||
if (createType === 'fromRaw') {
|
||||
return (
|
||||
<RawKey
|
||||
store={ this.store }
|
||||
createStore={ this.createStore }
|
||||
vaultStore={ this.vaultStore }
|
||||
/>
|
||||
);
|
||||
@ -159,7 +180,7 @@ class CreateAccount extends Component {
|
||||
|
||||
return (
|
||||
<NewImport
|
||||
store={ this.store }
|
||||
createStore={ this.createStore }
|
||||
vaultStore={ this.vaultStore }
|
||||
/>
|
||||
);
|
||||
@ -167,18 +188,18 @@ class CreateAccount extends Component {
|
||||
case STAGE_INFO:
|
||||
if (createType === 'fromGeth') {
|
||||
return (
|
||||
<AccountDetailsGeth store={ this.store } />
|
||||
<AccountDetailsGeth createStore={ this.createStore } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountDetails store={ this.store } />
|
||||
<AccountDetails createStore={ this.createStore } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderDialogActions () {
|
||||
const { createType, canCreate, isBusy, stage } = this.store;
|
||||
const { createType, canCreate, isBusy, stage } = this.createStore;
|
||||
|
||||
const cancelBtn = (
|
||||
<Button
|
||||
@ -207,7 +228,7 @@ class CreateAccount extends Component {
|
||||
defaultMessage='Next'
|
||||
/>
|
||||
}
|
||||
onClick={ this.store.nextStage }
|
||||
onClick={ this.createStore.nextStage }
|
||||
/>
|
||||
];
|
||||
|
||||
@ -223,7 +244,7 @@ class CreateAccount extends Component {
|
||||
defaultMessage='Back'
|
||||
/>
|
||||
}
|
||||
onClick={ this.store.prevStage }
|
||||
onClick={ this.createStore.prevStage }
|
||||
/>,
|
||||
<Button
|
||||
disabled={ !canCreate || isBusy }
|
||||
@ -281,17 +302,17 @@ class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
onCreate = () => {
|
||||
this.store.setBusy(true);
|
||||
this.createStore.setBusy(true);
|
||||
|
||||
return this.store
|
||||
return this.createStore
|
||||
.createAccount(this.vaultStore)
|
||||
.then(() => {
|
||||
this.store.setBusy(false);
|
||||
this.store.nextStage();
|
||||
this.createStore.setBusy(false);
|
||||
this.createStore.nextStage();
|
||||
this.props.onUpdate && this.props.onUpdate();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.store.setBusy(false);
|
||||
this.createStore.setBusy(false);
|
||||
this.props.newError(error);
|
||||
});
|
||||
}
|
||||
@ -301,7 +322,7 @@ class CreateAccount extends Component {
|
||||
}
|
||||
|
||||
printPhrase = () => {
|
||||
const { address, name, phrase } = this.store;
|
||||
const { address, name, phrase } = this.createStore;
|
||||
const identity = createIdentityImg(address);
|
||||
|
||||
print(recoveryPage({
|
||||
|
@ -41,6 +41,7 @@ export default class Store {
|
||||
@observable passwordHint = '';
|
||||
@observable passwordRepeat = '';
|
||||
@observable phrase = '';
|
||||
@observable qrAddress = null;
|
||||
@observable rawKey = '';
|
||||
@observable rawKeyError = ERRORS.nokey;
|
||||
@observable stage = STAGE_SELECT_TYPE;
|
||||
@ -73,6 +74,9 @@ export default class Store {
|
||||
case 'fromPhrase':
|
||||
return !(this.nameError || this.passwordRepeatError);
|
||||
|
||||
case 'fromQr':
|
||||
return this.qrAddressValid && !this.nameError;
|
||||
|
||||
case 'fromRaw':
|
||||
return !(this.nameError || this.passwordRepeatError || this.rawKeyError);
|
||||
|
||||
@ -87,13 +91,19 @@ export default class Store {
|
||||
: ERRORS.noMatchPassword;
|
||||
}
|
||||
|
||||
@computed get qrAddressValid () {
|
||||
return this._api.util.isAddressValid(this.qrAddress);
|
||||
}
|
||||
|
||||
@action clearErrors = () => {
|
||||
transaction(() => {
|
||||
this.description = '';
|
||||
this.password = '';
|
||||
this.passwordRepeat = '';
|
||||
this.phrase = '';
|
||||
this.name = '';
|
||||
this.nameError = null;
|
||||
this.qrAddress = null;
|
||||
this.rawKey = '';
|
||||
this.rawKeyError = null;
|
||||
this.vaultName = '';
|
||||
@ -136,6 +146,14 @@ export default class Store {
|
||||
this.gethImported = gethImported;
|
||||
}
|
||||
|
||||
@action setQrAddress = (qrAddress) => {
|
||||
if (qrAddress && qrAddress.substr(0, 2) !== '0x') {
|
||||
qrAddress = `0x${qrAddress}`;
|
||||
}
|
||||
|
||||
this.qrAddress = qrAddress;
|
||||
}
|
||||
|
||||
@action setVaultName = (vaultName) => {
|
||||
this.vaultName = vaultName;
|
||||
}
|
||||
@ -260,6 +278,9 @@ export default class Store {
|
||||
case 'fromPhrase':
|
||||
return this.createAccountFromPhrase();
|
||||
|
||||
case 'fromQr':
|
||||
return this.createAccountFromQr();
|
||||
|
||||
case 'fromRaw':
|
||||
return this.createAccountFromRaw();
|
||||
|
||||
@ -274,17 +295,13 @@ export default class Store {
|
||||
.then((gethImported) => {
|
||||
console.log('createAccountFromGeth', gethImported);
|
||||
|
||||
this.setName('Geth Import');
|
||||
this.setDescription('Imported from Geth keystore');
|
||||
this.setGethImported(gethImported);
|
||||
|
||||
return Promise
|
||||
.all(gethImported.map((address) => {
|
||||
return this._api.parity.setAccountName(address, 'Geth Import');
|
||||
}))
|
||||
.then(() => {
|
||||
return Promise.all(gethImported.map((address) => {
|
||||
return this._api.parity.setAccountMeta(address, { timestamp });
|
||||
}));
|
||||
});
|
||||
return Promise.all(gethImported.map((address) => {
|
||||
return this.setupMeta(address, timestamp);
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('createAccountFromGeth', error);
|
||||
@ -307,12 +324,7 @@ export default class Store {
|
||||
.then((address) => {
|
||||
this.setAddress(address);
|
||||
|
||||
return this._api.parity
|
||||
.setAccountName(address, this.name)
|
||||
.then(() => this._api.parity.setAccountMeta(address, {
|
||||
passwordHint: this.passwordHint,
|
||||
timestamp
|
||||
}));
|
||||
return this.setupMeta(address, timestamp);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('createAccount', error);
|
||||
@ -320,18 +332,19 @@ export default class Store {
|
||||
});
|
||||
}
|
||||
|
||||
createAccountFromQr = (timestamp = Date.now()) => {
|
||||
this.setAddress(this.qrAddress);
|
||||
|
||||
return this.setupMeta(this.qrAddress, timestamp, { external: true });
|
||||
}
|
||||
|
||||
createAccountFromRaw = (timestamp = Date.now()) => {
|
||||
return this._api.parity
|
||||
.newAccountFromSecret(this.rawKey, this.password)
|
||||
.then((address) => {
|
||||
this.setAddress(address);
|
||||
|
||||
return this._api.parity
|
||||
.setAccountName(address, this.name)
|
||||
.then(() => this._api.parity.setAccountMeta(address, {
|
||||
passwordHint: this.passwordHint,
|
||||
timestamp
|
||||
}));
|
||||
return this.setupMeta(address, timestamp);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('createAccount', error);
|
||||
@ -345,12 +358,7 @@ export default class Store {
|
||||
.then((address) => {
|
||||
this.setAddress(address);
|
||||
|
||||
return this._api.parity
|
||||
.setAccountName(address, this.name)
|
||||
.then(() => this._api.parity.setAccountMeta(address, {
|
||||
passwordHint: this.passwordHint,
|
||||
timestamp
|
||||
}));
|
||||
return this.setupMeta(address, timestamp);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('createAccount', error);
|
||||
@ -358,6 +366,18 @@ export default class Store {
|
||||
});
|
||||
}
|
||||
|
||||
setupMeta = (address, timestamp = Date.now(), extra = {}) => {
|
||||
const meta = Object.assign({}, extra, {
|
||||
description: this.description,
|
||||
passwordHint: this.passwordHint,
|
||||
timestamp
|
||||
});
|
||||
|
||||
return this._api.parity
|
||||
.setAccountName(address, this.name)
|
||||
.then(() => this._api.parity.setAccountMeta(address, meta));
|
||||
}
|
||||
|
||||
createIdentities = () => {
|
||||
return Promise
|
||||
.all([
|
||||
|
@ -79,8 +79,9 @@ describe('modals/CreateAccount/Store', () => {
|
||||
beforeEach(() => {
|
||||
store.setName('testing');
|
||||
store.setPassword('testing');
|
||||
store.setVaultName('testing');
|
||||
store.setQrAddress('testing');
|
||||
store.setRawKey('test');
|
||||
store.setVaultName('testing');
|
||||
store.setWalletFile('test');
|
||||
store.setWalletJson('test');
|
||||
});
|
||||
@ -92,6 +93,7 @@ describe('modals/CreateAccount/Store', () => {
|
||||
expect(store.nameError).to.be.null;
|
||||
expect(store.password).to.equal('');
|
||||
expect(store.passwordRepeatError).to.be.null;
|
||||
expect(store.qrAddress).to.be.null;
|
||||
expect(store.rawKey).to.equal('');
|
||||
expect(store.rawKeyError).to.be.null;
|
||||
expect(store.vaultName).to.equal('');
|
||||
@ -182,6 +184,20 @@ describe('modals/CreateAccount/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setQrAddress', () => {
|
||||
const ADDR = '0x1234567890123456789012345678901234567890';
|
||||
|
||||
it('sets the address', () => {
|
||||
store.setQrAddress(ADDR);
|
||||
expect(store.qrAddress).to.equal(ADDR);
|
||||
});
|
||||
|
||||
it('adds leading 0x if not found', () => {
|
||||
store.setQrAddress(ADDR.substr(2));
|
||||
expect(store.qrAddress).to.equal(ADDR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRawKey', () => {
|
||||
it('sets error when empty key', () => {
|
||||
store.setRawKey(null);
|
||||
@ -402,10 +418,34 @@ describe('modals/CreateAccount/Store', () => {
|
||||
});
|
||||
|
||||
describe('operations', () => {
|
||||
describe('setupMeta', () => {
|
||||
beforeEach(() => {
|
||||
store.setDescription('test description');
|
||||
store.setName('test name');
|
||||
store.setPasswordHint('some hint');
|
||||
|
||||
return store.setupMeta('testaddr', 'timestamp', { something: 'else' });
|
||||
});
|
||||
|
||||
it('sets the name for the acocunt', () => {
|
||||
expect(api.parity.setAccountName).to.have.been.calledWith('testaddr', 'test name');
|
||||
});
|
||||
|
||||
it('sets the meta for the account', () => {
|
||||
expect(api.parity.setAccountMeta).to.have.been.calledWith('testaddr', {
|
||||
description: 'test description',
|
||||
passwordHint: 'some hint',
|
||||
something: 'else',
|
||||
timestamp: 'timestamp'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAccount', () => {
|
||||
let createAccountFromGethSpy;
|
||||
let createAccountFromWalletSpy;
|
||||
let createAccountFromPhraseSpy;
|
||||
let createAccountFromQrSpy;
|
||||
let createAccountFromRawSpy;
|
||||
let busySpy;
|
||||
|
||||
@ -413,6 +453,7 @@ describe('modals/CreateAccount/Store', () => {
|
||||
createAccountFromGethSpy = sinon.spy(store, 'createAccountFromGeth');
|
||||
createAccountFromWalletSpy = sinon.spy(store, 'createAccountFromWallet');
|
||||
createAccountFromPhraseSpy = sinon.spy(store, 'createAccountFromPhrase');
|
||||
createAccountFromQrSpy = sinon.spy(store, 'createAccountFromQr');
|
||||
createAccountFromRawSpy = sinon.spy(store, 'createAccountFromRaw');
|
||||
busySpy = sinon.spy(store, 'setBusy');
|
||||
});
|
||||
@ -422,6 +463,7 @@ describe('modals/CreateAccount/Store', () => {
|
||||
store.createAccountFromWallet.restore();
|
||||
store.createAccountFromPhrase.restore();
|
||||
store.createAccountFromRaw.restore();
|
||||
store.createAccountFromQr.restore();
|
||||
store.setBusy.restore();
|
||||
});
|
||||
|
||||
@ -470,6 +512,14 @@ describe('modals/CreateAccount/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('calls createAccountFromQr on createType === fromQr', () => {
|
||||
store.setCreateType('fromQr');
|
||||
|
||||
return store.createAccount().then(() => {
|
||||
expect(createAccountFromQrSpy).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
it('calls createAccountFromRaw on createType === fromRaw', () => {
|
||||
store.setCreateType('fromRaw');
|
||||
|
||||
@ -516,6 +566,8 @@ describe('modals/CreateAccount/Store', () => {
|
||||
it('sets the account meta', () => {
|
||||
return store.createAccountFromGeth(-1).then(() => {
|
||||
expect(store._api.parity.setAccountMeta).to.have.been.calledWith(GETH_ADDRESSES[0], {
|
||||
description: 'Imported from Geth keystore',
|
||||
passwordHint: '',
|
||||
timestamp: -1
|
||||
});
|
||||
});
|
||||
@ -552,6 +604,7 @@ describe('modals/CreateAccount/Store', () => {
|
||||
it('sets the account meta', () => {
|
||||
return store.createAccountFromPhrase(-1).then(() => {
|
||||
expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, {
|
||||
description: '',
|
||||
passwordHint: 'some hint',
|
||||
timestamp: -1
|
||||
});
|
||||
@ -574,6 +627,29 @@ describe('modals/CreateAccount/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAccountFromQr', () => {
|
||||
beforeEach(() => {
|
||||
store.setName('some name');
|
||||
store.setDescription('some desc');
|
||||
store.setQrAddress('0x123');
|
||||
|
||||
return store.createAccountFromQr(-1);
|
||||
});
|
||||
|
||||
it('sets the accountInfo name', () => {
|
||||
expect(api.parity.setAccountName).to.have.been.calledWith('0x123', 'some name');
|
||||
});
|
||||
|
||||
it('sets the meta (with extrenal flag)', () => {
|
||||
expect(api.parity.setAccountMeta).to.have.been.calledWith('0x123', {
|
||||
description: 'some desc',
|
||||
passwordHint: '',
|
||||
timestamp: -1,
|
||||
external: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAccountFromRaw', () => {
|
||||
beforeEach(() => {
|
||||
store.setName('some name');
|
||||
@ -603,6 +679,7 @@ describe('modals/CreateAccount/Store', () => {
|
||||
it('sets the account meta', () => {
|
||||
return store.createAccountFromRaw(-1).then(() => {
|
||||
expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, {
|
||||
description: '',
|
||||
passwordHint: 'some hint',
|
||||
timestamp: -1
|
||||
});
|
||||
@ -639,6 +716,7 @@ describe('modals/CreateAccount/Store', () => {
|
||||
it('sets the account meta', () => {
|
||||
return store.createAccountFromWallet(-1).then(() => {
|
||||
expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, {
|
||||
description: '',
|
||||
passwordHint: 'some hint',
|
||||
timestamp: -1
|
||||
});
|
||||
|
@ -124,12 +124,12 @@ class FirstRun extends Component {
|
||||
return (
|
||||
<NewAccount
|
||||
newError={ this.props.newError }
|
||||
store={ this.createStore }
|
||||
createStore={ this.createStore }
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<AccountDetails store={ this.createStore } />
|
||||
<AccountDetails createStore={ this.createStore } />
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
|
@ -48,6 +48,9 @@ export function personalAccountsInfo (accountsInfo) {
|
||||
account.hardware = true;
|
||||
hardware[account.address] = account;
|
||||
accounts[account.address] = account;
|
||||
} else if (account.meta.external) {
|
||||
account.external = true;
|
||||
accounts[account.address] = account;
|
||||
} else {
|
||||
contacts[account.address] = account;
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import * as actions from './signerActions';
|
||||
|
||||
import { inHex } from '~/api/format/input';
|
||||
import HardwareStore from '~/mobx/hardwareStore';
|
||||
import { createSignedTx } from '~/util/qrscan';
|
||||
import { Signer } from '~/util/signer';
|
||||
|
||||
export default class SignerMiddleware {
|
||||
@ -86,14 +87,25 @@ export default class SignerMiddleware {
|
||||
return this._hwstore.signLedger(transaction);
|
||||
})
|
||||
.then((rawTx) => {
|
||||
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
||||
|
||||
return handlePromise(this._api.signer.confirmRequestRaw(id, rawTx));
|
||||
return this.confirmRawTransaction(store, id, rawTx);
|
||||
});
|
||||
}
|
||||
|
||||
confirmWalletTransaction (store, id, transaction, wallet, password) {
|
||||
confirmRawTransaction (store, id, rawTx) {
|
||||
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
||||
|
||||
return handlePromise(this._api.signer.confirmRequestRaw(id, rawTx));
|
||||
}
|
||||
|
||||
confirmSignedTransaction (store, id, txSigned) {
|
||||
const { netVersion } = store.getState().nodeStatus;
|
||||
const { signature, tx } = txSigned;
|
||||
const { rlp } = createSignedTx(netVersion, signature, tx);
|
||||
|
||||
return this.confirmRawTransaction(store, id, rlp);
|
||||
}
|
||||
|
||||
confirmWalletTransaction (store, id, transaction, wallet, password) {
|
||||
const { worker } = store.getState().worker;
|
||||
|
||||
const signerPromise = worker && worker._worker.state === 'activated'
|
||||
@ -126,7 +138,7 @@ export default class SignerMiddleware {
|
||||
return signer.signTransaction(txData);
|
||||
})
|
||||
.then((rawTx) => {
|
||||
return handlePromise(this._api.signer.confirmRequestRaw(id, rawTx));
|
||||
return this.confirmRawTransaction(store, id, rawTx);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
@ -135,7 +147,7 @@ export default class SignerMiddleware {
|
||||
}
|
||||
|
||||
onConfirmStart = (store, action) => {
|
||||
const { condition, gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload;
|
||||
const { condition, gas = 0, gasPrice = 0, id, password, payload, txSigned, wallet } = action.payload;
|
||||
const handlePromise = this._createConfirmPromiseHandler(store, id);
|
||||
const transaction = payload.sendTransaction || payload.signTransaction;
|
||||
|
||||
@ -144,6 +156,8 @@ export default class SignerMiddleware {
|
||||
|
||||
if (wallet) {
|
||||
return this.confirmWalletTransaction(store, id, transaction, wallet, password);
|
||||
} else if (txSigned) {
|
||||
return this.confirmSignedTransaction(store, id, txSigned);
|
||||
} else if (hardwareAccount) {
|
||||
switch (hardwareAccount.via) {
|
||||
case 'ledger':
|
||||
|
@ -74,6 +74,18 @@ export default class GasPriceEditor {
|
||||
}
|
||||
}
|
||||
|
||||
@computed get conditionValue () {
|
||||
switch (this.conditionType) {
|
||||
case CONDITIONS.BLOCK:
|
||||
return { block: new BigNumber(this.condition.block || 0) };
|
||||
|
||||
case CONDITIONS.TIME:
|
||||
return { time: this.condition.time };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@action setConditionType = (conditionType = CONDITIONS.NONE) => {
|
||||
transaction(() => {
|
||||
this.conditionBlockError = null;
|
||||
@ -224,11 +236,8 @@ export default class GasPriceEditor {
|
||||
|
||||
switch (this.conditionType) {
|
||||
case CONDITIONS.BLOCK:
|
||||
result.condition = { block: new BigNumber(this.condition.block || 0) };
|
||||
break;
|
||||
|
||||
case CONDITIONS.TIME:
|
||||
result.condition = { time: this.condition.time };
|
||||
result.condition = this.conditionValue;
|
||||
break;
|
||||
|
||||
case CONDITIONS.NONE:
|
||||
|
@ -49,6 +49,7 @@ export MembershipIcon from 'material-ui/svg-icons/action/card-membership';
|
||||
export MoveIcon from 'material-ui/svg-icons/action/open-with';
|
||||
export NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
|
||||
export PauseIcon from 'material-ui/svg-icons/av/pause';
|
||||
export PhoneLock from 'material-ui/svg-icons/communication/phonelink-lock';
|
||||
export PlayIcon from 'material-ui/svg-icons/av/play-arrow';
|
||||
export PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
||||
export PrintIcon from 'material-ui/svg-icons/action/print';
|
||||
|
32
js/src/ui/QrCode/qrCode.css
Normal file
32
js/src/ui/QrCode/qrCode.css
Normal file
@ -0,0 +1,32 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
.qr {
|
||||
max-width: 15em;
|
||||
max-height: 15em;
|
||||
|
||||
img {
|
||||
height: auto !important;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
background: white;
|
||||
}
|
||||
}
|
@ -14,14 +14,17 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// https://github.com/cmanzana/qrcode-npm packaging the standard
|
||||
// https://github.com/kazuhikoarase/qrcode-generator
|
||||
import { qrcode } from 'qrcode-npm';
|
||||
import qrcode from 'qrcode-generator/js/qrcode';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { calculateType } from './qrSize';
|
||||
|
||||
import styles from './qrCode.css';
|
||||
|
||||
const QROPTS = {
|
||||
CODE_TYPE: 4,
|
||||
ERROR_LEVEL: 'M'
|
||||
ERROR_LEVEL: 'M',
|
||||
MAX_SIZE: 40,
|
||||
MIN_SIZE: 5
|
||||
};
|
||||
|
||||
export default class QrCode extends Component {
|
||||
@ -61,7 +64,7 @@ export default class QrCode extends Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ className }
|
||||
className={ [styles.qr, className].join(' ') }
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: image
|
||||
} }
|
||||
@ -71,9 +74,10 @@ export default class QrCode extends Component {
|
||||
|
||||
generateCode (props) {
|
||||
const { margin, size, value } = props;
|
||||
const qr = qrcode(QROPTS.CODE_TYPE, QROPTS.ERROR_LEVEL);
|
||||
const type = calculateType(value.length, QROPTS.ERROR_LEVEL);
|
||||
const qr = qrcode(type, QROPTS.ERROR_LEVEL);
|
||||
|
||||
qr.addData(value);
|
||||
qr.addData(value, 'Byte');
|
||||
qr.make();
|
||||
|
||||
this.setState({
|
||||
|
77
js/src/ui/QrCode/qrSize.js
Normal file
77
js/src/ui/QrCode/qrSize.js
Normal file
@ -0,0 +1,77 @@
|
||||
// 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 _QR_SIZES = { 'L': [], 'M': [], 'H': [], 'Q': [] };
|
||||
const QR_LEVELS = Object.keys(_QR_SIZES);
|
||||
|
||||
/* eslint-disable indent,no-multi-spaces */
|
||||
const QR_SIZES = [
|
||||
19, 16, 13, 9,
|
||||
34, 28, 22, 16,
|
||||
55, 44, 34, 26,
|
||||
80, 64, 48, 36,
|
||||
108, 86, 62, 46,
|
||||
136, 108, 76, 60,
|
||||
156, 124, 88, 66,
|
||||
194, 154, 110, 86,
|
||||
232, 182, 132, 100,
|
||||
274, 216, 154, 122,
|
||||
324, 254, 180, 140,
|
||||
370, 290, 206, 158,
|
||||
428, 334, 244, 180,
|
||||
461, 365, 261, 197,
|
||||
523, 415, 295, 223,
|
||||
589, 453, 325, 253,
|
||||
647, 507, 367, 283,
|
||||
721, 563, 397, 313,
|
||||
795, 627, 445, 341,
|
||||
861, 669, 485, 385,
|
||||
932, 714, 512, 406,
|
||||
1006, 782, 568, 442,
|
||||
1094, 860, 614, 464,
|
||||
1174, 914, 664, 514,
|
||||
1276, 1000, 718, 538,
|
||||
1370, 1062, 754, 596,
|
||||
1468, 1128, 808, 628,
|
||||
1531, 1193, 871, 661,
|
||||
1631, 1267, 911, 701,
|
||||
1735, 1373, 985, 745,
|
||||
1843, 1455, 1033, 793,
|
||||
1955, 1541, 1115, 845,
|
||||
2071, 1631, 1171, 901,
|
||||
2191, 1725, 1231, 961,
|
||||
2306, 1812, 1286, 986,
|
||||
2434, 1914, 1354, 1054,
|
||||
2566, 1992, 1426, 1096,
|
||||
2702, 2102, 1502, 1142,
|
||||
2812, 2216, 1582, 1222,
|
||||
2956, 2334, 1666, 1276
|
||||
].reduce((sizes, value, index) => {
|
||||
sizes[QR_LEVELS[index % 4]].push(value);
|
||||
|
||||
return sizes;
|
||||
}, _QR_SIZES);
|
||||
/* eslint-enable indent,no-multi-spaces */
|
||||
|
||||
export function calculateType (lengthBytes, errorLevel = 'M') {
|
||||
let type = 5;
|
||||
|
||||
while (type < 40 && lengthBytes > QR_SIZES[errorLevel][type - 1]) {
|
||||
type++;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
17
js/src/ui/QrScan/index.js
Normal file
17
js/src/ui/QrScan/index.js
Normal 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 './qrScan';
|
25
js/src/ui/QrScan/qrScan.css
Normal file
25
js/src/ui/QrScan/qrScan.css
Normal file
@ -0,0 +1,25 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
.qr {
|
||||
text-align: center;
|
||||
|
||||
video {
|
||||
border: 2px solid rgb(200, 200, 200);
|
||||
margin: 1.5em auto;
|
||||
}
|
||||
}
|
50
js/src/ui/QrScan/qrScan.js
Normal file
50
js/src/ui/QrScan/qrScan.js
Normal 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/>.
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import Reader from 'react-qr-reader';
|
||||
|
||||
import styles from './qrScan.css';
|
||||
|
||||
const SCAN_DELAY = 100;
|
||||
const SCAN_STYLE = {
|
||||
display: 'inline-block',
|
||||
width: '30em'
|
||||
};
|
||||
|
||||
export default function QrScan ({ onError, onScan }) {
|
||||
return (
|
||||
<div className={ styles.qr }>
|
||||
<Reader
|
||||
delay={ SCAN_DELAY }
|
||||
onError={ onError }
|
||||
onScan={ onScan }
|
||||
style={ SCAN_STYLE }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QrScan.propTypes = {
|
||||
onError: PropTypes.func,
|
||||
onScan: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
QrScan.defaultProps = {
|
||||
onError: (error) => {
|
||||
console.log('QrScan', error);
|
||||
}
|
||||
};
|
69
js/src/ui/QrScan/qrScan.spec.js
Normal file
69
js/src/ui/QrScan/qrScan.spec.js
Normal file
@ -0,0 +1,69 @@
|
||||
// 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 QrScan from './';
|
||||
|
||||
let component;
|
||||
let onError;
|
||||
let onScan;
|
||||
|
||||
function render () {
|
||||
onError = sinon.stub();
|
||||
onScan = sinon.stub();
|
||||
|
||||
component = shallow(
|
||||
<QrScan
|
||||
onError={ onError }
|
||||
onScan={ onScan }
|
||||
/>
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('ui/QrScan', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
describe('Reader', () => {
|
||||
let reader;
|
||||
|
||||
beforeEach(() => {
|
||||
reader = component.find('Reader');
|
||||
});
|
||||
|
||||
it('renders component', () => {
|
||||
expect(reader.get(0)).to.be.ok;
|
||||
});
|
||||
|
||||
it('attaches onError', () => {
|
||||
expect(reader.props().onError).to.equal(onError);
|
||||
});
|
||||
|
||||
it('attaches onScan', () => {
|
||||
expect(reader.props().onScan).to.equal(onScan);
|
||||
});
|
||||
});
|
||||
});
|
@ -46,6 +46,7 @@ export Page from './Page';
|
||||
export ParityBackground from './ParityBackground';
|
||||
export Portal from './Portal';
|
||||
export QrCode from './QrCode';
|
||||
export QrScan from './QrScan';
|
||||
export ScrollableText from './ScrollableText';
|
||||
export SectionList from './SectionList';
|
||||
export SelectionList from './SelectionList';
|
||||
|
121
js/src/util/qrscan.js
Normal file
121
js/src/util/qrscan.js
Normal file
@ -0,0 +1,121 @@
|
||||
// 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 Transaction from 'ethereumjs-tx';
|
||||
|
||||
import { inAddress, inHex, inNumber10 } from '~/api/format/input';
|
||||
import { sha3 } from '~/api/util/sha3';
|
||||
|
||||
export function createUnsignedTx (api, netVersion, gasStore, transaction) {
|
||||
const { data, from, gas, gasPrice, to, value } = gasStore.overrideTransaction(transaction);
|
||||
|
||||
return api.parity
|
||||
.nextNonce(from)
|
||||
.then((_nonce) => {
|
||||
const chainId = parseInt(netVersion, 10);
|
||||
const nonce = (!transaction.nonce || transaction.nonce.isZero())
|
||||
? _nonce
|
||||
: transaction.nonce;
|
||||
|
||||
const tx = new Transaction({
|
||||
chainId,
|
||||
data: inHex(data),
|
||||
gasPrice: inHex(gasPrice),
|
||||
gasLimit: inHex(gas),
|
||||
nonce: inHex(nonce),
|
||||
to: to ? inHex(to) : undefined,
|
||||
value: inHex(value),
|
||||
r: 0,
|
||||
s: 0,
|
||||
v: chainId
|
||||
});
|
||||
|
||||
const rlp = inHex(tx.serialize().toString('hex'));
|
||||
const hash = sha3(rlp);
|
||||
|
||||
return {
|
||||
chainId,
|
||||
hash,
|
||||
nonce,
|
||||
rlp,
|
||||
tx
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function createSignedTx (netVersion, signature, unsignedTx) {
|
||||
const chainId = parseInt(netVersion, 10);
|
||||
const { data, gasPrice, gasLimit, nonce, to, value } = unsignedTx;
|
||||
|
||||
const r = Buffer.from(signature.substr(2, 64), 'hex');
|
||||
const s = Buffer.from(signature.substr(66, 64), 'hex');
|
||||
const v = Buffer.from([parseInt(signature.substr(130, 2), 16) + (chainId * 2) + 35]);
|
||||
|
||||
const tx = new Transaction({
|
||||
chainId,
|
||||
data,
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
nonce,
|
||||
to,
|
||||
value,
|
||||
r,
|
||||
s,
|
||||
v
|
||||
});
|
||||
|
||||
return {
|
||||
chainId,
|
||||
rlp: inHex(tx.serialize().toString('hex')),
|
||||
tx
|
||||
};
|
||||
}
|
||||
|
||||
export function generateQr (from, tx, hash, rlp) {
|
||||
if (tx.data && tx.data.length > 64) {
|
||||
return JSON.stringify({
|
||||
action: 'signTransactionHash',
|
||||
data: {
|
||||
account: from.substr(2),
|
||||
hash: hash.substr(2),
|
||||
details: {
|
||||
gasPrice: inNumber10(inHex(tx.gasPrice.toString('hex'))),
|
||||
gas: inNumber10(inHex(tx.gasLimit.toString('hex'))),
|
||||
nonce: inNumber10(inHex(tx.nonce.toString('hex'))),
|
||||
to: inAddress(tx.to.toString('hex')),
|
||||
value: inHex(tx.value.toString('hex') || '0')
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
action: 'signTransaction',
|
||||
data: {
|
||||
account: from.substr(2),
|
||||
rlp: rlp.substr(2)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function generateTxQr (api, netVersion, gasStore, transaction) {
|
||||
return createUnsignedTx(api, netVersion, gasStore, transaction)
|
||||
.then((qr) => {
|
||||
qr.value = generateQr(transaction.from, qr.tx, qr.hash, qr.rlp);
|
||||
|
||||
return qr;
|
||||
});
|
||||
}
|
@ -94,10 +94,15 @@ export class Signer {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
signTransactionObject (tx) {
|
||||
tx.sign(this.seed);
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
signTransaction (transaction) {
|
||||
const tx = new Transaction(transaction);
|
||||
|
||||
tx.sign(this.seed);
|
||||
return inHex(tx.serialize().toString('hex'));
|
||||
return inHex(this.signTransactionObject(tx).serialize().toString('hex'));
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +220,7 @@ class Account extends Component {
|
||||
}
|
||||
onClick={ this.store.toggleEditDialog }
|
||||
/>,
|
||||
!account.hardware && (
|
||||
!(account.external || account.hardware) && (
|
||||
<Button
|
||||
icon={ <LockedIcon /> }
|
||||
key='passwordManager'
|
||||
@ -237,10 +237,19 @@ class Account extends Component {
|
||||
icon={ <DeleteIcon /> }
|
||||
key='delete'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.button.delete'
|
||||
defaultMessage='delete'
|
||||
/>
|
||||
account.external || account.hardware
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='account.button.forget'
|
||||
defaultMessage='forget'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FormattedMessage
|
||||
id='account.button.delete'
|
||||
defaultMessage='delete'
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={ this.store.toggleDeleteDialog }
|
||||
/>
|
||||
@ -281,6 +290,23 @@ class Account extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (account.external) {
|
||||
return (
|
||||
<DeleteAddress
|
||||
account={ account }
|
||||
confirmMessage={
|
||||
<FormattedMessage
|
||||
id='account.external.confirmDelete'
|
||||
defaultMessage='Are you sure you want to remove the following external address from your account list?'
|
||||
/>
|
||||
}
|
||||
visible
|
||||
route='/accounts'
|
||||
onClose={ this.store.toggleDeleteDialog }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DeleteAccount
|
||||
account={ account }
|
||||
|
@ -110,7 +110,7 @@ class Accounts extends Component {
|
||||
}
|
||||
/>
|
||||
|
||||
{ this.renderHwWallets() }
|
||||
{ this.renderExternalAccounts() }
|
||||
{ this.renderWallets() }
|
||||
{ this.renderAccounts() }
|
||||
</Page>
|
||||
@ -182,13 +182,14 @@ class Accounts extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderHwWallets () {
|
||||
renderExternalAccounts () {
|
||||
const { accounts, balances } = this.props;
|
||||
const { wallets } = this.hwstore;
|
||||
const hardware = pickBy(accounts, (account) => account.hardware);
|
||||
const hasHardware = Object.keys(hardware).length > 0;
|
||||
const external = pickBy(accounts, (account) => account.external);
|
||||
const all = Object.assign({}, hardware, external);
|
||||
|
||||
if (!hasHardware) {
|
||||
if (Object.keys(all).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -208,7 +209,7 @@ class Accounts extends Component {
|
||||
return (
|
||||
<List
|
||||
search={ searchValues }
|
||||
accounts={ hardware }
|
||||
accounts={ all }
|
||||
balances={ balances }
|
||||
disabled={ disabled }
|
||||
order={ sortOrder }
|
||||
|
@ -80,10 +80,11 @@ export default class AccountStore {
|
||||
.keys(allAccounts)
|
||||
.filter((address) => {
|
||||
const account = allAccounts[address];
|
||||
const isAccount = account.uuid || (account.meta && account.meta.hardware);
|
||||
const isAccount = account.uuid;
|
||||
const isExternal = account.meta && (account.meta.external || account.meta.hardware);
|
||||
const isWhitelisted = !whitelist || whitelist.includes(address);
|
||||
|
||||
return isAccount && isWhitelisted;
|
||||
return (isAccount || isExternal) && isWhitelisted;
|
||||
})
|
||||
.map((address) => {
|
||||
return {
|
||||
|
@ -127,9 +127,12 @@ class TransactionPending extends Component {
|
||||
address={ from }
|
||||
disabled={ disabled }
|
||||
focus={ focus }
|
||||
gasStore={ this.gasStore }
|
||||
isSending={ isSending }
|
||||
netVersion={ netVersion }
|
||||
onConfirm={ this.onConfirm }
|
||||
onReject={ this.onReject }
|
||||
transaction={ transaction }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -157,7 +160,7 @@ class TransactionPending extends Component {
|
||||
|
||||
onConfirm = (data) => {
|
||||
const { id, transaction } = this.props;
|
||||
const { password, wallet } = data;
|
||||
const { password, txSigned, wallet } = data;
|
||||
const { condition, gas, gasPrice } = this.gasStore.overrideTransaction(transaction);
|
||||
|
||||
const options = {
|
||||
@ -165,6 +168,7 @@ class TransactionPending extends Component {
|
||||
gasPrice,
|
||||
id,
|
||||
password,
|
||||
txSigned,
|
||||
wallet
|
||||
};
|
||||
|
||||
|
@ -44,3 +44,8 @@
|
||||
.fileInput input {
|
||||
top: 22px;
|
||||
}
|
||||
|
||||
.qr {
|
||||
margin-bottom: 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -21,18 +21,30 @@ import ReactDOM from 'react-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
import { Form, Input, IdentityIcon } from '~/ui';
|
||||
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,
|
||||
focus: PropTypes.bool
|
||||
transaction: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -44,6 +56,8 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
|
||||
state = {
|
||||
password: '',
|
||||
qrState: QR_VISIBLE,
|
||||
qr: {},
|
||||
wallet: null,
|
||||
walletError: null
|
||||
}
|
||||
@ -52,6 +66,15 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.readNonce();
|
||||
this.subscribeNonce();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.unsubscribeNonce();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!this.props.focus && nextProps.focus) {
|
||||
this.focus(nextProps);
|
||||
@ -93,64 +116,96 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
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 isWalletOk = account.hardware || account.uuid || (walletError === null && wallet !== null);
|
||||
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() }
|
||||
<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={
|
||||
isSending
|
||||
? (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.buttons.confirmBusy'
|
||||
defaultMessage='Confirming...'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FormattedMessage
|
||||
id='signer.txPendingConfirm.buttons.confirmRequest'
|
||||
defaultMessage='Confirm Request'
|
||||
/>
|
||||
)
|
||||
}
|
||||
onTouchTap={ this.onConfirm }
|
||||
primary
|
||||
/>
|
||||
</div>
|
||||
{ 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) {
|
||||
if (account.hardware || account.external) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -199,6 +254,34 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
|
||||
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) {
|
||||
@ -241,11 +324,45 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (account.uuid || account.wallet || account.hardware || account.external) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -274,8 +391,8 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
renderTooltip () {
|
||||
const { account } = this.props;
|
||||
|
||||
if (this.state.password.length || account.hardware) {
|
||||
return;
|
||||
if (this.state.password.length || account.hardware || account.external) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -288,6 +405,25 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -338,7 +474,12 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
}
|
||||
|
||||
onConfirm = () => {
|
||||
const { password, wallet } = this.state;
|
||||
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,
|
||||
@ -346,6 +487,15 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@ -355,4 +505,43 @@ export default class TransactionPendingFormConfirm extends Component {
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -27,12 +27,15 @@ 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,
|
||||
className: PropTypes.string,
|
||||
focus: PropTypes.bool
|
||||
transaction: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -56,7 +59,7 @@ export default class TransactionPendingForm extends Component {
|
||||
}
|
||||
|
||||
renderForm () {
|
||||
const { account, address, disabled, focus, isSending, onConfirm, onReject } = this.props;
|
||||
const { account, address, disabled, focus, gasStore, isSending, netVersion, onConfirm, onReject, transaction } = this.props;
|
||||
|
||||
if (this.state.isRejectOpen) {
|
||||
return (
|
||||
@ -70,8 +73,11 @@ export default class TransactionPendingForm extends Component {
|
||||
account={ account }
|
||||
disabled={ disabled }
|
||||
focus={ focus }
|
||||
gasStore={ gasStore }
|
||||
netVersion={ netVersion }
|
||||
isSending={ isSending }
|
||||
onConfirm={ onConfirm }
|
||||
transaction={ transaction }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
|
||||
global.window = document.defaultView;
|
||||
global.navigator = global.window.navigator;
|
||||
global.location = global.window.location;
|
||||
global.Blob = () => {};
|
||||
|
||||
// attach mocked localStorage onto the window as exposed by jsdom
|
||||
global.window.localStorage = global.localStorage;
|
||||
|
Loading…
Reference in New Issue
Block a user