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:
parent
923f85d90d
commit
6655e7e3c0
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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: ''
|
||||
|
@ -36,9 +36,6 @@ export default class Personal {
|
||||
}
|
||||
|
||||
this._store.dispatch(personalAccountsInfo(accountsInfo));
|
||||
})
|
||||
.then((subscriptionId) => {
|
||||
console.log('personal._subscribeAccountsInfo', 'subscriptionId', subscriptionId);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -34,9 +34,6 @@ export default class Signer {
|
||||
}
|
||||
|
||||
this._store.dispatch(signerRequestsToConfirm(pending || []));
|
||||
})
|
||||
.then((subscriptionId) => {
|
||||
console.log('signer._subscribeRequestsToConfirm', 'subscriptionId', subscriptionId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -59,9 +59,6 @@ export default class Status {
|
||||
.catch((error) => {
|
||||
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
|
||||
});
|
||||
})
|
||||
.then((subscriptionId) => {
|
||||
console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 }
|
||||
/>
|
||||
|
@ -17,8 +17,8 @@
|
||||
|
||||
.layout {
|
||||
padding: 0.25em 0.25em 1em 0.25em;
|
||||
}
|
||||
|
||||
.layout>div {
|
||||
padding-bottom: 0.75em;
|
||||
> * {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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}` }
|
||||
|
@ -188,7 +188,7 @@ class TabBar extends Component {
|
||||
return (
|
||||
<ToolbarGroup>
|
||||
<div className={ styles.logo }>
|
||||
<img src={ imagesEthcoreBlock } />
|
||||
<img src={ imagesEthcoreBlock } height={ 28 } />
|
||||
</div>
|
||||
</ToolbarGroup>
|
||||
);
|
||||
|
@ -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));
|
||||
|
@ -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 }>
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user