Merge pull request #36 from status-im/features/backend_tab/functions

Features/backend tab/functions
This commit is contained in:
Iuri Matias 2018-08-21 14:12:08 -04:00 committed by GitHub
commit 6c2489d5e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 365 additions and 14 deletions

View File

@ -111,6 +111,20 @@ export const contractFile = {
failure: (error) => action(CONTRACT_FILE[FAILURE], {error})
};
export const CONTRACT_FUNCTION = createRequestTypes('CONTRACT_FUNCTION');
export const contractFunction = {
post: (contractName, method, inputs) => action(CONTRACT_FUNCTION[REQUEST], {contractName, method, inputs}),
success: (result, payload) => action(CONTRACT_FUNCTION[SUCCESS], {contractFunctions: [{...result, ...payload}]}),
failure: (error) => action(CONTRACT_FUNCTION[FAILURE], {error})
};
export const CONTRACT_DEPLOY = createRequestTypes('CONTRACT_DEPLOY');
export const contractDeploy = {
post: (contractName, method, inputs) => action(CONTRACT_DEPLOY[REQUEST], {contractName, method, inputs}),
success: (result, payload) => action(CONTRACT_DEPLOY[SUCCESS], {contractDeploys: [{...result, ...payload}]}),
failure: (error) => action(CONTRACT_DEPLOY[FAILURE], {error})
};
export const VERSIONS = createRequestTypes('VERSIONS');
export const versions = {
request: () => action(VERSIONS[REQUEST]),

View File

@ -68,6 +68,14 @@ export function fetchContract(payload) {
return get(`/contract/${payload.contractName}`);
}
export function postContractFunction(payload) {
return post(`/contract/${payload.contractName}/function`, payload);
}
export function postContractDeploy(payload) {
return post(`/contract/${payload.contractName}/deploy`, payload);
}
export function fetchVersions() {
return get('/versions');
}

View File

@ -0,0 +1,126 @@
import PropTypes from "prop-types";
import React, {Component} from 'react';
import {
Page,
Grid,
Form,
Button,
List,
Card
} from "tabler-react";
class ContractFunction extends Component {
constructor(props) {
super(props);
this.state = { inputs: {} };
}
buttonTitle() {
const { method } =this.props;
if (method.name === 'constructor') {
return 'Deploy';
}
return (method.mutability === 'view' || method.mutability === 'pure') ? 'Call' : 'Send';
}
inputsAsArray(){
return this.props.method.inputs
.map(input => this.state.inputs[input.name])
.filter(value => value);
}
handleChange(e, name) {
let newInputs = this.state.inputs;
newInputs[name] = e.target.value;
this.setState({ inputs: newInputs});
}
handleCall(e) {
e.preventDefault();
this.props.postContractFunction(this.props.contractProfile.name, this.props.method.name, this.inputsAsArray());
}
callDisabled() {
return this.inputsAsArray().length !== this.props.method.inputs.length;
}
render() {
return (
<Grid.Row>
<Grid.Col>
<Card>
<Card.Header>
<Card.Title>{this.props.method.name}</Card.Title>
</Card.Header>
<Card.Body>
{this.props.method.inputs.map(input => (
<Form.Group key={input.name} label={input.name}>
<Form.Input placeholder={input.type} onChange={(e) => this.handleChange(e, input.name)}/>
</Form.Group>
))}
<Button color="primary" disabled={this.callDisabled()} onClick={(e) => this.handleCall(e)}>
{this.buttonTitle()}
</Button>
</Card.Body>
<Card.Footer>
<List>
{this.props.contractFunctions.map(contractFunction => (
<List.Item key={contractFunction.result}>
{contractFunction.inputs.length > 0 && <p>Inputs: {contractFunction.inputs.join(', ')}</p>}
<strong>Result: {contractFunction.result}</strong>
</List.Item>
))}
</List>
</Card.Footer>
</Card>
</Grid.Col>
</Grid.Row>
);
}
}
ContractFunction.propTypes = {
contractProfile: PropTypes.object,
method: PropTypes.object,
contractFunctions: PropTypes.arrayOf(PropTypes.object),
postContractFunction: PropTypes.func
};
const filterContractFunctions = (contractFunctions, contractName, method) => {
return contractFunctions.filter((contractFunction) => (
contractFunction.contractName === contractName && contractFunction.method === method
));
};
const ContractFunctions = (props) => {
const {contractProfile} = props;
return (
<Page.Content title={contractProfile.name + ' Functions'}>
{contractProfile.methods
.filter((method) => {
return props.onlyConstructor ? method.name === 'constructor' : method.name !== 'constructor';
})
.map(method => <ContractFunction key={method.name}
method={method}
contractFunctions={filterContractFunctions(props.contractFunctions, contractProfile.name, method.name)}
contractProfile={contractProfile}
postContractFunction={props.postContractFunction} />)}
</Page.Content>
);
};
ContractFunctions.propTypes = {
onlyConstructor: PropTypes.bool,
contractProfile: PropTypes.object,
contractFunctions: PropTypes.arrayOf(PropTypes.object),
postContractFunction: PropTypes.func
};
ContractFunctions.defaultProps = {
onlyConstructor: false
};
export default ContractFunctions;

View File

@ -9,6 +9,8 @@ import {
import ContractContainer from '../containers/ContractContainer';
import ContractLoggerContainer from '../containers/ContractLoggerContainer';
import ContractFunctionsContainer from '../containers/ContractFunctionsContainer';
import ContractDeploymentContainer from '../containers/ContractDeploymentContainer';
import ContractProfileContainer from '../containers/ContractProfileContainer';
import ContractSourceContainer from '../containers/ContractSourceContainer';
@ -72,6 +74,8 @@ const ContractLayout = ({match}) => (
<Grid.Col md={9}>
<Switch>
<Route exact path="/embark/contracts/:contractName/overview" component={ContractContainer} />
<Route exact path="/embark/contracts/:contractName/deployment" component={ContractDeploymentContainer} />
<Route exact path="/embark/contracts/:contractName/functions" component={ContractFunctionsContainer} />
<Route exact path="/embark/contracts/:contractName/source" component={ContractSourceContainer} />
<Route exact path="/embark/contracts/:contractName/profiler" component={ContractProfileContainer} />
<Route exact path="/embark/contracts/:contractName/logger" component={ContractLoggerContainer} />

View File

@ -3,7 +3,6 @@ import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {withRouter} from 'react-router-dom';
import {contract as contractAction} from '../actions';
import Contract from '../components/Contract';
import DataWrapper from "../components/DataWrapper";
import {getContract} from "../reducers/selectors";
@ -33,8 +32,5 @@ ContractContainer.propTypes = {
};
export default withRouter(connect(
mapStateToProps,
{
fetchContract: contractAction.request
}
mapStateToProps
)(ContractContainer));

View File

@ -0,0 +1,54 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {withRouter} from 'react-router-dom';
import {contractProfile as contractProfileAction, contractDeploy as contractDeployAction} from '../actions';
import ContractFunctions from '../components/ContractFunctions';
import DataWrapper from "../components/DataWrapper";
import {getContractProfile, getContractDeploys} from "../reducers/selectors";
class ContractDeploymentContainer extends Component {
componentDidMount() {
this.props.fetchContractProfile(this.props.match.params.contractName);
}
render() {
return (
<DataWrapper shouldRender={this.props.contractProfile !== undefined }
{...this.props}
render={({contractProfile, contractDeploys, postContractDeploy}) => (
<ContractFunctions contractProfile={contractProfile}
contractFunctions={contractDeploys}
onlyConstructor
postContractFunction={postContractDeploy}/>
)} />
);
}
}
function mapStateToProps(state, props) {
return {
contractProfile: getContractProfile(state, props.match.params.contractName),
contractDeploys: getContractDeploys(state, props.match.params.contractName),
error: state.errorMessage,
loading: state.loading
};
}
ContractDeploymentContainer.propTypes = {
match: PropTypes.object,
contractProfile: PropTypes.object,
contractFunctions: PropTypes.arrayOf(PropTypes.object),
postContractDeploy: PropTypes.func,
fetchContractProfile: PropTypes.func,
error: PropTypes.string
};
export default withRouter(connect(
mapStateToProps,
{
fetchContractProfile: contractProfileAction.request,
postContractDeploy: contractDeployAction.post
}
)(ContractDeploymentContainer));

View File

@ -0,0 +1,53 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {withRouter} from 'react-router-dom';
import {contractProfile as contractProfileAction, contractFunction as contractFunctionAction} from '../actions';
import ContractFunctions from '../components/ContractFunctions';
import DataWrapper from "../components/DataWrapper";
import {getContractProfile, getContractFunctions} from "../reducers/selectors";
class ContractFunctionsContainer extends Component {
componentDidMount() {
this.props.fetchContractProfile(this.props.match.params.contractName);
}
render() {
return (
<DataWrapper shouldRender={this.props.contractProfile !== undefined }
{...this.props}
render={({contractProfile, contractFunctions, postContractFunction}) => (
<ContractFunctions contractProfile={contractProfile}
contractFunctions={contractFunctions}
postContractFunction={postContractFunction}/>
)} />
);
}
}
function mapStateToProps(state, props) {
return {
contractProfile: getContractProfile(state, props.match.params.contractName),
contractFunctions: getContractFunctions(state, props.match.params.contractName),
error: state.errorMessage,
loading: state.loading
};
}
ContractFunctionsContainer.propTypes = {
match: PropTypes.object,
contractProfile: PropTypes.object,
contractFunctions: PropTypes.arrayOf(PropTypes.object),
postContractFunction: PropTypes.func,
fetchContractProfile: PropTypes.func,
error: PropTypes.string
};
export default withRouter(connect(
mapStateToProps,
{
fetchContractProfile: contractProfileAction.request,
postContractFunction: contractFunctionAction.post
}
)(ContractFunctionsContainer));

View File

@ -27,7 +27,7 @@ function mapStateToProps(state) {
ContractsContainer.propTypes = {
contracts: PropTypes.array,
fetchContracts: PropTypes.func,
fetchContracts: PropTypes.func
};
export default connect(

View File

@ -13,6 +13,8 @@ const entitiesDefaultState = {
contracts: [],
contractProfiles: [],
contractFiles: [],
contractFunctions: [],
contractDeploys: [],
contractLogs: [],
commands: [],
messages: [],

View File

@ -66,6 +66,14 @@ export function getContractFile(state, filename) {
return state.entities.contractFiles.find((contractFile => contractFile.filename === filename));
}
export function getContractFunctions(state, contractName) {
return state.entities.contractFunctions.filter((contractFunction => contractFunction.contractName === contractName));
}
export function getContractDeploys(state, contractName) {
return state.entities.contractDeploys.filter((contractDeploy => contractDeploy.contractName === contractName));
}
export function getVersions(state) {
return state.entities.versions;
}

View File

@ -5,7 +5,7 @@ import {all, call, fork, put, takeEvery, take} from 'redux-saga/effects';
const {account, accounts, block, blocks, transaction, transactions, processes, commands, processLogs,
contracts, contract, contractProfile, messageSend, versions, plugins, messageListen, fiddle,
ensRecord, ensRecords, contractLogs, contractFile} = actions;
ensRecord, ensRecords, contractLogs, contractFile, contractFunction, contractDeploy} = actions;
function *doRequest(entity, apiFn, payload) {
const {response, error} = yield call(apiFn, payload);
@ -32,6 +32,8 @@ export const fetchContracts = doRequest.bind(null, contracts, api.fetchContracts
export const fetchContract = doRequest.bind(null, contract, api.fetchContract);
export const fetchContractProfile = doRequest.bind(null, contractProfile, api.fetchContractProfile);
export const fetchContractFile = doRequest.bind(null, contractFile, api.fetchContractFile);
export const postContractFunction = doRequest.bind(null, contractFunction, api.postContractFunction);
export const postContractDeploy = doRequest.bind(null, contractDeploy, api.postContractDeploy);
export const fetchFiddle = doRequest.bind(null, fiddle, api.fetchFiddle);
export const sendMessage = doRequest.bind(null, messageSend, api.sendMessage);
export const fetchEnsRecord = doRequest.bind(null, ensRecord, api.fetchEnsRecord);
@ -93,6 +95,14 @@ export function *watchFetchContractFile() {
yield takeEvery(actions.CONTRACT_FILE[actions.REQUEST], fetchContractFile);
}
export function *watchPostContractFunction() {
yield takeEvery(actions.CONTRACT_FUNCTION[actions.REQUEST], postContractFunction);
}
export function *watchPostContractDeploy() {
yield takeEvery(actions.CONTRACT_DEPLOY[actions.REQUEST], postContractDeploy);
}
export function *watchFetchVersions() {
yield takeEvery(actions.VERSIONS[actions.REQUEST], fetchVersions);
}
@ -200,6 +210,8 @@ export default function *root() {
fork(watchFetchContracts),
fork(watchFetchContractProfile),
fork(watchFetchContractFile),
fork(watchPostContractFunction),
fork(watchPostContractDeploy),
fork(watchListenToMessages),
fork(watchSendMessage),
fork(watchFetchContract),

View File

@ -1,3 +1,4 @@
/*global web3*/
let toposort = require('toposort');
let async = require('async');
const cloneDeep = require('clone-deep');
@ -83,6 +84,73 @@ class ContractsManager {
}
);
plugin.registerAPICall(
'post',
'/embark-api/contract/:contractName/function',
(req, res) => {
async.parallel({
contract: (callback) => {
self.events.request('contracts:contract', req.body.contractName, (contract) => callback(null, contract));
},
account: (callback) => {
self.events.request("blockchain:defaultAccount:get", (account) => callback(null, account));
}
}, (error, result) => {
if (error) {
return res.send({error: error.message});
}
const {account, contract} = result;
const abi = contract.abiDefinition.find(definition => definition.name === req.body.method);
const funcCall = (abi.constant === true || abi.stateMutability === 'view' || abi.stateMutability === 'pure') ? 'call' : 'send';
self.events.request("blockchain:contract:create", {abi: contract.abiDefinition, address: contract.deployedAddress}, (contractObj) => {
try {
contractObj.methods[req.body.method].apply(this, req.body.inputs)[funcCall]({from: account}, (error, result) => {
if (error) {
return res.send({result: error.message});
}
res.send({result});
});
} catch (e) {
res.send({result: e.message});
}
});
});
}
);
plugin.registerAPICall(
'post',
'/embark-api/contract/:contractName/deploy',
(req, res) => {
async.parallel({
contract: (callback) => {
self.events.request('contracts:contract', req.body.contractName, (contract) => callback(null, contract));
},
account: (callback) => {
self.events.request("blockchain:defaultAccount:get", (account) => callback(null, account));
}
}, (error, result) => {
if (error) {
return res.send({error: error.message});
}
const {account, contract} = result;
self.events.request("blockchain:contract:create", {abi: contract.abiDefinition}, async (contractObj) => {
try {
const params = {data: `0x${contract.code}`, arguments: req.body.inputs};
let gas = await contractObj.deploy(params).estimateGas();
let newContract = await contractObj.deploy(params).send({from: account, gas});
res.send({result: newContract._address});
} catch (e) {
res.send({result: e.message});
}
});
});
}
);
plugin.registerAPICall(
'get',
'/embark-api/contracts',
@ -203,7 +271,10 @@ class ContractsManager {
}
if (parentContract === undefined) {
self.logger.error(__("{{className}}: couldn't find instanceOf contract {{parentContractName}}", {className: className, parentContractName: parentContractName}));
self.logger.error(__("{{className}}: couldn't find instanceOf contract {{parentContractName}}", {
className: className,
parentContractName: parentContractName
}));
let suggestion = utils.proposeAlternative(parentContractName, dictionary, [className, parentContractName]);
if (suggestion) {
self.logger.warn(__('did you mean "%s"?', suggestion));
@ -216,7 +287,10 @@ class ContractsManager {
}
if (contract.code !== undefined) {
self.logger.error(__("{{className}} has code associated to it but it's configured as an instanceOf {{parentContractName}}", {className: className, parentContractName: parentContractName}));
self.logger.error(__("{{className}} has code associated to it but it's configured as an instanceOf {{parentContractName}}", {
className: className,
parentContractName: parentContractName
}));
}
contract.code = parentContract.code;
@ -261,8 +335,8 @@ class ContractsManager {
// look in code for dependencies
let libMatches = (contract.code.match(/:(.*?)(?=_)/g) || []);
for (let match of libMatches) {
self.contractDependencies[className] = self.contractDependencies[className] || [];
self.contractDependencies[className].push(match.substr(1));
self.contractDependencies[className] = self.contractDependencies[className] || [];
self.contractDependencies[className].push(match.substr(1));
}
// look in arguments for dependencies
@ -358,7 +432,7 @@ class ContractsManager {
self.compileError = true;
self.events.emit("status", __("Compile/Build error"));
self.logger.error(__("Error Compiling/Building contracts: ") + err);
}else{
} else {
self.compileError = false;
}
self.logger.trace("finished".underline);
@ -398,8 +472,8 @@ class ContractsManager {
let orderedDependencies;
try {
orderedDependencies = toposort(converted_dependencies.filter((x) => x[0] !== x[1])).reverse();
} catch(e) {
orderedDependencies = toposort(converted_dependencies.filter((x) => x[0] !== x[1])).reverse();
} catch (e) {
this.logger.error((__("Error: ") + e.message).red);
this.logger.error(__("there are two or more contracts that depend on each other in a cyclic manner").bold.red);
this.logger.error(__("Embark couldn't determine which one to deploy first").red);