├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.js ├── ide.png ├── jsconfig.json ├── package.json ├── public ├── favicon.ico └── index.html ├── server.ts ├── src ├── App.vue ├── assets │ ├── global.styl │ └── tailwind.css ├── components │ ├── Compiler.vue │ ├── Deployer.vue │ ├── Editor.vue │ ├── FileManager.vue │ ├── Menubar.vue │ ├── Statusbar.vue │ ├── Terminal.vue │ ├── Toolbar.vue │ ├── index.js │ └── template.vue ├── main.ts ├── plugins │ ├── element-ui.ts │ └── resolve-github.ts ├── router │ └── index.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── store │ ├── editor.ts │ ├── index.ts │ ├── storage.ts │ └── type.ts ├── utils │ ├── antenna.ts │ ├── constant.ts │ ├── directives.ts │ ├── eventBus.ts │ ├── filter.ts │ ├── fs.ts │ ├── helper.ts │ ├── index.ts │ ├── lodash.ts │ ├── sharefolder.ts │ ├── solc.ts │ ├── vm.ts │ └── ws-plugin.ts └── views │ └── Home.vue ├── tsconfig.json ├── tslint.json └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | *lock.json 24 | local 25 | package-lock.json 26 | yarn.lock 27 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 200, 3 | "bracketSpacing": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## Project setup 6 | ``` 7 | yarn install 8 | ``` 9 | 10 | ### Compiles and hot-reloads for development 11 | ``` 12 | yarn run dev 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | ``` 17 | yarn run build 18 | ``` 19 | 20 | ### Run your tests 21 | ``` 22 | yarn run test 23 | ``` 24 | 25 | ### Lints and fixes files 26 | ``` 27 | yarn run lint 28 | ``` 29 | 30 | ### Customize configuration 31 | See [Configuration Reference](https://cli.vuejs.org/config/). 32 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | plugins: [ 4 | [ 5 | "component", 6 | { 7 | libraryName: "element-ui", 8 | styleLibraryName: "theme-chalk" 9 | } 10 | ] 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /ide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotexproject/iotex-studio/4d6c350499c305394781ce04b1fdcb5833e27037/ide.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iotex-editor", 3 | "version": "0.0.2", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "pretty": "pretty-quick", 10 | "start": "ts-node -T ./server.ts" 11 | }, 12 | "husky": { 13 | "hooks": { 14 | "pre-commit": "pretty-quick --staged" 15 | } 16 | }, 17 | "dependencies": { 18 | "@types/axios": "^0.14.0", 19 | "@types/cors": "^2.8.7", 20 | "@types/express-http-proxy": "^1.6.1", 21 | "@types/parse-github-url": "^1.0.0", 22 | "@types/prettier": "^2.0.1", 23 | "ace-mode-solidity": "^0.1.1", 24 | "axios": "^0.19.2", 25 | "brace": "^0.11.1", 26 | "browserfs": "^1.4.3", 27 | "comlink": "^4.3.0", 28 | "comlink-fetch": "^0.1.2", 29 | "core-js": "^3.3.2", 30 | "cors": "^2.8.5", 31 | "element-ui": "^2.12.0", 32 | "ethereumjs-abi": "^0.6.8", 33 | "ethereumjs-account": "^3.0.0", 34 | "ethereumjs-tx": "^2.1.1", 35 | "ethereumjs-util": "^7.0.2", 36 | "ethereumjs-vm": "^4.1.1", 37 | "ethers": "^5.0.2", 38 | "express": "^4.17.1", 39 | "express-http-proxy": "^1.6.2", 40 | "express-http-to-https": "^1.1.4", 41 | "fs.promises": "^0.1.2", 42 | "helpful-decorators": "^2.0.5", 43 | "http-proxy-middleware": "^1.0.5", 44 | "iotex-antenna": "^0.30.0", 45 | "lodash": "^4.17.15", 46 | "module": "^1.2.5", 47 | "parse-github-url": "^1.0.2", 48 | "prettier": "^2.0.5", 49 | "prettier-plugin-solidity": "^1.0.0-alpha.36", 50 | "prettier-standalone": "^1.3.1-0", 51 | "promise-retry": "^2.0.1", 52 | "remix-solidity": "^0.3.31", 53 | "resolve-github": "^0.2.0", 54 | "resolve-http": "^0.2.0", 55 | "resolve-ipfs": "^0.1.0", 56 | "semver": "^7.3.2", 57 | "shrink-ray-current": "^4.1.2", 58 | "solc": "^0.6.3", 59 | "solc-js": "^1.0.1", 60 | "solc-resolver": "^0.2.3", 61 | "solc-version": "^0.3.1", 62 | "solcjs-core-fix": "^0.7.0", 63 | "ts-node": "^8.10.2", 64 | "tslib": "^2.0.0", 65 | "typescript": "^3.9.5", 66 | "v-contextmenu": "^2.8.1", 67 | "vue": "^2.6.10", 68 | "vue-clipboard2": "^0.3.1", 69 | "vue-router": "^3.1.3", 70 | "vue-split-panel": "^1.0.4", 71 | "vuex": "^3.0.1", 72 | "vuex-class-modules": "^1.1.2", 73 | "vuex-pathify": "^1.4.0", 74 | "vuex-persist": "^2.2.0", 75 | "websocket-as-promised": "^1.0.1" 76 | }, 77 | "devDependencies": { 78 | "@types/ace": "0.0.43", 79 | "@types/ethereumjs-abi": "^0.6.3", 80 | "@types/ethereumjs-tx": "^2.0.0", 81 | "@types/ethereumjs-util": "^6.1.0", 82 | "@types/express": "^4.17.2", 83 | "@types/fs-promise": "^2.0.0", 84 | "@types/lodash": "^4.14.149", 85 | "@types/lodash-webpack-plugin": "^0.11.3", 86 | "@types/promise-retry": "^1.1.3", 87 | "@types/pug": "^2.0.4", 88 | "@types/vue-splitpane": "^1.0.0", 89 | "@vue/cli-plugin-babel": "^4.0.0", 90 | "@vue/cli-plugin-typescript": "^4.0.0", 91 | "@vue/cli-service": "^4.0.0", 92 | "babel-plugin-component": "^1.1.1", 93 | "comlink-loader": "^2.0.0", 94 | "husky": "^4.0.10", 95 | "pretty-quick": "^2.0.1", 96 | "pug": "^3.0.0", 97 | "pug-loader": "^2.4.0", 98 | "pug-plain-loader": "^1.0.0", 99 | "stylus": "^0.54.7", 100 | "stylus-loader": "^3.0.2", 101 | "tailwindcss": "^1.1.3", 102 | "terser-webpack-plugin": "^3.0.5", 103 | "tslint-config-prettier": "^1.18.0", 104 | "tslint-plugin-prettier": "^2.0.1", 105 | "typed-emitter": "^1.2.0", 106 | "vue-cli-plugin-element-ui": "^1.1.4", 107 | "vue-property-decorator": "^8.3.0", 108 | "vue-runtime-helpers": "^1.1.2", 109 | "vue-template-compiler": "^2.6.10" 110 | }, 111 | "postcss": { 112 | "plugins": { 113 | "autoprefixer": {}, 114 | "tailwindcss": {} 115 | } 116 | }, 117 | "browserslist": [ 118 | "> 1%", 119 | "last 2 versions" 120 | ] 121 | } 122 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotexproject/iotex-studio/4d6c350499c305394781ce04b1fdcb5833e27037/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | IoTeX-Studio 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import serveStatic from "serve-static"; 3 | import compression from "shrink-ray-current"; 4 | import { redirectToHTTPS } from "express-http-to-https"; 5 | import axios from "axios"; 6 | import cors from "cors"; 7 | 8 | const app = express(); 9 | 10 | app 11 | .use(cors()) 12 | .use(redirectToHTTPS([/localhost:(\d{4})/], [/\/insecure/], 301)) 13 | .use(compression()) 14 | .use(serveStatic(__dirname + "/dist", { maxAge: 86400 * 1000 })); 15 | 16 | app.get("/bin/:version", async (req, res, next) => { 17 | const version = req.params["version"]; 18 | axios.get(`https://solc-bin.ethereum.org/bin/${version}`, { responseType: "stream" }).then((response) => { 19 | res.set({ 20 | ...response.headers, 21 | "Access-Control-Allow-Origin": "*", 22 | "Access-Control-Allow-Methods": "PUT,POST,GET,DELETE,OPTIONS", 23 | "Cache-Control": "public, max-age=31557600", 24 | }); 25 | response.data.pipe(res); 26 | }); 27 | }); 28 | 29 | var port = process.env.PORT || 5000; 30 | app.listen(port); 31 | console.log("server started " + port); 32 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 115 | -------------------------------------------------------------------------------- /src/assets/global.styl: -------------------------------------------------------------------------------- 1 | color-primary = #06BFC1 2 | color-dark = #2d2d2d 3 | color-dark-text = rgb(190, 188, 183) 4 | color-dark-input-border = lighten(color-dark, 20%) 5 | color-dark-border = darken(color-dark, 15%) 6 | ::-webkit-scrollbar 7 | background-color darken(color-dark, 10%) 8 | width 8px 9 | background-clip padding-box 10 | ::-webkit-scrollbar-thumb 11 | background-color lighten(color-dark, 10%) 12 | border 1px solid lighten(color-dark, 20%) 13 | border-radius 5px 14 | ::-webkit-scrollbar-corner 15 | background-color darken(color-dark, 10%) 16 | -------------------------------------------------------------------------------- /src/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/components/Compiler.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 165 | -------------------------------------------------------------------------------- /src/components/Deployer.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 504 | 505 | 514 | -------------------------------------------------------------------------------- /src/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 97 | -------------------------------------------------------------------------------- /src/components/FileManager.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 372 | 373 | 384 | -------------------------------------------------------------------------------- /src/components/Menubar.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | 44 | 85 | -------------------------------------------------------------------------------- /src/components/Statusbar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/components/Terminal.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 76 | 77 | 98 | -------------------------------------------------------------------------------- /src/components/Toolbar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 61 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | import Editor from "./Editor.vue"; 4 | import Compiler from "./Compiler.vue"; 5 | import Deployer from "./Deployer.vue"; 6 | import FileManager from "./FileManager.vue"; 7 | import Terminal from "./Terminal.vue"; 8 | import Toolbar from "./Toolbar.vue"; 9 | import Menubar from "./Menubar.vue"; 10 | import StatusBar from "./Statusbar.vue"; 11 | 12 | Vue.component("editor", Editor); 13 | Vue.component("compiler", Compiler); 14 | Vue.component("deployer", Deployer); 15 | Vue.component("file-manager", FileManager); 16 | Vue.component("terminal", Terminal); 17 | Vue.component("toolbar", Toolbar); 18 | Vue.component("menubar", Menubar); 19 | Vue.component("statusbar", StatusBar); 20 | -------------------------------------------------------------------------------- /src/components/template.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import "./components"; 6 | import "./utils/filter"; 7 | import "./utils/directives"; 8 | import { _ } from "./utils/lodash"; 9 | 10 | import "./plugins/element-ui"; 11 | 12 | import VueClipboard from "vue-clipboard2"; 13 | Vue.use(VueClipboard); 14 | 15 | import contentmenu from "v-contextmenu"; 16 | Vue.use(contentmenu); 17 | 18 | import VueSplit from "vue-split-panel"; 19 | import { eventBus } from "./utils/eventBus"; 20 | Vue.use(VueSplit); 21 | 22 | // Vue.config.errorHandler = (err, vm, info) => { 23 | // console.log({ err, vm, info }); 24 | // // eventBus.emit("term.error", { 25 | // // text: "err" 26 | // // }); 27 | // }; 28 | 29 | Vue.prototype.$_ = _; 30 | 31 | Vue.config.productionTip = false; 32 | new Vue({ 33 | router, 34 | store, 35 | render: (h) => h(App), 36 | }).$mount("#app"); 37 | -------------------------------------------------------------------------------- /src/plugins/element-ui.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { Button, Select, Option, Form, FormItem, Icon, Divider, Tree, Tabs, Alert, Dialog, Input, Menu, MenuItem, Submenu, Message, TabPane, MessageBox, Loading, Checkbox } from "element-ui"; 3 | 4 | import lang from "element-ui/lib/locale/lang/en"; 5 | import locale from "element-ui/lib/locale"; 6 | locale.use(lang); 7 | 8 | Vue.use(Button) 9 | .use(Select) 10 | .use(Option) 11 | .use(Form) 12 | .use(FormItem) 13 | .use(Icon) 14 | .use(Divider) 15 | .use(Tree) 16 | .use(Alert) 17 | .use(Dialog) 18 | .use(Input) 19 | .use(Menu) 20 | .use(MenuItem) 21 | .use(Submenu) 22 | .use(Tabs) 23 | .use(TabPane) 24 | .use(Loading) 25 | .use(Checkbox); 26 | 27 | Vue.prototype.$alert = Alert; 28 | Vue.prototype.$message = Message; 29 | Vue.prototype.$msgbox = MessageBox; 30 | Vue.prototype.$confirm = MessageBox.confirm; 31 | -------------------------------------------------------------------------------- /src/plugins/resolve-github.ts: -------------------------------------------------------------------------------- 1 | // ref: https://github.com/alincode/resolve-github/blob/master/src/resolver.js 2 | import { replaceContent } from "solc-import"; 3 | import gh from "parse-github-url"; 4 | 5 | const match = /^(https?:\/\/)?(www.)?github.com\/([^/]*\/[^/]*)\/(.*)/; 6 | 7 | const resolver = (content, from, subImportPath) => { 8 | let newContent = content; 9 | let url = new window.URL(subImportPath, from); 10 | let fixedPath = url.href; 11 | newContent = newContent.replace(`import '${subImportPath}'`, `import '${fixedPath}'`); 12 | newContent = newContent.replace(`import "${subImportPath}"`, `import "${fixedPath}"`); 13 | return newContent; 14 | }; 15 | 16 | const parser = async function (importPath) { 17 | const { owner, name, repo, branch, filepath } = gh(importPath); 18 | 19 | let url = `https://raw.githubusercontent.com/${repo}/${branch}/${filepath}`; 20 | try { 21 | let data = await getData(url); 22 | if (isSymbolicLink(data)) { 23 | const tmps = url.split("/"); 24 | const filename = tmps[tmps.length - 1]; 25 | url = url.replace(filename, data); 26 | data = await getData(url); 27 | } else { 28 | data = replaceContent(data, importPath, resolver); 29 | } 30 | return data; 31 | } catch (error) { 32 | throw error; 33 | } 34 | }; 35 | 36 | async function getData(url) { 37 | let response = await fetch(url, { method: "GET" }); 38 | let data = await response.text(); 39 | if (!response.ok || response.status !== 200) throw Error("Content " + data); 40 | return data; 41 | } 42 | 43 | function isSymbolicLink(data) { 44 | if (data.length < 50 && data.indexOf(".sol")) return true; 45 | return false; 46 | } 47 | 48 | // async function getSource(importPath, root, path) { 49 | // const url = `https://api.github.com/repos/${root}/contents/${path}`; 50 | // // console.log('url:', url); 51 | // try { 52 | // const response = await fetch(url, { method: 'GET' }); 53 | // let data = await response.text(); 54 | // if (!response.ok || response.status !== 200) throw Error(data); 55 | // data = JSON.parse(data); 56 | // data.content = window.atob(data.content); 57 | // data.content = replaceContent(data.content, importPath, pathResolve); 58 | // if ('content' in data) return data.content; 59 | // if ('message' in data) throw Error(data.message); 60 | // throw Error('Content not received'); 61 | // } catch (error) { 62 | // // Unknown transport error 63 | // throw error; 64 | // } 65 | // } 66 | 67 | export default { 68 | type: "github", 69 | parser, 70 | resolver, 71 | match, 72 | }; 73 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import Home from "../views/Home.vue"; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const routes = [ 8 | { 9 | path: "/", 10 | name: "home", 11 | component: Home 12 | } 13 | ]; 14 | 15 | const router = new VueRouter({ 16 | mode: "history", 17 | base: process.env.BASE_URL, 18 | routes 19 | }); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | declare global { 3 | namespace JSX { 4 | // tslint:disable no-empty-interface 5 | interface Element extends VNode {} 6 | // tslint:disable no-empty-interface 7 | interface ElementClass extends Vue {} 8 | interface ElementAttributesProperty { 9 | $props: any; // specify the property name to use 10 | } 11 | interface IntrinsicElements { 12 | [elem: string]: any; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | import { _ } from "./utils/lodash"; 2 | 3 | declare module "*.vue" { 4 | import Vue from "vue"; 5 | 6 | export default Vue; 7 | } 8 | 9 | declare module "vue/types/vue" { 10 | interface Vue { 11 | $_: _; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/store/editor.ts: -------------------------------------------------------------------------------- 1 | import { make } from "vuex-pathify"; 2 | import * as constant from "../utils/constant"; 3 | import store from "@/store"; 4 | import { FS } from "../utils/fs"; 5 | import ace from "brace"; 6 | import { CompiledContract } from "./type"; 7 | import { _ } from "../utils/lodash"; 8 | import semver from "semver"; 9 | 10 | const state: { 11 | fileManager: { 12 | file?: FS["file"]; 13 | files: { 14 | [key: string]: FS["file"]; 15 | }; 16 | filesLoaded: FS["files"]; 17 | defaultFiles: { path: string; content: string; ensure: boolean }[]; 18 | }; 19 | ace: { 20 | editor: ace.Editor; 21 | }; 22 | solc: { 23 | loading: boolean; 24 | compileLoading: boolean; 25 | compiler: any; 26 | compileResult: Record; 27 | currentContractName: string; 28 | versions: { 29 | builds: { build: string; keccak256: string; longVersion: string; path: string; urls: string[] }[]; 30 | latestRelease: string; 31 | releases: { [key: string]: string }; 32 | }; 33 | }; 34 | } = { 35 | ace: { 36 | editor: null, 37 | }, 38 | fileManager: { 39 | files: {}, 40 | filesLoaded: [], 41 | defaultFiles: [ 42 | { path: "/project/default/test.sol", content: constant.defaultContract, ensure: true }, 43 | { path: "/project/default/erc20/erc20.sol", content: constant.erc20, ensure: true }, 44 | { path: "/project/default/erc721/erc721.sol", content: constant.erc721, ensure: true }, 45 | ], 46 | }, 47 | 48 | solc: { 49 | loading: false, 50 | compileLoading: false, 51 | compiler: null, 52 | currentContractName: "", 53 | compileResult: {}, 54 | versions: { 55 | builds: [], 56 | latestRelease: "", 57 | releases: {}, 58 | }, 59 | }, 60 | }; 61 | 62 | export type EditorStore = typeof state; 63 | 64 | const getters: { 65 | [key: string]: (state: EditorStore) => any; 66 | } = { 67 | curFile: (state) => store.state.editor.fileManager.files[store.state.storage.curProject.fileManager.curFilePath], 68 | versions: (state) => { 69 | const versions = {}; 70 | _.each(state.solc.versions.releases, (v, k) => { 71 | if (semver.satisfies(k, "0.1.1 - 0.5.13")) { 72 | versions[k] = v; 73 | } 74 | }); 75 | return versions; 76 | }, 77 | }; 78 | const mutations = make.mutations(state); 79 | 80 | export default { 81 | namespaced: true, 82 | getters, 83 | mutations, 84 | state, 85 | }; 86 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import editor from "./editor"; 4 | import storage from "./storage"; 5 | 6 | import pathify, { make } from "vuex-pathify"; 7 | pathify.options.mapping = "simple"; 8 | 9 | import VuexPersist from "vuex-persist"; 10 | import { StorageStore } from "./storage"; 11 | import { EditorStore } from "./editor"; 12 | 13 | const storagePersist = new VuexPersist({ 14 | key: "iotex-studio", 15 | storage: window.localStorage, 16 | modules: ["storage"], 17 | }); 18 | 19 | const state = {}; 20 | 21 | const mutations = make.mutations(state); 22 | 23 | Vue.use(Vuex); 24 | export default new Vuex.Store<{ 25 | editor: EditorStore; 26 | storage: StorageStore; 27 | }>({ 28 | plugins: [pathify.plugin, storagePersist.plugin], 29 | //@ts-ignore 30 | state, 31 | mutations, 32 | actions: {}, 33 | modules: { 34 | editor, 35 | storage, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /src/store/storage.ts: -------------------------------------------------------------------------------- 1 | import { make } from "vuex-pathify"; 2 | import { FS } from "../utils/fs"; 3 | import { CompiledContract } from "./type"; 4 | 5 | const state: { 6 | curProject: { 7 | solc: { 8 | version: string; 9 | optimizer: boolean; 10 | }; 11 | fileManager: { 12 | curDir: string; 13 | curFilePath: string; 14 | curLinkStatus: "init" | "connecting" | "failed" | "connected"; 15 | }; 16 | toolbar: { 17 | tabs: { 18 | [key: string]: Partial; 19 | }; 20 | }; 21 | }; 22 | ace: { 23 | content: string; 24 | theme: string; 25 | lang: string; 26 | options: any; 27 | }; 28 | 29 | split: { 30 | size: { 31 | main: number[]; 32 | editor: number[]; 33 | }; 34 | }; 35 | } = { 36 | ace: { 37 | content: "", 38 | theme: "tomorrow_night_eighties", 39 | lang: "solidity", 40 | options: { 41 | enableBasicAutocompletion: true, 42 | enableLiveAutocompletion: true, 43 | // enableSnippets: true 44 | }, 45 | }, 46 | split: { 47 | size: { 48 | main: [10, 70, 20], 49 | editor: [75, 25], 50 | }, 51 | }, 52 | curProject: { 53 | solc: { 54 | version: "soljson-v0.5.5+commit.47a71e8f.js", 55 | optimizer: false, 56 | }, 57 | fileManager: { 58 | curDir: "/project/default", 59 | curFilePath: null, 60 | curLinkStatus: "init", 61 | }, 62 | toolbar: { 63 | tabs: {}, 64 | }, 65 | }, 66 | }; 67 | 68 | export type StorageStore = typeof state; 69 | 70 | const mutations = make.mutations(state); 71 | 72 | export default { 73 | namespaced: true, 74 | mutations, 75 | state, 76 | }; 77 | -------------------------------------------------------------------------------- /src/store/type.ts: -------------------------------------------------------------------------------- 1 | import ace from "brace"; 2 | import { FS } from "../utils/fs"; 3 | 4 | export interface StdoutType { 5 | text: string; 6 | component?: "alert" | "json"; 7 | data?: Object; 8 | type?: "success" | "info" | "warning" | "error"; 9 | description?: string; 10 | expanded?: boolean; 11 | } 12 | 13 | export type AbiFunc = { 14 | constant: boolean; 15 | inputs: { 16 | name: string; 17 | type: string; 18 | value?: string; 19 | }[]; 20 | outputs: { 21 | name: string; 22 | type: string; 23 | }[]; 24 | datas?: string; 25 | name: string; 26 | payable: boolean; 27 | stateMutability: string; 28 | type: string; 29 | }; 30 | 31 | export type CompiledContract = { 32 | abi: AbiFunc[]; 33 | showDetail?: boolean; 34 | assembly: Object; 35 | binary: Object; 36 | compiler: Object; 37 | metadata: Object; 38 | name: string; 39 | sources: Object; 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/antenna.ts: -------------------------------------------------------------------------------- 1 | import Antenna from "iotex-antenna"; 2 | import { WsSignerPlugin } from "./ws-plugin"; 3 | 4 | export const wsSigner = new WsSignerPlugin({ 5 | options: { 6 | packMessage: (data) => JSON.stringify(data), 7 | //@ts-ignore 8 | unpackMessage: (data) => JSON.parse(data), 9 | attachRequestId: (data, requestId) => Object.assign({ reqId: requestId }, data), 10 | extractRequestId: (data) => data && data.reqId, 11 | }, 12 | }); 13 | 14 | export class AntennaUtils { 15 | static providers = { 16 | mainnet: { name: "mainnet", url: "https://api.iotex.one:443" }, 17 | testnet: { name: "testnet", url: "https://api.testnet.iotex.one:443" }, 18 | }; 19 | 20 | static getProdiver(name: string) { 21 | if (this.providers[name]) { 22 | return this.providers[name].url; 23 | } 24 | throw new Error(`provider ${name} not exists`); 25 | } 26 | } 27 | 28 | export const antenna = new Antenna(AntennaUtils.providers.testnet.url, { 29 | signer: wsSigner, 30 | }); 31 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const erc20 = `pragma solidity ^0.5.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol"; 5 | 6 | contract TestToken is ERC20, ERC20Detailed { 7 | constructor(uint256 initialSupply) ERC20Detailed("Test", "TEST", 18) public { 8 | _mint(msg.sender, initialSupply); 9 | } 10 | }`; 11 | 12 | export const defaultContract = `pragma solidity ^0.5.0; 13 | 14 | contract Array { 15 | uint256[] public arr; 16 | 17 | constructor(uint256[] memory _arr) public { 18 | arr = _arr; 19 | } 20 | 21 | function insert(uint256 element) public { 22 | arr.push(element); 23 | } 24 | 25 | function removeOne() public { 26 | arr.pop(); 27 | } 28 | }`; 29 | 30 | export const erc721 = `pragma solidity ^0.5.0; 31 | 32 | import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol"; 33 | 34 | 35 | contract ViperToken is ERC721Full { 36 | using SafeMath for uint256; 37 | // This struct will be used to represent one viper 38 | struct Viper { 39 | uint8 genes; 40 | uint256 matronId; 41 | uint256 sireId; 42 | } 43 | 44 | // List of existing vipers 45 | Viper[] public vipers; 46 | 47 | // Event that will be emitted whenever a new viper is created 48 | event Birth( 49 | address owner, 50 | uint256 viperId, 51 | uint256 matronId, 52 | uint256 sireId, 53 | uint8 genes 54 | ); 55 | 56 | // Initializing an ERC-721 Token named 'Vipers' with a symbol 'VPR' 57 | constructor() ERC721Full("Vipers", "VPR") public { 58 | } 59 | 60 | // Fallback function 61 | function() external payable { 62 | } 63 | 64 | /** @dev Function to determine a viper's characteristics. 65 | * @param matron ID of viper's matron (one parent) 66 | * @param sire ID of viper's sire (other parent) 67 | * @return The viper's genes in the form of uint8 68 | */ 69 | function generateViperGenes( 70 | uint256 matron, 71 | uint256 sire 72 | ) 73 | internal 74 | pure 75 | returns (uint8) 76 | { 77 | return uint8(matron.add(sire)) % 6 + 1; 78 | } 79 | 80 | /** @dev Function to create a new viper 81 | * @param matron ID of new viper's matron (one parent) 82 | * @param sire ID of new viper's sire (other parent) 83 | * @param viperOwner Address of new viper's owner 84 | * @return The new viper's ID 85 | */ 86 | function createViper( 87 | uint256 matron, 88 | uint256 sire, 89 | address viperOwner 90 | ) 91 | internal 92 | returns (uint) 93 | { 94 | require(viperOwner != address(0)); 95 | uint8 newGenes = generateViperGenes(matron, sire); 96 | Viper memory newViper = Viper({ 97 | genes: newGenes, 98 | matronId: matron, 99 | sireId: sire 100 | }); 101 | uint256 newViperId = vipers.push(newViper).sub(1); 102 | super._mint(viperOwner, newViperId); 103 | emit Birth( 104 | viperOwner, 105 | newViperId, 106 | newViper.matronId, 107 | newViper.sireId, 108 | newViper.genes 109 | ); 110 | return newViperId; 111 | } 112 | 113 | /** @dev Function to allow user to buy a new viper (calls createViper()) 114 | * @return The new viper's ID 115 | */ 116 | function buyViper() external payable returns (uint256) { 117 | require(msg.value == 0.02 ether); 118 | return createViper(0, 0, msg.sender); 119 | } 120 | 121 | /** @dev Function to breed 2 vipers to create a new one 122 | * @param matronId ID of new viper's matron (one parent) 123 | * @param sireId ID of new viper's sire (other parent) 124 | * @return The new viper's ID 125 | */ 126 | function breedVipers(uint256 matronId, uint256 sireId) external payable returns (uint256) { 127 | require(msg.value == 0.05 ether); 128 | return createViper(matronId, sireId, msg.sender); 129 | } 130 | 131 | /** @dev Function to retrieve a specific viper's details. 132 | * @param viperId ID of the viper who's details will be retrieved 133 | * @return An array, [viper's ID, viper's genes, matron's ID, sire's ID] 134 | */ 135 | function getViperDetails(uint256 viperId) external view returns (uint256, uint8, uint256, uint256) { 136 | Viper storage viper = vipers[viperId]; 137 | return (viperId, viper.genes, viper.matronId, viper.sireId); 138 | } 139 | 140 | /** @dev Function to get a list of owned vipers' IDs 141 | * @return A uint array which contains IDs of all owned vipers 142 | */ 143 | function ownedVipers() external view returns(uint256[] memory) { 144 | uint256 viperCount = balanceOf(msg.sender); 145 | if (viperCount == 0) { 146 | return new uint256[](0); 147 | } else { 148 | uint256[] memory result = new uint256[](viperCount); 149 | uint256 totalVipers = vipers.length; 150 | uint256 resultIndex = 0; 151 | uint256 viperId = 0; 152 | while (viperId < totalVipers) { 153 | if (ownerOf(viperId) == msg.sender) { 154 | result[resultIndex] = viperId; 155 | resultIndex = resultIndex.add(1); 156 | } 157 | viperId = viperId.add(1); 158 | } 159 | return result; 160 | } 161 | } 162 | } 163 | `; 164 | 165 | export const defaultTypeValue = { 166 | address: "io1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqd39ym7", 167 | uint256: "0", 168 | "uint256[]": "[]", 169 | string: "", 170 | "string[]": "[]", 171 | "address[]": [], 172 | }; 173 | -------------------------------------------------------------------------------- /src/utils/directives.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | Vue.directive("focus", { 3 | inserted: function (el) { 4 | el.querySelector("input").focus(); 5 | }, 6 | }); 7 | -------------------------------------------------------------------------------- /src/utils/eventBus.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import TypedEmitter from "typed-emitter"; 3 | import ace from "brace"; 4 | import { StdoutType } from "../store/type"; 5 | import { FS } from "./fs"; 6 | import { EditorStore } from "../store/editor"; 7 | import { StorageStore } from "../store/storage"; 8 | 9 | interface MessageEvents { 10 | "editor.save": () => void; 11 | "editor.init": (editor: ace.Editor) => void; 12 | "editor.content.update": (content: string) => void; 13 | "solc.compile": () => void; 14 | "solc.compiled.finished": (result: EditorStore["solc"]["compileResult"]) => void; 15 | "solc.compiled.failed": (err: Error) => void; 16 | "term.message": (message: StdoutType) => void; 17 | "term.error": (message: StdoutType) => void; 18 | "term.success": (message: StdoutType) => void; 19 | "term.warning": (message: StdoutType) => void; 20 | "term.info": (message: StdoutType) => void; 21 | "term.messages": (messages: StdoutType[]) => void; 22 | "fs.ready": () => void; 23 | "fs.select": (file: FS["file"]) => void; 24 | "fs.loadFiles": (files: EditorStore["fileManager"]["files"]) => void; 25 | "toolbar.tab.select": (file: Partial) => void; 26 | "menubar.newFile": () => void; 27 | "menubar.newFolder": () => void; 28 | "menubar.undo": () => void; 29 | "menubar.redo": () => void; 30 | "menubar.saveAll": () => void; 31 | "sharefolder.ws.connected": () => void; 32 | "sharefolder.ws.closed": () => void; 33 | "sharefolder.ws.error": (e: Error) => void; 34 | } 35 | 36 | export const eventBus = new EventEmitter() as TypedEmitter; 37 | 38 | eventBus.on("editor.init", (editor) => { 39 | require("brace/ext/language_tools"); 40 | require("brace/mode/javascript"); 41 | require("ace-mode-solidity/build/remix-ide/mode-solidity"); 42 | require("brace/ext/searchbox"); 43 | require("brace/snippets/javascript"); 44 | editor.commands.addCommand({ 45 | name: "SaveAndCompile", 46 | bindKey: { win: "Ctrl-S", mac: "Command-S" }, 47 | exec: () => { 48 | eventBus.emit("editor.save"); 49 | }, 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/utils/filter.ts: -------------------------------------------------------------------------------- 1 | import { Vue } from "vue-property-decorator"; 2 | import { utils } from "ethers"; 3 | export const truncate = (fullStr = "", strLen, separator = "") => { 4 | if (fullStr.length <= strLen) return fullStr; 5 | 6 | separator = separator || "..."; 7 | 8 | const sepLen = separator.length; 9 | const charsToShow = strLen - sepLen; 10 | const frontChars = Math.ceil(charsToShow / 2); 11 | const backChars = Math.floor(charsToShow / 2); 12 | 13 | return fullStr.substr(0, frontChars) + separator + fullStr.substr(fullStr.length - backChars); 14 | }; 15 | 16 | Vue.filter("truncate", (val, length, separator) => truncate(val, length, separator)); 17 | Vue.filter("formatEther", val => utils.formatEther); 18 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import util from "util"; 2 | import _fs, { stat } from "fs"; 3 | import { Helper } from "./helper"; 4 | import * as path from "path"; 5 | import { app } from "./index"; 6 | 7 | export const promisify = (obj) => { 8 | return new Proxy(obj, { 9 | get: (target, name) => { 10 | if (name in target) { 11 | if (target[name] instanceof Function) { 12 | return util.promisify(target[name]); 13 | } 14 | } 15 | return target[name]; 16 | }, 17 | }); 18 | }; 19 | 20 | export const fs = _fs; 21 | if (!fs.promises) { 22 | fs.promises = promisify(fs); 23 | } 24 | 25 | export class FS { 26 | file: { name: string; content: string | null; edit?: boolean; editName?: string; isDir: boolean; path: string; children: FS["files"] | null }; 27 | files: FS["file"][]; 28 | 29 | constructor(props: Partial) { 30 | Object.assign(this, props); 31 | } 32 | async list(dir, options: { ensure?: boolean; onDir?: (data: FS["file"]) => any; onFile?: (data: FS["file"]) => any } = {}): Promise { 33 | const { ensure = true, onDir, onFile } = options; 34 | if (ensure) await this.ensureDir(dir); 35 | const filesInDir = await fs.promises.readdir(dir); 36 | const files = await Promise.all( 37 | filesInDir.map(async (file) => { 38 | const filePath = path.join(dir, file); 39 | const stats = await fs.promises.stat(filePath); 40 | const isDir = stats.isDirectory(); 41 | 42 | let data = { 43 | name: file, 44 | isDir, 45 | path: filePath, 46 | content: null, 47 | children: null, 48 | }; 49 | if (isDir) { 50 | data.children = await this.list(filePath, options); 51 | onDir && (await onDir(data)); 52 | } else { 53 | data.content = (await fs.promises.readFile(filePath)).toString(); 54 | onFile && (await onFile(data)); 55 | } 56 | 57 | return data; 58 | }) 59 | ); 60 | return files; 61 | } 62 | async ensureDir(dir) { 63 | if (dir == "/") return; 64 | //@ts-ignore 65 | const [exists] = await app.helper.runAsync(fs.promises.exists(dir)); 66 | if (!exists) { 67 | await this.ensureDir(path.dirname(dir)); 68 | await fs.promises.mkdir(dir); 69 | return false; 70 | } 71 | return true; 72 | } 73 | async ensureWrite(filePath, content, options?) { 74 | await this.ensureDir(path.dirname(filePath)); 75 | const [err, stats] = await app.helper.runAsync(fs.promises.stat(filePath)); 76 | if (err || !stats.isFile()) { 77 | await fs.promises.writeFile(filePath, content, options); 78 | } 79 | } 80 | async writeFile(filePath, content, options?) { 81 | return fs.promises.writeFile(filePath, content, options); 82 | } 83 | 84 | async rm(path) { 85 | const [err, stat] = await app.helper.runAsync(fs.promises.lstat(path)); 86 | if (err) throw err; 87 | if (stat.isDirectory()) { 88 | const files = await fs.promises.readdir(path); 89 | 90 | for (let file of files) { 91 | const curPath = `${path}/${file}`; 92 | const stat = await fs.promises.lstat(curPath); 93 | if (stat.isDirectory) { 94 | await this.rm(curPath); 95 | } else { 96 | await fs.promises.unlink(curPath); 97 | } 98 | } 99 | await fs.promises.rmdir(path); 100 | } else if (stat.isFile()) { 101 | await fs.promises.unlink(path); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | export class Helper { 2 | async sleep(ms) { 3 | return new Promise((resolve) => setTimeout(resolve, ms)); 4 | } 5 | async runAsync(promise: Promise): Promise<[U | null, T | null]> { 6 | return promise 7 | .then<[null, T]>((data: T) => [null, data]) 8 | .catch<[U, null]>((err) => [err, null]); 9 | } 10 | safeJSONParse(value: any) { 11 | try { 12 | return JSON.parse(value); 13 | } catch (error) { 14 | return value; 15 | } 16 | } 17 | } 18 | 19 | export const helper = new Helper(); 20 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import store from "@/store"; 2 | import { eventBus } from "./eventBus"; 3 | import { fs } from "./fs"; 4 | import { jsvm } from "./vm"; 5 | import { helper } from "./helper"; 6 | import { _ } from "./lodash"; 7 | import { antenna } from "./antenna"; 8 | import { sf } from "./sharefolder"; 9 | 10 | export const app = { 11 | store, 12 | eventBus, 13 | fs, 14 | jsvm, 15 | helper, 16 | _, 17 | antenna, 18 | sf, 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/lodash.ts: -------------------------------------------------------------------------------- 1 | import each from "lodash/each"; 2 | import get from "lodash/get"; 3 | import set from "lodash/set"; 4 | import keyBy from "lodash/keyBy"; 5 | import groupBy from "lodash/groupBy"; 6 | import range from "lodash/range"; 7 | import omitBy from "lodash/omitBy"; 8 | import omit from "lodash/omit"; 9 | import pick from "lodash/pick"; 10 | import orderBy from "lodash/orderBy"; 11 | import isNil from "lodash/isNil"; 12 | import compact from "lodash/compact"; 13 | import cloneDeep from "lodash/cloneDeep"; 14 | import reduce from "lodash/reduce"; 15 | 16 | export const _ = { 17 | each, 18 | get, 19 | set, 20 | keyBy, 21 | groupBy, 22 | range, 23 | omitBy, 24 | isNil, 25 | compact, 26 | omit, 27 | pick, 28 | cloneDeep, 29 | orderBy, 30 | reduce 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/sharefolder.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "websocket-as-promised"; 2 | import { eventBus } from "./eventBus"; 3 | import { app } from "./index"; 4 | 5 | export class ShareFolder { 6 | public ws: WebSocket; 7 | 8 | async start() { 9 | this.ws = new WebSocket("ws://localhost:65520", { 10 | timeout: 5000, 11 | connectionTimeout: 10000, 12 | packMessage: (data) => JSON.stringify(data), 13 | //@ts-ignore 14 | unpackMessage: (data) => JSON.parse(data), 15 | attachRequestId: (data, requestId) => Object.assign({ id: requestId }, data), 16 | extractRequestId: (data) => data && data.id, 17 | }); 18 | this.ws.onOpen.addListener(() => { 19 | console.log("open"); 20 | eventBus.emit("sharefolder.ws.connected"); 21 | }); 22 | this.ws.onClose.addListener(() => { 23 | console.log("close"); 24 | eventBus.emit("sharefolder.ws.closed"); 25 | }); 26 | this.ws.onError.addListener((e) => { 27 | console.error(e); 28 | eventBus.emit("sharefolder.ws.error", e); 29 | }); 30 | // this.ws.onMessage.addListener((e) => { 31 | // // console.log(e); 32 | // }); 33 | await this.ws.open(); 34 | await this.ws.sendRequest({ 35 | action: "request", 36 | key: "handshake", 37 | payload: ["iotex-studio"], 38 | }); 39 | 40 | return this; 41 | } 42 | 43 | async ensureSocket() { 44 | if (this.ws?.isOpened || this.ws?.isOpening) return this.ws; 45 | await this.start(); 46 | 47 | return this.ws; 48 | } 49 | 50 | async close() { 51 | return this.ws.close(); 52 | } 53 | 54 | async call({ args = [], method }: { args?: any; method: string }) { 55 | await this.ensureSocket(); 56 | return this.ws.sendRequest({ 57 | action: "request", 58 | requestInfo: { path: "sharedfolder" }, 59 | key: method, 60 | payload: args, 61 | }); 62 | } 63 | 64 | async dir() { 65 | const res = await this.call({ method: "list" }); 66 | return res.payload; 67 | } 68 | async get({ path }: { path: string }) { 69 | const res = await this.call({ method: "get", args: [{ path }] }); 70 | return res.payload; 71 | } 72 | async set({ path, content }) { 73 | await this.call({ method: "set", args: [{ path, content }] }); 74 | } 75 | async rename({ oldPath, newPath }) { 76 | await this.call({ method: "rename", args: [{ oldPath, newPath }] }); 77 | } 78 | async remove({ path }) { 79 | await await this.call({ method: "remove", args: [{ path }] }); 80 | } 81 | } 82 | 83 | export const sf = new ShareFolder(); 84 | -------------------------------------------------------------------------------- /src/utils/solc.ts: -------------------------------------------------------------------------------- 1 | import solcjsCore from "solcjs-core-fix"; 2 | import { resolverEngine } from "solc-resolver"; 3 | import resolveGithub from "../plugins/resolve-github"; 4 | import resolveHttp from "resolve-http"; 5 | import store from "@/store"; 6 | import path from "path"; 7 | 8 | const solcWrapper = solcjsCore.solcWrapper.wrapper; 9 | export class SolcmManager { 10 | static resolveEngine = new resolverEngine().addResolver(resolveGithub).addResolver(resolveHttp); 11 | static compiler: any; 12 | static async loadSolc(version) { 13 | // const url = await solcVersion.version2url(version); 14 | const url = process.env.NODE_ENV == "production" ? `https://ide-solc-iotex.b-cdn.net/bin/${version}` : `/bin/${version}`; 15 | console.time("[fetch compiler]"); 16 | let compilersource = await solcjsCore.getCompilersource(url); 17 | console.timeEnd("[fetch compiler]"); 18 | const solcjson = solcjsCore.loadModule(compilersource); 19 | const compiler = (this.compiler = solcWrapper(solcjson)); 20 | return compiler; 21 | } 22 | 23 | static async compile({ name, content, optimizer = 0 }: { name: string; content: string; optimizer: number }) { 24 | content = content.replace(/("|')..\//g, `$1${path.dirname(path.dirname(name))}/`).replace(/("|').\//g, `$1${path.dirname(name)}/`); 25 | let readCallback = await solcjsCore.getReadCallback(content, async (filePath: string) => { 26 | const { files } = store.state.editor.fileManager; 27 | const _filePath = path.resolve(path.dirname(name), filePath); 28 | const file = files[filePath] || files[_filePath]; 29 | 30 | if (file) { 31 | let _content = file.content.replace(/("|')..\//g, `$1${path.dirname(path.dirname(filePath))}/`).replace(/("|').\//g, `$1${path.dirname(filePath)}/`); 32 | 33 | return _content; 34 | } 35 | if (/(openzeppelin\/|@openzeppelin\/|openzeppelin-solidity\/)/.test(filePath)) { 36 | filePath = filePath.replace(/(openzeppelin\/|@openzeppelin\/|openzeppelin-solidity\/)/, "https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/release-v2.3.0/"); 37 | } 38 | const data = await this.resolveEngine.require(filePath); 39 | 40 | return data; 41 | }); 42 | const res = this.compiler.compile(content, optimizer, readCallback); 43 | console.log({ res }); 44 | const { 45 | contracts: { MyContract: MyContract_contract, ...otherContracts } = { MyContract: {} }, 46 | sources: { MyContract: MyContract_source, ...otherSources }, 47 | } = res; 48 | 49 | return { 50 | errors: res.errors, 51 | contracts: { 52 | [name]: MyContract_contract, 53 | ...otherContracts, 54 | }, 55 | sources: { 56 | [name]: MyContract_source, 57 | ...otherSources, 58 | }, 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/vm.ts: -------------------------------------------------------------------------------- 1 | import VM from "ethereumjs-vm"; 2 | import abi from "ethereumjs-abi"; 3 | import { Transaction } from "ethereumjs-tx"; 4 | import { promisify } from "util"; 5 | import * as util from "ethereumjs-util"; 6 | import Account from "ethereumjs-account"; 7 | import { Wallet, utils } from "ethers"; 8 | import { eventBus } from "./eventBus"; 9 | import { truncate } from "./filter"; 10 | 11 | export class JSVM { 12 | vm = new VM(); 13 | 14 | async generateAccount() { 15 | const account = new Account({ balance: 1e18 * 100 }); 16 | const wallet = Wallet.createRandom(); 17 | 18 | const privateKey = wallet.privateKey.replace(/^0x/, ""); 19 | const privateKeyBuffer = Buffer.from(privateKey, "hex"); 20 | const addressBuffer = util.privateToAddress(privateKeyBuffer); 21 | 22 | await this.vm.pStateManager.putAccount(addressBuffer, account); 23 | return { 24 | address: wallet.address.replace(/^0x/, "io").toLowerCase(), 25 | addressBuffer, 26 | privateKey, 27 | privateKeyBuffer, 28 | account, 29 | }; 30 | } 31 | 32 | async getAccountNonce(accountPrivateKey: Buffer) { 33 | const account = (await this.vm.pStateManager.getAccount(util.privateToAddress(accountPrivateKey))) as Account; 34 | return account.nonce; 35 | } 36 | 37 | async deplyContract({ 38 | senderPrivateKey, 39 | bytecode, 40 | types = [], 41 | datas = [], 42 | gasLimit = 3000000, 43 | gasPrice = 1, 44 | value = 0, 45 | }: { 46 | senderPrivateKey: Buffer; 47 | bytecode: Buffer; 48 | types: string[]; 49 | datas: string[]; 50 | gasLimit?: number; 51 | gasPrice?: number; 52 | value?: number; 53 | }): Promise { 54 | const params = abi.rawEncode(types, datas); 55 | const nonce = await this.getAccountNonce(senderPrivateKey); 56 | let data = "0x" + bytecode.toString("hex") + params.toString("hex"); 57 | 58 | const tx = new Transaction({ 59 | value, 60 | gasLimit, 61 | gasPrice, 62 | data, 63 | nonce, 64 | }); 65 | 66 | tx.sign(senderPrivateKey); 67 | const deploymentResult = await this.vm.runTx({ tx }); 68 | if (deploymentResult.execResult.exceptionError) { 69 | throw deploymentResult.execResult.exceptionError; 70 | } 71 | 72 | return util.bufferToHex(deploymentResult.createdAddress).replace(/^0x/, "io"); 73 | } 74 | 75 | async readContract({ contractAddress, callerAddress, types, datas, method }: { method: string; contractAddress: string; callerAddress: string; types?: string[]; datas?: string[] }) { 76 | const params = abi.rawEncode(types, datas); 77 | let data = "0x" + abi.methodID(method, types).toString("hex") + params.toString("hex"); 78 | 79 | const _contractAddress = util.toBuffer(contractAddress.replace(/^io/, "0x")); 80 | const _callerAddress = util.toBuffer(callerAddress.replace(/^io/, "0x")); 81 | const result = await this.vm.runCall({ 82 | to: _contractAddress, 83 | caller: _callerAddress, 84 | origin: _callerAddress, 85 | data: util.toBuffer(data), 86 | }); 87 | 88 | eventBus.emit("term.info", { 89 | text: `call from: ${truncate(callerAddress, 12, "...")},to:${truncate(contractAddress, 12, "...")},data:${truncate(data, 12, "...")} `, 90 | data: { 91 | from: callerAddress, 92 | to: contractAddress, 93 | data, 94 | }, 95 | }); 96 | 97 | if (result.execResult.exceptionError) { 98 | throw result.execResult.exceptionError; 99 | } 100 | return result; 101 | } 102 | 103 | getData({ types = [], datas = [], method }: { types?: string[]; datas?: string[]; method: string }) { 104 | datas = datas.map((i) => i.replace(/^io/, "0x")); 105 | const params = abi.rawEncode(types, datas); 106 | let data = abi.methodID(method, types).toString("hex") + params.toString("hex"); 107 | return data; 108 | } 109 | 110 | async interactContract({ 111 | gasLimit = 2000000, 112 | gasPrice = 1, 113 | value = 0, 114 | senderPrivateKey, 115 | contractAddress, 116 | types = [], 117 | datas = [], 118 | method, 119 | }: { 120 | method: string; 121 | senderPrivateKey: Buffer; 122 | contractAddress: string; 123 | types?: string[]; 124 | datas?: string[]; 125 | gasLimit?: number; 126 | gasPrice?: number; 127 | value?: number; 128 | }) { 129 | const params = abi.rawEncode(types, datas); 130 | const nonce = await this.getAccountNonce(senderPrivateKey); 131 | let data = "0x" + abi.methodID(method, types).toString("hex") + params.toString("hex"); 132 | const _contractAddress = util.toBuffer(contractAddress.replace(/^io/, "0x")); 133 | 134 | const tx = new Transaction({ 135 | to: _contractAddress, 136 | value, 137 | gasLimit, 138 | gasPrice, 139 | data, 140 | nonce, 141 | }); 142 | 143 | tx.sign(senderPrivateKey); 144 | const result = await this.vm.runTx({ tx }); 145 | const message = { 146 | senderAddress: `io${tx.getSenderAddress().toString("hex")}`, 147 | contractAddress, 148 | data, 149 | }; 150 | eventBus.emit("term.info", { 151 | text: `call from: ${truncate(message.senderAddress, 12, "...")},to:${truncate(message.contractAddress, 12, "...")},data:${truncate(message.data, 12, "...")} `, 152 | data: { 153 | from: message.senderAddress, 154 | to: message.contractAddress, 155 | data: message.data, 156 | }, 157 | }); 158 | 159 | if (result.execResult.exceptionError) { 160 | throw result.execResult.exceptionError; 161 | } 162 | return result; 163 | } 164 | } 165 | 166 | export const jsvm = new JSVM(); 167 | -------------------------------------------------------------------------------- /src/utils/ws-plugin.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import window from "global/window"; 3 | import WebSocket from "websocket-as-promised"; 4 | import { Account } from "iotex-antenna/lib/account/account"; 5 | import { Envelop } from "iotex-antenna/lib/action/envelop"; 6 | import { SignerPlugin } from "iotex-antenna/lib/action/method"; 7 | import Options from "websocket-as-promised/types/options"; 8 | import { eventBus } from "./eventBus"; 9 | import { helper } from "./helper"; 10 | 11 | interface IRequest { 12 | reqId?: number; 13 | type: "SIGN_AND_SEND" | "GET_ACCOUNTS"; 14 | envelop?: string; // serialized proto string 15 | origin?: string; 16 | } 17 | 18 | export interface WsSignerPluginOptions extends Options { 19 | retryCount?: number; 20 | retryDuration?: number; 21 | } 22 | 23 | export interface WsRequest { 24 | // tslint:disable-next-line: no-any 25 | [key: string]: any; 26 | } 27 | 28 | export class WsSignerPlugin implements SignerPlugin { 29 | private ws: WebSocket; 30 | 31 | private readonly provider: string; 32 | 33 | private readonly options: WsSignerPluginOptions; 34 | 35 | constructor({ provider = "wss://local.iotex.io:64102", options = { retryCount: 3, retryDuration: 50, timeout: 5000 } }: { provider?: string; options: WsSignerPluginOptions }) { 36 | this.provider = provider; 37 | 38 | this.options = options; 39 | 40 | this.init(); 41 | } 42 | 43 | private async init() { 44 | this.ws = new WebSocket(this.provider, this.options); 45 | this.ws.onOpen.addListener(() => { 46 | eventBus.emit("term.message", { text: "[iopay-desktop] connected" }); 47 | }); 48 | this.ws.onClose.addListener = () => { 49 | eventBus.emit("term.warning", { text: "[iopay-desktop] disconnected" }); 50 | }; 51 | await this.ws.open(); 52 | return this; 53 | } 54 | 55 | private async wait() { 56 | while (!this.ws.isOpened) { 57 | await helper.sleep(500); 58 | if (!this.ws.isOpened) await this.init(); 59 | } 60 | return Promise.resolve(true); 61 | } 62 | 63 | public async signAndSend(envelop: Envelop): Promise { 64 | await this.wait(); 65 | const envelopString = Buffer.from(envelop.bytestream()).toString("hex"); 66 | 67 | const req: IRequest = { 68 | envelop: envelopString, 69 | type: "SIGN_AND_SEND", 70 | origin: this.getOrigin(), 71 | }; 72 | const res = await this.ws.sendRequest(req); 73 | return res; 74 | } 75 | 76 | public async getAccount(address: string): Promise { 77 | const acct = new Account(); 78 | acct.address = address; 79 | return acct; 80 | } 81 | 82 | public async getAccounts(): Promise> { 83 | await this.wait(); 84 | const req = { 85 | type: "GET_ACCOUNTS", 86 | }; 87 | const res = await this.ws.sendRequest(req); 88 | return res.accounts; 89 | } 90 | 91 | public getOrigin(plugin: string = ""): string { 92 | let origin: string = ""; 93 | if (location !== undefined && location.hasOwnProperty("hostname") && location.hostname.length) { 94 | origin = location.hostname; 95 | } else { 96 | origin = plugin; 97 | } 98 | 99 | if (origin.substr(0, 4) === "www.") { 100 | origin = origin.replace("www.", ""); 101 | } 102 | console.log(origin); 103 | return origin; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 40 | 41 | 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | // "module": "esnext", 5 | // "strict": true, 6 | "jsx": "preserve", 7 | "allowJs": true, 8 | "noImplicitAny": false, 9 | "noEmit": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "importHelpers": true, 13 | "moduleResolution": "node", 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "emitDecoratorMetadata": true, 17 | "experimentalDecorators": true, 18 | "sourceMap": true, 19 | "baseUrl": ".", 20 | "types": ["node", "webpack-env"], 21 | "paths": { 22 | "@/*": ["src/*"] 23 | }, 24 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 25 | }, 26 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": ["tslint-config-prettier"], 4 | "linterOptions": { 5 | "exclude": ["node_modules/**"] 6 | }, 7 | "rules": { 8 | "indent": [true, "spaces", 2] 9 | }, 10 | "jsRules": { 11 | "no-empty": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { import("@vue/cli-service").ProjectOptions } Options 3 | */ 4 | 5 | const { ProvidePlugin } = require("webpack"); 6 | 7 | /** @type {Options} */ 8 | const options = { 9 | configureWebpack: { 10 | devtool: "source-map", 11 | devServer: { 12 | historyApiFallback: true, 13 | open: false, 14 | host: 'localhost', 15 | port: 8080, 16 | https: false, 17 | proxy: { 18 | "/bin": { 19 | "target": "http://solc-bin.ethereum.org/", 20 | "changeOrigin": true 21 | } 22 | } 23 | 24 | }, 25 | resolve: { 26 | alias: { 27 | fs: "browserfs/dist/shims/fs.js", 28 | buffer: "browserfs/dist/shims/buffer.js", 29 | path: "browserfs/dist/shims/path.js", 30 | processGlobal: "browserfs/dist/shims/process.js", 31 | bufferGlobal: "browserfs/dist/shims/bufferGlobal.js", 32 | bfsGlobal: require.resolve("browserfs"), 33 | }, 34 | }, 35 | module: { 36 | noParse: [/browserfs\.js/], 37 | }, 38 | plugins: [new ProvidePlugin({ BrowserFS: "bfsGlobal", process: "processGlobal", Buffer: "bufferGlobal" })], 39 | node: { 40 | process: false, 41 | Buffer: false, 42 | module: false, 43 | }, 44 | }, 45 | }; 46 | 47 | module.exports = options; 48 | --------------------------------------------------------------------------------