openethereum/js/packages/dapp-develop/store.js

573 lines
14 KiB
JavaScript
Raw Normal View History

// Copyright 2015-2017 Parity Technologies (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 { debounce } from 'lodash';
import { action, computed, observable, transaction } from 'mobx';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import store from 'store';
2017-04-21 11:40:22 +02:00
import { sha3 } from '@parity/api/util/sha3';
import SolidityUtils from '@parity/shared/util/solidity';
const SOLIDITY_LIST_URL = 'https://rawgit.com/ethereum/solc-bin/gh-pages/bin/list.json';
2017-04-24 13:21:22 +02:00
const WRITE_CONTRACT_STORE_KEY = '_parity::contractDevelop';
const SNIPPETS = {
snippet0: {
name: 'Token.sol',
description: (
<FormattedMessage
id='writeContract.type.standardErc20'
defaultMessage='Standard ERC20 Token Contract'
/>
),
id: 'snippet0',
sourcecode: require('raw-loader!@parity/shared/contracts/snippets/token.sol')
},
snippet1: {
name: 'StandardToken.sol',
description: (
<FormattedMessage
id='writeContract.type.implementErc20'
defaultMessage='Implementation of ERC20 Token Contract'
/>
),
id: 'snippet1',
sourcecode: require('raw-loader!@parity/shared/contracts/snippets/standard-token.sol')
},
snippet2: {
name: 'HumanStandardToken.sol',
description: (
<FormattedMessage
id='writeContract.type.humanErc20'
defaultMessage='Implementation of the Human Token Contract'
/>
),
id: 'snippet2',
sourcecode: require('raw-loader!@parity/shared/contracts/snippets/human-standard-token.sol')
},
snippet3: {
name: 'Wallet.sol',
description: (
<FormattedMessage
id='writeContract.type.multisig'
defaultMessage='Implementation of a multisig Wallet'
/>
),
id: 'snippet3',
sourcecode: require('raw-loader!@parity/shared/contracts/snippets/wallet.sol')
}
};
let instance = null;
2017-04-24 13:21:22 +02:00
export default class ContractDevelopStore {
@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;
2016-12-20 02:11:04 +01:00
@observable autocompile = false;
@observable optimize = false;
@observable showDeployModal = false;
@observable showSaveModal = false;
@observable showLoadModal = false;
@observable savedContracts = {};
@observable selectedContract = {};
@observable workerError = null;
2016-12-19 14:39:10 +01:00
loadingSolidity = false;
lastCompilation = {};
snippets = SNIPPETS;
2016-12-20 02:37:16 +01:00
worker = undefined;
2016-12-20 01:55:57 +01:00
useWorker = true;
solc = {};
constructor () {
this.debouncedCompile = debounce(this.handleCompile, 1000);
}
static get () {
if (!instance) {
2017-04-24 13:21:22 +02:00
instance = new ContractDevelopStore();
}
return instance;
}
@action setWorkerError (error) {
this.workerError = error;
}
@action setEditor (editor) {
this.editor = editor;
}
@action setWorker (worker) {
2016-12-20 02:37:16 +01:00
if (this.worker !== undefined) {
return;
}
this.worker = worker;
return Promise.all([
this.fetchSolidityVersions().then(() => this.handleCompile()),
this.reloadContracts(undefined, undefined, false)
]);
}
fetchSolidityVersions () {
return fetch(SOLIDITY_LIST_URL)
.then((response) => response.json())
.then((data) => {
const { builds, releases, latestRelease } = data;
let latestIndex = -1;
let promise = Promise.resolve();
this.builds = builds.reverse().map((build, index) => {
if (releases[build.version] === build.path) {
build.release = true;
if (build.version === latestRelease) {
build.latest = true;
promise = promise.then(() => this.loadSolidityVersion(build));
latestIndex = index;
}
}
return build;
});
this.selectedBuild = latestIndex;
return promise;
2016-12-20 01:55:57 +01:00
})
.catch((error) => {
this.setWorkerError(error);
});
}
@action handleImport = (sourcecode) => {
this.reloadContracts(-1, sourcecode);
}
@action handleSelectBuild = (event, value) => {
this.selectedBuild = value;
2016-12-20 02:37:16 +01:00
return this
.loadSolidityVersion(this.builds[value])
.then(() => this.handleCompile());
}
2016-12-20 01:55:57 +01:00
getCompiler (build) {
const { longVersion } = build;
if (!this.solc[longVersion]) {
this.solc[longVersion] = SolidityUtils
.getCompiler(build)
.then((compiler) => {
this.solc[longVersion] = compiler;
return compiler;
})
.catch((error) => {
this.setWorkerError(error);
throw error;
2016-12-20 01:55:57 +01:00
});
}
return Promise.resolve(this.solc[longVersion]);
}
@action loadSolidityVersion = (build) => {
2016-12-20 01:55:57 +01:00
if (this.worker === undefined) {
return;
2016-12-20 01:55:57 +01:00
} else if (this.worker === null) {
this.useWorker = false;
}
2016-12-19 14:39:10 +01:00
if (this.loadingSolidity) {
return this.loadingSolidity;
}
2016-12-20 02:37:16 +01:00
this.loading = true;
2016-12-20 01:55:57 +01:00
if (this.useWorker) {
this.loadingSolidity = this.worker
.postMessage({
action: 'load',
data: build
})
.then((result) => {
if (result !== 'ok') {
throw new Error('error while loading solidity: ' + result);
}
this.loadingSolidity = false;
this.loading = false;
})
.catch((error) => {
console.warn('error while loading solidity', error);
this.useWorker = false;
this.loadingSolidity = null;
return this.loadSolidityVersion(build);
});
} else {
this.loadingSolidity = this
.getCompiler(build)
.then(() => {
this.loadingSolidity = false;
this.loading = false;
return 'ok';
})
.catch((error) => {
this.setWorkerError(error);
this.loadingSolidity = false;
this.loading = false;
});
}
2016-12-19 14:39:10 +01:00
return this.loadingSolidity;
}
@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 = (event, value) => {
this.contractIndex = value;
this.contract = this.contracts[Object.keys(this.contracts)[value]];
}
2016-12-20 01:55:57 +01:00
compile = (data) => {
const { name = '' } = this.selectedContract;
2016-12-20 01:55:57 +01:00
if (this.useWorker) {
return this.worker.postMessage({
action: 'compile',
data: {
...data,
name
}
2016-12-20 01:55:57 +01:00
});
}
return new Promise((resolve, reject) => {
window.setTimeout(() => {
this
.getCompiler(data.build)
.then((compiler) => {
return SolidityUtils.compile({
...data,
name
}, compiler);
2016-12-20 01:55:57 +01:00
})
.then(resolve)
.catch(reject);
}, 0);
});
}
@computed get isPristine () {
return this.getHash() === this.lastCompilation.hash;
}
getHash () {
const build = this.builds[this.selectedBuild];
const version = build.longVersion;
const sourcecode = this.sourcecode.replace(/\s+/g, ' ');
return sha3(JSON.stringify({ version, sourcecode, optimize: this.optimize }));
}
2016-12-20 02:37:16 +01:00
@action handleCompile = () => {
2016-12-20 01:55:57 +01:00
transaction(() => {
this.compiled = false;
this.compiling = true;
this.setWorkerError(null);
2016-12-20 01:55:57 +01:00
});
const build = this.builds[this.selectedBuild];
const hash = this.getHash();
let promise = Promise.resolve(null);
if (hash === this.lastCompilation.hash) {
promise = new Promise((resolve) => {
window.setTimeout(() => {
resolve(this.lastCompilation);
}, 500);
});
2016-12-20 01:55:57 +01:00
} else {
2016-12-20 02:37:16 +01:00
promise = this
.compile({
sourcecode: this.sourcecode,
2016-12-20 02:37:16 +01:00
build: build,
optimize: this.optimize,
files: this.files
})
.then((data) => {
const result = this.parseCompiled(data);
this.lastCompilation = {
result: result,
date: new Date(),
version: data.version,
hash
};
return this.lastCompilation;
})
.catch((error) => {
this.setWorkerError(error);
});
}
2016-12-19 14:39:10 +01:00
return promise.then((data = null) => {
if (data) {
const {
contract, contractIndex,
annotations, contracts, errors
} = data.result;
if (!contract && errors && errors.length > 0) {
this.setWorkerError(errors[0]);
} else {
this.contract = contract;
this.contractIndex = contractIndex;
this.annotations = annotations;
this.contracts = contracts;
this.errors = errors;
}
2016-12-19 14:39:10 +01:00
}
this.compiled = true;
this.compiling = false;
});
}
2016-12-20 02:11:04 +01:00
@action handleAutocompileToggle = () => {
this.autocompile = !this.autocompile;
}
@action handleOptimizeToggle = () => {
this.optimize = !this.optimize;
}
@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
);
const contractKeys = Object.keys(contracts || {});
const contract = contractKeys.length ? contracts[contractKeys[0]] : null;
const contractIndex = contractKeys.length ? 0 : -1;
return {
contract, contractIndex,
contracts, errors, annotations
};
}
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 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();
2016-12-20 02:11:04 +01:00
} else if (this.autocompile) {
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, recompile = true) => {
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.resizeEditor();
if (recompile) {
return this.handleCompile();
}
}
@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) {}
}
2016-12-20 02:38:40 +01:00
get files () {
const files = [].concat(
Object.values(this.snippets),
Object.values(this.savedContracts)
);
2016-12-20 02:37:16 +01:00
return files;
}
}