diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index 5e26ecd49..958f69037 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -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; diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js index 8751a1cd1..054c5f401 100644 --- a/js/src/modals/Transfer/transfer.js +++ b/js/src/modals/Transfer/transfer.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . 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() } ); @@ -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 = ( + + ); + + return ( + + ); + } + + return null; + } + renderAccount () { const { account } = this.props; diff --git a/js/src/modals/WalletSettings/walletSettings.css b/js/src/modals/WalletSettings/walletSettings.css index 5612c9011..6b087083e 100644 --- a/js/src/modals/WalletSettings/walletSettings.css +++ b/js/src/modals/WalletSettings/walletSettings.css @@ -61,3 +61,8 @@ margin-left: 0.125em; } +.modifications { + color: white; + margin: 0; +} + diff --git a/js/src/modals/WalletSettings/walletSettings.js b/js/src/modals/WalletSettings/walletSettings.js index 826dc8098..b4590c59c 100644 --- a/js/src/modals/WalletSettings/walletSettings.js +++ b/js/src/modals/WalletSettings/walletSettings.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . 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 ( + } actions={ this.renderDialogActions() } > + } + state={ + + } /> ); @@ -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 (

- 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... +

+ + } + onChange={ this.store.onModificationsStringChange } + /> + + } + hint={ + + } value={ wallet.sender } error={ errors.sender } onChange={ this.store.onSenderChange } accounts={ senders } /> - +
-
- + { + fromString + ? null + : ( +
+ + } + value={ wallet.owners.slice() } + onChange={ this.store.onOwnersChange } + accounts={ accountsInfo } + param='address[]' + /> - -
+
+ + } + hint={ + + } + error={ errors.require } + min={ 1 } + onChange={ this.store.onRequireChange } + max={ wallet.owners.length } + param='uint' + value={ wallet.require } + /> + + + } + hint={ + + } + value={ wallet.dailylimit } + error={ errors.dailylimit } + onChange={ this.store.onDailylimitChange } + param='uint' + isEth + /> +
+
+ ) + } ); } @@ -171,7 +254,12 @@ class WalletSettings extends Component { renderChanges (changes) { if (changes.length === 0) { return ( -

No modifications have been made to the Wallet settings.

+

+ +

); } @@ -183,7 +271,31 @@ class WalletSettings extends Component { return (
-

You are about to make the following modifications

+

+ +

+ + +

+ +

{ modifications }
); diff --git a/js/src/modals/WalletSettings/walletSettingsStore.js b/js/src/modals/WalletSettings/walletSettingsStore.js index d7ac0bff0..bff3458f6 100644 --- a/js/src/modals/WalletSettings/walletSettingsStore.js +++ b/js/src/modals/WalletSettings/walletSettingsStore.js @@ -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; diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js index fb515165a..d73152347 100644 --- a/js/src/redux/providers/balancesActions.js +++ b/js/src/redux/providers/balancesActions.js @@ -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)); }; diff --git a/js/src/ui/TxHash/txHash.js b/js/src/ui/TxHash/txHash.js index 8dec93085..6b8278a74 100644 --- a/js/src/ui/TxHash/txHash.js +++ b/js/src/ui/TxHash/txHash.js @@ -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 (
+ { this.renderWarning() }

{ 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 ( + + } + /> + ); + } + renderConfirmations () { const { maxConfirmations } = this.props; const { blockNumber, transaction } = this.state; diff --git a/js/src/ui/TxHash/txHash.spec.js b/js/src/ui/TxHash/txHash.spec.js index fb37528c1..328a43836 100644 --- a/js/src/ui/TxHash/txHash.spec.js +++ b/js/src/ui/TxHash/txHash.spec.js @@ -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/); + }); }); }); }); diff --git a/js/src/views/Account/Header/header.css b/js/src/views/Account/Header/header.css index 87e72517f..6709851bd 100644 --- a/js/src/views/Account/Header/header.css +++ b/js/src/views/Account/Header/header.css @@ -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 { diff --git a/js/src/views/Wallet/wallet.css b/js/src/views/Wallet/wallet.css index 8755c96b0..e54760d67 100644 --- a/js/src/views/Wallet/wallet.css +++ b/js/src/views/Wallet/wallet.css @@ -55,6 +55,7 @@ .header { flex: 1; margin-right: 0.25em; + overflow: hidden; } .details {