diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e52599f78..8342edd599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,3 +57,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [0.4.3] - 2018-02-05 ### Changed * Renamed Cosmos UI to Cosmos Voyager. @nylira +* Added Google Analytics for testnet versions +* Added Sentry error reporting for testnet versions \ No newline at end of file diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a5c5776b89..73a70e213f 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -3,8 +3,8 @@ ### Issue - - + +closes: ISSUE ### Screenshots diff --git a/app/index.ejs b/app/index.ejs index 9f518f6bae..995a4d7b2f 100644 --- a/app/index.ejs +++ b/app/index.ejs @@ -23,3 +23,16 @@ + + +<% if (htmlWebpackPlugin.options.enableAnalytics) { %> + + + +<% } %> \ No newline at end of file diff --git a/app/src/main/index.js b/app/src/main/index.js index bfc8e41764..b60f3d24cb 100644 --- a/app/src/main/index.js +++ b/app/src/main/index.js @@ -10,6 +10,7 @@ let semver = require('semver') let event = require('event-to-promise') let toml = require('toml') let axios = require('axios') +let Raven = require('raven') let pkg = require('../../../package.json') let relayServer = require('./relayServer.js') @@ -177,6 +178,8 @@ function startProcess (name, args, env) { child.on('exit', (code) => !shuttingDown && log(`${name} exited with code ${code}`)) child.on('error', function (err) { if (!(shuttingDown && err.code === 'ECONNRESET')) { + // TODO test + Raven.captureException(err) // if we throw errors here, they are not handled by the main process console.error('[Uncaught Exception] Child', name, 'produced an unhandled exception:', err) console.log('Shutting down UI') @@ -303,15 +306,16 @@ function setupLogging (root) { if (!TEST) { process.on('exit', shutdown) + // on uncaught exceptions we wait so the sentry event can be sent process.on('uncaughtException', async function (err) { + await sleep(1000) logError('[Uncaught Exception]', err) - console.error('[Uncaught Exception]', err) await shutdown() process.exit(1) }) process.on('unhandledRejection', async function (err) { + await sleep(1000) logError('[Unhandled Promise Rejection]', err) - console.error('[Unhandled Promise Rejection]', err) await shutdown() process.exit(1) }) @@ -388,7 +392,21 @@ async function reconnect (seeds) { return nodeIP } +function setupAnalytics () { + let networkIsWhitelisted = config.analytics_networks.indexOf(config.default_network) !== -1 + if (networkIsWhitelisted) { + log('Adding analytics') + } + + // only enable sending of error events in production setups and if the network is a testnet + Raven.config(networkIsWhitelisted && process.env.NODE_ENV === 'production' ? config.sentry_dsn : '', { + captureUnhandledRejections: true + }).install() +} + async function main () { + setupAnalytics() + let appVersionPath = join(root, 'app_version') let genesisPath = join(root, 'genesis.json') let configPath = join(root, 'config.toml') @@ -421,7 +439,7 @@ async function main () { // 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=${pkg.version}`) + data=${existingVersion} app=${pkg.version}`) } } else { throw Error(`The data directory (${root}) has missing files`) @@ -463,7 +481,7 @@ async function main () { // TODO: semver check, or exact match? if (gaiaVersion !== expectedGaiaVersion) { throw Error(`Requires gaia ${expectedGaiaVersion}, but got ${gaiaVersion}. - Please update your gaia installation or build with a newer binary.`) + Please update your gaia installation or build with a newer binary.`) } // read chainId from genesis.json @@ -479,8 +497,8 @@ async function main () { } catch (e) { throw new Error(`Can't open config.toml: ${e.message}`) } - let config = toml.parse(configText) - let seeds = config.p2p.seeds.split(',').filter(x => x !== '') + let configTOML = toml.parse(configText) + let seeds = configTOML.p2p.seeds.split(',').filter(x => x !== '') if (seeds.length === 0) { throw new Error('No seeds specified in config.toml') } diff --git a/app/src/network.js b/app/src/network.js index 0a743d5752..f7c417ca7f 100644 --- a/app/src/network.js +++ b/app/src/network.js @@ -1,9 +1,10 @@ let { join } = require('path') let { readFileSync } = require('fs') +let config = require('../../config.js') // this network gets used if none is specified via the // COSMOS_NETWORK env var -let DEFAULT_NETWORK = join(__dirname, '../networks/gaia-2') +let DEFAULT_NETWORK = join(__dirname, '../networks/' + config.default_network) let networkPath = process.env.COSMOS_NETWORK || DEFAULT_NETWORK let genesisText = readFileSync(join(networkPath, 'genesis.json'), 'utf8') diff --git a/app/src/renderer/components/staking/LiDelegate.vue b/app/src/renderer/components/staking/LiDelegate.vue index 355fb587d1..063d845571 100644 --- a/app/src/renderer/components/staking/LiDelegate.vue +++ b/app/src/renderer/components/staking/LiDelegate.vue @@ -8,11 +8,13 @@ span {{ num.prettyInt(delegate.voting_power) }} .bar(:style='vpStyles') .li-delegate__value.bonded_by_you - span {{ amountBonded }} + span {{ bondedByYou }} .li-delegate__value.status span {{ delegateType }} template(v-if="userCanDelegate") - .li-delegate__value.checkbox#remove-from-cart(v-if="inCart" @click='rm(delegate)') + .li-delegate__value.checkbox(v-if="bondedByYou > 0") + i.material-icons lock + .li-delegate__value.checkbox#remove-from-cart(v-else-if="inCart" @click='rm(delegate)') i.material-icons check_box .li-delegate__value.checkbox#add-to-cart(v-else @click='add(delegate)') i.material-icons check_box_outline_blank @@ -33,12 +35,12 @@ export default { }, computed: { ...mapGetters(['shoppingCart', 'delegates', 'config', 'committedDelegations', 'user']), - amountBonded () { + bondedByYou () { return this.num.prettyInt(this.committedDelegations[this.delegate.id]) }, styles () { let value = '' - if (this.inCart) value += 'li-delegate-active ' + if (this.inCart || this.bondedByYou > 0) value += 'li-delegate-active ' if (this.delegate.isValidator) value += 'li-delegate-validator ' return value }, @@ -78,6 +80,13 @@ export default { methods: { add (delegate) { this.$store.commit('addToCart', delegate) }, rm (delegate) { this.$store.commit('removeFromCart', delegate.id) } + }, + watch: { + bondedByYou (newVal, oldVal) { + if (newVal > 0) { + this.$store.commit('addToCart', this.delegate) + } + } } } @@ -141,6 +150,7 @@ export default { &.checkbox justify-content center + cursor pointer span white-space nowrap diff --git a/app/src/renderer/components/staking/PageBond.vue b/app/src/renderer/components/staking/PageBond.vue index 8ff42fc5c9..dbe920a44c 100644 --- a/app/src/renderer/components/staking/PageBond.vue +++ b/app/src/renderer/components/staking/PageBond.vue @@ -57,7 +57,10 @@ page.page-bond(title="Bond Atoms") type="number" placeholder="Atoms" step="1" - v-model.number="d.atoms") + min="0" + :max="$v.fields.delegates.$each[index].atoms.$params.between.max" + v-model.number="d.atoms" + @change.native="limitMax(d, $event)") form-msg(name="Atoms" type="required" v-if="!$v.fields.delegates.$each[index].atoms.required") @@ -149,10 +152,6 @@ export default { oldUnbondedAtoms () { return this.totalAtoms - this.oldBondedAtoms }, - // not used - // newBondedAtoms () { - // return this.fields.delegates.reduce((sum, d) => sum + (d.atoms || 0), 0) - // }, newUnbondedAtoms () { return this.fields.delegates.reduce((atoms, d) => { let delta = d.oldAtoms - d.atoms @@ -206,8 +205,8 @@ export default { this.$store.commit('activateDelegation') try { await this.$store.dispatch('submitDelegation', this.fields) - this.$store.commit('notify', { title: 'Atoms Bonded', - body: 'You have successfully updated your delegations.' }) + this.$store.commit('notify', { title: 'Successful Delegation', + body: 'You have successfully bonded / unbonded.' }) this.$router.push('/staking') } catch (err) { this.$store.commit('notifyError', { title: 'Error While Bonding Atoms', @@ -284,14 +283,17 @@ export default { restrictSize: { min: { width: offset } } }) .on('resizemove', (event) => { - let target = event.target - let ratio = (event.rect.width - offset) / (this.bondBarOuterWidth - offset) - let rawAtoms = ratio * this.totalAtoms + this.handleResize(event.target, event.rect.width) + }) + }, + handleResize (element, width) { + let offset = this.bondBarScrubWidth + let ratio = Math.round((width - offset) / (this.bondBarOuterWidth - offset) * 100) / 100 + let rawAtoms = ratio * this.totalAtoms - target.style.width = event.rect.width + 'px' + element.style.width = width + 'px' - this.updateDelegateAtoms(target.id.split('-')[1], rawAtoms) - }) + return this.updateDelegateAtoms(element.id.split('-')[1], rawAtoms) }, updateDelegateAtoms (delegateId, rawAtoms) { let d = this.fields.delegates.find(d => d.id === delegateId) @@ -301,8 +303,8 @@ export default { d.deltaAtoms = this.delta(rawAtoms, d.oldAtoms, 'int') d.deltaAtomsPercent = this.percent(this.delta(rawAtoms, d.oldAtoms), this.totalAtoms) + return d } - return d.atoms }, setBondBarOuterWidth () { let outerBar = this.$el.querySelector('.bond-bar__outer') @@ -325,6 +327,14 @@ export default { value = Math.round(ratio * 100) } return value + '%' + }, + limitMax (delegate, event) { + let max = parseInt(event.target.max) + if (delegate.atoms >= max) { + console.log(`${delegate.atoms} <= ${max}`) + delegate.atoms = max + return + } } }, async mounted () { diff --git a/app/src/renderer/components/staking/PageDelegates.vue b/app/src/renderer/components/staking/PageDelegates.vue index 8ab7d5ee48..0cba7c9a13 100644 --- a/app/src/renderer/components/staking/PageDelegates.vue +++ b/app/src/renderer/components/staking/PageDelegates.vue @@ -30,7 +30,6 @@ page(title='Validators and Candidates') @@ -123,6 +121,7 @@ export default { left 0 right 0 z-index z(toolBar) + .label color bright line-height 2rem diff --git a/app/src/renderer/lcdClient.js b/app/src/renderer/lcdClient.js new file mode 100644 index 0000000000..ae4b76654c --- /dev/null +++ b/app/src/renderer/lcdClient.js @@ -0,0 +1,105 @@ +'use strict' + +const axios = require('axios') + +// returns an async function which makes a request for the given +// HTTP method (GET/POST/DELETE/etc) and path (/foo/bar) +function req (method, path) { + return async function (data) { + return await this.request(method, path, data) + } +} + +// returns an async function which makes a request for the given +// HTTP method and path, which accepts arguments to be appended +// to the path (/foo/{arg}/...) +function argReq (method, path) { + return async function (args, data) { + // `args` can either be a single value or an array + if (Array.isArray(args)) { + args = args.join('/') + } + return await this.request(method, `${path}/${args}`, data) + } +} + +class Client { + constructor (server = 'http://localhost:8998') { + this.server = server + } + + async request (method, path, data) { + try { + let res = await axios[method.toLowerCase()](this.server + path, data) + return res.data + } catch (resError) { + if (!resError.response || !resError.response.data) { + throw resError + } + let data = resError.response.data + // server responded with error message, create an Error from that + let error = Error(data.error) + error.code = data.code + throw error + } + } +} + +let fetchAccount = argReq('GET', '/query/account') +let fetchNonce = argReq('GET', '/query/nonce') + +Object.assign(Client.prototype, { + sign: req('POST', '/sign'), + postTx: req('POST', '/tx'), + + // keys + generateKey: req('POST', '/keys'), + listKeys: req('GET', '/keys'), + getKey: argReq('GET', '/keys'), + updateKey: argReq('PUT', '/keys'), + deleteKey: argReq('DELETE', '/keys'), + recoverKey: req('POST', '/keys/recover'), + + // coins + buildSend: req('POST', '/build/send'), + async queryAccount (address) { + try { + return await fetchAccount.call(this, address) + } catch (err) { + // if account not found, return null instead of throwing + if (err.message.includes('account bytes are empty')) { + return null + } + throw err + } + }, + coinTxs: argReq('GET', '/tx/coin'), + + // nonce + async queryNonce (address) { + try { + return await fetchNonce.call(this, address) + } catch (err) { + // if nonce not found, return 0 instead of throwing + if (err.message.includes('nonce empty')) { + return 0 + } + throw err + } + }, + + // Tendermint RPC + status: req('GET', '/tendermint/status'), + + // staking + candidate: argReq('GET', '/query/stake/candidates'), + candidates: req('GET', '/query/stake/candidates'), + buildDelegate: req('POST', '/build/stake/delegate'), + buildUnbond: req('POST', '/build/stake/unbond'), + bondingsByDelegator: argReq('GET', '/tx/bondings/delegator'), + bondingsByValidator: argReq('GET', '/tx/bondings/validator') + + // TODO: separate API registration for different modules +}) + +module.exports = Client diff --git a/app/src/renderer/main.js b/app/src/renderer/main.js index 5042de94d4..9454bec942 100644 --- a/app/src/renderer/main.js +++ b/app/src/renderer/main.js @@ -5,16 +5,33 @@ import Router from 'vue-router' import Vuelidate from 'vuelidate' import shrinkStacktrace from '../helpers/shrink-stacktrace.js' import axios from 'axios' +import Raven from 'raven-js' +import {remote} from 'electron' + +const config = require('../../../config') import App from './App' import routes from './routes' import Node from './node' import Store from './vuex/store' +// setup sentry remote error reporting on testnets +const networkIsWhitelisted = config.analytics_networks.indexOf(config.default_network) !== -1 +Raven.config(networkIsWhitelisted && remote.getGlobal('process').env.NODE_ENV === 'production' ? config.sentry_dsn : '').install() + +// handle uncaught errors +window.addEventListener('unhandledrejection', function (event) { + Raven.captureException(event.reason) +}) +window.addEventListener('error', function (event) { + Raven.captureException(event.reason) +}) Vue.config.errorHandler = (error, vm, info) => { + Raven.captureException(error) shrinkStacktrace(error) return true } + Vue.use(Electron) Vue.use(Resource) Vue.use(Router) diff --git a/app/src/renderer/node.js b/app/src/renderer/node.js index dc29d7d7a0..4699891385 100644 --- a/app/src/renderer/node.js +++ b/app/src/renderer/node.js @@ -1,6 +1,6 @@ 'use strict' const RpcClient = require('tendermint') -const RestClient = require('cosmos-sdk') +const RestClient = require('./lcdClient.js') module.exports = function (nodeIP, relayPort, lcdPort) { const RELAY_SERVER = 'http://localhost:' + relayPort diff --git a/app/src/renderer/vuex/modules/delegates.js b/app/src/renderer/vuex/modules/delegates.js index 5ec7ba6cd6..2a5698faef 100644 --- a/app/src/renderer/vuex/modules/delegates.js +++ b/app/src/renderer/vuex/modules/delegates.js @@ -1,4 +1,5 @@ import axios from 'axios' +import indicateValidators from 'scripts/indicateValidators' export default ({ dispatch, node }) => { const state = { @@ -24,14 +25,15 @@ export default ({ dispatch, node }) => { } const actions = { - async getDelegates ({ state, dispatch }) { + async getDelegates ({ state, dispatch, rootState }) { state.loading = true let delegatePubkeys = (await node.candidates()).data - let delegates = await Promise.all(delegatePubkeys.map(pubkey => { + await Promise.all(delegatePubkeys.map(pubkey => { return dispatch('getDelegate', pubkey) })) + state.delegates = indicateValidators(state.delegates, rootState.config.maxValidators) state.loading = false - return delegates + return state.delegates }, async getDelegate ({ commit }, pubkey) { let delegate = (await axios.get(`http://localhost:${node.relayPort}/query/stake/candidate/${pubkey.data}`)).data.data diff --git a/config.js b/config.js index 04855b61b0..eb2dffe301 100644 --- a/config.js +++ b/config.js @@ -30,7 +30,12 @@ let config = { overwrite: true, platform: process.env.PLATFORM_TARGET || 'darwin,linux,win32', packageManager: 'yarn' - } + }, + + default_network: 'gaia-2', + analytics_networks: ['gaia-2', 'gaia-3-dev', 'gaia-3'], + google_analytics: 'UA-51029217-3', + sentry_dsn: 'https://4dee9f70a7d94cc0959a265c45902d84:cbf160384aab4cdeafbe9a08dee3b961@sentry.io/288169' } config.building.name = config.name diff --git a/package.json b/package.json index e8a93a5372..cb5c234869 100644 --- a/package.json +++ b/package.json @@ -96,13 +96,11 @@ "@nylira/vue-button": "^4.3.2", "@nylira/vue-field": "^1.1.17", "@nylira/vue-form-msg": "^1.0.3", - "@nylira/vue-input": "^3.2.0", "@nylira/vue-notifications": "^1.4.4", "@vue/test-utils": "^1.0.0-beta.11", "axios": "^0.17.0", "casual": "^1.5.19", "chart.js": "^2.6.0", - "cosmos-sdk": "^1.5.1", "deterministic-tar": "^0.1.2", "deterministic-zip": "^1.0.5", "electron": "^1.7.5", @@ -121,6 +119,8 @@ "no-scroll": "^2.1.0", "numeral": "^2.0.6", "perfect-scrollbar": "^1.3.0", + "raven": "^2.4.1", + "raven-js": "^3.22.3", "semver": "^5.4.1", "shortid": "^2.2.8", "stacktrace-js": "^2.0.0", diff --git a/tasks/release.js b/tasks/release.js index ddd61fd143..bb63e65632 100644 --- a/tasks/release.js +++ b/tasks/release.js @@ -124,9 +124,18 @@ function zipFolder (inDir, outDir, version) { return reject(err) } files - .filter(file => !fs.lstatSync(file).isDirectory()) .forEach(file => { - zip.file(path.relative(inDir, file), fs.readFileSync(file), {date: new Date('1987-08-16')}) // make the zip deterministic by changing all file times + // make the zip deterministic by changing all file times + if (fs.lstatSync(file).isDirectory()) { + zip.file(path.relative(inDir, file), null, { + dir: true, + date: new Date('1993-06-16') + }) + } else { + zip.file(path.relative(inDir, file), fs.readFileSync(file), { + date: new Date('1987-08-16') + }) + } }) resolve() }) @@ -142,34 +151,44 @@ function zipFolder (inDir, outDir, version) { }) } -function tarFolder (inDir, outDir, version) { - return new Promise(async (resolve, reject) => { - let name = path.parse(inDir).name - let outFile = path.join(outDir, `${name}_${version}.tar.gz`) - var pack = tar.pack() +async function tarFolder (inDir, outDir, version) { + let name = path.parse(inDir).name + let outFile = path.join(outDir, `${name}_${version}.tar.gz`) + var pack = tar.pack() - await new Promise((resolve) => { - glob(inDir + '/**/*', (err, files) => { - if (err) { - return reject(err) - } - // add files to tar - files - .filter(file => !fs.lstatSync(file).isDirectory()) - .forEach(file => { - try { - pack.entry({ name: path.relative(inDir, file) }, fs.readFileSync(file)) - } catch (err) { - console.error(`Couldn't pack file`, file, err) - // skip this file - } - }) - pack.finalize() - resolve() + let files = glob(inDir + '/**', { sync: true }) + + // add files to tar + for (let file of files) { + try { + let stats = fs.lstatSync(file) + + let contents, linkname, type + if (stats.isDirectory()) { + continue + } else if (stats.isSymbolicLink()) { + linkname = fs.readlinkSync(file) + type = 'symlink' + } else { + contents = fs.readFileSync(file) + type = 'file' + } + await new Promise((resolve) => { + pack.entry(Object.assign({}, stats, { + name: path.relative(inDir, file), + type, + linkname + }), contents, resolve) }) - }) + } catch (err) { + console.error(`Couldn't pack file`, file, err) + // skip this file + } + } + pack.finalize() - // make tar deterministic + // make tar deterministic + await new Promise((resolve) => { pack .pipe(deterministicTar()) // save tar to disc @@ -193,17 +212,17 @@ function deterministicTar () { var extract = tar.extract() .on('entry', function (header, stream, cb) { - if (header.type !== 'file') return cb() - header.mtime = header.atime = header.ctime = UNIXZERO header.uid = header.gid = 0 delete header.uname delete header.gname - header.mode = 0o777 - - stream.pipe(pack.entry(header, cb)) + if (header.type === 'file') { + stream.pipe(pack.entry(header, cb)) + } else { + pack.entry(header, cb) + } }) .on('finish', function () { pack.finalize() diff --git a/test/unit/specs/App.spec.js b/test/unit/specs/App.spec.js index 3f96ae0d62..838257c607 100644 --- a/test/unit/specs/App.spec.js +++ b/test/unit/specs/App.spec.js @@ -1,4 +1,17 @@ jest.mock('renderer/node.js', () => () => require('../helpers/node_mock')) +jest.mock('electron', () => ({ + require: jest.genMockFunction(), + match: jest.genMockFunction(), + app: jest.genMockFunction(), + remote: { + getGlobal: () => ({ + env: { + NODE_ENV: 'test' + } + }) + }, + dialog: jest.genMockFunction() +})) describe('App', () => { it('has all dependencies', async done => { diff --git a/test/unit/specs/components/staking/PageBond.spec.js b/test/unit/specs/components/staking/PageBond.spec.js index 0df32b42d4..ebd2f03b6c 100644 --- a/test/unit/specs/components/staking/PageBond.spec.js +++ b/test/unit/specs/components/staking/PageBond.spec.js @@ -94,6 +94,11 @@ describe('PageBond', () => { expect(delegate.atoms).toBe(88) }) + it('only shows percent based on showing atoms', () => { + let updatedDelegate = wrapper.vm.handleResize(wrapper.vm.$el.querySelector('#delegate-pubkeyX'), 0.12) + expect(updatedDelegate.deltaAtomsPercent).toBe('100%') + }) + it('calculates delta', () => { expect(wrapper.vm.delta(100.23293423, 90.5304934)).toBe(9.70244083) expect(wrapper.vm.delta(100, 90, 'int')).toBe(10) diff --git a/test/unit/specs/components/staking/__snapshots__/PageBond.spec.js.snap b/test/unit/specs/components/staking/__snapshots__/PageBond.spec.js.snap index e29f470808..eba8a4932d 100644 --- a/test/unit/specs/components/staking/__snapshots__/PageBond.spec.js.snap +++ b/test/unit/specs/components/staking/__snapshots__/PageBond.spec.js.snap @@ -242,6 +242,8 @@ exports[`PageBond has the expected html structure 1`] = ` -
+
candidateY @@ -97,14 +97,14 @@ exports[`PageDelegates has the expected html structure 1`] = ` 0
- Candidate + Validator
check_box_outline_blank
-
+
candidateX @@ -120,7 +120,7 @@ exports[`PageDelegates has the expected html structure 1`] = ` 0
- Candidate + Validator
check_box_outline_blank diff --git a/test/unit/specs/lcdClient.spec.js b/test/unit/specs/lcdClient.spec.js new file mode 100644 index 0000000000..775db24a2e --- /dev/null +++ b/test/unit/specs/lcdClient.spec.js @@ -0,0 +1,125 @@ +let axios = require('axios') +let LcdClient = require('../../../app/src/renderer/lcdClient.js') + +describe('LCD Client', () => { + let client = new LcdClient() + + it('makes a GET request with no args', async () => { + axios.get = jest.fn() + .mockReturnValueOnce(Promise.resolve({ + data: { foo: 'bar' } + })) + + let res = await client.status() + expect(res).toEqual({ foo: 'bar' }) + expect(axios.get.mock.calls[0]).toEqual([ + 'http://localhost:8998/tendermint/status', + undefined + ]) + }) + + it('makes a GET request with one arg', async () => { + axios.get = jest.fn() + .mockReturnValueOnce(Promise.resolve({ + data: { foo: 'bar' } + })) + + let res = await client.getKey('myKey') + expect(res).toEqual({ foo: 'bar' }) + expect(axios.get.mock.calls[0]).toEqual([ + 'http://localhost:8998/keys/myKey', + undefined + ]) + }) + + it('makes a GET request with multiple args', async () => { + axios.get = jest.fn() + .mockReturnValueOnce(Promise.resolve({ + data: { foo: 'bar' } + })) + + let res = await client.bondingsByDelegator([ 'foo', 'bar' ]) + expect(res).toEqual({ foo: 'bar' }) + expect(axios.get.mock.calls[0]).toEqual([ + 'http://localhost:8998/tx/bondings/delegator/foo/bar', + undefined + ]) + }) + + it('makes a POST request', async () => { + axios.post = jest.fn() + .mockReturnValueOnce(Promise.resolve({ + data: { foo: 'bar' } + })) + + let res = await client.generateKey() + expect(res).toEqual({ foo: 'bar' }) + expect(axios.post.mock.calls[0]).toEqual([ + 'http://localhost:8998/keys', + undefined + ]) + }) + + it('makes a POST request with args and data', async () => { + axios.put = jest.fn() + .mockReturnValueOnce(Promise.resolve({ + data: { foo: 'bar' } + })) + + let res = await client.updateKey('myKey', { abc: 123 }) + expect(res).toEqual({ foo: 'bar' }) + expect(axios.put.mock.calls[0]).toEqual([ + 'http://localhost:8998/keys/myKey', + { abc: 123 } + ]) + }) + + it('makes a GET request with an error', async () => { + axios.get = jest.fn() + .mockReturnValueOnce(Promise.reject({ + response: { + data: { + error: 'foo', + code: 123 + } + } + })) + + try { + await client.status() + } catch (err) { + expect(err.message).toBe('foo') + expect(err.code).toBe(123) + } + expect(axios.get.mock.calls[0]).toEqual([ + 'http://localhost:8998/tendermint/status', + undefined + ]) + }) + + it('does not throw error for empty results', async () => { + axios.get = jest.fn() + .mockReturnValueOnce(Promise.reject({ + response: { + data: { + error: 'account bytes are empty', + code: 1 + } + } + })) + let res = await client.queryAccount('address') + expect(res).toBe(null) + + axios.get = jest.fn() + .mockReturnValueOnce(Promise.reject({ + response: { + data: { + error: 'nonce empty', + code: 2 + } + } + })) + res = await client.queryNonce('address') + expect(res).toBe(0) + }) +}) diff --git a/test/unit/specs/main.spec.js b/test/unit/specs/main.spec.js index fe15f67ea9..2073258a12 100644 --- a/test/unit/specs/main.spec.js +++ b/test/unit/specs/main.spec.js @@ -287,8 +287,7 @@ describe('Startup Process', () => { } catch (_err) { err = _err } - expect(err.message).toBe(`Data was created with an incompatible app version - data=0.1.0 app=1.1.1`) + expect(err.message).toContain(`incompatible app version`) let appVersion = fs.readFileSync(testRoot + 'app_version', 'utf8') expect(appVersion).toBe('0.1.0') diff --git a/webpack.renderer.config.js b/webpack.renderer.config.js index 520697cbaf..83ad71f919 100644 --- a/webpack.renderer.config.js +++ b/webpack.renderer.config.js @@ -90,7 +90,8 @@ let rendererConfig = { appModules: process.env.NODE_ENV !== 'production' ? path.resolve(__dirname, 'app/node_modules') : false, - styles: stylus(fs.readFileSync('./app/src/renderer/styles/index.styl', 'utf8')).import('./app/src/renderer/styles/variables.styl').render() + styles: stylus(fs.readFileSync('./app/src/renderer/styles/index.styl', 'utf8')).import('./app/src/renderer/styles/variables.styl').render(), + enableAnalytics: process.env.NODE_ENV === 'production' && settings.analytics_networks.indexOf(settings.default_network) !== -1 }), new webpack.NoEmitOnErrorsPlugin(), // warnings caused by websocket-stream, which has a server-part that is unavailable on the the client diff --git a/yarn.lock b/yarn.lock index 129d9163f7..cff2ec73ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,17 +11,13 @@ resolved "https://registry.yarnpkg.com/@nylira/vue-button/-/vue-button-4.3.2.tgz#f27b90a931a1a3b5e27736dba11dfcaa305d6ddb" "@nylira/vue-field@^1.1.17": - version "1.1.17" - resolved "https://registry.yarnpkg.com/@nylira/vue-field/-/vue-field-1.1.17.tgz#a873c2d5e01b7ec664df0de71611b0fd9e066b97" + version "1.2.3" + resolved "https://registry.yarnpkg.com/@nylira/vue-field/-/vue-field-1.2.3.tgz#47b5cde79bbb9d2993b4f5734304e0424734dab1" "@nylira/vue-form-msg@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@nylira/vue-form-msg/-/vue-form-msg-1.0.3.tgz#257e3fcb1da345690f4fba959372b70e8abc9c20" -"@nylira/vue-input@^3.2.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@nylira/vue-input/-/vue-input-3.4.0.tgz#741346ce0613b831b756fccb253731015fa5c246" - "@nylira/vue-notifications@^1.4.4": version "1.4.4" resolved "https://registry.yarnpkg.com/@nylira/vue-notifications/-/vue-notifications-1.4.4.tgz#8ef4d3bbdbcbdf5570feaf17afd45c83c43d5496" @@ -395,13 +391,6 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" -axios@^0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d" - dependencies: - follow-redirects "^1.2.3" - is-buffer "^1.1.5" - axios@^0.17.0, axios@^0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d" @@ -1503,6 +1492,10 @@ chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + chart.js@^2.6.0: version "2.7.1" resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.1.tgz#ae90b4aa4ff1f02decd6b1a2a8dabfd73c9f9886" @@ -1874,13 +1867,6 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: parse-json "^2.2.0" require-from-string "^1.1.0" -cosmos-sdk@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/cosmos-sdk/-/cosmos-sdk-1.5.1.tgz#4f648aa7eaa781883dfa1bef9ee78b4fba299cf3" - dependencies: - axios "^0.16.2" - old "^0.2.0" - crc32-stream@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-1.0.1.tgz#ce2c5dc3bd8ffb3830f9cb47f540222c63c90fab" @@ -1952,6 +1938,10 @@ cross-unzip@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/cross-unzip/-/cross-unzip-0.0.2.tgz#5183bc47a09559befcf98cc4657964999359372f" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -3242,7 +3232,7 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" -follow-redirects@^1.2.3, follow-redirects@^1.2.5: +follow-redirects@^1.2.5: version "1.4.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" dependencies: @@ -4065,7 +4055,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5: +is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5222,6 +5212,14 @@ md5.js@^1.3.4: hash-base "^3.0.0" inherits "^2.0.1" +md5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -5791,12 +5789,6 @@ old@^0.1.3: dependencies: object-assign "^4.1.0" -old@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/old/-/old-0.2.0.tgz#ae75a9f33bae7cb3fe06312899b7ae5a73ba24ef" - dependencies: - object-assign "^4.1.0" - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -6695,6 +6687,20 @@ range-parser@^1.0.3, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" +raven-js@^3.22.3: + version "3.22.3" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.3.tgz#8330dcc102b699ffbc2f48790978b997bf4d8f75" + +raven@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/raven/-/raven-2.4.1.tgz#7a6a6ff1c42d0a3892308f44c94273e7f88677fd" + dependencies: + cookie "0.3.1" + md5 "^2.2.1" + stack-trace "0.0.9" + timed-out "4.0.1" + uuid "3.0.0" + raw-body@2.3.2, raw-body@^2.3.0: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" @@ -7585,6 +7591,10 @@ stack-generator@^2.0.1: dependencies: stackframe "^1.0.4" +stack-trace@0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" + stackframe@^1.0.3, stackframe@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b" @@ -7996,7 +8006,7 @@ time-stamp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" -timed-out@^4.0.0: +timed-out@4.0.1, timed-out@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" @@ -8339,6 +8349,10 @@ utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" +uuid@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.0.tgz#6728fc0459c450d796a99c31837569bdf672d728" + uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"