Skip to content

Commit 4656af4

Browse files
committed
fix: show loading and unavailable states for operator balance and fee
The operator detail page rendered a confident "0 ETH" for Balance and Annual Fee whenever the on-chain reads were pending or failing, because useOperatorEarningsAndFees coalesced `data ?? 0n`. - expose earningsStatus / feesStatus from the hook via combineQueryStatus - treat fetchStatus "paused" as unavailable: a read that cannot reach the RPC parks at status "pending" and never resolves to an error, which would otherwise leave the UI on a skeleton indefinitely - render a skeleton while pending and an explicit unavailable message instead of a zero amount - gate Withdraw and Update Fee on query success rather than on the coalesced bigint, so a failed read no longer leaves Update Fee clickable into the fee wizard # Conflicts: # src/app/routes/dashboard/operators/operator.tsx
1 parent b2a88cc commit 4656af4

2 files changed

Lines changed: 109 additions & 20 deletions

File tree

‎src/app/routes/dashboard/operators/operator.tsx‎

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,43 @@ import { IncreaseOperatorFeeStatusBadge } from "@/components/operator/increase-o
1919
import { OperatorValidatorsList } from "@/components/operator/operator-validators-list";
2020
import { OperatorStatusBadge } from "@/components/operator/operator-status-badge";
2121
import { useOperatorEarningsAndFees } from "@/hooks/operator/use-operator-earnings-and-fees";
22+
import { Skeleton } from "@/components/ui/skeleton";
23+
24+
const AmountSkeleton: FC = () => (
25+
<div className="flex flex-col gap-2">
26+
<Skeleton className="h-7 w-32" />
27+
<Skeleton className="h-4 w-16" />
28+
</div>
29+
);
30+
31+
const AmountUnavailable: FC = () => (
32+
<div className="flex flex-col items-start gap-2">
33+
<Text variant="body-2-medium" className="text-error-500">
34+
Couldn't read this value from the network.
35+
</Text>
36+
<Text variant="body-3-medium" className="text-gray-500">
37+
It will appear once the connection recovers.
38+
</Text>
39+
</div>
40+
);
2241

