a99721004b
* Watch the requests and display them throughout the app * Linting * Showing Requests * Fully working Transaction Requests Display * Add FormattedMessage to Requests * Clean-up the Transfer dialog * Update Validations * Cleanup Create Wallet * Clean Deploy Contract Dialog * Cleanup Contract Execution * Fix Requests * Cleanup Wallet Settings * Don't show stepper in Portal if less than 2 steps * WIP local storage requests * Caching requests and saving contract deployments * Add Historic prop to Requests MethodDecoding * Fix tests * Add Contract address to MethodDecoding * PR Grumbles - Part I * PR Grumbles - Part II * Use API Subscription methods * Linting * Move SavedRequests and add tests * Added tests for Requests Actions * Fixing tests * PR Grumbles + Playground fix * Revert Playground changes * PR Grumbles * Better showEth in MethodDecoding
514 lines
12 KiB
JavaScript
514 lines
12 KiB
JavaScript
// Copyright 2015-2017 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 BigNumber from 'bignumber.js';
|
|
import { pick } from 'lodash';
|
|
import { observer } from 'mobx-react';
|
|
import React, { Component, PropTypes } from 'react';
|
|
import { FormattedMessage } from 'react-intl';
|
|
import { connect } from 'react-redux';
|
|
import { bindActionCreators } from 'redux';
|
|
|
|
import { Button, GasPriceEditor, IdentityIcon, Portal, Warning } from '~/ui';
|
|
import { CancelIcon } from '~/ui/Icons';
|
|
import { ERRORS, validateAbi, validateCode, validateName, validatePositiveNumber } from '~/util/validation';
|
|
import { deploy, deployEstimateGas } from '~/util/tx';
|
|
import { setRequest } from '~/redux/providers/requestsActions';
|
|
|
|
import DetailsStep from './DetailsStep';
|
|
import ParametersStep from './ParametersStep';
|
|
import Extras from '../Transfer/Extras';
|
|
|
|
const STEPS = {
|
|
CONTRACT_DETAILS: {
|
|
title: (
|
|
<FormattedMessage
|
|
id='deployContract.title.details'
|
|
defaultMessage='contract details'
|
|
/>
|
|
)
|
|
},
|
|
CONTRACT_PARAMETERS: {
|
|
title: (
|
|
<FormattedMessage
|
|
id='deployContract.title.parameters'
|
|
defaultMessage='contract parameters'
|
|
/>
|
|
)
|
|
},
|
|
EXTRAS: {
|
|
title: (
|
|
<FormattedMessage
|
|
id='deployContract.title.extras'
|
|
defaultMessage='extra information'
|
|
/>
|
|
)
|
|
}
|
|
};
|
|
|
|
@observer
|
|
class DeployContract extends Component {
|
|
static contextTypes = {
|
|
api: PropTypes.object.isRequired,
|
|
store: PropTypes.object.isRequired
|
|
};
|
|
|
|
static propTypes = {
|
|
accounts: PropTypes.object.isRequired,
|
|
abi: PropTypes.string,
|
|
balances: PropTypes.object,
|
|
code: PropTypes.string,
|
|
gasLimit: PropTypes.object.isRequired,
|
|
onClose: PropTypes.func.isRequired,
|
|
onSetRequest: PropTypes.func.isRequired,
|
|
readOnly: PropTypes.bool,
|
|
source: PropTypes.string
|
|
};
|
|
|
|
static defaultProps = {
|
|
readOnly: false,
|
|
source: ''
|
|
};
|
|
|
|
gasStore = new GasPriceEditor.Store(this.context.api, { gasLimit: this.props.gasLimit });
|
|
|
|
state = {
|
|
abi: '',
|
|
abiError: ERRORS.invalidAbi,
|
|
amount: '0',
|
|
amountValue: new BigNumber(0),
|
|
amountError: '',
|
|
code: '',
|
|
codeError: ERRORS.invalidCode,
|
|
description: '',
|
|
descriptionError: null,
|
|
extras: false,
|
|
fromAddress: Object.keys(this.props.accounts)[0],
|
|
fromAddressError: null,
|
|
name: '',
|
|
nameError: ERRORS.invalidName,
|
|
params: [],
|
|
paramsError: [],
|
|
inputs: [],
|
|
step: 'CONTRACT_DETAILS'
|
|
};
|
|
|
|
componentWillMount () {
|
|
const { abi, code } = this.props;
|
|
|
|
if (abi && code) {
|
|
this.setState({ abi, code });
|
|
}
|
|
}
|
|
|
|
componentWillReceiveProps (nextProps) {
|
|
const { abi, code } = nextProps;
|
|
const newState = {};
|
|
|
|
if (abi !== this.props.abi) {
|
|
newState.abi = abi;
|
|
}
|
|
|
|
if (code !== this.props.code) {
|
|
newState.code = code;
|
|
}
|
|
|
|
if (Object.keys(newState).length) {
|
|
this.setState(newState);
|
|
}
|
|
}
|
|
|
|
render () {
|
|
const { step, inputs } = this.state;
|
|
|
|
const realStepKeys = Object.keys(STEPS)
|
|
.filter((k) => {
|
|
if (k === 'CONTRACT_PARAMETERS') {
|
|
return inputs.length > 0;
|
|
}
|
|
|
|
if (k === 'EXTRAS') {
|
|
return this.state.extras;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
const realStep = realStepKeys.findIndex((k) => k === step);
|
|
const realSteps = realStepKeys.map((k) => STEPS[k]);
|
|
|
|
return (
|
|
<Portal
|
|
buttons={ this.renderDialogActions() }
|
|
activeStep={ realStep }
|
|
onClose={ this.onClose }
|
|
open
|
|
steps={ realSteps.map((s) => s.title) }
|
|
>
|
|
{ this.renderExceptionWarning() }
|
|
{ this.renderStep() }
|
|
</Portal>
|
|
);
|
|
}
|
|
|
|
renderExceptionWarning () {
|
|
const { step } = this.state;
|
|
const { errorEstimated } = this.gasStore;
|
|
const realStep = Object.keys(STEPS).findIndex((k) => k === step);
|
|
|
|
if (!errorEstimated || realStep >= 2) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Warning warning={ errorEstimated } />
|
|
);
|
|
}
|
|
|
|
renderDialogActions () {
|
|
const { deployError, abiError, amountError, codeError, nameError, descriptionError, fromAddressError, fromAddress, step } = this.state;
|
|
const isValid = !nameError && !fromAddressError && !descriptionError && !abiError && !codeError && !amountError;
|
|
|
|
const cancelBtn = (
|
|
<Button
|
|
icon={ <CancelIcon /> }
|
|
key='cancel'
|
|
label={
|
|
<FormattedMessage
|
|
id='deployContract.button.cancel'
|
|
defaultMessage='Cancel'
|
|
/>
|
|
}
|
|
onClick={ this.onClose }
|
|
/>
|
|
);
|
|
|
|
const closeBtn = (
|
|
<Button
|
|
icon={ <CancelIcon /> }
|
|
key='close'
|
|
label={
|
|
<FormattedMessage
|
|
id='deployContract.button.close'
|
|
defaultMessage='Close'
|
|
/>
|
|
}
|
|
onClick={ this.onClose }
|
|
/>
|
|
);
|
|
|
|
if (deployError) {
|
|
return closeBtn;
|
|
}
|
|
|
|
const createButton = (
|
|
<Button
|
|
icon={
|
|
<IdentityIcon
|
|
address={ fromAddress }
|
|
button
|
|
/>
|
|
}
|
|
key='create'
|
|
label={
|
|
<FormattedMessage
|
|
id='deployContract.button.create'
|
|
defaultMessage='Create'
|
|
/>
|
|
}
|
|
onClick={ this.onDeployStart }
|
|
/>
|
|
);
|
|
|
|
const nextButton = (
|
|
<Button
|
|
disabled={ !isValid }
|
|
key='next'
|
|
icon={
|
|
<IdentityIcon
|
|
address={ fromAddress }
|
|
button
|
|
/>
|
|
}
|
|
label={
|
|
<FormattedMessage
|
|
id='deployContract.button.next'
|
|
defaultMessage='Next'
|
|
/>
|
|
}
|
|
onClick={ this.onNextStep }
|
|
/>
|
|
);
|
|
|
|
const hasParameters = this.state.inputs.length > 0;
|
|
const showExtras = this.state.extras;
|
|
|
|
switch (step) {
|
|
case 'CONTRACT_DETAILS':
|
|
return [
|
|
cancelBtn,
|
|
hasParameters || showExtras
|
|
? nextButton
|
|
: createButton
|
|
];
|
|
|
|
case 'CONTRACT_PARAMETERS':
|
|
return [
|
|
cancelBtn,
|
|
showExtras
|
|
? nextButton
|
|
: createButton
|
|
];
|
|
|
|
case 'EXTRAS':
|
|
return [
|
|
cancelBtn,
|
|
createButton
|
|
];
|
|
}
|
|
}
|
|
|
|
renderStep () {
|
|
const { accounts, readOnly, balances } = this.props;
|
|
const { step } = this.state;
|
|
|
|
switch (step) {
|
|
case 'CONTRACT_DETAILS':
|
|
return (
|
|
<DetailsStep
|
|
{ ...this.state }
|
|
accounts={ accounts }
|
|
balances={ balances }
|
|
onAmountChange={ this.onAmountChange }
|
|
onExtrasChange={ this.onExtrasChange }
|
|
onFromAddressChange={ this.onFromAddressChange }
|
|
onDescriptionChange={ this.onDescriptionChange }
|
|
onNameChange={ this.onNameChange }
|
|
onAbiChange={ this.onAbiChange }
|
|
onCodeChange={ this.onCodeChange }
|
|
onParamsChange={ this.onParamsChange }
|
|
onInputsChange={ this.onInputsChange }
|
|
readOnly={ readOnly }
|
|
/>
|
|
);
|
|
|
|
case 'CONTRACT_PARAMETERS':
|
|
return (
|
|
<ParametersStep
|
|
{ ...this.state }
|
|
accounts={ accounts }
|
|
onParamsChange={ this.onParamsChange }
|
|
readOnly={ readOnly }
|
|
/>
|
|
);
|
|
|
|
case 'EXTRAS':
|
|
return this.renderExtrasPage();
|
|
}
|
|
}
|
|
|
|
renderExtrasPage () {
|
|
if (!this.gasStore.histogram) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Extras
|
|
gasStore={ this.gasStore }
|
|
hideData
|
|
isEth
|
|
/>
|
|
);
|
|
}
|
|
|
|
estimateGas = () => {
|
|
const { api } = this.context;
|
|
const { abiError, abiParsed, amountValue, amountError, code, codeError, fromAddress, fromAddressError, params } = this.state;
|
|
|
|
if (abiError || codeError || fromAddressError || amountError) {
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
data: code,
|
|
from: fromAddress,
|
|
value: amountValue
|
|
};
|
|
|
|
const contract = api.newContract(abiParsed);
|
|
|
|
deployEstimateGas(contract, options, params)
|
|
.then(([gasEst, gas]) => {
|
|
this.gasStore.setEstimated(gasEst.toFixed(0));
|
|
this.gasStore.setGas(gas.toFixed(0));
|
|
})
|
|
.catch((error) => {
|
|
this.gasStore.setEstimatedError();
|
|
console.warn('estimateGas', error);
|
|
});
|
|
}
|
|
|
|
onNextStep = () => {
|
|
switch (this.state.step) {
|
|
case 'CONTRACT_DETAILS':
|
|
return this.onParametersStep();
|
|
|
|
case 'CONTRACT_PARAMETERS':
|
|
return this.onExtrasStep();
|
|
|
|
default:
|
|
console.warn('wrong call of "onNextStep" from', this.state.step);
|
|
}
|
|
}
|
|
|
|
onParametersStep = () => {
|
|
const { inputs } = this.state;
|
|
|
|
if (inputs.length) {
|
|
return this.setState({ step: 'CONTRACT_PARAMETERS' });
|
|
}
|
|
|
|
return this.onExtrasStep();
|
|
}
|
|
|
|
onExtrasStep = () => {
|
|
if (this.state.extras) {
|
|
return this.setState({ step: 'EXTRAS' });
|
|
}
|
|
|
|
return this.onDeployStart();
|
|
}
|
|
|
|
onDescriptionChange = (description) => {
|
|
this.setState({ description, descriptionError: null });
|
|
}
|
|
|
|
onFromAddressChange = (fromAddress) => {
|
|
const { api } = this.context;
|
|
|
|
const fromAddressError = api.util.isAddressValid(fromAddress)
|
|
? null
|
|
: (
|
|
<FormattedMessage
|
|
id='deployContract.owner.noneSelected'
|
|
defaultMessage='a valid account as the contract owner needs to be selected'
|
|
/>
|
|
);
|
|
|
|
this.setState({ fromAddress, fromAddressError }, this.estimateGas);
|
|
}
|
|
|
|
onNameChange = (name) => {
|
|
this.setState(validateName(name));
|
|
}
|
|
|
|
onParamsChange = (params) => {
|
|
this.setState({ params }, this.estimateGas);
|
|
}
|
|
|
|
onInputsChange = (inputs) => {
|
|
this.setState({ inputs }, this.estimateGas);
|
|
}
|
|
|
|
onAbiChange = (abi) => {
|
|
const { api } = this.context;
|
|
|
|
this.setState(validateAbi(abi, api), this.estimateGas);
|
|
}
|
|
|
|
onCodeChange = (code) => {
|
|
this.setState(validateCode(code), this.estimateGas);
|
|
}
|
|
|
|
onAmountChange = (amount) => {
|
|
const { numberError } = validatePositiveNumber(amount);
|
|
const nextAmountValue = numberError
|
|
? new BigNumber(0)
|
|
: this.context.api.util.toWei(amount);
|
|
|
|
this.gasStore.setEthValue(nextAmountValue);
|
|
this.setState({ amount, amountValue: nextAmountValue, amountError: numberError }, this.estimateGas);
|
|
}
|
|
|
|
onExtrasChange = (extras) => {
|
|
this.setState({ extras });
|
|
}
|
|
|
|
onDeployStart = () => {
|
|
const { api } = this.context;
|
|
const { source } = this.props;
|
|
const { abiParsed, amountValue, code, description, name, params, fromAddress } = this.state;
|
|
|
|
const metadata = {
|
|
abi: abiParsed,
|
|
contract: true,
|
|
deleted: false,
|
|
timestamp: Date.now(),
|
|
name,
|
|
description,
|
|
source
|
|
};
|
|
|
|
const options = this.gasStore.overrideTransaction({
|
|
data: code,
|
|
from: fromAddress,
|
|
value: amountValue
|
|
});
|
|
|
|
const contract = api.newContract(abiParsed);
|
|
|
|
this.onClose();
|
|
deploy(contract, options, params, true)
|
|
.then((requestId) => {
|
|
const requestMetadata = { ...metadata, deployment: true };
|
|
|
|
this.props.onSetRequest(requestId, { metadata: requestMetadata }, false);
|
|
});
|
|
}
|
|
|
|
onClose = () => {
|
|
this.props.onClose();
|
|
}
|
|
}
|
|
|
|
function mapStateToProps (initState, initProps) {
|
|
const { accounts } = initProps;
|
|
|
|
const fromAddresses = Object.keys(accounts);
|
|
|
|
return (state) => {
|
|
const balances = pick(state.balances.balances, fromAddresses);
|
|
const { gasLimit } = state.nodeStatus;
|
|
|
|
return {
|
|
accounts,
|
|
balances,
|
|
gasLimit
|
|
};
|
|
};
|
|
}
|
|
|
|
function mapDispatchToProps (dispatch) {
|
|
return bindActionCreators({
|
|
onSetRequest: setRequest
|
|
}, dispatch);
|
|
}
|
|
|
|
export default connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(DeployContract);
|