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

View File

@ -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: ''

View File

@ -36,9 +36,6 @@ export default class Personal {
}
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 || []));
})
.then((subscriptionId) => {
console.log('signer._subscribeRequestsToConfirm', 'subscriptionId', subscriptionId);
});
}
}

View File

@ -59,9 +59,6 @@ export default class Status {
.catch((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
autoComplete='off'
className={ className }
style={ textFieldStyle }
readOnly={ readOnly }
errorText={ error }
floatingLabelFixed
floatingLabelText={ label }
fullWidth
hintText={ hint }
id={ NAME_ID }
inputStyle={ inputStyle }
fullWidth
max={ max }
min={ min }
multiLine={ multiLine }
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 }
onChange={ this.onChange }
onKeyDown={ this.onKeyDown }
onPaste={ this.onPaste }
inputStyle={ inputStyle }
min={ min }
max={ max }
readOnly={ readOnly }
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 }
</TextField>

View File

@ -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 (
<div className={ styles.ethInput }>
<div className={ styles.input }>
{ this.renderNumber(value, this.onEthValueChange) }
{ this.state.isEth ? (<div className={ styles.label }>ETH</div>) : null }
{ input }
{ isEth ? (<div className={ styles.label }>ETH</div>) : null }
</div>
<div className={ styles.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 realValue = value && typeof value.toNumber === 'function'
? value.toNumber()
: value;
@ -212,6 +217,35 @@ export default class TypedInput extends Component {
error={ error }
onChange={ onChange }
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) }
max={ max !== null ? max : null }
/>

View File

@ -17,8 +17,8 @@
.layout {
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
// 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 { 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);
});
}

View File

@ -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 (
<div className={ styles.owners }>
{
ownersValid.map((owner) => (
<div key={ owner.address }>
ownersValid.map((owner, index) => (
<div key={ `${index}_${owner.address}` }>
<div
data-tip
data-for={ `owner_${owner.address}` }

View File

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

View File

@ -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 (
<Container title='events'>
<div>
<Loading size={ 2 } />
</div>
</Container>
);
}
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));

View File

@ -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 (
<Container title='queries'>
<div className={ styles.methods }>

View File

@ -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 }
/>
<Queries
contract={ contract }
values={ queryValues } />
values={ queryValues }
/>
<Events
isTest={ isTest }
events={ allEvents } />
isLoading={ loadingEvents }
events={ allEvents }
/>
{ this.renderDetails(account) }
</Page>
@ -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;

View File

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