Solidity Compiler in UI (#3279)

* Added new Deploy Contract page // Use Brace in React #2276

* Adding Web Wrokers WIP

* Compiling Solidity code // Getting mandatory params #2276

* Working editor and deployment #2276

* WIP : displaying source code

* Added Solidity hightling, editor component in UI

* Re-adding the standard Deploy Modal #2276

* Using MobX in Contract Edition // Save to Localstorage #2276

* User select Solidity version #2276

* Loading Solidity versions and closing worker properly #2276

* Adds export to solidity editor #2276

* Adding Import to Contract Editor #2276

* Persistent Worker => Don't load twice Solidity Code #2276

* UI Fixes

* Editor tweaks

* Added Details with ABI in Contract view

* Adds Save capabilities to contract editor // WIP on Load #3279

* Working Load and Save contracts... #3231

* Adding loader of Snippets // Export with name #3279

* Added snippets / Importing from files and from URL

* Fix wrong ID in saved Contract

* Fix lint

* Fixed Formal errors as warning #3279

* Fixing lint issues

* Use NPM Module for valid URL (fixes linting issue too)

* Don't clobber tests.
This commit is contained in:
Nicolas Gotchac
2016-11-11 15:00:04 +01:00
committed by Jaco Greeff
parent 5d8f74ed57
commit 0e4ef539fc
38 changed files with 3555 additions and 21 deletions

View File

@@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './writeContract';

View File

@@ -0,0 +1,174 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.outer, .page, .editor {
flex: 1;
display: flex;
flex-direction: column;
}
.timestamp {
font-size: 0.75em;
margin-left: 1em;
color: #ccc;
}
.container {
padding: 1em 0;
display: flex;
flex: 1;
flex-direction: row;
> * {
margin: 0;
> h2 {
margin-top: 0;
}
}
}
.mainEditor {
&:global(.ace-solarized-dark) {
background-color: rgba(0, 0, 0, 0.5);
:global(.ace_gutter) {
background-color: rgba(0, 0, 0, 0.7);
}
:global(.ace_content) {
background-color: transparent;
}
}
}
.big {
font-size: 1.2em;
}
.centeredMessage {
width: 100%;
height: 75%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.resizing * {
cursor: ew-resize !important;
user-select: none !important;
}
.editor {
width: 0;
margin-left: 0.5em;
}
.parameters {
width: 0;
display: flex;
flex-direction: column;
margin-right: 0.5em;
.panel {
background-color: rgba(0, 0, 0, 0.5);
padding: 1em;
flex: 1;
display: flex;
flex-direction: column;
}
.compilation {
flex: 1 0 0;
display: flex;
flex-direction: column;
}
.errors {
flex: 1 0 0;
overflow: auto;
margin-right: -0.5em;
margin-top: 0.5em;
}
}
.messageContainer {
padding: 0.5em 0;
margin-right: 0.5em;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
}
.errorPosition {
background-color: rgba(0, 0, 0, 0.5);
padding: 0.25em 0.5em;
top: 0;
position: relative;
margin-bottom: 0.25em;
font-size: 0.75em;
}
}
.message {
font-family: monospace;
padding: 0.5em;
font-size: 0.9em;
white-space: pre;
overflow: auto;
&.error {
background-color: rgba(244, 67, 54, 0.5);
}
&.warning {
background-color: rgba(255, 235, 59, 0.5);
}
&.formal {
background-color: rgba(243, 156, 18, 0.5);
}
}
.messagesHeader {
margin-bottom: 0.25em;
text-transform: uppercase;
font-size: 0.9em;
}
.sliderContainer {
flex: 0 0 .8em;
display: flex;
align-items: center;
justify-content: center;
.slider {
width: 0.4em;
height: 3em;
border-radius: 0.75em;
background-color: rgba(0, 0, 0, 0.5);
content: ' ';
&:hover {
cursor: ew-resize;
}
}
}

View File

@@ -0,0 +1,502 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { PropTypes, Component } from 'react';
import { observer } from 'mobx-react';
import { MenuItem } from 'material-ui';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import CircularProgress from 'material-ui/CircularProgress';
import moment from 'moment';
import ContentClear from 'material-ui/svg-icons/content/clear';
import SaveIcon from 'material-ui/svg-icons/content/save';
import ListIcon from 'material-ui/svg-icons/action/view-list';
import SettingsIcon from 'material-ui/svg-icons/action/settings';
import SendIcon from 'material-ui/svg-icons/content/send';
import { Actionbar, ActionbarExport, ActionbarImport, Button, Editor, Page, Select, Input } from '../../ui';
import { DeployContract, SaveContract, LoadContract } from '../../modals';
import { setupWorker } from '../../redux/providers/compilerActions';
import WriteContractStore from './writeContractStore';
import styles from './writeContract.css';
@observer
class WriteContract extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
setupWorker: PropTypes.func.isRequired,
worker: PropTypes.object
};
store = new WriteContractStore();
state = {
resizing: false,
size: 65
};
componentWillMount () {
const { setupWorker, worker } = this.props;
setupWorker();
if (worker) {
this.store.setCompiler(worker);
}
}
componentDidMount () {
this.store.setEditor(this.refs.editor);
// Wait for editor to be loaded
window.setTimeout(() => {
this.store.resizeEditor();
}, 2000);
}
componentWillReceiveProps (nextProps) {
if (!this.props.worker && nextProps.worker) {
this.store.setCompiler(nextProps.worker);
}
}
render () {
const { sourcecode } = this.store;
const { size, resizing } = this.state;
const annotations = this.store.annotations
.slice()
.filter((a) => a.contract === '');
return (
<div className={ styles.outer }>
{ this.renderDeployModal() }
{ this.renderSaveModal() }
{ this.renderLoadModal() }
{ this.renderActionBar() }
<Page className={ styles.page }>
<div
className={ `${styles.container} ${resizing ? styles.resizing : ''}` }
onMouseMove={ this.handleResize }
onMouseUp={ this.handleStopResize }
onMouseLeave={ this.handleStopResize }
>
<div
className={ styles.editor }
style={ { flex: `${size}%` } }
>
<h2>{ this.renderTitle() }</h2>
<Editor
ref='editor'
onChange={ this.store.handleEditSourcecode }
onExecute={ this.store.handleCompile }
annotations={ annotations }
value={ sourcecode }
className={ styles.mainEditor }
/>
</div>
<div className={ styles.sliderContainer }>
<span
className={ styles.slider }
onMouseDown={ this.handleStartResize }
>
</span>
</div>
<div
className={ styles.parameters }
style={ { flex: `${100 - size}%` } }
>
<h2>Parameters</h2>
{ this.renderParameters() }
</div>
</div>
</Page>
</div>
);
}
renderTitle () {
const { selectedContract } = this.store;
if (!selectedContract || !selectedContract.name) {
return 'New Solidity Contract';
}
return (
<span>
{ selectedContract.name }
<span
className={ styles.timestamp }
title={ `saved @ ${(new Date(selectedContract.timestamp)).toISOString()}` }
>
(saved { moment(selectedContract.timestamp).fromNow() })
</span>
</span>
);
}
renderActionBar () {
const { sourcecode, selectedContract } = this.store;
const filename = selectedContract && selectedContract.name
? selectedContract.name
.replace(/[^a-z0-9]+/gi, '-')
.replace(/-$/, '')
.toLowerCase()
: 'contract.sol';
const extension = /\.sol$/.test(filename) ? '' : '.sol';
const buttons = [
<Button
icon={ <ContentClear /> }
label='New'
key='newContract'
onClick={ this.store.handleNewContract }
/>,
<Button
icon={ <ListIcon /> }
label='Load'
key='loadContract'
onClick={ this.store.handleOpenLoadModal }
/>,
<Button
icon={ <SaveIcon /> }
label='Save'
key='saveContract'
onClick={ this.store.handleSaveContract }
/>,
<ActionbarExport
key='exportSourcecode'
content={ sourcecode }
filename={ `${filename}${extension}` }
/>,
<ActionbarImport
key='importSourcecode'
title='Import Solidity code'
onConfirm={ this.store.handleImport }
renderValidation={ this.renderImportValidation }
/>
];
return (
<Actionbar
title='Write a Contract'
buttons={ buttons }
/>
);
}
renderImportValidation = (content) => {
return (
<Editor
readOnly
value={ content }
maxLines={ 20 }
/>
);
}
renderParameters () {
const { compiling, contract, selectedBuild, loading } = this.store;
if (selectedBuild < 0) {
return (
<div className={ `${styles.panel} ${styles.centeredMessage}` }>
<CircularProgress size={ 80 } thickness={ 5 } />
<p>Loading...</p>
</div>
);
}
if (loading) {
const { longVersion } = this.store.builds[selectedBuild];
return (
<div className={ styles.panel }>
<div className={ styles.centeredMessage }>
<CircularProgress size={ 80 } thickness={ 5 } />
<p>Loading Solidity { longVersion }</p>
</div>
</div>
);
}
return (
<div className={ styles.panel }>
<div>
<Button
icon={ <SettingsIcon /> }
label='Compile'
onClick={ this.store.handleCompile }
primary={ false }
disabled={ compiling }
/>
{
contract
? <Button
icon={ <SendIcon /> }
label='Deploy'
onClick={ this.store.handleOpenDeployModal }
primary={ false }
/>
: null
}
</div>
{ this.renderSolidityVersions() }
{ this.renderCompilation() }
</div>
);
}
renderSolidityVersions () {
const { builds, selectedBuild } = this.store;
const buildsList = builds.map((build, index) => (
<MenuItem
key={ index }
value={ index }
label={ build.release ? build.version : build.longVersion }
>
{
build.release
? (<span className={ styles.big }>{ build.version }</span>)
: build.longVersion
}
</MenuItem>
));
return (
<div>
<Select
label='Select a Solidity version'
value={ selectedBuild }
onChange={ this.store.handleSelectBuild }
>
{ buildsList }
</Select>
</div>
);
}
renderDeployModal () {
const { showDeployModal, contract, sourcecode } = this.store;
if (!showDeployModal) {
return null;
}
return (
<DeployContract
abi={ contract.interface }
code={ `0x${contract.bytecode}` }
source={ sourcecode }
accounts={ this.props.accounts }
onClose={ this.store.handleCloseDeployModal }
readOnly
/>
);
}
renderLoadModal () {
const { showLoadModal } = this.store;
if (!showLoadModal) {
return null;
}
return (
<LoadContract
onLoad={ this.store.handleLoadContract }
onDelete={ this.store.handleDeleteContract }
onClose={ this.store.handleCloseLoadModal }
contracts={ this.store.savedContracts }
snippets={ this.store.snippets }
/>
);
}
renderSaveModal () {
const { showSaveModal, sourcecode } = this.store;
if (!showSaveModal) {
return null;
}
return (
<SaveContract
sourcecode={ sourcecode }
onSave={ this.store.handleSaveNewContract }
onClose={ this.store.handleCloseSaveModal }
/>
);
}
renderCompilation () {
const { compiled, contracts, compiling, contractIndex, contract } = this.store;
if (compiling) {
return (
<div className={ styles.centeredMessage }>
<CircularProgress size={ 80 } thickness={ 5 } />
<p>Compiling...</p>
</div>
);
}
if (!compiled) {
return (
<div className={ styles.centeredMessage }>
<p>Please compile the source code.</p>
</div>
);
}
if (!contracts) {
return this.renderErrors();
}
const contractKeys = Object.keys(contracts);
if (contractKeys.length === 0) {
return (
<div className={ styles.centeredMessage }>
<p>No contract has been found.</p>
</div>
);
}
const contractsList = contractKeys.map((name, index) => (
<MenuItem
key={ index }
value={ index }
label={ name }
>
{ name }
</MenuItem>
));
return (
<div className={ styles.compilation }>
<Select
label='Select a contract'
value={ contractIndex }
onChange={ this.store.handleSelectContract }
>
{ contractsList }
</Select>
{ this.renderContract(contract) }
<h4 className={ styles.messagesHeader }>Compiler messages</h4>
{ this.renderErrors() }
</div>
);
}
renderContract (contract) {
const { bytecode } = contract;
const abi = contract.interface;
return (
<div>
<Input
readOnly
value={ abi }
label='ABI Interface'
/>
<Input
readOnly
value={ `0x${bytecode}` }
label='Bytecode'
/>
</div>
);
}
renderErrors () {
const { annotations } = this.store;
const body = annotations.map((annotation, index) => {
const { text, row, column, contract, type, formal } = annotation;
const classType = formal ? 'formal' : type;
const classes = [ styles.message, styles[classType] ];
return (
<div key={ index } className={ styles.messageContainer }>
<div className={ classes.join(' ') }>{ text }</div>
<span className={ styles.errorPosition }>
{ contract ? `[ ${contract} ] ` : '' }
{ row }: { column }
</span>
</div>
);
});
return (
<div className={ styles.errors }>
{ body }
</div>
);
}
handleStartResize = () => {
this.setState({ resizing: true });
}
handleStopResize = () => {
this.setState({ resizing: false });
}
handleResize = (event) => {
if (!this.state.resizing) {
return;
}
const { pageX, currentTarget } = event;
const { width, left } = currentTarget.getBoundingClientRect();
const x = pageX - left;
this.setState({ size: 100 * x / width });
event.stopPropagation();
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
const { worker } = state.compiler;
return { accounts, worker };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
setupWorker
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(WriteContract);

View File

@@ -0,0 +1,373 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, observable } from 'mobx';
import store from 'store';
import { debounce } from 'lodash';
const WRITE_CONTRACT_STORE_KEY = '_parity::writeContractStore';
const SNIPPETS = {
snippet0: {
name: 'Token.sol',
description: 'Standard ERP20 Token Contract',
id: 'snippet0', sourcecode: require('raw!../../contracts/snippets/token.sol')
},
snippet1: {
name: 'StandardToken.sol',
description: 'Implementation of ERP20 Token Contract',
id: 'snippet1', sourcecode: require('raw!../../contracts/snippets/standard-token.sol')
},
snippet2: {
name: 'HumanStandardToken.sol',
description: 'Implementation of the Human Token Contract',
id: 'snippet2', sourcecode: require('raw!../../contracts/snippets/human-standard-token.sol')
}
};
export default class WriteContractStore {
@observable sourcecode = '';
@observable compiled = false;
@observable compiling = false;
@observable loading = true;
@observable contractIndex = -1;
@observable contract = null;
@observable contracts = {};
@observable errors = [];
@observable annotations = [];
@observable builds = [];
@observable selectedBuild = -1;
@observable showDeployModal = false;
@observable showSaveModal = false;
@observable showLoadModal = false;
@observable savedContracts = {};
@observable selectedContract = {};
snippets = SNIPPETS;
constructor () {
this.reloadContracts();
this.fetchSolidityVersions();
this.debouncedCompile = debounce(this.handleCompile, 1000);
}
@action setEditor (editor) {
this.editor = editor;
}
@action setCompiler (compiler) {
this.compiler = compiler;
this.compiler.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.event) {
case 'compiled':
this.parseCompiled(message.data);
break;
case 'loading':
this.parseLoading(message.data);
break;
case 'try-again':
this.handleCompile();
break;
}
};
}
fetchSolidityVersions () {
fetch('https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/list.json')
.then((r) => r.json())
.then((data) => {
const { builds, releases, latestRelease } = data;
let latestIndex = -1;
this.builds = builds.reverse().map((build, index) => {
if (releases[build.version] === build.path) {
build.release = true;
if (build.version === latestRelease) {
build.latest = true;
this.loadSolidityVersion(build);
latestIndex = index;
}
}
return build;
});
this.selectedBuild = latestIndex;
});
}
@action closeWorker = () => {
this.compiler.postMessage(JSON.stringify({
action: 'close'
}));
}
@action handleImport = (sourcecode) => {
this.reloadContracts(-1, sourcecode);
}
@action handleSelectBuild = (_, index, value) => {
this.selectedBuild = value;
this.loadSolidityVersion(this.builds[value]);
}
@action loadSolidityVersion = (build) => {
this.compiler.postMessage(JSON.stringify({
action: 'load',
data: build
}));
}
@action handleOpenDeployModal = () => {
this.showDeployModal = true;
}
@action handleCloseDeployModal = () => {
this.showDeployModal = false;
}
@action handleOpenLoadModal = () => {
this.showLoadModal = true;
}
@action handleCloseLoadModal = () => {
this.showLoadModal = false;
}
@action handleOpenSaveModal = () => {
this.showSaveModal = true;
}
@action handleCloseSaveModal = () => {
this.showSaveModal = false;
}
@action handleSelectContract = (_, index, value) => {
this.contractIndex = value;
this.contract = this.contracts[Object.keys(this.contracts)[value]];
}
@action handleCompile = () => {
this.compiled = false;
this.compiling = true;
const build = this.builds[this.selectedBuild];
if (this.compiler && typeof this.compiler.postMessage === 'function') {
this.sendFilesToWorker();
this.compiler.postMessage(JSON.stringify({
action: 'compile',
data: {
sourcecode: this.sourcecode,
build: build
}
}));
}
}
parseErrors = (data, formal = false) => {
const regex = /^(.*):(\d+):(\d+):\s*([a-z]+):\s*((.|[\r\n])+)$/i;
return (data || [])
.filter((e) => regex.test(e))
.map((error, index) => {
const match = regex.exec(error);
const contract = match[1];
const row = parseInt(match[2]) - 1;
const column = parseInt(match[3]);
const type = formal ? 'warning' : match[4].toLowerCase();
const text = match[5];
return {
contract,
row, column,
type, text,
formal
};
});
}
@action parseCompiled = (data) => {
const { contracts } = data;
const { errors = [] } = data;
const errorAnnotations = this.parseErrors(errors);
const formalAnnotations = this.parseErrors(data.formal && data.formal.errors, true);
const annotations = [].concat(
errorAnnotations,
formalAnnotations
);
if (annotations.findIndex((a) => /__parity_tryAgain/.test(a.text)) > -1) {
return;
}
const contractKeys = Object.keys(contracts || {});
this.contract = contractKeys.length ? contracts[contractKeys[0]] : null;
this.contractIndex = contractKeys.length ? 0 : -1;
this.contracts = contracts;
this.errors = errors;
this.annotations = annotations;
this.compiled = true;
this.compiling = false;
}
@action parseLoading = (isLoading) => {
this.loading = isLoading;
if (!isLoading) {
this.handleCompile();
}
}
@action handleEditSourcecode = (value, compile = false) => {
this.sourcecode = value;
const localStore = store.get(WRITE_CONTRACT_STORE_KEY) || {};
store.set(WRITE_CONTRACT_STORE_KEY, {
...localStore,
current: value
});
if (compile) {
this.handleCompile();
} else {
this.debouncedCompile();
}
}
@action handleSaveContract = () => {
if (this.selectedContract && this.selectedContract.id !== undefined) {
return this.handleSaveNewContract({
...this.selectedContract,
sourcecode: this.sourcecode
});
}
return this.handleOpenSaveModal();
}
getId (contracts) {
return Object.values(contracts)
.map((c) => c.id)
.reduce((max, id) => Math.max(max, id), 0) + 1;
}
@action handleSaveNewContract = (data) => {
const { name, sourcecode, id } = data;
const localStore = store.get(WRITE_CONTRACT_STORE_KEY) || {};
const savedContracts = localStore.saved || {};
const cId = (id !== undefined)
? id
: this.getId(savedContracts);
store.set(WRITE_CONTRACT_STORE_KEY, {
...localStore,
saved: {
...savedContracts,
[ cId ]: { sourcecode, id: cId, name, timestamp: Date.now() }
}
});
this.reloadContracts(cId);
}
@action reloadContracts = (id, sourcecode) => {
const localStore = store.get(WRITE_CONTRACT_STORE_KEY) || {};
this.savedContracts = localStore.saved || {};
const cId = id !== undefined ? id : localStore.currentId;
this.selectedContract = this.savedContracts[cId] || {};
this.sourcecode = sourcecode !== undefined
? sourcecode
: this.selectedContract.sourcecode || localStore.current || '';
store.set(WRITE_CONTRACT_STORE_KEY, {
...localStore,
currentId: this.selectedContract ? cId : null,
current: this.sourcecode
});
this.handleCompile();
this.resizeEditor();
}
@action handleLoadContract = (contract) => {
const { sourcecode, id } = contract;
this.reloadContracts(id, sourcecode);
}
@action handleDeleteContract = (id) => {
const localStore = store.get(WRITE_CONTRACT_STORE_KEY) || {};
const savedContracts = Object.assign({}, localStore.saved || {});
if (savedContracts[id]) {
delete savedContracts[id];
}
store.set(WRITE_CONTRACT_STORE_KEY, {
...localStore,
saved: savedContracts
});
this.reloadContracts();
}
@action handleNewContract = () => {
this.reloadContracts(-1, '');
}
@action resizeEditor = () => {
try {
this.editor.refs.brace.editor.resize();
} catch (e) {}
}
sendFilesToWorker = () => {
const files = [].concat(
Object.values(this.snippets),
Object.values(this.savedContracts)
);
this.compiler.postMessage(JSON.stringify({
action: 'setFiles',
data: files
}));
}
}