2342
export const Operator: FC<ComponentPropsWithoutRef<"div">> = ({ ...props }) => {
2443
const params = useOperatorPageParams();
2544
const operatorId = BigInt(params.operatorId!);
2645
const operator = useOperator(operatorId!);
27-
const { feeEth, yearlyFeeEth, yearlyFeeSSV, balanceEth, balanceSSV } =
28-
useOperatorEarningsAndFees(operatorId);
46+
const {
47+
feeEth,
48+
yearlyFeeEth,
49+
yearlyFeeSSV,
50+
balanceEth,
51+
balanceSSV,
52+
earningsStatus,
53+
feesStatus,
54+
} = useOperatorEarningsAndFees(operatorId);
2955

30-
const hasBalance = balanceEth > 0n || balanceSSV > 0n;
56+
const hasBalance =
57+
earningsStatus.isSuccess && (balanceEth > 0n || balanceSSV > 0n);
58+
const canUpdateFee = feesStatus.isSuccess && feeEth.data !== 0n;
3159

3260
if (!operator.data) return null;
3361

@@ -101,13 +129,29 @@ export const Operator: FC<ComponentPropsWithoutRef<"div">> = ({ ...props }) => {
101129
<Text variant="headline4" className="text-gray-500">
102130
Balance
103131
</Text>
104-
<div className="flex flex-col gap-4">
105-
<BalanceDisplay amount={balanceEth} token="ETH" />
106-
{yearlyFeeSSV !== 0n && !operator.data.migrated && <BalanceDisplay amount={balanceSSV} token="SSV" />}
107-
</div>
132+
{earningsStatus.isUnavailable ? (
133+
<AmountUnavailable />
134+
) : earningsStatus.isPending ? (
135+
<AmountSkeleton />
136+
) : (
137+
<div className="flex flex-col gap-4">
138+
<BalanceDisplay amount={balanceEth} token="ETH" />
139+
{yearlyFeeSSV !== 0n && !operator.data.migrated && (
140+
<BalanceDisplay amount={balanceSSV} token="SSV" />
141+
)}
142+
</div>
143+
)}
108144
<Tooltip
109145
asChild
110-
content={!hasBalance ? "No balance to withdraw" : undefined}
146+
content={
147+
earningsStatus.isUnavailable
148+
? "Balance unavailable"
149+
: earningsStatus.isPending
150+
? "Loading balance"
151+
: !hasBalance
152+
? "No balance to withdraw"
153+
: undefined
154+
}
111155
>
112156
<Button
113157
as={Link}
@@ -127,16 +171,26 @@ export const Operator: FC<ComponentPropsWithoutRef<"div">> = ({ ...props }) => {
127171
</Text>
128172
<IncreaseOperatorFeeStatusBadge />
129173
</div>
130-
<div className="flex flex-col gap-4">
131-
<BalanceDisplay amount={yearlyFeeEth} token="ETH" />
132-
{yearlyFeeSSV > 0 && (
133-
<BalanceDisplay amount={yearlyFeeSSV} token="SSV" />
134-
)}
135-
</div>
174+
{feesStatus.isUnavailable ? (
175+
<AmountUnavailable />
176+
) : feesStatus.isPending ? (
177+
<AmountSkeleton />
178+
) : (
179+
<div className="flex flex-col gap-4">
180+
<BalanceDisplay amount={yearlyFeeEth} token="ETH" />
181+
{yearlyFeeSSV > 0 && (
182+
<BalanceDisplay amount={yearlyFeeSSV} token="SSV" />
183+
)}
184+
</div>
185+
)}
136186
<Tooltip
137187
asChild
138188
content={
139-
feeEth.data === 0n ? (
189+
feesStatus.isUnavailable ? (
190+
"Fee unavailable"
191+
) : feesStatus.isPending ? (
192+
"Loading fee"
193+
) : feeEth.data === 0n ? (
140194
<>
141195
Operators with a fee of 0 cannot change their fee.{" "}
142196
<Button
@@ -154,7 +208,7 @@ export const Operator: FC<ComponentPropsWithoutRef<"div">> = ({ ...props }) => {
154208
>
155209
<Button
156210
as={Link}
157-
disabled={feeEth.isLoading || feeEth.data === 0n}
211+
disabled={!canUpdateFee}
158212
to="fee/update"
159213
variant="secondary"
160214
size="xl"

‎src/hooks/operator/use-operator-earnings-and-fees.ts‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,52 @@ import { useGetOperatorEarnings } from "@/lib/contract-interactions/hooks/getter
22
import { useGetOperatorEarningsSSV } from "@/lib/contract-interactions/hooks/getter";
33
import { useGetOperatorFee } from "@/lib/contract-interactions/hooks/getter";
44
import { useGetOperatorFeeSSV } from "@/lib/contract-interactions/hooks/getter";
5+
import { combineQueryStatus } from "@/lib/react-query";
56
import { getYearlyFee } from "@/lib/utils/operator";
67

8+
type FetchStatusOnly = { fetchStatus: "fetching" | "paused" | "idle" };
9+
10+
// A read that cannot reach the RPC parks at fetchStatus "paused" and never
11+
// resolves to an error, so paused counts as unavailable too.
12+
const withUnavailable = <T extends { isError: boolean; isPending: boolean }>(
13+
status: T,
14+
queries: FetchStatusOnly[],
15+
) => {
16+
const isPaused = queries.some((query) => query.fetchStatus === "paused");
17+
return {
18+
...status,
19+
isPaused,
20+
isUnavailable: status.isError || isPaused,
21+
isPending: status.isPending && !isPaused,
22+
};
23+
};
24+
725
export const useOperatorEarningsAndFees = (operatorId: bigint) => {
8-
const earningsEth = useGetOperatorEarnings({ id: operatorId });
9-
const earningsSSV = useGetOperatorEarningsSSV({ id: operatorId });
26+
const queryOptions = { retry: 2, networkMode: "always" } as const;
1027

11-
const feeEth = useGetOperatorFee({ operatorId });
12-
const feeSSV = useGetOperatorFeeSSV({ operatorId });
28+
const earningsEth = useGetOperatorEarnings({ id: operatorId }, queryOptions);
29+
const earningsSSV = useGetOperatorEarningsSSV(
30+
{ id: operatorId },
31+
queryOptions,
32+
);
33+
34+
const feeEth = useGetOperatorFee({ operatorId }, queryOptions);
35+
const feeSSV = useGetOperatorFeeSSV({ operatorId }, queryOptions);
1336
const yearlyFeeEth = getYearlyFee(feeEth.data ?? 0n);
1437
const yearlyFeeSSV = getYearlyFee(feeSSV.data ?? 0n);
1538
const balanceEth = earningsEth.data ?? 0n;
1639
const balanceSSV = earningsSSV.data ?? 0n;
1740

41+
// `?? 0n` above is indistinguishable from an on-chain zero — gate on these.
42+
const earningsStatus = withUnavailable(
43+
combineQueryStatus(earningsEth, earningsSSV),
44+
[earningsEth, earningsSSV],
45+
);
46+
const feesStatus = withUnavailable(combineQueryStatus(feeEth, feeSSV), [
47+
feeEth,
48+
feeSSV,
49+
]);
50+
1851
return {
1952
earningsEth,
2053
earningsSSV,
@@ -24,5 +57,7 @@ export const useOperatorEarningsAndFees = (operatorId: bigint) => {
2457
yearlyFeeSSV,
2558
balanceEth,
2659
balanceSSV,
60+
earningsStatus,
61+
feesStatus,
2762
};
2863
};

0 commit comments

Comments
 (0)