MyCrypto/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx

264 lines
7.6 KiB
TypeScript

import React, { Component } from 'react';
import translate from 'translations';
import './InteractExplorer.scss';
import { TShowNotification, showNotification } from 'actions/notifications';
import { getNodeLib } from 'selectors/config';
import { getTo, getDataExists } from 'selectors/transaction';
import { GenerateTransaction } from 'components/GenerateTransaction';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { Fields } from './components';
import { setDataField, TSetDataField } from 'actions/transaction';
import { Data } from 'libs/units';
import { Web3Node } from 'libs/nodes';
import RpcNode from 'libs/nodes/rpc';
import { Input } from 'components/ui';
import Dropdown from 'components/ui/Dropdown';
interface StateProps {
nodeLib: RpcNode | Web3Node;
to: AppState['transaction']['fields']['to'];
dataExists: boolean;
}
interface DispatchProps {
showNotification: TShowNotification;
setDataField: TSetDataField;
}
interface OwnProps {
contractFunctions: any;
}
type Props = StateProps & DispatchProps & OwnProps;
interface State {
inputs: {
[key: string]: { rawData: string; parsedData: string[] | string };
};
outputs: any;
selectedFunction: null | ContractOption;
}
interface ContractFunction {
constant: boolean;
decodeInput: any;
decodeOutput: any;
encodeInput: any;
inputs: any[];
outputs: any;
}
interface ContractOption {
contract: ContractFunction;
name: string;
}
class InteractExplorerClass extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
contractFunctions: {}
};
public state: State = {
selectedFunction: null,
inputs: {},
outputs: {}
};
public render() {
const { inputs, outputs, selectedFunction } = this.state;
const contractFunctionsOptions = this.contractOptions();
const { to } = this.props;
const generateOrWriteButton = this.props.dataExists ? (
<GenerateTransaction />
) : (
<button
className="InteractExplorer-func-submit btn btn-primary"
onClick={this.handleFunctionSend}
>
{translate('CONTRACT_Write')}
</button>
);
return (
<div className="InteractExplorer">
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">
{translate('CONTRACT_Interact_Title')}
<div className="flex-spacer" />
<span className="small">{to.raw}</span>
</div>
<Dropdown
name="exploreContract"
value={selectedFunction as any}
placeholder="Please select a function..."
onChange={this.handleFunctionSelect}
options={contractFunctionsOptions}
clearable={false}
labelKey="name"
valueKey="contract"
/>
</label>
</div>
{selectedFunction && (
<div key={selectedFunction.name} className="InteractExplorer-func">
{/* TODO: Use reusable components with validation */}
{selectedFunction.contract.inputs.map(input => {
const { type, name } = input;
return (
<div key={name} className="input-group-wrapper InteractExplorer-func-in">
<label className="input-group">
<div className="input-group-header">{name + ' ' + type}</div>
<Input
className="InteractExplorer-func-in-input"
name={name}
value={(inputs[name] && inputs[name].rawData) || ''}
onChange={this.handleInputChange}
/>
</label>
</div>
);
})}
{selectedFunction.contract.outputs.map((output: any, index: number) => {
const { type, name } = output;
const parsedName = name === '' ? index : name;
return (
<div key={parsedName} className="input-group-wrapper InteractExplorer-func-out">
<label className="input-group">
<div className="input-group-header"> {name + ' ' + type}</div>
<Input
className="InteractExplorer-func-out-input "
value={outputs[parsedName] || ''}
disabled={true}
/>
</label>
</div>
);
})}
{selectedFunction.contract.constant ? (
<button
className="InteractExplorer-func-submit btn btn-primary"
onClick={this.handleFunctionCall}
>
{translate('CONTRACT_Read')}
</button>
) : (
<React.Fragment>
<Fields button={generateOrWriteButton} />
</React.Fragment>
)}
</div>
)}
</div>
);
}
private contractOptions = () => {
const { contractFunctions } = this.props;
const transformedContractFunction: ContractOption[] = Object.keys(contractFunctions).map(
contractFunction => {
const contract = contractFunctions[contractFunction];
return {
name: contractFunction,
contract
};
}
);
return transformedContractFunction;
};
private handleFunctionCall = async (_: React.FormEvent<HTMLButtonElement>) => {
try {
const data = this.encodeData();
const { nodeLib, to } = this.props;
const { selectedFunction } = this.state;
if (!to.value) {
throw Error();
}
const callData = { to: to.raw, data };
const results = await nodeLib.sendCallRequest(callData);
const parsedResult = selectedFunction!.contract.decodeOutput(results);
this.setState({ outputs: parsedResult });
} catch (e) {
this.props.showNotification(
'warning',
`Function call error: ${(e as Error).message}` || 'Invalid input parameters',
5000
);
}
};
private handleFunctionSend = (_: React.FormEvent<HTMLButtonElement>) => {
try {
const data = this.encodeData();
this.props.setDataField({ raw: data, value: Data(data) });
} catch (e) {
this.props.showNotification(
'danger',
`Function send error: ${(e as Error).message}` || 'Invalid input parameters',
5000
);
}
};
private handleFunctionSelect = (selectedFunction: ContractOption) => {
this.setState({
selectedFunction,
outputs: {},
inputs: {}
});
};
private encodeData(): string {
const { selectedFunction, inputs } = this.state;
const parsedInputs = Object.keys(inputs).reduce(
(accu, key) => ({ ...accu, [key]: inputs[key].parsedData }),
{}
);
return selectedFunction!.contract.encodeInput(parsedInputs);
}
private tryParseJSON(input: string) {
try {
return JSON.parse(input);
} catch {
return input;
}
}
private handleInputChange = (ev: React.FormEvent<HTMLInputElement>) => {
const rawValue: string = ev.currentTarget.value;
const isArr = rawValue.startsWith('[') && rawValue.endsWith(']');
const value = {
rawData: rawValue,
parsedData: isArr ? this.tryParseJSON(rawValue) : rawValue
};
this.setState({
inputs: {
...this.state.inputs,
[ev.currentTarget.name]: value
}
});
};
}
export const InteractExplorer = connect(
(state: AppState) => ({
nodeLib: getNodeLib(state),
to: getTo(state),
dataExists: getDataExists(state)
}),
{ showNotification, setDataField }
)(InteractExplorerClass);