Cleanup AddContract with store (#3981)
* Splits (WIP) * Expand getters & setters * Initial abi type set * Expand * Don't rely on passed api * Store tests in place * Allow RadioButtons to accept MobX arrays * Fixes for manual testing
This commit is contained in:
parent
7e600b5a82
commit
8677c3b91f
@ -14,38 +14,17 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import ContentAdd from 'material-ui/svg-icons/content/add';
|
import { FormattedMessage } from 'react-intl';
|
||||||
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 { newError } from '~/redux/actions';
|
||||||
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
|
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
|
||||||
import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation';
|
import { AddIcon, CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
import { eip20, wallet } from '~/contracts/abi';
|
import Store from './store';
|
||||||
|
|
||||||
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' ];
|
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class AddContract extends Component {
|
export default class AddContract extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
@ -56,219 +35,217 @@ export default class AddContract extends Component {
|
|||||||
onClose: PropTypes.func
|
onClose: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
store = new Store(this.context.api, this.props.contracts);
|
||||||
abi: '',
|
|
||||||
abiError: ERRORS.invalidAbi,
|
|
||||||
abiType: ABI_TYPES[2],
|
|
||||||
abiTypeIndex: 2,
|
|
||||||
abiParsed: null,
|
|
||||||
address: '',
|
|
||||||
addressError: ERRORS.invalidAddress,
|
|
||||||
name: '',
|
|
||||||
nameError: ERRORS.invalidName,
|
|
||||||
description: '',
|
|
||||||
step: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.onChangeABIType(null, this.state.abiTypeIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { step } = this.state;
|
const { step } = this.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
|
||||||
actions={ this.renderDialogActions() }
|
actions={ this.renderDialogActions() }
|
||||||
steps={ STEPS }
|
|
||||||
current={ step }
|
current={ step }
|
||||||
>
|
steps={ [
|
||||||
{ this.renderStep(step) }
|
<FormattedMessage
|
||||||
|
id='addContract.title.type'
|
||||||
|
defaultMessage='choose a contract type'
|
||||||
|
key='type' />,
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.title.details'
|
||||||
|
defaultMessage='enter contract details'
|
||||||
|
key='details' />
|
||||||
|
] }
|
||||||
|
visible>
|
||||||
|
{ this.renderStep() }
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStep (step) {
|
renderStep () {
|
||||||
|
const { step } = this.store;
|
||||||
|
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 0:
|
case 0:
|
||||||
return this.renderContractTypeSelector();
|
return this.renderContractTypeSelector();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return this.renderFields();
|
return this.renderFields();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContractTypeSelector () {
|
renderContractTypeSelector () {
|
||||||
const { abiTypeIndex } = this.state;
|
const { abiTypeIndex, abiTypes } = this.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioButtons
|
<RadioButtons
|
||||||
name='contractType'
|
name='contractType'
|
||||||
value={ abiTypeIndex }
|
value={ abiTypeIndex }
|
||||||
values={ this.getAbiTypes() }
|
values={ abiTypes }
|
||||||
onChange={ this.onChangeABIType }
|
onChange={ this.onChangeABIType }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDialogActions () {
|
renderDialogActions () {
|
||||||
const { addressError, nameError, step } = this.state;
|
const { step } = this.store;
|
||||||
const hasError = !!(addressError || nameError);
|
|
||||||
|
|
||||||
const cancelBtn = (
|
const cancelBtn = (
|
||||||
<Button
|
<Button
|
||||||
icon={ <ContentClear /> }
|
icon={ <CancelIcon /> }
|
||||||
label='Cancel'
|
key='cancel'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.button.cancel'
|
||||||
|
defaultMessage='Cancel' />
|
||||||
|
}
|
||||||
onClick={ this.onClose } />
|
onClick={ this.onClose } />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (step === 0) {
|
if (step === 0) {
|
||||||
const nextBtn = (
|
return [
|
||||||
|
cancelBtn,
|
||||||
<Button
|
<Button
|
||||||
icon={ <NavigationArrowForward /> }
|
icon={ <NextIcon /> }
|
||||||
label='Next'
|
key='next'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.button.next'
|
||||||
|
defaultMessage='Next' />
|
||||||
|
}
|
||||||
onClick={ this.onNext } />
|
onClick={ this.onNext } />
|
||||||
);
|
];
|
||||||
|
|
||||||
return [ cancelBtn, nextBtn ];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevBtn = (
|
return [
|
||||||
|
cancelBtn,
|
||||||
<Button
|
<Button
|
||||||
icon={ <NavigationArrowBack /> }
|
icon={ <PrevIcon /> }
|
||||||
label='Back'
|
key='prev'
|
||||||
onClick={ this.onPrev } />
|
label={
|
||||||
);
|
<FormattedMessage
|
||||||
|
id='addContract.button.prev'
|
||||||
const addBtn = (
|
defaultMessage='Back' />
|
||||||
|
}
|
||||||
|
onClick={ this.onPrev } />,
|
||||||
<Button
|
<Button
|
||||||
icon={ <ContentAdd /> }
|
icon={ <AddIcon /> }
|
||||||
label='Add Contract'
|
key='add'
|
||||||
disabled={ hasError }
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.button.add'
|
||||||
|
defaultMessage='Add Contract' />
|
||||||
|
}
|
||||||
|
disabled={ this.store.hasError }
|
||||||
onClick={ this.onAdd } />
|
onClick={ this.onAdd } />
|
||||||
);
|
];
|
||||||
|
|
||||||
return [ cancelBtn, prevBtn, addBtn ];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFields () {
|
renderFields () {
|
||||||
const { abi, abiError, address, addressError, description, name, nameError, abiType } = this.state;
|
const { abi, abiError, abiType, address, addressError, description, name, nameError } = this.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<InputAddress
|
<InputAddress
|
||||||
label='network address'
|
|
||||||
hint='the network address for the contract'
|
|
||||||
error={ addressError }
|
error={ addressError }
|
||||||
value={ address }
|
hint={
|
||||||
onSubmit={ this.onEditAddress }
|
<FormattedMessage
|
||||||
|
id='addContract.address.hint'
|
||||||
|
defaultMessage='the network address for the contract' />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.address.label'
|
||||||
|
defaultMessage='network address' />
|
||||||
|
}
|
||||||
onChange={ this.onChangeAddress }
|
onChange={ this.onChangeAddress }
|
||||||
/>
|
onSubmit={ this.onEditAddress }
|
||||||
|
value={ address } />
|
||||||
<Input
|
<Input
|
||||||
label='contract name'
|
|
||||||
hint='a descriptive name for the contract'
|
|
||||||
error={ nameError }
|
error={ nameError }
|
||||||
value={ name }
|
hint={
|
||||||
onSubmit={ this.onEditName } />
|
<FormattedMessage
|
||||||
|
id='addContract.name.hint'
|
||||||
|
defaultMessage='a descriptive name for the contract' />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.name.label'
|
||||||
|
defaultMessage='contract name' />
|
||||||
|
}
|
||||||
|
onSubmit={ this.onEditName }
|
||||||
|
value={ name } />
|
||||||
<Input
|
<Input
|
||||||
multiLine
|
hint={
|
||||||
rows={ 1 }
|
<FormattedMessage
|
||||||
label='(optional) contract description'
|
id='addContract.description.hint'
|
||||||
hint='an expanded description for the entry'
|
defaultMessage='an expanded description for the entry' />
|
||||||
value={ description }
|
}
|
||||||
onSubmit={ this.onEditDescription } />
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.description.label'
|
||||||
|
defaultMessage='(optional) contract description' />
|
||||||
|
}
|
||||||
|
onSubmit={ this.onEditDescription }
|
||||||
|
value={ description } />
|
||||||
<Input
|
<Input
|
||||||
label='contract abi'
|
|
||||||
hint='the abi for the contract'
|
|
||||||
error={ abiError }
|
error={ abiError }
|
||||||
value={ abi }
|
hint={
|
||||||
readOnly={ abiType.readOnly }
|
<FormattedMessage
|
||||||
|
id='addContract.abi.hint'
|
||||||
|
defaultMessage='the abi for the contract' />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abi.label'
|
||||||
|
defaultMessage='contract abi' />
|
||||||
|
}
|
||||||
onSubmit={ this.onEditAbi }
|
onSubmit={ this.onEditAbi }
|
||||||
/>
|
readOnly={ abiType.readOnly }
|
||||||
|
value={ abi } />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAbiTypes () {
|
|
||||||
return ABI_TYPES.map((type, index) => ({
|
|
||||||
label: type.label,
|
|
||||||
description: type.description,
|
|
||||||
key: index,
|
|
||||||
...type
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
onNext = () => {
|
onNext = () => {
|
||||||
this.setState({ step: this.state.step + 1 });
|
this.store.nextStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
onPrev = () => {
|
onPrev = () => {
|
||||||
this.setState({ step: this.state.step - 1 });
|
this.store.prevStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeABIType = (value, index) => {
|
onChangeABIType = (value, index) => {
|
||||||
const abiType = value || ABI_TYPES[index];
|
this.store.setAbiTypeIndex(index);
|
||||||
this.setState({ abiTypeIndex: index, abiType });
|
|
||||||
this.onEditAbi(abiType.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditAbi = (abiIn) => {
|
onEditAbi = (abi) => {
|
||||||
const { api } = this.context;
|
this.store.setAbi(abi);
|
||||||
const { abi, abiError, abiParsed } = validateAbi(abiIn, api);
|
|
||||||
|
|
||||||
this.setState({ abi, abiError, abiParsed });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeAddress = (event, value) => {
|
onChangeAddress = (event, address) => {
|
||||||
this.onEditAddress(value);
|
this.onEditAddress(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditAddress = (_address) => {
|
onEditAddress = (address) => {
|
||||||
const { contracts } = this.props;
|
this.store.setAddress(address);
|
||||||
let { address, addressError } = validateAddress(_address);
|
|
||||||
|
|
||||||
if (!addressError) {
|
|
||||||
const contract = contracts[address];
|
|
||||||
|
|
||||||
if (contract) {
|
|
||||||
addressError = ERRORS.duplicateAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
address,
|
|
||||||
addressError
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditDescription = (description) => {
|
onEditDescription = (description) => {
|
||||||
this.setState({ description });
|
this.store.setDescription(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditName = (name) => {
|
onEditName = (name) => {
|
||||||
this.setState(validateName(name));
|
this.store.setName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd = () => {
|
onAdd = () => {
|
||||||
const { api } = this.context;
|
return this.store
|
||||||
const { abiParsed, address, name, description, abiType } = this.state;
|
.addContract()
|
||||||
|
.then(() => {
|
||||||
Promise.all([
|
this.onClose();
|
||||||
api.parity.setAccountName(address, name),
|
|
||||||
api.parity.setAccountMeta(address, {
|
|
||||||
contract: true,
|
|
||||||
deleted: false,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
abi: abiParsed,
|
|
||||||
type: abiType.type,
|
|
||||||
description
|
|
||||||
})
|
})
|
||||||
]).catch((error) => {
|
.catch((error) => {
|
||||||
console.error('onAdd', error);
|
newError(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.onClose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
|
55
js/src/modals/AddContract/addContract.spec.js
Normal file
55
js/src/modals/AddContract/addContract.spec.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (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/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import AddContract from './';
|
||||||
|
|
||||||
|
import { CONTRACTS, createApi } from './addContract.test.js';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let onClose;
|
||||||
|
|
||||||
|
function renderShallow (props) {
|
||||||
|
onClose = sinon.stub();
|
||||||
|
component = shallow(
|
||||||
|
<AddContract
|
||||||
|
{ ...props }
|
||||||
|
contracts={ CONTRACTS }
|
||||||
|
onClose={ onClose } />,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
api: createApi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/AddContract', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
renderShallow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the defauls', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
38
js/src/modals/AddContract/addContract.test.js
Normal file
38
js/src/modals/AddContract/addContract.test.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (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/>.
|
||||||
|
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
const ABI = '[{"constant":true,"inputs":[],"name":"totalDonated","outputs":[{"name":"","type":"uint256"}],"type":"function"}]';
|
||||||
|
|
||||||
|
const CONTRACTS = {
|
||||||
|
'0x1234567890123456789012345678901234567890': {}
|
||||||
|
};
|
||||||
|
|
||||||
|
function createApi () {
|
||||||
|
return {
|
||||||
|
parity: {
|
||||||
|
setAccountMeta: sinon.stub().resolves(),
|
||||||
|
setAccountName: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ABI,
|
||||||
|
CONTRACTS,
|
||||||
|
createApi
|
||||||
|
};
|
126
js/src/modals/AddContract/store.js
Normal file
126
js/src/modals/AddContract/store.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (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/>.
|
||||||
|
|
||||||
|
import { action, computed, observable, transaction } from 'mobx';
|
||||||
|
|
||||||
|
import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation';
|
||||||
|
|
||||||
|
import { ABI_TYPES } from './types';
|
||||||
|
|
||||||
|
export default class Store {
|
||||||
|
@observable abi = '';
|
||||||
|
@observable abiError = ERRORS.invalidAbi;
|
||||||
|
@observable abiParsed = null;
|
||||||
|
@observable abiTypes = ABI_TYPES;
|
||||||
|
@observable abiTypeIndex = 0;
|
||||||
|
@observable address = '';
|
||||||
|
@observable addressError = ERRORS.invalidAddress;
|
||||||
|
@observable description = '';
|
||||||
|
@observable name = '';
|
||||||
|
@observable nameError = ERRORS.invalidName;
|
||||||
|
@observable step = 0;
|
||||||
|
|
||||||
|
constructor (api, contracts) {
|
||||||
|
this._api = api;
|
||||||
|
this._contracts = contracts;
|
||||||
|
|
||||||
|
this.setAbiTypeIndex(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get abiType () {
|
||||||
|
return this.abiTypes[this.abiTypeIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get hasError () {
|
||||||
|
return !!(this.abiError || this.addressError || this.nameError);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action nextStep = () => {
|
||||||
|
this.step++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action prevStep = () => {
|
||||||
|
this.step--;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setAbi = (_abi) => {
|
||||||
|
const { abi, abiError, abiParsed } = validateAbi(_abi);
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.abi = abi;
|
||||||
|
this.abiError = abiError;
|
||||||
|
this.abiParsed = abiParsed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setAbiTypeIndex = (abiTypeIndex) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.abiTypeIndex = abiTypeIndex;
|
||||||
|
this.setAbi(this.abiTypes[abiTypeIndex].value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setAddress = (_address) => {
|
||||||
|
let { address, addressError } = validateAddress(_address);
|
||||||
|
|
||||||
|
if (!addressError) {
|
||||||
|
const contract = this._contracts[address];
|
||||||
|
|
||||||
|
if (contract) {
|
||||||
|
addressError = ERRORS.duplicateAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.address = address;
|
||||||
|
this.addressError = addressError;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setDescription = (description) => {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setName = (_name) => {
|
||||||
|
const { name, nameError } = validateName(_name);
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.name = name;
|
||||||
|
this.nameError = nameError;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addContract () {
|
||||||
|
const meta = {
|
||||||
|
contract: true,
|
||||||
|
deleted: false,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
abi: this.abiParsed,
|
||||||
|
type: this.abiType.type,
|
||||||
|
description: this.description
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise
|
||||||
|
.all([
|
||||||
|
this._api.parity.setAccountName(this.address, this.name),
|
||||||
|
this._api.parity.setAccountMeta(this.address, meta)
|
||||||
|
])
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('addContract', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
171
js/src/modals/AddContract/store.spec.js
Normal file
171
js/src/modals/AddContract/store.spec.js
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (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/>.
|
||||||
|
|
||||||
|
import Store from './store';
|
||||||
|
|
||||||
|
import { ABI, CONTRACTS, createApi } from './addContract.test.js';
|
||||||
|
|
||||||
|
const INVALID_ADDR = '0x123';
|
||||||
|
const VALID_ADDR = '0x5A5eFF38DA95b0D58b6C616f2699168B480953C9';
|
||||||
|
const DUPE_ADDR = Object.keys(CONTRACTS)[0];
|
||||||
|
|
||||||
|
let api;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function createStore () {
|
||||||
|
api = createApi();
|
||||||
|
store = new Store(api, CONTRACTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/AddContract/Store', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('creates an instance', () => {
|
||||||
|
expect(store).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to custom ABI', () => {
|
||||||
|
expect(store.abiType.type).to.equal('custom');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@actions', () => {
|
||||||
|
describe('nextStep/prevStep', () => {
|
||||||
|
it('moves to the next/prev step', () => {
|
||||||
|
expect(store.step).to.equal(0);
|
||||||
|
store.nextStep();
|
||||||
|
expect(store.step).to.equal(1);
|
||||||
|
store.prevStep();
|
||||||
|
expect(store.step).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAbiTypeIndex', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setAbiTypeIndex(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes the index', () => {
|
||||||
|
expect(store.abiTypeIndex).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes the abi', () => {
|
||||||
|
expect(store.abi).to.deep.equal(store.abiTypes[1].value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAddress', () => {
|
||||||
|
it('sets a valid address', () => {
|
||||||
|
store.setAddress(VALID_ADDR);
|
||||||
|
expect(store.address).to.equal(VALID_ADDR);
|
||||||
|
expect(store.addressError).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the error on invalid address', () => {
|
||||||
|
store.setAddress(INVALID_ADDR);
|
||||||
|
expect(store.address).to.equal(INVALID_ADDR);
|
||||||
|
expect(store.addressError).not.to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the error on suplicate address', () => {
|
||||||
|
store.setAddress(DUPE_ADDR);
|
||||||
|
expect(store.address).to.equal(DUPE_ADDR);
|
||||||
|
expect(store.addressError).not.to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDescription', () => {
|
||||||
|
it('sets the description', () => {
|
||||||
|
store.setDescription('test description');
|
||||||
|
expect(store.description).to.equal('test description');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setName', () => {
|
||||||
|
it('sets the name', () => {
|
||||||
|
store.setName('some name');
|
||||||
|
expect(store.name).to.equal('some name');
|
||||||
|
expect(store.nameError).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the error', () => {
|
||||||
|
store.setName('s');
|
||||||
|
expect(store.name).to.equal('s');
|
||||||
|
expect(store.nameError).not.to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@computed', () => {
|
||||||
|
describe('abiType', () => {
|
||||||
|
it('matches the index', () => {
|
||||||
|
expect(store.abiType).to.deep.equal(store.abiTypes[2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasError', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setAddress(VALID_ADDR);
|
||||||
|
store.setName('valid name');
|
||||||
|
store.setAbi(ABI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is false with no errors', () => {
|
||||||
|
expect(store.hasError).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is true with address error', () => {
|
||||||
|
store.setAddress(DUPE_ADDR);
|
||||||
|
expect(store.hasError).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is true with name error', () => {
|
||||||
|
store.setName('s');
|
||||||
|
expect(store.hasError).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is true with abi error', () => {
|
||||||
|
store.setAbi('');
|
||||||
|
expect(store.hasError).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interactions', () => {
|
||||||
|
describe('addContract', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setAddress(VALID_ADDR);
|
||||||
|
store.setName('valid name');
|
||||||
|
store.setAbi(ABI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account name', () => {
|
||||||
|
return store.addContract().then(() => {
|
||||||
|
expect(api.parity.setAccountName).to.have.been.calledWith(VALID_ADDR, 'valid name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the account meta', () => {
|
||||||
|
return store.addContract().then(() => {
|
||||||
|
expect(api.parity.setAccountMeta).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
81
js/src/modals/AddContract/types.js
Normal file
81
js/src/modals/AddContract/types.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2015, 2016 Parity Technologies (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/>.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { eip20, wallet } from '~/contracts/abi';
|
||||||
|
|
||||||
|
const ABI_TYPES = [
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abiType.token.description'
|
||||||
|
defaultMessage='A standard {erc20} token'
|
||||||
|
values={ {
|
||||||
|
erc20:
|
||||||
|
<a href='https://github.com/ethereum/EIPs/issues/20' target='_blank'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abiType.token.erc20'
|
||||||
|
defaultMessage='ERC 20' />
|
||||||
|
</a>
|
||||||
|
} } />,
|
||||||
|
label:
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abiType.token.label'
|
||||||
|
defaultMessage='Token' />,
|
||||||
|
readOnly: true,
|
||||||
|
type: 'token',
|
||||||
|
value: JSON.stringify(eip20)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abiType.multisigWallet.description'
|
||||||
|
defaultMessage='Ethereum Multisig contract {link}'
|
||||||
|
values={ {
|
||||||
|
link:
|
||||||
|
<a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abiType.multisigWallet.link'
|
||||||
|
defaultMessage='see contract code' />
|
||||||
|
</a>
|
||||||
|
} } />,
|
||||||
|
label:
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abiType.multisigWallet.label'
|
||||||
|
defaultMessage='Multisig Wallet' />,
|
||||||
|
readOnly: true,
|
||||||
|
type: 'multisig',
|
||||||
|
value: JSON.stringify(wallet)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abiType.custom.description'
|
||||||
|
defaultMessage='Contract created from custom ABI' />,
|
||||||
|
label:
|
||||||
|
<FormattedMessage
|
||||||
|
id='addContract.abiType.custom.label'
|
||||||
|
defaultMessage='Custom Contract' />,
|
||||||
|
type: 'custom',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export {
|
||||||
|
ABI_TYPES
|
||||||
|
};
|
@ -14,19 +14,18 @@
|
|||||||
// 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 { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
import { arrayOrObjectProptype } from '~/util/proptypes';
|
||||||
|
|
||||||
import styles from './radioButtons.css';
|
import styles from './radioButtons.css';
|
||||||
|
|
||||||
export default class RadioButtons extends Component {
|
export default class RadioButtons extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
name: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
values: PropTypes.array.isRequired,
|
|
||||||
|
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
name: PropTypes.string
|
values: arrayOrObjectProptype().isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -40,16 +39,16 @@ export default class RadioButtons extends Component {
|
|||||||
const index = Number.isNaN(parseInt(value))
|
const index = Number.isNaN(parseInt(value))
|
||||||
? values.findIndex((val) => val.key === value)
|
? values.findIndex((val) => val.key === value)
|
||||||
: parseInt(value);
|
: parseInt(value);
|
||||||
|
const selectedValue = typeof value !== 'object'
|
||||||
const selectedValue = typeof value !== 'object' ? values[index] : value;
|
? values[index]
|
||||||
|
: value;
|
||||||
const key = this.getKey(selectedValue, index);
|
const key = this.getKey(selectedValue, index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
valueSelected={ key }
|
|
||||||
name={ name }
|
name={ name }
|
||||||
onChange={ this.onChange }
|
onChange={ this.onChange }
|
||||||
>
|
valueSelected={ key } >
|
||||||
{ this.renderContent() }
|
{ this.renderContent() }
|
||||||
</RadioButtonGroup>
|
</RadioButtonGroup>
|
||||||
);
|
);
|
||||||
@ -59,7 +58,9 @@ export default class RadioButtons extends Component {
|
|||||||
const { values } = this.props;
|
const { values } = this.props;
|
||||||
|
|
||||||
return values.map((value, index) => {
|
return values.map((value, index) => {
|
||||||
const label = typeof value === 'string' ? value : value.label || '';
|
const label = typeof value === 'string'
|
||||||
|
? value
|
||||||
|
: value.label || '';
|
||||||
const description = (typeof value !== 'string' && value.description) || null;
|
const description = (typeof value !== 'string' && value.description) || null;
|
||||||
const key = this.getKey(value, index);
|
const key = this.getKey(value, index);
|
||||||
|
|
||||||
@ -67,28 +68,26 @@ export default class RadioButtons extends Component {
|
|||||||
<RadioButton
|
<RadioButton
|
||||||
className={ styles.spaced }
|
className={ styles.spaced }
|
||||||
key={ index }
|
key={ index }
|
||||||
|
label={
|
||||||
value={ key }
|
|
||||||
label={ (
|
|
||||||
<div className={ styles.typeContainer }>
|
<div className={ styles.typeContainer }>
|
||||||
<span>{ label }</span>
|
<span>{ label }</span>
|
||||||
{
|
{
|
||||||
description
|
description
|
||||||
? (
|
? <span className={ styles.desc }>{ description }</span>
|
||||||
<span className={ styles.desc }>{ description }</span>
|
|
||||||
)
|
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
) }
|
}
|
||||||
/>
|
value={ key } />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getKey (value, index) {
|
getKey (value, index) {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return typeof value.key === 'undefined' ? index : value.key;
|
return typeof value.key === 'undefined'
|
||||||
|
? index
|
||||||
|
: value.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
return index;
|
return index;
|
||||||
@ -96,8 +95,8 @@ export default class RadioButtons extends Component {
|
|||||||
|
|
||||||
onChange = (event, index) => {
|
onChange = (event, index) => {
|
||||||
const { onChange, values } = this.props;
|
const { onChange, values } = this.props;
|
||||||
|
|
||||||
const value = values[index] || values.find((v) => v.key === index);
|
const value = values[index] || values.find((v) => v.key === index);
|
||||||
|
|
||||||
onChange(value, index);
|
onChange(value, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,13 @@
|
|||||||
|
|
||||||
import { PropTypes } from 'react';
|
import { PropTypes } from 'react';
|
||||||
|
|
||||||
|
export function arrayOrObjectProptype () {
|
||||||
|
return PropTypes.oneOfType([
|
||||||
|
PropTypes.array,
|
||||||
|
PropTypes.object
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
export function nullableProptype (type) {
|
export function nullableProptype (type) {
|
||||||
return PropTypes.oneOfType([
|
return PropTypes.oneOfType([
|
||||||
PropTypes.oneOf([ null ]),
|
PropTypes.oneOf([ null ]),
|
||||||
|
@ -35,21 +35,21 @@ export const ERRORS = {
|
|||||||
gasBlockLimit: 'the transaction execution will exceed the block gas limit'
|
gasBlockLimit: 'the transaction execution will exceed the block gas limit'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function validateAbi (abi, api) {
|
export function validateAbi (abi) {
|
||||||
let abiError = null;
|
let abiError = null;
|
||||||
let abiParsed = null;
|
let abiParsed = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
abiParsed = JSON.parse(abi);
|
abiParsed = JSON.parse(abi);
|
||||||
|
|
||||||
if (!api.util.isArray(abiParsed)) {
|
if (!util.isArray(abiParsed)) {
|
||||||
abiError = ERRORS.invalidAbi;
|
abiError = ERRORS.invalidAbi;
|
||||||
return { abi, abiError, abiParsed };
|
return { abi, abiError, abiParsed };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate each elements of the Array
|
// Validate each elements of the Array
|
||||||
const invalidIndex = abiParsed
|
const invalidIndex = abiParsed
|
||||||
.map((o) => isValidAbiEvent(o, api) || isValidAbiFunction(o, api) || isAbiFallback(o))
|
.map((o) => isValidAbiEvent(o) || isValidAbiFunction(o) || isAbiFallback(o))
|
||||||
.findIndex((valid) => !valid);
|
.findIndex((valid) => !valid);
|
||||||
|
|
||||||
if (invalidIndex !== -1) {
|
if (invalidIndex !== -1) {
|
||||||
@ -70,13 +70,13 @@ export function validateAbi (abi, api) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidAbiFunction (object, api) {
|
function isValidAbiFunction (object) {
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
|
return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
|
||||||
(object.inputs && api.util.isArray(object.inputs));
|
(object.inputs && util.isArray(object.inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAbiFallback (object) {
|
function isAbiFallback (object) {
|
||||||
@ -87,14 +87,14 @@ function isAbiFallback (object) {
|
|||||||
return object.type === 'fallback';
|
return object.type === 'fallback';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidAbiEvent (object, api) {
|
function isValidAbiEvent (object) {
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (object.type === 'event') &&
|
return (object.type === 'event') &&
|
||||||
(object.name) &&
|
(object.name) &&
|
||||||
(object.inputs && api.util.isArray(object.inputs));
|
(object.inputs && util.isArray(object.inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateAddress (address) {
|
export function validateAddress (address) {
|
||||||
|
Loading…
Reference in New Issue
Block a user