Address: {params.address}
- {/* Metrics grid */}
{[
{ label: "Fills", value: solver.fills },
{ label: "Failed", value: solver.failed },
{ label: "Success Rate", value: `${solver.successRatePct}%` },
- { label: "Total Volume", value: usdCompact.format(solver.volumeUsd) },
+ { label: "Total Volume", value: usdCompact(solver.volumeUsd) },
{ label: "Avg Fill Time", value: `${solver.avgFillTimeSeconds}s` },
- { label: "Bond", value: usdCompact.format(solver.bondUsd) },
].map(({ label, value }) => (
{label}
@@ -110,10 +87,10 @@ export default function SolverDetailPage({ params }: { params: { address: string
const chainName = chainMeta?.name ?? chainId;
const chainColor = chainMeta?.color ?? "#6B7280";
return (
-
Recent Fills by Solver
- {historyLoading && fillHistory.length === 0 ? (
-
-
-
- ) : historyError ? (
-
- Couldn't load fill history right now.
-
- ) : fillHistory.filter(item => item.solver === solver.address).length === 0 ? (
-
- No fills from this solver in the history.
-
- ) : (
-
- {fillHistory
- .filter(item => item.solver === solver.address)
- .slice(0, 10)
- .map(fill => (
-
-
-
-
-
- ID: {fill.id}
-
-
- {fill.srcAmount} {fill.srcToken} → {fill.dstToken}
-
-
-
-
-
- {fill.srcChain} · {timeAgo(fill.createdAt)}
-
-
-
- ))}
-
- )}
+
>
)}
diff --git a/src/components/SolverFillHistory.test.tsx b/src/components/SolverFillHistory.test.tsx
index 1ef3e0e..3e0ea59 100644
--- a/src/components/SolverFillHistory.test.tsx
+++ b/src/components/SolverFillHistory.test.tsx
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import type { FeedItem } from "@/lib/types";
+import { SolverFillHistory } from "./SolverFillHistory";
const { useIntentFeedMock } = vi.hoisted(() => ({
useIntentFeedMock: vi.fn(),
@@ -29,68 +30,6 @@ vi.mock("@/lib/time", () => ({
},
}));
-function SolverFillHistory({ solverAddress }: { solverAddress: string }) {
- const { items: fillHistory, isLoading, error } = useIntentFeedMock();
-
- const solverFills = fillHistory
- .filter((item: FeedItem) => item.solver === solverAddress)
- .slice(0, 10);
-
- if (isLoading) {
- return (
-
- {[0, 1, 2].map((i) => (
-
- ))}
-
- );
- }
-
- if (error) {
- return (
-
- Couldn't load fill history right now.
-
- );
- }
-
- if (solverFills.length === 0) {
- return (
-
- No fills from this solver in the history.
-
- );
- }
-
- return (
-
- {solverFills.map((fill: FeedItem) => (
-
-
-
-
-
- ID: {fill.id}
-
-
- {fill.srcAmount} {fill.srcToken} → {fill.dstToken}
-
-
-
{fill.status}
-
-
- {fill.srcChain} · a moment ago
-
-
-
- ))}
-
- );
-}
-
describe("SolverFillHistory", () => {
it("shows loading skeleton while fills are being fetched", () => {
useIntentFeedMock.mockReturnValue({
diff --git a/src/components/SolverFillHistory.tsx b/src/components/SolverFillHistory.tsx
new file mode 100644
index 0000000..f3863bd
--- /dev/null
+++ b/src/components/SolverFillHistory.tsx
@@ -0,0 +1,66 @@
+import { useIntentFeed } from "@/hooks/useIntentFeed";
+import { IntentStatusBadge } from "@/components/IntentStatusBadge";
+import { timeAgo } from "@/lib/time";
+import type { FeedItem } from "@/lib/types";
+
+export function SolverFillHistory({ solverAddress }: { solverAddress: string }) {
+ const { items: fillHistory, isLoading, error } = useIntentFeed();
+
+ const solverFills = fillHistory
+ .filter((item: FeedItem) => item.solver === solverAddress)
+ .slice(0, 10);
+
+ if (isLoading) {
+ return (
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Couldn't load fill history right now.
+
+ );
+ }
+
+ if (solverFills.length === 0) {
+ return (
+
+ No fills from this solver in the history.
+
+ );
+ }
+
+ return (
+
+ {solverFills.map((fill: FeedItem) => (
+
+
+
+
+
+ ID: {fill.id}
+
+
+ {fill.srcAmount} {fill.srcToken} → {fill.dstToken}
+
+
+
+
+
+ {fill.srcChain} · {timeAgo(fill.createdAt)}
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/components/SolverHeaderCard.test.tsx b/src/components/SolverHeaderCard.test.tsx
index 719a495..913840a 100644
--- a/src/components/SolverHeaderCard.test.tsx
+++ b/src/components/SolverHeaderCard.test.tsx
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import type { Solver } from "@/lib/types";
+import { SolverHeaderCard } from "./SolverHeaderCard";
vi.mock("@/components/IntentStatusBadge", () => ({
IntentStatusBadge: ({ status }: { status: string }) => (
@@ -8,70 +9,6 @@ vi.mock("@/components/IntentStatusBadge", () => ({
),
}));
-function SolverHeaderCard({ solver }: { solver: Solver }) {
- function truncateAddress(address: string) {
- if (address.length <= 12) return address;
- return `${address.slice(0, 6)}...${address.slice(-6)}`;
- }
-
- const usdCompact = new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- notation: "compact",
- maximumFractionDigits: 1,
- });
-
- return (
-
-
-
-
Solver
-
- {solver.name}
-
-
-
- {solver.status === "active" ? "Active" : "Inactive"}
-
-
-
-
- Address: {truncateAddress(solver.address)}
-
-
-
-
-
Bond
-
- {usdCompact.format(solver.bondUsd)}
-
-
-
-
-
- );
-}
-
describe("SolverHeaderCard", () => {
const mockSolver: Solver = {
name: "Alpha Market Making",
diff --git a/src/components/SolverHeaderCard.tsx b/src/components/SolverHeaderCard.tsx
new file mode 100644
index 0000000..983b024
--- /dev/null
+++ b/src/components/SolverHeaderCard.tsx
@@ -0,0 +1,67 @@
+import type { Solver } from "@/lib/types";
+import { formatCurrency, toBCP47 } from "@/lib/format";
+import { useLocale } from "@/lib/i18n/I18nProvider";
+
+function truncateAddress(address: string) {
+ if (address.length <= 12) return address;
+ return `${address.slice(0, 6)}...${address.slice(-6)}`;
+}
+
+export function SolverHeaderCard({ solver }: { solver: Solver }) {
+ const locale = useLocale();
+ const usdCompact = (value: number) =>
+ formatCurrency(value, toBCP47(locale), {
+ notation: "compact",
+ maximumFractionDigits: 1,
+ });
+
+ return (
+
+
+
+
Solver
+
+ {solver.name}
+
+
+
+ {solver.status === "active" ? "Active" : "Inactive"}
+
+
+
+
+ Address: {truncateAddress(solver.address)}
+
+
+
+
+
Bond
+
+ {usdCompact(solver.bondUsd)}
+
+
+
+
+
+ );
+}
diff --git a/src/components/SwapCard.tsx b/src/components/SwapCard.tsx
index 3524333..0dfb9b7 100644
--- a/src/components/SwapCard.tsx
+++ b/src/components/SwapCard.tsx
@@ -6,7 +6,7 @@ import { useDebouncedValue } from "@/hooks/useDebouncedValue";
import { useSwapSubmission } from "@/hooks/useSwapSubmission";
import { useToastStore } from "@/store/toast";
import { CHAINS, SRC_TOKENS, DST_TOKENS } from "@/lib/marketData";
-import { formatCurrency, formatTokenAmount } from "@/lib/format";
+import { formatCurrency, formatTokenAmount, toBCP47 } from "@/lib/format";
import { useTranslation } from "@/lib/i18n/I18nProvider";
import { isValidStellarPublicKey } from "@/lib/stellarAddress";
import type { MessageKey } from "@/lib/i18n";
@@ -33,7 +33,7 @@ export function SwapCard({
previewQuote,
onPreviewSubmit,
}: SwapCardProps = {}) {
- const { t } = useTranslation();
+ const { t, locale } = useTranslation();
const [srcChain, setSrcChain] = useState("ethereum");
const [srcToken, setSrcToken] = useState(SRC_TOKENS["ethereum"][0]);
@@ -285,7 +285,7 @@ export function SwapCard({
${token.symbol === srcToken.symbol ? "bg-vx-lav-bg text-vx-lav" : "hover:bg-vx-surface text-vx-muted hover:text-vx-text"}`}
>
{token.symbol}
-
${token.priceUSD.toLocaleString()}
+
${formatTokenAmount(token.priceUSD, toBCP47(locale))}
))}
@@ -293,9 +293,8 @@ export function SwapCard({
{srcValueUSD > 0 && (
- {/* Number formatting stays locale-hardcoded here; issue #63 owns making it locale-aware. */}
{t("swap.from.approxValue", {
- value: srcValueUSD.toLocaleString("en-US", { maximumFractionDigits: 2 }),
+ value: formatTokenAmount(srcValueUSD, toBCP47(locale), { maximumFractionDigits: 2 }),
})}
)}
diff --git a/src/hooks/useIntent.ts b/src/hooks/useIntent.ts
index 7ec6c29..0938fff 100644
--- a/src/hooks/useIntent.ts
+++ b/src/hooks/useIntent.ts
@@ -2,8 +2,13 @@ import useSWR from "swr";
import { fetcher } from "@/lib/api";
import type { IntentDetail } from "@/lib/types";
-// Single-intent detail fetch. No WebSocket or polling needed — the user
-// navigates here to check a specific intent's outcome.
+const POLL_INTERVAL_MS = 5_000;
+const TERMINAL_STATUSES = new Set(["filled", "failed"]);
+
+// Single-intent detail fetch. The WebSocket feed only carries FeedItem
+// summaries (not full IntentDetail), so per-intent live updates are done via
+// SWR polling instead of subscribing to the shared socket. Polling stops
+// once the intent reaches a terminal status to avoid wasted requests.
//
// revalidateOnFocus: true (default) is kept because a user may tab away,
// wait for a fill, and tab back — which should show the updated status.
@@ -15,7 +20,8 @@ export function useIntent(id: string | null) {
id ? `/intents/${id}` : null,
fetcher,
{
- refreshInterval: 0,
+ refreshInterval: (latest) =>
+ latest && TERMINAL_STATUSES.has(latest.status) ? 0 : POLL_INTERVAL_MS,
dedupingInterval: 5_000,
revalidateOnFocus: true,
},
diff --git a/src/hooks/useSolverRegistration.ts b/src/hooks/useSolverRegistration.ts
index 4ced07d..2826820 100644
--- a/src/hooks/useSolverRegistration.ts
+++ b/src/hooks/useSolverRegistration.ts
@@ -43,6 +43,7 @@ function RegistrationErrorMessage(err: unknown): string {
export function useSolverRegistration() {
const [status, setStatus] = useState