diff --git a/CHANGELOG.md b/CHANGELOG.md index f93f063496..ea917ae00f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * use correct set of seed nodes @ƒaboweb * validator page showed incorrect voting power @faboweb +* improves poor performance at start by throttling the updates of the cached store @faboweb * Run lint test in CI. @NodeGuy +* current atoms in PageBond still showed old atoms right after staking @ƒaboweb +* fix showing undefined for bonding denom in staking sucess message @faboweb +* fix not showing a lock (I have stake with this one) on a validator if stake is less then 1 @faboweb +* fix showing wrong error on pagebond validation @okwme ## [0.10.1] - 2018-08-29 diff --git a/app/networks b/app/networks deleted file mode 120000 index 8eae0ce5ff..0000000000 --- a/app/networks +++ /dev/null @@ -1 +0,0 @@ -../builds/testnets diff --git a/app/networks b/app/networks new file mode 160000 index 0000000000..07c6bbebad --- /dev/null +++ b/app/networks @@ -0,0 +1 @@ +Subproject commit 07c6bbebad8728533166463716a623b886045e04 diff --git a/app/src/renderer/components/staking/LiDelegate.vue b/app/src/renderer/components/staking/LiDelegate.vue index 4c15abe2c9..fc95446f83 100644 --- a/app/src/renderer/components/staking/LiDelegate.vue +++ b/app/src/renderer/components/staking/LiDelegate.vue @@ -12,7 +12,7 @@ .li-delegate__value.status span {{ delegateType }} template(v-if="userCanDelegate") - .li-delegate__value.checkbox(v-if="yourVotes > 0") + .li-delegate__value.checkbox(v-if="committedDelegations[delegate.id]") i.material-icons lock .li-delegate__value.checkbox#remove-from-cart(v-else-if="inCart" @click='rm(delegate)') i.material-icons check_box diff --git a/app/src/renderer/components/staking/PageBond.vue b/app/src/renderer/components/staking/PageBond.vue index 28b7d952a5..8fb5ea02de 100644 --- a/app/src/renderer/components/staking/PageBond.vue +++ b/app/src/renderer/components/staking/PageBond.vue @@ -38,8 +38,8 @@ tm-page.page-bond(title="Staking") :class="bondGroupClass(delta(d.atoms, d.oldAtoms))") .bond-group__fields .bond-bar - label.bond-bar__label(v-if="!d.delegate.revoked") {{ d.delegate.moniker }} - label.bond-bar__label.revoked(v-if="d.delegate.revoked") {{ d.delegate.moniker }} + label.bond-bar__label(v-if="!d.delegate.revoked") {{ d.delegate.description.moniker }} + label.bond-bar__label.revoked(v-if="d.delegate.revoked") {{ d.delegate.description.moniker }} label.bond-bar__revoked(v-if="d.delegate.revoked") REVOKED .bond-bar__input .bond-bar-old__outer @@ -72,7 +72,7 @@ tm-page.page-bond(title="Staking") tm-form-msg(:name="bondingDenom + 's'" type="required" v-if="!$v.fields.delegates.$each[index].atoms.required") - tm-form-msg(name="bondingDenom + 's'" type="numeric" + tm-form-msg(:name="bondingDenom + 's'" type="numeric" v-if="!$v.fields.delegates.$each[index].atoms.numeric") .bond-group.bond-group--unbonding( @@ -232,7 +232,7 @@ export default { await this.$store.dispatch("submitDelegation", this.fields.delegates) this.$store.commit("notify", { title: "Successful Staking!", - body: `You have successfully staked your ${this.denom}s.` + body: `You have successfully staked your ${this.bondingDenom}s.` }) this.$router.push("/staking") } catch (err) { diff --git a/app/src/renderer/components/staking/PageStaking.vue b/app/src/renderer/components/staking/PageStaking.vue index 791657325d..2d571fe924 100644 --- a/app/src/renderer/components/staking/PageStaking.vue +++ b/app/src/renderer/components/staking/PageStaking.vue @@ -67,7 +67,8 @@ export default { "config", "user", "connected", - "bondingDenom" + "bondingDenom", + "keybase" ]), address() { return this.user.address @@ -93,6 +94,7 @@ export default { v.small_moniker = v.description.moniker.toLowerCase() v.percent_of_vote = num.percent(v.voting_power / this.vpTotal) v.your_votes = this.num.prettyInt(this.committedDelegations[v.id]) + v.keybase = this.keybase[v.description.identity] return v }) }, diff --git a/app/src/renderer/components/staking/PageValidator.vue b/app/src/renderer/components/staking/PageValidator.vue index 011e4b48d9..affad58da7 100644 --- a/app/src/renderer/components/staking/PageValidator.vue +++ b/app/src/renderer/components/staking/PageValidator.vue @@ -53,11 +53,14 @@ export default { TmDataError }, computed: { - ...mapGetters(["delegates", "config"]), + ...mapGetters(["delegates", "config", "keybase"]), validator() { - return this.delegates.delegates.find( + let validator = this.delegates.delegates.find( v => this.$route.params.validator === v.owner ) + if (validator) + validator.keybase = this.keybase[validator.description.identity] + return validator }, selfBond() { parseFloat(this.validator.tokens) - diff --git a/app/src/renderer/vuex/getters.js b/app/src/renderer/vuex/getters.js index ffd02474d1..70332d3e60 100644 --- a/app/src/renderer/vuex/getters.js +++ b/app/src/renderer/vuex/getters.js @@ -48,6 +48,7 @@ export const committedDelegations = state => state.delegation.committedDelegates export const delegates = state => state.delegates export const shoppingCart = state => state.delegation.delegates export const validators = state => state.validators.validators +export const keybase = state => state.keybase.identities // govern export const proposals = state => state.proposals diff --git a/app/src/renderer/vuex/modules/delegates.js b/app/src/renderer/vuex/modules/delegates.js index a1aa50a22c..f40142ae03 100644 --- a/app/src/renderer/vuex/modules/delegates.js +++ b/app/src/renderer/vuex/modules/delegates.js @@ -1,5 +1,3 @@ -import axios from "axios" - export default ({ node }) => { const emptyState = { delegates: [], @@ -38,16 +36,6 @@ export default ({ node }) => { } state.delegates.push(delegate) - }, - setKeybaseIdentity( - state, - { validatorOwner, avatarUrl, profileUrl, userName } - ) { - let validator = state.delegates.find(v => v.owner === validatorOwner) - if (!validator.keybase) validator.keybase = {} - validator.keybase.avatarUrl = avatarUrl - validator.keybase.profileUrl = profileUrl - validator.keybase.userName = userName } } @@ -73,32 +61,9 @@ export default ({ node }) => { commit("setDelegates", candidates) commit("setDelegateLoading", false) - dispatch("updateValidatorAvatars") + dispatch("getKeybaseIdentities", candidates) return state.delegates - }, - async updateValidatorAvatars({ state, commit }) { - return Promise.all( - state.delegates.map(async validator => { - if (validator.description.identity && !validator.keybase) { - let urlPrefix = - "https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=" - let fullUrl = urlPrefix + validator.description.identity - let json = await axios.get(fullUrl) - if (json.data.status.name === "OK") { - let user = json.data.them[0] - if (user && user.pictures && user.pictures.primary) { - commit("setKeybaseIdentity", { - validatorOwner: validator.owner, - avatarUrl: user.pictures.primary.url, - userName: user.basics.username, - profileUrl: "https://keybase.io/" + user.basics.username - }) - } - } - } - }) - ) } } diff --git a/app/src/renderer/vuex/modules/delegation.js b/app/src/renderer/vuex/modules/delegation.js index a789068aa1..d862132422 100644 --- a/app/src/renderer/vuex/modules/delegation.js +++ b/app/src/renderer/vuex/modules/delegation.js @@ -147,15 +147,23 @@ export default ({ node }) => { begin_unbondings: unbond }) - // usually I would just query the new state through the LCD but at this point we still get the old shares - dispatch("updateDelegates").then(() => { - for (let delegation of delegations) { - commit("setCommittedDelegation", { - candidateId: delegation.delegate.owner, - value: delegation.atoms - }) - } - }) + // (optimistic update) we update the atoms of the user before we get the new values from chain + let atomsDiff = delegations + // compare old and new delegations and diff against old atoms + .map( + delegation => + state.committedDelegates[delegation.delegate.owner] - + delegation.atoms + ) + .reduce((sum, diff) => sum + diff, 0) + commit("setAtoms", rootState.user.atoms + atomsDiff) + + // we optimistically update the committed delegations + updateCommittedDelegations(delegations, commit) + // TODO usually I would just query the new state through the LCD and update the state with the result, but at this point we still get the old shares + dispatch("updateDelegates").then(() => + updateCommittedDelegations(delegations, commit) + ) } } @@ -165,3 +173,12 @@ export default ({ node }) => { actions } } + +function updateCommittedDelegations(delegations, commit) { + for (let delegation of delegations) { + commit("setCommittedDelegation", { + candidateId: delegation.delegate.owner, + value: delegation.atoms + }) + } +} diff --git a/app/src/renderer/vuex/modules/index.js b/app/src/renderer/vuex/modules/index.js index d9d7f70188..bff34f5571 100644 --- a/app/src/renderer/vuex/modules/index.js +++ b/app/src/renderer/vuex/modules/index.js @@ -12,5 +12,6 @@ export default opts => ({ themes: require("./themes.js").default(opts), user: require("./user.js").default(opts), validators: require("./validators.js").default(opts), - wallet: require("./wallet.js").default(opts) + wallet: require("./wallet.js").default(opts), + keybase: require("./keybase.js").default(opts) }) diff --git a/app/src/renderer/vuex/modules/keybase.js b/app/src/renderer/vuex/modules/keybase.js new file mode 100644 index 0000000000..c47ab512c8 --- /dev/null +++ b/app/src/renderer/vuex/modules/keybase.js @@ -0,0 +1,60 @@ +import axios from "axios" + +export default ({}) => { + const emptyState = { + identities: {}, + loading: false + } + const state = JSON.parse(JSON.stringify(emptyState)) + + const mutations = { + setKeybaseIdentities(state, identities) { + identities.forEach(identity => { + state.identities[identity.keybaseId] = identity + }) + } + } + + const actions = { + async getKeybaseIdentity({ state }, keybaseId) { + if (!/.{16}/.test(keybaseId)) return // the keybase id is not correct + if (state.identities[keybaseId]) return // we already have this identity + + let urlPrefix = + "https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=" + let fullUrl = urlPrefix + keybaseId + let json = await axios.get(fullUrl) + if (json.data.status.name === "OK") { + let user = json.data.them[0] + if (user && user.pictures && user.pictures.primary) { + return { + keybaseId, + avatarUrl: user.pictures.primary.url, + userName: user.basics.username, + profileUrl: "https://keybase.io/" + user.basics.username + } + } + } + }, + async getKeybaseIdentities({ dispatch, commit }, validators) { + return Promise.all( + validators.map(async validator => { + if (validator.description.identity) { + return dispatch( + "getKeybaseIdentity", + validator.description.identity + ) + } + }) + ).then(identities => { + commit("setKeybaseIdentities", identities.filter(x => !!x)) + }) + } + } + + return { + state, + actions, + mutations + } +} diff --git a/app/src/renderer/vuex/modules/node.js b/app/src/renderer/vuex/modules/node.js index ea585e6370..300b508c3b 100644 --- a/app/src/renderer/vuex/modules/node.js +++ b/app/src/renderer/vuex/modules/node.js @@ -44,7 +44,11 @@ export default function({ node }) { rootState.wallet.zoneIds.unshift(header.chain_id) } - await dispatch("maybeUpdateValidators", header) + // updating the header is done even while the user is not logged in + // to prevent errors popping up from the LCD before the user is signed on, we skip updating validators before + // TODO identify why rest calls fail at this point + if (rootState.user.signedIn) + await dispatch("maybeUpdateValidators", header) }, async reconnect({ commit }) { if (state.stopConnecting) return diff --git a/app/src/renderer/vuex/modules/user.js b/app/src/renderer/vuex/modules/user.js index d085fbb8dd..d4be7f7b8a 100644 --- a/app/src/renderer/vuex/modules/user.js +++ b/app/src/renderer/vuex/modules/user.js @@ -106,7 +106,7 @@ export default ({ node }) => { let { address } = await node.getKey(account) state.address = address - commit("loadPersistedState", { password }) + dispatch("loadPersistedState", { password }) commit("setModalSession", false) dispatch("initializeWallet", address) dispatch("loadErrorCollection", account) diff --git a/app/src/renderer/vuex/modules/wallet.js b/app/src/renderer/vuex/modules/wallet.js index efe87852e5..e3fa3f7a12 100644 --- a/app/src/renderer/vuex/modules/wallet.js +++ b/app/src/renderer/vuex/modules/wallet.js @@ -37,15 +37,6 @@ export default ({ node }) => { }, setDenoms(state, denoms) { state.denoms = denoms - }, - setTransactionTime(state, { blockHeight, blockMetaInfo }) { - state.history = state.history.map(t => { - if (t.height === blockHeight) { - // console.log("blockMetaInfo", blockMetaInfo) - t.time = blockMetaInfo && blockMetaInfo.header.time - } - return t - }) } } @@ -99,30 +90,20 @@ export default ({ node }) => { if (!res) return const uniqueTransactions = uniqBy(res, "hash") - commit("setWalletHistory", uniqueTransactions) - await dispatch("enrichTransactions", uniqueTransactions) + commit("setWalletHistory", uniqueTransactions) commit("setHistoryLoading", false) }, async enrichTransactions({ dispatch }, transactions) { - let blockHeights = [] - transactions.forEach(t => { - if (!blockHeights.find(h => h === t.height)) { - blockHeights.push(t.height) - } - }) - await Promise.all( - blockHeights.map(h => dispatch("queryTransactionTime", h)) + transactions = await Promise.all( + transactions.map(async t => { + let blockMetaInfo = await dispatch("queryBlockInfo", t.height) + t.time = blockMetaInfo && blockMetaInfo.header.time + return t + }) ) - }, - async queryTransactionTime({ commit, dispatch }, blockHeight) { - let blockMetaInfo = await dispatch("queryBlockInfo", blockHeight) - // console.log( - // "received blockMetaInfo at height " + blockHeight, - // blockMetaInfo - // ) - commit("setTransactionTime", { blockHeight, blockMetaInfo }) + return transactions }, async loadDenoms({ commit }) { // read genesis.json to get default denoms diff --git a/app/src/renderer/vuex/store.js b/app/src/renderer/vuex/store.js index ee2246ad74..63287e0845 100644 --- a/app/src/renderer/vuex/store.js +++ b/app/src/renderer/vuex/store.js @@ -14,17 +14,32 @@ export default (opts = {}) => { getters, // strict: true, modules: modules(opts), - mutations: { + actions: { loadPersistedState } }) + let pending = null store.subscribe((mutation, state) => { + // since persisting the state is costly we should only do it on mutations that change the data + const updatingMutations = [ + "setWalletBalances", + "setWalletHistory", + "setCommittedDelegation", + "setDelegates", + "setKeybaseIdentities" + ] + if (updatingMutations.indexOf(mutation.type) === -1) return + // if the user is logged in cache the balances and the tx-history for that user - // skip persisting the state before the potentially persisted state has been loaded - if (state.user.stateLoaded && state.user.account && state.user.password) { - persistState(state) + if (!state.user.account || !state.user.password) return + + if (pending) { + clearTimeout(pending) } + pending = setTimeout(() => { + persistState(state) + }, 5000) }) return store @@ -42,6 +57,9 @@ function persistState(state) { }, delegates: { delegates: state.delegates.delegates + }, + keybase: { + identities: state.keybase.identities } }), state.user.password @@ -56,7 +74,7 @@ function getStorageKey(state) { return `store_${chainId}_${address}` } -function loadPersistedState(state, { password }) { +function loadPersistedState({ state, commit }, { password }) { const cachedState = localStorage.getItem(getStorageKey(state)) if (cachedState) { const bytes = CryptoJS.AES.decrypt(cachedState, password) @@ -75,7 +93,12 @@ function loadPersistedState(state, { password }) { } }) this.replaceState(state) - } - state.user.stateLoaded = true + // add all delegates the user has bond with already to the cart + state.delegates.delegates + .filter(d => state.delegation.committedDelegates[d.owner]) + .forEach(d => { + commit("addToCart", d) + }) + } } diff --git a/test/e2e/common.js b/test/e2e/common.js index af28ce132c..02656ad2c4 100644 --- a/test/e2e/common.js +++ b/test/e2e/common.js @@ -88,6 +88,8 @@ module.exports = { } await sleep(100) } + + return true }, async login(app, account = "default") { console.log("logging into " + account) diff --git a/test/e2e/staking.js b/test/e2e/staking.js index 6d2ea9b2ff..adec5e3b24 100644 --- a/test/e2e/staking.js +++ b/test/e2e/staking.js @@ -117,12 +117,11 @@ test("staking", async function(t) { // validator should already be in the cart so we only need to click a button to go to the bonding view await app.client.$("#go-to-bonding-btn").click() - t.doesNotThrow( - async () => - await waitForValue( - () => app.client.$("#new-unbonded-atoms"), - (totalUserStake - bondedStake).toString() - ), + t.ok( + await waitForValue( + () => app.client.$("#new-unbonded-atoms"), + (totalUserStake - bondedStake).toString() + ), "Left over steak shows correctly" ) @@ -136,15 +135,6 @@ test("staking", async function(t) { .$(".bond-candidate .bond-value__input") .setValue(bondedStake - 20) - t.doesNotThrow( - async () => - await waitForValue( - () => app.client.$("#new-unbonded-atoms"), - (totalUserStake - bondedStake + 20).toString() - ), - "Left over steak shows correctly after adjusting bond" - ) - t.equal( await app.client.$("#new-unbonding-atoms").getValue(), (20).toString(), diff --git a/test/unit/helpers/vuex-setup.js b/test/unit/helpers/vuex-setup.js index cc6da7777b..e91541b361 100644 --- a/test/unit/helpers/vuex-setup.js +++ b/test/unit/helpers/vuex-setup.js @@ -23,7 +23,7 @@ export default function vuexSetup() { let store = new Vuex.Store({ getters: Object.assign({}, Getters, getters), modules, - mutations: { + actions: { loadPersistedState: () => {} } }) diff --git a/test/unit/specs/components/staking/PageBond.spec.js b/test/unit/specs/components/staking/PageBond.spec.js index 05625d5d16..831bf0be85 100644 --- a/test/unit/specs/components/staking/PageBond.spec.js +++ b/test/unit/specs/components/staking/PageBond.spec.js @@ -22,9 +22,11 @@ describe("PageBond", () => { }, voting_power: 10000, shares: 5000, - description: "descriptionX", - country: "USA", - moniker: "someValidator" + description: { + description: "descriptionX", + country: "USA", + moniker: "someValidator" + } }) store.commit("addToCart", { id: "pubkeyY", @@ -34,23 +36,24 @@ describe("PageBond", () => { }, voting_power: 30000, shares: 10000, - description: "descriptionY", - country: "Canada", - moniker: "someOtherValidator" + description: { + description: "descriptionY", + country: "Canada", + moniker: "someOtherValidator" + } }) - store.commit( - "addToCart", - Object.assign( - { - id: "pubkeyZ", - voting_power: 20000, - shares: 75000, - moniker: "aChileanValidator" - }, - candidates[2] // this is the revoked one - ) + let chileanValidator = Object.assign( + { + id: "pubkeyZ", + voting_power: 20000, + shares: 75000 + }, + candidates[2] // this is the revoked one ) + chileanValidator.description.moniker = "aChileanValidator" + + store.commit("addToCart", chileanValidator) store.commit("setUnbondingDelegations", { candidateId: "pubkeyY", @@ -468,9 +471,9 @@ describe("PageBond", () => { it("disables bonding if not connected", async () => { store.commit("setConnected", false) + wrapper.update() wrapper.vm.onSubmit = jest.fn() - wrapper.findAll("#btn-bond").trigger("click") - expect(wrapper.vm.onSubmit).not.toHaveBeenCalled() + expect(wrapper.find("#btn-bond").exists()).toBeFalsy() expect(wrapper.vm.$el).toMatchSnapshot() }) }) 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 94aee26efc..e08b3c7763 100644 --- a/test/unit/specs/components/staking/__snapshots__/PageBond.spec.js.snap +++ b/test/unit/specs/components/staking/__snapshots__/PageBond.spec.js.snap @@ -404,7 +404,7 @@ exports[`PageBond disables bonding if not connected 1`] = `