Skip to content
Open
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
2,156 changes: 2,076 additions & 80 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"scripts": {
"dev": "vite",
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly",
"build": "vite build --config vite.lib.config.ts && tsc -p tsconfig.lib.json",
"build:app": "vite build",
"preview": "vite preview",
"test": "vitest",
Expand All @@ -47,8 +47,13 @@
}
],
"dependencies": {
"@hugeicons/core-free-icons": "^4.2.2",
"@hugeicons/react": "^1.1.9",
"@radix-ui/react-slot": "^1.3.0",
"clsx": "^2.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"tailwind-merge": "^3.6.0"
},
"peerDependencies": {
"tailwindcss": "^3.0.0"
Expand All @@ -59,13 +64,23 @@
}
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@size-limit/file": "^11.2.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^10.6.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.7.0",
"jsdom": "^29.1.1",
"size-limit": "^11.2.0",
"tailwindcss": "^3.3.0",
"terser": "^5.48.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.62.0",
"vite": "^5.0.0",
"vitest": "^1.0.0"
}
Expand Down
28 changes: 23 additions & 5 deletions src/components/AssetBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,35 @@ describe("AssetBadge", () => {
expect(document.querySelector("[data-address]")).not.toBeInTheDocument();
});

it("falls back to grey/surface-2 for an unknown asset", () => {
it("applies deterministic color classes for an unknown asset", () => {
const { container } = render(<AssetBadge balance={unknownBalance} />);
const icon = container.querySelector(".bg-surface-2");
// WAVEX (hash % 10 = 5) maps to purple background/text
const icon = container.querySelector(".text-purple");
expect(icon).toBeInTheDocument();
});

it("renders the asset code for an unknown asset", () => {
render(<AssetBadge balance={unknownBalance} />);
expect(screen.getByText("WAVEX")).toBeInTheDocument();
});

it("renders 1-character asset codes centered in the icon circle", () => {
const oneCharBalance: Balance = {
assetType: "credit_alphanum4",
assetCode: "A",
assetIssuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
balance: "100",
balanceFloat: 100,
};
const { container } = render(<AssetBadge balance={oneCharBalance} />);
const icon = container.querySelector(".rounded-full");
expect(icon).toHaveClass("flex");
expect(icon).toHaveClass("items-center");
expect(icon).toHaveClass("justify-center");
expect(icon).toHaveClass("text-center");
expect(icon).toHaveClass("leading-none");
expect(icon?.textContent).toBe("A");
});
});

describe("AssetPill", () => {
Expand All @@ -96,11 +115,10 @@ describe("AssetPill", () => {
expect(screen.getByText("USDC")).toHaveClass("text-brand");
});

it("falls back to grey for an unknown asset code", () => {
it("applies deterministic color for an unknown asset code", () => {
render(<AssetPill assetCode="WAVEX" />);
const pill = screen.getByText("WAVEX");
expect(pill).toHaveClass("bg-surface-2");
expect(pill).toHaveClass("text-ink-2");
expect(pill).toHaveClass("text-purple");
});

it("merges a custom className", () => {
Expand Down
27 changes: 19 additions & 8 deletions src/components/AssetBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@ import { cn } from "@/lib/utils";
import { truncateAddress } from "@/lib/utils";
import type { Balance } from "@/lib/client";

const ASSET_COLORS: Record<string, { bg: string; text: string }> = {
XLM: { bg: "bg-[rgba(20,184,166,0.12)]", text: "text-teal" },
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" },
};
const PALETTE = [
{ bg: "bg-success-dim-strong", text: "text-green" }, // 0: USDT (hash % 10 = 0)
{ bg: "bg-[rgba(20,184,166,0.12)]", text: "text-teal" }, // 1: XLM (hash % 10 = 1)
{ bg: "bg-error-dim", text: "text-red" }, // 2: Red
{ bg: "bg-[rgba(86,69,212,0.12)]", text: "text-brand" }, // 3: USDC (hash % 10 = 3)
{ bg: "bg-[rgba(236,72,153,0.12)]", text: "text-[rgb(236,72,153)]" }, // 4: Pink
{ bg: "bg-[rgba(168,85,247,0.12)]", text: "text-purple" }, // 5: ETH / WAVEX (hash % 10 = 5)
{ bg: "bg-[rgba(6,182,212,0.12)]", text: "text-[rgb(6,182,212)]" }, // 6: Cyan
{ bg: "bg-[rgba(249,115,22,0.12)]", text: "text-orange" }, // 7: BTC (hash % 10 = 7)
{ bg: "bg-[rgba(234,179,8,0.12)]", text: "text-[rgb(234,179,8)]" }, // 8: Yellow
{ bg: "bg-[rgba(99,102,241,0.12)]", text: "text-[rgb(99,102,241)]" }, // 9: Indigo
];

function getAssetColor(code: string) {
return ASSET_COLORS[code] ?? { bg: "bg-surface-2", text: "text-ink-2" };
let hash = 0;
for (let i = 0; i < code.length; i++) {
hash = code.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % 10;
return PALETTE[index];
}

interface AssetBadgeProps {
Expand Down Expand Up @@ -53,6 +63,7 @@ export function AssetBadge({
className={cn(
"rounded-full flex items-center justify-center font-bold shrink-0",
iconSize,
"text-center leading-none",
bg,
text,
)}
Expand Down
4 changes: 3 additions & 1 deletion src/components/ContractEventFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ContractEvent } from "@/lib/client";

/**
* ContractEventFeed Component
*
Expand Down Expand Up @@ -48,7 +50,7 @@ export function ContractEventFeed({
// Component implementation
}

interface ContractEventFeedProps {
export interface ContractEventFeedProps {
contractId: string;
limit?: number;
autoRefresh?: number;
Expand Down
2 changes: 1 addition & 1 deletion src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function ErrorBoundary({
// Component implementation
}

interface ErrorBoundaryProps {
export interface ErrorBoundaryProps {
children: React.ReactNode;
fallback?: React.ReactNode;
onError?: (error: Error) => void;
Expand Down
31 changes: 29 additions & 2 deletions src/components/FeeEstimator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,40 @@ describe("FeeEstimator", () => {
expect(screen.getByText("Recommended")).toBeInTheDocument();
});

it("renders the error message when the client returns an error", async () => {
mockEstimateFee({ data: null, error: "Rate limit exceeded" });
it("renders the error message and a retry button when the client returns an error", async () => {
let callCount = 0;
vi.mocked(getClient).mockReturnValue({
transaction: {
estimateFee: vi.fn().mockImplementation(() => {
callCount++;
if (callCount === 1) {
return Promise.resolve({ data: null, error: "Rate limit exceeded" });
} else {
return Promise.resolve({ data: { baseFee: "150", recommended: "600" }, error: null });
}
}),
},
} as unknown as SorokitClient);

render(<FeeEstimator />);

// Initial check for error state
await waitFor(() => {
expect(screen.getByText("Rate limit exceeded")).toBeInTheDocument();
});

const retryButton = screen.getByRole("button", { name: "Retry" });
expect(retryButton).toBeInTheDocument();

// Click retry
fireEvent.click(retryButton);

// Should resolve with new data, clearing the error
await waitFor(() => {
expect(screen.getByText("150")).toBeInTheDocument();
expect(screen.getByText("600")).toBeInTheDocument();
});
expect(screen.queryByText("Rate limit exceeded")).not.toBeInTheDocument();
});

it("clicking the refresh button triggers a new estimateFee call", async () => {
Expand Down
Loading