Several fixes to the Wallet in general (#4504)
* Fix address non-ellipsis on Wallet view * Add warning if daily limit has been reached * Add OOG warnings to TxHash * Fixes to the Warning * Added stringified version of the changes * Add wallet link for notifications * Fix tests * Use Formatted Messages * React Intl * s/ui.walletSettings/walletSettings in React Intl
This commit is contained in:
parent
da2e28dad1
commit
ace5c27211
@ -20,6 +20,7 @@ import { uniq } from 'lodash';
|
||||
|
||||
import { wallet as walletAbi } from '~/contracts/abi';
|
||||
import { bytesToHex } from '~/api/util/format';
|
||||
import { fromWei } from '~/api/util/wei';
|
||||
import Contract from '~/api/contract';
|
||||
import ERRORS from './errors';
|
||||
import { ERROR_CODES } from '~/api/transport/error';
|
||||
@ -39,6 +40,8 @@ const TITLES = {
|
||||
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
|
||||
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete];
|
||||
|
||||
export const WALLET_WARNING_SPENT_TODAY_LIMIT = 'WALLET_WARNING_SPENT_TODAY_LIMIT';
|
||||
|
||||
export default class TransferStore {
|
||||
@observable stage = 0;
|
||||
@observable extras = false;
|
||||
@ -65,6 +68,8 @@ export default class TransferStore {
|
||||
@observable value = '0.0';
|
||||
@observable valueError = null;
|
||||
|
||||
@observable walletWarning = null;
|
||||
|
||||
account = null;
|
||||
balance = null;
|
||||
onClose = null;
|
||||
@ -332,6 +337,21 @@ export default class TransferStore {
|
||||
valueError = this._validateDecimals(value);
|
||||
}
|
||||
|
||||
if (this.isWallet && !valueError) {
|
||||
const { last, limit, spent } = this.wallet.dailylimit;
|
||||
const remains = fromWei(limit.minus(spent));
|
||||
const today = Math.round(Date.now() / (24 * 3600 * 1000));
|
||||
const isResetable = last.lt(today);
|
||||
|
||||
if ((!isResetable && remains.lt(value)) || fromWei(limit).lt(value)) {
|
||||
// already spent too much today
|
||||
this.walletWarning = WALLET_WARNING_SPENT_TODAY_LIMIT;
|
||||
} else if (this.walletWarning) {
|
||||
// all ok
|
||||
this.walletWarning = null;
|
||||
}
|
||||
}
|
||||
|
||||
transaction(() => {
|
||||
this.value = value;
|
||||
this.valueError = valueError;
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { observer } from 'mobx-react';
|
||||
@ -28,7 +29,7 @@ import { nullableProptype } from '~/util/proptypes';
|
||||
import Details from './Details';
|
||||
import Extras from './Extras';
|
||||
|
||||
import TransferStore from './store';
|
||||
import TransferStore, { WALLET_WARNING_SPENT_TODAY_LIMIT } from './store';
|
||||
import styles from './transfer.css';
|
||||
|
||||
const STEP_DETAILS = 0;
|
||||
@ -71,6 +72,7 @@ class Transfer extends Component {
|
||||
visible
|
||||
>
|
||||
{ this.renderExceptionWarning() }
|
||||
{ this.renderWalletWarning() }
|
||||
{ this.renderPage() }
|
||||
</Modal>
|
||||
);
|
||||
@ -89,6 +91,29 @@ class Transfer extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderWalletWarning () {
|
||||
const { walletWarning } = this.store;
|
||||
|
||||
if (!walletWarning) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (walletWarning === WALLET_WARNING_SPENT_TODAY_LIMIT) {
|
||||
const warning = (
|
||||
<FormattedMessage
|
||||
id='transfer.warning.wallet_spent_limit'
|
||||
defaultMessage='This transaction value is above the remaining daily limit. It will need to be confirmed by other owners.'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Warning warning={ warning } />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderAccount () {
|
||||
const { account } = this.props;
|
||||
|
||||
|
@ -61,3 +61,8 @@
|
||||
margin-left: 0.125em;
|
||||
}
|
||||
|
||||
.modifications {
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pick } from 'lodash';
|
||||
@ -23,7 +24,7 @@ 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 { Button, Modal, TxHash, BusyStep, Form, TypedInput, InputAddress, AddressSelect } from '~/ui';
|
||||
import { Button, Modal, TxHash, BusyStep, Form, TypedInput, Input, InputAddress, AddressSelect } from '~/ui';
|
||||
import { fromWei } from '~/api/util/wei';
|
||||
|
||||
import WalletSettingsStore from './walletSettingsStore.js';
|
||||
@ -51,12 +52,27 @@ class WalletSettings extends Component {
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
title='rejected'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='walletSettings.rejected.title'
|
||||
defaultMessage='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.'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='walletSettings.rejected.busyStep.title'
|
||||
defaultMessage='The modifications have been rejected'
|
||||
/>
|
||||
}
|
||||
state={
|
||||
<FormattedMessage
|
||||
id='walletSettings.rejected.busyStep.state'
|
||||
defaultMessage='The wallet settings will not be modified. You can safely close this window.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
@ -112,57 +128,124 @@ class WalletSettings extends Component {
|
||||
|
||||
default:
|
||||
case 'EDIT':
|
||||
const { wallet, errors } = this.store;
|
||||
const { errors, fromString, wallet } = this.store;
|
||||
const { accountsInfo, 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...
|
||||
<FormattedMessage
|
||||
id='walletSettings.edit.message'
|
||||
defaultMessage={
|
||||
`In order to edit this contract's settings, at
|
||||
least { owners, number } { owners, plural,
|
||||
one { owner }
|
||||
other { owners }
|
||||
} have to
|
||||
send the very same modifications. You can paste a stringified version
|
||||
of the modifications here.`
|
||||
}
|
||||
values={ {
|
||||
owners: this.store.initialWallet.require.toNumber()
|
||||
} }
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Input
|
||||
hint='[ ... ]'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='walletSettings.modifications.fromString.label'
|
||||
defaultMessage='modifications'
|
||||
/>
|
||||
}
|
||||
onChange={ this.store.onModificationsStringChange }
|
||||
/>
|
||||
|
||||
<AddressSelect
|
||||
label='from account (wallet owner)'
|
||||
hint='send modifications as this owner'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='walletSettings.modifications.sender.label'
|
||||
defaultMessage='from account (wallet owner)'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='walletSettings.modifications.sender.hint'
|
||||
defaultMessage='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={ accountsInfo }
|
||||
param='address[]'
|
||||
/>
|
||||
<br />
|
||||
|
||||
<div className={ styles.splitInput }>
|
||||
<TypedInput
|
||||
label='required owners'
|
||||
hint='number of required owners to accept a transaction'
|
||||
error={ errors.require }
|
||||
min={ 1 }
|
||||
onChange={ this.store.onRequireChange }
|
||||
max={ wallet.owners.length }
|
||||
param='uint'
|
||||
value={ wallet.require }
|
||||
/>
|
||||
{
|
||||
fromString
|
||||
? null
|
||||
: (
|
||||
<div>
|
||||
<TypedInput
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='walletSettings.modifications.owners.label'
|
||||
defaultMessage='other wallet owners'
|
||||
/>
|
||||
}
|
||||
value={ wallet.owners.slice() }
|
||||
onChange={ this.store.onOwnersChange }
|
||||
accounts={ accountsInfo }
|
||||
param='address[]'
|
||||
/>
|
||||
|
||||
<TypedInput
|
||||
label='wallet day limit'
|
||||
hint='amount of ETH spendable without confirmations'
|
||||
value={ wallet.dailylimit }
|
||||
error={ errors.dailylimit }
|
||||
onChange={ this.store.onDailylimitChange }
|
||||
param='uint'
|
||||
isEth
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.splitInput }>
|
||||
<TypedInput
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='walletSettings.modifications.required.label'
|
||||
defaultMessage='required owners'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='walletSettings.modifications.required.hint'
|
||||
defaultMessage='number of required owners to accept a transaction'
|
||||
/>
|
||||
}
|
||||
error={ errors.require }
|
||||
min={ 1 }
|
||||
onChange={ this.store.onRequireChange }
|
||||
max={ wallet.owners.length }
|
||||
param='uint'
|
||||
value={ wallet.require }
|
||||
/>
|
||||
|
||||
<TypedInput
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='walletSettings.modifications.daylimit.label'
|
||||
defaultMessage='wallet day limit'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='walletSettings.modifications.daylimit.hint'
|
||||
defaultMessage='amount of ETH spendable without confirmations'
|
||||
/>
|
||||
}
|
||||
value={ wallet.dailylimit }
|
||||
error={ errors.dailylimit }
|
||||
onChange={ this.store.onDailylimitChange }
|
||||
param='uint'
|
||||
isEth
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -171,7 +254,12 @@ class WalletSettings extends Component {
|
||||
renderChanges (changes) {
|
||||
if (changes.length === 0) {
|
||||
return (
|
||||
<p>No modifications have been made to the Wallet settings.</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='walletSettings.changes.none'
|
||||
defaultMessage='No modifications have been made to the Wallet settings.'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@ -183,7 +271,31 @@ class WalletSettings extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>You are about to make the following modifications</p>
|
||||
<p className={ styles.modifications }>
|
||||
<FormattedMessage
|
||||
id='walletSettings.changes.modificationString'
|
||||
defaultMessage={
|
||||
`For your modifications to be taken into account,
|
||||
other owners have to send the same modifications. They can paste
|
||||
this string to make it easier:`
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<Input
|
||||
allowCopy
|
||||
label='modifications'
|
||||
readOnly
|
||||
value={ this.store.changesToString() }
|
||||
/>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='walletSettings.changes.overview'
|
||||
defaultMessage={
|
||||
`You are about to make the following modifications`
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
{ modifications }
|
||||
</div>
|
||||
);
|
||||
|
@ -30,10 +30,11 @@ const STEPS = {
|
||||
export default class WalletSettingsStore {
|
||||
accounts = {};
|
||||
|
||||
@observable step = null;
|
||||
@observable requests = [];
|
||||
@observable deployState = '';
|
||||
@observable done = false;
|
||||
@observable fromString = false;
|
||||
@observable requests = [];
|
||||
@observable step = null;
|
||||
|
||||
@observable wallet = {
|
||||
owners: null,
|
||||
@ -79,6 +80,51 @@ export default class WalletSettingsStore {
|
||||
.map((s) => s.idx);
|
||||
}
|
||||
|
||||
@action
|
||||
changesFromString (json) {
|
||||
try {
|
||||
const data = JSON.parse(json);
|
||||
const changes = data.map((datum) => {
|
||||
const [ type, valueStr ] = datum.split(';');
|
||||
|
||||
let value = valueStr;
|
||||
|
||||
// Only addresses start with `0x`, the others
|
||||
// are BigNumbers
|
||||
if (!/^0x/.test(valueStr)) {
|
||||
value = new BigNumber(valueStr, 16);
|
||||
}
|
||||
|
||||
return { type, value };
|
||||
});
|
||||
|
||||
this.changes = changes;
|
||||
} catch (error) {
|
||||
if (!(error instanceof SyntaxError)) {
|
||||
console.error('changes from string', error);
|
||||
}
|
||||
|
||||
this.changes = [];
|
||||
}
|
||||
}
|
||||
|
||||
changesToString () {
|
||||
const changes = this.changes.map((change) => {
|
||||
const { type, value } = change;
|
||||
|
||||
const valueStr = (value && typeof value.plus === 'function')
|
||||
? value.toString(16)
|
||||
: value;
|
||||
|
||||
return [
|
||||
type,
|
||||
valueStr
|
||||
].join(';');
|
||||
});
|
||||
|
||||
return JSON.stringify(changes);
|
||||
}
|
||||
|
||||
get changes () {
|
||||
const changes = [];
|
||||
|
||||
@ -127,6 +173,36 @@ export default class WalletSettingsStore {
|
||||
return changes;
|
||||
}
|
||||
|
||||
set changes (changes) {
|
||||
transaction(() => {
|
||||
this.wallet.dailylimit = this.initialWallet.dailylimit;
|
||||
this.wallet.require = this.initialWallet.require;
|
||||
this.wallet.owners = this.initialWallet.owners.slice();
|
||||
|
||||
changes.forEach((change) => {
|
||||
const { type, value } = change;
|
||||
|
||||
switch (type) {
|
||||
case 'dailylimit':
|
||||
this.wallet.dailylimit = value;
|
||||
break;
|
||||
|
||||
case 'require':
|
||||
this.wallet.require = value;
|
||||
break;
|
||||
|
||||
case 'remove_owner':
|
||||
this.wallet.owners = this.wallet.owners.filter((owner) => owner !== value);
|
||||
break;
|
||||
|
||||
case 'add_owner':
|
||||
this.wallet.owners.push(value);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
constructor (api, wallet) {
|
||||
this.api = api;
|
||||
this.step = this.stepsKeys[0];
|
||||
@ -177,6 +253,16 @@ export default class WalletSettingsStore {
|
||||
this.onChange({ dailylimit });
|
||||
}
|
||||
|
||||
@action onModificationsStringChange = (event, value) => {
|
||||
this.changesFromString(value);
|
||||
|
||||
if (this.changes && this.changes.length > 0) {
|
||||
this.fromString = true;
|
||||
} else {
|
||||
this.fromString = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action send = () => {
|
||||
const changes = this.changes;
|
||||
const walletInstance = this.walletInstance;
|
||||
|
@ -90,7 +90,11 @@ function setBalances (_balances, skipNotifications = false) {
|
||||
const txValue = value.minus(prevValue);
|
||||
|
||||
const redirectToAccount = () => {
|
||||
const route = `/accounts/${account.address}`;
|
||||
const basePath = account.wallet
|
||||
? 'wallet'
|
||||
: 'accounts';
|
||||
|
||||
const route = `/${basePath}/${account.address}`;
|
||||
|
||||
dispatch(push(route));
|
||||
};
|
||||
|
@ -21,8 +21,9 @@ import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { txLink } from '~/3rdparty/etherscan/links';
|
||||
import ShortenedHash from '../ShortenedHash';
|
||||
import Warning from '~/ui/Warning';
|
||||
|
||||
import ShortenedHash from '../ShortenedHash';
|
||||
import styles from './txHash.css';
|
||||
|
||||
class TxHash extends Component {
|
||||
@ -43,10 +44,44 @@ class TxHash extends Component {
|
||||
|
||||
state = {
|
||||
blockNumber: new BigNumber(0),
|
||||
gas: {},
|
||||
subscriptionId: null,
|
||||
transaction: null
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.fetchTransactionGas();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const prevHash = this.props.hash;
|
||||
const nextHash = nextProps.hash;
|
||||
|
||||
if (prevHash !== nextHash) {
|
||||
this.fetchTransactionGas(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the gas send for the current transaction
|
||||
* and save the value in the state
|
||||
*/
|
||||
fetchTransactionGas (props = this.props) {
|
||||
const { hash } = props;
|
||||
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.api.eth
|
||||
.getTransactionByHash(hash)
|
||||
.then((transaction = {}) => {
|
||||
const { gas = new BigNumber(0) } = transaction;
|
||||
|
||||
this.setState({ gas: { hash, value: gas } });
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { api } = this.context;
|
||||
|
||||
@ -73,6 +108,7 @@ class TxHash extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this.renderWarning() }
|
||||
<p>{
|
||||
summary
|
||||
? hashLink
|
||||
@ -87,6 +123,32 @@ class TxHash extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderWarning () {
|
||||
const { gas, transaction } = this.state;
|
||||
|
||||
if (!(transaction && transaction.blockNumber && transaction.blockNumber.gt(0))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { gasUsed = new BigNumber(0) } = transaction;
|
||||
const isOog = transaction.transactionHash === gas.hash && gasUsed.gte(gas.value);
|
||||
|
||||
if (!isOog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Warning
|
||||
warning={
|
||||
<FormattedMessage
|
||||
id='ui.txHash.oog'
|
||||
defaultMessage='The transaction might have gone out of gas. Try again with more gas.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderConfirmations () {
|
||||
const { maxConfirmations } = this.props;
|
||||
const { blockNumber, transaction } = this.state;
|
||||
|
@ -33,10 +33,17 @@ function createApi () {
|
||||
blockNumber = new BigNumber(100);
|
||||
api = {
|
||||
eth: {
|
||||
getTransactionByHash: (hash) => {
|
||||
return Promise.resolve({
|
||||
blockNumber: new BigNumber(100),
|
||||
gas: new BigNumber(42000)
|
||||
});
|
||||
},
|
||||
getTransactionReceipt: (hash) => {
|
||||
return Promise.resolve({
|
||||
blockNumber: new BigNumber(100),
|
||||
hash
|
||||
transactionHash: hash,
|
||||
gasUsed: new BigNumber(42000)
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -129,6 +136,14 @@ describe('ui/TxHash', () => {
|
||||
it('renders confirmation text', () => {
|
||||
expect(child.find('FormattedMessage').props().id).to.equal('ui.txHash.confirmations');
|
||||
});
|
||||
|
||||
it('renders with warnings', () => {
|
||||
expect(component.find('Warning')).to.have.length.gte(1);
|
||||
});
|
||||
|
||||
it('renders with oog warning', () => {
|
||||
expect(component.find('Warning').shallow().find('FormattedMessage').prop('id')).to.match(/oog/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -59,9 +59,15 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.addressline {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.address {
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
margin-left: 0.5em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tags {
|
||||
|
@ -55,6 +55,7 @@
|
||||
.header {
|
||||
flex: 1;
|
||||
margin-right: 0.25em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.details {
|
||||
|
Loading…
Reference in New Issue
Block a user