Merge branch 'jg-test-ui-2' into jg-test-ui

This commit is contained in:
Jaco Greeff 2016-12-08 17:33:02 +01:00
commit 0544e0e786
51 changed files with 1151 additions and 151 deletions

2
Cargo.lock generated
View File

@ -1271,7 +1271,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#e5eabdb4f30e9c6bf9b7cb98f0029a71ba202a19"
source = "git+https://github.com/ethcore/js-precompiled.git#a59b62ecec8773715d1db7e070bbbe5443eb7378"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

@ -1,6 +1,6 @@
{
"name": "parity.js",
"version": "0.2.98",
"version": "0.2.99",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>",

View File

@ -112,11 +112,15 @@ export function inNumber10 (number) {
}
export function inNumber16 (number) {
if (isInstanceOf(number, BigNumber)) {
return inHex(number.toString(16));
const bn = isInstanceOf(number, BigNumber)
? number
: (new BigNumber(number || 0));
if (!bn.isInteger()) {
throw new Error(`[format/input::inNumber16] the given number is not an integer: ${bn.toFormat()}`);
}
return inHex((new BigNumber(number || 0)).toString(16));
return inHex(bn.toString(16));
}
export function inOptions (options) {
@ -130,6 +134,9 @@ export function inOptions (options) {
case 'gas':
case 'gasPrice':
options[key] = inNumber16((new BigNumber(options[key])).round());
break;
case 'value':
case 'nonce':
options[key] = inNumber16(options[key]);

View File

@ -68,6 +68,7 @@ export default class Personal {
this._accountsInfo();
return;
case 'parity_removeAddress':
case 'parity_setAccountName':
case 'parity_setAccountMeta':
this._accountsInfo();

File diff suppressed because one or more lines are too long

View File

@ -94,7 +94,6 @@ export default class Application extends Component {
tokenregInstance,
accounts: Object
.keys(accountsInfo)
.filter((address) => !accountsInfo[address].meta.deleted)
.sort((a, b) => {
return (accountsInfo[b].uuid || '').localeCompare(accountsInfo[a].uuid || '');
})

View File

@ -24,7 +24,6 @@ export const fetch = () => (dispatch) => {
.then((accountsInfo) => {
const addresses = Object
.keys(accountsInfo)
.filter((address) => accountsInfo[address] && !accountsInfo[address].meta.deleted)
.map((address) => ({
...accountsInfo[address],
address,

View File

@ -102,7 +102,7 @@ export default class AddAddress extends Component {
if (!addressError) {
const contact = contacts[address];
if (contact && !contact.meta.deleted) {
if (contact) {
addressError = ERRORS.duplicateAddress;
}
}

View File

@ -231,7 +231,7 @@ export default class AddContract extends Component {
if (!addressError) {
const contract = contracts[address];
if (contract && !contract.meta.deleted) {
if (contract) {
addressError = ERRORS.duplicateAddress;
}
}

View File

@ -117,15 +117,17 @@ export default class WalletDetails extends Component {
onChange={ this.onRequiredChange }
param={ parseAbiType('uint') }
min={ 1 }
max={ wallet.owners.length + 1 }
/>
<TypedInput
label='wallet day limit'
hint='number of days to wait for other owners confirmation'
hint='amount of ETH spendable without confirmations'
value={ wallet.daylimit }
error={ errors.daylimit }
onChange={ this.onDaylimitChange }
param={ parseAbiType('uint') }
isEth
/>
</div>
</Form>

View File

@ -17,6 +17,7 @@
import React, { Component, PropTypes } from 'react';
import { CompletedStep, IdentityIcon, CopyToClipboard } from '~/ui';
import { fromWei } from '~/api/util/wei';
import styles from '../createWallet.css';
@ -62,7 +63,7 @@ export default class WalletInfo extends Component {
<code>{ required }</code> owners are required to confirm a transaction.
</p>
<p>
The daily limit is set to <code>{ daylimit }</code>.
The daily limit is set to <code>{ fromWei(daylimit).toFormat() }</code> ETH.
</p>
</CompletedStep>
);

View File

@ -43,7 +43,13 @@ export default class WalletType extends Component {
return [
{
label: 'Multi-Sig wallet', key: 'MULTISIG',
description: 'A standard multi-signature Wallet'
description: (
<span>
<span>Create/Deploy a </span>
<a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>standard multi-signature </a>
<span> Wallet</span>
</span>
)
},
{
label: 'Watch a wallet', key: 'WATCH',

View File

@ -16,7 +16,7 @@
import { observable, computed, action, transaction } from 'mobx';
import { validateUint, validateAddress, validateName } from '../../util/validation';
import { validateUint, validateAddress, validateName } from '~/util/validation';
import { ERROR_CODES } from '~/api/transport/error';
import Contract from '~/api/contract';

View File

@ -23,7 +23,7 @@ import { bytesToHex } from '~/api/util/format';
import Contract from '~/api/contract';
import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
const TITLES = {
transfer: 'transfer details',
@ -116,7 +116,6 @@ export default class TransferStore {
this.api = api;
const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props;
this.account = account;
this.balance = balance;
this.gasLimit = gasLimit;
@ -412,34 +411,38 @@ export default class TransferStore {
return;
}
const { gas, gasPrice, tag, valueAll, isEth } = this;
const { gas, gasPrice, tag, valueAll, isEth, isWallet } = this;
const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0));
const availableEth = new BigNumber(balance.tokens[0].value);
const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag);
const available = new BigNumber(senderBalance.value);
const format = new BigNumber(senderBalance.token.format || 1);
const available = isWallet
? this.api.util.fromWei(new BigNumber(senderBalance.value))
: (new BigNumber(senderBalance.value)).div(format);
let { value, valueError } = this;
let totalEth = gasTotal;
let totalError = null;
if (valueAll) {
if (isEth) {
if (isEth && !isWallet) {
const bn = this.api.util.fromWei(availableEth.minus(gasTotal));
value = (bn.lt(0) ? new BigNumber(0.0) : bn).toString();
} else if (isEth) {
value = (available.lt(0) ? new BigNumber(0.0) : available).toString();
} else {
value = available.div(format).toString();
value = available.toString();
}
}
if (isEth) {
if (isEth && !isWallet) {
totalEth = totalEth.plus(this.api.util.toWei(value || 0));
}
if (new BigNumber(value || 0).gt(available.div(format))) {
if (new BigNumber(value || 0).gt(available)) {
valueError = ERRORS.largeAmount;
} else if (valueError === ERRORS.largeAmount) {
valueError = null;

View File

@ -139,8 +139,8 @@ class Transfer extends Component {
? (
<div>
<br />
<p>
This transaction needs confirmation from other owners.
<div>
<p>This transaction needs confirmation from other owners.</p>
<Input
style={ { width: '50%', margin: '0 auto' } }
value={ this.store.operation }
@ -148,7 +148,7 @@ class Transfer extends Component {
readOnly
allowCopy
/>
</p>
</div>
</div>
)
: null
@ -298,7 +298,6 @@ function mapStateToProps (initState, initProps) {
return (state) => {
const { gasLimit } = state.nodeStatus;
const sendersBalances = senders ? pick(state.balances.balances, Object.keys(senders)) : null;
return { gasLimit, wallet, senders, sendersBalances };
};
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './walletSettings';

View File

@ -0,0 +1,63 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.splitInput {
display: flex;
flex-direction: row;
> * {
flex: 1;
margin: 0 0.25em;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
.change {
background-color: rgba(255, 255, 255, 0.1);
padding: 0.75em 1.75em;
margin-bottom: 1em;
&.add {
background-color: rgba(139, 195, 74, 0.5);
}
&.remove {
background-color: rgba(244, 67, 54, 0.5);
}
.label {
text-transform: uppercase;
margin-bottom: 0.5em;
margin-left: -1em;
font-size: 0.8em;
}
}
.eth:after {
content: 'ETH';
font-size: 0.75em;
margin-left: 0.125em;
}

View File

@ -0,0 +1,321 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { observer } from 'mobx-react';
import { pick } from 'lodash';
import ActionDone from 'material-ui/svg-icons/action/done';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { parseAbiType } from '~/util/abi';
import { Button, Modal, TxHash, BusyStep, Form, TypedInput, InputAddress, AddressSelect } from '~/ui';
import { fromWei } from '~/api/util/wei';
import WalletSettingsStore from './walletSettingsStore.js';
import styles from './walletSettings.css';
@observer
class WalletSettings extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
senders: PropTypes.object.isRequired
};
store = new WalletSettingsStore(this.context.api, this.props.wallet);
render () {
const { stage, steps, waiting, rejected } = this.store;
if (rejected) {
return (
<Modal
visible
title='rejected'
actions={ this.renderDialogActions() }
>
<BusyStep
title='The modifications have been rejected'
state='The wallet settings will not be modified. You can safely close this window.'
/>
</Modal>
);
}
return (
<Modal
visible
actions={ this.renderDialogActions() }
current={ stage }
steps={ steps.map((s) => s.title) }
waiting={ waiting }
>
{ this.renderPage() }
</Modal>
);
}
renderPage () {
const { step } = this.store;
switch (step) {
case 'SENDING':
return (
<BusyStep
title='The modifications are currently being sent'
state={ this.store.deployState }
>
{
this.store.requests.map((req) => {
const key = req.id;
if (req.txhash) {
return (<TxHash key={ key } hash={ req.txhash } />);
}
if (req.rejected) {
return (<p key={ key }>The transaction #{parseInt(key, 16)} has been rejected</p>);
}
})
}
</BusyStep>
);
case 'CONFIRMATION':
const { changes } = this.store;
return (
<div>
<p>You are about to make the following modifications</p>
<div>
{ this.renderChanges(changes) }
</div>
</div>
);
default:
case 'EDIT':
const { wallet, errors } = this.store;
const { accounts, senders } = this.props;
return (
<Form>
<p>
In order to edit this contract's settings, at
least { this.store.initialWallet.require.toNumber() } owners have to
send the very same modifications.
Otherwise, no modification will be taken into account...
</p>
<AddressSelect
label='from account (wallet owner)'
hint='send modifications as this owner'
value={ wallet.sender }
error={ errors.sender }
onChange={ this.store.onSenderChange }
accounts={ senders }
/>
<TypedInput
label='other wallet owners'
value={ wallet.owners.slice() }
onChange={ this.store.onOwnersChange }
accounts={ accounts }
param={ parseAbiType('address[]') }
/>
<div className={ styles.splitInput }>
<TypedInput
label='required owners'
hint='number of required owners to accept a transaction'
value={ wallet.require }
error={ errors.require }
onChange={ this.store.onRequireChange }
param={ parseAbiType('uint') }
min={ 1 }
max={ wallet.owners.length }
/>
<TypedInput
label='wallet day limit'
hint='amount of ETH spendable without confirmations'
value={ wallet.dailylimit }
error={ errors.dailylimit }
onChange={ this.store.onDailylimitChange }
param={ parseAbiType('uint') }
isEth
/>
</div>
</Form>
);
}
}
renderChanges (changes) {
return changes.map((change, index) => (
<div key={ `${change.type}_${index}` }>
{ this.renderChange(change) }
</div>
));
}
renderChange (change) {
const { accounts } = this.props;
switch (change.type) {
case 'dailylimit':
return (
<div className={ styles.change }>
<div className={ styles.label }>Change Daily Limit</div>
<div>
<span> from </span>
<code> { fromWei(change.initial).toFormat() }</code>
<span className={ styles.eth } />
<span> to </span>
<code> { fromWei(change.value).toFormat() }</code>
<span className={ styles.eth } />
</div>
</div>
);
case 'require':
return (
<div className={ styles.change }>
<div className={ styles.label }>Change Required Owners</div>
<div>
<span> from </span>
<code> { change.initial.toNumber() }</code>
<span> to </span>
<code> { change.value.toNumber() }</code>
</div>
</div>
);
case 'add_owner':
return (
<div className={ [ styles.change, styles.add ].join(' ') }>
<div className={ styles.label }>Add Owner</div>
<div>
<InputAddress
disabled
value={ change.value }
accounts={ accounts }
/>
</div>
</div>
);
case 'remove_owner':
return (
<div className={ [ styles.change, styles.remove ].join(' ') }>
<div className={ styles.label }>Remove Owner</div>
<div>
<InputAddress
disabled
value={ change.value }
accounts={ accounts }
/>
</div>
</div>
);
}
}
renderDialogActions () {
const { onClose } = this.props;
const { step, hasErrors, rejected, onNext, send, done } = this.store;
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
onClick={ onClose }
/>
);
const closeBtn = (
<Button
icon={ <ContentClear /> }
label='Close'
onClick={ onClose }
/>
);
const sendingBtn = (
<Button
icon={ <ActionDone /> }
label='Sending...'
disabled
/>
);
const nextBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Next'
onClick={ onNext }
disabled={ hasErrors }
/>
);
const sendBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Send'
onClick={ send }
disabled={ hasErrors }
/>
);
if (rejected) {
return [ closeBtn ];
}
switch (step) {
case 'SENDING':
return done ? [ closeBtn ] : [ closeBtn, sendingBtn ];
case 'CONFIRMATION':
return [ cancelBtn, sendBtn ];
default:
case 'TYPE':
return [ cancelBtn, nextBtn ];
}
}
}
function mapStateToProps (initState, initProps) {
const { accountsInfo, accounts } = initState.personal;
const { owners } = initProps.wallet;
const senders = pick(accounts, owners);
return () => {
return { accounts: accountsInfo, senders };
};
}
export default connect(mapStateToProps)(WalletSettings);

View File

@ -0,0 +1,306 @@
// Copyright 2015, 2016 Ethcore (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 { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js';
import { validateUint, validateAddress } from '~/util/validation';
import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import { ERROR_CODES } from '~/api/transport/error';
const STEPS = {
EDIT: { title: 'wallet settings' },
CONFIRMATION: { title: 'confirmation' },
SENDING: { title: 'sending transaction', waiting: true }
};
export default class WalletSettingsStore {
@observable step = null;
@observable requests = [];
@observable deployState = '';
@observable done = false;
@observable wallet = {
owners: null,
require: null,
dailylimit: null,
sender: ''
};
@observable errors = {
owners: null,
require: null,
dailylimit: null,
sender: null
};
@computed get stage () {
return this.stepsKeys.findIndex((k) => k === this.step);
}
@computed get hasErrors () {
return !!Object.keys(this.errors).find((key) => !!this.errors[key]);
}
@computed get stepsKeys () {
return this.steps.map((s) => s.key);
}
@computed get steps () {
return Object
.keys(STEPS)
.map((key) => {
return {
...STEPS[key],
key
};
});
}
@computed get waiting () {
this.steps
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
}
get changes () {
const changes = [];
const prevDailylimit = new BigNumber(this.initialWallet.dailylimit);
const nextDailylimit = new BigNumber(this.wallet.dailylimit);
const prevRequire = new BigNumber(this.initialWallet.require);
const nextRequire = new BigNumber(this.wallet.require);
if (!prevDailylimit.equals(nextDailylimit)) {
changes.push({
type: 'dailylimit',
initial: prevDailylimit,
value: nextDailylimit
});
}
if (!prevRequire.equals(nextRequire)) {
changes.push({
type: 'require',
initial: prevRequire,
value: nextRequire
});
}
const prevOwners = this.initialWallet.owners;
const nextOwners = this.wallet.owners;
const ownersToRemove = prevOwners.filter((owner) => !nextOwners.includes(owner));
const ownersToAdd = nextOwners.filter((owner) => !prevOwners.includes(owner));
ownersToRemove.forEach((owner) => {
changes.push({
type: 'remove_owner',
value: owner
});
});
ownersToAdd.forEach((owner) => {
changes.push({
type: 'add_owner',
value: owner
});
});
return changes;
}
constructor (api, wallet) {
this.api = api;
this.step = this.stepsKeys[0];
this.walletInstance = wallet.instance;
this.initialWallet = {
address: wallet.address,
owners: wallet.owners,
require: wallet.require,
dailylimit: wallet.dailylimit.limit
};
transaction(() => {
this.wallet.owners = wallet.owners;
this.wallet.require = wallet.require;
this.wallet.dailylimit = wallet.dailylimit.limit;
this.validateWallet(this.wallet);
});
}
@action onNext = () => {
const stepIndex = this.stepsKeys.findIndex((k) => k === this.step) + 1;
this.step = this.stepsKeys[stepIndex];
}
@action onChange = (_wallet) => {
const newWallet = Object.assign({}, this.wallet, _wallet);
this.validateWallet(newWallet);
}
@action onOwnersChange = (owners) => {
this.onChange({ owners });
}
@action onRequireChange = (require) => {
this.onChange({ require });
}
@action onSenderChange = (_, sender) => {
this.onChange({ sender });
}
@action onDailylimitChange = (dailylimit) => {
this.onChange({ dailylimit });
}
@action send = () => {
const changes = this.changes;
const walletInstance = this.walletInstance;
this.step = 'SENDING';
this.onTransactionsState('postTransaction');
Promise
.all(changes.map((change) => this.sendChange(change, walletInstance)))
.then((requestIds) => {
this.onTransactionsState('checkRequest');
this.requests = requestIds.map((id) => ({ id, rejected: false, txhash: null }));
return Promise
.all(requestIds.map((id) => {
return this.api
.pollMethod('parity_checkRequest', id)
.then((txhash) => {
const index = this.requests.findIndex((r) => r.id === id);
this.requests[index].txhash = txhash;
})
.catch((e) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
const index = this.requests.findIndex((r) => r.id === id);
this.requests[index].rejected = true;
return false;
}
throw e;
});
}));
})
.then(() => {
this.done = true;
this.onTransactionsState('completed');
});
}
@action sendChange = (change, walletInstance) => {
const { method, values } = this.getChangeMethod(change, walletInstance);
const options = {
from: this.wallet.sender,
to: this.initialWallet.address,
gas: MAX_GAS_ESTIMATION
};
return method
.estimateGas(options, values)
.then((gasEst) => {
let gas = gasEst;
if (gas.gt(DEFAULT_GAS)) {
gas = gas.mul(1.2);
}
options.gas = gas;
return method.postTransaction(options, values);
});
}
getChangeMethod = (change, walletInstance) => {
if (change.type === 'require') {
return {
method: walletInstance.changeRequirement,
values: [ change.value ]
};
}
if (change.type === 'dailylimit') {
return {
method: walletInstance.setDailyLimit,
values: [ change.value ]
};
}
if (change.type === 'add_owner') {
return {
method: walletInstance.addOwner,
values: [ change.value ]
};
}
if (change.type === 'remove_owner') {
return {
method: walletInstance.removeOwner,
values: [ change.value ]
};
}
}
@action onTransactionsState = (state) => {
switch (state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = 'Preparing transaction for network transmission';
return;
case 'checkRequest':
this.deployState = 'Waiting for confirmation of the transaction in the Parity Secure Signer';
return;
case 'completed':
this.deployState = '';
return;
}
}
@action validateWallet = (_wallet) => {
const senderValidation = validateAddress(_wallet.sender);
const requireValidation = validateUint(_wallet.require);
const dailylimitValidation = validateUint(_wallet.dailylimit);
const errors = {
sender: senderValidation.addressError,
require: requireValidation.valueError,
dailylimit: dailylimitValidation.valueError
};
const wallet = {
..._wallet,
sender: senderValidation.address,
require: requireValidation.value,
dailylimit: dailylimitValidation.value
};
transaction(() => {
this.wallet = wallet;
this.errors = errors;
});
}
}

View File

@ -29,6 +29,7 @@ import Transfer from './Transfer';
import PasswordManager from './PasswordManager';
import SaveContract from './SaveContract';
import LoadContract from './LoadContract';
import WalletSettings from './WalletSettings';
export {
AddAddress,
@ -45,5 +46,6 @@ export {
Transfer,
PasswordManager,
LoadContract,
SaveContract
SaveContract,
WalletSettings
};

View File

@ -23,6 +23,7 @@ export default class Personal {
}
start () {
this._removeDeleted();
this._subscribeAccountsInfo();
}
@ -40,4 +41,29 @@ export default class Personal {
console.log('personal._subscribeAccountsInfo', 'subscriptionId', subscriptionId);
});
}
_removeDeleted () {
this._api.parity
.accountsInfo()
.then((accountsInfo) => {
return Promise.all(
Object
.keys(accountsInfo)
.filter((address) => {
const account = accountsInfo[address];
return !account.uuid && account.meta.deleted;
})
.map((address) => this._api.parity.removeAddress(address))
);
})
.then((results) => {
if (results.length) {
console.log(`Removed ${results.length} previously marked addresses`);
}
})
.catch((error) => {
console.warn('removeDeleted', error);
});
}
}

View File

@ -228,7 +228,7 @@ function fetchWalletInfo (contract, update, getState) {
const owners = ownersUpdate && ownersUpdate.value || null;
const transactions = transactionsUpdate && transactionsUpdate.value || null;
return fetchWalletConfirmations(contract, owners, transactions, getState)
return fetchWalletConfirmations(contract, update[UPDATE_CONFIRMATIONS], owners, transactions, getState)
.then((update) => {
updates.push(update);
return updates;
@ -292,77 +292,113 @@ function fetchWalletDailylimit (contract) {
});
}
function fetchWalletConfirmations (contract, _owners = null, _transactions = null, getState) {
function fetchWalletConfirmations (contract, _operations, _owners = null, _transactions = null, getState) {
const walletInstance = contract.instance;
const wallet = getState().wallet.wallets[contract.address];
const owners = _owners || (wallet && wallet.owners) || null;
const transactions = _transactions || (wallet && wallet.transactions) || null;
// Full load if no operations given, or if the one given aren't loaded yet
const fullLoad = !Array.isArray(_operations) || _operations
.filter((op) => !wallet.confirmations.find((conf) => conf.operation === op))
.length > 0;
return walletInstance
.ConfirmationNeeded
.getAllLogs()
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logA.blockNumber.comparedTo(logB.blockNumber);
let promise;
if (comp !== 0) {
return comp;
if (fullLoad) {
promise = walletInstance
.ConfirmationNeeded
.getAllLogs()
.then((logs) => {
return logs.map((log) => ({
initiator: log.params.initiator.value,
to: log.params.to.value,
data: log.params.data.value,
value: log.params.value.value,
operation: bytesToHex(log.params.operation.value),
transactionIndex: log.transactionIndex,
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
confirmedBy: []
}));
})
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logA.blockNumber.comparedTo(logB.blockNumber);
if (comp !== 0) {
return comp;
}
return logA.transactionIndex.comparedTo(logB.transactionIndex);
});
})
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
}
return logA.transactionIndex.comparedTo(logB.transactionIndex);
// Only fetch confirmations for operations not
// yet confirmed (ie. not yet a transaction)
if (transactions) {
const operations = transactions
.filter((t) => t.operation)
.map((t) => t.operation);
return confirmations.filter((confirmation) => {
return !operations.includes(confirmation.operation);
});
}
return confirmations;
});
})
.then((logs) => {
return logs.map((log) => ({
initiator: log.params.initiator.value,
to: log.params.to.value,
data: log.params.data.value,
value: log.params.value.value,
operation: bytesToHex(log.params.operation.value),
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
confirmedBy: []
}));
})
} else {
const { confirmations } = wallet;
const nextConfirmations = confirmations
.filter((conf) => _operations.includes(conf.operation));
promise = Promise.resolve(nextConfirmations);
}
return promise
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
}
if (transactions) {
const operations = transactions
.filter((t) => t.operation)
.map((t) => t.operation);
const uniqConfirmations = Object.values(
confirmations.reduce((confirmations, confirmation) => {
confirmations[confirmation.operation] = confirmation;
return confirmations;
}, {})
);
return confirmations.filter((confirmation) => {
return !operations.includes(confirmation.operation);
});
}
const operations = uniqConfirmations.map((conf) => conf.operation);
return confirmations;
})
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
}
const operations = confirmations.map((conf) => conf.operation);
return Promise
.all(operations.map((op) => fetchOperationConfirmations(contract, op, owners)))
.then((confirmedBys) => {
confirmations.forEach((_, index) => {
confirmations[index].confirmedBy = confirmedBys[index];
uniqConfirmations.forEach((_, index) => {
uniqConfirmations[index].confirmedBy = confirmedBys[index];
});
return confirmations;
return uniqConfirmations;
});
})
.then((confirmations) => {
const prevConfirmations = wallet.confirmations || [];
const nextConfirmations = prevConfirmations
.filter((conA) => !confirmations.find((conB) => conB.operation === conA.operation))
.concat(confirmations)
.map((conf) => ({
...conf,
pending: false
}));
return {
key: UPDATE_CONFIRMATIONS,
value: confirmations
value: nextConfirmations
};
});
}
@ -417,7 +453,10 @@ function parseLogs (logs) {
logs.forEach((log) => {
const { address, topics } = log;
const eventSignature = toHex(topics[0]);
const prev = updates[address] || { address };
const prev = updates[address] || {
[ UPDATE_DAILYLIMIT ]: true,
address
};
switch (eventSignature) {
case signatures.OwnerChanged:
@ -436,16 +475,18 @@ function parseLogs (logs) {
};
return;
case signatures.ConfirmationNeeded:
case signatures.Confirmation:
case signatures.Revoke:
const operation = log.params.operation.value;
const operation = bytesToHex(log.params.operation.value);
updates[address] = {
...prev,
[ UPDATE_CONFIRMATIONS ]: uniq(
(prev.operations || []).concat(operation)
(prev[UPDATE_CONFIRMATIONS] || []).concat(operation)
)
};
return;
case signatures.Deposit:
@ -456,17 +497,6 @@ function parseLogs (logs) {
[ UPDATE_TRANSACTIONS ]: true
};
return;
case signatures.ConfirmationNeeded:
const op = log.params.operation.value;
updates[address] = {
...prev,
[ UPDATE_CONFIRMATIONS ]: uniq(
(prev.operations || []).concat(op)
)
};
return;
}
});

View File

@ -20,5 +20,14 @@
}
.data {
flex: 1;
font-family: monospace;
padding: 0 0.5em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.container {
display: flex;
}

View File

@ -80,7 +80,13 @@ class CopyToClipboard extends Component {
onCopy = () => {
const { data, onCopy, cooldown, showSnackbar } = this.props;
const message = (<div>copied <code className={ styles.data }>{ data }</code> to clipboard</div>);
const message = (
<div className={ styles.container }>
<span>copied </span>
<code className={ styles.data }> { data } </code>
<span> to clipboard</span>
</div>
);
this.setState({
copied: true,

View File

@ -170,7 +170,7 @@ export default class AddressSelect extends Component {
handleFilter = (searchText, name, item) => {
const { address } = item;
const entry = this.state.entries[address];
const lowCaseSearch = searchText.toLowerCase();
const lowCaseSearch = (searchText || '').toLowerCase();
return [entry.name, entry.address]
.some(text => text.toLowerCase().indexOf(lowCaseSearch) !== -1);

View File

@ -53,7 +53,6 @@ class InputAddress extends Component {
const { small, allowCopy, hideUnderline, onSubmit, accountsInfo, tokens } = this.props;
const account = accountsInfo[value] || tokens[value];
const hasAccount = account && !(account.meta && account.meta.deleted);
const icon = this.renderIcon();
@ -74,7 +73,7 @@ class InputAddress extends Component {
label={ label }
hint={ hint }
error={ error }
value={ text && hasAccount ? account.name : value }
value={ text && account ? account.name : value }
onChange={ this.handleInputChange }
onSubmit={ onSubmit }
allowCopy={ allowCopy && (disabled ? value : false) }

View File

@ -29,3 +29,30 @@
position: relative;
}
}
.ethInput {
display: flex;
flex-direction: row;
align-items: flex-end;
.input {
flex: 1;
position: relative;
.label {
position: absolute;
right: 1.5em;
bottom: 1em;
font-size: 0.85em;
margin-left: 0.5em;
}
}
.toggle {
margin-bottom: 0.5em;
display: flex;
flex-direction: row;
align-items: center;
}
}

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui';
import { MenuItem, Toggle } from 'material-ui';
import { range } from 'lodash';
import IconButton from 'material-ui/IconButton';
@ -26,7 +26,8 @@ import Input from '~/ui/Form/Input';
import InputAddressSelect from '~/ui/Form/InputAddressSelect';
import Select from '~/ui/Form/Select';
import { ABI_TYPES } from '../../../util/abi';
import { ABI_TYPES } from '~/util/abi';
import { fromWei, toWei } from '~/api/util/wei';
import styles from './typedInput.css';
@ -42,16 +43,29 @@ export default class TypedInput extends Component {
label: PropTypes.string,
hint: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number
max: PropTypes.number,
isEth: PropTypes.bool
};
static defaultProps = {
min: null,
max: null
max: null,
isEth: false
};
state = {
isEth: true,
ethValue: 0
};
componentDidMount () {
if (this.props.isEth && this.props.value) {
this.setState({ ethValue: fromWei(this.props.value) });
}
}
render () {
const { param } = this.props;
const { param, isEth } = this.props;
const { type } = param;
if (type === ABI_TYPES.ARRAY) {
@ -87,6 +101,10 @@ export default class TypedInput extends Component {
);
}
if (isEth) {
return this.renderEth();
}
return this.renderType(type);
}
@ -157,16 +175,43 @@ export default class TypedInput extends Component {
return this.renderDefault();
}
renderNumber () {
const { label, value, error, param, hint, min, max } = this.props;
renderEth () {
const { ethValue } = this.state;
const value = ethValue && typeof ethValue.toNumber === 'function'
? ethValue.toNumber()
: ethValue;
return (
<div className={ styles.ethInput }>
<div className={ styles.input }>
{ this.renderNumber(value, this.onEthValueChange) }
{ this.state.isEth ? (<div className={ styles.label }>ETH</div>) : null }
</div>
<div className={ styles.toggle }>
<Toggle
toggled={ this.state.isEth }
onToggle={ this.onEthTypeChange }
style={ { width: 46 } }
/>
</div>
</div>
);
}
renderNumber (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props;
const realValue = value && typeof value.toNumber === 'function'
? value.toNumber()
: value;
return (
<Input
label={ label }
hint={ hint }
value={ value }
value={ realValue }
error={ error }
onChange={ this.onChange }
onChange={ onChange }
type='number'
min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null }
@ -236,6 +281,28 @@ export default class TypedInput extends Component {
this.props.onChange(value === 'true');
}
onEthTypeChange = () => {
const { isEth, ethValue } = this.state;
if (ethValue === '' || ethValue === undefined) {
return this.setState({ isEth: !isEth });
}
const value = isEth ? toWei(ethValue) : fromWei(ethValue);
this.setState({ isEth: !isEth, ethValue: value }, () => {
this.onEthValueChange(null, value);
});
}
onEthValueChange = (event, value) => {
const realValue = this.state.isEth && value !== '' && value !== undefined
? toWei(value)
: value;
this.setState({ ethValue: value });
this.props.onChange(realValue);
}
onChange = (event, value) => {
this.props.onChange(value);
}

View File

@ -37,17 +37,16 @@ class IdentityName extends Component {
render () {
const { address, accountsInfo, tokens, empty, name, shorten, unknown, className } = this.props;
const account = accountsInfo[address] || tokens[address];
const hasAccount = account && (account.uuid || !account.meta || !account.meta.deleted);
if (!hasAccount && empty) {
if (!account && empty) {
return null;
}
const addressFallback = shorten ? (<ShortenedHash data={ address } />) : address;
const fallback = unknown ? defaultName : addressFallback;
const isUuid = hasAccount && account.name === account.uuid;
const isUuid = account && account.name === account.uuid;
const displayName = (name && name.toUpperCase().trim()) ||
(hasAccount && !isUuid
(account && !isUuid
? account.name.toUpperCase().trim()
: fallback);

View File

@ -140,7 +140,7 @@ export function validateUint (value) {
const bn = new BigNumber(value);
if (bn.lt(0)) {
valueError = ERRORS.negativeNumber;
} else if (bn.toString().indexOf('.') !== -1) {
} else if (!bn.isInteger()) {
valueError = ERRORS.decimalNumber;
}
} catch (e) {

View File

@ -24,6 +24,7 @@ import styles from './list.css';
export default class List extends Component {
static propTypes = {
accounts: PropTypes.object,
walletsOwners: PropTypes.object,
balances: PropTypes.object,
link: PropTypes.string,
search: PropTypes.array,
@ -42,7 +43,7 @@ export default class List extends Component {
}
renderAccounts () {
const { accounts, balances, link, empty, handleAddSearchToken } = this.props;
const { accounts, balances, link, empty, handleAddSearchToken, walletsOwners } = this.props;
if (empty) {
return (
@ -60,6 +61,8 @@ export default class List extends Component {
const account = accounts[address] || {};
const balance = balances[address] || {};
const owners = walletsOwners && walletsOwners[address] || null;
return (
<div
className={ styles.item }
@ -68,6 +71,7 @@ export default class List extends Component {
link={ link }
account={ account }
balance={ balance }
owners={ owners }
handleAddSearchToken={ handleAddSearchToken } />
</div>
);

View File

@ -17,8 +17,12 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import { isEqual } from 'lodash';
import ReactTooltip from 'react-tooltip';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui';
import { nullableProptype } from '~/util/proptypes';
import styles from '../accounts.css';
export default class Summary extends Component {
static contextTypes = {
@ -31,7 +35,8 @@ export default class Summary extends Component {
link: PropTypes.string,
name: PropTypes.string,
noLink: PropTypes.bool,
handleAddSearchToken: PropTypes.func
handleAddSearchToken: PropTypes.func,
owners: nullableProptype(PropTypes.array)
};
static defaultProps = {
@ -100,11 +105,41 @@ export default class Summary extends Component {
title={ this.renderLink() }
byline={ addressComponent } />
{ this.renderOwners() }
{ this.renderBalance() }
</Container>
);
}
renderOwners () {
const { owners } = this.props;
if (!owners || owners.length === 0) {
return null;
}
return (
<div className={ styles.owners }>
{
owners.map((owner) => (
<div key={ owner.address }>
<div
data-tip
data-for={ `owner_${owner.address}` }
data-effect='solid'
>
<IdentityIcon address={ owner.address } button />
</div>
<ReactTooltip id={ `owner_${owner.address}` }>
<strong>{ owner.name } </strong><small> (owner)</small>
</ReactTooltip>
</div>
))
}
</div>
);
}
renderLink () {
const { link, noLink, account, name } = this.props;

View File

@ -22,6 +22,12 @@
left: 7em;
}
.owners {
margin-top: 1em;
display: flex;
margin-bottom: -0.5em;
}
.toolbar {
position: relative;
}

View File

@ -37,6 +37,7 @@ class Accounts extends Component {
accounts: PropTypes.object.isRequired,
hasAccounts: PropTypes.bool.isRequired,
wallets: PropTypes.object.isRequired,
walletsOwners: PropTypes.object.isRequired,
hasWallets: PropTypes.bool.isRequired,
balances: PropTypes.object
@ -137,9 +138,13 @@ class Accounts extends Component {
return this.renderLoading(this.props.wallets);
}
const { wallets, hasWallets, balances } = this.props;
const { wallets, hasWallets, balances, walletsOwners } = this.props;
const { searchValues, sortOrder } = this.state;
if (!wallets || Object.keys(wallets).length === 0) {
return null;
}
return (
<List
link='wallet'
@ -149,6 +154,7 @@ class Accounts extends Component {
empty={ !hasWallets }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken }
walletsOwners={ walletsOwners }
/>
);
}
@ -281,13 +287,29 @@ class Accounts extends Component {
}
function mapStateToProps (state) {
const { accounts, hasAccounts, wallets, hasWallets } = state.personal;
const { accounts, hasAccounts, wallets, hasWallets, accountsInfo } = state.personal;
const { balances } = state.balances;
const walletsInfo = state.wallet.wallets;
const walletsOwners = Object
.keys(walletsInfo)
.map((wallet) => ({
owners: walletsInfo[wallet].owners.map((owner) => ({
address: owner,
name: accountsInfo[owner] && accountsInfo[owner].name || owner
})),
address: wallet
}))
.reduce((walletsOwners, wallet) => {
walletsOwners[wallet.address] = wallet.owners;
return walletsOwners;
}, {});
return {
accounts,
hasAccounts,
wallets,
walletsOwners,
hasWallets,
balances
};

View File

@ -80,10 +80,8 @@ class Delete extends Component {
const { api, router } = this.context;
const { account, route, newError } = this.props;
account.meta.deleted = true;
api.parity
.setAccountMeta(account.address, account.meta)
.removeAddress(account.address)
.then(() => {
router.push(route);
this.closeDeleteDialog();

View File

@ -121,7 +121,7 @@ class Address extends Component {
return (
<Actionbar
title='Address Information'
buttons={ !contact || contact.meta.deleted ? [] : buttons } />
buttons={ !contact ? [] : buttons } />
);
}

View File

@ -229,7 +229,7 @@ class Contract extends Component {
return (
<Actionbar
title='Contract Information'
buttons={ !account || account.meta.deleted ? [] : buttons } />
buttons={ !account ? [] : buttons } />
);
}

View File

@ -17,7 +17,7 @@
import BigNumber from 'bignumber.js';
import { getShortData, getFee, getTotalValue } from './transaction';
describe('util/transaction', () => {
describe('views/Signer/components/util/transaction', () => {
describe('getEstimatedMiningTime', () => {
it('should return estimated mining time', () => {
});

View File

@ -21,7 +21,7 @@ import getMuiTheme from 'material-ui/styles/getMuiTheme';
import WrappedAutoComplete from './AutoComplete';
describe('components/AutoComplete', () => {
describe('views/Status/components/AutoComplete', () => {
describe('rendering', () => {
let rendered;

View File

@ -19,7 +19,7 @@ import { shallow } from 'enzyme';
import Box from './Box';
describe('components/Box', () => {
describe('views/Status/components/Box', () => {
describe('rendering', () => {
const title = 'test title';
let rendered;

View File

@ -22,7 +22,7 @@ import '../../../../environment/tests';
import Call from './Call';
describe('components/Call', () => {
describe('views/Status/components/Call', () => {
const call = { callIdx: 123, callNo: 456, name: 'eth_call', params: [{ name: '123' }], response: '' };
const element = 'dummyElement';

View File

@ -21,7 +21,7 @@ import '../../../../environment/tests';
import Calls from './Calls';
describe('components/Calls', () => {
describe('views/Status/components/Calls', () => {
describe('rendering (no calls)', () => {
let rendered;

View File

@ -22,7 +22,7 @@ import '../../../../environment/tests';
import CallsToolbar from './CallsToolbar';
describe('components/CallsToolbar', () => {
describe('views/Status/components/CallsToolbar', () => {
const callEl = { offsetTop: 0 };
const containerEl = { scrollTop: 0, clientHeight: 0, scrollHeight: 999 };

View File

@ -16,7 +16,7 @@
import { decodeExtraData } from './decodeExtraData';
describe('MINING SETTINGS', () => {
describe('views/Status/components/MiningSettings/decodeExtraData', () => {
describe('EXTRA DATA', () => {
const str = 'parity/1.0.0/1.0.0-beta2';
const encoded = '0xd783010000867061726974798b312e302e302d6265746132';

View File

@ -16,7 +16,7 @@
import { numberFromString } from './numberFromString';
describe('NUMBER FROM STRING', () => {
describe('views/Status/components/MiningSettings/numberFromString', () => {
it('should convert string to number', () => {
expect(numberFromString('12345'), 12345);
});

View File

@ -21,7 +21,7 @@ import '../../../../environment/tests';
import Response from './Response';
describe('components/Response', () => {
describe('views/Status/components/Response', () => {
describe('rendering', () => {
it('renders non-arrays/non-objects exactly as received', () => {
const TEST = '1234567890';

View File

@ -21,7 +21,7 @@ import { syncRpcStateFromLocalStorage } from '../actions/localstorage';
import rpcData from '../data/rpc.json';
import LocalStorageMiddleware from './localstorage';
describe('MIDDLEWARE: LOCAL STORAGE', () => {
describe('views/Status/middleware/localstorage', () => {
let cut, state;
beforeEach('mock cut', () => {

View File

@ -17,7 +17,7 @@
import sinon from 'sinon';
import * as ErrorUtil from './error';
describe('util/error', () => {
describe('views/Status/util/error', () => {
beforeEach('spy on isError', () => {
sinon.spy(ErrorUtil, 'isError');
});

View File

@ -16,7 +16,7 @@
import { toPromise, identity } from './';
describe('util', () => {
describe('views/Status/util', () => {
describe('toPromise', () => {
it('rejects on error result', () => {
const ERROR = new Error();

View File

@ -60,12 +60,14 @@ class WalletConfirmations extends Component {
}
renderConfirmations () {
const { confirmations, ...others } = this.props;
const realConfirmations = confirmations && confirmations
.filter((conf) => conf.confirmedBy.length > 0);
if (!confirmations) {
if (!realConfirmations) {
return null;
}
if (confirmations.length === 0) {
if (realConfirmations.length === 0) {
return (
<div>
<p>No transactions needs confirmation right now.</p>
@ -73,13 +75,14 @@ class WalletConfirmations extends Component {
);
}
return confirmations.map((confirmation) => (
<WalletConfirmation
key={ confirmation.operation }
confirmation={ confirmation }
{ ...others }
/>
));
return realConfirmations
.map((confirmation) => (
<WalletConfirmation
key={ confirmation.operation }
confirmation={ confirmation }
{ ...others }
/>
));
}
}
@ -320,22 +323,35 @@ class WalletConfirmation extends Component {
const { address, isTest } = this.props;
const { operation, transactionHash, blockNumber, value, to, data } = confirmation;
if (value && to && data) {
return (
<TxRow
className={ className }
key={ operation }
tx={ {
hash: transactionHash,
blockNumber: blockNumber,
from: address,
to: to,
value: value,
input: bytesToHex(data)
} }
address={ address }
isTest={ isTest }
historic={ false }
/>
);
}
return (
<TxRow
className={ className }
<tr
key={ operation }
tx={ {
hash: transactionHash,
blockNumber: blockNumber,
from: address,
to: to,
value: value,
input: bytesToHex(data)
} }
address={ address }
isTest={ isTest }
historic={ false }
/>
className={ className }
>
<td colSpan={ 5 }>
<code>{ operation }</code>
</td>
</tr>
);
}

View File

@ -22,9 +22,10 @@ import moment from 'moment';
import ContentCreate from 'material-ui/svg-icons/content/create';
import ActionDelete from 'material-ui/svg-icons/action/delete';
import ContentSend from 'material-ui/svg-icons/content/send';
import SettingsIcon from 'material-ui/svg-icons/action/settings';
import { nullableProptype } from '~/util/proptypes';
import { EditMeta, Transfer } from '~/modals';
import { EditMeta, Transfer, WalletSettings } from '~/modals';
import { Actionbar, Button, Page, Loading } from '~/ui';
import Delete from '../Address/Delete';
@ -74,6 +75,7 @@ class Wallet extends Component {
state = {
showEditDialog: false,
showSettingsDialog: false,
showTransferDialog: false,
showDeleteDialog: false
};
@ -115,6 +117,7 @@ class Wallet extends Component {
return (
<div className={ styles.wallet }>
{ this.renderEditDialog(wallet) }
{ this.renderSettingsDialog() }
{ this.renderTransferDialog() }
{ this.renderDeleteDialog(wallet) }
{ this.renderActionbar() }
@ -212,13 +215,18 @@ class Wallet extends Component {
<Button
key='delete'
icon={ <ActionDelete /> }
label='delete wallet'
label='delete'
onClick={ this.showDeleteDialog } />,
<Button
key='editmeta'
icon={ <ContentCreate /> }
label='edit'
onClick={ this.onEditClick } />
onClick={ this.onEditClick } />,
<Button
key='settings'
icon={ <SettingsIcon /> }
label='settings'
onClick={ this.onSettingsClick } />
];
return (
@ -250,11 +258,27 @@ class Wallet extends Component {
return (
<EditMeta
account={ wallet }
keys={ ['description', 'passwordHint'] }
keys={ ['description'] }
onClose={ this.onEditClick } />
);
}
renderSettingsDialog () {
const { wallet } = this.props;
const { showSettingsDialog } = this.state;
if (!showSettingsDialog) {
return null;
}
return (
<WalletSettings
wallet={ wallet }
onClose={ this.onSettingsClick }
/>
);
}
renderTransferDialog () {
const { showTransferDialog } = this.state;
@ -281,6 +305,12 @@ class Wallet extends Component {
});
}
onSettingsClick = () => {
this.setState({
showSettingsDialog: !this.state.showSettingsDialog
});
}
onTransferClick = () => {
this.setState({
showTransferDialog: !this.state.showTransferDialog