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
This commit is contained in:
Nicolas Gotchac 2016-12-10 01:26:28 +01:00 committed by Jaco Greeff
parent 923f85d90d
commit 6655e7e3c0
15 changed files with 228 additions and 74 deletions

View File

@ -19,7 +19,10 @@ import * as abis from './abi';
export default class Registry { export default class Registry {
constructor (api) { constructor (api) {
this._api = api; this._api = api;
this._contracts = [];
this._contracts = {};
this._pendingContracts = {};
this._instance = null; this._instance = null;
this._fetching = false; this._fetching = false;
this._queue = []; this._queue = [];
@ -59,20 +62,25 @@ export default class Registry {
getContract (_name) { getContract (_name) {
const name = _name.toLowerCase(); const name = _name.toLowerCase();
return new Promise((resolve, reject) => { if (this._contracts[name]) {
if (this._contracts[name]) { return Promise.resolve(this._contracts[name]);
resolve(this._contracts[name]); }
return;
}
this if (this._pendingContracts[name]) {
.lookupAddress(name) return this._pendingContracts[name];
.then((address) => { }
this._contracts[name] = this._api.newContract(abis[name], address);
resolve(this._contracts[name]); const promise = this
}) .lookupAddress(name)
.catch(reject); .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) { getContractInstance (_name) {
@ -89,7 +97,7 @@ export default class Registry {
return instance.getAddress.call({}, [sha3, 'A']); return instance.getAddress.call({}, [sha3, 'A']);
}) })
.then((address) => { .then((address) => {
console.log('lookupAddress', name, sha3, address); console.log('[lookupAddress]', `(${sha3}) ${name}: ${address}`);
return address; return address;
}); });
} }

View File

@ -23,6 +23,7 @@ import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet'; import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet';
import { validateUint, validateAddress, validateName } from '~/util/validation'; import { validateUint, validateAddress, validateName } from '~/util/validation';
import { toWei } from '~/api/util/wei';
import WalletsUtils from '~/util/wallets'; import WalletsUtils from '~/util/wallets';
const STEPS = { const STEPS = {
@ -47,7 +48,7 @@ export default class CreateWalletStore {
address: '', address: '',
owners: [], owners: [],
required: 1, required: 1,
daylimit: 0, daylimit: toWei(1),
name: '', name: '',
description: '' description: ''

View File

@ -36,9 +36,6 @@ export default class Personal {
} }
this._store.dispatch(personalAccountsInfo(accountsInfo)); this._store.dispatch(personalAccountsInfo(accountsInfo));
})
.then((subscriptionId) => {
console.log('personal._subscribeAccountsInfo', 'subscriptionId', subscriptionId);
}); });
} }

View File

@ -34,9 +34,6 @@ export default class Signer {
} }
this._store.dispatch(signerRequestsToConfirm(pending || [])); this._store.dispatch(signerRequestsToConfirm(pending || []));
})
.then((subscriptionId) => {
console.log('signer._subscribeRequestsToConfirm', 'subscriptionId', subscriptionId);
}); });
} }
} }

View File

@ -59,9 +59,6 @@ export default class Status {
.catch((error) => { .catch((error) => {
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error); console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
}); });
})
.then((subscriptionId) => {
console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId);
}); });
} }

View File

