Fiddle file stored in filesystem

Fiddles are now stored in the filesystem so they can be preserved across page reloads, but also so that contracts deployed via fiddles can have their source code shown.

Fiddles deployed as contracts now fully work with existing contract list UI and functionality. Fiddle deployed contracts are listed in a separate section in the UI.

Current limitation is that only single contracts per file are supported. If the fiddle contains multiple contracts, it’s currently not supported.
This commit is contained in:
emizzle 2018-08-27 12:49:01 +10:00 committed by Pascal Precht
parent 59d3a3be83
commit cba4e9fb91
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
13 changed files with 124 additions and 27 deletions

View File

@ -179,11 +179,18 @@ 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.contractNames]});
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: (source) => action(FIDDLE_FILE[SUCCESS], {fiddleFiles: [{source, filename: 'temp'}]}),
failure: (error) => action(FIDDLE_FILE[FAILURE], {error})
};
// Web Socket
export const WATCH_NEW_PROCESS_LOGS = 'WATCH_NEW_PROCESS_LOGS';
export const WATCH_NEW_CONTRACT_LOGS = 'WATCH_NEW_CONTRACT_LOGS';

View File

@ -108,6 +108,10 @@ export function fetchContractFile(payload) {
return get('/files/contracts', {params: payload});
}
export function fetchLastFiddle() {
return get('/files/lastfiddle', {params: 'temp'});
}
export function listenToChannel(channel) {
return new WebSocket(`${constants.wsEndpoint}/communication/listenTo/${channel}`);
}

View File

