// Copyright 2015, 2016 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 . import { action, observable } from 'mobx'; import store from 'store'; import { debounce } from 'lodash'; import { sha3 } from '~/api/util/sha3'; const WRITE_CONTRACT_STORE_KEY = '_parity::writeContractStore'; const SNIPPETS = { snippet0: { name: 'Token.sol', description: 'Standard ERP20 Token Contract', id: 'snippet0', sourcecode: require('raw-loader!../../contracts/snippets/token.sol') }, snippet1: { name: 'StandardToken.sol', description: 'Implementation of ERP20 Token Contract', id: 'snippet1', sourcecode: require('raw-loader!../../contracts/snippets/standard-token.sol') }, snippet2: { name: 'HumanStandardToken.sol', description: 'Implementation of the Human Token Contract', id: 'snippet2', sourcecode: require('raw-loader!../../contracts/snippets/human-standard-token.sol') }, snippet3: { name: 'Wallet.sol', description: 'Implementation of a multisig Wallet', id: 'snippet3', sourcecode: require('raw-loader!../../contracts/snippets/wallet.sol') } }; let instance = null; 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 = {}; @observable workerError = null; loadingSolidity = false; lastCompilation = {}; snippets = SNIPPETS; worker = null; constructor () { this.debouncedCompile = debounce(this.handleCompile, 1000); } static get () { if (!instance) { instance = new WriteContractStore(); } return instance; } @action setWorkerError (error) { this.workerError = error; } @action setEditor (editor) { this.editor = editor; } @action setWorker (worker) { this.worker = worker; this .fetchSolidityVersions() .then(() => this.reloadContracts()); } fetchSolidityVersions () { return 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; 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; }); } @action handleImport = (sourcecode) => { this.reloadContracts(-1, sourcecode); } @action handleSelectBuild = (_, index, value) => { this.selectedBuild = value; return this.loadSolidityVersion(this.builds[value]); } @action loadSolidityVersion = (build) => { if (!this.worker) { return; } if (this.loadingSolidity) { return this.loadingSolidity; } this.loadingSolidity = this.worker .postMessage({ action: 'load', data: build }) .then((result) => { if (result !== 'ok') { this.setWorkerError(result); } }) .catch((error) => { this.setWorkerError(error); }) .then(() => { this.loadingSolidity = false; this.loading = false; }); 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 = (_, index, value) => { this.contractIndex = value; this.contract = this.contracts[Object.keys(this.contracts)[value]]; } @action handleCompile = (loadFiles = false) => { this.compiled = false; this.compiling = true; const build = this.builds[this.selectedBuild]; const version = build.longVersion; const sourcecode = this.sourcecode.replace(/\n+/g, '\n').replace(/\s(\s+)/g, ' '); const hash = sha3(JSON.stringify({ version, sourcecode })); let promise = Promise.resolve(null); if (hash === this.lastCompilation.hash) { promise = new Promise((resolve) => { window.setTimeout(() => { resolve(this.lastCompilation); }, 500); }); } else if (this.worker) { promise = loadFiles ? this.sendFilesToWorker() : Promise.resolve(); promise = promise .then(() => { return this.worker.postMessage({ action: 'compile', data: { sourcecode: sourcecode, build: build } }); }) .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); }); } return promise.then((data = null) => { if (data) { const { contract, contractIndex, annotations, contracts, errors } = data.result; this.contract = contract; this.contractIndex = contractIndex; this.annotations = annotations; this.contracts = contracts; this.errors = errors; } this.compiled = true; this.compiling = false; }); } @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 ); 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(); } 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.resizeEditor(); // Send the new files to the Worker and compile return this.handleCompile(true); } @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) ); return this.worker.postMessage({ action: 'setFiles', data: files }); } }