@ -113,32 +113,38 @@ export default class Input extends Component {
<TextField <TextField
autoComplete='off' autoComplete='off'
className={ className } className={ className }
style={ textFieldStyle }
readOnly={ readOnly }
errorText={ error } errorText={ error }
floatingLabelFixed floatingLabelFixed
floatingLabelText={ label } floatingLabelText={ label }
fullWidth
hintText={ hint } hintText={ hint }
id={ NAME_ID }
inputStyle={ inputStyle }
fullWidth
max={ max }
min={ min }
multiLine={ multiLine } multiLine={ multiLine }
name={ NAME_ID } name={ NAME_ID }
id={ NAME_ID }
rows={ rows }
type={ type || 'text' }
underlineDisabledStyle={ UNDERLINE_DISABLED }
underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
underlineFocusStyle={ readOnly ? { display: 'none' } : null }
underlineShow={ !hideUnderline }
value={ value }
onBlur={ this.onBlur } onBlur={ this.onBlur }
onChange={ this.onChange } onChange={ this.onChange }
onKeyDown={ this.onKeyDown } onKeyDown={ this.onKeyDown }
onPaste={ this.onPaste } onPaste={ this.onPaste }
inputStyle={ inputStyle }
min={ min } readOnly={ readOnly }
max={ max } rows={ rows }
style={ textFieldStyle }
type={ type || 'text' }
underlineDisabledStyle={ UNDERLINE_DISABLED }
underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
underlineFocusStyle={ readOnly ? { display: 'none' } : null }
underlineShow={ !hideUnderline }
value={ value }
> >
{ children } { children }
</TextField> </TextField>

View File

@ -53,13 +53,13 @@ export default class TypedInput extends Component {
}; };
state = { state = {
isEth: true, isEth: false,
ethValue: 0 ethValue: 0
}; };
componentDidMount () { componentWillMount () {
if (this.props.isEth && this.props.value) { 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) { if (type === ABI_TYPES.INT) {
return this.renderNumber(); return this.renderEth();
} }
if (type === ABI_TYPES.FIXED) { if (type === ABI_TYPES.FIXED) {
return this.renderNumber(); return this.renderFloat();
} }
return this.renderDefault(); return this.renderDefault();
} }
renderEth () { renderEth () {
const { ethValue } = this.state; const { ethValue, isEth } = this.state;
const value = ethValue && typeof ethValue.toNumber === 'function' const value = ethValue && typeof ethValue.toNumber === 'function'
? ethValue.toNumber() ? ethValue.toNumber()
: ethValue; : ethValue;
const input = isEth
? this.renderFloat(value, this.onEthValueChange)
: this.renderInteger(value, this.onEthValueChange);
return ( return (
<div className={ styles.ethInput }> <div className={ styles.ethInput }>
<div className={ styles.input }> <div className={ styles.input }>
{ this.renderNumber(value, this.onEthValueChange) } { input }
{ this.state.isEth ? (<div className={ styles.label }>ETH</div>) : null } { isEth ? (<div className={ styles.label }>ETH</div>) : null }
</div> </div>
<div className={ styles.toggle }> <div className={ styles.toggle }>
<Toggle <Toggle
@ -198,8 +202,9 @@ export default class TypedInput extends Component {
); );
} }
renderNumber (value = this.props.value, onChange = this.onChange) { renderInteger (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props; const { label, error, param, hint, min, max } = this.props;
const realValue = value && typeof value.toNumber === 'function' const realValue = value && typeof value.toNumber === 'function'
? value.toNumber() ? value.toNumber()
: value; : value;
@ -212,6 +217,35 @@ export default class TypedInput extends Component {
error={ error } error={ error }
onChange={ onChange } onChange={ onChange }
type='number' type='number'
step={ 1 }
min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : 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 (
<Input
label={ label }
hint={ hint }
value={ realValue }
error={ error }
onChange={ onChange }
type='text'
min={ min !== null ? min : (param.signed ? null : 0) } min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null } max={ max !== null ? max : null }
/> />

View File

@ -17,8 +17,8 @@
.layout { .layout {
padding: 0.25em 0.25em 1em 0.25em; padding: 0.25em 0.25em 1em 0.25em;
}
.layout>div { > * {
padding-bottom: 0.75em; margin-bottom: 0.75em;
}
} }

View File

@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { range } from 'lodash'; import { range, uniq } from 'lodash';
import { bytesToHex, toHex } from '~/api/util/format'; import { bytesToHex, toHex } from '~/api/util/format';
import { validateAddress } from '~/util/validation';
export default class WalletsUtils { export default class WalletsUtils {
@ -26,10 +27,82 @@ export default class WalletsUtils {
static fetchOwners (walletContract) { static fetchOwners (walletContract) {
const walletInstance = walletContract.instance; const walletInstance = walletContract.instance;
return walletInstance return walletInstance
.m_numOwners.call() .m_numOwners.call()
.then((mNumOwners) => { .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);
}); });
} }

View File

@ -75,6 +75,13 @@ export default class Summary extends Component {
return true; return true;
} }
const prevOwners = this.props.owners;
const nextOwners = nextProps.owners;
if (!isEqual(prevOwners, nextOwners)) {
return true;
}
return false; return false;
} }
@ -123,8 +130,8 @@ export default class Summary extends Component {
return ( return (
<div className={ styles.owners }> <div className={ styles.owners }>
{ {
ownersValid.map((owner) => ( ownersValid.map((owner, index) => (
<div key={ owner.address }> <div key={ `${index}_${owner.address}` }>
<div <div
data-tip data-tip
data-for={ `owner_${owner.address}` } data-for={ `owner_${owner.address}` }

View File

@ -188,7 +188,7 @@ class TabBar extends Component {
return ( return (
<ToolbarGroup> <ToolbarGroup>
<div className={ styles.logo }> <div className={ styles.logo }>
<img src={ imagesEthcoreBlock } /> <img src={ imagesEthcoreBlock } height={ 28 } />
</div> </div>
</ToolbarGroup> </ToolbarGroup>
); );

View File

@ -17,7 +17,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import { Container } from '~/ui'; import { Container, Loading } from '~/ui';
import Event from './Event'; import Event from './Event';
import styles from '../contract.css'; import styles from '../contract.css';
@ -25,18 +25,38 @@ import styles from '../contract.css';
export default class Events extends Component { export default class Events extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object api: PropTypes.object
} };
static propTypes = { 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 () { render () {
const { events, isTest } = this.props; const { events, isTest, isLoading } = this.props;
if (isLoading) {
return (
<Container title='events'>
<div>
<Loading size={ 2 } />
</div>
</Container>
);
}
if (!events || !events.length) { if (!events || !events.length) {
return null; return (
<Container title='events'>
<p>No events has been sent from this contract.</p>
</Container>
);
} }
const eventsKey = uniq(events.map((e) => e.key)); const eventsKey = uniq(events.map((e) => e.key));

View File

@ -54,6 +54,10 @@ export default class Queries extends Component {
.filter((fn) => fn.inputs.length > 0) .filter((fn) => fn.inputs.length > 0)
.map((fn) => this.renderInputQuery(fn)); .map((fn) => this.renderInputQuery(fn));
if (queries.length + noInputQueries.length + withInputQueries.length === 0) {
return null;
}
return ( return (
<Container title='queries'> <Container title='queries'>
<div className={ styles.methods }> <div className={ styles.methods }>

View File

@ -40,7 +40,7 @@ import styles from './contract.css';
class Contract extends Component { class Contract extends Component {
static contextTypes = { static contextTypes = {
api: React.PropTypes.object.isRequired api: React.PropTypes.object.isRequired
} };
static propTypes = { static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
@ -50,7 +50,7 @@ class Contract extends Component {
contracts: PropTypes.object, contracts: PropTypes.object,
isTest: PropTypes.bool, isTest: PropTypes.bool,
params: PropTypes.object params: PropTypes.object
} };
state = { state = {
contract: null, contract: null,
@ -64,8 +64,9 @@ class Contract extends Component {
allEvents: [], allEvents: [],
minedEvents: [], minedEvents: [],
pendingEvents: [], pendingEvents: [],
queryValues: {} queryValues: {},
} loadingEvents: true
};
componentDidMount () { componentDidMount () {
const { api } = this.context; const { api } = this.context;
@ -115,7 +116,7 @@ class Contract extends Component {
render () { render () {
const { balances, contracts, params, isTest } = this.props; 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 account = contracts[params.address];
const balance = balances[params.address]; const balance = balances[params.address];
@ -134,12 +135,17 @@ class Contract extends Component {
account={ account } account={ account }
balance={ balance } balance={ balance }
/> />
<Queries <Queries
contract={ contract } contract={ contract }
values={ queryValues } /> values={ queryValues }
/>
<Events <Events
isTest={ isTest } isTest={ isTest }
events={ allEvents } /> isLoading={ loadingEvents }
events={ allEvents }
/>
{ this.renderDetails(account) } { this.renderDetails(account) }
</Page> </Page>
@ -358,6 +364,10 @@ class Contract extends Component {
} }
_receiveEvents = (error, logs) => { _receiveEvents = (error, logs) => {
if (this.state.loadingEvents) {
this.setState({ loadingEvents: false });
}
if (error) { if (error) {
console.error('_receiveEvents', error); console.error('_receiveEvents', error);
return; return;

View File

@ -55,9 +55,9 @@ export default class WalletDetails extends Component {
return null; return null;
} }
const ownersList = owners.map((address) => ( const ownersList = owners.map((address, idx) => (
<InputAddress <InputAddress
key={ address } key={ `${idx}_${address}` }
value={ address } value={ address }
disabled disabled
text text