@ -9,8 +9,8 @@ import {
import {Link} from 'react-router-dom';
import {formatContractForDisplay} from '../utils/presentation';
const Contracts = ({contracts}) => (
<Page.Content title="Contracts">
const Contracts = ({contracts, title = "Contracts"}) => (
<Page.Content title={title}>
<Grid.Row>
<Grid.Col>
<Card>
@ -48,7 +48,8 @@ const Contracts = ({contracts}) => (
);
Contracts.propTypes = {
contracts: PropTypes.arrayOf(PropTypes.object)
contracts: PropTypes.array,
title: PropTypes.string
};
export default Contracts;

View File

@ -116,7 +116,7 @@ class FiddleResults extends Component {
<Card.Body>
<Dimmer active={isLoading ? "active" : ""} loader>
<Button
to={`/embark/contracts/${deployedContracts[0]}/overview`}
to={`/embark/contracts/${deployedContracts}/overview`}
RootComponent={NavLink}
>Play with my contract(s)</Button>
</Dimmer>
@ -153,7 +153,7 @@ FiddleResults.propTypes = {
fatalFiddle: PropTypes.string,
fatalFiddleDeploy: PropTypes.string,
isLoading: PropTypes.bool,
deployedContracts: PropTypes.object
deployedContracts: PropTypes.string
};
export default FiddleResults;

View File

@ -11,7 +11,7 @@ import {getContract, getContractFile} from "../reducers/selectors";
class ContractSourceContainer extends Component {
componentDidMount() {
this.props.fetchContractFile(this.props.contract.originalFilename);
this.props.fetchContractFile(this.props.contract.filename);
}
render() {
@ -27,7 +27,7 @@ class ContractSourceContainer extends Component {
function mapStateToProps(state, props) {
const contract = getContract(state, props.match.params.contractName);
const contractFile = getContractFile(state, contract.originalFilename);
const contractFile = getContractFile(state, contract.filename);
return {
contract,

View File

@ -5,7 +5,7 @@ import {contracts as contractsAction} from "../actions";
import Contracts from '../components/Contracts';
import DataWrapper from "../components/DataWrapper";
import {getContracts} from "../reducers/selectors";
import {getContracts, getFiddleContracts} from "../reducers/selectors";
class ContractsContainer extends Component {
componentDidMount() {
@ -14,19 +14,31 @@ class ContractsContainer extends Component {
render() {
return (
<DataWrapper shouldRender={this.props.contracts.length > 0} {...this.props} render={({contracts}) => (
<Contracts contracts={contracts} />
)} />
<React.Fragment>
<DataWrapper shouldRender={this.props.contracts.length > 0} {...this.props} render={({contracts}) => (
<Contracts contracts={contracts} />
)} />
<DataWrapper shouldRender={this.props.fiddleContracts.length > 0} {...this.props} render={({fiddleContracts}) => (
<React.Fragment>
<Contracts contracts={fiddleContracts} title="Fiddle contracts" />
</React.Fragment>
)} />
</React.Fragment>
);
}
}
function mapStateToProps(state) {
return {contracts: getContracts(state), error: state.errorMessage, loading: state.loading};
return {
contracts: getContracts(state),
fiddleContracts: getFiddleContracts(state),
error: state.errorMessage,
loading: state.loading};
}
ContractsContainer.propTypes = {
contracts: PropTypes.array,
fiddleContracts: PropTypes.array,
fetchContracts: PropTypes.func
};

View File

@ -3,12 +3,12 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {fiddle as fiddleAction, fiddleDeploy as fiddleDeployAction} from '../actions';
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 {getFiddle, getFiddleDeploy, getLastFiddle} from "../reducers/selectors";
import CompilerError from "../components/CompilerError";
class FiddleContainer extends Component {
@ -24,6 +24,16 @@ class FiddleContainer extends Component {
this.editor = null;
}
componentDidMount() {
this.props.fetchLastFiddle();
}
componentDidUpdate(prevProps){
if(prevProps.lastFiddle !== this.props.lastFiddle){
this._onCodeChange(this.props.lastFiddle);
}
}
_onCodeChange(newValue) {
this.setState({value: newValue});
if (this.compileTimeout) clearTimeout(this.compileTimeout);
@ -82,7 +92,7 @@ class FiddleContainer extends Component {
}
render() {
const {fiddle, loading, fiddleError, fiddleDeployError, deployedContracts} = this.props;
const {fiddle, loading, fiddleError, fiddleDeployError, deployedContracts, lastFiddle} = this.props;
const {loadingMessage} = this.state;
let renderings = [];
let warnings = [];
@ -104,7 +114,7 @@ class FiddleContainer extends Component {
onDeployClick={(e) => this._onDeployClick(e)}
/>
<Fiddle
value={this.state.value}
value={this.state.value || lastFiddle}
onCodeChange={(n) => this._onCodeChange(n)}
errors={errors}
warnings={warnings}
@ -142,11 +152,13 @@ class FiddleContainer extends Component {
function mapStateToProps(state) {
const fiddle = getFiddle(state);
const deployedFiddle = getFiddleDeploy(state);
const lastFiddle = getLastFiddle(state);
return {
fiddle: fiddle.data,
deployedContracts: deployedFiddle.data,
fiddleError: fiddle.error,
fiddleDeployError: deployedFiddle.error,
lastFiddle: lastFiddle ? lastFiddle.source : '',
loading: state.loading
};
}
@ -158,13 +170,16 @@ FiddleContainer.propTypes = {
loading: PropTypes.bool,
postFiddle: PropTypes.func,
postFiddleDeploy: PropTypes.func,
deployedContracts: PropTypes.object
deployedContracts: PropTypes.string,
fetchLastFiddle: PropTypes.func,
lastFiddle: PropTypes.string
};
export default connect(
mapStateToProps,
{
postFiddle: fiddleAction.post,
postFiddleDeploy: fiddleDeployAction.post
postFiddleDeploy: fiddleDeployAction.post,
fetchLastFiddle: fiddleFileAction.request
},
)(FiddleContainer);

View File

@ -22,6 +22,7 @@ const entitiesDefaultState = {
messageChannels: [],
fiddles: [],
fiddleDeploys: [],
fiddleFiles: [],
versions: [],
plugins: [],
ensRecords: []

View File

@ -53,7 +53,11 @@ export function getContractLogsByContract(state, contractName) {
}
export function getContracts(state) {
return state.entities.contracts;
return state.entities.contracts.filter(contract => !contract.isFiddle);
}
export function getFiddleContracts(state) {
return state.entities.contracts.filter(contract => contract.isFiddle);
}
export function getContract(state, contractName) {
@ -118,6 +122,10 @@ export function getFiddleDeploy(state) {
};
}
export function getLastFiddle(state) {
return state.entities.fiddleFiles.find((fiddleFile => fiddleFile.filename === 'temp'));
}
export function getEnsRecords(state) {
return state.entities.ensRecords;
}

View File

@ -5,7 +5,8 @@ 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} = actions;
fiddleDeploy, ensRecord, ensRecords, contractLogs, contractFile, contractFunction, contractDeploy,
fiddleFile} = actions;
function *doRequest(entity, apiFn, payload) {
const {response, error} = yield call(apiFn, payload);
@ -32,6 +33,7 @@ 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);
@ -96,6 +98,10 @@ 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);
}
@ -223,6 +229,7 @@ export default function *root() {
fork(watchFetchTransaction),
fork(watchPostFiddle),
fork(watchPostFiddleDeploy),
fork(watchFetchLastFiddle),
fork(watchFetchEnsRecord),
fork(watchPostEnsRecords)
]);

View File

@ -76,7 +76,8 @@ class ContractsManager {
className: contract.className,
deploy: contract.deploy,
error: contract.error,
address: contract.deployedAddress
address: contract.deployedAddress,
isFiddle: Boolean(contract.isFiddle)
});
i += 1;
}
@ -181,11 +182,12 @@ class ContractsManager {
const builtContracts = _.pick(self.contracts, contractNames);
// for each contract, deploy (in parallel)
async.eachOf(builtContracts, (contract, contractName, callback) => {
async.eachOf(builtContracts, (contract, contractName, next) => {
contract.args = []; /* TODO: override contract.args */
contract.className = contractName;
contract.isFiddle = true;
self.events.request("deploy:contract", contract, (err) => {
callback(err);
next(err);
});
}, (err) => {
let responseData = {};

View File

@ -24,6 +24,13 @@ class Pipeline {
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"});
@ -39,6 +46,17 @@ class Pipeline {
self.events.request('files:contract', req.query.filename, res.send.bind(res));
}
);
plugin.registerAPICall(
'get',
'/embark-api/files/lastfiddle',
(req, res) => {
fs.readFile(fs.dappPath('.embark/fiddles/temp.sol'), 'utf8', (err, source) => {
if (err) return res.send({error: err});
res.send(source);
});
}
);
}
build({modifiedAssets}, callback) {

View File

@ -23,8 +23,10 @@ class Solidity {
const input = {'fiddle': {content: req.body.code.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 filePath = `.embark/fiddles/${Object.keys(compilationResult).join('_')}.sol`;
fs.writeFile(filePath, req.body.code, 'utf8'); // async, do not need to wait
const className = !compilationResult ? 'temp' : Object.keys(compilationResult).join('_');
this._writeFiddleToFileAsync(req.body.code, 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};
this.logger.trace(`POST response /embark-api/contract/compile:\n ${JSON.stringify(responseData)}`);
@ -34,6 +36,26 @@ class Solidity {
);
}
_writeFiddleToFileAsync(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) {
@ -128,7 +150,7 @@ class Solidity {
const className = contractName;
let filename = contractFile;
if(filename === 'fiddle') filename = `.embark/fiddles/${className}.sol`;
if(filename === 'fiddle') filename = Solidity._getFiddlePath(className);
compiled_object[className] = {};
compiled_object[className].code = contract.evm.bytecode.object;