File Editor

This commit is contained in:
Anthony Laibe 2018-08-30 13:13:37 +01:00 committed by Pascal Precht
parent 5b16ce0691
commit a08690ef43
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
28 changed files with 3034 additions and 3139 deletions

File diff suppressed because it is too large Load Diff

View File

@ -125,6 +125,13 @@ export const contractDeploy = {
failure: (error) => action(CONTRACT_DEPLOY[FAILURE], {error})
};
export const CONTRACT_COMPILE = createRequestTypes('CONTRACT_COMPILE');
export const contractCompile = {
post: (code, name) => action(CONTRACT_COMPILE[REQUEST], {code, name}),
success: (result, payload) => action(CONTRACT_COMPILE[SUCCESS], {contractCompiles: [{...result, ...payload}]}),
failure: (error) => action(CONTRACT_COMPILE[FAILURE], {error})
};
export const VERSIONS = createRequestTypes('VERSIONS');
export const versions = {
request: () => action(VERSIONS[REQUEST]),
@ -168,31 +175,6 @@ export const ensRecords = {
failure: (error) => action(ENS_RECORDS[FAILURE], {error})
};
export const FIDDLE = createRequestTypes('FIDDLE');
export const fiddle = {
post: (codeToCompile, timestamp) => action(FIDDLE[REQUEST], {codeToCompile, timestamp}),
success: (fiddle, payload) => {
return action(FIDDLE[SUCCESS], {fiddles: [{...fiddle, ...payload}]});
},
failure: (error) => action(FIDDLE[FAILURE], {error})
};
export const FIDDLE_DEPLOY = createRequestTypes('FIDDLE_DEPLOY');
export const fiddleDeploy = {
post: (compiledCode) => action(FIDDLE_DEPLOY[REQUEST], {compiledCode}),
success: (response) => {
return action(FIDDLE_DEPLOY[SUCCESS], {fiddleDeploys: response.result});
},
failure: (error) => action(FIDDLE_DEPLOY[FAILURE], {error})
};
export const FIDDLE_FILE = createRequestTypes('FIDDLE_FILE');
export const fiddleFile = {
request: () => action(FIDDLE_FILE[REQUEST]),
success: (codeToCompile) => action(FIDDLE_FILE[SUCCESS], {codeToCompile}),
failure: (error) => action(FIDDLE_FILE[FAILURE], {error})
};
export const FILES = createRequestTypes('FILES');
export const files = {
request: () => action(FILES[REQUEST]),
@ -200,6 +182,43 @@ export const files = {
failure: (error) => action(FILES[FAILURE], {error})
};
export const FILE = createRequestTypes('FILE');
export const file = {
request: (file) => action(FILE[REQUEST], file),
success: (file) => action(FILE[SUCCESS], file),
failure: (error) => action(FILE[FAILURE], {error})
};
export const SAVE_FILE = createRequestTypes('SAVE_FILE');
export const saveFile = {
request: ({name, path, content}) => {
return action(SAVE_FILE[REQUEST], {name, path, content});
},
success: () => action(SAVE_FILE[SUCCESS]),
failure: (error) => action(SAVE_FILE[FAILURE], {error})
};
export const REMOVE_FILE = createRequestTypes('REMOVE_FILE');
export const removeFile = {
request: ({name, path, content}) => action(REMOVE_FILE[REQUEST], {name, path, content}),
success: () => action(REMOVE_FILE[SUCCESS]),
failure: (error) => action(REMOVE_FILE[FAILURE], {error})
};
export const CURRENT_FILE = createRequestTypes('CURRENT_FILE');
export const currentFile = {
request: () => action(CURRENT_FILE[REQUEST]),
success: (file) => action(CURRENT_FILE[SUCCESS], {currentFiles: [file]}),
failure: () => action(CURRENT_FILE[FAILURE])
};
export const SAVE_CURRENT_FILE = createRequestTypes('SAVE_CURRENT_FILE');
export const saveCurrentFile = {
request: (file) => action(SAVE_CURRENT_FILE[REQUEST], file),
success: (file) => action(SAVE_CURRENT_FILE[SUCCESS], {currentFiles: [file]}),
failure: () => action(SAVE_CURRENT_FILE[FAILURE])
};
export const GAS_ORACLE = createRequestTypes('GAS_ORACLE');
export const gasOracle = {
request: () => action(GAS_ORACLE[REQUEST]),

View File

@ -1,27 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Badge} from 'tabler-react';
const CompilerError = ({ index, onClick, errorType, row, errorMessage}) => (
<a
href="#editor"
className="list-group-item list-group-item-action"
onClick={onClick}
key={index}
>
<Badge color={errorType === "error" ? "danger" : errorType} className="mr-1" key={index}>
Line {row}
</Badge>
{errorMessage}
</a>
);
CompilerError.propTypes = {
index: PropTypes.number,
onClick: PropTypes.func,
errorType: PropTypes.string,
row: PropTypes.string,
errorMessage: PropTypes.string
};
export default CompilerError;

View File

@ -1,53 +0,0 @@
import React from 'react';
import AceEditor from 'react-ace';
import 'brace/mode/javascript';
import 'brace/theme/tomorrow_night_blue';
import 'ace-mode-solidity/build/remix-ide/mode-solidity';
import PropTypes from 'prop-types';
class Fiddle extends React.Component {
constructor(props) {
super(props);
this.ace = null;
}
render() {
const {onCodeChange, value, errors, warnings} = this.props;
const annotations = errors && errors.map((error) => { return error.annotation; }).concat(warnings.map(warning => { return warning.annotation; }));
return (
<React.Fragment>
<AceEditor
mode="solidity"
theme="tomorrow_night_blue"
name="fiddle"
height="60em"
width="100%"
onChange={onCodeChange}
value={value}
showGutter={true}
annotations={annotations}
ref={(ace) => { this.ace = ace; }}
setOptions={{
useWorker: false
}}
editorProps={{
$blockScrolling: Infinity,
enableLiveAutocompletion:true,
highlightSelectedWord: true
}}
/>
</React.Fragment>
);
}
}
Fiddle.propTypes = {
onCodeChange: PropTypes.func,
value: PropTypes.string,
errors: PropTypes.array,
warnings: PropTypes.array
};
export default Fiddle;

View File

@ -1,19 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'tabler-react';
const FiddleDeployButton = ({ onDeployClick }) => (
<Button
color="dark"
size="sm"
icon="upload-cloud"
onClick={onDeployClick}>
Deploy
</Button>
);
FiddleDeployButton.propTypes = {
onDeployClick: PropTypes.func.isRequired
};
export default FiddleDeployButton;

View File

@ -1,25 +1,26 @@
import React from 'react';
import {Route, Switch} from 'react-router-dom';
import {
Page,
Grid
} from "tabler-react";
import FiddleContainer from '../containers/FiddleContainer';
import TextEditorContainer from '../containers/TextEditorContainer';
import FileExplorerContainer from '../containers/FileExplorerContainer';
const ExplorerLayout = () => (
<Grid.Row>
class FiddleLayout extends React.Component {
render() {
return (
<Grid.Row className="my-5">
<Grid.Col md={3}>
<Page.Title className="my-5">Fiddle</Page.Title>
<Page.Title>Fiddle</Page.Title>
<FileExplorerContainer />
</Grid.Col>
<Grid.Col md={9}>
<Switch>
<Route exact path="/embark/fiddle/" component={FiddleContainer} />
</Switch>
<TextEditorContainer />
</Grid.Col>
</Grid.Row>
);
}
}
export default ExplorerLayout;
export default FiddleLayout;

View File

@ -1,25 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const FiddleResults = ({warningsCard, errorsCard, fatalFiddleCard, fatalFiddleDeployCard, deployedContractsCard, fatalErrorCard, forwardedRef}) => (
<div ref={forwardedRef}>
{fatalErrorCard}
{fatalFiddleCard}
{fatalFiddleDeployCard}
{deployedContractsCard}
{errorsCard}
{warningsCard}
</div>
);
FiddleResults.propTypes = {
errorsCard: PropTypes.node,
warningsCard: PropTypes.node,
fatalFiddleCard: PropTypes.node,
fatalFiddleDeployCard: PropTypes.node,
deployedContractsCard: PropTypes.node,
fatalErrorCard: PropTypes.node,
forwardedRef: PropTypes.any
};
export default FiddleResults;

View File

@ -1,69 +0,0 @@
import React, {Component} from 'react';
import {Badge, Icon, Loader} from 'tabler-react';
import PropTypes from 'prop-types';
import FiddleDeployButton from './FiddleDeployButton';
import classNames from 'classnames';
class FiddleResultsSummary extends Component {
_renderFatal(fatalType, title) {
return <a className="badge-link" href={`#fatal-${fatalType}`} onClick={(e) => this.props.onFatalClick(e)}><Badge color="danger"><Icon name="slash" className="mr-1" />{title}</Badge></a>;
}
_renderError(errorType, numErrors) {
const color = errorType === 'error' ? 'danger' : 'warning';
const clickAction = errorType === 'error' ? this.props.onWarningsClick : this.props.onErrorsClick;
return <a className="badge-link" href={`#${errorType}`} onClick={(e) => clickAction(e)}><Badge color={color}>{numErrors} {errorType}{numErrors > 1 ? "s" : ""}</Badge></a>;
}
render() {
const {numWarnings, numErrors, isLoading, loadingMessage, isVisible, showDeploy, showFatalFiddle, showFatalFiddleDeploy, showFatalError} = this.props;
const classes = classNames("compilation-summary", {
'visible': isVisible
});
return (
<div className={classes}>
{isLoading &&
<Loader className="mr-1">
<span className="loader-text">{loadingMessage}</span>
</Loader>}
{showFatalError && this._renderFatal("error", "Error")}
{showFatalFiddle && this._renderFatal("compile", "Compilation")}
{showFatalFiddleDeploy && this._renderFatal("deploy", "Deployment")}
{numErrors > 0 && this._renderError("error", numErrors)}
{numWarnings > 0 && this._renderError("warning", numWarnings)}
{showDeploy &&
<React.Fragment key="success">
<Badge className="badge-link" color="success">Compiled</Badge>
<FiddleDeployButton onDeployClick={(e) => this.props.onDeployClick(e)} />
</React.Fragment>
}
</div>
);
}
}
FiddleResultsSummary.propTypes = {
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
isVisible: PropTypes.bool,
showDeploy: PropTypes.bool,
showFatalError: PropTypes.bool,
showFatalFiddle: PropTypes.bool,
showFatalFiddleDeploy: PropTypes.bool,
onDeployClick: PropTypes.func,
numErrors: PropTypes.number,
numWarnings: PropTypes.number,
onWarningsClick: PropTypes.func.isRequired,
onErrorsClick: PropTypes.func.isRequired,
onFatalClick: PropTypes.func.isRequired
};
export default FiddleResultsSummary;

View File

@ -29,26 +29,35 @@ class FileExplorer extends React.Component {
super(props);
this.state = {};
}
onToggle(node, toggled){
let oldNode = this.state.cursor;
if(oldNode) {
oldNode.active = false;
}
node.active = true;
if(node.children) {
node.toggled = toggled;
} else {
this.props.fetchFile(node);
}
this.setState({ cursor: node });
}
render(){
return (
<Treebeard
data={this.props.files}
decorators={decorators}
onToggle={(node, toggled) => this.onToggle(node, toggled)}
onToggle={this.onToggle.bind(this)}
/>
);
}
}
FileExplorer.propTypes = {
files: PropTypes.array
files: PropTypes.array,
fetchFile: PropTypes.func
};
export default FileExplorer;

View File

@ -1,80 +0,0 @@
/* eslint {jsx-a11y/anchor-has-content:"off"} */
import React from 'react';
import PropTypes from 'prop-types';
import {Card, Icon, Dimmer} from 'tabler-react';
import classNames from 'classnames';
class LoadingCardWithIcon extends React.Component {
constructor(props) {
super(props);
this.state = {
errorsCollapsed: false,
warningsCollapsed: false,
errorsFullscreen: false,
warningsFullscreen: false
};
}
_onToggle(e, type) {
const className = e.currentTarget.parentElement.className.replace('card-options', '').replace(' ', '');
const updatedState = {};
updatedState[className + type] = !(this.state[className + type]);
this.setState(updatedState);
}
render() {
const {
color,
className,
iconName,
headerTitle,
isLoading,
body,
showCardOptions = true,
cardOptionsClassName} = this.props;
const isFullscreen = Boolean(this.state[cardOptionsClassName + 'Fullscreen']);
const classes = classNames(className, {
'card-fullscreen': showCardOptions && Boolean(this.state[cardOptionsClassName + 'Fullscreen']),
'card-collapsed': showCardOptions && Boolean(this.state[cardOptionsClassName + 'Collapsed']) && !isFullscreen
});
return (
<Card
statusColor={color}
statusSide="true"
className={classes}
isCollapsible={showCardOptions}
isFullscreenable={showCardOptions}>
<Card.Header>
<Card.Title color={color}>{iconName && <Icon name={iconName} className="mr-1" />}{headerTitle}</Card.Title>
{showCardOptions &&
<Card.Options className={cardOptionsClassName}>
<Card.OptionsItem key="0" type="collapse" icon="chevron-up" onClick={(e) => this._onToggle(e, 'Collapsed')} />
<Card.OptionsItem key="1" type="fullscreen" icon="maximize" onClick={(e) => this._onToggle(e, 'Fullscreen')} />
</Card.Options>
}
</Card.Header>
<Card.Body>
<Dimmer active={isLoading ? "active" : ""} loader>
{body}
</Dimmer>
</Card.Body>
</Card>
);
}
}
LoadingCardWithIcon.propTypes = {
color: PropTypes.string.isRequired,
className: PropTypes.string.isRequired,
iconName: PropTypes.string,
headerTitle: PropTypes.any,
isLoading: PropTypes.bool.isRequired,
body: PropTypes.node,
showCardOptions: PropTypes.bool,
onOptionToggle: PropTypes.func,
cardOptionsClassName: PropTypes.string
};
export default LoadingCardWithIcon;

View File

@ -0,0 +1,61 @@
import React from 'react';
import AceEditor from 'react-ace';
import 'brace/mode/javascript';
import 'brace/theme/tomorrow_night_blue';
import 'ace-mode-solidity/build/remix-ide/mode-solidity';
import PropTypes from 'prop-types';
class TextEditor extends React.Component {
extractRowCol(errorMessage) {
const errorSplit = errorMessage.split(':');
if (errorSplit.length >= 3) {
return {row: errorSplit[1], col: errorSplit[2]};
}
return {row: 0, col: 0};
}
annotations() {
const {errors, warnings} = this.props.contractCompile;
return [].concat(errors).concat(warnings).filter((e) => e).map((e) => {
const rowCol = this.extractRowCol(e.formattedMessage);
return Object.assign({}, {
row: rowCol.row - 1,
column: rowCol.col - 1,
text: e.formattedMessage,
type: e.severity
});
});
}
render() {
return (
<AceEditor
mode="solidity"
theme="tomorrow_night_blue"
name="fiddle"
height="60em"
width="100%"
onChange={this.props.onFileContentChange}
value={this.props.value}
showGutter={true}
annotations={this.annotations()}
setOptions={{
useWorker: false
}}
editorProps={{
$blockScrolling: Infinity,
enableLiveAutocompletion:true,
highlightSelectedWord: true
}}
/>
);
}
}
TextEditor.propTypes = {
onFileContentChange: PropTypes.func,
value: PropTypes.string,
contractCompile: PropTypes.object
};
export default TextEditor;

View File

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import {Card, Icon, Button} from 'tabler-react';
const TextEditorContractDeploy = (props) => (
<Card statusColor="success"
statusSide
className="success-card">
<Card.Header>
<Card.Title color="success">
<Icon name="check" className="mr-1" />
Deploy Contract
</Card.Title>
</Card.Header>
<Card.Body>
<Button to={`/embark/contracts/${Object.keys(props.result)[0]}/deployment`}
RootComponent={NavLink}>
Deploy my contract(s)
</Button>
</Card.Body>
</Card>
);
TextEditorContractDeploy.propTypes = {
result: PropTypes.object
};
export default TextEditorContractDeploy;

View File

@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Card, Icon, List} from 'tabler-react';
const TextEditorContractErrors = (props) => (
<Card statusColor="danger"
statusSide
className="errors-card">
<Card.Header>
<Card.Title color="danger">
<Icon name="alert-circle" className="mr-1" />
Failed to compile
</Card.Title>
</Card.Header>
<Card.Body>
<List.Group>
{props.errors.map((error, index) => <List.GroupItem key={index}>{error.formattedMessage}</List.GroupItem>)}
</List.Group>
</Card.Body>
</Card>
);
TextEditorContractErrors.propTypes = {
errors: PropTypes.array
};
export default TextEditorContractErrors;

View File

@ -0,0 +1,31 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {Grid, Badge, Icon} from 'tabler-react';
import TextEditorToolbar from './TextEditorToolbar';
class TextEditorContractToolbar extends Component {
render(){
return (
<React.Fragment>
<TextEditorToolbar {...this.props} />
<Grid.Col md={6} className="text-right">
{this.props.compilingContract &&
<Badge color="warning"><Icon name="slash" className="mr-1" />compiling</Badge>}
{!this.props.compilingContract && this.props.contractCompile.result &&
<Badge color="success"><Icon name="check" className="mr-1" />compiled</Badge>}
</Grid.Col>
</React.Fragment>
);
}
}
TextEditorContractToolbar.propTypes = {
currentFile: PropTypes.object,
contractCompile: PropTypes.object,
compilingContract: PropTypes.bool,
deploy: PropTypes.func,
save: PropTypes.func,
remove: PropTypes.func
};
export default TextEditorContractToolbar;

View File

@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Card, Icon, List} from 'tabler-react';
const TextEditorContractWarnings = (props) => (
<Card statusColor="warning"
statusSide
className="warnings-card">
<Card.Header>
<Card.Title color="warning">
<Icon name="alert-triangle" className="mr-1" />
Warning during compilation
</Card.Title>
</Card.Header>
<Card.Body>
<List.Group>
{props.warnings.map((warning, index) => <List.GroupItem key={index}>{warning.formattedMessage}</List.GroupItem>)}
</List.Group>
</Card.Body>
</Card>
);
TextEditorContractWarnings.propTypes = {
warnings: PropTypes.array
};
export default TextEditorContractWarnings;

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Grid, Button} from 'tabler-react';
const TextEditorToolbar = (props) => (
<Grid.Col md={6}>
<strong>{props.currentFile.name}</strong>
<span className="mx-2">|</span>
<Button color="green" size="sm" icon="save" onClick={props.save}>Save</Button>
<span className="mx-2">|</span>
<Button color="red" size="sm" icon="delete" onClick={props.remove}>Delete</Button>
</Grid.Col>
);
TextEditorToolbar.propTypes = {
currentFile: PropTypes.object,
save: PropTypes.func,
remove: PropTypes.func
};
export default TextEditorToolbar;

