├── .gitignore ├── LICENSE ├── README.md ├── browser ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── build │ └── index.js ├── jsconfig.json ├── mock │ ├── index.js │ ├── mock-server.js │ ├── table.js │ └── user.js ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── table.js │ │ └── user.js │ ├── assets │ │ └── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ ├── components │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ └── SvgIcon │ │ │ └── index.vue │ ├── icons │ │ ├── index.js │ │ ├── svg │ │ │ ├── dashboard.svg │ │ │ ├── example.svg │ │ │ ├── eye-open.svg │ │ │ ├── eye.svg │ │ │ ├── form.svg │ │ │ ├── link.svg │ │ │ ├── nested.svg │ │ │ ├── password.svg │ │ │ ├── table.svg │ │ │ ├── tree.svg │ │ │ └── user.svg │ │ └── svgo.yml │ ├── layout │ │ ├── components │ │ │ ├── AppMain.vue │ │ │ ├── Navbar.vue │ │ │ ├── Sidebar │ │ │ │ ├── FixiOSBug.js │ │ │ │ ├── Item.vue │ │ │ │ ├── Link.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.vue │ │ │ └── index.js │ │ ├── index.vue │ │ └── mixin │ │ │ └── ResizeHandler.js │ ├── main.js │ ├── permission.js │ ├── router │ │ └── index.js │ ├── settings.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── account.js │ │ │ ├── app.js │ │ │ ├── blockchain.js │ │ │ ├── config.js │ │ │ ├── settings.js │ │ │ └── user.js │ ├── styles │ │ ├── element-ui.scss │ │ ├── index.scss │ │ ├── mixin.scss │ │ ├── sidebar.scss │ │ ├── transition.scss │ │ └── variables.scss │ ├── utils │ │ ├── auth.js │ │ ├── date.js │ │ ├── get-page-title.js │ │ ├── index.js │ │ ├── num.js │ │ ├── request.js │ │ ├── tx.js │ │ ├── validate.js │ │ └── votingValidators.js │ └── views │ │ ├── 404.vue │ │ ├── account │ │ └── index.vue │ │ ├── assets │ │ ├── broadcast.vue │ │ ├── index.vue │ │ └── query.vue │ │ ├── blocks │ │ ├── blockdetail.vue │ │ ├── index.vue │ │ └── mockTable.vue │ │ ├── dashboard │ │ └── index.vue │ │ ├── form │ │ └── index.vue │ │ ├── login │ │ └── index.vue │ │ ├── nested │ │ ├── menu1 │ │ │ ├── index.vue │ │ │ ├── menu1-1 │ │ │ │ └── index.vue │ │ │ ├── menu1-2 │ │ │ │ ├── index.vue │ │ │ │ ├── menu1-2-1 │ │ │ │ │ └── index.vue │ │ │ │ └── menu1-2-2 │ │ │ │ │ └── index.vue │ │ │ └── menu1-3 │ │ │ │ └── index.vue │ │ └── menu2 │ │ │ └── index.vue │ │ ├── nodes │ │ ├── index.vue │ │ └── node.vue │ │ └── tree │ │ └── index.vue └── vue.config.js ├── core ├── README.md ├── core.go ├── delivertx.go ├── go.mod ├── go.sum ├── main.go ├── mongodbOperate.go ├── token.go └── verify.go ├── example └── kvstore │ ├── app.go │ ├── go.mod │ └── main.go ├── faq.md ├── getting-start.md ├── inst-tm.sh ├── making-seeds.md ├── making-testnet.md ├── release └── v0.0.1 │ └── codechain_v0.0.1_linux_amd64.sh └── webserver ├── account.go ├── assets.go ├── broadcast.go ├── go.mod ├── go.sum ├── main.go └── readme.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | /.vscode 17 | /pkg 18 | /key 19 | /example/kvstore/go.sum 20 | /core/core 21 | /browser/.DS_Store 22 | /browser/node_modules/ 23 | /browser/dist/ 24 | /browser/package-lock.json 25 | /browser/tests/**/coverage/ 26 | /browser/.editorconfig -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 simon gao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codechain 2 | 3 | ​ codechain基于 Tendermint共识机制的代码链,采用go语言实现,并且依赖于Tendermint环境,建议在ubuntu16.04下开发, 4 | 5 | - ​ 为了便于后续的开发,首先建立起git、golang、tendermint代码库、vscode的开发环境,参考 [建立开发环境](https://github.com/little51/codechain/blob/master/getting-start.md) 。 6 | - 为了验证Tendermint的四个节点的运行情况,参考 [运行环境组网](https://github.com/little51/codechain/blob/master/making-testnet.md) 。 7 | - 为了验证Tendermint更多的节点加入,参考 [Seed模式组网](https://github.com/little51/codechain/blob/master/making-seeds.md) 。 8 | - Tendermint一些难点,可参考 [FAQ](https://github.com/little51/codechain/blob/master/faq.md) 。 9 | 10 | ## Codechain核心 11 | 12 | ### 安装Mongodb 13 | 14 | ​ 资产数据采用Mongodb保存,所以请先安装Mongodb。 15 | 16 | ```shell 17 | sudo apt-get install mongodb 18 | ``` 19 | 20 | ### 二进制文件安装 21 | 22 | codechain采用go语言开发,所以无依赖库,直接下载可执行文件即可运行,预览功能,安装脚本见 23 | 24 | https://github.com/little51/codechain/blob/master/release/v0.0.1/codechain_v0.0.1_linux_amd64.sh 25 | 26 | ### 程序编译 27 | 28 | 如果已经配置好go开发环境,可以编译程序源码。 29 | 30 | ```shell 31 | export GO111MODULE=on 32 | export GOPROXY=https://goproxy.io 33 | cd core 34 | go build 35 | ``` 36 | 37 | ### 运行 38 | 39 | #### 链初始化 40 | 41 | ```shell 42 | tendermint init 43 | ``` 44 | 45 | 链初始化只做一次,执行完成后,会在~/.tendermint目录下生成配置文件和数据文件。 46 | 47 | #### 运行应用 48 | 进入codechain/core目录下执行下面操作: 49 | ```shell 50 | cd codechain/core 51 | go build 52 | ./core 53 | ``` 54 | 55 | core程序会在26658端口监听tendermint进程发过来的交易。 56 | 57 | #### 运行单节点 58 | 59 | ​ 再开一个命令行,执行: 60 | 61 | ```shell 62 | tendermint node 63 | ``` 64 | 65 | tendermint会在26657端口监听。 66 | 67 | ### 测试 68 | 69 | ```shell 70 | curl -s 'localhost:26657/broadcast_tx_commit?tx="eyJwdWJsaWNrZXkiOiJBNUZFQTY0NUMwOTlGNjlFRTVGQ0Q4QjY3RkU4M0VBQ0QwQ0JBQzQxMEFCQzVCOEZBNUNCNEU0NTFENUY1Q0VDIiwic2lnbiI6IjNiOTM2ODc5MzkyMDJiNTJhZWJlZDA2YjU1ZjY2M2ZiN2M0ZjYyNzU2MGY3OTA3NDM5ZDg4ZjYzMWM5NWI4ZjNjYzdkODBkNzEzMTgwZjRhMDg1MDBkZDU5YTgzNjlhODZmODlhODQ4NDI1NzZjMDJkMTFkYTI3NTdhNzBiZDBkIiwibXNnIjoiZXlKMGIydGxiaUk2SWtWUFJpSXNJbVp5YjIwaU9pSkJOVVpGUVRZME5VTXdPVGxHTmpsRlJUVkdRMFE0UWpZM1JrVTRNMFZCUTBRd1EwSkJRelF4TUVGQ1F6VkNPRVpCTlVOQ05FVTBOVEZFTlVZMVEwVkRJaXdpZEc4aU9pSkVOemM1UkRZelJqZzNRa0kyT1RreE4wTTBSRE5FUkRKQ056WXlNekUxT1RVeE9URTVOVVExUVRWQlJEYzBSakV3UWpjNFJEbEJNamRCT1RZMk5UVkRJaXdpWVcxdmRXNTBJam9pTlRBaWZRPT0ifQ=="' 71 | curl -s 'localhost:26657/abci_query?data="key=key1"' 72 | ``` 73 | 74 | ## Web应用 75 | 76 | ​ Web应用目前提供了账户管理和简单的资产管理,可实现账户密钥对的创建,资产的签名、登记。 77 | 78 | ### 编译运行 79 | 80 | ```shell 81 | cd webserver 82 | go build 83 | ./webserver 84 | ``` 85 | 86 | ### 测试 87 | 88 | ​ 新建账户,返回公私钥对,对资产字符串进行私钥签名,用公钥验证。 89 | 90 | #### 新建账户 91 | 92 | ```shell 93 | curl -X POST http://localhost:4000/account/new 94 | ``` 95 | 96 | 结果如下: 97 | 98 | ```json 99 | { 100 | "address":"ECDCE3D5B6164768C0D4DFB74BF6B9E2C4D1682B", 101 | "error":"", 102 | "privateKey":"80d7f6a76d6bbc9893efff3721a49432f5ffdba39e0c98256d7e3fdd53e6b807d4e4cf08ec338970efc0e6b8e1dde3d4129a6bb5588ba90f72f9a0d6c0ed62ce", 103 | "publicKey":"D4E4CF08EC338970EFC0E6B8E1DDE3D4129A6BB5588BA90F72F9A0D6C0ED62CE" 104 | } 105 | ``` 106 | 107 | #### 资产签名 108 | 资产交易需要先进行资产签名,验证发起资产交易账户的公私钥对 109 | 110 | 测试如下 111 | ```shell 112 | curl -H "Content-Type: application/json" -d '{"privatekey":"80d7f6a76d6bbc9893efff3721a49432f5ffdba39e0c98256d7e3fdd53e6b807d4e4cf08ec338970efc0e6b8e1dde3d4129a6bb5588ba90f72f9a0d6c0ed62ce","msg":"eyJ0b2tlbiI6IktLSyIsImZyb20iOiJENEU0Q0YwOEVDMzM4OTcwRUZDMEU2QjhFMURERTNENDEyOUE2QkI1NTg4QkE5MEY3MkY5QTBENkMwRUQ2MkNFIiwidG8iOiIiLCJhbW91bnQiOiI2MDAifQ=="}' -X POST http://localhost:4000/account/sign 113 | ``` 114 | 115 | 结果如下: 116 | 117 | ```json 118 | {"error":"","sign":"25a76ff76196706d63c3190cfb97edd45f322f046cb9d78592eda1368b42c15c63a6780a777a40de626b9b9530e9433f51d8ee300b881ea55941cc53b5b27507"} 119 | ``` 120 | 121 | #### 资产创世 122 | 在资产交易之前,应该确认发起者拥有足够的token,可以通过资产创世的方式来创建一种新的token 123 | 124 | 测试如下: 125 | ```shell 126 | curl -H "Content-Type: application/json" -d '{"publickey":"D4E4CF08EC338970EFC0E6B8E1DDE3D4129A6BB5588BA90F72F9A0D6C0ED62CE","sign":"25a76ff76196706d63c3190cfb97edd45f322f046cb9d78592eda1368b42c15c63a6780a777a40de626b9b9530e9433f51d8ee300b881ea55941cc53b5b27507","msg":"eyJ0b2tlbiI6IktLSyIsImZyb20iOiJENEU0Q0YwOEVDMzM4OTcwRUZDMEU2QjhFMURERTNENDEyOUE2QkI1NTg4QkE5MEY3MkY5QTBENkMwRUQ2MkNFIiwidG8iOiIiLCJhbW91bnQiOiI2MDAifQ=="}' -X POST http://localhost:4000/assets/new 127 | ``` 128 | 129 | 结果如下: 130 | 131 | ```json 132 | { 133 | "error": "", 134 | "info": "{ \n \"jsonrpc\": "2.0",\n \"id\": \"\",\n \"result\": {\n \"check_tx\": {\n \"code\": 0,\n \"data\": null,\n \"log\": \"\",\n \"info\": \"CheckTx successfully carried out the signVerify\",\n \"gasWanted\": \"1\" \n \"gasUsed\": "0",\n \"events\": [], \n \"codespace\": \"\"},\n \"deliver_tx\": {\n \"code\": 0,\n \"data\": null, \n \"log\": \"\", \n \"info\": \"\",\n \"gasWanted\": \"0\",\n \"gasUsed\": \"0\",\n \"events\": [],\n \"codespace\": \"\"},\n \"hash\": \"91C2DA3A01E9093FB7CF1FD4A30C93A5164A8F20FFCE28020BD7AB1497FB3AE8\",\n \"height\": \"82\"}}\n", 135 | "result": true 136 | } 137 | ``` 138 | 139 | #### 资产交易 140 | 两个不同账户之间可以进行相同token的交易 141 | 142 | ```shell 143 | curl -H "Content-Type: application/json" -d '{"publickey":"D4E4CF08EC338970EFC0E6B8E1DDE3D4129A6BB5588BA90F72F9A0D6C0ED62CE","sign":"df2d0652c8994ddc14ec70f9532aff7fb9b4ccd3077bf9c88c936a65a18a500e380f639f410d4058deddc2d4047bee070e229b0b5e86aaa97448374ac854770d","msg":"eyJ0b2tlbiI6IktLSyIsImZyb20iOiJENEU0Q0YwOEVDMzM4OTcwRUZDMEU2QjhFMURERTNENDEyOUE2QkI1NTg4QkE5MEY3MkY5QTBENkMwRUQ2MkNFIiwidG8iOiJBNUZFQTY0NUMwOTlGNjlFRTVGQ0Q4QjY3RkU4M0VBQ0QwQ0JBQzQxMEFCQzVCOEZBNUNCNEU0NTFENUY1Q0VDIiwiYW1vdW50IjoiMzAwIn0="}' -X POST http://localhost:4000/assets/new 144 | ``` 145 | 146 | 结果如下: 147 | 148 | ```json 149 | { 150 | "error": "", 151 | "info": "{ \n \"jsonrpc\": "2.0",\n \"id\": \"\",\n \"result\": {\n \"check_tx\": {\n \"code\": 0,\n \"data\": null,\n \"log\": \"\",\n \"info\": \"CheckTx successfully carried out the signVerify\",\n \"gasWanted\": \"1\" \n \"gasUsed\": "0",\n \"events\": [], \n \"codespace\": \"\"},\n \"deliver_tx\": {\n \"code\": 0,\n \"data\": null, \n \"log\": \"\", \n \"info\": \"\",\n \"gasWanted\": \"0\",\n \"gasUsed\": \"0\",\n \"events\": [],\n \"codespace\": \"\"},\n \"hash\": \"B80F6B79569989513E5F23552CDBB53BA854F468FC2A4BFF1696C4C897660734\",\n \"height\": \"83\"}}\n", 152 | "result": true 153 | } 154 | ``` 155 | 156 | #### 资产查询 157 | 通过公钥可以来查询到当前所有不同token的资产信息 158 | 159 | 测试如下: 160 | ```shell 161 | curl -H "Content-Type: application/json" -d '{"key": "D4E4CF08EC338970EFC0E6B8E1DDE3D4129A6BB5588BA90F72F9A0D6C0ED62CE"}' -X POST http://localhost:4000/assets/query 162 | ``` 163 | 164 | 结果如下: 165 | ```json 166 | { 167 | "error":"", 168 | "info":"{\n \"jsonrpc\": \"2.0\",\n \"id\": -1,\n \"result\": {\n \"response\": {\n \"code\": 0,\n \"log\": \"\",\n \"info\": \"D4E4CF08EC338970EFC0E6B8E1DDE3D4129A6BB5588BA90F72F9A0D6C0ED62CE\",\n \"index\": \"0\",\n \"key\": null,\n \"value\": \"eyJhcnJheSI6IFt7InB1YmxpY2tleSI6IkQ0RTRDRjA4RUMzMzg5NzBFRkMwRTZCOEUxRERFM0Q0MTI5QTZCQjU1ODhCQTkwRjcyRjlBMEQ2QzBFRDYyQ0UiLCJ0b2tlbiI6IktLSyIsImFtb3VudCI6MzAwfV19\",\n \"proof\": null,\n \"height\": \"0\",\n \"codespace\": \"\"\n }\n }\n}", 169 | "result":true 170 | } 171 | ``` 172 | 173 | #### msg说明 174 | 在资产签名,资产创世,资产查询测试中,POST请求数据中包含msg字段,该字段是处理后的一段加密Base64的字符串。其原始信息为包含token、from、to和amount四个属性的javascript对象,在前端经JSON.stringify()和Base64.encode()先后处理后形成最终的msg值。 175 | 176 | ### 数据库查询 177 | 178 | ​ 资产会保存到Mongodb中,通过Mongodb客户端可以查询chain数据库的assets集合。 179 | 180 | ## 链浏览器 181 | 182 | 参见 [链浏览器](https://github.com/little51/codechain/blob/master/browser/README.md) 的说明。 183 | 184 | ## 路线图 185 | 186 | | 序号 | 类别 | 任务 | 完成情况 | 187 | | :--: | ----- | -------------------------------------------- | -------- | 188 | | 1 | 基础 | 开发环境 | 完成 | 189 | | 2 | | 运行环境 | 完成 | 190 | | 3 | | 一键自动安装应用 | 完成 | 191 | | 4 | 资产 | 基本key-value(mongledb) | 完成 | 192 | | 5 | | 账户account与签名 | 完成 | 193 | | 6 | | 可分隔资产 | 开发中 | 194 | | 7 | API | API规划 | 完成 | 195 | | 8 | WEBUI | 块浏览器 | 开发中 | 196 | | 9 | | 资产管理 | 开发中 | 197 | | 10 | | 账户管理 | 完成 | 198 | | 11 | 网络 | 协调节点 | 完成 | 199 | | 12 | | 节点管理(增减节点) | 开发中 | 200 | | 13 | 应用 | 生态系统(代码仓库、分布式应用、资产、价值) | 规划完成 | 201 | 202 | -------------------------------------------------------------------------------- /browser/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/dev-api' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /browser/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/prod-api' 6 | 7 | -------------------------------------------------------------------------------- /browser/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /browser/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src 3 | public 4 | dist 5 | src/store 6 | -------------------------------------------------------------------------------- /browser/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | "vue/max-attributes-per-line": [2, { 18 | "singleline": 10, 19 | "multiline": { 20 | "max": 1, 21 | "allowFirstLine": false 22 | } 23 | }], 24 | "vue/singleline-html-element-content-newline": "off", 25 | "vue/multiline-html-element-content-newline":"off", 26 | "vue/name-property-casing": ["error", "PascalCase"], 27 | "vue/no-v-html": "off", 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': ["error", "always", {"null": "ignore"}], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 0, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | // 'quotes': [2, 'single', { 166 | // 'avoidEscape': true, 167 | // 'allowTemplateLiterals': true 168 | // }], 169 | 'semi': [2, 'never'], 170 | 'semi-spacing': [2, { 171 | 'before': false, 172 | 'after': true 173 | }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 0, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /browser/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /browser/README.md: -------------------------------------------------------------------------------- 1 | # vue-tendermint-explorer 2 | `vue-tendermint-explorder`区块链浏览器是以`tendermint-explorder`为依据,在`vue-admin-template`基础上进行的框架二次开发,该项目启动我们需要两个步骤,分别是: 3 | + 配置`tendermint`跨域 4 | + 启动`vue-admin-template` 5 | 6 | ## 配置跨域 7 | ### 1. 链初始化 8 | ```shell 9 | tendermint init 10 | ``` 11 | 链初始化只做一次,如果之前已经执行了这一步可以忽略了,执行完成后,会在`~/.tendermint`目录下面生成配置文件`config`和数据文件`data`。 12 | 13 | ### 2. 配置跨域 14 | 由于`tendermint`会监听26657端口,前端和后端项目都会通过`rpc`和`http`的方式去访问,所以我们需要进入`tendermint`的配置文件: 15 | ```shell 16 | cd ~/.tendermint/config 17 | sudo vim config.toml 18 | ``` 19 | 然后我们我们修改`config.toml`文件其中的`rpc`配置如下: 20 | ```shell 21 | ##### rpc server configuration options ##### 22 | [rpc] 23 | 24 | # TCP or UNIX socket address for the RPC server to listen on 25 | laddr = "tcp://0.0.0.0:26657" 26 | 27 | # A list of origins a cross-domain request can be executed from 28 | # Default value '[]' disables cors support 29 | # Use '["*"]' to allow any origin 30 | cors_allowed_origins = ["*"] 31 | ``` 32 | 然后我们就可以跨域请求`127.0.0.1:26657`或者`localhost:26657`了,从而实现和`tendermint`的交互。 33 | 34 | ## 启动项目 35 | ### 1. 启动后端服务 36 | 进入后端文件开始编译代码: 37 | ```shell 38 | cd ~/codechain/webserver/ 39 | go build 40 | ``` 41 | 启动后端服务,启动在`4000`端口: 42 | ```shell 43 | ./webserver 44 | ``` 45 | ### 2. 启动前端服务 46 | ```shell 47 | cd ~/codechain/browser 48 | npm install --registry=https://registry.npm.taobao.org 49 | npm run dev 50 | ``` 51 | 访问`localhost:9528` -------------------------------------------------------------------------------- /browser/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /browser/build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /browser/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /browser/mock/index.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { param2Obj } from '../src/utils' 3 | 4 | import user from './user' 5 | import table from './table' 6 | 7 | const mocks = [ 8 | ...user, 9 | ...table 10 | ] 11 | 12 | // for front mock 13 | // please use it cautiously, it will redefine XMLHttpRequest, 14 | // which will cause many of your third-party libraries to be invalidated(like progress event). 15 | export function mockXHR() { 16 | // mock patch 17 | // https://github.com/nuysoft/Mock/issues/300 18 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 19 | Mock.XHR.prototype.send = function() { 20 | if (this.custom.xhr) { 21 | this.custom.xhr.withCredentials = this.withCredentials || false 22 | 23 | if (this.responseType) { 24 | this.custom.xhr.responseType = this.responseType 25 | } 26 | } 27 | this.proxy_send(...arguments) 28 | } 29 | 30 | function XHR2ExpressReqWrap(respond) { 31 | return function(options) { 32 | let result = null 33 | if (respond instanceof Function) { 34 | const { body, type, url } = options 35 | // https://expressjs.com/en/4x/api.html#req 36 | result = respond({ 37 | method: type, 38 | body: JSON.parse(body), 39 | query: param2Obj(url) 40 | }) 41 | } else { 42 | result = respond 43 | } 44 | return Mock.mock(result) 45 | } 46 | } 47 | 48 | for (const i of mocks) { 49 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 50 | } 51 | } 52 | 53 | // for mock server 54 | const responseFake = (url, type, respond) => { 55 | return { 56 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), 57 | type: type || 'get', 58 | response(req, res) { 59 | console.log('request invoke:' + req.path) 60 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 61 | } 62 | } 63 | } 64 | 65 | export default mocks.map(route => { 66 | return responseFake(route.url, route.type, route.response) 67 | }) 68 | -------------------------------------------------------------------------------- /browser/mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | 6 | const mockDir = path.join(process.cwd(), 'mock') 7 | 8 | function registerRoutes(app) { 9 | let mockLastIndex 10 | const { default: mocks } = require('./index.js') 11 | for (const mock of mocks) { 12 | app[mock.type](mock.url, mock.response) 13 | mockLastIndex = app._router.stack.length 14 | } 15 | const mockRoutesLength = Object.keys(mocks).length 16 | return { 17 | mockRoutesLength: mockRoutesLength, 18 | mockStartIndex: mockLastIndex - mockRoutesLength 19 | } 20 | } 21 | 22 | function unregisterRoutes() { 23 | Object.keys(require.cache).forEach(i => { 24 | if (i.includes(mockDir)) { 25 | delete require.cache[require.resolve(i)] 26 | } 27 | }) 28 | } 29 | 30 | module.exports = app => { 31 | // es6 polyfill 32 | require('@babel/register') 33 | 34 | // parse app.body 35 | // https://expressjs.com/en/4x/api.html#req.body 36 | app.use(bodyParser.json()) 37 | app.use(bodyParser.urlencoded({ 38 | extended: true 39 | })) 40 | 41 | const mockRoutes = registerRoutes(app) 42 | var mockRoutesLength = mockRoutes.mockRoutesLength 43 | var mockStartIndex = mockRoutes.mockStartIndex 44 | 45 | // watch files, hot reload mock server 46 | chokidar.watch(mockDir, { 47 | ignored: /mock-server/, 48 | ignoreInitial: true 49 | }).on('all', (event, path) => { 50 | if (event === 'change' || event === 'add') { 51 | try { 52 | // remove mock routes stack 53 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 54 | 55 | // clear routes cache 56 | unregisterRoutes() 57 | 58 | const mockRoutes = registerRoutes(app) 59 | mockRoutesLength = mockRoutes.mockRoutesLength 60 | mockStartIndex = mockRoutes.mockStartIndex 61 | 62 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 63 | } catch (error) { 64 | console.log(chalk.redBright(error)) 65 | } 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /browser/mock/table.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | title: '@sentence(10, 20)', 7 | 'status|1': ['published', 'draft', 'deleted'], 8 | author: 'name', 9 | display_time: '@datetime', 10 | pageviews: '@integer(300, 5000)' 11 | }] 12 | }) 13 | 14 | export default [ 15 | { 16 | url: '/vue-admin-template/table/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /browser/mock/user.js: -------------------------------------------------------------------------------- 1 | 2 | const tokens = { 3 | admin: { 4 | token: 'admin-token' 5 | }, 6 | editor: { 7 | token: 'editor-token' 8 | } 9 | } 10 | 11 | const users = { 12 | 'admin-token': { 13 | roles: ['admin'], 14 | introduction: 'I am a super administrator', 15 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 16 | name: 'Super Admin' 17 | }, 18 | 'editor-token': { 19 | roles: ['editor'], 20 | introduction: 'I am an editor', 21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 22 | name: 'Normal Editor' 23 | } 24 | } 25 | 26 | export default [ 27 | // user login 28 | { 29 | url: '/vue-admin-template/user/login', 30 | type: 'post', 31 | response: config => { 32 | const { username } = config.body 33 | const token = tokens[username] 34 | 35 | // mock error 36 | if (!token) { 37 | return { 38 | code: 60204, 39 | message: 'Account and password are incorrect.' 40 | } 41 | } 42 | 43 | return { 44 | code: 20000, 45 | data: token 46 | } 47 | } 48 | }, 49 | 50 | // get user info 51 | { 52 | url: '/vue-admin-template/user/info\.*', 53 | type: 'get', 54 | response: config => { 55 | const { token } = config.query 56 | const info = users[token] 57 | 58 | // mock error 59 | if (!info) { 60 | return { 61 | code: 50008, 62 | message: 'Login failed, unable to get user details.' 63 | } 64 | } 65 | 66 | return { 67 | code: 20000, 68 | data: info 69 | } 70 | } 71 | }, 72 | 73 | // user logout 74 | { 75 | url: '/vue-admin-template/user/logout', 76 | type: 'post', 77 | response: _ => { 78 | return { 79 | code: 20000, 80 | data: 'success' 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-template", 3 | "version": "4.2.1", 4 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", 5 | "author": "Pan ", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "vue-cli-service serve", 9 | "build:prod": "vue-cli-service build", 10 | "build:stage": "vue-cli-service build --mode staging", 11 | "preview": "node build/index.js --preview", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit", 15 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" 16 | }, 17 | "dependencies": { 18 | "axios": "0.18.1", 19 | "base64-js": "^1.3.0", 20 | "create-hash": "^1.2.0", 21 | "element-ui": "2.13.0", 22 | "js-base64": "^2.5.2", 23 | "js-cookie": "2.2.0", 24 | "normalize.css": "7.0.0", 25 | "nprogress": "0.2.0", 26 | "path-to-regexp": "2.4.0", 27 | "tendermint": "^3.1.11", 28 | "varint": "^5.0.0", 29 | "vue": "2.6.10", 30 | "vue-router": "3.0.6", 31 | "vuex": "3.1.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "7.0.0", 35 | "@babel/register": "7.0.0", 36 | "@vue/cli-plugin-babel": "3.6.0", 37 | "@vue/cli-plugin-eslint": "^3.9.1", 38 | "@vue/cli-plugin-unit-jest": "3.6.3", 39 | "@vue/cli-service": "3.6.0", 40 | "@vue/test-utils": "1.0.0-beta.29", 41 | "autoprefixer": "^9.5.1", 42 | "babel-core": "7.0.0-bridge.0", 43 | "babel-eslint": "10.0.1", 44 | "babel-jest": "23.6.0", 45 | "chalk": "2.4.2", 46 | "connect": "3.6.6", 47 | "eslint": "5.15.3", 48 | "eslint-plugin-vue": "5.2.2", 49 | "html-webpack-plugin": "3.2.0", 50 | "lodash": "^4.17.15", 51 | "mockjs": "1.0.1-beta3", 52 | "moment": "^2.22.1", 53 | "node-sass": "^4.9.0", 54 | "numeral": "2.0.6", 55 | "request-interval": "2.0.0", 56 | "runjs": "^4.3.2", 57 | "sass-loader": "^7.1.0", 58 | "script-ext-html-webpack-plugin": "2.1.3", 59 | "script-loader": "0.7.2", 60 | "serve-static": "^1.13.2", 61 | "svg-sprite-loader": "4.1.3", 62 | "svgo": "1.2.2", 63 | "vue-template-compiler": "2.6.10" 64 | }, 65 | "engines": { 66 | "node": ">=8.9", 67 | "npm": ">= 3.0.0" 68 | }, 69 | "browserslist": [ 70 | "> 1%", 71 | "last 2 versions" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /browser/postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /browser/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/little51/codechain/aca3b68320e8386d186d65139b3b33b0ae8f1207/browser/public/favicon.ico -------------------------------------------------------------------------------- /browser/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /browser/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 41 | -------------------------------------------------------------------------------- /browser/src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: '/vue-admin-template/table/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /browser/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/vue-admin-template/user/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/vue-admin-template/user/info', 14 | method: 'get', 15 | params: { token } 16 | }) 17 | } 18 | 19 | export function logout() { 20 | return request({ 21 | url: '/vue-admin-template/user/logout', 22 | method: 'post' 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /browser/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/little51/codechain/aca3b68320e8386d186d65139b3b33b0ae8f1207/browser/src/assets/404_images/404.png -------------------------------------------------------------------------------- /browser/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/little51/codechain/aca3b68320e8386d186d65139b3b33b0ae8f1207/browser/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /browser/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /browser/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /browser/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /browser/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /browser/src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /browser/src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /browser/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /browser/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 61 | 62 | 140 | -------------------------------------------------------------------------------- /browser/src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /browser/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /browser/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /browser/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /browser/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /browser/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /browser/src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /browser/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | 53 | 94 | -------------------------------------------------------------------------------- /browser/src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /browser/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import locale from 'element-ui/lib/locale/lang/en' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import store from './store' 13 | import router from './router' 14 | 15 | import '@/icons' // icon 16 | import '@/permission' // permission control 17 | 18 | /** 19 | * If you don't want to use mock-server 20 | * you want to use MockJs for mock api 21 | * you can execute: mockXHR() 22 | * 23 | * Currently MockJs will be used in the production environment, 24 | * please remove it before going online ! ! ! 25 | */ 26 | if (process.env.NODE_ENV === 'production') { 27 | const { mockXHR } = require('../mock') 28 | mockXHR() 29 | } 30 | 31 | // set ElementUI lang to EN 32 | Vue.use(ElementUI, { locale }) 33 | // 如果想要中文版 element-ui,按如下方式声明 34 | // Vue.use(ElementUI) 35 | 36 | Vue.config.productionTip = false 37 | 38 | new Vue({ 39 | el: '#app', 40 | store, 41 | router, 42 | render: h => h(App) 43 | }) 44 | -------------------------------------------------------------------------------- /browser/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | const hasGetUserInfo = store.getters.name 30 | if (hasGetUserInfo) { 31 | next() 32 | } else { 33 | try { 34 | // get user info 35 | await store.dispatch('user/getInfo') 36 | 37 | next() 38 | } catch (error) { 39 | // remove token and go to login page to re-login 40 | await store.dispatch('user/resetToken') 41 | Message.error(error || 'Has Error') 42 | next(`/login?redirect=${to.path}`) 43 | NProgress.done() 44 | } 45 | } 46 | } 47 | } else { 48 | /* has no token*/ 49 | 50 | if (whiteList.indexOf(to.path) !== -1) { 51 | // in the free login whitelist, go directly 52 | next() 53 | } else { 54 | // other pages that do not have permission to access are redirected to the login page. 55 | next(`/login?redirect=${to.path}`) 56 | NProgress.done() 57 | } 58 | } 59 | }) 60 | 61 | router.afterEach(() => { 62 | // finish progress bar 63 | NProgress.done() 64 | }) 65 | -------------------------------------------------------------------------------- /browser/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | /* Layout */ 7 | import Layout from '@/layout' 8 | 9 | /** 10 | * Note: sub-menu only appear when route children.length >= 1 11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html 12 | * 13 | * hidden: true if set true, item will not show in the sidebar(default is false) 14 | * alwaysShow: true if set true, will always show the root menu 15 | * if not set alwaysShow, when item has more than one children route, 16 | * it will becomes nested mode, otherwise not show the root menu 17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb 18 | * name:'router-name' the name is used by (must set!!!) 19 | * meta : { 20 | roles: ['admin','editor'] control the page roles (you can set multiple roles) 21 | title: 'title' the name show in sidebar and breadcrumb (recommend set) 22 | icon: 'svg-name' the icon show in the sidebar 23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) 24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set 25 | } 26 | */ 27 | 28 | /** 29 | * constantRoutes 30 | * a base page that does not have permission requirements 31 | * all roles can be accessed 32 | */ 33 | export const constantRoutes = [ 34 | { 35 | path: '/login', 36 | component: () => import('@/views/login/index'), 37 | hidden: true 38 | }, 39 | 40 | { 41 | path: '/404', 42 | component: () => import('@/views/404'), 43 | hidden: true 44 | }, 45 | 46 | { 47 | path: '/', 48 | component: Layout, 49 | redirect: '/dashboard', 50 | children: [{ 51 | path: 'dashboard', 52 | name: 'Dashboard', 53 | component: () => import('@/views/dashboard/index'), 54 | meta: { title: 'Overview', icon: 'dashboard' } 55 | }] 56 | }, 57 | { 58 | path: '/Blocks', 59 | component: Layout, 60 | // redirect: '/example/table', 61 | name: '/Blocks', 62 | meta: { title: 'Blockchain', icon: 'example' }, 63 | children: [ 64 | { 65 | path: 'blocks', 66 | name: 'blocks', 67 | component: () => import('@/views/blocks/index'), 68 | meta: { title: 'blocks', icon: 'table' } 69 | }, 70 | { 71 | path: 'blockdetail/:block', 72 | name: 'blockdetail', 73 | component: () => import('@/views/blocks/blockdetail') 74 | // meta: { title: 'block-detail', icon: 'tree' } 75 | } 76 | // { 77 | // path: 'tree', 78 | // name: 'Tree', 79 | // // component: () => import('@/views/tree/index'), 80 | // component: () => import('@/views/blocks/mockTable'), 81 | // meta: { title: 'block-detail', icon: 'tree' } 82 | // } 83 | ] 84 | }, 85 | { 86 | path: '/Nodes', 87 | component: Layout, 88 | name: 'Nodes', 89 | meta: { title: 'Nodes', icon: 'form' }, 90 | children: [ 91 | { 92 | path: 'nodes', 93 | name: 'nodes', 94 | component: () => import('@/views/nodes/index'), 95 | meta: { title: 'Full Nodes', icon: 'link' } 96 | }, 97 | { 98 | path: 'node/:node', 99 | name: 'node', 100 | component: () => import('@/views/nodes/node') 101 | } 102 | ] 103 | }, 104 | { 105 | path: '/Account', 106 | component: Layout, 107 | name: 'Account', 108 | meta: { title: 'Account', icon: 'user' }, 109 | children: [ 110 | { 111 | path: 'new', 112 | name: 'new', 113 | component: () => import('@/views/account/index'), 114 | meta: { title: 'Add Account', icon: 'link' } 115 | } 116 | ] 117 | }, 118 | { 119 | path: '/Assets', 120 | component: Layout, 121 | name: 'Assets', 122 | meta: { title: 'Assets', icon: 'nested' }, 123 | children: [ 124 | { 125 | path: 'new', 126 | name: 'new', 127 | component: () => import('@/views/assets/index'), 128 | meta: { title: 'Asset Transaction', icon: 'link' } 129 | }, 130 | { 131 | path: 'query', 132 | name: 'query', 133 | component: () => import('@/views/assets/query'), 134 | meta: { title: 'Query Asset', icon: 'link' } 135 | } 136 | // { 137 | // path: 'broadcast', 138 | // name: 'broadcast', 139 | // component: () => import('@/views/assets/broadcast'), 140 | // meta: { title: 'Broadcast msg', icon: 'link' } 141 | // } 142 | ] 143 | }, 144 | // { 145 | // path: '/form', 146 | // component: Layout, 147 | // children: [ 148 | // { 149 | // path: 'index', 150 | // name: 'Form', 151 | // component: () => import('@/views/form/index'), 152 | // meta: { title: 'Form', icon: 'form' } 153 | // } 154 | // ] 155 | // }, 156 | 157 | // { 158 | // path: '/nested', 159 | // component: Layout, 160 | // redirect: '/nested/menu1', 161 | // name: 'Nested', 162 | // meta: { 163 | // title: 'Nested', 164 | // icon: 'nested' 165 | // }, 166 | // children: [ 167 | // { 168 | // path: 'menu1', 169 | // component: () => import('@/views/nested/menu1/index'), // Parent router-view 170 | // name: 'Menu1', 171 | // meta: { title: 'Menu1' }, 172 | // children: [ 173 | // { 174 | // path: 'menu1-1', 175 | // component: () => import('@/views/nested/menu1/menu1-1'), 176 | // name: 'Menu1-1', 177 | // meta: { title: 'Menu1-1' } 178 | // }, 179 | // { 180 | // path: 'menu1-2', 181 | // component: () => import('@/views/nested/menu1/menu1-2'), 182 | // name: 'Menu1-2', 183 | // meta: { title: 'Menu1-2' }, 184 | // children: [ 185 | // { 186 | // path: 'menu1-2-1', 187 | // component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), 188 | // name: 'Menu1-2-1', 189 | // meta: { title: 'Menu1-2-1' } 190 | // }, 191 | // { 192 | // path: 'menu1-2-2', 193 | // component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'), 194 | // name: 'Menu1-2-2', 195 | // meta: { title: 'Menu1-2-2' } 196 | // } 197 | // ] 198 | // }, 199 | // { 200 | // path: 'menu1-3', 201 | // component: () => import('@/views/nested/menu1/menu1-3'), 202 | // name: 'Menu1-3', 203 | // meta: { title: 'Menu1-3' } 204 | // } 205 | // ] 206 | // }, 207 | // { 208 | // path: 'menu2', 209 | // component: () => import('@/views/nested/menu2/index'), 210 | // meta: { title: 'menu2' } 211 | // } 212 | // ] 213 | // }, 214 | 215 | // { 216 | // path: 'external-link', 217 | // component: Layout, 218 | // children: [ 219 | // { 220 | // path: 'https://panjiachen.github.io/vue-element-admin-site/#/', 221 | // meta: { title: 'External Link', icon: 'link' } 222 | // } 223 | // ] 224 | // }, 225 | 226 | // 404 page must be placed at the end !!! 227 | { path: '*', redirect: '/404', hidden: true } 228 | ] 229 | 230 | const createRouter = () => new Router({ 231 | mode: 'history', // require service support 232 | scrollBehavior: () => ({ y: 0 }), 233 | routes: constantRoutes 234 | }) 235 | 236 | const router = createRouter() 237 | 238 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 239 | export function resetRouter() { 240 | const newRouter = createRouter() 241 | router.matcher = newRouter.matcher // reset router 242 | } 243 | 244 | export default router 245 | -------------------------------------------------------------------------------- /browser/src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'Code Chain', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: false, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether show the logo in sidebar 14 | */ 15 | sidebarLogo: false 16 | } 17 | -------------------------------------------------------------------------------- /browser/src/store/getters.js: -------------------------------------------------------------------------------- 1 | export const sidebar = state => state.app.sidebar 2 | export const device = state => state.app.device 3 | export const token = state => state.user.token 4 | export const avatar = state => state.user.avatar 5 | export const name = state => state.user.name 6 | 7 | // blockchain 8 | export const blockchain = state => state.blockchain 9 | export const blocks = state => state.blockchain.blocks 10 | export const config = state => state.config 11 | export const consensusState = state => state.blockchain.consensusState 12 | export const dumpConsensusState = state => state.blockchain.dumpConsensusState 13 | export const materials = state => state.materials 14 | export const nodes = state => state.blockchain.nodes 15 | export const roundStep = state => state.blockchain.roundStep 16 | export const validators = state => state.blockchain.validators 17 | export const bc = state => state.blockchain 18 | 19 | // TODO rename to lastBlockHeader? such as it doesn't include block txs 20 | export const latestBlock = (state, getters) => { 21 | let { blocks } = getters 22 | if (blocks && blocks.length >= 1) { 23 | return blocks[0].header 24 | } else { 25 | return { 26 | height: 0, 27 | time: "", 28 | last_commit_hash: "", 29 | num_txs: 0, // txs in this block 30 | total_txs: 0 // total txs in blockchain at the moment of this block 31 | } 32 | } 33 | } 34 | 35 | export const totalBlocks = (state, getters) => parseInt(getters.latestBlock.height) 36 | export const totalTxs = (state, getters) => parseInt(getters.latestBlock.total_txs) 37 | 38 | // Abount account 39 | export const account = state => state.account.tem_new_account 40 | -------------------------------------------------------------------------------- /browser/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | // import getters from './getters' 5 | import * as getters from './getters' 6 | // import * as actions from './actions' 7 | 8 | import app from './modules/app' 9 | import settings from './modules/settings' 10 | import user from './modules/user' 11 | import blockchain from './modules/blockchain' 12 | import config from './modules/config' 13 | import account from './modules/account' 14 | 15 | Vue.use(Vuex) 16 | 17 | const store = new Vuex.Store({ 18 | modules: { 19 | app, 20 | settings, 21 | user, 22 | blockchain, 23 | config, 24 | account 25 | }, 26 | getters 27 | // actions 28 | }) 29 | 30 | export default store 31 | -------------------------------------------------------------------------------- /browser/src/store/modules/account.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const defaultAccount = { 4 | address: '', 5 | error: '', 6 | privateKey: '', 7 | publicKey: '' 8 | } 9 | 10 | const state = { 11 | tem_new_account: defaultAccount 12 | } 13 | 14 | const actions = { 15 | async add_new_account({ commit }) { 16 | let json = await axios.post(`http://localhost:4000/account/new`, {}) 17 | let result = json.data 18 | commit('set_tem_new_account', result) 19 | return Promise.resolve() 20 | }, 21 | delete_new_account({ commit }) { 22 | commit('delete_tem_new_accunt') 23 | } 24 | } 25 | 26 | const mutations = { 27 | set_tem_new_account(state, account) { 28 | console.log(`正在创建新的账户`) 29 | state.tem_new_account = account 30 | }, 31 | delete_tem_new_accunt(state) { 32 | console.log(`正在清除临时账户`) 33 | state.tem_new_account = defaultAccount 34 | } 35 | } 36 | 37 | export default { 38 | namespaced: true, 39 | state, 40 | actions, 41 | mutations 42 | } 43 | -------------------------------------------------------------------------------- /browser/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /browser/src/store/modules/blockchain.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { RpcClient } from "tendermint" 3 | 4 | const state = { 5 | // rpc: "http://172.16.62.48:26659", 6 | rpc: 'http://localhost:26657', 7 | status: { 8 | listen_addr: "", 9 | sync_info: { 10 | latest_block_height: 0, 11 | latest_block_hash: "" 12 | }, 13 | node_info: { 14 | version: null, 15 | network: null, 16 | moniker: null 17 | } 18 | }, 19 | nodes: [], 20 | validators: [], 21 | consensusState: {}, 22 | dumpConsensusState: {}, 23 | blocks: [], 24 | roundStep: "" 25 | } 26 | 27 | // const client = RpcClient("ws://172.16.62.48:26659") 28 | const client = RpcClient("ws://localhost:26657") 29 | 30 | const actions = { 31 | subNewBlock({ commit, dispatch }) { 32 | client.subscribe({ query: "tm.event = 'NewBlock'" }, event => { 33 | commit("addBlock", event.block) 34 | // check for new nodes every 10 blocks 35 | if (event.block.header.height % 10 === 0) { 36 | dispatch("getNodes") 37 | dispatch("getValidators") 38 | } 39 | }) 40 | }, 41 | subRoundStep({ commit, dispatch }) { 42 | let eventName = "NewRoundStep" 43 | client.subscribe({ query: `tm.event = '${eventName}'` }, event => { 44 | let stepName = event.step 45 | let step 46 | switch (stepName) { 47 | case "RoundStepPropose": 48 | step = 0 49 | dispatch("getDumpConsensusState") 50 | break 51 | case "RoundStepPrevote": 52 | step = 1 53 | break 54 | case "RoundStepPrecommit": 55 | step = 2 56 | break 57 | case "RoundStepCommit": 58 | step = 3 59 | break 60 | case "RoundStepNewHeight": 61 | step = 4 62 | break 63 | } 64 | commit("setRoundStep", step) 65 | }) 66 | }, 67 | async getStatus({ commit }) { 68 | let json = await axios.get(`${state.rpc}/status`) 69 | let status = json.data.result 70 | commit("setStatus", status) 71 | return Promise.resolve() 72 | }, 73 | async getNodes({ state, commit }) { 74 | let json = await axios.get(`${state.rpc}/net_info`) 75 | let nodes = json.data.result.peers 76 | commit("setNodes", nodes) 77 | return Promise.resolve() 78 | }, 79 | async getValidators({ state, commit, dispatch }) { 80 | let json = await axios.get(`${state.rpc}/validators`) 81 | commit("setValidators", json.data.result.validators) 82 | dispatch("updateValidatorAvatars") 83 | return Promise.resolve() 84 | }, 85 | async getLastBlock({ state, commit }) { 86 | let json = await axios.get(`${state.rpc}/block`) 87 | commit("addBlock", json.data.result.block) 88 | return Promise.resolve() 89 | }, 90 | async getConsensusState({ state, commit }) { 91 | let json = await axios.get(`${state.rpc}/consensus_state`) 92 | let consensusState = json.data.result.round_state 93 | commit("setConsensusState", consensusState) 94 | return Promise.resolve() 95 | }, 96 | async getDumpConsensusState({ state, commit }) { 97 | let json = await axios.get(`${state.rpc}/dump_consensus_state`) 98 | commit("setDumpConsensusState", json.data.result) 99 | return Promise.resolve() 100 | }, 101 | async updateValidatorAvatars({ state, commit }) { 102 | state.validators.map(async validator => { 103 | if (validator.description && validator.description.identity) { 104 | let urlPrefix = 105 | "https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=" 106 | let fullUrl = urlPrefix + validator.description.identity 107 | let json = await axios.get(fullUrl) 108 | if (json.data.status.name === "OK") { 109 | let user = json.data.them[0] 110 | if (user.pictures && user.pictures.primary) { 111 | commit("setValidatorAvatar", { 112 | validatorOwner: validator.owner, 113 | avatarUrl: user.pictures.primary.url 114 | }) 115 | } 116 | } 117 | } 118 | }) 119 | } 120 | } 121 | 122 | const mutations = { 123 | setUrl(state, value) { 124 | state.rpc = value 125 | }, 126 | setStatus(state, value) { 127 | state.status = value 128 | }, 129 | setValidators(state, value) { 130 | if (value) { 131 | // add some default ugly avatars 132 | let validators = value.map(v => { 133 | v.avatarUrl = "http://via.placeholder.com/94/191F24/FFFFFF?text=?" 134 | return v 135 | }) 136 | state.validators = validators 137 | } 138 | }, 139 | setNodes(state, value) { 140 | let nodes = value 141 | nodes.push(state.status) 142 | state.nodes = nodes 143 | }, 144 | identifyValidator(state, { address, node_info }) { 145 | let validator = state.validators.find(v => v.address === address) 146 | validator.node_info = node_info 147 | }, 148 | setValidatorAvatar(state, { validatorOwner, avatarUrl }) { 149 | let validator = state.validators.find(v => v.owner === validatorOwner) 150 | validator.avatarUrl = avatarUrl 151 | }, 152 | setConsensusState(state, value) { 153 | state.consensusState = value 154 | }, 155 | setDumpConsensusState(state, value) { 156 | state.dumpConsensusState = value 157 | }, 158 | setProposer(state, address) { 159 | let proposer = state.validators.find(v => v.address === address) 160 | if (proposer) { 161 | proposer.isProposer = true 162 | state.validators.map(v => { 163 | if (v.address !== address) { 164 | v.isProposer = false 165 | } 166 | }) 167 | } 168 | }, 169 | addBlock(state, block) { 170 | state.blocks.unshift(block) 171 | const maxBlocks = 100 172 | if (state.blocks.length > maxBlocks) { 173 | state.blocks = state.blocks.slice(0, maxBlocks) 174 | } 175 | }, 176 | setRoundStep(state, step) { 177 | state.roundStep = step 178 | } 179 | } 180 | 181 | export default { 182 | namespaced: true, 183 | state, 184 | actions, 185 | mutations 186 | } 187 | -------------------------------------------------------------------------------- /browser/src/store/modules/config.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | activeMenu: '', 3 | blockchainSelect: false, 4 | desktop: false 5 | } 6 | 7 | const mutations = { 8 | SET_CONFIG_BLOCKCHAIN_SELECT (state, value) { 9 | state.blockchainSelect = value 10 | }, 11 | setActiveMenu (state, value) { 12 | state.activeMenu = value 13 | }, 14 | SET_CONFIG_DESKTOP (state, value) { 15 | state.desktop = value 16 | } 17 | } 18 | 19 | export default { 20 | namespaced: true, 21 | state, 22 | mutations 23 | } 24 | -------------------------------------------------------------------------------- /browser/src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | sidebarLogo: sidebarLogo 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | if (state.hasOwnProperty(key)) { 14 | state[key] = value 15 | } 16 | } 17 | } 18 | 19 | const actions = { 20 | changeSetting({ commit }, data) { 21 | commit('CHANGE_SETTING', data) 22 | } 23 | } 24 | 25 | export default { 26 | namespaced: true, 27 | state, 28 | mutations, 29 | actions 30 | } 31 | 32 | -------------------------------------------------------------------------------- /browser/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, logout, getInfo } from '@/api/user' 2 | import { getToken, setToken, removeToken } from '@/utils/auth' 3 | import { resetRouter } from '@/router' 4 | 5 | const getDefaultState = () => { 6 | return { 7 | token: getToken(), 8 | name: '', 9 | avatar: '' 10 | } 11 | } 12 | 13 | const state = getDefaultState() 14 | 15 | const mutations = { 16 | RESET_STATE: (state) => { 17 | Object.assign(state, getDefaultState()) 18 | }, 19 | SET_TOKEN: (state, token) => { 20 | state.token = token 21 | }, 22 | SET_NAME: (state, name) => { 23 | state.name = name 24 | }, 25 | SET_AVATAR: (state, avatar) => { 26 | state.avatar = avatar 27 | } 28 | } 29 | 30 | const actions = { 31 | // user login 32 | login({ commit }, userInfo) { 33 | const { username, password } = userInfo 34 | return new Promise((resolve, reject) => { 35 | login({ username: username.trim(), password: password }).then(response => { 36 | const { data } = response 37 | commit('SET_TOKEN', data.token) 38 | setToken(data.token) 39 | resolve() 40 | }).catch(error => { 41 | reject(error) 42 | }) 43 | }) 44 | }, 45 | 46 | // get user info 47 | getInfo({ commit, state }) { 48 | return new Promise((resolve, reject) => { 49 | getInfo(state.token).then(response => { 50 | const { data } = response 51 | 52 | if (!data) { 53 | reject('Verification failed, please Login again.') 54 | } 55 | 56 | const { name, avatar } = data 57 | 58 | commit('SET_NAME', name) 59 | commit('SET_AVATAR', avatar) 60 | resolve(data) 61 | }).catch(error => { 62 | reject(error) 63 | }) 64 | }) 65 | }, 66 | 67 | // user logout 68 | logout({ commit, state }) { 69 | return new Promise((resolve, reject) => { 70 | logout(state.token).then(() => { 71 | removeToken() // must remove token first 72 | resetRouter() 73 | commit('RESET_STATE') 74 | resolve() 75 | }).catch(error => { 76 | reject(error) 77 | }) 78 | }) 79 | }, 80 | 81 | // remove token 82 | resetToken({ commit }) { 83 | return new Promise(resolve => { 84 | removeToken() // must remove token first 85 | commit('RESET_STATE') 86 | resolve() 87 | }) 88 | } 89 | } 90 | 91 | export default { 92 | namespaced: true, 93 | state, 94 | mutations, 95 | actions 96 | } 97 | 98 | -------------------------------------------------------------------------------- /browser/src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | 46 | // to fix el-date-picker css style 47 | .el-range-separator { 48 | box-sizing: content-box; 49 | } 50 | -------------------------------------------------------------------------------- /browser/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /browser/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /browser/src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: $sideBarWidth; 7 | position: relative; 8 | } 9 | 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: $sideBarWidth !important; 13 | background-color: $menuBg; 14 | height: 100%; 15 | position: fixed; 16 | font-size: 0px; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 1001; 21 | overflow: hidden; 22 | 23 | // reset element-ui css 24 | .horizontal-collapse-transition { 25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 26 | } 27 | 28 | .scrollbar-wrapper { 29 | overflow-x: hidden !important; 30 | } 31 | 32 | .el-scrollbar__bar.is-vertical { 33 | right: 0px; 34 | } 35 | 36 | .el-scrollbar { 37 | height: 100%; 38 | } 39 | 40 | &.has-logo { 41 | .el-scrollbar { 42 | height: calc(100% - 50px); 43 | } 44 | } 45 | 46 | .is-horizontal { 47 | display: none; 48 | } 49 | 50 | a { 51 | display: inline-block; 52 | width: 100%; 53 | overflow: hidden; 54 | } 55 | 56 | .svg-icon { 57 | margin-right: 16px; 58 | } 59 | 60 | .el-menu { 61 | border: none; 62 | height: 100%; 63 | width: 100% !important; 64 | } 65 | 66 | // menu hover 67 | .submenu-title-noDropdown, 68 | .el-submenu__title { 69 | &:hover { 70 | background-color: $menuHover !important; 71 | } 72 | } 73 | 74 | .is-active>.el-submenu__title { 75 | color: $subMenuActiveText !important; 76 | } 77 | 78 | & .nest-menu .el-submenu>.el-submenu__title, 79 | & .el-submenu .el-menu-item { 80 | min-width: $sideBarWidth !important; 81 | background-color: $subMenuBg !important; 82 | 83 | &:hover { 84 | background-color: $subMenuHover !important; 85 | } 86 | } 87 | } 88 | 89 | .hideSidebar { 90 | .sidebar-container { 91 | width: 54px !important; 92 | } 93 | 94 | .main-container { 95 | margin-left: 54px; 96 | } 97 | 98 | .submenu-title-noDropdown { 99 | padding: 0 !important; 100 | position: relative; 101 | 102 | .el-tooltip { 103 | padding: 0 !important; 104 | 105 | .svg-icon { 106 | margin-left: 20px; 107 | } 108 | } 109 | } 110 | 111 | .el-submenu { 112 | overflow: hidden; 113 | 114 | &>.el-submenu__title { 115 | padding: 0 !important; 116 | 117 | .svg-icon { 118 | margin-left: 20px; 119 | } 120 | 121 | .el-submenu__icon-arrow { 122 | display: none; 123 | } 124 | } 125 | } 126 | 127 | .el-menu--collapse { 128 | .el-submenu { 129 | &>.el-submenu__title { 130 | &>span { 131 | height: 0; 132 | width: 0; 133 | overflow: hidden; 134 | visibility: hidden; 135 | display: inline-block; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | .el-menu--collapse .el-menu .el-submenu { 143 | min-width: $sideBarWidth !important; 144 | } 145 | 146 | // mobile responsive 147 | .mobile { 148 | .main-container { 149 | margin-left: 0px; 150 | } 151 | 152 | .sidebar-container { 153 | transition: transform .28s; 154 | width: $sideBarWidth !important; 155 | } 156 | 157 | &.hideSidebar { 158 | .sidebar-container { 159 | pointer-events: none; 160 | transition-duration: 0.3s; 161 | transform: translate3d(-$sideBarWidth, 0, 0); 162 | } 163 | } 164 | } 165 | 166 | .withoutAnimation { 167 | 168 | .main-container, 169 | .sidebar-container { 170 | transition: none; 171 | } 172 | } 173 | } 174 | 175 | // when menu collapsed 176 | .el-menu--vertical { 177 | &>.el-menu { 178 | .svg-icon { 179 | margin-right: 16px; 180 | } 181 | } 182 | 183 | .nest-menu .el-submenu>.el-submenu__title, 184 | .el-menu-item { 185 | &:hover { 186 | // you can use $subMenuHover 187 | background-color: $menuHover !important; 188 | } 189 | } 190 | 191 | // the scroll bar appears when the subMenu is too long 192 | >.el-menu--popup { 193 | max-height: 100vh; 194 | overflow-y: auto; 195 | 196 | &::-webkit-scrollbar-track-piece { 197 | background: #d3dce6; 198 | } 199 | 200 | &::-webkit-scrollbar { 201 | width: 6px; 202 | } 203 | 204 | &::-webkit-scrollbar-thumb { 205 | background: #99a9bf; 206 | border-radius: 20px; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /browser/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /browser/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /browser/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'vue_admin_template_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /browser/src/utils/date.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export const readableDate = ms => { 4 | return moment(ms).format('YYYY-MM-DD hh:mm:ss') 5 | } 6 | -------------------------------------------------------------------------------- /browser/src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Code Chain' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /browser/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string | null} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 21 | time = parseInt(time) 22 | } 23 | if ((typeof time === 'number') && (time.toString().length === 10)) { 24 | time = time * 1000 25 | } 26 | date = new Date(time) 27 | } 28 | const formatObj = { 29 | y: date.getFullYear(), 30 | m: date.getMonth() + 1, 31 | d: date.getDate(), 32 | h: date.getHours(), 33 | i: date.getMinutes(), 34 | s: date.getSeconds(), 35 | a: date.getDay() 36 | } 37 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { 38 | const value = formatObj[key] 39 | // Note: getDay() returns 0 on Sunday 40 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 41 | return value.toString().padStart(2, '0') 42 | }) 43 | return time_str 44 | } 45 | 46 | /** 47 | * @param {number} time 48 | * @param {string} option 49 | * @returns {string} 50 | */ 51 | export function formatTime(time, option) { 52 | if (('' + time).length === 10) { 53 | time = parseInt(time) * 1000 54 | } else { 55 | time = +time 56 | } 57 | const d = new Date(time) 58 | const now = Date.now() 59 | 60 | const diff = (now - d) / 1000 61 | 62 | if (diff < 30) { 63 | return '刚刚' 64 | } else if (diff < 3600) { 65 | // less 1 hour 66 | return Math.ceil(diff / 60) + '分钟前' 67 | } else if (diff < 3600 * 24) { 68 | return Math.ceil(diff / 3600) + '小时前' 69 | } else if (diff < 3600 * 24 * 2) { 70 | return '1天前' 71 | } 72 | if (option) { 73 | return parseTime(time, option) 74 | } else { 75 | return ( 76 | d.getMonth() + 77 | 1 + 78 | '月' + 79 | d.getDate() + 80 | '日' + 81 | d.getHours() + 82 | '时' + 83 | d.getMinutes() + 84 | '分' 85 | ) 86 | } 87 | } 88 | 89 | /** 90 | * @param {string} url 91 | * @returns {Object} 92 | */ 93 | export function param2Obj(url) { 94 | const search = url.split('?')[1] 95 | if (!search) { 96 | return {} 97 | } 98 | return JSON.parse( 99 | '{"' + 100 | decodeURIComponent(search) 101 | .replace(/"/g, '\\"') 102 | .replace(/&/g, '","') 103 | .replace(/=/g, '":"') 104 | .replace(/\+/g, ' ') + 105 | '"}' 106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /browser/src/utils/num.js: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral' 2 | 3 | function fiveNines(x) { 4 | return (Math.round(x * 10000) / 100).toFixed(3) + '%' 5 | } 6 | function usd(x) { 7 | return numeral(x).format('$0,0.00') 8 | } 9 | function usdInt(x) { 10 | return numeral(x).format('$0,0') 11 | } 12 | function full(x) { 13 | return numeral(x).format('0,0.00000000') 14 | } 15 | function pretty(x) { 16 | return numeral(x).format('0,0.000') 17 | } 18 | function prettyInt(x) { 19 | return numeral(x).format('0,0') 20 | } 21 | function short(x) { 22 | if (x > 1000000000) { 23 | return pretty(x / 1000000000) + 'B' 24 | } 25 | if (x > 1000000) { 26 | return pretty(x / 1000000) + 'M' 27 | } 28 | if (x > 1000) { 29 | return pretty(x / 1000) + 'K' 30 | } 31 | return numeral(x).format('0,0.00') 32 | } 33 | function shortInt(x) { 34 | if (x > 1000000000) { 35 | return integerize(x / 1000000000) + 'B' 36 | } 37 | if (x > 1000000) { 38 | return integerize(x / 1000000) + 'MM' 39 | } 40 | if (x > 1000) { 41 | return integerize(x / 1000) + 'K' 42 | } 43 | return integerize(x) 44 | } 45 | function integerize(x) { 46 | return numeral(x).format('0,0') 47 | } 48 | function fractionize(x) { 49 | return numeral(x).format('.00') 50 | } 51 | 52 | export default { 53 | fiveNines, 54 | usd, 55 | usdInt, 56 | full: full, 57 | pretty: pretty, 58 | prettyInt: prettyInt, 59 | int: integerize, 60 | frac: fractionize, 61 | short: short, 62 | shortInt 63 | } 64 | -------------------------------------------------------------------------------- /browser/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['X-Token'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (res.code !== 20000) { 50 | Message({ 51 | message: res.message || 'Error', 52 | type: 'error', 53 | duration: 5 * 1000 54 | }) 55 | 56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 58 | // to re-login 59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 60 | confirmButtonText: 'Re-Login', 61 | cancelButtonText: 'Cancel', 62 | type: 'warning' 63 | }).then(() => { 64 | store.dispatch('user/resetToken').then(() => { 65 | location.reload() 66 | }) 67 | }) 68 | } 69 | return Promise.reject(new Error(res.message || 'Error')) 70 | } else { 71 | return res 72 | } 73 | }, 74 | error => { 75 | console.log('err' + error) // for debug 76 | Message({ 77 | message: error.message, 78 | type: 'error', 79 | duration: 5 * 1000 80 | }) 81 | return Promise.reject(error) 82 | } 83 | ) 84 | 85 | export default service 86 | -------------------------------------------------------------------------------- /browser/src/utils/tx.js: -------------------------------------------------------------------------------- 1 | import b64 from 'base64-js' 2 | 3 | // See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 4 | export const encodeBase64 = (str, encoding = 'utf-8') => { 5 | let bytes = new TextEncoder(encoding).encode(str) 6 | // let bytes = new (TextEncoder || TextEncoderLite)(encoding).encode(str) 7 | return b64.fromByteArray(bytes) 8 | } 9 | 10 | // See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 11 | export const decodeBase64 = (str, encoding = 'utf-8') => { 12 | let bytes = b64.toByteArray(str) 13 | // no-undef 14 | return new TextDecoder(encoding).decode(bytes) 15 | // return new (TextDecoder || TextDecoderLite)(encoding).decode(bytes) 16 | } 17 | 18 | export const decodeTx = (base64str) => { 19 | let str = decodeBase64(base64str) 20 | let idx = str.indexOf('{') 21 | let json = str.substring(idx) 22 | return JSON.parse(json) 23 | } 24 | -------------------------------------------------------------------------------- /browser/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | -------------------------------------------------------------------------------- /browser/src/utils/votingValidators.js: -------------------------------------------------------------------------------- 1 | export default function(validators) { 2 | if (validators && validators.length > 0) { 3 | return validators.filter(v => !v.revoked) 4 | } else { 5 | return [] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /browser/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | 229 | -------------------------------------------------------------------------------- /browser/src/views/account/index.vue: -------------------------------------------------------------------------------- 1 |