Skip to content

Commit 8633eeb

Browse files
committed
Add node version check when joining sponsorships
1 parent 780ebd3 commit 8633eeb

File tree

2 files changed

+81
-12
lines changed

2 files changed

+81
-12
lines changed

src/modals/JoinSponsorshipModal.tsx

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import CopyIcon from '@atlaskit/icon/glyph/copy'
2-
import React, { useMemo, useState } from 'react'
2+
import React, { useMemo, useState, useEffect } from 'react'
33
import styled from 'styled-components'
44
import { toaster } from 'toasterhea'
55
import { Alert } from '~/components/Alert'
@@ -9,7 +9,7 @@ import { getSelfDelegationFraction } from '~/getters'
99
import { useConfigValueFromChain, useMediaQuery } from '~/hooks'
1010
import { useAllOperatorsForWalletQuery } from '~/hooks/operators'
1111
import { useSponsorshipTokenInfo } from '~/hooks/sponsorships'
12-
import { useInterceptHeartbeats } from '~/hooks/useInterceptHeartbeats'
12+
import { Heartbeat, useInterceptHeartbeats } from '~/hooks/useInterceptHeartbeats'
1313
import useOperatorLiveNodes from '~/hooks/useOperatorLiveNodes'
1414
import { SelectField2 } from '~/marketplace/components/SelectField2'
1515
import FormModal, {
@@ -35,6 +35,7 @@ import Label from '~/shared/components/Ui/Label'
3535
import useCopy from '~/shared/hooks/useCopy'
3636
import { useWalletAccount } from '~/shared/stores/wallet'
3737
import Toast from '~/shared/toasts/Toast'
38+
import { isNodeVersionGreaterThanOrEqualTo } from '~/shared/utils/nodeVersion'
3839
import { COLORS } from '~/shared/utils/styled'
3940
import { truncate } from '~/shared/utils/text'
4041
import { humanize } from '~/shared/utils/time'
@@ -55,6 +56,8 @@ interface Props extends Pick<FormModalProps, 'onReject'> {
5556

5657
const limitErrorToaster = toaster(Toast, Layer.Toast)
5758

59+
const requiredNodeVersion = '102.0.0'
60+
5861
function JoinSponsorshipModal({
5962
chainId,
6063
onResolve,
@@ -295,10 +298,6 @@ function JoinSponsorshipModal({
295298
</li>
296299
</PropList>
297300
</Section>
298-
<StyledAlert type="notice" title="Node version check">
299-
Node versions 100.2.3 and below are vulnerable to slashing. Ensure that
300-
all of your nodes are not running an outdated version.
301-
</StyledAlert>
302301
{isBelowSelfFundingLimit && (
303302
<StyledAlert type="error" title="Low self-funding">
304303
You cannot stake on Sponsorships because your Operator is below the
@@ -317,9 +316,10 @@ function JoinSponsorshipModal({
317316
</StyledAlert>
318317
)}
319318
{!isBelowSelfFundingLimit && !hasUndelegationQueue && (
320-
<LiveNodesCheck
319+
<LiveNodesAndVersionCheck
321320
liveNodesCountLoading={liveNodesCountLoading}
322321
liveNodesCount={liveNodesCount}
322+
heartbeats={heartbeats}
323323
/>
324324
)}
325325
{sponsorship.minimumStakingPeriodSeconds > 0 && (
@@ -343,21 +343,65 @@ function JoinSponsorshipModal({
343343
interface LiveNodesCheckProps {
344344
liveNodesCountLoading: boolean
345345
liveNodesCount: number
346+
heartbeats: Record<string, Heartbeat | undefined>
346347
}
347348

348-
function LiveNodesCheck({ liveNodesCountLoading, liveNodesCount }: LiveNodesCheckProps) {
349-
if (liveNodesCountLoading) {
349+
function LiveNodesAndVersionCheck({
350+
liveNodesCountLoading,
351+
liveNodesCount,
352+
heartbeats,
353+
}: LiveNodesCheckProps) {
354+
const [hasWaitedForHeartbeats, setHasWaitedForHeartbeats] = useState(false)
355+
356+
useEffect(() => {
357+
const timer = setTimeout(() => {
358+
setHasWaitedForHeartbeats(true)
359+
}, 6000) // Wait for 6 seconds to ensure heartbeats are available from ~all nodes
360+
361+
return () => clearTimeout(timer)
362+
}, [])
363+
364+
if (!hasWaitedForHeartbeats || liveNodesCountLoading) {
350365
return (
351366
<StyledAlert type="loading" title="Checking Streamr nodes">
352367
<span>
353-
In order to continue, you need to have one or more Streamr nodes
354-
running and correctly configured. You will be slashed if you stake
355-
without your nodes contributing resources to the stream.
368+
In order to continue, you need to have, 1 or more Streamr nodes
369+
running, and have them all be correctly configured. All of your nodes
370+
must be running version {requiredNodeVersion} or higher. You are
371+
vulnerable to slashing if you stake with misconfigured nodes.
356372
</span>
357373
</StyledAlert>
358374
)
359375
}
360376

377+
const nodeVersions = Object.values(heartbeats).map(
378+
(heartbeat) => heartbeat?.applicationVersion,
379+
)
380+
const areAllNodesRunningRequiredVersion = nodeVersions.every((version) => {
381+
// If version is undefined (old node) or null, treat it as not meeting the requirement
382+
if (!version) {
383+
return false
384+
}
385+
return isNodeVersionGreaterThanOrEqualTo(version, requiredNodeVersion)
386+
})
387+
if (!areAllNodesRunningRequiredVersion) {
388+
return (
389+
<StyledAlert type="error" title="Streamr nodes are outdated">
390+
<p>
391+
The minimum required version for all of your Streamr nodes is{' '}
392+
{requiredNodeVersion} or higher. Please update your nodes.
393+
</p>
394+
<a
395+
href={R.docs('/guides/how-to-update-your-streamr-node/')}
396+
target="_blank"
397+
rel="noreferrer noopener"
398+
>
399+
How to upgrade a Streamr node <LinkIcon name="externalLink" />
400+
</a>
401+
</StyledAlert>
402+
)
403+
}
404+
361405
if (liveNodesCount > 0) {
362406
return (
363407
<StyledAlert type="success" title="Streamr nodes detected">

src/shared/utils/nodeVersion.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const isNodeVersionGreaterThanOrEqualTo = (
2+
version: string,
3+
requiredVersion: string,
4+
): boolean => {
5+
// Split versions and pad with zeros if parts are missing
6+
const normalize = (ver: string) => {
7+
const parts = ver.split('.').map((part) => parseInt(part, 10))
8+
return [parts[0] || 0, parts[1] || 0, parts[2] || 0]
9+
}
10+
11+
const [major, minor, patch] = normalize(version)
12+
const [requiredMajor, requiredMinor, requiredPatch] = normalize(requiredVersion)
13+
14+
// Handle invalid inputs
15+
if (isNaN(major) || isNaN(requiredMajor)) {
16+
return false
17+
}
18+
19+
return (
20+
major > requiredMajor ||
21+
(major === requiredMajor &&
22+
(minor > requiredMinor ||
23+
(minor === requiredMinor && patch >= requiredPatch)))
24+
)
25+
}

0 commit comments

Comments
 (0)