diff --git a/embark-ui/src/actions/index.js b/embark-ui/src/actions/index.js index 45ebf9c1..c839e103 100644 --- a/embark-ui/src/actions/index.js +++ b/embark-ui/src/actions/index.js @@ -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]), diff --git a/embark-ui/src/api/index.js b/embark-ui/src/api/index.js index 2de94a6f..3718d061 100644 --- a/embark-ui/src/api/index.js +++ b/embark-ui/src/api/index.js @@ -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'); } diff --git a/embark-ui/src/components/ContractFunctions.js b/embark-ui/src/components/ContractFunctions.js new file mode 100644 index 00000000..e52289f3 --- /dev/null +++ b/embark-ui/src/components/ContractFunctions.js @@ -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 ( + + + + + {this.props.method.name} + + + {this.props.method.inputs.map(input => ( + + this.handleChange(e, input.name)}/> + + ))} + this.handleCall(e)}> + {this.buttonTitle()} + + + + + {this.props.contractFunctions.map(contractFunction => ( + + {contractFunction.inputs.length > 0 && Inputs: {contractFunction.inputs.join(', ')}} + Result: {contractFunction.result} + + ))} + + + + + + ); + } +} + +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 ( + + {contractProfile.methods + .filter((method) => { + return props.onlyConstructor ? method.name === 'constructor' : method.name !== 'constructor'; + }) + .map(method => )} + + ); +}; + +ContractFunctions.propTypes = { + onlyConstructor: PropTypes.bool, + contractProfile: PropTypes.object, + contractFunctions: PropTypes.arrayOf(PropTypes.object), + postContractFunction: PropTypes.func +}; + +ContractFunctions.defaultProps = { + onlyConstructor: false +}; + +export default ContractFunctions; + diff --git a/embark-ui/src/components/ContractLayout.js b/embark-ui/src/components/ContractLayout.js index 71b838a7..b9950471 100644 --- a/embark-ui/src/components/ContractLayout.js +++ b/embark-ui/src/components/ContractLayout.js @@ -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}) => ( + + diff --git a/embark-ui/src/containers/ContractContainer.js b/embark-ui/src/containers/ContractContainer.js index 925135e8..5c8f516f 100644 --- a/embark-ui/src/containers/ContractContainer.js +++ b/embark-ui/src/containers/ContractContainer.js @@ -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)); diff --git a/embark-ui/src/containers/ContractDeploymentContainer.js b/embark-ui/src/containers/ContractDeploymentContainer.js new file mode 100644 index 00000000..b1bf2741 --- /dev/null +++ b/embark-ui/src/containers/ContractDeploymentContainer.js @@ -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 ( + ( + + )} /> + ); + } +} + +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)); diff --git a/embark-ui/src/containers/ContractFunctionsContainer.js b/embark-ui/src/containers/ContractFunctionsContainer.js new file mode 100644 index 00000000..cf273c99 --- /dev/null +++ b/embark-ui/src/containers/ContractFunctionsContainer.js @@ -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 ( + ( + + )} /> + ); + } +} + +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)); diff --git a/embark-ui/src/containers/ContractsContainer.js b/embark-ui/src/containers/ContractsContainer.js index 94b0839f..1d769ede 100644 --- a/embark-ui/src/containers/ContractsContainer.js +++ b/embark-ui/src/containers/ContractsContainer.js @@ -27,7 +27,7 @@ function mapStateToProps(state) { ContractsContainer.propTypes = { contracts: PropTypes.array, - fetchContracts: PropTypes.func, + fetchContracts: PropTypes.func }; export default connect( diff --git a/embark-ui/src/reducers/index.js b/embark-ui/src/reducers/index.js index c6836e7b..c3d7f283 100644 --- a/embark-ui/src/reducers/index.js +++ b/embark-ui/src/reducers/index.js @@ -13,6 +13,8 @@ const entitiesDefaultState = { contracts: [], contractProfiles: [], contractFiles: [], + contractFunctions: [], + contractDeploys: [], contractLogs: [], commands: [], messages: [], diff --git a/embark-ui/src/reducers/selectors.js b/embark-ui/src/reducers/selectors.js index d733e65f..b7ef021a 100644 --- a/embark-ui/src/reducers/selectors.js +++ b/embark-ui/src/reducers/selectors.js @@ -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; } diff --git a/embark-ui/src/sagas/index.js b/embark-ui/src/sagas/index.js index 2518f85e..668767a1 100644 --- a/embark-ui/src/sagas/index.js +++ b/embark-ui/src/sagas/index.js @@ -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), diff --git a/lib/contracts/contracts.js b/lib/contracts/contracts.js index eccf5b64..5f599db5 100644 --- a/lib/contracts/contracts.js +++ b/lib/contracts/contracts.js @@ -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);
Inputs: {contractFunction.inputs.join(', ')}