diff --git a/components/bounty-detail/bounty-detail-client.tsx b/components/bounty-detail/bounty-detail-client.tsx
index 3e93b7fa..cda5bb15 100644
--- a/components/bounty-detail/bounty-detail-client.tsx
+++ b/components/bounty-detail/bounty-detail-client.tsx
@@ -18,6 +18,7 @@ import { RefundStatusTracker } from "../bounty/refund-status";
import { FeeCalculator } from "../bounty/fee-calculator";
import { useEscrowPool } from "@/hooks/use-escrow";
import { authClient } from "@/lib/auth-client";
+import { useWalletAddress } from "@/hooks/use-wallet-address";
import { useDeadlinePassed } from "@/hooks/use-deadline-passed";
import type { CancellationRecord } from "@/types/escrow";
import { MilestoneFunnel } from "@/components/bounty/milestone-funnel";
@@ -87,6 +88,8 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
}, []);
const pastDeadline = useDeadlinePassed(bounty?.bountyWindow?.endDate);
+ // walletAddress is required for contract actions. Do NOT fallback to user.id.
+ const walletAddress = useWalletAddress() ?? "";
if (isPending) return ;
@@ -145,9 +148,6 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
const isCreator =
(session?.user as { id?: string } | undefined)?.id === bounty.createdBy;
const isFinalized = bounty.status === "COMPLETED";
- // walletAddress is required for contract actions. Do NOT fallback to user.id.
- const walletAddress =
- (session?.user as { walletAddress?: string })?.walletAddress || "";
// Identify if the current user is the assigned contributor
// using a fallback check on submissions or assumed backend field.
diff --git a/components/bounty/competition-judging.tsx b/components/bounty/competition-judging.tsx
index 36e45bbb..30e9a8ba 100644
--- a/components/bounty/competition-judging.tsx
+++ b/components/bounty/competition-judging.tsx
@@ -3,7 +3,7 @@
import { useState } from "react";
import { Loader2, Trophy, Award, CheckCircle2, Lock } from "lucide-react";
import { toast } from "sonner";
-import { authClient } from "@/lib/auth-client";
+import { useWalletAddress } from "@/hooks/use-wallet-address";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -45,7 +45,7 @@ export function CompetitionJudging({
totalReward,
currency,
}: CompetitionJudgingProps) {
- const { data: session } = authClient.useSession();
+ const walletAddress = useWalletAddress();
const approveMutation = useApproveContestWinner();
const finalizeMutation = useFinalizeContest();
@@ -55,13 +55,6 @@ export function CompetitionJudging({
// On next query invalidation the backend status takes over.
const [localApproved, setLocalApproved] = useState>(new Set());
- const walletAddress =
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.walletAddress ||
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.address ||
- null;
-
const handleApprove = async (sub: Submission) => {
if (!walletAddress) {
toast.error("Connect your wallet to approve.");
diff --git a/components/bounty/competition-submission.tsx b/components/bounty/competition-submission.tsx
index 669c07b7..ef59f712 100644
--- a/components/bounty/competition-submission.tsx
+++ b/components/bounty/competition-submission.tsx
@@ -3,7 +3,7 @@
import { useState, useEffect } from "react";
import { Loader2, Lock, Send, Clock } from "lucide-react";
import { toast } from "sonner";
-import { authClient } from "@/lib/auth-client";
+import { useWalletAddress } from "@/hooks/use-wallet-address";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
@@ -40,7 +40,7 @@ export function CompetitionSubmission({
deadline,
hasJoined,
}: CompetitionSubmissionProps) {
- const { data: session } = authClient.useSession();
+ const walletAddress = useWalletAddress();
const [workCid, setWorkCid] = useState("");
const submitMutation = useSubmitContestWork();
const isPastDeadline = useDeadlinePassed(deadline);
@@ -55,13 +55,6 @@ export function CompetitionSubmission({
return () => clearInterval(id);
}, [deadline]);
- const walletAddress =
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.walletAddress ||
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.address ||
- null;
-
if (!hasJoined) return null;
const trimmed = workCid.trim();
diff --git a/components/bounty/fcfs-approval-panel.tsx b/components/bounty/fcfs-approval-panel.tsx
index df066bc8..9574bcbe 100644
--- a/components/bounty/fcfs-approval-panel.tsx
+++ b/components/bounty/fcfs-approval-panel.tsx
@@ -5,6 +5,7 @@ import Link from "next/link";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
+import { useWalletAddress } from "@/hooks/use-wallet-address";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -29,12 +30,7 @@ export function FcfsApprovalPanel({ bounty }: { bounty: FcfsApprovalBounty }) {
const [points, setPoints] = useState(10);
const currentUserId = (session?.user as { id?: string } | undefined)?.id;
- const walletAddress =
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.walletAddress ||
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.address ||
- null;
+ const walletAddress = useWalletAddress();
const isCreator = Boolean(
currentUserId && currentUserId === bounty.createdBy,
diff --git a/components/bounty/fcfs-claim-button.tsx b/components/bounty/fcfs-claim-button.tsx
index ba7e2232..7578e680 100644
--- a/components/bounty/fcfs-claim-button.tsx
+++ b/components/bounty/fcfs-claim-button.tsx
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import { AlertTriangle, Clock3, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
+import { useWalletAddress } from "@/hooks/use-wallet-address";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -73,12 +74,7 @@ export function FcfsClaimButton({ bounty }: { bounty: FcfsBounty }) {
}, []);
const currentUserId = (session?.user as { id?: string } | undefined)?.id;
- const walletAddress =
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.walletAddress ||
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.address ||
- null;
+ const walletAddress = useWalletAddress();
const isFcfs = bounty.type === "FIXED_PRICE";
const isOpen = bounty.status === "OPEN";
diff --git a/hooks/__tests__/use-wallet-address.test.ts b/hooks/__tests__/use-wallet-address.test.ts
new file mode 100644
index 00000000..3124d61a
--- /dev/null
+++ b/hooks/__tests__/use-wallet-address.test.ts
@@ -0,0 +1,49 @@
+import { renderHook } from "@testing-library/react";
+import { useWalletAddress } from "../use-wallet-address";
+import { authClient } from "@/lib/auth-client";
+
+jest.mock("@/lib/auth-client", () => ({
+ authClient: {
+ useSession: jest.fn(),
+ },
+}));
+
+describe("useWalletAddress", () => {
+ it("returns null when there is no session", () => {
+ (authClient.useSession as jest.Mock).mockReturnValue({ data: null });
+ const { result } = renderHook(() => useWalletAddress());
+ expect(result.current).toBeNull();
+ });
+
+ it("returns null when session has no user", () => {
+ (authClient.useSession as jest.Mock).mockReturnValue({
+ data: { user: undefined },
+ });
+ const { result } = renderHook(() => useWalletAddress());
+ expect(result.current).toBeNull();
+ });
+
+ it("returns walletAddress when available", () => {
+ (authClient.useSession as jest.Mock).mockReturnValue({
+ data: { user: { walletAddress: "0xABC", address: "0xDEF" } },
+ });
+ const { result } = renderHook(() => useWalletAddress());
+ expect(result.current).toBe("0xABC");
+ });
+
+ it("falls back to address when walletAddress is missing", () => {
+ (authClient.useSession as jest.Mock).mockReturnValue({
+ data: { user: { address: "0xDEF" } },
+ });
+ const { result } = renderHook(() => useWalletAddress());
+ expect(result.current).toBe("0xDEF");
+ });
+
+ it("returns null when neither walletAddress nor address exists", () => {
+ (authClient.useSession as jest.Mock).mockReturnValue({
+ data: { user: { name: "test" } },
+ });
+ const { result } = renderHook(() => useWalletAddress());
+ expect(result.current).toBeNull();
+ });
+});
diff --git a/hooks/use-competition-join-state.ts b/hooks/use-competition-join-state.ts
index f3c5ee21..07787ed9 100644
--- a/hooks/use-competition-join-state.ts
+++ b/hooks/use-competition-join-state.ts
@@ -2,7 +2,7 @@
import { useState } from "react";
import { toast } from "sonner";
-import { authClient } from "@/lib/auth-client";
+import { useWalletAddress } from "@/hooks/use-wallet-address";
import {
useJoinCompetition,
ContestError,
@@ -22,17 +22,10 @@ interface CompetitionJoinState {
export function useCompetitionJoinState(
bounty: BountyFieldsFragment & Partial,
): CompetitionJoinState {
- const { data: session } = authClient.useSession();
+ const walletAddress = useWalletAddress();
const joinMutation = useJoinCompetition();
const [localJoined, setLocalJoined] = useState(false);
- const walletAddress =
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.walletAddress ||
- (session?.user as { walletAddress?: string; address?: string } | undefined)
- ?.address ||
- null;
-
const deadline = bounty.bountyWindow?.endDate ?? null;
const isPastDeadline = useDeadlinePassed(deadline);
diff --git a/hooks/use-wallet-address.ts b/hooks/use-wallet-address.ts
new file mode 100644
index 00000000..676b21b4
--- /dev/null
+++ b/hooks/use-wallet-address.ts
@@ -0,0 +1,15 @@
+"use client";
+
+import { authClient } from "@/lib/auth-client";
+
+/**
+ * Returns the connected wallet address from the current session, or null
+ * if the user is not signed in or has no wallet attached.
+ */
+export function useWalletAddress(): string | null {
+ const { data: session } = authClient.useSession();
+ const user = session?.user as
+ | { walletAddress?: string; address?: string }
+ | undefined;
+ return user?.walletAddress || user?.address || null;
+}