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 (
);
}
@@ -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 {