Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/containers/Ledgers/LedgerMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const LedgerMetrics = ({
content = renderXRP(data[key], language)
} else if (key === 'ledger_interval' && data[key] !== '--') {
content = `${data[key]} ${t('seconds_short')}`
} else if (key === 'nUnl' && data[key]?.length === 0) {
} else if (key === 'nUnl' && (!data[key] || data[key]?.length === 0)) {
Comment thread
ckeshava marked this conversation as resolved.
return null
} else if (key === 'nUnl') {
content = data[key]?.length
Expand Down
4 changes: 2 additions & 2 deletions src/containers/Ledgers/Ledgers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ export const Ledgers = ({ paused }: { paused: boolean }) => {
ledgers,
paused,
)
const isOnline = useIsOnline()
const { isOnline } = useIsOnline()

return (
<div className="ledgers">
{isOnline && ledgers ? (
{isOnline && Object.keys(ledgers).length > 0 ? (
<>
<Legend />
<div className="control">
Expand Down
3 changes: 2 additions & 1 deletion src/containers/Ledgers/test/LedgersPage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,12 @@ describe('Ledgers Page container', () => {
expect(wrapper.find('.txn').length).toBe(0)

server.send(prevLedgerMessage)
await sleep(260)
wrapper.update()
expect(wrapper.exists()).toBe(true)
expect(wrapper.find('.ledgers').length).toBe(1)
expect(wrapper.find('.ledger-list').length).toBe(1)
expect(wrapper.find('.ledger').length).toBe(1)
expect(wrapper.find('.ledger').length).toBe(2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the two .ledger react components in the wrapper? Since we send one prevLedgerMessage, shouldn't we expect to see one of the .ledger react component?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the same reason 2 components were expected here -

expect(wrapper.find('.ledger').length).toBe(2)

One is the latest validated ledger and another from prevLedgerMessage on same reasoning -

// Recent refactor of the StreamsProvider eagerly fetches the latest validated ledger. The objective is to reduce the latency of displaying the first validated ledger. Consequently, loading the page fetches the latest validated ledger.


server.send(validationMessage)
// validation-messages are processed in batches every 200 ms
Expand Down
34 changes: 11 additions & 23 deletions src/containers/Network/Validators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,42 +36,30 @@ export const ValidatorsData = () => {
const [feeSettings, setFeeSettings] = useState<FeeSettings>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amongst the diff'ed lines in this file, what is the fix for this bug:

Loads Validator table with VHS data if data from live websocket connection is not available yet.

I'm trying to understand the goal of the changes in this file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Goal is to return the data from VHS even if data from clio is yet not available. It used to do that previous to #1232. You can test it by running it locally on a previous commit.

The difference in livenet tab and localhost tab in the video will make it more clear.


const merged = useMemo(() => {
const fromVHS = validatorsFromVHS ?? {}
const fromStream = validatorsFromValidations ?? {}

if (
!validatorsFromVHS ||
!(
validatorsFromValidations &&
Object.keys(validatorsFromValidations).length
)
Object.keys(fromVHS).length === 0 &&
Object.keys(fromStream).length === 0
) {
return []
}

const updated: Record<string, StreamValidator> = {}
const keys = new Set(
Object.keys(validatorsFromVHS).concat(
Object.keys(validatorsFromValidations),
),
)
const keys = new Set(Object.keys(fromVHS).concat(Object.keys(fromStream)))
keys.forEach((d: string) => {
const newData: StreamValidator =
validatorsFromVHS[d] || validatorsFromValidations[d]
const newData: StreamValidator = fromVHS[d] ?? fromStream[d]
if (
newData.ledger_index == null &&
validatorsFromValidations[d] &&
validatorsFromValidations[d].ledger_index
fromStream[d] &&
fromStream[d].ledger_index
) {
// VHS uses `current_index` instead of `ledger_index`
// If `ledger_index` isn't defined, then we're still using the VHS data,
// instead of the Streams data
newData.ledger_index = validatorsFromValidations[d].ledger_index
}
if (newData.current_index == null) {
newData.signing_key = validatorsFromValidations[d].signing_key
}
Comment on lines -68 to -70

@achowdhry-ripple achowdhry-ripple Dec 3, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can we remove this part? it seems like the properties that are being set have changed with this logic change now

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signing_key was never a property on validatorsFromValidations[d]. I don't know why this was added during recent refactor, but if we go further down the rendering part, master_key is given priority irrespective of the source of data.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If signing_key property is not defined, then this line will not render the validator component.

} else if (data?.master_key || data?.signing_key) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If signing_key property is not defined, then this line will not render the validator component.

master_key is given preference, and it will render the component. Won't render when both are not present.
In any case signing_key was not set by the code prior to #1232 and it's incorrectly set currently. So why the concern now?

// latest hash and time comes from the validations stream
if (validatorsFromValidations[d]) {
newData.time = validatorsFromValidations[d].time
newData.ledger_hash = validatorsFromValidations[d].ledger_hash
newData.ledger_index = fromStream[d].ledger_index
newData.ledger_hash = fromStream[d].ledger_hash
}

updated[d] = newData
Expand Down
111 changes: 69 additions & 42 deletions src/containers/Network/test/validators.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,29 @@ import i18n from '../../../i18n/testConfig'
import mockValidators from './mockValidators.json'
import validationMessage from './mockValidation.json'
import SocketContext from '../../shared/SocketContext'
import NetworkContext from '../../shared/NetworkContext'
import MockWsClient from '../../test/mockWsClient'
import { QuickHarness } from '../../test/utils'
import { VALIDATORS_ROUTE } from '../../App/routes'
import { Validators } from '../Validators'
import { StreamsProvider } from '../../shared/components/Streams'
import { queryClient } from '../../shared/QueryClient'
import { VHSValidatorsProvider } from '../../shared/components/VHSValidators'

const WS_URL = 'ws://localhost:1234'

describe('Validators Tab container', () => {
let server
let client

const createWrapper = () =>
mount(
<SocketContext.Provider value={client}>
<QueryClientProvider client={queryClient}>
<VHSValidatorsProvider>
<StreamsProvider>
<QuickHarness
i18n={i18n}
initialEntries={['/network/validators']}
>
<Route path={VALIDATORS_ROUTE.path} element={<Validators />} />
</QuickHarness>
</StreamsProvider>
</VHSValidatorsProvider>
</QueryClientProvider>
<NetworkContext.Provider value="main">
<QueryClientProvider client={queryClient}>
<QuickHarness i18n={i18n} initialEntries={['/network/validators']}>
<Route path={VALIDATORS_ROUTE.path} element={<Validators />} />
</QuickHarness>
</QueryClientProvider>
</NetworkContext.Provider>
</SocketContext.Provider>,
)

Expand All @@ -57,43 +52,75 @@ describe('Validators Tab container', () => {
wrapper.unmount()
})

it('receives live validation', async () => {
it('displays validators from VHS API', (done) => {
moxios.stubRequest(`${process.env.VITE_DATA_URL}/validators/main`, {
status: 200,
response: { validators: mockValidators },
})

const wrapper = createWrapper()

moxios.wait(() => {
setTimeout(() => {
wrapper.update()

// Should show 4 validators from VHS with 2 in UNL
expect(wrapper.find('.stat').text()).toEqual(
'validators_found: 4 (unl: 2)',
)

// Should render hexagons visualization
expect(wrapper.find('.hexagons').length).toBe(1)

wrapper.unmount()
done()
}, 100)
})
})

it('merges validators from both VHS API and WebSocket stream', (done) => {
// Mock the VHS API response with 4 validators
moxios.stubRequest(`${process.env.VITE_DATA_URL}/validators/main`, {
status: 200,
response: mockValidators,
response: { validators: mockValidators },
})

expect(wrapper.find('.validators').length).toBe(1)
expect(wrapper.find('.stat').html()).toBe(
'<div class="stat"><span>validators_found: </span><span>0<i> (unl: )</i></span></div>',
)
expect(wrapper.find('.validators-table').length).toBe(1)
const wrapper = createWrapper()

// Send a live validation message via WebSocket
// This validator (n9KaxgJv69FucW5kkiaMhCqS6sAR1wUVxpZaZmLGVXxAcAse9YhR) is NOT in mockValidators
server.send(validationMessage)

setTimeout(() => {
wrapper.update()
expect(wrapper.find('.stat').html()).toBe(
'<div class="stat"><span>validators_found: </span><span>4<i> (unl: 2)</i></span></div>',
)
expect(wrapper.find('.validators .tooltip').length).toBe(0)

wrapper.find('.validators .hexagon').first().simulate('mouseOver')
wrapper.update()
expect(wrapper.find('.validators-container .tooltip').length).toBe(1)
expect(
wrapper.find('.validators-container .tooltip .pubkey').html(),
).toBe(
'<div class="pubkey">n9KaxgJv69FucW5kkiaMhCqS6sAR1wUVxpZaZmLGVXxAcAse9YhR</div>',
)
wrapper.find('.validators .hexagon').first().simulate('mouseLeave')
wrapper.update()
expect(wrapper.find('.validators-container .tooltip').length).toBe(0)
expect(wrapper.find('.validators .hexagons').length).toBe(1)
expect(wrapper.find('.validators-table table tr').length).toBe(5)
wrapper.unmount()
// Wait for both VHS API and WebSocket data to be processed
moxios.wait(() => {
setTimeout(() => {
wrapper.update()

// Should show 5 validators total (4 from VHS + 1 new from WebSocket)
// Note: The validation_public_key in mockValidation.json is different from all signing_keys in mockValidators.json
expect(wrapper.find('.stat').text()).toEqual(
'validators_found: 5 (unl: 2)',
)

// Should render hexagons with merged data
expect(wrapper.find('.hexagons').length).toBe(1)

// Should render validators table
expect(wrapper.find('.validators-table').length).toBe(1)

// Check table has 5 data rows (4 from VHS + 1 from WebSocket)
const tableRows = wrapper.find('.validators-table table tbody tr')
expect(tableRows.length).toBe(5)

// Verify that the WebSocket validator is in the table
const tableText = wrapper.find('.validators-table').text()
expect(tableText).toContain(
'n9KaxgJv69FucW5kkiaMhCqS6sAR1wUVxpZaZmLGVXxAcAse9YhR',
)

wrapper.unmount()
done()
}, 100)
})
})
})
6 changes: 2 additions & 4 deletions src/containers/shared/components/Streams/StreamsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const truncateLedgers = (ledgers: Record<string, Ledger>, count) =>
export const StreamsProvider: FC = ({ children }) => {
// In custom mode we populate metrics from ledgers loaded into memory
const useServerMetrics = process.env.VITE_ENVIRONMENT !== 'custom'
const [ledgers, setLedgers] = useState<Record<number, Ledger>>([])
const [ledgers, setLedgers] = useState<Record<number, Ledger>>({})
Comment thread
Patel-Raj11 marked this conversation as resolved.
const ledgersRef = useRef<Record<number, Ledger>>(ledgers)
const firstLedgerRef = useRef<number>(0)
const [validators, setValidators] = useState<Record<number, StreamValidator>>(
Expand All @@ -78,9 +78,7 @@ export const StreamsProvider: FC = ({ children }) => {
{ enabled: socket.getState().online },
)
const { data: serverRunningMetrics, refetch: refetchServerRunningMetrics } =
useQuery<string[]>('runningMetrics', () => fetchMetrics(), {
enabled: socket.getState().online,
Comment thread
ckeshava marked this conversation as resolved.
})
useQuery<string[]>('runningMetrics', () => fetchMetrics())

function addLedger(index: number | string) {
// Only add new ledgers that are newer than the last one added.
Expand Down