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
91 changes: 91 additions & 0 deletions src/hooks/__tests__/useStakingDashboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import {
useStakeKeysMutation,
useUnstakeKeysMutation,
useClaimStakingRewardsMutation,
} from '../useStakingDashboard';
import showToast from '@/utils/toast.util';

vi.mock('@/utils/toast.util', () => ({
default: {
success: vi.fn(),
error: vi.fn(),
message: vi.fn(),
},
}));

function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

describe('useStakingDashboard hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('stakes creator keys and triggers success toast', async () => {
const { result } = renderHook(
() => useStakeKeysMutation('GAAA1234567890'),
{ wrapper: createWrapper() }
);

await act(async () => {
await result.current.mutateAsync({
keyId: 'creator-alpha',
quantity: 5,
lockPeriodDays: 90,
});
});

expect(showToast.success).toHaveBeenCalledWith(
'Keys staked successfully'
);
});

it('unstakes creator keys and triggers success toast', async () => {
const { result } = renderHook(
() => useUnstakeKeysMutation('GAAA1234567890'),
{ wrapper: createWrapper() }
);

await act(async () => {
await result.current.mutateAsync({
positionId: 'pos-101',
keyId: 'creator-alpha',
});
});

expect(showToast.success).toHaveBeenCalledWith(
'Keys unstaked successfully'
);
});

it('claims rewards and triggers success toast', async () => {
const { result } = renderHook(
() => useClaimStakingRewardsMutation('GAAA1234567890'),
{ wrapper: createWrapper() }
);

await act(async () => {
await result.current.mutateAsync({
positionId: 'pos-102',
keyId: 'creator-beta',
});
});

expect(showToast.success).toHaveBeenCalledWith(
'Staking rewards claimed'
);
});
});
119 changes: 119 additions & 0 deletions src/hooks/useStakingDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/queryKeys';
import showToast from '@/utils/toast.util';
import { getSignatureErrorMessage } from '@/utils/errorHandling.utils';

/**
* Staking dashboard contract mutations (#917).
*
* Stake, unstake, and claim-reward actions for the staking dashboard page.
* The on-chain wiring is not in the client yet, so each mutation simulates
* signing latency and resolves. On success the staking-positions and wallet
* holdings queries are invalidated so the dashboard reflects new state.
*/

const SIGN_LATENCY_MS = 1200;

async function submitStakingCall(fn: string, args: unknown) {
void fn;
void args;
await new Promise<void>(resolve =>
window.setTimeout(resolve, SIGN_LATENCY_MS)
);
return { success: true as const };
}

// ── Stake keys ──────────────────────────────────────────────────────────

export interface StakeKeysVariables {
/** Creator key to stake. */
keyId: string;
/** Number of keys to stake. */
quantity: number;
/** Lock period in days. */
lockPeriodDays: number;
}

export function useStakeKeysMutation(address: string) {
const queryClient = useQueryClient();

return useMutation({
mutationKey: ['contract', 'stake_keys', address],
mutationFn: (variables: StakeKeysVariables) =>
submitStakingCall('stake_keys', { address, ...variables }),
onError: error => {
showToast.error(getSignatureErrorMessage(error));
},
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: queryKeys.wallet.stakingPositions(address),
});
void queryClient.invalidateQueries({
queryKey: queryKeys.wallet.holdings(address),
});
showToast.success('Keys staked successfully');
},
});
}

// ── Unstake keys ────────────────────────────────────────────────────────

export interface UnstakeKeysVariables {
/** Staking position id being unstaked. */
positionId: string;
/** Creator key id of the staked position. */
keyId: string;
}

export function useUnstakeKeysMutation(address: string) {
const queryClient = useQueryClient();

return useMutation({
mutationKey: ['contract', 'unstake_keys', address],
mutationFn: (variables: UnstakeKeysVariables) =>
submitStakingCall('unstake_keys', { address, ...variables }),
onError: error => {
showToast.error(getSignatureErrorMessage(error));
},
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: queryKeys.wallet.stakingPositions(address),
});
void queryClient.invalidateQueries({
queryKey: queryKeys.wallet.holdings(address),
});
showToast.success('Keys unstaked successfully');
},
});
}

// ── Claim staking rewards ───────────────────────────────────────────────

export interface ClaimStakingRewardsVariables {
/** Staking position id to claim rewards from. */
positionId: string;
/** Creator key id. */
keyId: string;
}

export function useClaimStakingRewardsMutation(address: string) {
const queryClient = useQueryClient();

return useMutation({
mutationKey: ['contract', 'claim_staking_rewards', address],
mutationFn: (variables: ClaimStakingRewardsVariables) =>
submitStakingCall('claim_staking_rewards', {
address,
...variables,
}),
onError: error => {
showToast.error(getSignatureErrorMessage(error));
},
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: queryKeys.wallet.stakingPositions(address),
});
showToast.success('Staking rewards claimed');
},
});
}
5 changes: 5 additions & 0 deletions src/lib/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ export const queryKeys = {
summary: (wallet: string) => ['referrals', wallet, 'summary'] as const,
wallets: (wallet: string) => ['referrals', wallet, 'wallets'] as const,
},
staking: {
all: () => ['staking'] as const,
dashboard: (wallet: string) =>
['staking', wallet, 'dashboard'] as const,
},
atomicSwap: {
all: () => ['atomic-swap'] as const,
proposal: (proposalId: string) =>
Expand Down
Loading
Loading