Default contract type on UI (#3310)

* Added Token and Wallet ABI in Watch Contract #3126

* Improved ABI Validator #3281

* Select contract type on first screen #3126

* Added types decsription

* Add ABI type to Contract metadata // Custom as default type #3310
This commit is contained in:
Nicolas Gotchac 2016-11-10 11:27:35 +01:00 committed by Gav Wood
parent 2f98169539
commit 67ac05ef39
6 changed files with 227 additions and 33 deletions

View File

@ -136,27 +136,30 @@ export default class Contract {
} }
parseEventLogs (logs) { parseEventLogs (logs) {
return logs.map((log) => { return logs
const signature = log.topics[0].substr(2); .map((log) => {
const event = this.events.find((evt) => evt.signature === signature); const signature = log.topics[0].substr(2);
const event = this.events.find((evt) => evt.signature === signature);
if (!event) { if (!event) {
throw new Error(`Unable to find event matching signature ${signature}`); console.warn(`Unable to find event matching signature ${signature}`);
} return null;
}
const decoded = event.decodeLog(log.topics, log.data); const decoded = event.decodeLog(log.topics, log.data);
log.params = {}; log.params = {};
log.event = event.name; log.event = event.name;
decoded.params.forEach((param) => { decoded.params.forEach((param) => {
const { type, value } = param.token; const { type, value } = param.token;
log.params[param.name] = { type, value }; log.params[param.name] = { type, value };
}); });
return log; return log;
}); })
.filter((log) => log);
} }
parseTransactionEvents (receipt) { parseTransactionEvents (receipt) {

View File

@ -24,6 +24,7 @@ import owned from './owned.json';
import registry from './registry.json'; import registry from './registry.json';
import signaturereg from './signaturereg.json'; import signaturereg from './signaturereg.json';
import tokenreg from './tokenreg.json'; import tokenreg from './tokenreg.json';
import wallet from './wallet.json';
export { export {
basiccoin, basiccoin,
@ -35,5 +36,6 @@ export {
owned, owned,
registry, registry,
signaturereg, signaturereg,
tokenreg tokenreg,
wallet
}; };

View File

@ -0,0 +1 @@
[{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"removeOwner","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"}],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"m_numOwners","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"m_lastDay","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[],"name":"resetSpentToday","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_spentToday","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"addOwner","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_required","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_h","type":"bytes32"}],"name":"confirm","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_newLimit","type":"uint256"}],"name":"setDailyLimit","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"_r","type":"bytes32"}],"type":"function"},{"constant":false,"inputs":[{"name":"_operation","type":"bytes32"}],"name":"revoke","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_newRequired","type":"uint256"}],"name":"changeRequirement","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_operation","type":"bytes32"},{"name":"_owner","type":"address"}],"name":"hasConfirmed","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"ownerIndex","type":"uint256"}],"name":"getOwner","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"}],"name":"kill","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"name":"changeOwner","outputs":[],"type":"function"},{"constant":true,"inputs":[],"name":"m_dailyLimit","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"inputs":[{"name":"_owners","type":"address[]"},{"name":"_required","type":"uint256"},{"name":"_daylimit","type":"uint256"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Confirmation","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"}],"name":"Revoke","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"},{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newOwner","type":"address"}],"name":"OwnerAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oldOwner","type":"address"}],"name":"OwnerRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newRequirement","type":"uint256"}],"name":"RequirementChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"SingleTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"owner","type":"address"},{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"MultiTransact","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"operation","type":"bytes32"},{"indexed":false,"name":"initiator","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"to","type":"address"},{"indexed":false,"name":"data","type":"bytes"}],"name":"ConfirmationNeeded","type":"event"}]

View File

@ -0,0 +1,32 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.spaced {
margin: 0.25em 0;
}
.typeContainer {
display: flex;
flex-direction: column;
.desc {
font-size: 0.8em;
margin-bottom: 0.5em;
color: #ccc;
z-index: 2;
}
}

View File

@ -17,10 +17,38 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ContentAdd from 'material-ui/svg-icons/content/add'; import ContentAdd from 'material-ui/svg-icons/content/add';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import { Button, Modal, Form, Input, InputAddress } from '../../ui'; import { Button, Modal, Form, Input, InputAddress } from '../../ui';
import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation'; import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation';
import { eip20, wallet } from '../../contracts/abi';
import styles from './addContract.css';
const ABI_TYPES = [
{
label: 'Token', readOnly: true, value: JSON.stringify(eip20),
type: 'token',
description: (<span>A standard <a href='https://github.com/ethereum/EIPs/issues/20' target='_blank'>ERC 20</a> token</span>)
},
{
label: 'Multisig Wallet', readOnly: true,
type: 'multisig',
value: JSON.stringify(wallet),
description: (<span>Official Multisig contract: <a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>see contract code</a></span>)
},
{
label: 'Custom Contract', value: '',
type: 'custom',
description: 'Contract created from custom ABI'
}
];
const STEPS = [ 'choose a contract type', 'enter contract details' ];
export default class AddContract extends Component { export default class AddContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
@ -34,44 +62,101 @@ export default class AddContract extends Component {
state = { state = {
abi: '', abi: '',
abiError: ERRORS.invalidAbi, abiError: ERRORS.invalidAbi,
abiType: ABI_TYPES[2],
abiTypeIndex: 2,
abiParsed: null, abiParsed: null,
address: '', address: '',
addressError: ERRORS.invalidAddress, addressError: ERRORS.invalidAddress,
name: '', name: '',
nameError: ERRORS.invalidName, nameError: ERRORS.invalidName,
description: '' description: '',
step: 0
}; };
componentDidMount () {
this.onChangeABIType(null, this.state.abiTypeIndex);
}
render () { render () {
const { step } = this.state;
return ( return (
<Modal <Modal
visible visible
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
title='watch contract'> steps={ STEPS }
{ this.renderFields() } current={ step }
>
{ this.renderStep(step) }
</Modal> </Modal>
); );
} }
renderStep (step) {
switch (step) {
case 0:
return this.renderContractTypeSelector();
default:
return this.renderFields();
}
}
renderContractTypeSelector () {
const { abiTypeIndex } = this.state;
return (
<RadioButtonGroup
valueSelected={ abiTypeIndex }
name='contractType'
onChange={ this.onChangeABIType }
>
{ this.renderAbiTypes() }
</RadioButtonGroup>
);
}
renderDialogActions () { renderDialogActions () {
const { addressError, nameError } = this.state; const { addressError, nameError, step } = this.state;
const hasError = !!(addressError || nameError); const hasError = !!(addressError || nameError);
return ([ const cancelBtn = (
<Button <Button
icon={ <ContentClear /> } icon={ <ContentClear /> }
label='Cancel' label='Cancel'
onClick={ this.onClose } />, onClick={ this.onClose } />
);
if (step === 0) {
const nextBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Next'
onClick={ this.onNext } />
);
return [ cancelBtn, nextBtn ];
}
const prevBtn = (
<Button
icon={ <NavigationArrowBack /> }
label='Back'
onClick={ this.onPrev } />
);
const addBtn = (
<Button <Button
icon={ <ContentAdd /> } icon={ <ContentAdd /> }
label='Add Contract' label='Add Contract'
disabled={ hasError } disabled={ hasError }
onClick={ this.onAdd } /> onClick={ this.onAdd } />
]); );
return [ cancelBtn, prevBtn, addBtn ];
} }
renderFields () { renderFields () {
const { abi, abiError, address, addressError, description, name, nameError } = this.state; const { abi, abiError, address, addressError, description, name, nameError, abiType } = this.state;
return ( return (
<Form> <Form>
@ -80,7 +165,9 @@ export default class AddContract extends Component {
hint='the network address for the contract' hint='the network address for the contract'
error={ addressError } error={ addressError }
value={ address } value={ address }
onSubmit={ this.onEditAddress } /> onSubmit={ this.onEditAddress }
onChange={ this.onChangeAddress }
/>
<Input <Input
label='contract name' label='contract name'
hint='a descriptive name for the contract' hint='a descriptive name for the contract'
@ -94,20 +181,57 @@ export default class AddContract extends Component {
hint='an expanded description for the entry' hint='an expanded description for the entry'
value={ description } value={ description }
onSubmit={ this.onEditDescription } /> onSubmit={ this.onEditDescription } />
<Input <Input
label='contract abi' label='contract abi'
hint='the abi for the contract' hint='the abi for the contract'
error={ abiError } error={ abiError }
value={ abi } value={ abi }
onSubmit={ this.onEditAbi } /> readOnly={ abiType.readOnly }
onSubmit={ this.onEditAbi }
/>
</Form> </Form>
); );
} }
onEditAbi = (abi) => { renderAbiTypes () {
const { api } = this.context; return ABI_TYPES.map((type, index) => (
<RadioButton
className={ styles.spaced }
value={ index }
label={ (
<div className={ styles.typeContainer }>
<span>{ type.label }</span>
<span className={ styles.desc }>{ type.description }</span>
</div>
) }
key={ index }
/>
));
}
this.setState(validateAbi(abi, api)); onNext = () => {
this.setState({ step: this.state.step + 1 });
}
onPrev = () => {
this.setState({ step: this.state.step - 1 });
}
onChangeABIType = (event, index) => {
const abiType = ABI_TYPES[index];
this.setState({ abiTypeIndex: index, abiType });
this.onEditAbi(abiType.value);
}
onEditAbi = (abiIn) => {
const { api } = this.context;
const { abi, abiError, abiParsed } = validateAbi(abiIn, api);
this.setState({ abi, abiError, abiParsed });
}
onChangeAddress = (event, value) => {
this.onEditAddress(value);
} }
onEditAddress = (_address) => { onEditAddress = (_address) => {
@ -138,7 +262,7 @@ export default class AddContract extends Component {
onAdd = () => { onAdd = () => {
const { api } = this.context; const { api } = this.context;
const { abiParsed, address, name, description } = this.state; const { abiParsed, address, name, description, abiType } = this.state;
Promise.all([ Promise.all([
api.parity.setAccountName(address, name), api.parity.setAccountName(address, name),
@ -147,6 +271,7 @@ export default class AddContract extends Component {
deleted: false, deleted: false,
timestamp: Date.now(), timestamp: Date.now(),
abi: abiParsed, abi: abiParsed,
type: abiType.type,
description description
}) })
]).catch((error) => { ]).catch((error) => {

View File

@ -38,10 +38,22 @@ export function validateAbi (abi, api) {
abiParsed = JSON.parse(abi); abiParsed = JSON.parse(abi);
if (!api.util.isArray(abiParsed) || !abiParsed.length) { if (!api.util.isArray(abiParsed) || !abiParsed.length) {
abiError = ERRORS.inavlidAbi; abiError = ERRORS.invalidAbi;
} else { return { abi, abiError, abiParsed };
abi = JSON.stringify(abiParsed);
} }
// Validate each elements of the Array
const invalidIndex = abiParsed
.map((o) => isValidAbiEvent(o, api) || isValidAbiFunction(o, api))
.findIndex((valid) => !valid);
if (invalidIndex !== -1) {
const invalid = abiParsed[invalidIndex];
abiError = `${ERRORS.invalidAbi} (#${invalidIndex}: ${invalid.name || invalid.type})`;
return { abi, abiError, abiParsed };
}
abi = JSON.stringify(abiParsed);
} catch (error) { } catch (error) {
abiError = ERRORS.invalidAbi; abiError = ERRORS.invalidAbi;
} }
@ -53,6 +65,25 @@ export function validateAbi (abi, api) {
}; };
} }
function isValidAbiFunction (object, api) {
if (!object) {
return false;
}
return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
(object.inputs && api.util.isArray(object.inputs));
}
function isValidAbiEvent (object, api) {
if (!object) {
return false;
}
return (object.type === 'event') &&
(object.name) &&
(object.inputs && api.util.isArray(object.inputs));
}
export function validateAddress (address) { export function validateAddress (address) {
let addressError = null; let addressError = null;