diff --git a/src/components/AssetBadge.test.tsx b/src/components/AssetBadge.test.tsx index 8d3fee8..ae6e04b 100644 --- a/src/components/AssetBadge.test.tsx +++ b/src/components/AssetBadge.test.tsx @@ -92,10 +92,10 @@ describe("AssetBadge", () => { expect(document.querySelector("[data-address]")).not.toBeInTheDocument(); }); - it("falls back to grey/surface-2 for an unknown asset", () => { + it("renders deterministic color for unknown assets (not grey)", () => { const { container } = render(); const icon = container.querySelector(".bg-surface-2"); - expect(icon).toBeInTheDocument(); + expect(icon).not.toBeInTheDocument(); }); it("renders the asset code for an unknown asset", () => { @@ -103,6 +103,80 @@ describe("AssetBadge", () => { expect(screen.getByText("WAVEX")).toBeInTheDocument(); }); + it("renders yXLM with distinct green color", () => { + const yxlmBalance: Balance = { + assetType: "credit_alphanum12", + assetCode: "yXLM", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "100", + balanceFloat: 100, + }; + const { container } = render(); + const icon = container.querySelector(".text-green"); + expect(icon).toBeInTheDocument(); + }); + + it("renders AQUA with distinct blue color", () => { + const aquaBalance: Balance = { + assetType: "credit_alphanum12", + assetCode: "AQUA", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "50", + balanceFloat: 50, + }; + const { container } = render(); + const icon = container.querySelector(".text-blue"); + expect(icon).toBeInTheDocument(); + }); + + it("renders SHX with distinct pink color", () => { + const shxBalance: Balance = { + assetType: "credit_alphanum12", + assetCode: "SHX", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "25", + balanceFloat: 25, + }; + const { container } = render(); + const icon = container.querySelector(".text-pink"); + expect(icon).toBeInTheDocument(); + }); + + it("renders BLND with distinct yellow color", () => { + const blndBalance: Balance = { + assetType: "credit_alphanum12", + assetCode: "BLND", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "75", + balanceFloat: 75, + }; + const { container } = render(); + const icon = container.querySelector(".text-yellow"); + expect(icon).toBeInTheDocument(); + }); + + it("assigns deterministic colors to unknown assets and not grey", () => { + const customBalance1: Balance = { + assetType: "credit_alphanum12", + assetCode: "CUSTOM1", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "10", + balanceFloat: 10, + }; + const customBalance2: Balance = { + assetType: "credit_alphanum12", + assetCode: "CUSTOM2", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "20", + balanceFloat: 20, + }; + const { container: c1 } = render(); + const { container: c2 } = render(); + + expect(c1.querySelector(".bg-surface-2")).not.toBeInTheDocument(); + expect(c2.querySelector(".bg-surface-2")).not.toBeInTheDocument(); + }); + it("renders 'LP' for liquidity_pool_shares without undefined display", () => { const { container } = render(); expect(screen.getAllByText("LP").length).toBeGreaterThanOrEqual(1); @@ -335,11 +409,11 @@ describe("AssetPill", () => { expect(screen.getByText("USDC")).toHaveClass("text-brand"); }); - it("falls back to grey for an unknown asset code", () => { + it("uses deterministic color for unknown asset code (not grey)", () => { render(); const pill = screen.getByText("WAVEX"); - expect(pill).toHaveClass("bg-surface-2"); - expect(pill).toHaveClass("text-ink-2"); + expect(pill).not.toHaveClass("bg-surface-2"); + expect(pill).not.toHaveClass("text-ink-2"); }); it("merges a custom className", () => { diff --git a/src/components/AssetBadge.tsx b/src/components/AssetBadge.tsx index 8d56f2b..8baa94c 100644 --- a/src/components/AssetBadge.tsx +++ b/src/components/AssetBadge.tsx @@ -3,12 +3,37 @@ import { cn, truncateAddress } from "@/lib/utils"; export const ASSET_COLORS: Record = { XLM: { bg: "bg-[rgba(20,184,166,0.12)]", text: "text-teal" }, + yXLM: { bg: "bg-[rgba(34,197,94,0.12)]", text: "text-green" }, USDC: { bg: "bg-[rgba(86,69,212,0.12)]", text: "text-brand" }, USDT: { bg: "bg-success-dim-strong", text: "text-green" }, BTC: { bg: "bg-[rgba(249,115,22,0.12)]", text: "text-orange" }, ETH: { bg: "bg-[rgba(168,85,247,0.12)]", text: "text-purple" }, + AQUA: { bg: "bg-[rgba(59,130,246,0.12)]", text: "text-blue" }, + SHX: { bg: "bg-[rgba(236,72,153,0.12)]", text: "text-pink" }, + BLND: { bg: "bg-[rgba(236,204,41,0.12)]", text: "text-yellow" }, }; +const FALLBACK_COLOR_PALETTE = [ + { bg: "bg-[rgba(168,85,247,0.12)]", text: "text-purple" }, + { bg: "bg-[rgba(59,130,246,0.12)]", text: "text-blue" }, + { bg: "bg-[rgba(236,72,153,0.12)]", text: "text-pink" }, + { bg: "bg-[rgba(236,204,41,0.12)]", text: "text-yellow" }, + { bg: "bg-[rgba(34,197,94,0.12)]", text: "text-green" }, + { bg: "bg-[rgba(249,115,22,0.12)]", text: "text-orange" }, + { bg: "bg-[rgba(14,165,233,0.12)]", text: "text-cyan" }, + { bg: "bg-[rgba(229,57,53,0.12)]", text: "text-red" }, +]; + +function hashCode(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + return Math.abs(hash); +} + export function getAssetColor( code: string, colorMap?: Record, @@ -21,17 +46,18 @@ export function getAssetColor( ); if (matchedKey && colorMap[matchedKey]) return colorMap[matchedKey]; } - return ( - ASSET_COLORS[code] ?? - ASSET_COLORS[code.toUpperCase()] ?? { bg: "bg-surface-2", text: "text-ink-2" } - ); + if (ASSET_COLORS[code]) return ASSET_COLORS[code]; + const upper = code.toUpperCase(); + if (ASSET_COLORS[upper]) return ASSET_COLORS[upper]; + const hash = hashCode(code); + return FALLBACK_COLOR_PALETTE[hash % FALLBACK_COLOR_PALETTE.length]; } /** * Assets whose issuer is well known enough that printing the issuer address * next to the code is noise rather than information. */ -const KNOWN_ASSETS = new Set(["XLM", "USDC", "USDT", "BTC", "ETH"]); +const KNOWN_ASSETS = new Set(["XLM", "yXLM", "USDC", "USDT", "BTC", "ETH", "AQUA", "SHX", "BLND"]); /** Whether `code` is in the built-in known-asset registry. */ export function isKnownAsset(code: string): boolean { diff --git a/src/components/WalletConnectButton.test.tsx b/src/components/WalletConnectButton.test.tsx index 6c11288..8b23ec1 100644 --- a/src/components/WalletConnectButton.test.tsx +++ b/src/components/WalletConnectButton.test.tsx @@ -158,4 +158,22 @@ describe("WalletConnectButton", () => { expect(screen.queryByText("Previous error")).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: /wallet connected/i })).toBeInTheDocument(); }); + + it("calls onOpenModal when connected address pill is clicked", () => { + const mockOnOpenModal = vi.fn(); + const fullAddress = "GABC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ + isConnected: true, + address: fullAddress, + connectWallet: mockConnect, + clearError: mockClearError, + })); + + render(); + const addressPill = screen.getByRole("button", { + name: `Wallet connected: ${fullAddress}. Click to manage.`, + }); + fireEvent.click(addressPill); + expect(mockOnOpenModal).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/components/index.ts b/src/components/index.ts index d7cc8d1..cdb122f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -41,6 +41,8 @@ export { Input } from "./ui/Input"; export { LabelledValue } from "./ui/LabelledValue"; export { Separator } from "./ui/Separator"; export { AssetRowSkeleton, Skeleton, SkeletonCard, SkeletonRow } from "./ui/Skeleton"; +export type { InfoCellProps } from "./ui/InfoCell"; +export { InfoCell } from "./ui/InfoCell"; // Error handling export { ErrorBoundary } from "./ErrorBoundary";