diff --git a/.circleci/config.yml b/.circleci/config.yml index de87fb03df..b558f73e95 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,7 +32,7 @@ jobs: changelogUpdated: docker: - - image: circleci/node:10.13.0-browsers + - image: circleci/node:10.15.0-browsers steps: - checkout @@ -41,7 +41,7 @@ jobs: testUnit: docker: - - image: circleci/node:10.13.0-browsers + - image: circleci/node:10.15.0-browsers steps: - checkout @@ -51,8 +51,8 @@ jobs: - v3-dependencies-root-{{ checksum "package.json" }} - v3-dependencies-root- + - run: sudo npm -g uninstall yarn ; sudo npm -g install yarn@1.13.0 - run: yarn install - - save_cache: paths: - yarn.lock @@ -70,9 +70,33 @@ jobs: command: bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN when: on_success + security: + docker: + - image: circleci/node:10.15.0-browsers + + steps: + - checkout + + - run: sudo npm -g uninstall yarn ; sudo npm -g install yarn@1.13.0 + + - run: + name: Audit + command: | + set +e + + SUMMARY="$(yarn audit | grep Severity)" + VULNERABILITIES=".*(High|Critical).*" + + if [[ $SUMMARY =~ $VULNERABILITIES ]]; then + echo "Unsafe dependencies found: $SUMMARY" >&2 + exit 1 + fi + echo "Your dependencies are secure enough: $SUMMARY" + exit 0 + testE2e: docker: - - image: circleci/node:10.13.0-browsers + - image: circleci/node:10.15.0-browsers environment: - BINARY_PATH: "/home/circleci/project/builds/Gaia/linux_amd64/gaiacli" @@ -89,8 +113,8 @@ jobs: - v2-dependencies-root-{{ checksum "package.json" }} - v2-dependencies-root- + - run: sudo npm -g uninstall yarn ; sudo npm -g install yarn@1.13.0 - run: yarn install - - save_cache: paths: - yarn.lock @@ -109,7 +133,7 @@ jobs: # Create release. release: docker: - - image: circleci/node:10.13.0 + - image: circleci/node:10.15.0 steps: - checkout @@ -119,13 +143,14 @@ jobs: - v2-dependencies-root-{{ checksum "package.json" }} - v2-dependencies-root- + - run: sudo npm -g uninstall yarn ; sudo npm -g install yarn@1.13.0 - run: yarn install - run: node tasks/createReleasePR.js # Publish the release to GitHub. publish: docker: - - image: circleci/node:10.13.0 + - image: circleci/node:10.15.0 steps: - checkout @@ -145,6 +170,8 @@ jobs: git tag -d release-candidate git push --delete bot release-candidate + npm -g uninstall yarn ; npm -g install yarn@1.13.0 + sudo tasks/build/installWine.sh yarn install @@ -182,9 +209,14 @@ workflows: branches: ignore: release - - testE2e: - requires: - - buildGaia + # - testE2e: + # requires: + # - buildGaia + # filters: + # branches: + # ignore: release + + - security: filters: branches: ignore: release @@ -194,7 +226,7 @@ workflows: - changelogUpdated - buildGaia - testUnit - - testE2e + # - testE2e filters: branches: only: develop diff --git a/.gitignore b/.gitignore index c663c999fa..227ce832ef 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,5 @@ testArtifacts/* app/networks/local-testnet/* docs/cosmos-voyager .idea/* -server_dev.key -server_dev.crt -Cosmos_* -app/networks/* -app/package.json +*.crt +*.key diff --git a/CHANGELOG.md b/CHANGELOG.md index 46510d0e9e..f9214e1501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - [\#1660](https://github.com/cosmos/voyager/issues/1660) Add parameters and pool to store @fedekunze - [\#1739](https://github.com/cosmos/voyager/issues/1739) Init jsDoc into project @sabau - [\#1674](https://github.com/cosmos/voyager/issues/1674) Add PageProfile component with shared styles for validator and proposal profiles @jbibla +- [\#1806](https://github.com/cosmos/voyager/issues/1806) CircleCI security check in dependencies with yarn audit @sabau +- [\#1804](https://github.com/cosmos/voyager/issues/1804) Moved Voyager to the web @faboweb - [\#1835](https://github.com/cosmos/voyager/issues/1835) allow user to use different signing methods @faboweb ### Changed @@ -154,6 +156,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - [\#1572](https://github.com/cosmos/voyager/issues/1572) Fixed scroll bug when switching between tabs @jbibla - [\#1749](https://github.com/cosmos/voyager/issues/1749) Fixed proposal tally update after voting @fedekunze - [\#1765](https://github.com/cosmos/voyager/pull/1765) Fixed proposal deposit update after submitting a deposit @fedekunze +- [\#1791](https://github.com/cosmos/voyager/issue/1791) Fixed a problem with initializing the Voyager config dir @faboweb +- [\#1815](https://github.com/cosmos/voyager/issue/1815) Fixed getters for proposals denominator, reverted to 945803d586b83d65547cd16f4cd5994eac2957ea until interfaces are ready @sabau +- [\#1809](https://github.com/cosmos/voyager/issue/1809) Fixed optimistically updating the header on sending @faboweb - [\#1791](https://github.com/cosmos/voyager/pull/1791) Fixed a problem with initializing the Voyager config dir @faboweb - [\#1754](https://github.com/cosmos/voyager/pull/1754) Fixed form UX, UI and other miscellaneous styling issues @jbibla - [\#1707](https://github.com/cosmos/voyager/issues/1707) Governance txs are now disabled if the user doesn't hold any min_deposit token @fedekunze @@ -162,6 +167,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - [\#1818](https://github.com/cosmos/voyager/issues/1818) Fixed error on validator loading showing as no validators @faboweb - Fixed error locations sucked up by Sentry @faboweb - [\#1815](https://github.com/cosmos/voyager/pull/1785) Fixed small bug on preferences page @jbibla +- [\#1831](https://github.com/cosmos/voyager/issues/1831) Fixed websocket reconnection @faboweb +- [\#1850](https://github.com/cosmos/voyager/pull/1850) Snapshots aligned for unit tests @sabau +- [\#1859](https://github.com/cosmos/voyager/pull/1859) Fix security check in circleci @sabau ## [0.10.7] - 2018-10-10 diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000000..30fdf8cf6a --- /dev/null +++ b/Caddyfile @@ -0,0 +1,13 @@ +localhost:8080 { + cors + + header / { + Access-Control-Allow-Origin "*" + Access-Control-Allow-Methods "OPTIONS, HEAD, GET, POST, PUT, DELETE" + Access-Control-Allow-Headers "*" + } + + proxy / https://localhost:9070/ { + insecure_skip_verify + } +} \ No newline at end of file diff --git a/README.md b/README.md index e5fbde647e..366025f3bb 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ cd voyager yarn install ``` +--- + +## Voyager Development + ### Gaia (Cosmos SDK) Since Voyager runs on top of the Cosmos Hub blockchain, we also need to install Gaia (the Cosmos Hub application) and download the supported testnets. @@ -63,7 +67,13 @@ To connect to a testnet, Voyager needs the configuration files of those networks yarn build:testnets ``` -## Voyager Development +### Caddy Proxy + +Currently we need a proxy to enable easy local development. We use [Caddy](https://caddyserver.com). To download run: + +```bash +curl https://getcaddy.com | bash -s personal http.cors +``` ### Active testnets @@ -73,15 +83,40 @@ To run Voyager on the default testnet (if active): yarn start ``` -To run Voyager on a specific testnet you can use the following command. Click [here](https://github.com/cosmos/testnets) for a complete list of the supported official and community testnets. +## Local testnet + +Sometimes you may want to run a local node, i.e. in the case there is no available network. To do so first [Build Gaia](#gaia-cosmos-sdk), then use our automatic script or the manual process to set up your node. + +### Generate SSL certificates + +If you want to have a predictable environment for Voyager please generate some ssl certificates: ```bash -yarn start +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout server_dev.key -out server_dev.crt \ + -subj "/C=US/ST=CA/L=Irvine/O=Acme Inc./CN=localhost" \ + -reqexts v3_req -reqexts SAN -extensions SAN \ + -config \ + <(echo -e ' + [req]\n + distinguished_name=req_distinguished_name\n + [req_distinguished_name]\n + [SAN]\n + subjectKeyIdentifier=hash\n + authorityKeyIdentifier=keyid:always,issuer:always\n + basicConstraints=CA:TRUE\n + subjectAltName=@alt_names + [alt_names] + DNS.1 = localhost + ') ``` -## Local testnet +Afterwards you can use: -Sometimes you may want to run a local node, i.e. in the case there is no available network. To do so first [Build Gaia](#gaia-cosmos-sdk), then use our automatic script or the manual process to set up your node. +```bash +yarn backend:fixed-https +yarn frontend:fixed-https +``` ### Build diff --git a/app/config.toml b/app/config.toml deleted file mode 100644 index 223ff6c8d9..0000000000 --- a/app/config.toml +++ /dev/null @@ -1,21 +0,0 @@ -# Name of electron app -# Will be used in production builds -name = "Cosmos Voyager" - -# webpack-dev-server port -wds_port = 9080 -lcd_port = 9070 -lcd_port_prod = 9071 -relay_port = 9060 -relay_port_prod = 9061 -default_tendermint_port = 26657 - -default_network = "game_of_stakes_3" -node_lcd = "https://fabo.interblock.io:1317" -node_rpc = "https://fabo.interblock.io:26657" - -google_analytics_uid = "UA-51029217-3" -sentry_dsn = "https://4dee9f70a7d94cc0959a265c45902d84:cbf160384aab4cdeafbe9a08dee3b961@sentry.io/288169" - -# time to wait for a block until node is declared halted -node_halted_timeout = 120000 diff --git a/app/index.ejs b/app/index.ejs index 98861f9afd..4556a8a5fe 100644 --- a/app/index.ejs +++ b/app/index.ejs @@ -6,13 +6,10 @@ Cosmos Voyager <% if (htmlWebpackPlugin.options.appModules) { %> - - <% } %> +
diff --git a/app/src/config.js b/app/src/config.js deleted file mode 100644 index 7d28535f0e..0000000000 --- a/app/src/config.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict" - -const fs = require(`fs`) -const path = require(`path`) -const toml = require(`toml`) - -module.exports = toml.parse( - fs.readFileSync(path.join(__dirname, `../config.toml`), { - encoding: `utf8` - }) -) diff --git a/app/src/config.json b/app/src/config.json new file mode 100644 index 0000000000..e2b21975f0 --- /dev/null +++ b/app/src/config.json @@ -0,0 +1,12 @@ +{ + "name": "Cosmos Voyager", + "default_network": "local-testnet", + "denoms": ["stake", "photino"], + "node_lcd": "http://localhost:8080", + "node_rpc": "http://localhost:26657", + "google_analytics_uid": "UA-51029217-3", + "sentry_dsn": "https://4dee9f70a7d94cc0959a265c45902d84:cbf160384aab4cdeafbe9a08dee3b961@sentry.io/288169", + "node_halted_timeout": 120000, + "block_timeout": 10000, + "default_gas": 500000 +} diff --git a/app/src/main/addressbook.js b/app/src/main/addressbook.js deleted file mode 100644 index e427ee2a39..0000000000 --- a/app/src/main/addressbook.js +++ /dev/null @@ -1,171 +0,0 @@ -"use strict" - -const fs = require(`fs-extra`) -const { join } = require(`path`) -const axios = require(`axios`) - -const LOGGING = JSON.parse(process.env.LOGGING || `true`) !== false -const FIXED_NODE = process.env.COSMOS_NODE - -module.exports = class Addressbook { - constructor( - config, - configPath, - { persistent_peers = [], onConnectionMessage = () => {} } = {} - ) { - this.peers = [] - this.config = config - this.onConnectionMessage = onConnectionMessage - - // if we define a fixed node, we skip persistence - if (FIXED_NODE) { - this.addPeer(FIXED_NODE) - return - } - - this.addressbookPath = join(configPath, `addressbook.json`) - this.loadFromDisc() - - // add persistent peers to already stored peers - persistent_peers.forEach(peer => this.addPeer(peer)) - - if (persistent_peers.length > 0) { - this.persistToDisc() - } - } - - // adds the new peer to the list of peers - addPeer(peerHost) { - const peerIsKnown = this.peers.find( - peer => peer.host.indexOf(peerHost) !== -1 - ) - - if (!peerIsKnown) { - LOGGING && console.log(`Adding new peer:`, peerHost) - this.peers.push({ - host: peerHost, - // assume that new peers are available - state: `available` - }) - } - } - - loadFromDisc() { - // if there is no address book file yet, there are no peers stored yet - // the file will be created when persisting any peers to disc - let exists = fs.existsSync(this.addressbookPath) - if (!exists) { - this.peers = [] - return - } - let content = fs.readFileSync(this.addressbookPath, `utf8`) - let peers = JSON.parse(content) - this.peers = peers.map(host => ({ - host, - state: `available` - })) - } - - persistToDisc() { - let peers = this.peers - // only remember available nodes - .filter(p => p.state === `available`) - .map(p => p.host) - fs.ensureFileSync(this.addressbookPath) - fs.writeFileSync(this.addressbookPath, JSON.stringify(peers), `utf8`) - } - - // returns an available node or throws if it can't find any - async pickNode() { - let curNode - - if (FIXED_NODE) { - // we skip discovery for fixed nodes as we want to always return the same - // node - curNode = { host: FIXED_NODE } - - // ping fixed node - let alive = await axios - .get( - `http://${FIXED_NODE}:${ - this.config.default_tendermint_port - }/net_info`, - { timeout: 3000 } - ) - .then(() => true, () => false) - if (!alive) - throw Error(`The fixed node you tried to connect to is not reachable.`) - } else { - let availableNodes = this.peers.filter(node => node.state === `available`) - if (availableNodes.length === 0) { - throw Error(`No nodes available to connect to`) - } - // pick a random node - curNode = - availableNodes[Math.floor(Math.random() * availableNodes.length)] - - try { - let peerIP = curNode.host.split(`:`)[0] - await this.discoverPeers(peerIP) - } catch (exception) { - console.log( - `Unable to discover peers from node ${require(`util`).inspect( - curNode - )}: ${exception}` - ) - - this.flagNodeOffline(curNode.host) - return this.pickNode() - } - - // remember the peers of the node and store them in the addressbook - this.persistToDisc() - } - - this.onConnectionMessage(`Picked node: ` + curNode.host) - return `${curNode.host.split(`:`)[0]}:${ - this.config.default_tendermint_port - }` // export the picked node with the correct tendermint port - } - - flagNodeOffline(host) { - let peer = this.peers.find(p => p.host === host) - if (peer) peer.state = `down` - } - - flagNodeIncompatible(host) { - let peer = this.peers.find(p => p.host === host) - if (peer) peer.state = `incompatible` - } - - resetNodes() { - this.peers = this.peers.map(peer => - Object.assign({}, peer, { - state: `available` - }) - ) - } - - async discoverPeers(peerIP) { - this.onConnectionMessage(`Querying node: ${peerIP}`) - - let subPeers = (await axios.get( - `http://${peerIP}:${this.config.default_tendermint_port}/net_info`, - { timeout: 3000 } - )).data.result.peers - - this.onConnectionMessage(`Node ${peerIP} is alive.`) - - let subPeersHostnames = subPeers.map(peer => peer.node_info.listen_addr) - - subPeersHostnames - // add new peers to state - .forEach(subPeerHostname => { - this.addPeer(subPeerHostname) - }) - - if (subPeersHostnames.length > 0) { - this.persistToDisc() - } - } -} diff --git a/app/src/main/index.dev.js b/app/src/main/index.dev.js deleted file mode 100644 index 374b59ba07..0000000000 --- a/app/src/main/index.dev.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict" - -/** - * This file is used specifically and only for development. It enables the use of ES6+ - * features for the main process and installs `electron-debug` & `vue-devtools`. There - * shouldn't be any need to modify this file, but it can be used to extend your - * development environment. - */ - -/* eslint-disable no-console */ - -// Set babel `env` and install `babel-register` -process.env.BABEL_ENV = `main` - -require(`babel-register`)({ ignore: /node_modules/ }) - -// Install `vue-devtools` -require(`electron`).app.on(`ready`, () => { - let installExtension = require(`electron-devtools-installer`) - installExtension - .default(installExtension.VUEJS_DEVTOOLS) - .then(() => {}) - .catch(err => { - console.log(`Unable to install \`vue-devtools\`: \n`, err) - }) -}) - -// Require `main` process to boot app -require(`./index`) diff --git a/app/src/main/index.js b/app/src/main/index.js deleted file mode 100644 index 94e85d998e..0000000000 --- a/app/src/main/index.js +++ /dev/null @@ -1,699 +0,0 @@ -"use strict" - -const assert = require(`assert`) -let { app, BrowserWindow, ipcMain } = require(`electron`) -let fs = require(`fs-extra`) -const https = require(`https`) -let { join, relative } = require(`path`) -let childProcess = require(`child_process`) -let semver = require(`semver`) -const Sentry = require(`@sentry/node`) -const readline = require(`readline`) -let axios = require(`axios`) - -let { version: pkgVersion } = require(`../../../package.json`) -let addMenu = require(`./menu.js`) -let config = require(`../config.js`) -config.node_lcd = process.env.LCD_URL || config.node_lcd -config.node_rpc = process.env.RPC_URL || config.node_rpc -let LcdClient = require(`../renderer/connectors/lcdClient.js`) -global.config = config // to make the config accessable from renderer -global.config.version = pkgVersion - -require(`electron-debug`)() - -let shuttingDown = false -let mainWindow -let gaiaLiteProcess -let streams = [] -let connecting = true -let chainId -let booted = false -let expectedGaiaCliVersion - -const root = require(`../root.js`) -global.root = root // to make the root accessable from renderer -const networkPath = require(`../network.js`).path - -const lcdHome = join(root, `lcd`) -const WIN = /^win/.test(process.platform) -const DEV = process.env.NODE_ENV === `development` -const TEST = process.env.NODE_ENV === `testing` -global.config.development = DEV || TEST -// TODO default logging or default disable logging? -const LOGGING = JSON.parse(process.env.LOGGING || `true`) !== false -const winURL = DEV - ? `http://localhost:${config.wds_port}` - : `file://${__dirname}/index.html` -const LCD_PORT = DEV ? config.lcd_port : config.lcd_port_prod -const MOCK = - process.env.COSMOS_MOCKED !== undefined - ? JSON.parse(process.env.COSMOS_MOCKED) - : false -global.config.mocked = MOCK // persist resolved mock setting also in config used by view thread -const gaiaVersion = fs - .readFileSync(networkPath + `/gaiaversion.txt`) - .toString() - .split(`-`)[0] -process.env.GAIA_VERSION = gaiaVersion - -let LCD_BINARY_NAME = `gaiacli` + (WIN ? `.exe` : ``) - -function log(...args) { - if (LOGGING) { - console.log(...args) - } -} -function logError(...args) { - if (LOGGING) { - console.log(...args) - } -} - -function logProcess(process, logPath) { - fs.ensureFileSync(logPath) - // Writestreams are blocking fs cleanup in tests, if you get errors, disable logging - if (LOGGING) { - let logStream = fs.createWriteStream(logPath, { flags: `a` }) // 'a' means appending (old data will be preserved) - streams.push(logStream) - process.stdout.pipe(logStream) - process.stderr.pipe(logStream) - } -} - -function handleCrash(error) { - afterBooted(() => { - if (mainWindow) { - mainWindow.webContents.send(`error`, { - message: error - ? error.message - ? error.message - : error - : `An unspecified error occurred` - }) - } - }) -} - -async function shutdown() { - if (shuttingDown) return - - mainWindow = null - shuttingDown = true - - if (gaiaLiteProcess) { - await stopLCD() - } - - return Promise.all( - streams.map(stream => new Promise(resolve => stream.close(resolve))) - ).then(() => { - log(`[SHUTDOWN] Voyager has shutdown`) - }) -} - -function createWindow() { - mainWindow = new BrowserWindow({ - show: false, - minWidth: 320, - minHeight: 480, - width: 1024, - height: 768, - center: true, - title: `Cosmos Voyager`, - darkTheme: true, - titleBarStyle: `hidden`, - backgroundColor: `#15182d`, - webPreferences: { webSecurity: false } - }) - mainWindow.once(`ready-to-show`, () => { - setTimeout(() => { - mainWindow.show() - if (DEV || JSON.parse(process.env.COSMOS_DEVTOOLS || `false`)) { - mainWindow.webContents.openDevTools() - // we need to reload at this point to make sure sourcemaps are loaded correctly - mainWindow.reload() - } - if (DEV) { - mainWindow.maximize() - } - }, 300) - }) - - // start vue app - mainWindow.loadURL(winURL) - - mainWindow.on(`closed`, shutdown) - - // eslint-disable-next-line no-console - log(`mainWindow opened`) - - // handle opening external links in OS's browser - let webContents = mainWindow.webContents - let handleRedirect = (e, url) => { - if (url !== webContents.getURL()) { - e.preventDefault() - require(`electron`).shell.openExternal(url) - } - } - webContents.on(`will-navigate`, handleRedirect) - webContents.on(`new-window`, handleRedirect) - - addMenu(mainWindow) -} - -function startProcess(name, args, env) { - let binPath - if (process.env.BINARY_PATH) { - binPath = process.env.BINARY_PATH - } else if (DEV) { - // in development use the build gaia files from running `yarn build:gaia` - const osFolderName = (function() { - switch (process.platform) { - case `win32`: - return `windows_amd64` - case `darwin`: - return `darwin_amd64` - case `linux`: - return `linux_amd64` - } - })() - binPath = join(__dirname, `../../../builds/Gaia`, osFolderName, name) - } else { - // in production mode, use binaries packaged with app - binPath = join(__dirname, `..`, `bin`, name) - } - - let argString = args.map(arg => JSON.stringify(arg)).join(` `) - log(`spawning ${binPath} with args "${argString}"`) - let child - try { - child = childProcess.spawn(binPath, args, env) - } catch (error) { - log(`Err: Spawning ${name} failed`, error) - throw error - } - child.stdout.on(`data`, data => !shuttingDown && log(`${name}: ${data}`)) - child.stderr.on(`data`, data => !shuttingDown && log(`${name}: ${data}`)) - - // Make stdout more useful by emitting a line at a time. - readline.createInterface({ input: child.stdout }).on(`line`, line => { - child.stdout.emit(`line`, line) - }) - - // Make stderr more useful by emitting a line at a time. - readline.createInterface({ input: child.stderr }).on(`line`, line => { - child.stderr.emit(`line`, line) - }) - - child.on( - `exit`, - code => !shuttingDown && log(`${name} exited with code ${code}`) - ) - child.on(`error`, async function(error) { - if (!(shuttingDown && error.code === `ECONNRESET`)) { - // if we throw errors here, they are not handled by the main process - let errorMessage = [ - `[Uncaught Exception] Child`, - name, - `produced an unhandled exception:`, - error - ] - logError(...errorMessage) - console.error(...errorMessage) // also output to console for easier debugging - handleCrash(error) - - Sentry.captureException(error) - } - }) - - // need to kill child processes if main process dies - process.on(`exit`, () => { - child.kill() - }) - return child -} - -app.on(`window-all-closed`, () => { - app.quit() -}) - -app.on(`activate`, () => { - if (mainWindow === null) { - createWindow() - } -}) - -app.on(`ready`, () => createWindow()) - -// start lcd REST API -async function startLCD(home, nodeURL) { - assert.equal( - gaiaLiteProcess, - null, - `Can't start Gaia Lite because it's already running. Call StopLCD first.` - ) - - let lcdStarted = false // remember if the lcd has started to toggle the right error handling if it crashes async - return new Promise(async (resolve, reject) => { - log(`startLCD`, home) - let child = startProcess(LCD_BINARY_NAME, [ - `rest-server`, - `--laddr`, - `tcp://localhost:${LCD_PORT}`, - `--home`, - home, - `--node`, - nodeURL, - `--chain-id`, - chainId, - `--trust-node`, - true - ]) - logProcess(child, join(home, `lcd.log`)) - - child.stdout.on(`line`, line => { - if (/\(cert: "(.+?)"/.test(line)) { - const certPath = /\(cert: "(.+?)"/.exec(line)[1] - resolve({ ca: fs.readFileSync(certPath, `utf8`), process: child }) - lcdStarted = true - child.stdout.removeAllListeners(`line`) - } - }) - - child.stderr.on(`line`, error => { - let errorMessage = `The gaiacli rest-server (LCD) experienced an error:\n${error.toString( - `utf8` - )}`.substr(0, 1000) - lcdStarted - ? handleCrash(errorMessage) // if fails later - : reject(errorMessage) // if fails immediatly - }) - }) -} - -function stopLCD() { - return new Promise((resolve, reject) => { - if (!gaiaLiteProcess) { - resolve() - return - } - log(`Stopping the LCD server`) - try { - // prevent the exit to signal bad termination warnings - gaiaLiteProcess.removeAllListeners(`exit`) - gaiaLiteProcess.on(`exit`, () => { - gaiaLiteProcess = null - resolve() - }) - gaiaLiteProcess.kill(`SIGKILL`) - } catch (error) { - handleCrash(error) - reject(`Stopping the LCD resulted in an error: ${error.message}`) - } - }) -} - -async function getGaiacliVersion() { - let child = startProcess(LCD_BINARY_NAME, [`version`]) - let data = await new Promise(resolve => { - child.stdout.on(`data`, resolve) - }) - return data.toString(`utf8`).trim() -} - -function exists(path) { - try { - fs.accessSync(path) - return true - } catch (error) { - if (error.code !== `ENOENT`) throw error - return false - } -} - -// this function will call the passed in callback when the view is booted -// the purpose is to send events to the view thread only after it is ready to receive those events -// if we don't do this, the view thread misses out on those (i.e. an error that occures before the view is ready) -function afterBooted(cb) { - // in tests we trigger the booted callback always, this causes those events to be sent twice - // this is why we skip the callback if the message was sent already - let sent = false - ipcMain.on(`booted`, () => { - cb() - sent = true - }) - if (booted && !sent) { - cb() - } -} - -/* - * log to file - */ -function setupLogging(root) { - if (!LOGGING) return - - // initialize log file - let logFilePath = join(root, `main.log`) - fs.ensureFileSync(logFilePath) - let mainLog = fs.createWriteStream(logFilePath, { flags: `a` }) // 'a' means appending (old data will be preserved) - mainLog.write(`${new Date()} Running Cosmos-UI\r\n`) - // mainLog.write(`${new Date()} Environment: ${JSON.stringify(process.env)}\r\n`) // TODO should be filtered before adding it to the log - streams.push(mainLog) - - log(`Redirecting console output to logfile`, logFilePath) - // redirect stdout/err to logfile - // TODO overwriting console.log sounds like a bad idea, can we find an alternative? - // eslint-disable-next-line no-func-assign - log = function(...args) { - if (DEV) { - console.log(...args) - } - mainLog.write(`main-process: ${args.join(` `)}\r\n`) - } - // eslint-disable-next-line no-func-assign - logError = function(...args) { - if (DEV) { - console.error(...args) - } - mainLog.write(`main-process: ${args.join(` `)}\r\n`) - } -} - -if (!TEST) { - process.on(`exit`, shutdown) - // on uncaught exceptions we wait so the sentry event can be sent - process.on(`uncaughtException`, async function(error) { - logError(`[Uncaught Exception]`, error) - Sentry.captureException(error) - handleCrash(error) - }) - process.on(`unhandledRejection`, async function(error) { - logError(`[Unhandled Promise Rejection]`, error) - Sentry.captureException(error) - handleCrash(error) - }) -} - -const eventHandlers = { - booted: () => { - log(`View has booted`) - booted = true - }, - - "error-collection": (event, optin) => { - if (optin) { - Sentry.init({ - dsn: config.sentry_dsn, - release: `voyager@${pkgVersion}` - }) - } else { - Sentry.init({}) - } - }, - - mocked: value => { - global.config.mocked = value - }, - - reconnect: () => reconnect(), - - "stop-lcd": () => stopLCD(), - - "successful-launch": () => { - console.log(`[START SUCCESS] Vue app successfuly started`) - } -} - -// handle ipc messages from the renderer process -Object.entries(eventHandlers).forEach(([event, handler]) => { - ipcMain.on(event, handler) -}) - -// test an actual node version against the expected one and flag the node if incompatible -async function testNodeVersion(client, expectedGaiaVersion) { - let result = await client.nodeVersion() - let nodeVersion = result.split(`-`)[0] - let semverDiff = semver.diff(nodeVersion, expectedGaiaVersion) - if (semverDiff === `patch` || semverDiff === null) { - return { compatible: true, nodeVersion } - } - - return { compatible: false, nodeVersion } -} - -// Proxy requests to Axios through the main process because we need -// Node.js in order to support self-signed TLS certificates. -const AxiosListener = axios => { - return async (event, id, options) => { - let response - - try { - response = { - value: await axios(options) - } - } catch (exception) { - response = { exception } - } - - event.sender.send(`Axios/${id}`, response) - } -} - -// check if our node is reachable and the SDK version is compatible with the local one -async function pickAndConnect() { - let nodeURL = config.node_lcd - connecting = true - let certificate - - try { - certificate = (await connect(nodeURL)).ca - } catch (error) { - handleCrash(error) - return - } - - // make the tls certificate available to the view process - // https://en.wikipedia.org/wiki/Certificate_authority - global.config.ca = certificate - // TODO reenable certificate - const axiosInstance = axios.create({ - httpsAgent: new https.Agent({ - // ca: certificate - rejectUnauthorized: false - }) - }) - - let compatible, nodeVersion - try { - const client = LcdClient(axiosInstance, config.node_lcd) - const out = await testNodeVersion(client, expectedGaiaCliVersion) - - compatible = out.compatible - nodeVersion = out.nodeVersion - } catch (error) { - logError( - `Error in getting node SDK version, assuming node is incompatible. Error:`, - error - ) - await stopLCD() - - // retry - setTimeout(pickAndConnect, 2000) - return - } - - if (!compatible) { - let message = `Node ${nodeURL} uses SDK version ${nodeVersion} which is incompatible to the version used in Voyager ${expectedGaiaCliVersion}` - log(message) - await stopLCD() - - // retry - setTimeout(pickAndConnect, 2000) - return - } - - ipcMain.removeAllListeners(`Axios`) - ipcMain.on(`Axios`, AxiosListener(axiosInstance)) - - afterBooted(() => { - log(`Signaling connected node`) - mainWindow.webContents.send(`connected`, { - lcdURL: config.node_lcd, - rpcURL: config.node_rpc - }) - }) -} - -async function connect() { - log(`starting gaia rest server with nodeURL ${config.node_rpc}`) - - const { ca, process } = await startLCD(lcdHome, config.node_rpc) - gaiaLiteProcess = process - log(`gaia rest server ready`) - - connecting = false - return { ca, process } -} - -async function reconnect() { - if (connecting) return - log(`Starting reconnect`) - connecting = true - - await stopLCD() - - await pickAndConnect() -} - -function checkConsistentConfigDir( - appVersionPath, - genesisPath, - configPath, - gaiacliVersionPath -) { - let missingFile = - (!exists(genesisPath) && genesisPath) || - (!exists(appVersionPath) && appVersionPath) || - (!exists(configPath) && configPath) || - (!exists(gaiacliVersionPath) && gaiacliVersionPath) - if (missingFile) { - throw Error( - `The data directory (${root}) is missing ${relative(root, missingFile)}` - ) - } else { - let existingVersion = fs.readFileSync(appVersionPath, `utf8`).trim() - let semverDiff = semver.diff(existingVersion, pkgVersion) - let compatible = semverDiff !== `major` && semverDiff !== `minor` - if (compatible) { - log(`configs are compatible with current app version`) - } else { - // TODO: versions of the app with different data formats will need to learn how to - // migrate old data - throw Error(`Data was created with an incompatible app version - data=${existingVersion} app=${pkgVersion}`) - } - } -} - -const checkGaiaCompatibility = async gaiacliVersionPath => { - // XXX: currently ignores commit hash - let gaiacliVersion = (await getGaiacliVersion()).split(`-`)[0] - - expectedGaiaCliVersion = fs - .readFileSync(gaiacliVersionPath, `utf8`) - .trim() - .split(`-`)[0] - - log( - `gaiacli version: "${gaiacliVersion}", expected: "${expectedGaiaCliVersion}"` - ) - - let compatible = - semver.major(gaiacliVersion) == semver.major(expectedGaiaCliVersion) && - semver.minor(gaiacliVersion) == semver.minor(expectedGaiaCliVersion) - - if (!compatible) { - throw Error( - `The network you are trying to connect to requires gaia ${expectedGaiaCliVersion}, but the version Voyager is using is ${gaiacliVersion}.${ - DEV - ? ` Please update "tasks/build/Gaia/COMMIT.sh" with the required version and run "yarn build:gaia".` - : `` - }` - ) - } -} - -async function main() { - // Sentry is used for automatic error reporting. It is turned off by default. - Sentry.init({}) - - let appVersionPath = join(root, `app_version`) - let genesisPath = join(root, `genesis.json`) - let configPath = join(root, `config.toml`) - let gaiacliVersionPath = join(root, `gaiaversion.txt`) - - let rootExists = exists(root) - await fs.ensureDir(root) - - setupLogging(root) - - if (rootExists) { - log(`root exists (${root})`) - - // NOTE: when changing this code, always make sure the app can never - // overwrite/delete existing data without at least backing it up, - // since it may contain the user's private keys and they might not - // have written down their seed words. - // they might get pretty mad if the app deletes their money! - - // check if the existing data came from a compatible app version - // if not, fail with an error - checkConsistentConfigDir( - appVersionPath, - genesisPath, - configPath, - gaiacliVersionPath - ) - - // check to make sure the genesis.json we want to use matches the one - // we already have. if it has changed, replace it with the new one - let existingGenesis = fs.readFileSync(genesisPath, `utf8`) - let genesisJSON = JSON.parse(existingGenesis) - // skip this check for local testnet - if (genesisJSON.chain_id !== `local`) { - let specifiedGenesis = fs.readFileSync( - join(networkPath, `genesis.json`), - `utf8` - ) - if (existingGenesis.trim() !== specifiedGenesis.trim()) { - fs.copySync(networkPath, root) - log( - `genesis.json at "${genesisPath}" was overridden by genesis.json from "${networkPath}"` - ) - } - } - } else { - log(`initializing data directory (${root})`) - await fs.ensureDir(root) - - // copy predefined genesis.json and config.toml into root - fs.accessSync(networkPath) // crash if invalid path - await fs.copyFile( - join(networkPath, `config.toml`), - join(root, `config.toml`) - ) - await fs.copyFile( - join(networkPath, `gaiaversion.txt`), - join(root, `gaiaversion.txt`) - ) - await fs.copyFile( - join(networkPath, `genesis.json`), - join(root, `genesis.json`) - ) - - fs.writeFileSync(appVersionPath, pkgVersion) - } - - await checkGaiaCompatibility(gaiacliVersionPath) - - // read chainId from genesis.json - let genesisText = fs.readFileSync(genesisPath, `utf8`) - let genesis = JSON.parse(genesisText) - chainId = genesis.chain_id // is set globaly - - // choose one random node to start from - await pickAndConnect() -} -module.exports = main() - .catch(error => { - logError(error) - handleCrash(error) - }) - .then(() => ({ - shutdown, - processes: { gaiaLiteProcess }, - eventHandlers, - getGaiaLiteProcess: () => gaiaLiteProcess - })) diff --git a/app/src/main/menu.js b/app/src/main/menu.js deleted file mode 100644 index 34d20acb25..0000000000 --- a/app/src/main/menu.js +++ /dev/null @@ -1,83 +0,0 @@ -"use strict" - -const { app, Menu, shell, dialog } = require(`electron`) -const { join } = require(`path`) - -module.exports = function() { - let template = [ - { - label: `Cosmos Voyager`, - submenu: [ - { - label: `About Cosmos Voyager`, - selector: `orderFrontStandardAboutPanel:`, - click: () => openAboutMenu() - }, - { type: `separator` }, - { - label: `Quit`, - accelerator: `Command+Q`, - click: () => app.quit() - } - ] - }, - { - label: `Edit`, - submenu: [ - { - label: `Cut`, - accelerator: `CmdOrCtrl+X`, - selector: `cut:` - }, - { - label: `Copy`, - accelerator: `CmdOrCtrl+C`, - selector: `copy:` - }, - { - label: `Paste`, - accelerator: `CmdOrCtrl+V`, - selector: `paste:` - } - ] - }, - { - label: `Help`, - submenu: [ - { - label: `Report An Issue`, - click() { - shell.openExternal(`https://github.com/cosmos/voyager/issues/new`) - } - }, - { - label: `View Application Log`, - click() { - shell.openItem(global.root + `/main.log`) - } - } - ] - } - ] - - let menu = Menu.buildFromTemplate(template) - Menu.setApplicationMenu(menu) -} - -function openAboutMenu() { - const voyagerVersion = require(`../../../package.json`).version - const gaiaVersion = process.env.GAIA_VERSION - const electronVersion = app.getVersion() - - const imageLocation = - process.env.NODE_ENV === `development` - ? join(__dirname, `../renderer/assets/images`) - : join(__dirname, `./imgs`) - - dialog.showMessageBox({ - type: `info`, - title: `About Voyager`, - message: `Versions\n\nVoyager ${voyagerVersion}\nCosmos SDK ${gaiaVersion}\nElectron ${electronVersion}`, - icon: join(imageLocation, `cosmos-logo.png`) - }) -} diff --git a/app/src/network.js b/app/src/network.js index 18e89302a5..1d8bbcdebf 100644 --- a/app/src/network.js +++ b/app/src/network.js @@ -1,19 +1,17 @@ -"use strict" +/* istanbul ignore file */ +import axios from "axios" -let { join } = require(`path`) -let { readFileSync } = require(`fs-extra`) -let config = require(`./config.js`) +let config = require(`./config.json`) -// this network gets used if none is specified via the -// COSMOS_NETWORK env var -let DEFAULT_NETWORK = join(__dirname, `../networks/` + config.default_network) -let networkPath = process.env.COSMOS_NETWORK || DEFAULT_NETWORK +const networkPath = `../networks/${config.default_network}` -let genesisText = readFileSync(join(networkPath, `genesis.json`), `utf8`) -let genesis = JSON.parse(genesisText) -let networkName = genesis.chain_id +export default async function() { + let genesis = (await axios(`${networkPath}/genesis.json`)).data + let networkName = genesis.chain_id -module.exports = { - path: networkPath, - name: networkName + return { + genesis, + path: networkPath, + name: networkName + } } diff --git a/app/src/renderer/App.vue b/app/src/renderer/App.vue index 8de5c5722f..e601a9a3f8 100644 --- a/app/src/renderer/App.vue +++ b/app/src/renderer/App.vue @@ -7,13 +7,11 @@
- + - - @@ -23,8 +21,6 @@ import AppHeader from "common/AppHeader" import TmNotifications from "common/TmNotifications" import ModalError from "common/TmModalError" import ModalHelp from "common/TmModalHelp" -import ModalLcdApproval from "common/TmModalLCDApproval" -import ModalNodeHalted from "common/TmModalNodeHalted" import Onboarding from "common/TmOnboarding" import Session from "common/TmSession" import store from "./vuex/store" @@ -36,9 +32,6 @@ import store from "./vuex/store" * @vue-data {Object} nothing * @vue-computed {function} notifications mapGetter * @vue-computed {function} config mapGetter - * @vue-computed {function} themes mapGetter - * @vue-computed {function} approval mapGetter - * @vue-computed {function} required mapGetter * @vue-computed {function} onboarding mapGetter */ export default { @@ -47,24 +40,15 @@ export default { AppHeader, ModalError, ModalHelp, - ModalLcdApproval, TmNotifications, - ModalNodeHalted, Onboarding, Session }, computed: { - ...mapGetters([ - `notifications`, - `config`, - `themes`, - `approvalRequired`, - `onboarding` - ]) + ...mapGetters([`notifications`, `config`, `onboarding`]) }, mounted() { this.$store.commit(`loadOnboarding`) - this.$store.commit(`setTheme`, `dark`) }, store } diff --git a/app/src/renderer/components/common/ActionModal.vue b/app/src/renderer/components/common/ActionModal.vue index 3c7544a70c..e92cf67bbb 100644 --- a/app/src/renderer/components/common/ActionModal.vue +++ b/app/src/renderer/components/common/ActionModal.vue @@ -155,6 +155,7 @@ export default { this.show = true }, close() { + this.password = null this.show = false }, async validateForm() { diff --git a/app/src/renderer/components/common/AnchorCopy.vue b/app/src/renderer/components/common/AnchorCopy.vue index 93df2a77f7..db3b24cddc 100644 --- a/app/src/renderer/components/common/AnchorCopy.vue +++ b/app/src/renderer/components/common/AnchorCopy.vue @@ -6,7 +6,6 @@ diff --git a/app/src/renderer/components/common/TmModalLCDApproval.vue b/app/src/renderer/components/common/TmModalLCDApproval.vue deleted file mode 100644 index b02bce3dba..0000000000 --- a/app/src/renderer/components/common/TmModalLCDApproval.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - - - diff --git a/app/src/renderer/components/common/TmModalNodeHalted.vue b/app/src/renderer/components/common/TmModalNodeHalted.vue deleted file mode 100644 index 78ed330bd4..0000000000 --- a/app/src/renderer/components/common/TmModalNodeHalted.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - diff --git a/app/src/renderer/components/common/TmNotifications.vue b/app/src/renderer/components/common/TmNotifications.vue index 950aeeb976..20b05fc417 100644 --- a/app/src/renderer/components/common/TmNotifications.vue +++ b/app/src/renderer/components/common/TmNotifications.vue @@ -9,7 +9,6 @@ :layout="notification.layout" :title="notification.title" :time="notification.time" - :theme="theme" > @@ -25,10 +24,6 @@ export default { notifications: { type: Array, required: true - }, - theme: { - type: String, - default: null } } } diff --git a/app/src/renderer/components/common/TmSessionImport.vue b/app/src/renderer/components/common/TmSessionImport.vue index 92c73efcca..4432bbfd72 100644 --- a/app/src/renderer/components/common/TmSessionImport.vue +++ b/app/src/renderer/components/common/TmSessionImport.vue @@ -1,6 +1,6 @@