View File

@ -4,21 +4,21 @@ import PropTypes from 'prop-types';
import {withRouter} from 'react-router-dom';
import {Page} from "tabler-react";
import {contractFile as contractFileAction} from '../actions';
import {file as FileAction} from '../actions';
import DataWrapper from "../components/DataWrapper";
import Fiddle from "../components/Fiddle";
import {getContract, getContractFile} from "../reducers/selectors";
import TextEditor from "../components/TextEditor";
import {getContract, getCurrentFile} from "../reducers/selectors";
class ContractSourceContainer extends Component {
componentDidMount() {
this.props.fetchContractFile(this.props.contract.filename);
this.props.fetchFile({path: this.props.contract.path});
}
render() {
return (
<Page.Content title={`${this.props.contract.className} Source`}>
<DataWrapper shouldRender={this.props.contractFile !== undefined } {...this.props} render={({contractFile}) => (
<Fiddle value={contractFile.source} />
<DataWrapper shouldRender={this.props.file !== undefined } {...this.props} render={({file}) => (
<TextEditor value={file.content} contractCompile={{}} />
)} />
</Page.Content>
);
@ -27,11 +27,11 @@ class ContractSourceContainer extends Component {
function mapStateToProps(state, props) {
const contract = getContract(state, props.match.params.contractName);
const contractFile = getContractFile(state, contract.filename);
const file = getCurrentFile(state);
return {
contract,
contractFile,
file,
error: state.errorMessage,
loading: state.loading
};
@ -40,14 +40,14 @@ function mapStateToProps(state, props) {
ContractSourceContainer.propTypes = {
match: PropTypes.object,
contract: PropTypes.object,
contractFile: PropTypes.object,
fetchContractFile: PropTypes.func,
file: PropTypes.object,
fetchFile: PropTypes.func,
error: PropTypes.string
};
export default withRouter(connect(
mapStateToProps,
{
fetchContractFile: contractFileAction.request
fetchFile: FileAction.request
}
)(ContractSourceContainer));

View File

@ -1,261 +0,0 @@
/* eslint multiline-ternary: "off" */
/* eslint operator-linebreak: "off" */
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {
fiddle as fiddleAction,
fiddleDeploy as fiddleDeployAction,
fiddleFile as fiddleFileAction
} from '../actions';
import Fiddle from '../components/Fiddle';
import FiddleResults from '../components/FiddleResults';
import FiddleResultsSummary from '../components/FiddleResultsSummary';
import scrollToComponent from 'react-scroll-to-component';
import {getFiddle, getFiddleDeploy} from "../reducers/selectors";
import CompilerError from "../components/CompilerError";
import {List, Badge, Button} from 'tabler-react';
import {NavLink} from 'react-router-dom';
import LoadingCardWithIcon from '../components/LoadingCardWithIcon';
import {hashCode} from '../utils/utils';
class FiddleContainer extends Component {
constructor(props) {
super(props);
this.state = {
value: undefined,
loadingMessage: 'Loading...',
readOnly: true
};
this.compileTimeout = null;
this.ace = null;
this.editor = null;
this.warningsCardRef = null;
this.errorsCardRef = null;
this.fatalCardRef = null;
this.deployedCardRef = null;
this.fiddleResultsRef = React.createRef();
}
componentDidMount() {
this.setState({loadingMessage: 'Loading saved state...'});
this.props.fetchLastFiddle();
}
componentDidUpdate(prevProps) {
const {lastFiddle} = this.props;
if (this.state.value === '' && prevProps.lastFiddle === lastFiddle) return;
if ((!this.state.value && lastFiddle && !lastFiddle.error) && this.state.value !== lastFiddle) {
this._onCodeChange(lastFiddle, true);
}
}
_getRowCol(errorMessage) {
const errorSplit = errorMessage.split(':');
if (errorSplit.length >= 3) {
return {row: errorSplit[1], col: errorSplit[2]};
}
return {row: 0, col: 0};
}
_onCodeChange(newValue, immediate = false) {
this.setState({readOnly: false, value: newValue});
if (this.compileTimeout) clearTimeout(this.compileTimeout);
this.compileTimeout = setTimeout(() => {
this.setState({loadingMessage: 'Compiling...'});
this.props.postFiddle(newValue, Date.now());
}, immediate ? 0 : 1000);
}
_onErrorClick(e, annotation) {
e.preventDefault();
this.editor.gotoLine(annotation.row + 1);
scrollToComponent(this.ace);
}
_onErrorSummaryClick(e, refName) {
scrollToComponent(this[refName]);
}
_onDeployClick(_e) {
this.setState({loadingMessage: 'Deploying...'});
this.props.postFiddleDeploy(this.props.fiddle.compilationResult);
scrollToComponent(this.deployedCardRef || this.fiddleResultsRef.current); // deployedCardRef null on first Deploy click
}
_renderErrors(errors, errorType) {
return errors.reduce(
(errors, error, index) => {
if (error.severity === errorType) {
const errorRowCol = this._getRowCol(error.formattedMessage);
const annotation = Object.assign({}, {
row: errorRowCol.row - 1, // must be 0 based
column: errorRowCol.col - 1, // must be 0 based
text: error.formattedMessage, // text to show in tooltip
type: error.severity // "error"|"warning"|"info"
});
errors.push({
solcError: error,
node:
<CompilerError
onClick={(e) => { this._onErrorClick(e, annotation); }}
key={`${errorType}_${index}`}
index={index}
errorType={errorType}
row={errorRowCol.row}
errorMessage={error.formattedMessage} />,
annotation: annotation
});
}
return errors;
}, []);
}
_renderErrorsCard(errors, errorType) {
const color = (errorType === "error" ? "danger" : errorType);
return (Boolean(errors.length) && <LoadingCardWithIcon
anchorId={errorType + "s"}
color={color}
className={errorType + "s-card "}
key={errorType + "s-card"}
showCardOptions={true}
isLoading={this.props.loading}
cardOptionsClassName={errorType + "s"}
body={
<List.Group>
{errors.map(error => { return error.node; })}
</List.Group>
}
headerTitle={
<React.Fragment>
<span className="mr-1">{errorType + "s"}</span><Badge color={color}>{errors.length}</Badge>
</React.Fragment>
}
ref={cardRef => { this[errorType + "sCardRef"] = cardRef; }}
/>);
}
_renderSuccessCard(title, body) {
return this._renderLoadingCard("success", "success-card", "check", title, body, (cardRef) => {
this.deployedCardRef = cardRef;
});
}
_renderFatalCard(title, body) {
return body && this._renderLoadingCard("danger", "fatal-card", "slash", title, body, (cardRef) => {
this.fatalCardRef = cardRef;
});
}
_renderLoadingCard(color, className, iconName, headerTitle, body, refCb) {
return (<LoadingCardWithIcon
color={color}
className={className}
iconName={iconName}
showCardOptions={false}
isLoading={this.props.loading}
body={body}
headerTitle={headerTitle}
key={hashCode([className, iconName, headerTitle].join(''))}
ref={refCb}
/>);
}
render() {
const {fiddle, loading, fiddleError, fiddleDeployError, deployedContracts, fatalError} = this.props;
const {loadingMessage, value, readOnly} = this.state;
let warnings = [];
let errors = [];
if (fiddle && fiddle.errors) {
warnings = this._renderErrors(fiddle.errors, "warning");
errors = this._renderErrors(fiddle.errors, "error");
}
const hasResult = Boolean(fiddle);
return (
<React.Fragment>
<h1 className="page-title">Fiddle</h1>
<p>Play around with contract code and deploy against your running node.</p>
<FiddleResultsSummary
numErrors={errors.length}
numWarnings={warnings.length}
isLoading={loading}
loadingMessage={loadingMessage}
showFatalError={Boolean(fatalError)}
showFatalFiddle={Boolean(fiddleError)}
showFatalFiddleDeploy={Boolean(fiddleDeployError)}
onDeployClick={(e) => this._onDeployClick(e)}
isVisible={Boolean(fatalError || hasResult || loading)}
showDeploy={hasResult && Boolean(fiddle.compilationResult)}
onWarningsClick={(e) => this._onErrorSummaryClick(e, "errorsCardRef")}
onErrorsClick={(e) => this._onErrorSummaryClick(e, "warningsCardRef")}
onFatalClick={(e) => this._onErrorSummaryClick(e, "fatalCardRef")}
/>
<Fiddle
value={value}
readOnly={readOnly}
onCodeChange={(n) => this._onCodeChange(n)}
errors={errors}
warnings={warnings}
ref={(fiddle) => {
if (fiddle) {
this.editor = fiddle.ace.editor;
this.ace = fiddle.ace;
}
}}
/>
<FiddleResults
key="results"
errorsCard={this._renderErrorsCard(errors, "error")}
warningsCard={this._renderErrorsCard(warnings, "warning")}
fatalErrorCard={this._renderFatalCard("Fatal error", fatalError)}
fatalFiddleCard={this._renderFatalCard("Failed to compile", fiddleError)}
fatalFiddleDeployCard={this._renderFatalCard("Failed to deploy", fiddleDeployError)}
deployedContractsCard={deployedContracts && this._renderSuccessCard("Contract(s) deployed!",
<Button
to={`/embark/contracts/${deployedContracts}/overview`}
RootComponent={NavLink}
>Play with my contract(s)</Button>
)}
forwardedRef={this.fiddleResultsRef}
/>
</React.Fragment>
);
}
}
function mapStateToProps(state) {
const fiddle = getFiddle(state);
const deployedFiddle = getFiddleDeploy(state);
return {
fiddle: fiddle.data,
deployedContracts: deployedFiddle.data,
fiddleError: fiddle.error,
fiddleDeployError: deployedFiddle.error,
loading: state.loading,
lastFiddle: fiddle.data ? fiddle.data.codeToCompile : undefined,
fatalError: state.errorMessage
};
}
FiddleContainer.propTypes = {
fiddle: PropTypes.object,
fiddleError: PropTypes.string,
fiddleDeployError: PropTypes.string,
loading: PropTypes.bool,
postFiddle: PropTypes.func,
postFiddleDeploy: PropTypes.func,
deployedContracts: PropTypes.string,
fetchLastFiddle: PropTypes.func,
lastFiddle: PropTypes.any,
fatalError: PropTypes.string
};
export default connect(
mapStateToProps,
{
postFiddle: fiddleAction.post,
postFiddleDeploy: fiddleDeployAction.post,
fetchLastFiddle: fiddleFileAction.request
},
)(FiddleContainer);

View File

@ -1,7 +1,7 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {files as filesAction} from "../actions";
import {files as filesAction, file as fileAction} from "../actions";
import FileExplorer from '../components/FileExplorer';
import DataWrapper from "../components/DataWrapper";
@ -14,8 +14,8 @@ class FileExplorerContainer extends Component {
render() {
return (
<DataWrapper shouldRender={this.props.files.length > 0} {...this.props} render={({files}) => (
<FileExplorer files={files} />
<DataWrapper shouldRender={this.props.files.length > 0} {...this.props} render={({files, fetchFile}) => (
<FileExplorer files={files} fetchFile={fetchFile} />
)} />
);
}
@ -27,11 +27,13 @@ function mapStateToProps(state) {
FileExplorerContainer.propTypes = {
files: PropTypes.array,
fetchFiles: PropTypes.func
fetchFiles: PropTypes.func,
fetchFile: PropTypes.func
};
export default connect(
mapStateToProps,{
fetchFiles: filesAction.request
fetchFiles: filesAction.request,
fetchFile: fileAction.request
}
)(FileExplorerContainer);

View File

@ -0,0 +1,152 @@
/* eslint multiline-ternary: "off" */
/* eslint operator-linebreak: "off" */
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {Grid} from 'tabler-react';
import TextEditor from '../components/TextEditor';
import TextEditorContractErrors from '../components/TextEditorContractErrors';
import TextEditorContractWarnings from '../components/TextEditorContractWarnings';
import TextEditorContractToolbar from '../components/TextEditorContractToolbar';
import TextEditorContractDeploy from '../components/TextEditorContractDeploy';
import TextEditorToolbar from '../components/TextEditorToolbar';
import {
currentFile as currentFileAction,
saveCurrentFile as saveCurrentFileAction,
saveFile as saveFileAction,
removeFile as removeFileAction,
contractCompile as contractCompileAction,
contractDeploy as contractDeployAction
} from '../actions';
import {getCurrentFile, getContractCompile} from '../reducers/selectors';
const DEFAULT_FILE = {name: 'newContract.sol', content: ''};
class TextEditorContainer extends Component {
constructor(props) {
super(props);
this.state = {currentFile: this.props.currentFile};
}
componentDidMount() {
if(this.props.currentFile.content === '') {
this.props.fetchCurrentFile();
}
}
componentDidUpdate(prevProps) {
if(this.props.currentFile.path !== prevProps.currentFile.path) {
this.setState({currentFile: this.props.currentFile});
}
}
isContract() {
return this.state.currentFile.name.endsWith('.sol');
}
onFileContentChange(newContent) {
const newCurrentFile = this.state.currentFile;
newCurrentFile.content = newContent;
this.setState({currentFile: newCurrentFile});
if (!this.isContract()) return;
this.compileTimeout = setTimeout(() => {
this.props.compileContract(newContent, this.state.currentFile.name);
}, 1000);
}
save() {
this.props.saveFile(this.state.currentFile);
this.props.saveCurrentFile(this.state.currentFile);
}
remove() {
this.props.removeFile(this.state.currentFile);
this.setState({currentFile: DEFAULT_FILE});
}
renderContractFooter() {
if (!this.isContract()) return <React.Fragment />;
let components = [];
const {errors, warnings, result} = this.props.contractCompile;
if (errors && errors.length > 0) {
components.push(<TextEditorContractErrors key={1} errors={errors} />);
}
if (warnings && warnings.length > 0) {
components.push(<TextEditorContractWarnings key={2} warnings={warnings} />);
}
if (result) {
components.push(<TextEditorContractDeploy key={3} result={result}/>);
}
return <React.Fragment>{components}</React.Fragment>;
}
renderToolbar(){
if (this.isContract()) {
return <TextEditorContractToolbar currentFile={this.state.currentFile}
contractCompile={this.props.contractCompile}
compilingContract={this.props.compilingContract}
save={() => this.save()}
remove={() => this.remove()} />;
}
return <TextEditorToolbar currentFile={this.state.currentFile}
contractCompile={this.props.contractCompile}
save={() => this.save()}
remove={() => this.remove()} />;
}
render() {
return (
<React.Fragment>
<Grid.Row className="my-2">
{this.renderToolbar()}
</Grid.Row>
<TextEditor value={this.state.currentFile.content}
contractCompile={this.props.contractCompile}
onFileContentChange={(newContent) => this.onFileContentChange(newContent)} />
{this.renderContractFooter()}
</React.Fragment>
);
}
}
function mapStateToProps(state) {
const currentFile = getCurrentFile(state) || DEFAULT_FILE;
const contractCompile = getContractCompile(state, currentFile) || {};
return {
currentFile,
contractCompile,
compilingContract: state.compilingContract,
loading: state.loading,
error: state.errorMessage
};
}
TextEditorContainer.propTypes = {
currentFile: PropTypes.object,
contractCompile: PropTypes.object,
saveCurrentFile: PropTypes.func,
fetchCurrentFile: PropTypes.func,
saveFile: PropTypes.func,
removeFile: PropTypes.func,
deployContract: PropTypes.func,
compileContract: PropTypes.func,
compilingContract: PropTypes.bool,
loading: PropTypes.bool,
error: PropTypes.string
};
export default connect(
mapStateToProps,
{
fetchCurrentFile: currentFileAction.request,
saveCurrentFile: saveCurrentFileAction.request,
saveFile: saveFileAction.request,
removeFile: removeFileAction.request,
deployContract: contractDeployAction.post,
compileContract: contractCompileAction.post
},
)(TextEditorContainer);

View File

@ -1,5 +1,5 @@
import {combineReducers} from 'redux';
import {REQUEST, SUCCESS} from "../actions";
import {REQUEST, SUCCESS, FAILURE, CONTRACT_COMPILE, FILES} from "../actions";
const BN_FACTOR = 10000;
const voidAddress = '0x0000000000000000000000000000000000000000';
@ -12,20 +12,19 @@ const entitiesDefaultState = {
processLogs: [],
contracts: [],
contractProfiles: [],
contractFiles: [],
contractFunctions: [],
contractDeploys: [],
contractCompiles: [],
contractLogs: [],
commands: [],
messages: [],
messageChannels: [],
fiddles: [],
fiddleDeploys: [],
versions: [],
plugins: [],
ensRecords: [],
files: [],
gasOracleStats: []
gasOracleStats: [],
currentFiles: []
};
const sorter = {
@ -61,9 +60,6 @@ const filtrer = {
contracts: function(contract, index, self) {
return index === self.findIndex((t) => t.className === contract.className);
},
contractFiles: function(contractFile, index, self) {
return index === self.findIndex((c) => c.filename === contractFile.filename);
},
accounts: function(account, index, self) {
return index === self.findIndex((t) => t.address === account.address);
},
@ -91,6 +87,9 @@ const filtrer = {
};
function entities(state = entitiesDefaultState, action) {
if (action.type === FILES[SUCCESS]) {
return {...state, files: action.files};
}
for (let name of Object.keys(state)) {
let filter = filtrer[name] || (() => true);
let sort = sorter[name] || (() => true);
@ -137,9 +136,20 @@ function loading(_state = false, action) {
return action.type.endsWith(REQUEST);
}
function compilingContract(state = false, action) {
if(action.type === CONTRACT_COMPILE[REQUEST]) {
return true;
} else if (action.type === CONTRACT_COMPILE[FAILURE] || action.type === CONTRACT_COMPILE[SUCCESS]) {
return false;
}
return state;
}
const rootReducer = combineReducers({
entities,
loading,
compilingContract,
errorMessage,
errorEntities
});

View File

@ -72,10 +72,6 @@ export function getContractProfile(state, contractName) {
return state.entities.contractProfiles.find((contractProfile => contractProfile.name === contractName));
}
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));
}
@ -84,6 +80,17 @@ export function getContractDeploys(state, contractName) {
return state.entities.contractDeploys.filter((contractDeploy => contractDeploy.contractName === contractName));
}
export function getContractCompile(state, file) {
let contractCompile = state.entities.contractCompiles.reverse().find((contractCompile => contractCompile.name === file.name));
if (!contractCompile) return;
if (contractCompile.errors) {
contractCompile.warnings = contractCompile.errors.filter((error) => error.severity === 'warning');
contractCompile.errors = contractCompile.errors.filter((error) => error.severity === 'error');
}
return contractCompile;
}
export function getVersions(state) {
return state.entities.versions;
}
@ -119,15 +126,6 @@ export function getMessages(state) {
return messages;
}
export function getFiddle(state) {
const fiddleCompilation = last(state.entities.fiddles.sort((a, b) => { return (a.timestamp || 0) - (b.timestamp || 0); }));
const isNoTempFileError = Boolean(fiddleCompilation && fiddleCompilation.codeToCompile && fiddleCompilation.codeToCompile.error && fiddleCompilation.codeToCompile.error.indexOf('ENOENT') > -1);
return {
data: fiddleCompilation,
error: isNoTempFileError ? undefined : state.errorEntities.fiddles
};
}
export function getFiddleDeploy(state) {
return {
data: last(state.entities.fiddleDeploys),
@ -150,3 +148,7 @@ export function isEnsEnabled(state) {
export function getFiles(state) {
return state.entities.files;
}
export function getCurrentFile(state) {
return last(state.entities.currentFiles);
}

View File

@ -1,15 +1,11 @@
import * as actions from '../actions';
import * as api from '../api';
import * as api from '../services/api';
import * as storage from '../services/storage';
import {eventChannel} from 'redux-saga';
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,
fiddleDeploy, ensRecord, ensRecords, contractLogs, contractFile, contractFunction, contractDeploy,
fiddleFile, files, gasOracle} = actions;
function *doRequest(entity, apiFn, payload) {
const {response, error} = yield call(apiFn, payload);
function *doRequest(entity, serviceFn, payload) {
const {response, error} = yield call(serviceFn, payload);
if(response) {
yield put(entity.success(response.data, payload));
} else if (error) {
@ -17,32 +13,37 @@ function *doRequest(entity, apiFn, payload) {
}
}
export const fetchPlugins = doRequest.bind(null, plugins, api.fetchPlugins);
export const fetchVersions = doRequest.bind(null, versions, api.fetchVersions);
export const fetchAccount = doRequest.bind(null, account, api.fetchAccount);
export const fetchBlock = doRequest.bind(null, block, api.fetchBlock);
export const fetchTransaction = doRequest.bind(null, transaction, api.fetchTransaction);
export const fetchAccounts = doRequest.bind(null, accounts, api.fetchAccounts);
export const fetchBlocks = doRequest.bind(null, blocks, api.fetchBlocks);
export const fetchTransactions = doRequest.bind(null, transactions, api.fetchTransactions);
export const fetchProcesses = doRequest.bind(null, processes, api.fetchProcesses);
export const postCommand = doRequest.bind(null, commands, api.postCommand);
export const fetchProcessLogs = doRequest.bind(null, processLogs, api.fetchProcessLogs);
export const fetchContractLogs = doRequest.bind(null, contractLogs, api.fetchContractLogs);
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 fetchLastFiddle = doRequest.bind(null, fiddleFile, api.fetchLastFiddle);
export const postContractFunction = doRequest.bind(null, contractFunction, api.postContractFunction);
export const postContractDeploy = doRequest.bind(null, contractDeploy, api.postContractDeploy);
export const postFiddle = doRequest.bind(null, fiddle, api.postFiddle);
export const postFiddleDeploy = doRequest.bind(null, fiddleDeploy, api.postFiddleDeploy);
export const sendMessage = doRequest.bind(null, messageSend, api.sendMessage);
export const fetchEnsRecord = doRequest.bind(null, ensRecord, api.fetchEnsRecord);
export const postEnsRecord = doRequest.bind(null, ensRecords, api.postEnsRecord);
export const fetchFiles = doRequest.bind(null, files, api.fetchFiles);
export const fetchEthGas = doRequest.bind(null, gasOracle, api.getEthGasAPI);
export const fetchPlugins = doRequest.bind(null, actions.plugins, api.fetchPlugins);
export const fetchVersions = doRequest.bind(null, actions.versions, api.fetchVersions);
export const fetchAccount = doRequest.bind(null, actions.account, api.fetchAccount);
export const fetchBlock = doRequest.bind(null, actions.block, api.fetchBlock);
export const fetchTransaction = doRequest.bind(null, actions.transaction, api.fetchTransaction);
export const fetchAccounts = doRequest.bind(null, actions.accounts, api.fetchAccounts);
export const fetchBlocks = doRequest.bind(null, actions.blocks, api.fetchBlocks);
export const fetchTransactions = doRequest.bind(null, actions.transactions, api.fetchTransactions);
export const fetchProcesses = doRequest.bind(null, actions.processes, api.fetchProcesses);
export const postCommand = doRequest.bind(null, actions.commands, api.postCommand);
export const fetchProcessLogs = doRequest.bind(null, actions.processLogs, api.fetchProcessLogs);
export const fetchContractLogs = doRequest.bind(null, actions.contractLogs, api.fetchContractLogs);
export const fetchContracts = doRequest.bind(null, actions.contracts, api.fetchContracts);
export const fetchContract = doRequest.bind(null, actions.contract, api.fetchContract);
export const fetchContractProfile = doRequest.bind(null, actions.contractProfile, api.fetchContractProfile);
export const postContractFunction = doRequest.bind(null, actions.contractFunction, api.postContractFunction);
export const postContractDeploy = doRequest.bind(null, actions.contractDeploy, api.postContractDeploy);
export const postContractCompile = doRequest.bind(null, actions.contractCompile, api.postContractCompile);
export const sendMessage = doRequest.bind(null, actions.messageSend, api.sendMessage);
export const fetchEnsRecord = doRequest.bind(null, actions.ensRecord, api.fetchEnsRecord);
export const postEnsRecord = doRequest.bind(null, actions.ensRecords, api.postEnsRecord);
export const fetchFiles = doRequest.bind(null, actions.files, api.fetchFiles);
export const fetchFile = doRequest.bind(null, actions.file, api.fetchFile);
export const postFile = doRequest.bind(null, actions.saveFile, api.postFile);
export const deleteFile = doRequest.bind(null, actions.removeFile, api.deleteFile);
export const fetchEthGas = doRequest.bind(null, actions.gasOracle, api.getEthGasAPI);
export const fetchCurrentFile = doRequest.bind(null, actions.currentFile, storage.fetchCurrentFile);
export const postCurrentFile = doRequest.bind(null, actions.saveCurrentFile, storage.postCurrentFile);
export const deleteCurrentFile = doRequest.bind(null, null, storage.deleteCurrentFile);
export function *watchFetchTransaction() {
yield takeEvery(actions.TRANSACTION[actions.REQUEST], fetchTransaction);
@ -96,14 +97,6 @@ export function *watchFetchContractProfile() {
yield takeEvery(actions.CONTRACT_PROFILE[actions.REQUEST], fetchContractProfile);
}
export function *watchFetchContractFile() {
yield takeEvery(actions.CONTRACT_FILE[actions.REQUEST], fetchContractFile);
}
export function *watchFetchLastFiddle() {
yield takeEvery(actions.FIDDLE_FILE[actions.REQUEST], fetchLastFiddle);
}
export function *watchPostContractFunction() {
yield takeEvery(actions.CONTRACT_FUNCTION[actions.REQUEST], postContractFunction);
}
@ -112,6 +105,10 @@ export function *watchPostContractDeploy() {
yield takeEvery(actions.CONTRACT_DEPLOY[actions.REQUEST], postContractDeploy);
}
export function *watchPostContractCompile() {
yield takeEvery(actions.CONTRACT_COMPILE[actions.REQUEST], postContractCompile);
}
export function *watchFetchVersions() {
yield takeEvery(actions.VERSIONS[actions.REQUEST], fetchVersions);
}
@ -136,22 +133,39 @@ export function *watchListenToMessages() {
yield takeEvery(actions.MESSAGE_LISTEN[actions.REQUEST], listenToMessages);
}
export function *watchPostFiddle() {
yield takeEvery(actions.FIDDLE[actions.REQUEST], postFiddle);
}
export function *watchFetchLastFiddleSuccess() {
yield takeEvery(actions.FIDDLE_FILE[actions.SUCCESS], postFiddle);
}
export function *watchPostFiddleDeploy() {
yield takeEvery(actions.FIDDLE_DEPLOY[actions.REQUEST], postFiddleDeploy);
}
export function *watchFetchFiles() {
yield takeEvery(actions.FILES[actions.REQUEST], fetchFiles);
}
export function *watchFetchFile() {
yield takeEvery(actions.FILE[actions.REQUEST], fetchFile);
}
export function *watchPostFile() {
yield takeEvery(actions.SAVE_FILE[actions.REQUEST], postFile);
}
export function *watchDeleteFile() {
yield takeEvery(actions.REMOVE_FILE[actions.REQUEST], deleteFile);
}
export function *watchDeleteFileSuccess() {
yield takeEvery(actions.REMOVE_FILE[actions.SUCCESS], fetchFiles);
yield takeEvery(actions.REMOVE_FILE[actions.SUCCESS], deleteCurrentFile);
}
export function *watchFetchFileSuccess() {
yield takeEvery(actions.FILE[actions.SUCCESS], postCurrentFile);
}
export function *watchFetchCurrentFile() {
yield takeEvery(actions.CURRENT_FILE[actions.REQUEST], fetchCurrentFile);
}
export function *watchPostCurrentFile() {
yield takeEvery(actions.SAVE_CURRENT_FILE[actions.REQUEST], postCurrentFile);
}
export function *watchFetchEthGas() {
yield takeEvery(actions.GAS_ORACLE[actions.REQUEST], fetchEthGas);
}
@ -186,7 +200,7 @@ export function *listenToProcessLogs(action) {
const channel = yield call(createChannel, socket);
while (true) {
const processLog = yield take(channel);
yield put(processLogs.success([processLog]));
yield put(actions.processLogs.success([processLog]));
}
}
@ -199,7 +213,7 @@ export function *listenToContractLogs() {
const channel = yield call(createChannel, socket);
while (true) {
const contractLog = yield take(channel);
yield put(contractLogs.success([contractLog]));
yield put(actions.contractLogs.success([contractLog]));
}
}
@ -212,7 +226,7 @@ export function *listenGasOracle() {
const channel = yield call(createChannel, socket);
while (true) {
const gasOracleStats = yield take(channel);
yield put(gasOracle.success(gasOracleStats));
yield put(actions.gasOracle.success(gasOracleStats));
}
}
@ -225,7 +239,7 @@ export function *listenToMessages(action) {
const channel = yield call(createChannel, socket);
while (true) {
const message = yield take(channel);
yield put(messageListen.success([{channel: action.messageChannels[0], message: message.data, time: message.time}]));
yield put(actions.messageListen.success([{channel: action.messageChannels[0], message: message.data, time: message.time}]));
}
}
@ -247,20 +261,23 @@ export default function *root() {
fork(watchFetchBlocks),
fork(watchFetchContracts),
fork(watchFetchContractProfile),
fork(watchFetchContractFile),
fork(watchPostContractFunction),
fork(watchPostContractDeploy),
fork(watchPostContractCompile),
fork(watchListenToMessages),
fork(watchSendMessage),
fork(watchFetchContract),
fork(watchFetchTransaction),
fork(watchPostFiddle),
fork(watchPostFiddleDeploy),
fork(watchFetchLastFiddle),
fork(watchFetchLastFiddleSuccess),
fork(watchFetchEnsRecord),
fork(watchPostEnsRecords),
fork(watchFetchFiles),
fork(watchFetchFile),
fork(watchPostFile),
fork(watchDeleteFile),
fork(watchDeleteFileSuccess),
fork(watchFetchFileSuccess),
fork(watchFetchCurrentFile),
fork(watchPostCurrentFile),
fork(watchFetchEthGas),
fork(watchListenGasOracle)
]);

View File

@ -20,6 +20,16 @@ function post(path, params) {
});
}
function destroy(path, params) {
return axios.delete(constants.httpEndpoint + path, params)
.then((response) => {
return {response, error: null};
})
.catch((error) => {
return {response: null, error: error.message || 'Something bad happened'};
});
}
export function postCommand(payload) {
return post('/command', payload);
}
@ -76,6 +86,10 @@ export function postContractDeploy(payload) {
return post(`/contract/${payload.contractName}/deploy`, payload);
}
export function postContractCompile(payload) {
return post('/contract/compile', payload);
}
export function fetchVersions() {
return get('/versions');
}
@ -104,16 +118,24 @@ export function postEnsRecord(payload) {
return post('/ens/register', payload);
}
export function fetchContractFile(payload) {
return get('/files/contracts', {params: payload});
}
export function getEthGasAPI() {
return get('/blockchain/gas/oracle', {});
}
export function fetchLastFiddle() {
return get('/files/lastfiddle', {params: 'temp'});
export function fetchFiles() {
return get('/files');
}
export function fetchFile(payload) {
return get('/file', {params: payload});
}
export function postFile(payload) {
return post('/files', payload);
}
export function deleteFile(payload) {
return destroy('/file', {params: payload});
}
export function listenToChannel(channel) {
@ -135,15 +157,3 @@ export function webSocketBlockHeader() {
export function websocketGasOracle() {
return new WebSocket(`${constants.wsEndpoint}/blockchain/gas/oracle`);
}
export function postFiddle(payload) {
return post('/contract/compile', payload);
}
export function postFiddleDeploy(payload) {
return post('/contract/deploy', {compiledContract: payload.compiledCode});
}
export function fetchFiles() {
return get('/files');
}

View File

@ -0,0 +1,19 @@
export function postCurrentFile(file) {
return new Promise(function(resolve) {
localStorage.setItem('currentFile', JSON.stringify(file));
resolve({response: {data: file}});
});
}
export function fetchCurrentFile() {
return new Promise(function(resolve) {
resolve({response: {data: JSON.parse(localStorage.getItem('currentFile'))}});
});
}
export function deleteCurrentFile() {
return new Promise(function(resolve) {
localStorage.removeItem('currentFile');
resolve({});
});
}

View File

@ -2,7 +2,8 @@ let toposort = require('toposort');
let async = require('async');
const cloneDeep = require('clone-deep');
let utils = require('../../utils/utils.js');
const utils = require('../../utils/utils.js');
const fs = require('../../core/fs');
// TODO: create a contract object
@ -273,6 +274,7 @@ class ContractsManager {
contract.abiDefinition = compiledContract.abiDefinition;
contract.filename = compiledContract.filename;
contract.originalFilename = compiledContract.originalFilename || ("contracts/" + contract.filename);
contract.path = fs.dappPath(contract.originalFilename);
contract.gas = (contractConfig && contractConfig.gas) || self.contractsConfig.gas || 'auto';

View File

@ -23,51 +23,62 @@ class Pipeline {
this.events.setCommandHandler('pipeline:build', (options, callback) => this.build(options, callback));
fs.removeSync(this.buildDir);
const self = this;
self.events.setCommandHandler("files:contract", (filename, cb) => {
// handle case where we have a fiddle file and not a file stored in the dapp
if(filename.indexOf('.embark/fiddles') > -1){
return fs.readFile(filename, 'utf8', (err, source) => {
if (err) return cb({error: err});
cb(source);
});
}
let file = self.contractsFiles.find((file) => file.filename === filename);
if (!file) {
return cb({error: filename + " not found"});
}
file.content(cb);
});
let plugin = this.plugins.createPlugin('deployment', {});
plugin.registerAPICall(
'get',
'/embark-api/files/contracts/',
'/embark-api/file',
(req, res) => {
self.events.request('files:contract', req.query.filename, res.send.bind(res));
if (!fs.existsSync(req.query.path) || !req.query.path.startsWith(fs.dappPath())) {
return res.send({error: 'Path is invalid'});
}
const name = path.basename(req.query.path);
const content = fs.readFileSync(req.query.path, 'utf8');
res.send({name, content, path: req.query.path});
}
);
plugin.registerAPICall(
'post',
'/embark-api/files',
(req, res) => {
try {
this.apiGuardBadFile(req.body.path);
} catch (error) {
return res.send({error: error.message});
}
fs.writeFileSync(req.body.path, req.body.content, { encoding: 'utf8'});
const name = path.basename(req.body.path);
res.send({name, path: req.body.path, content: req.body.content});
}
);
plugin.registerAPICall(
'delete',
'/embark-api/file',
(req, res) => {
try {
this.apiGuardBadFile(req.query.path);
} catch (error) {
return res.send({error: error.message});
}
fs.removeSync(req.query.path);
res.send();
}
);
plugin.registerAPICall(
'get',
'/embark-api/files/lastfiddle',
'/embark-api/files',
(req, res) => {
fs.readFile(fs.dappPath('.embark/fiddles/temp.sol'), 'utf8', (err, source) => {
if (err) return res.send({error: err.message});
res.send(source);
});
const rootPath = fs.dappPath();
const walk = (dir, filelist = []) => fs.readdirSync(dir).map(name => {
let isRoot = rootPath === dir;
if (fs.statSync(path.join(dir, name)).isDirectory()) {
return { isRoot, name, dirname: dir, children: walk(path.join(dir, name), filelist)};
}
);
plugin.registerAPICall(
'get',
'/embark-api/files/',
(req, res) => {
const walk = (dir, filelist = []) => fs.readdirSync(dir).map(file => {
if (fs.statSync(path.join(dir, file)).isDirectory()) {
return {name: file, children: walk(path.join(dir, file), filelist)};
}
return {name: file};
return {name, isRoot, path: path.join(dir, name), dirname: dir};
});
const files = walk(fs.dappPath());
res.send(files);
@ -75,6 +86,13 @@ class Pipeline {
);
}
apiGuardBadFile(pathToCheck) {
const dir = path.dirname(pathToCheck);
if (!fs.existsSync(pathToCheck) || !dir.startsWith(fs.dappPath())) {
throw new Error('Path is invalid');
}
}
build({modifiedAssets}, callback) {
let self = this;
const importsList = {};

View File

@ -1,6 +1,5 @@
let async = require('../../utils/async_extend.js');
let SolcW = require('./solcW.js');
const fs = require('../../core/fs');
class Solidity {
@ -20,18 +19,12 @@ class Solidity {
'post',
'/embark-api/contract/compile',
(req, res) => {
if(typeof req.body.codeToCompile !== 'string'){
return res.send({error: 'Body parameter \'codeToCompile\' must be a string'});
if(typeof req.body.code !== 'string'){
return res.send({error: 'Body parameter \'code\' must be a string'});
}
const input = {'fiddle': {content: req.body.codeToCompile.replace(/\r\n/g, '\n')}};
this.compile_solidity_code(input, {}, true, (errors, compilationResult) => {
// write code to filesystem so we can view the source after page refresh
const className = !compilationResult ? 'temp' : Object.keys(compilationResult).join('_');
this._writeFiddleToFile(req.body.codeToCompile, className, Boolean(compilationResult), (err) => {
if(err) this.logger.trace('Error writing fiddle to filesystem: ', err);
}); // async, do not need to wait
const responseData = {errors: errors, compilationResult: compilationResult};
const input = {[req.body.name]: {content: req.body.code.replace(/\r\n/g, '\n')}};
this.compile_solidity_code(input, {}, true, (errors, result) => {
const responseData = {errors: errors, result: result};
this.logger.trace(`POST response /embark-api/contract/compile:\n ${JSON.stringify(responseData)}`);
res.send(responseData);
});
@ -39,26 +32,6 @@ class Solidity {
);
}
_writeFiddleToFile(code, className, isCompiled, cb){
fs.mkdirp('.embark/fiddles', (err) => {
if(err) return cb(err);
// always write to temp.sol file
const filePath = Solidity._getFiddlePath('temp');
fs.writeFile(filePath, code, 'utf8', cb);
// if it's compiled, also write to [classname].sol
if(isCompiled){
const filePath = Solidity._getFiddlePath(className);
fs.writeFile(filePath, code, 'utf8', cb);
}
});
}
static _getFiddlePath(className){
return fs.dappPath(`.embark/fiddles/${className}.sol`);
}
_compile(jsonObj, returnAllErrors, callback) {
const self = this;
self.solcW.compile(jsonObj, function (err, output) {
@ -153,7 +126,6 @@ class Solidity {
const className = contractName;
let filename = contractFile;
if(filename === 'fiddle') filename = Solidity._getFiddlePath(className);
compiled_object[className] = {};
compiled_object[className].code = contract.evm.bytecode.object;