Tighten delegation summary query keys and rewards context

- Remove redeemAllRewards / TRewardsTransaction from rewards context
- Use a dedicated React Query key when no client address is set
This commit is contained in:
Tommy Verrall
2026-05-06 15:35:02 +02:00
parent 09548a9aa9
commit c4df05157a
6 changed files with 74 additions and 90 deletions
+13 -12
View File
@@ -1,23 +1,21 @@
import type { DelegationWithEverything, WrappedDelegationEvent } from '@nymproject/types';
import type { DecCoin, DelegationWithEverything, WrappedDelegationEvent } from '@nymproject/types';
import { getAllPendingDelegations, getDelegationSummary } from 'src/requests';
import { decCoinToDisplay } from 'src/utils';
export type DelegationSummaryBundle = {
delegations: (DelegationWithEverything | WrappedDelegationEvent)[];
pendingDelegations: WrappedDelegationEvent[];
totalDelegations: string;
totalRewards: string;
totalDelegationsAndRewards: string;
totalDelegations: DecCoin;
totalRewards: DecCoin;
totalDelegationsAndRewards: DecCoin;
};
export async function fetchDelegationSummaryQuery(): Promise<DelegationSummaryBundle> {
const data = await getDelegationSummary();
const pending = await getAllPendingDelegations();
const pendingOnNewNodes = pending.filter((event) => {
const some = data.delegations.some(({ node_identity }) => node_identity === event.node_identity);
return !some;
});
const delegatedIdentities = new Set(data.delegations.map((d) => d.node_identity));
const pendingOnNewNodes = pending.filter((event) => !delegatedIdentities.has(event.node_identity));
const items = data.delegations.map((delegation) => ({
...delegation,
amount: decCoinToDisplay(delegation.amount),
@@ -30,14 +28,17 @@ export async function fetchDelegationSummaryQuery(): Promise<DelegationSummaryBu
const td = parseFloat(data.total_delegations.amount);
const tr = parseFloat(data.total_rewards.amount);
const delegationsAndRewards = Number.isFinite(td) && Number.isFinite(tr) ? (td + tr).toFixed(6) : '0';
const combinedAmount = Number.isFinite(td) && Number.isFinite(tr) ? (td + tr).toFixed(6) : '0';
return {
delegations: [...items, ...pendingOnNewNodes],
pendingDelegations: pending,
totalDelegations: `${data.total_delegations.amount} ${data.total_delegations.denom}`,
totalRewards: `${data.total_rewards.amount} ${data.total_rewards.denom}`,
totalDelegationsAndRewards: `${delegationsAndRewards} ${data.total_delegations.denom}`,
totalDelegations: data.total_delegations,
totalRewards: data.total_rewards,
totalDelegationsAndRewards: {
amount: combinedAmount,
denom: data.total_delegations.denom,
},
};
}
@@ -4,4 +4,8 @@ describe('delegationQueryKeys', () => {
it('builds a stable summary key per client address', () => {
expect(delegationQueryKeys.summary('nyc1test')).toStrictEqual(['delegation', 'summary', 'nyc1test']);
});
it('uses a stable disabled summary key without empty address', () => {
expect(delegationQueryKeys.summaryDisabled).toStrictEqual(['delegation', 'summary', '__disabled__']);
});
});
@@ -1,4 +1,8 @@
const delegationRoot = ['delegation'] as const;
export const delegationQueryKeys = {
all: ['delegation'] as const,
summary: (clientAddress: string) => [...delegationQueryKeys.all, 'summary', clientAddress] as const,
all: delegationRoot,
/** Used when no client address so React Query never caches `summary('')`. */
summaryDisabled: [...delegationRoot, 'summary', '__disabled__'] as const,
summary: (clientAddress: string) => [...delegationRoot, 'summary', clientAddress] as const,
};
+10 -4
View File
@@ -53,6 +53,10 @@ export const isPendingDelegation = (delegation: DelegationWithEvent): delegation
export const isDelegation = (delegation: DelegationWithEvent): delegation is DelegationWithEverything =>
'owner' in delegation;
function formatDelegationSummaryCoin(coin: DecCoin): string {
return `${coin.amount} ${coin.denom}`;
}
export const DelegationContext = createContext<TDelegationContext>({
isLoading: false,
isFetching: false,
@@ -89,7 +93,7 @@ export const DelegationContextProvider: FC<{
const [delegationItemErrors, setDelegationItemErrors] = React.useState<{ nodeId: string; errors: string }>();
const query = useQuery({
queryKey: delegationQueryKeys.summary(clientAddress ?? ''),
queryKey: clientAddress ? delegationQueryKeys.summary(clientAddress) : delegationQueryKeys.summaryDisabled,
queryFn: fetchDelegationSummaryQuery,
enabled: Boolean(clientAddress) && onDelegationRoute,
staleTime: 5 * 60 * 1000,
@@ -132,9 +136,11 @@ export const DelegationContextProvider: FC<{
const delegations = bundle?.delegations;
const pendingDelegations = bundle?.pendingDelegations;
const totalDelegations = bundle?.totalDelegations;
const totalRewards = bundle?.totalRewards;
const totalDelegationsAndRewards = bundle?.totalDelegationsAndRewards;
const totalDelegations = bundle ? formatDelegationSummaryCoin(bundle.totalDelegations) : undefined;
const totalRewards = bundle ? formatDelegationSummaryCoin(bundle.totalRewards) : undefined;
const totalDelegationsAndRewards = bundle
? formatDelegationSummaryCoin(bundle.totalDelegationsAndRewards)
: undefined;
const isLoading = Boolean(clientAddress) && onDelegationRoute && query.isPending;
const isFetching = Boolean(clientAddress) && onDelegationRoute && query.isFetching;
+41 -60
View File
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { DelegationWithEverything, TransactionExecuteResult } from '@nymproject/types';
import { RewardsContext, TRewardsTransaction } from '../rewards';
import { RewardsContext } from '../rewards';
import { useDelegationContext } from '../delegations';
import { mockSleep } from './utils';
@@ -45,69 +45,51 @@ export const MockRewardsContextProvider: FCWithChildren = ({ children }) => {
refresh();
}, []);
const claimRewards = async (mixId: number): Promise<TransactionExecuteResult[]> => {
if (!delegations) {
throw new Error('No delegations');
}
const claimRewards = useCallback(
async (mixId: number): Promise<TransactionExecuteResult[]> => {
if (!delegations) {
throw new Error('No delegations');
}
const d = delegations.find((d1) => (d1 as DelegationWithEverything).mix_id === mixId);
const d = delegations.find((d1) => (d1 as DelegationWithEverything).mix_id === mixId);
if (!d) {
throw new Error(`Unable to find delegation for id = ${mixId}`);
}
if (!d) {
throw new Error(`Unable to find delegation for id = ${mixId}`);
}
await mockSleep(1000);
await mockSleep(1000);
return [
{
transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
fee: {
amount: '1',
denom: 'nym',
return [
{
transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
fee: {
amount: '1',
denom: 'nym',
},
logs_json: '[]',
msg_responses_json: '[]',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
},
},
logs_json: '[]',
msg_responses_json: '[]',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
{
transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
fee: {
amount: '1',
denom: 'nym',
},
msg_responses_json: '[]',
logs_json: '[]',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
},
},
},
{
transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
fee: {
amount: '1',
denom: 'nym',
},
msg_responses_json: '[]',
logs_json: '[]',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
},
},
];
};
const redeemAllRewards = async (): Promise<TRewardsTransaction[]> => {
if (!delegations) {
throw new Error('No delegations');
}
await mockSleep(1000);
return [
{
transactionUrl:
'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
transactionHash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
},
{
transactionUrl:
'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
transactionHash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
},
];
};
];
},
[delegations],
);
const memoizedValue = useMemo(
() => ({
@@ -116,9 +98,8 @@ export const MockRewardsContextProvider: FCWithChildren = ({ children }) => {
totalRewards,
refresh,
claimRewards,
redeemAllRewards,
}),
[isLoading, error, totalRewards],
[isLoading, error, totalRewards, refresh, claimRewards],
);
return <RewardsContext.Provider value={memoizedValue}>{children}</RewardsContext.Provider>;
-12
View File
@@ -3,18 +3,12 @@ import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { useDelegationContext } from './delegations';
import { claimDelegatorRewards } from '../requests';
export type TRewardsTransaction = {
transactionUrl: string;
transactionHash: string;
};
type TRewardsContext = {
isLoading: boolean;
error?: string;
totalRewards?: string;
refresh: () => Promise<void>;
claimRewards: (mixId: number, fee?: FeeDetails) => Promise<TransactionExecuteResult[]>;
redeemAllRewards: () => Promise<TRewardsTransaction[]>;
};
export const RewardsContext = createContext<TRewardsContext>({
@@ -23,9 +17,6 @@ export const RewardsContext = createContext<TRewardsContext>({
claimRewards: async () => {
throw new Error('Not implemented');
},
redeemAllRewards: async () => {
throw new Error('Not implemented');
},
});
export const RewardsContextProvider: FCWithChildren = ({ children }) => {
@@ -47,9 +38,6 @@ export const RewardsContextProvider: FCWithChildren = ({ children }) => {
totalRewards,
refresh,
claimRewards: claimDelegatorRewards,
redeemAllRewards: async () => {
throw new Error('Not implemented');
},
}),
[isLoading, error, totalRewards, refresh],
);