264 lines
7.7 KiB
TypeScript
264 lines
7.7 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={translate('SELECT_A_THING', { $thing: '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);
|