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 @@
-
-field(type='select' :options='blockchainOptions' v-model='blockchainName')
-
-
-
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 @@
-
-.modal-blockchain(v-if='config.modals.blockchain.active' @click='close($event)')
- .modal-blockchain-container: blockchain-select
-
-
-
-
-
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 () => {