From ec1a976349e89652b7160a7dc0bc78f2c370e767 Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Tue, 16 Dec 2025 13:40:55 +0100 Subject: [PATCH 1/5] Enhance wallet session handling and authorization checks - Updated the RootLayout component to include a delay before refetching the wallet session after authorization, ensuring cookies are set properly. - Improved the wallet queries in the PageWallets and useUserWallets hooks to only execute when the user is authorized, preventing 403 errors. - Added additional error handling for wallet address mismatches in the wallet router, enhancing security and user experience. - Implemented loading states and delays for smoother transitions during data fetching. --- .../common/overall-layout/layout.tsx | 26 +++++++++++++---- .../pages/homepage/wallets/index.tsx | 28 ++++++++++++++++--- src/hooks/useUserWallets.ts | 14 +++++++++- src/server/api/routers/wallets.ts | 11 ++++++++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/components/common/overall-layout/layout.tsx b/src/components/common/overall-layout/layout.tsx index 34ba872..dbbeda9 100644 --- a/src/components/common/overall-layout/layout.tsx +++ b/src/components/common/overall-layout/layout.tsx @@ -371,19 +371,35 @@ export default function RootLayout({ // Don't refetch here - let the natural query refetch handle it if needed }, []); - const handleAuthModalAuthorized = useCallback(() => { + const handleAuthModalAuthorized = useCallback(async () => { setShowAuthModal(false); setCheckingSession(false); setHasCheckedSession(true); // Mark as checked so we don't check again // Show loading skeleton for smooth transition setShowPostAuthLoading(true); - // Refetch session after authorization to update state (but don't show modal again) - void refetchWalletSession(); + + // Wait a moment for the cookie to be set by the browser, then refetch session + await new Promise(resolve => setTimeout(resolve, 200)); + + // Refetch session to update state + await refetchWalletSession(); + + // Invalidate wallet queries so they refetch with the new session + // Use a small delay to ensure cookie is available on subsequent requests + setTimeout(() => { + const userAddressForInvalidation = userAddress || address; + if (userAddressForInvalidation) { + void ctx.wallet.getUserWallets.invalidate({ address: userAddressForInvalidation }); + void ctx.wallet.getUserNewWallets.invalidate({ address: userAddressForInvalidation }); + void ctx.wallet.getUserNewWalletsNotOwner.invalidate({ address: userAddressForInvalidation }); + } + }, 300); + // Hide loading after a brief delay to allow data to load setTimeout(() => { setShowPostAuthLoading(false); - }, 1000); - }, [refetchWalletSession]); + }, 1500); + }, [refetchWalletSession, ctx.wallet, userAddress, address]); // Memoize computed route values const isWalletPath = useMemo(() => router.pathname.includes("/wallets/[wallet]"), [router.pathname]); diff --git a/src/components/pages/homepage/wallets/index.tsx b/src/components/pages/homepage/wallets/index.tsx index 645af3f..d696498 100644 --- a/src/components/pages/homepage/wallets/index.tsx +++ b/src/components/pages/homepage/wallets/index.tsx @@ -29,10 +29,21 @@ export default function PageWallets() { const [showArchived, setShowArchived] = useState(false); const userAddress = useUserStore((state) => state.userAddress); + // Check wallet session authorization before enabling queries + const { data: walletSession } = api.auth.getWalletSession.useQuery( + { address: userAddress ?? "" }, + { + enabled: !!userAddress && userAddress.length > 0, + refetchOnWindowFocus: false, + }, + ); + const isAuthorized = walletSession?.authorized ?? false; + const { data: newPendingWallets, isLoading: isLoadingNewWallets } = api.wallet.getUserNewWallets.useQuery( { address: userAddress! }, { - enabled: userAddress !== undefined, + // Only enable query when user is authorized (prevents 403 errors) + enabled: userAddress !== undefined && isAuthorized, retry: (failureCount, error) => { // Don't retry on authorization errors (403) if (error && typeof error === "object") { @@ -60,15 +71,24 @@ export default function PageWallets() { api.wallet.getUserNewWalletsNotOwner.useQuery( { address: userAddress! }, { - enabled: userAddress !== undefined, + // Only enable query when user is authorized (prevents 403 errors) + enabled: userAddress !== undefined && isAuthorized, retry: (failureCount, error) => { // Don't retry on authorization errors (403) if (error && typeof error === "object") { - const err = error as { code?: string; message?: string; data?: { code?: string } }; + const err = error as { + code?: string; + message?: string; + data?: { code?: string; httpStatus?: number }; + shape?: { code?: string; message?: string }; + }; + const errorMessage = err.message || err.shape?.message || ""; const isAuthError = err.code === "FORBIDDEN" || err.data?.code === "FORBIDDEN" || - err.message?.includes("Address mismatch"); + err.data?.httpStatus === 403 || + err.shape?.code === "FORBIDDEN" || + errorMessage.includes("Address mismatch"); if (isAuthError) return false; } return failureCount < 1; diff --git a/src/hooks/useUserWallets.ts b/src/hooks/useUserWallets.ts index c0e3001..8a7dfed 100644 --- a/src/hooks/useUserWallets.ts +++ b/src/hooks/useUserWallets.ts @@ -7,10 +7,22 @@ import { DbWalletWithLegacy } from "@/types/wallet"; export default function useUserWallets() { const network = useSiteStore((state) => state.network); const userAddress = useUserStore((state) => state.userAddress); + + // Check wallet session authorization before enabling queries + const { data: walletSession } = api.auth.getWalletSession.useQuery( + { address: userAddress ?? "" }, + { + enabled: !!userAddress && userAddress.length > 0, + refetchOnWindowFocus: false, + }, + ); + const isAuthorized = walletSession?.authorized ?? false; + const { data: wallets, isLoading } = api.wallet.getUserWallets.useQuery( { address: userAddress! }, { - enabled: userAddress !== undefined, + // Only enable query when user is authorized (prevents 403 errors) + enabled: userAddress !== undefined && isAuthorized, staleTime: 1 * 60 * 1000, // 1 minute (user/wallet data) gcTime: 5 * 60 * 1000, // 5 minutes retry: (failureCount, error) => { diff --git a/src/server/api/routers/wallets.ts b/src/server/api/routers/wallets.ts index 1eec027..e548f22 100644 --- a/src/server/api/routers/wallets.ts +++ b/src/server/api/routers/wallets.ts @@ -98,9 +98,12 @@ export const walletRouter = createTRPCRouter({ : ctx.sessionAddress ? [ctx.sessionAddress] : []; + // If user has an active session, validate that the requested address is authorized + // Throw error if address doesn't match (security: prevent unauthorized access) if (addresses.length > 0 && !addresses.includes(input.address)) { throw new TRPCError({ code: "FORBIDDEN", message: "Address mismatch" }); } + // Query wallets where the user is a signer return ctx.db.wallet.findMany({ where: { signersAddresses: { @@ -119,6 +122,8 @@ export const walletRouter = createTRPCRouter({ : ctx.sessionAddress ? [ctx.sessionAddress] : []; + // If user has an active session, validate that the requested address is authorized + // Throw error if address doesn't match (security: prevent unauthorized access) if (addresses.length > 0 && !addresses.includes(input.address)) { throw new TRPCError({ code: "FORBIDDEN", message: "Address mismatch" }); } @@ -268,9 +273,12 @@ export const walletRouter = createTRPCRouter({ : ctx.sessionAddress ? [ctx.sessionAddress] : []; + // If user has an active session, validate that the requested address is authorized + // Throw error if address doesn't match (security: prevent unauthorized access) if (addresses.length > 0 && !addresses.includes(input.address)) { throw new TRPCError({ code: "FORBIDDEN", message: "Address mismatch" }); } + // Query new wallets owned by the user return ctx.db.newWallet.findMany({ where: { ownerAddress: input.address, @@ -287,9 +295,12 @@ export const walletRouter = createTRPCRouter({ : ctx.sessionAddress ? [ctx.sessionAddress] : []; + // If user has an active session, validate that the requested address is authorized + // Throw error if address doesn't match (security: prevent unauthorized access) if (addresses.length > 0 && !addresses.includes(input.address)) { throw new TRPCError({ code: "FORBIDDEN", message: "Address mismatch" }); } + // Query new wallets where user is a signer but not the owner return ctx.db.newWallet.findMany({ where: { signersAddresses: { From 7dcd832e729417af766dc1f6a32e02607ec89799 Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Tue, 16 Dec 2025 17:50:36 +0100 Subject: [PATCH 2/5] Refactor wallet components and enhance DRep ID handling - Updated CardUI to conditionally render larger titles for wallet info cards based on class names. - Improved DRep ID retrieval logic across multiple components to ensure consistent fallback to appWallet for legacy wallets. - Added wallet type detection to enhance user experience with badge displays for Summon and Legacy wallets. - Refactored transaction components to utilize truncateTokenSymbol for better token symbol presentation. - Enhanced layout and styling for transaction cards and balance displays for improved user interaction. --- src/components/common/card-content.tsx | 7 +- .../wallet-data-loader-wrapper.tsx | 5 +- .../multisig/proxy/ProxyOverview.tsx | 3 +- src/components/multisig/proxy/ProxySpend.tsx | 3 +- .../pages/homepage/wallets/index.tsx | 20 +- .../wallets/new-wallet-flow/ready/index.tsx | 2 +- .../pages/wallet/governance/ballot/ballot.tsx | 4 +- .../pages/wallet/governance/card-info.tsx | 4 +- .../governance/proposal/voteButtton.tsx | 4 +- .../pages/wallet/governance/proposals.tsx | 4 +- .../pages/wallet/info/card-info.tsx | 82 ++++--- .../pages/wallet/new-transaction/index.tsx | 59 +++-- .../wallet/new-transaction/utxoSelector.tsx | 11 +- .../wallet/transactions/all-transactions.tsx | 140 +++++------- .../wallet/transactions/card-balance.tsx | 9 +- .../pages/wallet/transactions/index.tsx | 8 +- .../responsive-transactions-table.tsx | 182 ++------------- .../wallet/transactions/transaction-card.tsx | 206 ++++++++++++----- src/hooks/common.ts | 210 ------------------ src/hooks/useAppWallet.ts | 2 +- src/hooks/useMultisigWallet.ts | 2 +- src/hooks/useUserWallets.ts | 2 +- src/utils/common.ts | 193 ++++++++++++++-- src/utils/multisigSDK.ts | 16 +- src/utils/strings.ts | 12 + 25 files changed, 554 insertions(+), 636 deletions(-) delete mode 100644 src/hooks/common.ts diff --git a/src/components/common/card-content.tsx b/src/components/common/card-content.tsx index f2070be..574d0ae 100644 --- a/src/components/common/card-content.tsx +++ b/src/components/common/card-content.tsx @@ -16,10 +16,13 @@ export default function CardUI({ cardClassName?: string; headerDom?: ReactNode; }) { + // Make title larger for wallet info card (col-span-2) + const isLargeTitle = cardClassName?.includes('col-span-2'); + return ( - {title} + {title} {headerDom && headerDom} {icon && ( <> @@ -33,7 +36,7 @@ export default function CardUI({ )} - +
{description && (

{description}

diff --git a/src/components/common/overall-layout/mobile-wrappers/wallet-data-loader-wrapper.tsx b/src/components/common/overall-layout/mobile-wrappers/wallet-data-loader-wrapper.tsx index 18d7e2e..4ae3e8d 100644 --- a/src/components/common/overall-layout/mobile-wrappers/wallet-data-loader-wrapper.tsx +++ b/src/components/common/overall-layout/mobile-wrappers/wallet-data-loader-wrapper.tsx @@ -160,8 +160,9 @@ export default function WalletDataLoaderWrapper({ } function dRepIds() { - // Use multisig wallet DRep ID if available, otherwise fallback to appWallet - const dRepId = multisigWallet?.getKeysByRole(3) ? multisigWallet?.getDRepId() : appWallet?.dRepId; + // Use multisig wallet DRep ID if available (it handles no DRep keys by using payment script), + // otherwise fallback to appWallet (for legacy wallets without multisigWallet) + const dRepId = multisigWallet ? multisigWallet.getDRepId() : appWallet?.dRepId; if (!dRepId) return null; return getDRepIds(dRepId); } diff --git a/src/components/multisig/proxy/ProxyOverview.tsx b/src/components/multisig/proxy/ProxyOverview.tsx index 5041211..28e286d 100644 --- a/src/components/multisig/proxy/ProxyOverview.tsx +++ b/src/components/multisig/proxy/ProxyOverview.tsx @@ -1,4 +1,5 @@ import React, { memo, useState, useEffect } from "react"; +import { truncateTokenSymbol } from "@/utils/strings"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -270,7 +271,7 @@ const ProxyCard = memo(function ProxyCard({
{displayBalance.map((asset: any, index: number) => (
- {asset.unit === "lovelace" ? "ADA" : asset.unit}: + {asset.unit === "lovelace" ? "ADA" : truncateTokenSymbol(asset.unit)}: {asset.unit === "lovelace" ? `${(parseFloat(asset.quantity) / 1000000).toFixed(6)} ADA` diff --git a/src/components/multisig/proxy/ProxySpend.tsx b/src/components/multisig/proxy/ProxySpend.tsx index 4ecd8af..93340f3 100644 --- a/src/components/multisig/proxy/ProxySpend.tsx +++ b/src/components/multisig/proxy/ProxySpend.tsx @@ -1,4 +1,5 @@ import React, { memo } from "react"; +import { truncateTokenSymbol } from "@/utils/strings"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -139,7 +140,7 @@ const ProxySpend = memo(function ProxySpend({
- {asset.unit === "lovelace" ? "ADA" : asset.unit} + {asset.unit === "lovelace" ? "ADA" : truncateTokenSymbol(asset.unit)} {asset.unit === "lovelace" diff --git a/src/components/pages/homepage/wallets/index.tsx b/src/components/pages/homepage/wallets/index.tsx index d696498..523c10b 100644 --- a/src/components/pages/homepage/wallets/index.tsx +++ b/src/components/pages/homepage/wallets/index.tsx @@ -9,10 +9,12 @@ import { getFirstAndLast } from "@/utils/strings"; import { api } from "@/utils/api"; import { useUserStore } from "@/lib/zustand/user"; import { useSiteStore } from "@/lib/zustand/site"; -import { buildMultisigWallet } from "@/utils/common"; +import { buildMultisigWallet, getWalletType } from "@/utils/common"; import { addressToNetwork } from "@/utils/multisigSDK"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Archive } from "lucide-react"; import PageHeader from "@/components/common/page-header"; import CardUI from "@/components/common/card-content"; import RowLabelInfo from "@/components/common/row-label-info"; @@ -236,6 +238,11 @@ function CardWallet({ walletId: wallet.id, }); + // Check wallet type for badge display using centralized detection + const walletType = getWalletType(wallet); + const isSummonWallet = walletType === 'summon'; + const isLegacyWallet = walletType === 'legacy'; + // Rebuild the multisig wallet to get the correct canonical address for display // This ensures we show the correct address even if wallet.address was built incorrectly const displayAddress = useMemo(() => { @@ -260,6 +267,17 @@ function CardWallet({ title={`${wallet.name}${wallet.isArchived ? " (Archived)" : ""}`} description={wallet.description} cardClassName="" + headerDom={ + isSummonWallet ? ( + + + Summon + + ) : undefined + } > (null); // Get DRep info for standard mode - const currentDrepId = multisigWallet?.getKeysByRole(3) ? multisigWallet?.getDRepId() : appWallet?.dRepId; + // Use multisig wallet DRep ID if available (it handles no DRep keys by using payment script), + // otherwise fallback to appWallet (for legacy wallets without multisigWallet) + const currentDrepId = multisigWallet ? multisigWallet.getDRepId() : appWallet?.dRepId; const currentDrepInfo = drepInfo; diff --git a/src/components/pages/wallet/governance/proposal/voteButtton.tsx b/src/components/pages/wallet/governance/proposal/voteButtton.tsx index e1c9fa5..395a50a 100644 --- a/src/components/pages/wallet/governance/proposal/voteButtton.tsx +++ b/src/components/pages/wallet/governance/proposal/voteButtton.tsx @@ -231,7 +231,9 @@ export default function VoteButton({ } if (!multisigWallet) throw new Error("Multisig Wallet could not be built."); - const dRepId = multisigWallet?.getKeysByRole(3) ? multisigWallet?.getDRepId() : appWallet?.dRepId; + // Use multisig wallet DRep ID if available (it handles no DRep keys by using payment script), + // otherwise fallback to appWallet (for legacy wallets without multisigWallet) + const dRepId = multisigWallet ? multisigWallet.getDRepId() : appWallet?.dRepId; if (!dRepId) { setAlert("DRep not found"); toast({ diff --git a/src/components/pages/wallet/governance/proposals.tsx b/src/components/pages/wallet/governance/proposals.tsx index 4e57e00..a6e9c15 100644 --- a/src/components/pages/wallet/governance/proposals.tsx +++ b/src/components/pages/wallet/governance/proposals.tsx @@ -83,7 +83,9 @@ export default function AllProposals({ appWallet, utxos, selectedBallotId, onSel const order = "desc"; // Get DRep ID for fetching voting history (proxy mode or standard mode) - const standardDrepId = multisigWallet?.getKeysByRole(3) ? multisigWallet?.getDRepId() : appWallet?.dRepId; + // Use multisig wallet DRep ID if available (it handles no DRep keys by using payment script), + // otherwise fallback to appWallet (for legacy wallets without multisigWallet) + const standardDrepId = multisigWallet ? multisigWallet.getDRepId() : appWallet?.dRepId; // Get proxy DRep ID if proxy is enabled useEffect(() => { diff --git a/src/components/pages/wallet/info/card-info.tsx b/src/components/pages/wallet/info/card-info.tsx index 8bbe3ae..88a9464 100644 --- a/src/components/pages/wallet/info/card-info.tsx +++ b/src/components/pages/wallet/info/card-info.tsx @@ -39,28 +39,44 @@ import Code from "@/components/ui/code"; import { Carousel } from "@/components/ui/carousel"; import { type MultisigWallet } from "@/utils/multisigSDK"; import { getFirstAndLast } from "@/utils/strings"; +import { getWalletType } from "@/utils/common"; export default function CardInfo({ appWallet }: { appWallet: Wallet }) { const [showEdit, setShowEdit] = useState(false); + + // Check if this is a legacy wallet using the centralized detection + const walletType = getWalletType(appWallet); + const isLegacyWallet = walletType === 'legacy'; return ( - - - - - setShowEdit(!showEdit)}> - {showEdit ? "Close Edit" : "Edit Wallet"} - - - +
+ {isLegacyWallet && ( + + + Legacy + + )} + + + + + + setShowEdit(!showEdit)}> + {showEdit ? "Close Edit" : "Edit Wallet"} + + + +
} cardClassName="col-span-2" > @@ -338,15 +354,17 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) { setBalance(balance); }, [appWallet, walletsUtxos]); - // Check if this is a legacy wallet (doesn't use SDK) - const isLegacyWallet = !!appWallet.rawImportBodies?.multisig; + // Check if this is a legacy wallet using the centralized detection + const walletType = getWalletType(appWallet); + const isLegacyWallet = walletType === 'legacy'; // For legacy wallets, multisigWallet will be undefined, so use appWallet.address // For SDK wallets, prefer the address from multisigWallet if staking is enabled const address = multisigWallet?.getKeysByRole(2) ? multisigWallet?.getScript().address : appWallet.address; - // Get DRep ID from multisig wallet if available, otherwise fallback to appWallet - const dRepId = multisigWallet?.getKeysByRole(3) ? multisigWallet?.getDRepId() : appWallet?.dRepId; + // Get DRep ID from multisig wallet if available (it handles no DRep keys by using payment script), + // otherwise fallback to appWallet (for legacy wallets without multisigWallet) + const dRepId = multisigWallet ? multisigWallet.getDRepId() : appWallet?.dRepId; // For rawImportBodies wallets, dRepId may not be available const showDRepId = dRepId && dRepId.length > 0; @@ -379,22 +397,6 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) { return (
- {/* Wallet Name */} - {appWallet.name && ( -
-
-
Wallet Name
- {isLegacyWallet && ( - - - Legacy - - )} -
-
{appWallet.name}
-
- )} - {/* Desktop: Grid layout for addresses and balance */}
{/* Left Column */} @@ -407,7 +409,7 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) { allowOverflow={false} /> - {/* Stake Address - Only show if staking is enabled */} + {/* Stake Address - Show if staking is enabled */} {multisigWallet && multisigWallet.stakingEnabled() && (() => { const stakeAddress = multisigWallet.getStakeAddress(); return stakeAddress ? ( @@ -420,6 +422,16 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) { ) : null; })()} + {/* External Stake Key Hash - Always show if available */} + {appWallet?.stakeCredentialHash && ( + + )} + {/* DRep ID */} {showDRepId && dRepId ? (
+ +
+