diff --git a/app/src/renderer/components/monitor/BlockchainSelect.vue b/app/src/renderer/components/monitor/BlockchainSelect.vue deleted file mode 100644 index 4642b9a5dc..0000000000 --- a/app/src/renderer/components/monitor/BlockchainSelect.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - diff --git a/app/src/renderer/components/monitor/BlockchainSelectModal.vue b/app/src/renderer/components/monitor/BlockchainSelectModal.vue deleted file mode 100644 index 34a1f32325..0000000000 --- a/app/src/renderer/components/monitor/BlockchainSelectModal.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - - diff --git a/app/src/renderer/components/wallet/PageSend.vue b/app/src/renderer/components/wallet/PageSend.vue index 8a64ebdcea..5f45995c20 100644 --- a/app/src/renderer/components/wallet/PageSend.vue +++ b/app/src/renderer/components/wallet/PageSend.vue @@ -105,27 +105,25 @@ export default { await this.walletSend({ fees: { denom, amount: 0 }, to: address, - amount: [{ denom, amount }], - cb: (err) => { - this.sending = false - if (err) { - this.$store.commit('notifyError', { - title: 'Error Sending', - body: `An error occurred while trying to send: "${err.message}"` - }) - return - } - this.$store.commit('notify', { - title: 'Successfully Sent', - body: `Successfully sent ${amount} ${denom.toUpperCase()} to ${address}` - }) + amount: [{ denom, amount }] + }).then(() => { + this.sending = false + this.$store.commit('notify', { + title: 'Successfully Sent', + body: `Successfully sent ${amount} ${denom.toUpperCase()} to ${address}` + }) // resets send transaction form - this.resetForm() + this.resetForm() // refreshes user transaction history - this.$store.dispatch('queryWalletHistory') - } + this.$store.dispatch('queryWalletHistory') + }, err => { + this.sending = false + this.$store.commit('notifyError', { + title: 'Error Sending', + body: `An error occurred while trying to send: "${err.message}"` + }) }) }, ...mapActions(['walletSend']) diff --git a/app/src/renderer/vuex/actions.js b/app/src/renderer/vuex/actions.js deleted file mode 100644 index 32ee5b1280..0000000000 --- a/app/src/renderer/vuex/actions.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as types from './mutation-types' - -export const decrementMain = ({ commit }) => { - commit(types.DECREMENT_MAIN_COUNTER) -} - -export const incrementMain = ({ commit }) => { - commit(types.INCREMENT_MAIN_COUNTER) -} diff --git a/app/src/renderer/vuex/modules/delegation.js b/app/src/renderer/vuex/modules/delegation.js index cb2abcf0c2..b5db52f37c 100644 --- a/app/src/renderer/vuex/modules/delegation.js +++ b/app/src/renderer/vuex/modules/delegation.js @@ -57,23 +57,23 @@ export default ({ commit }) => { let bond = (await axios.get('http://localhost:8998/query/stake/delegator/' + address + '/' + pubkey)).data.data commit('setCommittedDelegation', {candidateId: bond.PubKey.data, value: bond.Shares}) }, - async walletDelegate ({ dispatch }, args) { + walletDelegate ({ dispatch }, args) { args.type = 'buildDelegate' - await dispatch('walletTx', args) + return dispatch('sendTx', args) }, - async walletUnbond ({ dispatch }, args) { + walletUnbond ({ dispatch }, args) { args.type = 'buildUnbond' - await dispatch('walletTx', args) + return dispatch('sendTx', args) }, - async submitDelegation ({ state, dispatch }, delegation) { - for (let delegate of delegation.delegates) { + submitDelegation ({ state, dispatch }, delegation) { + return Promise.all(delegation.delegates.map(delegate => { let candidateId = delegate.delegate.pub_key.data let currentlyDelegated = state.committedDelegates[candidateId] || 0 let amountChange = delegate.atoms - currentlyDelegated let action = amountChange > 0 ? 'walletDelegate' : 'walletUnbond' // skip if no change - if (amountChange === 0) continue + if (amountChange === 0) return null // bonding takes a 'coin' object, unbond just takes a number let amount @@ -84,17 +84,11 @@ export default ({ commit }) => { amount = Math.abs(amountChange) } - await new Promise((resolve, reject) => { - dispatch(action, { - amount, - pub_key: delegate.delegate.pub_key, - cb: (err, res) => { - if (err) return reject(err) - resolve(res) - } - }) + return dispatch(action, { + amount, + pub_key: delegate.delegate.pub_key }) - } + }).filter(x => x !== null)) } } diff --git a/app/src/renderer/vuex/modules/index.js b/app/src/renderer/vuex/modules/index.js index 87cd6a4d80..cdcfa0fabb 100644 --- a/app/src/renderer/vuex/modules/index.js +++ b/app/src/renderer/vuex/modules/index.js @@ -7,6 +7,7 @@ export default (opts) => ({ node: require('./node.js').default(opts), notifications: require('./notifications.js').default(opts), proposals: require('./proposals.js').default(opts), + send: require('./send.js').default(opts), user: require('./user.js').default(opts), validators: require('./validators.js').default(opts), wallet: require('./wallet.js').default(opts) diff --git a/app/src/renderer/vuex/modules/node.js b/app/src/renderer/vuex/modules/node.js index d2a69a324a..8858a7de72 100644 --- a/app/src/renderer/vuex/modules/node.js +++ b/app/src/renderer/vuex/modules/node.js @@ -8,6 +8,7 @@ export default function ({ node }) { const state = { nodeIP, + stopConnecting: false, connected: false, lastHeader: { height: 0, @@ -16,6 +17,9 @@ export default function ({ node }) { } const mutations = { + stopConnecting (state, stop) { + state.stopConnecting = stop + }, setConnected (state, connected) { state.connected = connected } @@ -27,11 +31,15 @@ export default function ({ node }) { dispatch('maybeUpdateValidators', header) }, async reconnect ({commit, dispatch}) { + if (state.stopConnecting) return + commit('setConnected', false) await node.rpcReconnect() dispatch('nodeSubscribe') }, nodeSubscribe ({commit, dispatch}) { + if (state.stopConnecting) return + // the rpc socket can be closed before we can even attach a listener // so we remember if the connection is open // we handle the reconnection here so we can attach all these listeners on reconnect @@ -56,7 +64,7 @@ export default function ({ node }) { chain_id: status.node_info.network }) }) - node.rpc.subscribe({ event: 'NewBlockHeader' }, (err, event) => { + node.rpc.subscribe({ query: 'tm.event = \'NewBlockHeader\'' }, (err, event) => { console.log(err) // if (err) return console.error('error subscribing to headers', err) // commit('setConnected', true) diff --git a/app/src/renderer/vuex/modules/send.js b/app/src/renderer/vuex/modules/send.js new file mode 100644 index 0000000000..6e899d939c --- /dev/null +++ b/app/src/renderer/vuex/modules/send.js @@ -0,0 +1,106 @@ +export default ({ commit, node }) => { + let state = { + // account nonce number, used to prevent replay attacks + nonce: 0, + // queue of transactions to be sent + queue: [], + sending: false + } + + const mutations = { + queueSend (state, sendReq) { + state.queue.push(sendReq) + }, + shiftSendQueue (state) { + state.queue = state.queue.slice(1) + }, + setSending (state, sending) { + state.sending = sending + }, + setNonce (state, nonce) { + state.nonce = nonce + } + } + + let actions = { + // queries for our account's nonce + async queryNonce ({ commit }, address) { + let res = await node.queryNonce(address) + if (!res) return + commit('setNonce', res.data) + }, + + // builds, signs, and broadcasts a tx of any type + sendTx ({ state, dispatch, commit, rootState }, args) { + // wait until the current send operation is done + if (state.sending) { + args.done = new Promise((resolve, reject) => { + args.resolve = resolve + args.reject = reject + }) + commit('queueSend', args) + return args.done + } + + return new Promise((resolve, reject) => { + commit('setSending', true) + + // once done, do next send in queue + function done (err, res) { + commit('setSending', false) + + if (state.queue.length > 0) { + // do next send + let send = state.queue[0] + commit('shiftSendQueue') + dispatch('sendTx', send) + } + + if (err) { + reject(err) + if (args.reject) args.reject(err) + } else { + resolve(res) + if (args.resolve) args.resolve(res) + } + } + + args.sequence = state.nonce + 1 + args.from = { + chain: '', + app: 'sigs', + addr: rootState.wallet.key.address + }; + + (async function () { + // build tx + let tx = await node[args.type](args) + + // sign tx + let signedTx = await node.sign({ + name: rootState.user.account, + password: rootState.user.password, + tx + }) + + // broadcast tx + let res = await node.postTx(signedTx) + + // check response code + if (res.check_tx.code || res.deliver_tx.code) { + let message = res.check_tx.log || res.deliver_tx.log + throw new Error('Error sending transaction: ' + message) + } + })().then(() => { + commit('setNonce', state.nonce + 1) + done(null, args) + dispatch('queryWalletBalances') + }, (err) => { + done(err || Error('Error sending transaction')) + }) + }) + } + } + + return { state, mutations, actions } +} diff --git a/app/src/renderer/vuex/modules/validators.js b/app/src/renderer/vuex/modules/validators.js index 51f28d1b21..b969080903 100644 --- a/app/src/renderer/vuex/modules/validators.js +++ b/app/src/renderer/vuex/modules/validators.js @@ -1,14 +1,10 @@ export default ({ commit, node }) => { const state = { - blockchainName: '', validators: {}, validatorHash: null } const mutations = { - setValidatorBlockchainName (state, name) { - state.blockchainName = name - }, setValidators (state, validators) { state.validators = validators }, diff --git a/app/src/renderer/vuex/modules/wallet.js b/app/src/renderer/vuex/modules/wallet.js index 028b90f079..6a162b55a8 100644 --- a/app/src/renderer/vuex/modules/wallet.js +++ b/app/src/renderer/vuex/modules/wallet.js @@ -1,17 +1,13 @@ let fs = require('fs-extra') let { join } = require('path') let root = require('../../../root.js') -const axios = require('axios') export default ({ commit, node }) => { let state = { balances: [], - sequence: 0, key: { address: '' }, history: [], denoms: [], - sendQueue: [], - sending: false, blockMetas: [] } @@ -24,10 +20,6 @@ export default ({ commit, node }) => { // clear previous account state state.balances = [] state.history = [] - state.sequence = 0 - }, - setWalletSequence (state, sequence) { - state.sequence = sequence }, setWalletHistory (state, history) { state.history = history @@ -35,15 +27,6 @@ export default ({ commit, node }) => { setDenoms (state, denoms) { state.denoms = denoms }, - queueSend (state, sendReq) { - state.sendQueue.push(sendReq) - }, - shiftSendQueue (state) { - state.sendQueue = state.sendQueue.shift(1) - }, - setSending (state, sending) { - state.sending = sending - }, setTransactionTime (state, { blockHeight, blockMetaInfo }) { state.history = state.history.map(t => { if (t.height === blockHeight) { @@ -62,7 +45,7 @@ export default ({ commit, node }) => { }, queryWalletState ({ state, dispatch }) { dispatch('queryWalletBalances') - dispatch('queryWalletSequence') + dispatch('queryNonce', state.key.address) dispatch('queryWalletHistory') }, async queryWalletBalances ({ state, rootState, commit }) { @@ -76,11 +59,6 @@ export default ({ commit, node }) => { } } }, - async queryWalletSequence ({ state, commit }) { - let res = await node.queryNonce(state.key.address) - if (!res) return - commit('setWalletSequence', res.data) - }, async queryWalletHistory ({ state, commit, dispatch }) { let res = await node.coinTxs(state.key.address) if (!res) return @@ -106,12 +84,12 @@ export default ({ commit, node }) => { return blockMetaInfo } blockMetaInfo = await new Promise((resolve, reject) => { - node.rpc.blockchain({ minHeight: height, maxHeight: height }, (err, {block_metas}) => { + node.rpc.blockchain({ minHeight: height, maxHeight: height }, (err, data) => { if (err) { commit('notifyError', {title: `Couldn't query block`, body: err.message}) - reject() + resolve(null) } else { - resolve(block_metas[0]) + resolve(data.block_metas[0]) } }) }) @@ -125,71 +103,7 @@ export default ({ commit, node }) => { app: 'sigs', addr: args.to } - await dispatch('walletTx', args) - }, - async walletTx ({ state, dispatch, commit, rootState }, args) { - // wait until the current send operation is done - if (state.sending) { - commit('queueSend', args) - return - } - commit('setSending', true) - - let cb = args.cb - delete args.cb - - // once done, do next send in queue - function done (err, res) { - commit('setSending', false) - - if (state.sendQueue.length > 0) { - // do next send - let send = state.sendQueue[0] - commit('shiftSendQueue') - dispatch('walletSend', send) - } - - cb(err, res) - } - - try { - if (args.sequence == null) { - args.sequence = state.sequence + 1 - } - args.from = { - chain: '', - app: 'sigs', - addr: state.key.address - } - // let tx = await node[args.type](args) - // TODO fix in cosmos-sdk-js - let tx = await (async function () { - switch (args.type) { - case 'buildDelegate': return axios.post('http://localhost:8998/build/stake/delegate', args) - .then(res => res.data) - case 'buildUnbond': return axios.post('http://localhost:8998/build/stake/unbond', args) - .then(res => res.data) - default: return node[args.type](args) - } - })() - let signedTx = await node.sign({ - name: rootState.user.account, - password: rootState.user.password, - tx - }) - let res = await node.postTx(signedTx) - // check response code - if (res.check_tx.code || res.deliver_tx.code) { - let message = res.check_tx.log || res.deliver_tx.log - return done(new Error('Error sending transaction: ' + message)) - } - - commit('setWalletSequence', args.sequence) - } catch (err) { - return done(err) - } - done(null, args) - dispatch('queryWalletBalances') + return dispatch('sendTx', args) }, async loadDenoms ({ state, commit }) { // read genesis.json to get default denoms diff --git a/app/src/renderer/vuex/mutation-types.js b/app/src/renderer/vuex/mutation-types.js deleted file mode 100644 index c55b17c025..0000000000 --- a/app/src/renderer/vuex/mutation-types.js +++ /dev/null @@ -1,2 +0,0 @@ -export const DECREMENT_MAIN_COUNTER = 'DECREMENT_MAIN_COUNTER' -export const INCREMENT_MAIN_COUNTER = 'INCREMENT_MAIN_COUNTER' diff --git a/app/src/renderer/vuex/store.js b/app/src/renderer/vuex/store.js index 0b1b677c28..00cb2ed848 100644 --- a/app/src/renderer/vuex/store.js +++ b/app/src/renderer/vuex/store.js @@ -1,6 +1,5 @@ import Vue from 'vue' import Vuex from 'vuex' -import * as actions from './actions' import * as getters from './getters' import modules from './modules' @@ -10,7 +9,6 @@ export default (opts = {}) => { opts.commit = (...args) => store.commit(...args) opts.dispatch = (...args) => store.dispatch(...args) var store = new Vuex.Store({ - actions, getters, // strict: true, modules: modules(opts) diff --git a/test/unit/helpers/json/validators.json b/test/unit/helpers/json/validators.json new file mode 100644 index 0000000000..0539ca9f93 --- /dev/null +++ b/test/unit/helpers/json/validators.json @@ -0,0 +1,56 @@ +[ + { + "address":"70705055A9FA5901735D0C3F0954501DDE667327", + "pub_key":{ + "type":"ed25519", + "data":"88564A32500A120AA72CEFBCF5462E078E5DDB70B6431F59F778A8DC4DA719A4" + }, + "voting_power":14, + "accum":585 + }, + { + "address":"760ACDE75EFC3DD0E4B2A6A3B96D91C05349EA31", + "pub_key":{ + "type":"ed25519", + "data":"7A9D783CE542B23FA23DC7F101460879861205772606B4C3FAEAFBEDFB00E7BD" + }, + "voting_power":32, + "accum":-1107 + }, + { + "address":"77C26DF82654C5A5DDE5C6B7B27F3F06E9C223C0", + "pub_key":{ + "type":"ed25519", + "data":"651E7B12B3C7234FB82B4417C59DCE30E4EA28F06AD0ACAEDFF05F013E463F10" + }, + "voting_power":19, + "accum":539 + }, + { + "address":"A130023895C57972021B7130FB0B25FB9BAD1557", + "pub_key":{ + "type":"ed25519", + "data":"2BA51EDE20AFC1EB462F22C10623A24A9C7E1B50282A5FB1E86A2E9B4FF19C47" + }, + "voting_power":1000, + "accum":943 + }, + { + "address":"D5B410389F606D5AA34BED52E143DDC40EF97C43", + "pub_key":{ + "type":"ed25519", + "data":"B3D42DEB413DA2BAE6CB1A9162B820783BD2905A90840795D2B11640344908C7" + }, + "voting_power":1000, + "accum":-1940 + }, + { + "address":"F173B5D14E4D9289E5654E4DCCCA3A3FF0D43799", + "pub_key":{ + "type":"ed25519", + "data":"C9B26A21B62EF839523D2040C0C4C046D86574CB6DFF92F3C0AF58DF209846FD" + }, + "voting_power":1000, + "accum":980 + } +] diff --git a/test/unit/helpers/node_mock.js b/test/unit/helpers/node_mock.js index 590b529343..1de883139e 100644 --- a/test/unit/helpers/node_mock.js +++ b/test/unit/helpers/node_mock.js @@ -1,3 +1,5 @@ +let mockValidators = require('./json/validators.json') + module.exports = { // REST lcdConnected: () => Promise.resolve(true), @@ -13,13 +15,13 @@ module.exports = { seed_phrase: 'a b c d e f g h i j k l' }), queryAccount: () => null, - queryNonce: () => '123', - buildSend: (args) => { - if (args.to.addr.indexOf('fail') !== -1) return Promise.reject('Failed on purpose') - return Promise.resolve(null) - }, + queryNonce: () => ({data: 123}), + buildSend: () => Promise.resolve(null), + buildDelegate: () => Promise.resolve(null), + buildUnbond: () => Promise.resolve(null), coinTxs: () => Promise.resolve([]), candidates: () => Promise.resolve({data: []}), + sendTx: () => Promise.resolve(), postTx: () => Promise.resolve({ check_tx: { code: 0 }, deliver_tx: { code: 0 } @@ -30,9 +32,9 @@ module.exports = { rpc: { on: () => {}, subscribe: () => {}, - validators: () => [], + validators: () => mockValidators, block: (args, cb) => cb({}), - blockchain: (args, cb) => cb({}), + blockchain: (args, cb) => cb(null, {block_metas: {}}), status: (cb) => cb(null, { latest_block_height: 42, node_info: { diff --git a/test/unit/helpers/vuex-setup.js b/test/unit/helpers/vuex-setup.js index 3ee2a7e336..15babc936d 100644 --- a/test/unit/helpers/vuex-setup.js +++ b/test/unit/helpers/vuex-setup.js @@ -13,7 +13,7 @@ export default function vuexSetup () { localVue.use(VueRouter) function init (componentConstructor, testType = shallow, {stubs, getters = {}, propsData}) { - const node = require('../helpers/node_mock') + const node = Object.assign({}, require('../helpers/node_mock')) const modules = Modules({ node }) diff --git a/test/unit/specs/components/wallet/PageSend.spec.js b/test/unit/specs/components/wallet/PageSend.spec.js index 511a8c9c81..cd72e53306 100644 --- a/test/unit/specs/components/wallet/PageSend.spec.js +++ b/test/unit/specs/components/wallet/PageSend.spec.js @@ -3,15 +3,16 @@ import Vuelidate from 'vuelidate' import PageSend from 'renderer/components/wallet/PageSend' describe('PageSend', () => { - let wrapper, store + let wrapper, store, node let {mount, localVue} = setup() localVue.use(Vuelidate) beforeEach(async () => { - let instance = mount(PageSend) - wrapper = instance.wrapper - store = instance.store + let test = mount(PageSend) + wrapper = test.wrapper + store = test.store + node = test.node store.commit('setAccounts', [{ address: '1234567890123456789012345678901234567890', name: 'default', @@ -61,7 +62,10 @@ describe('PageSend', () => { amount: 2 } }) + node.sign = () => Promise.reject() await wrapper.vm.onSubmit() - expect(store.commit).toHaveBeenCalledWith('notifyError', expect.any(Object)) + expect(store.state.notifications.length).toBe(1) + expect(store.state.notifications[0].title).toBe('Error Sending') + expect(store.state.notifications[0]).toMatchSnapshot() }) }) diff --git a/test/unit/specs/components/wallet/__snapshots__/PageSend.spec.js.snap b/test/unit/specs/components/wallet/__snapshots__/PageSend.spec.js.snap index b52a7c9783..b536db3761 100644 --- a/test/unit/specs/components/wallet/__snapshots__/PageSend.spec.js.snap +++ b/test/unit/specs/components/wallet/__snapshots__/PageSend.spec.js.snap @@ -534,3 +534,14 @@ exports[`PageSend has the expected html structure 1`] = ` `; + +exports[`PageSend should show notification for unsuccessful send 1`] = ` +Object { + "body": "An error occurred while trying to send: \\"Error sending transaction\\"", + "icon": "error", + "layout": "alert", + "time": 1608, + "title": "Error Sending", + "type": "error", +} +`; diff --git a/test/unit/specs/store/__snapshots__/delegation.spec.js.snap b/test/unit/specs/store/__snapshots__/delegation.spec.js.snap new file mode 100644 index 0000000000..10a90f5613 --- /dev/null +++ b/test/unit/specs/store/__snapshots__/delegation.spec.js.snap @@ -0,0 +1,169 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Module: Delegations submits delegation transaction 1`] = ` +Array [ + Array [ + Object { + "amount": Object { + "amount": 123, + "denom": "fermion", + }, + "from": Object { + "addr": "address", + "app": "sigs", + "chain": "", + }, + "pub_key": Object { + "data": "foo", + }, + "sequence": 1, + "type": "buildDelegate", + }, + ], + Array [ + Object { + "amount": Object { + "amount": 333, + "denom": "fermion", + }, + "done": Promise {}, + "from": Object { + "addr": "address", + "app": "sigs", + "chain": "", + }, + "pub_key": Object { + "data": "bar", + }, + "reject": [Function], + "resolve": [Function], + "sequence": 2, + "type": "buildDelegate", + }, + ], + Array [ + Object { + "amount": 789, + "done": Promise {}, + "from": Object { + "addr": "address", + "app": "sigs", + "chain": "", + }, + "pub_key": Object { + "data": "baz", + }, + "reject": [Function], + "resolve": [Function], + "sequence": 3, + "type": "buildUnbond", + }, + ], + Array [ + Object { + "amount": Object { + "amount": 333, + "denom": "fermion", + }, + "done": Promise {}, + "from": Object { + "addr": "address", + "app": "sigs", + "chain": "", + }, + "pub_key": Object { + "data": "bar", + }, + "reject": [Function], + "resolve": [Function], + "sequence": 2, + "type": "buildDelegate", + }, + ], + Array [ + Object { + "amount": 789, + "done": Promise {}, + "from": Object { + "addr": "address", + "app": "sigs", + "chain": "", + }, + "pub_key": Object { + "data": "baz", + }, + "reject": [Function], + "resolve": [Function], + "sequence": 3, + "type": "buildUnbond", + }, + ], +] +`; + +exports[`Module: Delegations submits delegation transaction 2`] = ` +Array [ + Array [ + Object { + "amount": Object { + "amount": 123, + "denom": "fermion", + }, + "from": Object { + "addr": "address", + "app": "sigs", + "chain": "", + }, + "pub_key": Object { + "data": "foo", + }, + "sequence": 1, + "type": "buildDelegate", + }, + ], + Array [ + Object { + "amount": Object { + "amount": 333, + "denom": "fermion", + }, + "done": Promise {}, + "from": Object { + "addr": "address", + "app": "sigs", + "chain": "", + }, + "pub_key": Object { + "data": "bar", + }, + "reject": [Function], + "resolve": [Function], + "sequence": 2, + "type": "buildDelegate", + }, + ], +] +`; + +exports[`Module: Delegations submits delegation transaction 3`] = ` +Array [ + Array [ + Object { + "amount": 789, + "done": Promise {}, + "from": Object { + "addr": "address", + "app": "sigs", + "chain": "", + }, + "pub_key": Object { + "data": "baz", + }, + "reject": [Function], + "resolve": [Function], + "sequence": 3, + "type": "buildUnbond", + }, + ], +] +`; diff --git a/test/unit/specs/store/__snapshots__/send.spec.js.snap b/test/unit/specs/store/__snapshots__/send.spec.js.snap new file mode 100644 index 0000000000..0770741341 --- /dev/null +++ b/test/unit/specs/store/__snapshots__/send.spec.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Module: Send send transactions should send a tx 1`] = `Array []`; + +exports[`Module: Send send transactions should send from wallet 1`] = `Array []`; diff --git a/test/unit/specs/store/__snapshots__/wallet.spec.js.snap b/test/unit/specs/store/__snapshots__/wallet.spec.js.snap new file mode 100644 index 0000000000..89a1b49244 --- /dev/null +++ b/test/unit/specs/store/__snapshots__/wallet.spec.js.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Module: Wallet query meta info should show an info if block info is unavailable 1`] = ` +Object { + "body": undefined, + "icon": "error", + "layout": "alert", + "time": 1608, + "title": "Couldn't query block", + "type": "error", +} +`; diff --git a/test/unit/specs/store/delegation.spec.js b/test/unit/specs/store/delegation.spec.js index 852644e322..7d726bdcb6 100644 --- a/test/unit/specs/store/delegation.spec.js +++ b/test/unit/specs/store/delegation.spec.js @@ -3,22 +3,30 @@ import { createLocalVue } from 'vue-test-utils' let axios = require('axios') const Delegation = require('renderer/vuex/modules/delegation').default +const Send = require('renderer/vuex/modules/send').default const localVue = createLocalVue() localVue.use(Vuex) describe('Module: Delegations', () => { - let store, walletTx + let store, node beforeEach(() => { - let node = require('../../helpers/node_mock') - walletTx = jest.fn((_, { cb }) => cb(null, {})) + node = require('../../helpers/node_mock') store = new Vuex.Store({ modules: { delegation: Delegation({ node }), + send: Send({ node }), wallet: { actions: { - walletTx + queryWalletBalances: () => Promise.resolve() + }, + state: { key: { address: 'address' } } + }, + user: { + state: { + account: 'foo', + password: 'bar' } } } @@ -114,6 +122,9 @@ describe('Module: Delegations', () => { it('submits delegation transaction', async () => { store.commit('setCommittedDelegation', { candidateId: 'bar', value: 123 }) store.commit('setCommittedDelegation', { candidateId: 'baz', value: 789 }) + jest.spyOn(store._actions.sendTx, '0') + jest.spyOn(node, 'buildDelegate') + jest.spyOn(node, 'buildUnbond') await store.dispatch('submitDelegation', { delegates: [ @@ -131,21 +142,8 @@ describe('Module: Delegations', () => { } ] }) - expect(walletTx.mock.calls.length).toBe(3) - - // bonding to a validator we were not previously bonded to - expect(walletTx.mock.calls[0][1].amount).toEqual({ amount: 123, denom: 'fermion' }) - expect(walletTx.mock.calls[0][1].pub_key).toEqual({ data: 'foo' }) - expect(walletTx.mock.calls[0][1].type).toBe('buildDelegate') - - // bonding to a validator we were previously bonded to - expect(walletTx.mock.calls[1][1].amount).toEqual({ amount: 333, denom: 'fermion' }) - expect(walletTx.mock.calls[1][1].pub_key).toEqual({ data: 'bar' }) - expect(walletTx.mock.calls[1][1].type).toBe('buildDelegate') - - // unbonding - expect(walletTx.mock.calls[2][1].amount).toEqual(789) - expect(walletTx.mock.calls[2][1].pub_key).toEqual({ data: 'baz' }) - expect(walletTx.mock.calls[2][1].type).toBe('buildUnbond') + expect(store._actions.sendTx[0].mock.calls).toMatchSnapshot() + expect(node.buildDelegate.mock.calls).toMatchSnapshot() + expect(node.buildUnbond.mock.calls).toMatchSnapshot() }) }) diff --git a/test/unit/specs/store/json/validators.json b/test/unit/specs/store/json/validators.json new file mode 100644 index 0000000000..0539ca9f93 --- /dev/null +++ b/test/unit/specs/store/json/validators.json @@ -0,0 +1,56 @@ +[ + { + "address":"70705055A9FA5901735D0C3F0954501DDE667327", + "pub_key":{ + "type":"ed25519", + "data":"88564A32500A120AA72CEFBCF5462E078E5DDB70B6431F59F778A8DC4DA719A4" + }, + "voting_power":14, + "accum":585 + }, + { + "address":"760ACDE75EFC3DD0E4B2A6A3B96D91C05349EA31", + "pub_key":{ + "type":"ed25519", + "data":"7A9D783CE542B23FA23DC7F101460879861205772606B4C3FAEAFBEDFB00E7BD" + }, + "voting_power":32, + "accum":-1107 + }, + { + "address":"77C26DF82654C5A5DDE5C6B7B27F3F06E9C223C0", + "pub_key":{ + "type":"ed25519", + "data":"651E7B12B3C7234FB82B4417C59DCE30E4EA28F06AD0ACAEDFF05F013E463F10" + }, + "voting_power":19, + "accum":539 + }, + { + "address":"A130023895C57972021B7130FB0B25FB9BAD1557", + "pub_key":{ + "type":"ed25519", + "data":"2BA51EDE20AFC1EB462F22C10623A24A9C7E1B50282A5FB1E86A2E9B4FF19C47" + }, + "voting_power":1000, + "accum":943 + }, + { + "address":"D5B410389F606D5AA34BED52E143DDC40EF97C43", + "pub_key":{ + "type":"ed25519", + "data":"B3D42DEB413DA2BAE6CB1A9162B820783BD2905A90840795D2B11640344908C7" + }, + "voting_power":1000, + "accum":-1940 + }, + { + "address":"F173B5D14E4D9289E5654E4DCCCA3A3FF0D43799", + "pub_key":{ + "type":"ed25519", + "data":"C9B26A21B62EF839523D2040C0C4C046D86574CB6DFF92F3C0AF58DF209846FD" + }, + "voting_power":1000, + "accum":980 + } +] diff --git a/test/unit/specs/store/node.spec.js b/test/unit/specs/store/node.spec.js index a1be78f5bf..96883b4e6f 100644 --- a/test/unit/specs/store/node.spec.js +++ b/test/unit/specs/store/node.spec.js @@ -10,12 +10,19 @@ describe('Module: Node', () => { store = test.store node = test.node + node.rpcOpen = true node.rpcReconnect = jest.fn(() => { node.rpcOpen = true return Promise.resolve() }) }) + jest.useFakeTimers() + + afterEach(() => { + jest.runAllTimers() + }) + it('sets the header', () => { store.dispatch('setLastHeader', { height: 5, @@ -50,9 +57,20 @@ describe('Module: Node', () => { store.dispatch('reconnect') }) + it('should not reconnect if stop reconnecting is set', () => { + store.commit('stopConnecting', true) + node.rpcReconnect = () => { + throw Error('Should not reconnect') + } + store.dispatch('reconnect') + }) + it('reacts to rpc disconnection with reconnect', done => { let failed = false - node.rpcReconnect = () => done() + node.rpcReconnect = () => { + store.commit('stopConnecting', true) + done() + } node.rpc.on = jest.fn((value, cb) => { if (value === 'error' && !failed) { failed = true @@ -65,8 +83,10 @@ describe('Module: Node', () => { store.dispatch('nodeSubscribe') }) - it('doesnt reconnect on errors that do not mean discconection', done => { - node.rpcReconnect = () => done.fail() + it('doesnt reconnect on errors that do not mean disconnection', () => { + node.rpcReconnect = () => { + throw Error('Shouldnt reconnect') + } node.rpc.on = jest.fn((value, cb) => { if (value === 'error') { cb({ @@ -76,7 +96,6 @@ describe('Module: Node', () => { } }) store.dispatch('nodeSubscribe') - setTimeout(() => done(), 200) }) it('should set the initial status', () => { @@ -153,13 +172,11 @@ describe('Module: Node', () => { store.dispatch('pollRPCConnection', 10) }) - it('should not reconnect if pinging node is successful', done => { + it('should not reconnect if pinging node is successful', () => { node.rpc.status = (cb) => cb(null, {node_info: {}}) - node.rpcReconnect = () => done.fail() + node.rpcReconnect = () => { + throw Error('Shouldnt reconnect') + } store.dispatch('pollRPCConnection', 50) - setTimeout(() => { - node.rpcReconnect = () => {} - done() - }, 500) }) }) diff --git a/test/unit/specs/store/send.spec.js b/test/unit/specs/store/send.spec.js new file mode 100644 index 0000000000..2b0a0489b6 --- /dev/null +++ b/test/unit/specs/store/send.spec.js @@ -0,0 +1,118 @@ +import setup from '../../helpers/vuex-setup' + +let instance = setup() + +describe('Module: Send', () => { + let store, node + + beforeEach(() => { + let test = instance.shallow(null) + store = test.store + node = test.node + }) + + // DEFAULT + + it('should have an empty state by default', () => { + const state = { + nonce: 0, + sending: false, + queue: [] + } + expect(store.state.send).toEqual(state) + }) + + // MUTATIONS + + it('should set wallet nonce', () => { + const nonce = 959 + store.commit('setNonce', nonce) + expect(store.state.send.nonce).toBe(nonce) + }) + + it('should add a tx to the queue', () => { + const tx = { denom: 'acoin', amount: '500' } + store.commit('queueSend', tx) + expect(store.state.send.queue).toContain(tx) + }) + + it('should shift the queue', () => { + const txOne = { id: 'first-tx', denom: 'acoin', amount: '50' } + const txTwo = { id: 'second-tx', denom: 'acoin', amount: '125' } + store.commit('queueSend', txOne) + store.commit('queueSend', txTwo) + store.commit('shiftSendQueue') + expect(store.state.send.queue[0]).toBe(txTwo) + }) + + it('should continue with the next tx in queue', async () => { + const txOne = { type: 'test', id: 'first-tx', denom: 'acoin', amount: 50 } + const txTwo = { type: 'test', id: 'second-tx', denom: 'acoin', amount: 125 } + node.test = (v) => Promise.resolve(v) + node.sign = (v) => Promise.resolve(v) + node.postTx = jest.fn(() => Promise.resolve({ + check_tx: {}, deliver_tx: {} + })) + + await store.dispatch('sendTx', txOne) + let txDone = await store.dispatch('sendTx', txTwo) + await txDone + expect(store.state.send.queue.length).toBe(0) + + expect(node.postTx.mock.calls.length).toBe(2) + }) + + it('should set sending', () => { + store.commit('setSending', true) + expect(store.state.send.sending).toBe(true) + }) + + // ACTIONS + + it('should query wallet nonce', async () => { + const key = { address: 'DC97A6E1A3E1FE868B55BA93C7FC626368261E09' } + const nonce = 42 + node.queryNonce = async (addr) => { + expect(addr).toBe(key.address) + return { data: nonce } + } + await store.dispatch('initializeWallet', key) + await store.dispatch('queryNonce', key.address) + expect(store.state.send.nonce).toBe(nonce) + }) + + describe('send transactions', () => { + beforeEach(async () => { + let account = 'abc' + let password = '123' + node.sendTx = jest.fn(() => Promise.resolve()) + await store.dispatch('signIn', {account, password}) + }) + + it('should send from wallet', async () => { + const args = { type: 'buildSend' } + await store.dispatch('walletSend', args) + expect(node.sendTx.mock.calls).toMatchSnapshot() + }) + + it('should send a tx ', async () => { + node.queryNonce = async (addr) => ({ data: 88 }) + const key = { address: 'DC97A6E1A3E1FE868B55BA93C7FC626368261E09' } + await store.dispatch('initializeWallet', key) + const args = { + to: 'foo', + type: 'buildSend', + amount: { denom: 'foocoin', amount: 9001 } + } + await store.dispatch('sendTx', args) + expect(node.sendTx.mock.calls).toMatchSnapshot() + }) + + it('should fail sending a wallet tx ', async done => { + node.sign = () => Promise.reject() + const args = { type: 'buildSend' } + store.dispatch('walletSend', args) + .then(done.fail, () => done()) + }) + }) +}) diff --git a/test/unit/specs/store/validators.spec.js b/test/unit/specs/store/validators.spec.js new file mode 100644 index 0000000000..b35d43af30 --- /dev/null +++ b/test/unit/specs/store/validators.spec.js @@ -0,0 +1,51 @@ +import Vuex from 'vuex' +import { createLocalVue } from 'vue-test-utils' +import mockValidators from './json/validators.json' +const mockValidatorHash = '1234567890123456789012345678901234567890' +const mockValidatorHashTwo = '0123456789012345678901234567890123456789' +const mockValidatorHeaderTwo = { + height: 31337, + chain_id: 'hash-browns', + validators_hash: mockValidatorHashTwo +} + +const Validators = require('renderer/vuex/modules/validators').default + +const localVue = createLocalVue() +localVue.use(Vuex) + +describe('Module: Validators', () => { + let node, store + + beforeEach(() => { + node = require('../../helpers/node_mock') + store = new Vuex.Store({ + modules: { + validators: Validators({node}) + } + }) + }) + + it('should have no validators by default', () => { + expect(store.state.validators.validators).toEqual({}) + }) + + it('should have a null validator hash by default', () => { + expect(store.state.validators.validatorHash).toEqual(null) + }) + + it('should set validators', () => { + store.commit('setValidators', mockValidators) + expect(store.state.validators.validators.length).toBe(6) + }) + + it('should set validator hash', () => { + store.commit('setValidatorHash', mockValidatorHash) + expect(store.state.validators.validatorHash).toBe(mockValidatorHash) + }) + + it('should update validators if the hash is different', () => { + store.dispatch('maybeUpdateValidators', mockValidatorHeaderTwo) + expect(store.state.validators.validatorHash).toBe(mockValidatorHashTwo) + }) +}) diff --git a/test/unit/specs/store/wallet.spec.js b/test/unit/specs/store/wallet.spec.js index 1d00c5fd73..870edd6fe4 100644 --- a/test/unit/specs/store/wallet.spec.js +++ b/test/unit/specs/store/wallet.spec.js @@ -1,23 +1,160 @@ -import Vuex from 'vuex' -import { createLocalVue } from 'vue-test-utils' +import setup from '../../helpers/vuex-setup' -const Wallet = require('renderer/vuex/modules/wallet').default -const notifications = require('renderer/vuex/modules/notifications').default({}) - -const localVue = createLocalVue() -localVue.use(Vuex) +let instance = setup() describe('Module: Wallet', () => { let store, node beforeEach(() => { - node = require('../../helpers/node_mock') - store = new Vuex.Store({ - modules: { - wallet: Wallet({node}), - notifications + let test = instance.shallow(null) + store = test.store + node = test.node + }) + + // DEFAULT + + it('should have an empty state by default', () => { + const state = { + balances: [], + key: { address: '' }, + history: [], + denoms: [], + blockMetas: [] + } + expect(store.state.wallet).toEqual(state) + }) + + // MUTATIONS + + it('should set wallet balances ', () => { + const balances = [ { denom: 'leetcoin', amount: '1337' } ] + store.commit('setWalletBalances', balances) + expect(store.state.wallet.balances).toBe(balances) + }) + + it('should set wallet key and clear balance ', () => { + const key = 'antique-gold-key' + store.commit('setWalletKey', key) + expect(store.state.wallet.key).toBe(key) + expect(store.state.wallet.balances).toEqual([]) + }) + + it('should set wallet history', () => { + const history = ['once', 'upon', 'a', 'time'] + store.commit('setWalletHistory', history) + expect(store.state.wallet.history).toBe(history) + }) + + it('should set denoms', () => { + const denoms = ['acoin', 'bcoin', 'ccoin'] + store.commit('setDenoms', denoms) + expect(store.state.wallet.denoms).toBe(denoms) + }) + + it('should set transaction time', () => { + const blockHeight = 31337 + const time = 1234567890 + const history = [ + { height: blockHeight } + ] + const blockMetaInfo = { + header: { + time: time + } + } + store.commit('setWalletHistory', history) + store.commit('setTransactionTime', { blockHeight, blockMetaInfo }) + expect(store.state.wallet.history[0].time).toBe(time) + }) + + // ACTIONS + + it('should initialize wallet', async () => { + const key = { address: 'DC97A6E1A3E1FE868B55BA93C7FC626368261E09' } + await store.dispatch('initializeWallet', key) + expect(store.state.wallet.key).toEqual(key) + }) + + it('should query wallet state', async () => { + store.dispatch('queryWalletState') + expect(store.state.wallet.balances).toEqual([]) + expect(store.state.wallet.history).toEqual([]) + expect(store.state.send.nonce).toBe(0) + }) + + it('should query wallet balances', async () => { + node.queryAccount = () => Promise.resolve({ + data: { + coins: [{ + denom: 'fermion', + amount: 42 + }] + } + }) + await store.dispatch('queryWalletBalances') + expect(store.state.wallet.balances).toEqual([{ + 'amount': 42, + 'denom': 'fermion' + }]) + }) + + describe('query meta info', () => { + let height = 100 + let blockMeta = { + header: { + height: 100, + time: 42 } + } + + beforeEach(() => { + // prefill history + store.commit('setWalletHistory', [{ + height + }]) + // prefill block metas + store.state.wallet.blockMetas = [blockMeta] + }) + + it('should query transaction time', async () => { + await store.dispatch('queryTransactionTime', height) + expect(store.state.wallet.history[0].time).toBe(42) }) + + it('should query block info', async () => { + store.state.wallet.blockMetas = [] + node.rpc.blockchain = jest.fn(({ minHeight, maxHeight }, cb) => { + cb(null, {block_metas: [blockMeta]}) + }) + + let output = await store.dispatch('queryBlockInfo', height) + expect(output).toBe(blockMeta) + }) + + it('should reuse queried block info', async () => { + store.state.wallet.blockMetas = [blockMeta] + node.rpc.blockchain = jest.fn() + + let output = await store.dispatch('queryBlockInfo', height) + expect(output).toBe(blockMeta) + expect(node.rpc.blockchain).not.toHaveBeenCalled() + }) + + it('should show an info if block info is unavailable', async () => { + store.state.wallet.blockMetas = [] + node.rpc.blockchain = (props, cb) => cb('Error') + // prefill history + let height = 100 + let output = await store.dispatch('queryBlockInfo', height) + expect(output).toBe(null) + expect(store.state.notifications.length).toBe(1) + expect(store.state.notifications[0]).toMatchSnapshot() + }) + }) + + it('should load denoms', async () => { + await store.dispatch('loadDenoms') + expect(store.state.wallet.denoms).toEqual(['mycoin', 'fermion', 'gregcoin']) }) it('should enrich transaction times', async () => {