From fd41a10319ee1689d2d7dc74a355c1694e978d85 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Fri, 30 Dec 2016 12:28:12 +0100 Subject: [PATCH] Make Wallet first-class citizens (#3990) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed hint in Address Select + Wallet as first-class-citizen * Separate Owned and not Owned Wallets * Fix balance not updating * Fix MethodDecoding for Contract Deployment * Fix TypedInput params * Fix Token Transfer for Wallet * Small change to contracts * Fix wallets shown twice * Fix separation of accounts and wallets in Accounts * Fix linting * Execute contract methods from Wallet ✓ * Fixing linting * Wallet as first-class citizen: Part 1 (Manual) #3784 * Lower level wallet transaction convertion * Fix linting * Proper autoFocus on right Signer input * PR Grumble: don't show Wallets in dApps Permissions * Add postTransaction and gasEstimate wrapper methods * Extract Wallet postTx and gasEstimate to utils + PATCH api * Remove invalid test It's totally valid for input's length not to be a multiple of 32 bytes. EG. for Wallet Contracts * Merge master * Fix linting * Fix merge issue * Rename Portal * Rename Protal => Portal (typo) --- js/src/abi/encoder/encoder.js | 47 +++++----- js/src/abi/spec/interface.js | 4 + js/src/api/api.js | 6 +- js/src/api/contract/contract.js | 26 ++++-- js/src/api/transport/ws/ws.js | 5 +- js/src/api/util/decode.js | 2 - js/src/api/util/decode.spec.js | 4 - js/src/index.js | 2 + .../WalletDetails/walletDetails.js | 6 +- .../modals/DeployContract/deployContract.js | 10 +- .../modals/ExecuteContract/executeContract.js | 1 + js/src/modals/Transfer/store.js | 16 +--- .../modals/WalletSettings/walletSettings.js | 14 +-- .../WalletSettings/walletSettingsStore.js | 2 + js/src/redux/providers/balancesActions.js | 4 +- js/src/redux/providers/personalActions.js | 59 ++++++++++-- js/src/redux/providers/personalReducer.js | 9 +- js/src/redux/providers/signerReducer.js | 2 +- js/src/ui/Form/AddressSelect/addressSelect.js | 12 ++- .../Form/AddressSelect/addressSelectStore.js | 10 +- js/src/ui/Form/Input/input.js | 4 +- .../InputAddressSelect/inputAddressSelect.js | 9 +- js/src/ui/Form/TypedInput/typedInput.js | 30 +++--- .../ui/MethodDecoding/methodDecodingStore.js | 13 ++- js/src/ui/Portal/portal.js | 8 +- js/src/ui/TxHash/txHash.js | 2 +- js/src/util/tx.js | 83 +++++++++++++++++ js/src/util/wallets.js | 81 +++++++++++++++- js/src/views/Account/Header/header.js | 1 - js/src/views/Accounts/List/list.js | 5 +- js/src/views/Accounts/Summary/summary.js | 6 +- js/src/views/Accounts/accounts.js | 62 +++++-------- js/src/views/Dapps/dapps.js | 10 +- .../RequestPending/requestPending.js | 6 +- .../components/SignRequest/signRequest.js | 17 +++- .../TransactionPending/transactionPending.js | 8 +- .../transactionPendingFormConfirm.js | 60 ++++++++++-- .../transactionPendingForm.js | 13 ++- .../Signer/containers/Embedded/embedded.js | 3 +- .../containers/RequestsPage/requestsPage.js | 3 +- js/src/views/Wallet/Details/details.js | 22 +++-- .../views/Wallet/Transactions/transactions.js | 4 +- js/src/views/Wallet/wallet.js | 92 +++++++++++-------- js/webpack/libraries.js | 7 ++ js/webpack/npm.js | 3 + js/webpack/vendor.js | 7 ++ 46 files changed, 570 insertions(+), 230 deletions(-) diff --git a/js/src/abi/encoder/encoder.js b/js/src/abi/encoder/encoder.js index 8634b9511..6797e978d 100644 --- a/js/src/abi/encoder/encoder.js +++ b/js/src/abi/encoder/encoder.js @@ -25,7 +25,7 @@ export default class Encoder { throw new Error('tokens should be array of Token'); } - const mediates = tokens.map((token) => Encoder.encodeToken(token)); + const mediates = tokens.map((token, index) => Encoder.encodeToken(token, index)); const inits = mediates .map((mediate, idx) => mediate.init(Mediate.offsetFor(mediates, idx))) .join(''); @@ -36,37 +36,40 @@ export default class Encoder { return `${inits}${closings}`; } - static encodeToken (token) { + static encodeToken (token, index = 0) { if (!isInstanceOf(token, Token)) { throw new Error('token should be instanceof Token'); } - switch (token.type) { - case 'address': - return new Mediate('raw', padAddress(token.value)); + try { + switch (token.type) { + case 'address': + return new Mediate('raw', padAddress(token.value)); - case 'int': - case 'uint': - return new Mediate('raw', padU32(token.value)); + case 'int': + case 'uint': + return new Mediate('raw', padU32(token.value)); - case 'bool': - return new Mediate('raw', padBool(token.value)); + case 'bool': + return new Mediate('raw', padBool(token.value)); - case 'fixedBytes': - return new Mediate('raw', padFixedBytes(token.value)); + case 'fixedBytes': + return new Mediate('raw', padFixedBytes(token.value)); - case 'bytes': - return new Mediate('prefixed', padBytes(token.value)); + case 'bytes': + return new Mediate('prefixed', padBytes(token.value)); - case 'string': - return new Mediate('prefixed', padString(token.value)); + case 'string': + return new Mediate('prefixed', padString(token.value)); - case 'fixedArray': - case 'array': - return new Mediate(token.type, token.value.map((token) => Encoder.encodeToken(token))); - - default: - throw new Error(`Invalid token type ${token.type} in encodeToken`); + case 'fixedArray': + case 'array': + return new Mediate(token.type, token.value.map((token) => Encoder.encodeToken(token))); + } + } catch (e) { + throw new Error(`Cannot encode token #${index} [${token.type}: ${token.value}]. ${e.message}`); } + + throw new Error(`Invalid token type ${token.type} in encodeToken`); } } diff --git a/js/src/abi/spec/interface.js b/js/src/abi/spec/interface.js index 9116f5ca3..3e1b5de4d 100644 --- a/js/src/abi/spec/interface.js +++ b/js/src/abi/spec/interface.js @@ -41,6 +41,10 @@ export default class Interface { } encodeTokens (paramTypes, values) { + return Interface.encodeTokens(paramTypes, values); + } + + static encodeTokens (paramTypes, values) { const createToken = function (paramType, value) { if (paramType.subtype) { return new Token(paramType.type, value.map((entry) => createToken(paramType.subtype, entry))); diff --git a/js/src/api/api.js b/js/src/api/api.js index 9e0ad11b8..bb622ab46 100644 --- a/js/src/api/api.js +++ b/js/src/api/api.js @@ -114,7 +114,11 @@ export default class Api { } }) .catch((error) => { - console.error('pollMethod', error); + // Don't print if the request is rejected: that's ok + if (error.type !== 'REQUEST_REJECTED') { + console.error('pollMethod', error); + } + reject(error); }); }; diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index eacb21fd1..af22191e5 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import Abi from '../../abi'; +import Abi from '~/abi'; let nextSubscriptionId = 0; @@ -53,6 +53,10 @@ export default class Contract { this._subscribedToBlock = false; this._blockSubscriptionId = null; + + if (api && api.patch && api.patch.contract) { + api.patch.contract(this); + } } get address () { @@ -90,8 +94,10 @@ export default class Contract { } deployEstimateGas (options, values) { + const _options = this._encodeOptions(this.constructors[0], options, values); + return this._api.eth - .estimateGas(this._encodeOptions(this.constructors[0], options, values)) + .estimateGas(_options) .then((gasEst) => { return [gasEst, gasEst.mul(1.2)]; }); @@ -115,8 +121,10 @@ export default class Contract { setState({ state: 'postTransaction', gas }); + const _options = this._encodeOptions(this.constructors[0], options, values); + return this._api.parity - .postTransaction(this._encodeOptions(this.constructors[0], options, values)) + .postTransaction(_options) .then((requestId) => { setState({ state: 'checkRequest', requestId }); return this._pollCheckRequest(requestId); @@ -199,7 +207,7 @@ export default class Contract { getCallData = (func, options, values) => { let data = options.data; - const tokens = func ? this._abi.encodeTokens(func.inputParamTypes(), values) : null; + const tokens = func ? Abi.encodeTokens(func.inputParamTypes(), values) : null; const call = tokens ? func.encodeCall(tokens) : null; if (data && data.substr(0, 2) === '0x') { @@ -221,6 +229,8 @@ export default class Contract { } _bindFunction = (func) => { + func.contract = this; + func.call = (options, values = []) => { const callParams = this._encodeOptions(func, this._addOptionsTo(options), values); @@ -233,13 +243,13 @@ export default class Contract { if (!func.constant) { func.postTransaction = (options, values = []) => { - return this._api.parity - .postTransaction(this._encodeOptions(func, this._addOptionsTo(options), values)); + const _options = this._encodeOptions(func, this._addOptionsTo(options), values); + return this._api.parity.postTransaction(_options); }; func.estimateGas = (options, values = []) => { - return this._api.eth - .estimateGas(this._encodeOptions(func, this._addOptionsTo(options), values)); + const _options = this._encodeOptions(func, this._addOptionsTo(options), values); + return this._api.eth.estimateGas(_options); }; } diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index 43f1403ce..591cf3062 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -209,7 +209,10 @@ export default class Ws extends JsonRpcBase { if (result.error) { this.error(event.data); - console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`); + // Don't print error if request rejected... + if (!/rejected/.test(result.error.message)) { + console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`); + } const error = new TransportError(method, result.error.code, result.error.message); reject(error); diff --git a/js/src/api/util/decode.js b/js/src/api/util/decode.js index a6a2ddda4..0e0164bec 100644 --- a/js/src/api/util/decode.js +++ b/js/src/api/util/decode.js @@ -47,8 +47,6 @@ export function decodeMethodInput (methodAbi, paramdata) { throw new Error('Input to decodeMethodInput should be a hex value'); } else if (paramdata.substr(0, 2) === '0x') { return decodeMethodInput(methodAbi, paramdata.slice(2)); - } else if (paramdata.length % 64 !== 0) { - throw new Error('Parameter length in decodeMethodInput not a multiple of 64 characters'); } } diff --git a/js/src/api/util/decode.spec.js b/js/src/api/util/decode.spec.js index 4652c7c5b..fa0102365 100644 --- a/js/src/api/util/decode.spec.js +++ b/js/src/api/util/decode.spec.js @@ -48,10 +48,6 @@ describe('api/util/decode', () => { expect(() => decodeMethodInput({}, 'invalid')).to.throw(/should be a hex value/); }); - it('throws on invalid lengths', () => { - expect(() => decodeMethodInput({}, DATA.slice(-32))).to.throw(/not a multiple of/); - }); - it('correctly decodes valid inputs', () => { expect(decodeMethodInput({ type: 'function', diff --git a/js/src/index.js b/js/src/index.js index 77f247b3b..72f26e12f 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -36,6 +36,7 @@ import ContextProvider from '~/ui/ContextProvider'; import muiTheme from '~/ui/Theme'; import MainApplication from './main'; +import { patchApi } from '~/util/tx'; import { setApi } from '~/redux/providers/apiActions'; import './environment'; @@ -60,6 +61,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) { } const api = new SecureApi(`ws://${parityUrl}`, token); +patchApi(api); ContractInstances.create(api); const store = initStore(api, hashHistory); diff --git a/js/src/modals/CreateWallet/WalletDetails/walletDetails.js b/js/src/modals/CreateWallet/WalletDetails/walletDetails.js index 17a7c68a3..bab1a09f6 100644 --- a/js/src/modals/CreateWallet/WalletDetails/walletDetails.js +++ b/js/src/modals/CreateWallet/WalletDetails/walletDetails.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { omitBy } from 'lodash'; import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui'; @@ -73,6 +74,9 @@ export default class WalletDetails extends Component { renderMultisigDetails () { const { accounts, wallet, errors } = this.props; + // Wallets cannot create contracts + const _accounts = omitBy(accounts, (a) => a.wallet); + return (
. -import { pick } from 'lodash'; +import { pick, omitBy } from 'lodash'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -561,13 +561,19 @@ class DeployContract extends Component { } function mapStateToProps (initState, initProps) { - const fromAddresses = Object.keys(initProps.accounts); + const { accounts } = initProps; + + // Skip Wallet accounts : they can't create Contracts + const _accounts = omitBy(accounts, (a) => a.wallet); + + const fromAddresses = Object.keys(_accounts); return (state) => { const balances = pick(state.balances.balances, fromAddresses); const { gasLimit } = state.nodeStatus; return { + accounts: _accounts, balances, gasLimit }; diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index 0836b1bf1..594ad3922 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -389,6 +389,7 @@ class ExecuteContract extends Component { const { advancedOptions, amount, func, minBlock, values } = this.state; const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC; const finalstep = steps.length - 1; + const options = { gas: this.gasStore.gas, gasPrice: this.gasStore.price, diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index c65a25fab..d2796b098 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -383,9 +383,7 @@ export default class TransferStore { const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag); 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); + const available = new BigNumber(senderBalance.value).div(format); let { value, valueError } = this; let totalEth = gasTotal; @@ -428,7 +426,6 @@ export default class TransferStore { send () { const { options, values } = this._getTransferParams(); - options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null; return this._getTransferMethod().postTransaction(options, values); @@ -440,16 +437,7 @@ export default class TransferStore { } estimateGas () { - if (this.isEth || !this.isWallet) { - return this._estimateGas(); - } - - return Promise - .all([ - this._estimateGas(true), - this._estimateGas() - ]) - .then((results) => results[0].plus(results[1])); + return this._estimateGas(); } _getTransferMethod (gas = false, forceToken = false) { diff --git a/js/src/modals/WalletSettings/walletSettings.js b/js/src/modals/WalletSettings/walletSettings.js index 088c0d657..36b8eb1b6 100644 --- a/js/src/modals/WalletSettings/walletSettings.js +++ b/js/src/modals/WalletSettings/walletSettings.js @@ -36,7 +36,7 @@ class WalletSettings extends Component { }; static propTypes = { - accounts: PropTypes.object.isRequired, + accountsInfo: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, senders: PropTypes.object.isRequired @@ -113,7 +113,7 @@ class WalletSettings extends Component { default: case 'EDIT': const { wallet, errors } = this.store; - const { accounts, senders } = this.props; + const { accountsInfo, senders } = this.props; return ( @@ -137,7 +137,7 @@ class WalletSettings extends Component { label='other wallet owners' value={ wallet.owners.slice() } onChange={ this.store.onOwnersChange } - accounts={ accounts } + accounts={ accountsInfo } param='address[]' /> @@ -190,7 +190,7 @@ class WalletSettings extends Component { } renderChange (change) { - const { accounts } = this.props; + const { accountsInfo } = this.props; switch (change.type) { case 'dailylimit': @@ -229,7 +229,7 @@ class WalletSettings extends Component { @@ -243,7 +243,7 @@ class WalletSettings extends Component { @@ -329,7 +329,7 @@ function mapStateToProps (initState, initProps) { const senders = pick(accounts, owners); return () => { - return { accounts: accountsInfo, senders }; + return { accountsInfo, senders }; }; } diff --git a/js/src/modals/WalletSettings/walletSettingsStore.js b/js/src/modals/WalletSettings/walletSettingsStore.js index 6db4469c3..6d70b8de3 100644 --- a/js/src/modals/WalletSettings/walletSettingsStore.js +++ b/js/src/modals/WalletSettings/walletSettingsStore.js @@ -28,6 +28,8 @@ const STEPS = { }; export default class WalletSettingsStore { + accounts = {}; + @observable step = null; @observable requests = []; @observable deployState = ''; diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js index 838f850d5..b99c44fcb 100644 --- a/js/src/redux/providers/balancesActions.js +++ b/js/src/redux/providers/balancesActions.js @@ -175,7 +175,7 @@ export function fetchBalances (_addresses) { const { api, personal } = getState(); const { visibleAccounts, accounts } = personal; - const addresses = uniq(_addresses || visibleAccounts || []); + const addresses = uniq((_addresses || visibleAccounts || []).concat(Object.keys(accounts))); if (addresses.length === 0) { return Promise.resolve(); @@ -183,7 +183,7 @@ export function fetchBalances (_addresses) { const fullFetch = addresses.length === 1; - const addressesToFetch = uniq(addresses.concat(Object.keys(accounts))); + const addressesToFetch = uniq(addresses); return Promise .all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch))) diff --git a/js/src/redux/providers/personalActions.js b/js/src/redux/providers/personalActions.js index 7ca7c3374..1ed39c05a 100644 --- a/js/src/redux/providers/personalActions.js +++ b/js/src/redux/providers/personalActions.js @@ -14,14 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { isEqual } from 'lodash'; +import { isEqual, intersection } from 'lodash'; import { fetchBalances } from './balancesActions'; import { attachWallets } from './walletActions'; +import Contract from '~/api/contract'; import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore'; +import WalletsUtils from '~/util/wallets'; +import { wallet as WalletAbi } from '~/contracts/abi'; export function personalAccountsInfo (accountsInfo) { + const addresses = []; const accounts = {}; const contacts = {}; const contracts = {}; @@ -32,6 +36,7 @@ export function personalAccountsInfo (accountsInfo) { .filter((account) => account.uuid || !account.meta.deleted) .forEach((account) => { if (account.uuid) { + addresses.push(account.address); accounts[account.address] = account; } else if (account.meta.wallet) { account.wallet = true; @@ -46,14 +51,52 @@ export function personalAccountsInfo (accountsInfo) { // Load user contracts for Method Decoding MethodDecodingStore.loadContracts(contracts); - return (dispatch) => { - const data = { - accountsInfo, - accounts, contacts, contracts, wallets - }; + return (dispatch, getState) => { + const { api } = getState(); - dispatch(_personalAccountsInfo(data)); - dispatch(attachWallets(wallets)); + const _fetchOwners = Object + .values(wallets) + .map((wallet) => { + const walletContract = new Contract(api, WalletAbi); + return WalletsUtils.fetchOwners(walletContract.at(wallet.address)); + }); + + Promise + .all(_fetchOwners) + .then((walletsOwners) => { + return Object + .values(wallets) + .map((wallet, index) => { + wallet.owners = walletsOwners[index].map((owner) => ({ + address: owner, + name: accountsInfo[owner] && accountsInfo[owner].name || owner + })); + + return wallet; + }); + }) + .then((_wallets) => { + _wallets.forEach((wallet) => { + const owners = wallet.owners.map((o) => o.address); + + // Owners ∩ Addresses not null : Wallet is owned + // by one of the accounts + if (intersection(owners, addresses).length > 0) { + accounts[wallet.address] = wallet; + } else { + contacts[wallet.address] = wallet; + } + }); + + const data = { + accountsInfo, + accounts, contacts, contracts + }; + + dispatch(_personalAccountsInfo(data)); + dispatch(attachWallets(wallets)); + dispatch(fetchBalances()); + }); }; } diff --git a/js/src/redux/providers/personalReducer.js b/js/src/redux/providers/personalReducer.js index 19ee421f0..daadd54b3 100644 --- a/js/src/redux/providers/personalReducer.js +++ b/js/src/redux/providers/personalReducer.js @@ -25,14 +25,13 @@ const initialState = { hasContacts: false, contracts: {}, hasContracts: false, - wallet: {}, - hasWallets: false, visibleAccounts: [] }; export default handleActions({ personalAccountsInfo (state, action) { - const { accountsInfo, accounts, contacts, contracts, wallets } = action; + const accountsInfo = action.accountsInfo || state.accountsInfo; + const { accounts, contacts, contracts } = action; return Object.assign({}, state, { accountsInfo, @@ -41,9 +40,7 @@ export default handleActions({ contacts, hasContacts: Object.keys(contacts).length !== 0, contracts, - hasContracts: Object.keys(contracts).length !== 0, - wallets, - hasWallets: Object.keys(wallets).length !== 0 + hasContracts: Object.keys(contracts).length !== 0 }); }, diff --git a/js/src/redux/providers/signerReducer.js b/js/src/redux/providers/signerReducer.js index c6d55f140..835fe8a70 100644 --- a/js/src/redux/providers/signerReducer.js +++ b/js/src/redux/providers/signerReducer.js @@ -90,7 +90,7 @@ export default handleActions({ signerSuccessRejectRequest (state, action) { const { id } = action.payload; const rejected = Object.assign( - state.pending.find(p => p.id === id), + state.pending.find(p => p.id === id) || { id }, { status: 'rejected' } ); return { diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index fcf48ab94..31e4d2207 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -26,8 +26,8 @@ import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline'; import AccountCard from '~/ui/AccountCard'; import InputAddress from '~/ui/Form/InputAddress'; import Portal from '~/ui/Portal'; -import { validateAddress } from '~/util/validation'; import { nodeOrStringProptype } from '~/util/proptypes'; +import { validateAddress } from '~/util/validation'; import AddressSelectStore from './addressSelectStore'; import styles from './addressSelect.css'; @@ -40,6 +40,7 @@ let currentId = 1; @observer class AddressSelect extends Component { static contextTypes = { + intl: React.PropTypes.object.isRequired, api: PropTypes.object.isRequired, muiTheme: PropTypes.object.isRequired }; @@ -55,7 +56,6 @@ class AddressSelect extends Component { contacts: PropTypes.object, contracts: PropTypes.object, tokens: PropTypes.object, - wallets: PropTypes.object, // Optional props allowInput: PropTypes.bool, @@ -160,6 +160,12 @@ class AddressSelect extends Component { } const id = `addressSelect_${++currentId}`; + const ilHint = typeof hint === 'string' || !(hint && hint.props) + ? (hint || '') + : this.context.intl.formatMessage( + hint.props, + hint.props.values || {} + ); return ( ), - values: [].concat( - Object.values(wallets), - Object.values(accounts) - ) + values: Object.values(accounts) }, { key: 'contacts', diff --git a/js/src/ui/Form/Input/input.js b/js/src/ui/Form/Input/input.js index 616bf23b1..d82ed8cf4 100644 --- a/js/src/ui/Form/Input/input.js +++ b/js/src/ui/Form/Input/input.js @@ -51,6 +51,7 @@ export default class Input extends Component { PropTypes.string, PropTypes.bool ]), + autoFocus: PropTypes.bool, children: PropTypes.node, className: PropTypes.string, disabled: PropTypes.bool, @@ -112,7 +113,7 @@ export default class Input extends Component { render () { const { value } = this.state; - const { children, className, hideUnderline, disabled, error, focused, label } = this.props; + const { autoFocus, children, className, hideUnderline, disabled, error, focused, label } = this.props; const { hint, onClick, onFocus, multiLine, rows, type, min, max, style, tabIndex } = this.props; const readOnly = this.props.readOnly || disabled; @@ -138,6 +139,7 @@ export default class Input extends Component { { this.renderCopyButton() } { - const { value, onChange, param } = this.props; + const { value, onChange } = this.props; + const param = this.getParam(); + const newValues = [].concat(value, param.subtype.default); onChange(newValues); @@ -392,4 +388,14 @@ export default class TypedInput extends Component { onChange(newValues); } + getParam = () => { + const { param } = this.props; + + if (typeof param === 'string') { + return parseAbiType(param); + } + + return param; + } + } diff --git a/js/src/ui/MethodDecoding/methodDecodingStore.js b/js/src/ui/MethodDecoding/methodDecodingStore.js index 3b5ccb5b0..5d518d3a9 100644 --- a/js/src/ui/MethodDecoding/methodDecodingStore.js +++ b/js/src/ui/MethodDecoding/methodDecodingStore.js @@ -118,6 +118,15 @@ export default class MethodDecodingStore { return Promise.resolve(result); } + try { + const { signature } = this.api.util.decodeCallData(input); + + if (signature === CONTRACT_CREATE || transaction.creates) { + result.contract = true; + return Promise.resolve({ ...result, deploy: true }); + } + } catch (e) {} + return this .isContract(contractAddress || transaction.creates) .then((isContract) => { @@ -132,7 +141,7 @@ export default class MethodDecodingStore { result.params = paramdata; // Contract deployment - if (!signature || signature === CONTRACT_CREATE || transaction.creates) { + if (!signature) { return Promise.resolve({ ...result, deploy: true }); } @@ -192,7 +201,7 @@ export default class MethodDecodingStore { */ isContract (contractAddress) { // If zero address, it isn't a contract - if (/^(0x)?0*$/.test(contractAddress)) { + if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) { return Promise.resolve(false); } diff --git a/js/src/ui/Portal/portal.js b/js/src/ui/Portal/portal.js index f81127540..8ab96c151 100644 --- a/js/src/ui/Portal/portal.js +++ b/js/src/ui/Portal/portal.js @@ -16,7 +16,7 @@ import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; -import Portal from 'react-portal'; +import ReactPortal from 'react-portal'; import keycode from 'keycode'; import { CloseIcon } from '~/ui/Icons'; @@ -24,7 +24,7 @@ import ParityBackground from '~/ui/ParityBackground'; import styles from './portal.css'; -export default class Protal extends Component { +export default class Portal extends Component { static propTypes = { onClose: PropTypes.func.isRequired, @@ -65,7 +65,7 @@ export default class Protal extends Component { } return ( - +
- + ); } diff --git a/js/src/ui/TxHash/txHash.js b/js/src/ui/TxHash/txHash.js index fcbe374e1..09905d594 100644 --- a/js/src/ui/TxHash/txHash.js +++ b/js/src/ui/TxHash/txHash.js @@ -134,7 +134,7 @@ class TxHash extends Component { const { api } = this.context; const { hash } = this.props; - if (error) { + if (error || !hash || /^(0x)?0*$/.test(hash)) { return; } diff --git a/js/src/util/tx.js b/js/src/util/tx.js index 591931ff3..90c78a42d 100644 --- a/js/src/util/tx.js +++ b/js/src/util/tx.js @@ -14,10 +14,93 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import WalletsUtils from '~/util/wallets'; + const isValidReceipt = (receipt) => { return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); }; +function getTxArgs (func, options, values = []) { + const { contract } = func; + const { api } = contract; + const address = options.from; + + if (!address) { + return Promise.resolve({ func, options, values }); + } + + return WalletsUtils + .isWallet(api, address) + .then((isWallet) => { + if (!isWallet) { + return { func, options, values }; + } + + options.data = contract.getCallData(func, options, values); + options.to = options.to || contract.address; + + if (!options.to) { + return { func, options, values }; + } + + return WalletsUtils + .getCallArgs(api, options, values) + .then((callArgs) => { + if (!callArgs) { + return { func, options, values }; + } + + return callArgs; + }); + }); +} + +export function estimateGas (_func, _options, _values = []) { + return getTxArgs(_func, _options, _values) + .then((callArgs) => { + const { func, options, values } = callArgs; + return func._estimateGas(options, values); + }) + .then((gas) => { + return WalletsUtils + .isWallet(_func.contract.api, _options.from) + .then((isWallet) => { + if (isWallet) { + return gas.mul(1.5); + } + + return gas; + }); + }); +} + +export function postTransaction (_func, _options, _values = []) { + return getTxArgs(_func, _options, _values) + .then((callArgs) => { + const { func, options, values } = callArgs; + return func._postTransaction(options, values); + }); +} + +export function patchApi (api) { + api.patch = { + ...api.patch, + contract: patchContract + }; +} + +export function patchContract (contract) { + contract._functions.forEach((func) => { + if (!func.constant) { + func._postTransaction = func.postTransaction; + func._estimateGas = func.estimateGas; + + func.postTransaction = postTransaction.bind(contract, func); + func.estimateGas = estimateGas.bind(contract, func); + } + }); +} + export function checkIfTxFailed (api, tx, gasSent) { return api.pollMethod('eth_getTransactionReceipt', tx) .then((receipt) => { diff --git a/js/src/util/wallets.js b/js/src/util/wallets.js index 3732840d8..739a1e3c2 100644 --- a/js/src/util/wallets.js +++ b/js/src/util/wallets.js @@ -14,13 +14,92 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { range, uniq } from 'lodash'; +import BigNumber from 'bignumber.js'; +import { intersection, range, uniq } from 'lodash'; +import Contract from '~/api/contract'; import { bytesToHex, toHex } from '~/api/util/format'; import { validateAddress } from '~/util/validation'; +import WalletAbi from '~/contracts/abi/wallet.json'; + +const _cachedWalletLookup = {}; export default class WalletsUtils { + static getCallArgs (api, options, values = []) { + const walletContract = new Contract(api, WalletAbi); + + const promises = [ + api.parity.accountsInfo(), + WalletsUtils.fetchOwners(walletContract.at(options.from)) + ]; + + return Promise + .all(promises) + .then(([ accounts, owners ]) => { + const addresses = Object.keys(accounts); + const owner = intersection(addresses, owners).pop(); + + if (!owner) { + return false; + } + + return owner; + }) + .then((owner) => { + if (!owner) { + return false; + } + + const _options = Object.assign({}, options); + const { from, to, value = new BigNumber(0), data } = options; + + delete _options.data; + + const nextValues = [ to, value, data ]; + const nextOptions = { + ..._options, + from: owner, + to: from, + value: new BigNumber(0) + }; + + const execFunc = walletContract.instance.execute; + + return { func: execFunc, options: nextOptions, values: nextValues }; + }); + } + + /** + * Check whether the given address could be + * a Wallet. The result is cached in order not + * to make unnecessary calls on non-wallet accounts + */ + static isWallet (api, address) { + if (!_cachedWalletLookup[address]) { + const walletContract = new Contract(api, WalletAbi); + + _cachedWalletLookup[address] = walletContract + .at(address) + .instance + .m_numOwners + .call() + .then((result) => { + if (!result || result.equals(0)) { + return false; + } + + return true; + }) + .then((bool) => { + _cachedWalletLookup[address] = Promise.resolve(bool); + return bool; + }); + } + + return _cachedWalletLookup[address]; + } + static fetchRequire (walletContract) { return walletContract.instance.m_required.call(); } diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index 058c20db3..6e508d05e 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -42,7 +42,6 @@ export default class Header extends Component { render () { const { account, balance, className, children, hideName } = this.props; const { address, meta, uuid } = account; - if (!account) { return null; } diff --git a/js/src/views/Accounts/List/list.js b/js/src/views/Accounts/List/list.js index 2bd7fadc1..9cebdda6e 100644 --- a/js/src/views/Accounts/List/list.js +++ b/js/src/views/Accounts/List/list.js @@ -34,7 +34,6 @@ class List extends Component { order: PropTypes.string, orderFallback: PropTypes.string, search: PropTypes.array, - walletsOwners: PropTypes.object, fetchCertifiers: PropTypes.func.isRequired, fetchCertifications: PropTypes.func.isRequired, @@ -58,7 +57,7 @@ class List extends Component { } renderAccounts () { - const { accounts, balances, empty, link, walletsOwners, handleAddSearchToken } = this.props; + const { accounts, balances, empty, link, handleAddSearchToken } = this.props; if (empty) { return ( @@ -76,7 +75,7 @@ class List extends Component { const account = accounts[address] || {}; const balance = balances[address] || {}; - const owners = walletsOwners && walletsOwners[address] || null; + const owners = account.owners || null; return (
diff --git a/js/src/views/Accounts/accounts.js b/js/src/views/Accounts/accounts.js index 70b6f770a..06322e436 100644 --- a/js/src/views/Accounts/accounts.js +++ b/js/src/views/Accounts/accounts.js @@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import ContentAdd from 'material-ui/svg-icons/content/add'; -import { uniq, isEqual } from 'lodash'; +import { uniq, isEqual, pickBy, omitBy } from 'lodash'; import List from './List'; import { CreateAccount, CreateWallet } from '~/modals'; @@ -36,9 +36,6 @@ class Accounts extends Component { setVisibleAccounts: PropTypes.func.isRequired, accounts: PropTypes.object.isRequired, hasAccounts: PropTypes.bool.isRequired, - wallets: PropTypes.object.isRequired, - walletsOwners: PropTypes.object.isRequired, - hasWallets: PropTypes.bool.isRequired, balances: PropTypes.object } @@ -62,8 +59,8 @@ class Accounts extends Component { } componentWillReceiveProps (nextProps) { - const prevAddresses = Object.keys({ ...this.props.accounts, ...this.props.wallets }); - const nextAddresses = Object.keys({ ...nextProps.accounts, ...nextProps.wallets }); + const prevAddresses = Object.keys(this.props.accounts); + const nextAddresses = Object.keys(nextProps.accounts); if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) { this.setVisibleAccounts(nextProps); @@ -75,8 +72,8 @@ class Accounts extends Component { } setVisibleAccounts (props = this.props) { - const { accounts, wallets, setVisibleAccounts } = props; - const addresses = Object.keys({ ...accounts, ...wallets }); + const { accounts, setVisibleAccounts } = props; + const addresses = Object.keys(accounts); setVisibleAccounts(addresses); } @@ -115,30 +112,38 @@ class Accounts extends Component { } renderAccounts () { + const { accounts, balances } = this.props; + + const _accounts = omitBy(accounts, (a) => a.wallet); + const _hasAccounts = Object.keys(_accounts).length > 0; + if (!this.state.show) { - return this.renderLoading(this.props.accounts); + return this.renderLoading(_accounts); } - const { accounts, hasAccounts, balances } = this.props; const { searchValues, sortOrder } = this.state; return ( ); } renderWallets () { + const { accounts, balances } = this.props; + + const wallets = pickBy(accounts, (a) => a.wallet); + const hasWallets = Object.keys(wallets).length > 0; + if (!this.state.show) { - return this.renderLoading(this.props.wallets); + return this.renderLoading(wallets); } - const { wallets, hasWallets, balances, walletsOwners } = this.props; const { searchValues, sortOrder } = this.state; if (!wallets || Object.keys(wallets).length === 0) { @@ -154,7 +159,6 @@ class Accounts extends Component { empty={ !hasWallets } order={ sortOrder } handleAddSearchToken={ this.onAddSearchToken } - walletsOwners={ walletsOwners } /> ); } @@ -287,34 +291,12 @@ class Accounts extends Component { } function mapStateToProps (state) { - const { accounts, hasAccounts, wallets, hasWallets, accountsInfo } = state.personal; + const { accounts, hasAccounts } = state.personal; const { balances } = state.balances; - const walletsInfo = state.wallet.wallets; - - const walletsOwners = Object - .keys(walletsInfo) - .map((wallet) => { - const owners = walletsInfo[wallet].owners || []; - - return { - owners: 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, + accounts: accounts, + hasAccounts: hasAccounts, balances }; } diff --git a/js/src/views/Dapps/dapps.js b/js/src/views/Dapps/dapps.js index 6462e1af9..e800263e4 100644 --- a/js/src/views/Dapps/dapps.js +++ b/js/src/views/Dapps/dapps.js @@ -20,6 +20,7 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import { omitBy } from 'lodash'; import { AddDapps, DappPermissions } from '~/modals'; import PermissionStore from '~/modals/DappPermissions/store'; @@ -150,8 +151,15 @@ class Dapps extends Component { function mapStateToProps (state) { const { accounts } = state.personal; + /** + * Do not show the Wallet Accounts in the Dapps + * Permissions Modal. This will come in v1.6, but + * for now it would break dApps using Web3... + */ + const _accounts = omitBy(accounts, (account) => account.wallet); + return { - accounts + accounts: _accounts }; } diff --git a/js/src/views/Signer/components/RequestPending/requestPending.js b/js/src/views/Signer/components/RequestPending/requestPending.js index 84783a614..3e8586063 100644 --- a/js/src/views/Signer/components/RequestPending/requestPending.js +++ b/js/src/views/Signer/components/RequestPending/requestPending.js @@ -23,6 +23,7 @@ export default class RequestPending extends Component { static propTypes = { className: PropTypes.string, date: PropTypes.instanceOf(Date).isRequired, + focus: PropTypes.bool, gasLimit: PropTypes.object.isRequired, id: PropTypes.object.isRequired, isSending: PropTypes.bool.isRequired, @@ -38,6 +39,7 @@ export default class RequestPending extends Component { }; static defaultProps = { + focus: false, isSending: false }; @@ -49,7 +51,7 @@ export default class RequestPending extends Component { }; render () { - const { className, date, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props; + const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props; if (payload.sign) { const { sign } = payload; @@ -58,6 +60,7 @@ export default class RequestPending extends Component { diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js index 360125d9f..02b7ef266 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import ReactDOM from 'react-dom'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import RaisedButton from 'material-ui/RaisedButton'; @@ -26,11 +27,16 @@ import styles from './transactionPendingFormConfirm.css'; class TransactionPendingFormConfirm extends Component { static propTypes = { - accounts: PropTypes.object.isRequired, + account: PropTypes.object.isRequired, address: PropTypes.string.isRequired, isSending: PropTypes.bool.isRequired, - onConfirm: PropTypes.func.isRequired - } + onConfirm: PropTypes.func.isRequired, + focus: PropTypes.bool + }; + + static defaultProps = { + focus: false + }; id = Math.random(); // for tooltip @@ -40,10 +46,39 @@ class TransactionPendingFormConfirm extends Component { walletError: null } + componentDidMount () { + this.focus(); + } + + componentWillReceiveProps (nextProps) { + if (!this.props.focus && nextProps.focus) { + this.focus(nextProps); + } + } + + /** + * Properly focus on the input element when needed. + * This might be fixed some day in MaterialUI with + * an autoFocus prop. + * + * @see https://github.com/callemall/material-ui/issues/5632 + */ + focus (props = this.props) { + if (props.focus) { + const textNode = ReactDOM.findDOMNode(this.refs.input); + + if (!textNode) { + return; + } + + const inputNode = textNode.querySelector('input'); + inputNode && inputNode.focus(); + } + } + render () { - const { accounts, address, isSending } = this.props; + const { account, address, isSending } = this.props; const { password, wallet, walletError } = this.state; - const account = accounts[address] || {}; const isExternal = !account.uuid; const passwordHint = account.meta && account.meta.passwordHint @@ -72,8 +107,10 @@ class TransactionPendingFormConfirm extends Component { } onChange={ this.onModifyPassword } onKeyDown={ this.onKeyDown } + ref='input' type='password' - value={ password } /> + value={ password } + />
{ passwordHint }
@@ -178,11 +215,14 @@ class TransactionPendingFormConfirm extends Component { } } -function mapStateToProps (state) { - const { accounts } = state.personal; +function mapStateToProps (initState, initProps) { + const { accounts } = initState.personal; + const { address } = initProps; - return { - accounts + const account = accounts[address] || {}; + + return () => { + return { account }; }; } diff --git a/js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.js b/js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.js index f6e92761f..f0b167f27 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.js @@ -28,7 +28,12 @@ export default class TransactionPendingForm extends Component { isSending: PropTypes.bool.isRequired, onConfirm: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, - className: PropTypes.string + className: PropTypes.string, + focus: PropTypes.bool + }; + + static defaultProps = { + focus: false }; state = { @@ -47,7 +52,7 @@ export default class TransactionPendingForm extends Component { } renderForm () { - const { address, isSending, onConfirm, onReject } = this.props; + const { address, focus, isSending, onConfirm, onReject } = this.props; if (this.state.isRejectOpen) { return ( @@ -59,8 +64,10 @@ export default class TransactionPendingForm extends Component { return ( + onConfirm={ onConfirm } + /> ); } diff --git a/js/src/views/Signer/containers/Embedded/embedded.js b/js/src/views/Signer/containers/Embedded/embedded.js index 3fa450473..b79657430 100644 --- a/js/src/views/Signer/containers/Embedded/embedded.js +++ b/js/src/views/Signer/containers/Embedded/embedded.js @@ -78,7 +78,7 @@ class Embedded extends Component { ); } - renderPending = (data) => { + renderPending = (data, index) => { const { actions, gasLimit, isTest } = this.props; const { date, id, isSending, payload } = data; @@ -86,6 +86,7 @@ class Embedded extends Component { { + renderPending = (data, index) => { const { actions, gasLimit, isTest } = this.props; const { date, id, isSending, payload } = data; @@ -112,6 +112,7 @@ class RequestsPage extends Component { ( - - )); + const ownersList = owners.map((owner, idx) => { + const address = typeof owner === 'object' + ? owner.address + : owner; + + return ( + + ); + }); return (
diff --git a/js/src/views/Wallet/Transactions/transactions.js b/js/src/views/Wallet/Transactions/transactions.js index aff1623a4..0f2558dfc 100644 --- a/js/src/views/Wallet/Transactions/transactions.js +++ b/js/src/views/Wallet/Transactions/transactions.js @@ -57,12 +57,12 @@ export default class WalletTransactions extends Component { ); } - const txRows = transactions.map((transaction) => { + const txRows = transactions.slice(0, 15).map((transaction, index) => { const { transactionHash, blockNumber, from, to, value, data } = transaction; return ( - { this.renderEditDialog(wallet) } + { this.renderEditDialog(walletAccount) } { this.renderSettingsDialog() } { this.renderTransferDialog() } - { this.renderDeleteDialog(wallet) } + { this.renderDeleteDialog(walletAccount) } { this.renderActionbar() }
@@ -209,32 +208,47 @@ class Wallet extends Component { } renderActionbar () { - const { balance } = this.props; + const { balance, owned } = this.props; const showTransferButton = !!(balance && balance.tokens); - const buttons = [ -