Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check stale validators with RPC #776

Merged
merged 5 commits into from Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -76,14 +76,6 @@ exports[`<ValidatorList /> empty should match snapshot 1`] = `
}
}

@media screen and (max-width:599px) {

}

@media screen and (max-width:599px) {

}

<div
class="c0"
>
Expand Down Expand Up @@ -148,70 +140,70 @@ exports[`<ValidatorList /> list should match snapshot 1`] = `
stroke: none;
}

.c18 {
.c17 {
display: inline-block;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
width: 24px;
height: 24px;
fill: #FF4040;
stroke: #FF4040;
fill: #00C781;
stroke: #00C781;
}

.c18 g {
.c17 g {
fill: inherit;
stroke: inherit;
}

.c18 *:not([stroke])[fill="none"] {
.c17 *:not([stroke])[fill="none"] {
stroke-width: 0;
}

.c18 *[stroke*="#"],
.c18 *[STROKE*="#"] {
.c17 *[stroke*="#"],
.c17 *[STROKE*="#"] {
stroke: inherit;
fill: none;
}

.c18 *[fill-rule],
.c18 *[FILL-RULE],
.c18 *[fill*="#"],
.c18 *[FILL*="#"] {
.c17 *[fill-rule],
.c17 *[FILL-RULE],
.c17 *[fill*="#"],
.c17 *[FILL*="#"] {
fill: inherit;
stroke: none;
}

.c17 {
.c18 {
display: inline-block;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
width: 24px;
height: 24px;
fill: #00C781;
stroke: #00C781;
fill: #FF4040;
stroke: #FF4040;
}

.c17 g {
.c18 g {
fill: inherit;
stroke: inherit;
}

.c17 *:not([stroke])[fill="none"] {
.c18 *:not([stroke])[fill="none"] {
stroke-width: 0;
}

.c17 *[stroke*="#"],
.c17 *[STROKE*="#"] {
.c18 *[stroke*="#"],
.c18 *[STROKE*="#"] {
stroke: inherit;
fill: none;
}

.c17 *[fill-rule],
.c17 *[FILL-RULE],
.c17 *[fill*="#"],
.c17 *[FILL*="#"] {
.c18 *[fill-rule],
.c18 *[FILL-RULE],
.c18 *[fill*="#"],
.c18 *[FILL*="#"] {
fill: inherit;
stroke: none;
}
Expand Down
Expand Up @@ -11,6 +11,7 @@ import { ValidatorList } from '..'

const activeValidator: Validator = {
address: 'oasis1qpc4ze5zzq3aa5mu5ttu4ku4ctp5t6x0asemymfz',
nodeAddress: 'oasis1qpwrtzadlddfuvdhjlta85268ju7dj0flsrfgg5x',
current_rate: 0.1,
rank: 0,
status: 'active',
Expand All @@ -25,6 +26,7 @@ const activeValidator: Validator = {
}
const inactiveValidator: Validator = {
address: 'oasis1qzyqaxestzlum26e2vdgvkerm6d9qgdp7gh2pxqe',
nodeAddress: 'oasis1qrdlqcv3tnv7qzuucnq0fncua52n66x7n5pm3n93',
current_rate: 0.2,
rank: 1,
status: 'inactive',
Expand All @@ -34,6 +36,7 @@ const inactiveValidator: Validator = {
}
const unknownValidator: Validator = {
address: 'oasis1qrfe9n26nq3t6vc9hlu9gnupwf4rm6wr0uglh3r7',
nodeAddress: 'oasis1qq672rjh7mldhaj35mlf4w34m8jtl9vu2c8qspkz',
current_rate: 0.2,
rank: 2,
status: 'unknown',
Expand Down Expand Up @@ -69,19 +72,37 @@ describe('<ValidatorList />', () => {

it('empty should match snapshot', () => {
const component = renderComponent(store)
store.dispatch(stakingActions.updateValidators([]))
store.dispatch(
stakingActions.updateValidators({
timestamp: new Date('2022').getTime(),
network: 'mainnet',
list: [],
}),
)
expect(component.container.firstChild).toMatchSnapshot()
})

it('list should match snapshot', () => {
const component = renderComponent(store)
store.dispatch(stakingActions.updateValidators([activeValidator, inactiveValidator, unknownValidator]))
store.dispatch(
stakingActions.updateValidators({
timestamp: new Date('2022').getTime(),
network: 'mainnet',
list: [activeValidator, inactiveValidator, unknownValidator],
}),
)
expect(component.container.firstChild).toMatchSnapshot()
})

it('should display validator details on click', async () => {
renderComponent(store)
store.dispatch(stakingActions.updateValidators([activeValidator]))
store.dispatch(
stakingActions.updateValidators({
timestamp: new Date('2022').getTime(),
network: 'mainnet',
list: [activeValidator],
}),
)

let row = screen.getByText(/test-validator/)
expect(row).toBeVisible()
Expand All @@ -97,7 +118,13 @@ describe('<ValidatorList />', () => {

it('should only display the details of a single validator', async () => {
renderComponent(store)
store.dispatch(stakingActions.updateValidators([activeValidator, inactiveValidator]))
store.dispatch(
stakingActions.updateValidators({
timestamp: new Date('2022').getTime(),
network: 'mainnet',
list: [activeValidator, inactiveValidator],
}),
)

let row = screen.getByText(/test-validator1/)
expect(row).toBeVisible()
Expand Down
12 changes: 5 additions & 7 deletions src/app/pages/StakingPage/Features/ValidatorList/index.tsx
Expand Up @@ -111,13 +111,11 @@ export const ValidatorList = memo((props: Props) => {
{t('common.validators', 'Validators')}
{updateValidatorsError && (
<p>
{t(
'account.validator.loadingError',
"Couldn't load validators. Showing validator list as of {{staleTimestamp}}.",
{
staleTimestamp: new Date(validatorsTimestamp).toLocaleString(),
},
)}
{t('account.validator.loadingError', "Couldn't load validators.")}{' '}
{validators.length > 0 &&
t('account.validator.showingStale', 'Showing validator list as of {{staleTimestamp}}.', {
staleTimestamp: new Date(validatorsTimestamp!).toLocaleString(),
})}
<br />
{updateValidatorsError}
</p>
Expand Down
25 changes: 7 additions & 18 deletions src/app/state/staking/index.ts
Expand Up @@ -3,21 +3,12 @@ import { createSlice } from 'utils/@reduxjs/toolkit'
import { useInjectReducer, useInjectSaga } from 'utils/redux-injectors'

import { stakingSaga } from './saga'
import { DebondingDelegation, Delegation, StakingState, Validator, ValidatorDetails } from './types'
import * as dump_validators from 'vendors/oasisscan/dump_validators.json'
import { DebondingDelegation, Delegation, StakingState, Validators, ValidatorDetails } from './types'

export const initialState: StakingState = {
debondingDelegations: [],
delegations: [],
validators: {
timestamp: dump_validators.dump_timestamp,
list: dump_validators.list.map(v => {
return {
...v,
status: 'unknown',
}
}),
},
validators: null,
updateValidatorsError: null,
selectedValidatorDetails: null,
selectedValidator: null,
Expand All @@ -32,15 +23,13 @@ const slice = createSlice({
state.loading = action.payload
},
fetchAccount(state, action: PayloadAction<string>) {},
updateValidators(state, action: PayloadAction<Validator[]>) {
updateValidators(state, action: PayloadAction<Validators>) {
state.updateValidatorsError = null
state.validators = {
timestamp: Date.now(),
list: action.payload,
}
state.validators = action.payload
},
updateValidatorsError(state, action: PayloadAction<string>) {
state.updateValidatorsError = action.payload
updateValidatorsError(state, action: PayloadAction<{ error: string; validators: Validators }>) {
state.updateValidatorsError = action.payload.error
state.validators = action.payload.validators
},
updateDelegations(state, action: PayloadAction<Delegation[]>) {
state.delegations = action.payload
Expand Down
78 changes: 77 additions & 1 deletion src/app/state/staking/saga.test.ts
Expand Up @@ -4,11 +4,12 @@ import { expectSaga } from 'redux-saga-test-plan'
import * as matchers from 'redux-saga-test-plan/matchers'
import { EffectProviders, StaticProvider } from 'redux-saga-test-plan/providers'
import { select } from 'redux-saga/effects'
import { RootState } from 'types'

import { initialState, stakingActions, stakingReducer } from '.'
import { getExplorerAPIs, getOasisNic } from '../network/saga'
import { selectEpoch } from '../network/selectors'
import { fetchAccount, stakingSaga } from './saga'
import { fetchAccount, getMainnetDumpValidators, refreshValidators, now, stakingSaga } from './saga'
import { StakingState, Validator } from './types'

const qty = (number: number) => oasis.quantity.fromBigInt(BigInt(number))
Expand All @@ -29,11 +30,13 @@ describe('Staking Sagas', () => {
stakingAccount: jest.fn(),
stakingDebondingDelegationInfosFor: jest.fn(),
stakingDelegationInfosFor: jest.fn(),
schedulerGetValidators: jest.fn(),
}

const providers: (EffectProviders | StaticProvider)[] = [
[matchers.call.fn(getExplorerAPIs), { getAllValidators }],
[matchers.call.fn(getOasisNic), nic],
[matchers.call.fn(now), new Date('2022').getTime()],
]
const validAddress = 'oasis1qqty93azxp4qeft3krvv23ljyj57g3tzk56tqhqe'

Expand Down Expand Up @@ -118,4 +121,77 @@ describe('Staking Sagas', () => {
])
})
})

describe('Fetch validators fallbacks', () => {
buberdds marked this conversation as resolved.
Show resolved Hide resolved
it('should load validators when switching network', () => {
getAllValidators.mockResolvedValue([{ address: 'fromApi' }] as Validator[])
return expectSaga(refreshValidators)
.withState({
network: { selectedNetwork: 'testnet' },
staking: { validators: { network: 'mainnet', list: [{ address: 'existing' }] } },
} as RootState)
.provide(providers)
.put(
stakingActions.updateValidators({
timestamp: new Date('2022').getTime(),
network: 'testnet',
list: [{ address: 'fromApi' }] as Validator[],
}),
)
.run()
})

it('should use fallback on mainnet', () => {
getAllValidators.mockRejectedValue('apiFailed')
const getMainnetDumpValidatorsMock = {
dump_timestamp: 1647996761337,
dump_timestamp_iso: '2022-03-23T00:52:41.337Z',
list: [
{
rank: 1,
address: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe',
name: 'stakefish',
nodeAddress: 'oasis1qrg52ccz4ts6cct2qu4retxn7kkdlusjh5pe74ar',
status: 'active',
_expectedStatus: 'active' as const,
},
{
rank: 2,
address: 'oasis1qqekv2ymgzmd8j2s2u7g0hhc7e77e654kvwqtjwm',
name: 'BinanceStaking',
nodeAddress: 'oasis1qqp0h2h92eev7nsxgqctvuegt8ge3vyg0qyluc4k',
status: 'active',
_expectedStatus: 'inactive' as const,
},
],
}
nic.schedulerGetValidators.mockResolvedValue([
{
// oasis1qrg52ccz4ts6cct2qu4retxn7kkdlusjh5pe74ar
id: oasis.misc.fromHex('91e7768ae47cd1641d6f883b97e3ea6d0286240bc3e3e2953c5c2e0dce6753a3'),
voting_power: 1,
},
] as oasis.types.SchedulerValidator[])

return expectSaga(refreshValidators)
.withState({
network: { selectedNetwork: 'mainnet' },
} as RootState)
.provide([...providers, [matchers.call.fn(getMainnetDumpValidators), getMainnetDumpValidatorsMock]])
.put(
stakingActions.updateValidatorsError({
error: 'apiFailed',
validators: {
timestamp: getMainnetDumpValidatorsMock.dump_timestamp,
network: 'mainnet',
list: getMainnetDumpValidatorsMock.list.map((v, ix) => ({
...v,
status: v._expectedStatus,
})),
},
}),
)
.run()
})
})
})