/** * Cashu mint reachability check. * * A Cashu mint is only usable from a WEB wallet if it answers `GET /v1/info` * with permissive CORS headers. Many popular mints (e.g. mint.minibits.cash) * work great in mobile apps but do NOT set `Access-Control-Allow-Origin`, so the * browser blocks them with a `NetworkError` — which otherwise only surfaces * later as a cryptic "Failed to create invoice". * * In a NATIVE (Capacitor) shell there is no CORS restriction, so every mint is * reachable and we short-circuit the probe to `ok`. */ import { isNativePlatform } from '@/lib/platform'; export type MintReachability = 'ok' | 'cors-blocked' | 'unreachable'; export interface MintProbeResult { reachability: MintReachability; /** Mint name from /v1/info when reachable. */ name?: string; } function normalizeMintUrl(url: string): string { let u = url.trim().replace(/\/+$/, ''); if (!/^https?:\/\//.test(u)) u = `https://${u}`; return u; } /** * Probe a mint's `/v1/info` from the browser. Distinguishes a CORS block * (fetch rejects with a TypeError and the request never yields a response) * from a genuinely down mint. Best-effort — treats ambiguous failures as * `unreachable` rather than falsely flagging CORS. */ export async function probeMintReachability( mintUrl: string, timeoutMs = 7000, ): Promise { // Native shells (Capacitor WebView) aren't subject to CORS — any mint works. if (isNativePlatform()) { return { reachability: 'ok' }; } const base = normalizeMintUrl(mintUrl); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(`${base}/v1/info`, { method: 'GET', headers: { Accept: 'application/json' }, signal: controller.signal, mode: 'cors', }); if (!res.ok) { return { reachability: 'unreachable' }; } let name: string | undefined; try { const info = await res.json(); if (info && typeof info.name === 'string') name = info.name; } catch { // info body wasn't JSON — still counts as reachable/CORS-ok } return { reachability: 'ok', name }; } catch (err) { // A CORS failure throws a TypeError with a "Failed to fetch"-style message // and no response object. An abort (timeout) is a genuine unreachable. if (controller.signal.aborted) { return { reachability: 'unreachable' }; } const msg = err instanceof Error ? err.message : String(err); if (/failed to fetch|networkerror|load failed|cors/i.test(msg)) { return { reachability: 'cors-blocked' }; } return { reachability: 'unreachable' }; } finally { clearTimeout(timer); } }