From 6655e7e3c0e2c3c23e4f3ba80c5886cf0fb78172 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 01:26:28 +0100 Subject: [PATCH] Add support for wallets without getOwner() interface (#3779) * Make Wallet Mist compatible #3282 * Owners icons on load * Fix oversized logo on load * Don't fetch registry twice (even when pending) * Better logging... * Better contract view : show if no events // show loading events * Better decimal typed input * PR grumble --- js/src/contracts/registry.js | 38 +++++---- .../modals/CreateWallet/createWalletStore.js | 3 +- js/src/redux/providers/personal.js | 3 - js/src/redux/providers/signer.js | 3 - js/src/redux/providers/status.js | 3 - js/src/ui/Form/Input/input.js | 38 +++++---- js/src/ui/Form/TypedInput/typedInput.js | 52 ++++++++++--- js/src/ui/Page/page.css | 6 +- js/src/util/wallets.js | 77 ++++++++++++++++++- js/src/views/Accounts/Summary/summary.js | 11 ++- js/src/views/Application/TabBar/tabBar.js | 2 +- js/src/views/Contract/Events/events.js | 34 ++++++-- js/src/views/Contract/Queries/queries.js | 4 + js/src/views/Contract/contract.js | 24 ++++-- js/src/views/Wallet/Details/details.js | 4 +- 15 files changed, 228 insertions(+), 74 deletions(-) diff --git a/js/src/contracts/registry.js b/js/src/contracts/registry.js index 2f61f7f4a..9354a59e5 100644 --- a/js/src/contracts/registry.js +++ b/js/src/contracts/registry.js @@ -19,7 +19,10 @@ import * as abis from './abi'; export default class Registry { constructor (api) { this._api = api; - this._contracts = []; + + this._contracts = {}; + this._pendingContracts = {}; + this._instance = null; this._fetching = false; this._queue = []; @@ -59,20 +62,25 @@ export default class Registry { getContract (_name) { const name = _name.toLowerCase(); - return new Promise((resolve, reject) => { - if (this._contracts[name]) { - resolve(this._contracts[name]); - return; - } + if (this._contracts[name]) { + return Promise.resolve(this._contracts[name]); + } - this - .lookupAddress(name) - .then((address) => { - this._contracts[name] = this._api.newContract(abis[name], address); - resolve(this._contracts[name]); - }) - .catch(reject); - }); + if (this._pendingContracts[name]) { + return this._pendingContracts[name]; + } + + const promise = this + .lookupAddress(name) + .then((address) => { + this._contracts[name] = this._api.newContract(abis[name], address); + delete this._pendingContracts[name]; + return this._contracts[name]; + }); + + this._pendingContracts[name] = promise; + + return promise; } getContractInstance (_name) { @@ -89,7 +97,7 @@ export default class Registry { return instance.getAddress.call({}, [sha3, 'A']); }) .then((address) => { - console.log('lookupAddress', name, sha3, address); + console.log('[lookupAddress]', `(${sha3}) ${name}: ${address}`); return address; }); } diff --git a/js/src/modals/CreateWallet/createWalletStore.js b/js/src/modals/CreateWallet/createWalletStore.js index 3edf8f638..b28bfbd26 100644 --- a/js/src/modals/CreateWallet/createWalletStore.js +++ b/js/src/modals/CreateWallet/createWalletStore.js @@ -23,6 +23,7 @@ import { wallet as walletAbi } from '~/contracts/abi'; import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet'; import { validateUint, validateAddress, validateName } from '~/util/validation'; +import { toWei } from '~/api/util/wei'; import WalletsUtils from '~/util/wallets'; const STEPS = { @@ -47,7 +48,7 @@ export default class CreateWalletStore { address: '', owners: [], required: 1, - daylimit: 0, + daylimit: toWei(1), name: '', description: '' diff --git a/js/src/redux/providers/personal.js b/js/src/redux/providers/personal.js index e061051b0..7629c4f46 100644 --- a/js/src/redux/providers/personal.js +++ b/js/src/redux/providers/personal.js @@ -36,9 +36,6 @@ export default class Personal { } this._store.dispatch(personalAccountsInfo(accountsInfo)); - }) - .then((subscriptionId) => { - console.log('personal._subscribeAccountsInfo', 'subscriptionId', subscriptionId); }); } diff --git a/js/src/redux/providers/signer.js b/js/src/redux/providers/signer.js index 5ece371c2..11b30c6b4 100644 --- a/js/src/redux/providers/signer.js +++ b/js/src/redux/providers/signer.js @@ -34,9 +34,6 @@ export default class Signer { } this._store.dispatch(signerRequestsToConfirm(pending || [])); - }) - .then((subscriptionId) => { - console.log('signer._subscribeRequestsToConfirm', 'subscriptionId', subscriptionId); }); } } diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 936fe3f25..830192bbe 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -59,9 +59,6 @@ export default class Status { .catch((error) => { console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error); }); - }) - .then((subscriptionId) => { - console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId); }); } diff --git a/js/src/ui/Form/Input/input.js b/js/src/ui/Form/Input/input.js index 701851ef9..f25ce207c 100644 --- a/js/src/ui/Form/Input/input.js +++ b/js/src/ui/Form/Input/input.js @@ -113,32 +113,38 @@ export default class Input extends Component { { children } diff --git a/js/src/ui/Form/TypedInput/typedInput.js b/js/src/ui/Form/TypedInput/typedInput.js index a81ec7b79..a54032999 100644 --- a/js/src/ui/Form/TypedInput/typedInput.js +++ b/js/src/ui/Form/TypedInput/typedInput.js @@ -53,13 +53,13 @@ export default class TypedInput extends Component { }; state = { - isEth: true, + isEth: false, ethValue: 0 }; - componentDidMount () { + componentWillMount () { if (this.props.isEth && this.props.value) { - this.setState({ ethValue: fromWei(this.props.value) }); + this.setState({ isEth: true, ethValue: fromWei(this.props.value) }); } } @@ -164,28 +164,32 @@ export default class TypedInput extends Component { } if (type === ABI_TYPES.INT) { - return this.renderNumber(); + return this.renderEth(); } if (type === ABI_TYPES.FIXED) { - return this.renderNumber(); + return this.renderFloat(); } return this.renderDefault(); } renderEth () { - const { ethValue } = this.state; + const { ethValue, isEth } = this.state; const value = ethValue && typeof ethValue.toNumber === 'function' ? ethValue.toNumber() : ethValue; + const input = isEth + ? this.renderFloat(value, this.onEthValueChange) + : this.renderInteger(value, this.onEthValueChange); + return (
- { this.renderNumber(value, this.onEthValueChange) } - { this.state.isEth ? (
ETH
) : null } + { input } + { isEth ? (
ETH
) : null }
+ ); + } + + /** + * Decimal numbers have to be input via text field + * because of some react issues with input number fields. + * Once the issue is fixed, this could be a number again. + * + * @see https://github.com/facebook/react/issues/1549 + */ + renderFloat (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 ( + diff --git a/js/src/ui/Page/page.css b/js/src/ui/Page/page.css index 9b7cfd62b..72a78dc22 100644 --- a/js/src/ui/Page/page.css +++ b/js/src/ui/Page/page.css @@ -17,8 +17,8 @@ .layout { padding: 0.25em 0.25em 1em 0.25em; -} -.layout>div { - padding-bottom: 0.75em; + > * { + margin-bottom: 0.75em; + } } diff --git a/js/src/util/wallets.js b/js/src/util/wallets.js index c335ce03c..1f0bc6735 100644 --- a/js/src/util/wallets.js +++ b/js/src/util/wallets.js @@ -14,9 +14,10 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { range } from 'lodash'; +import { range, uniq } from 'lodash'; import { bytesToHex, toHex } from '~/api/util/format'; +import { validateAddress } from '~/util/validation'; export default class WalletsUtils { @@ -26,10 +27,82 @@ export default class WalletsUtils { static fetchOwners (walletContract) { const walletInstance = walletContract.instance; + return walletInstance .m_numOwners.call() .then((mNumOwners) => { - return Promise.all(range(mNumOwners.toNumber()).map((idx) => walletInstance.getOwner.call({}, [ idx ]))); + const promises = range(mNumOwners.toNumber()) + .map((idx) => walletInstance.getOwner.call({}, [ idx ])); + + return Promise + .all(promises) + .then((owners) => { + const uniqOwners = uniq(owners); + + // If all owners are the zero account : must be Mist wallet contract + if (uniqOwners.length === 1 && /^(0x)?0*$/.test(owners[0])) { + return WalletsUtils.fetchMistOwners(walletContract, mNumOwners.toNumber()); + } + + return owners; + }); + }); + } + + static fetchMistOwners (walletContract, mNumOwners) { + const walletAddress = walletContract.address; + + return WalletsUtils + .getMistOwnersOffset(walletContract) + .then((result) => { + if (!result || result.offset === -1) { + return []; + } + + const owners = [ result.address ]; + + if (mNumOwners === 1) { + return owners; + } + + const initOffset = result.offset + 1; + let promise = Promise.resolve(); + + range(initOffset, initOffset + mNumOwners - 1).forEach((offset) => { + promise = promise + .then(() => { + return walletContract.api.eth.getStorageAt(walletAddress, offset); + }) + .then((result) => { + const resultAddress = '0x' + (result || '').slice(-40); + const { address } = validateAddress(resultAddress); + + owners.push(address); + }); + }); + + return promise.then(() => owners); + }); + } + + static getMistOwnersOffset (walletContract, offset = 3) { + return walletContract.api.eth + .getStorageAt(walletContract.address, offset) + .then((result) => { + if (result && !/^(0x)?0*$/.test(result)) { + const resultAddress = '0x' + result.slice(-40); + const { address, addressError } = validateAddress(resultAddress); + + if (!addressError) { + return { offset, address }; + } + } + + if (offset >= 100) { + return { offset: -1 }; + } + + return WalletsUtils.getMistOwnersOffset(walletContract, offset + 1); }); } diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index aeff8a2e5..3183a2903 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -75,6 +75,13 @@ export default class Summary extends Component { return true; } + const prevOwners = this.props.owners; + const nextOwners = nextProps.owners; + + if (!isEqual(prevOwners, nextOwners)) { + return true; + } + return false; } @@ -123,8 +130,8 @@ export default class Summary extends Component { return (
{ - ownersValid.map((owner) => ( -
+ ownersValid.map((owner, index) => ( +
- +
); diff --git a/js/src/views/Contract/Events/events.js b/js/src/views/Contract/Events/events.js index 69ae8fd6a..c29e624bf 100644 --- a/js/src/views/Contract/Events/events.js +++ b/js/src/views/Contract/Events/events.js @@ -17,7 +17,7 @@ import React, { Component, PropTypes } from 'react'; import { uniq } from 'lodash'; -import { Container } from '~/ui'; +import { Container, Loading } from '~/ui'; import Event from './Event'; import styles from '../contract.css'; @@ -25,18 +25,38 @@ import styles from '../contract.css'; export default class Events extends Component { static contextTypes = { api: PropTypes.object - } + }; static propTypes = { - events: PropTypes.array, - isTest: PropTypes.bool.isRequired - } + isTest: PropTypes.bool.isRequired, + isLoading: PropTypes.bool, + events: PropTypes.array + }; + + static defaultProps = { + isLoading: false, + events: [] + }; render () { - const { events, isTest } = this.props; + const { events, isTest, isLoading } = this.props; + + if (isLoading) { + return ( + +
+ +
+
+ ); + } if (!events || !events.length) { - return null; + return ( + +

No events has been sent from this contract.

+
+ ); } const eventsKey = uniq(events.map((e) => e.key)); diff --git a/js/src/views/Contract/Queries/queries.js b/js/src/views/Contract/Queries/queries.js index 9a13037f6..1bfd2fa5f 100644 --- a/js/src/views/Contract/Queries/queries.js +++ b/js/src/views/Contract/Queries/queries.js @@ -54,6 +54,10 @@ export default class Queries extends Component { .filter((fn) => fn.inputs.length > 0) .map((fn) => this.renderInputQuery(fn)); + if (queries.length + noInputQueries.length + withInputQueries.length === 0) { + return null; + } + return (
diff --git a/js/src/views/Contract/contract.js b/js/src/views/Contract/contract.js index 35ad95fe2..4f7570ebd 100644 --- a/js/src/views/Contract/contract.js +++ b/js/src/views/Contract/contract.js @@ -40,7 +40,7 @@ import styles from './contract.css'; class Contract extends Component { static contextTypes = { api: React.PropTypes.object.isRequired - } + }; static propTypes = { setVisibleAccounts: PropTypes.func.isRequired, @@ -50,7 +50,7 @@ class Contract extends Component { contracts: PropTypes.object, isTest: PropTypes.bool, params: PropTypes.object - } + }; state = { contract: null, @@ -64,8 +64,9 @@ class Contract extends Component { allEvents: [], minedEvents: [], pendingEvents: [], - queryValues: {} - } + queryValues: {}, + loadingEvents: true + }; componentDidMount () { const { api } = this.context; @@ -115,7 +116,7 @@ class Contract extends Component { render () { const { balances, contracts, params, isTest } = this.props; - const { allEvents, contract, queryValues } = this.state; + const { allEvents, contract, queryValues, loadingEvents } = this.state; const account = contracts[params.address]; const balance = balances[params.address]; @@ -134,12 +135,17 @@ class Contract extends Component { account={ account } balance={ balance } /> + + values={ queryValues } + /> + + isLoading={ loadingEvents } + events={ allEvents } + /> { this.renderDetails(account) } @@ -358,6 +364,10 @@ class Contract extends Component { } _receiveEvents = (error, logs) => { + if (this.state.loadingEvents) { + this.setState({ loadingEvents: false }); + } + if (error) { console.error('_receiveEvents', error); return; diff --git a/js/src/views/Wallet/Details/details.js b/js/src/views/Wallet/Details/details.js index 547b02601..fb08bbde2 100644 --- a/js/src/views/Wallet/Details/details.js +++ b/js/src/views/Wallet/Details/details.js @@ -55,9 +55,9 @@ export default class WalletDetails extends Component { return null; } - const ownersList = owners.map((address) => ( + const ownersList = owners.map((address, idx) => (