|
| 1 | +import React, {useEffect, useMemo, useState} from 'react'; |
| 2 | +import styles from './styles.module.css'; |
| 3 | + |
| 4 | +const DEFAULT_CATALOG_URL = |
| 5 | + 'https://raw.githubusercontent.com/powerloom/snapshotter-computes/bds_eth_uniswapv3_core/api/endpoints.json'; |
| 6 | + |
| 7 | +function epochDenominator(creditPerEpoch) { |
| 8 | + if (!Number.isFinite(creditPerEpoch) || creditPerEpoch <= 0) return null; |
| 9 | + const den = Math.round(1 / creditPerEpoch); |
| 10 | + return Math.abs(den * creditPerEpoch - 1) < 1e-9 ? den : null; |
| 11 | +} |
| 12 | + |
| 13 | +function creditsLabel(weight, billing) { |
| 14 | + const w = Number.isFinite(weight) && weight > 0 ? weight : 1; |
| 15 | + const perEpoch = billing?.credit_per_epoch; |
| 16 | + if (!Number.isFinite(perEpoch) || perEpoch <= 0) { |
| 17 | + return `×${w}`; |
| 18 | + } |
| 19 | + const den = epochDenominator(perEpoch); |
| 20 | + if (den != null) { |
| 21 | + return w === 1 ? `1/${den} credit` : `${w}/${den} credits`; |
| 22 | + } |
| 23 | + const product = w * perEpoch; |
| 24 | + return `${product} credit${product === 1 ? '' : 's'}`; |
| 25 | +} |
| 26 | + |
| 27 | +function billingColumn(ep, billing) { |
| 28 | + const mod = ep.billing_modifier; |
| 29 | + if (ep.sse === true || mod?.type === 'stream_session') { |
| 30 | + const session = |
| 31 | + Number.isFinite(mod?.credits_per_connection) && mod.credits_per_connection > 0 |
| 32 | + ? mod.credits_per_connection |
| 33 | + : billing?.credit_per_stream_session; |
| 34 | + if (Number.isFinite(session) && session > 0) { |
| 35 | + return `${session} credits / session`; |
| 36 | + } |
| 37 | + return 'flat rate / session'; |
| 38 | + } |
| 39 | + if (mod?.type === 'lookback_multiplier') { |
| 40 | + const base = Number.isFinite(ep.credit_weight) ? ep.credit_weight : 5; |
| 41 | + return `${creditsLabel(base, billing)} × lookback (see below)`; |
| 42 | + } |
| 43 | + return creditsLabel(ep.credit_weight, billing); |
| 44 | +} |
| 45 | + |
| 46 | +function weightColumn(ep) { |
| 47 | + const mod = ep.billing_modifier; |
| 48 | + if (ep.sse === true || mod?.type === 'stream_session') { |
| 49 | + return '—'; |
| 50 | + } |
| 51 | + if (mod?.type === 'lookback_multiplier') { |
| 52 | + const base = Number.isFinite(ep.credit_weight) ? ep.credit_weight : 5; |
| 53 | + return `${base} × lookback`; |
| 54 | + } |
| 55 | + const w = Number.isFinite(ep.credit_weight) ? ep.credit_weight : 1; |
| 56 | + return String(w); |
| 57 | +} |
| 58 | + |
| 59 | +function meteredEndpoints(catalog) { |
| 60 | + const eps = Array.isArray(catalog?.endpoints) ? catalog.endpoints : []; |
| 61 | + return eps |
| 62 | + .filter((e) => e && e.metered !== false && typeof e.path === 'string') |
| 63 | + .sort((a, b) => a.path.localeCompare(b.path)); |
| 64 | +} |
| 65 | + |
| 66 | +export default function LiveMeteredRoutesTable({catalogUrl = DEFAULT_CATALOG_URL}) { |
| 67 | + const [state, setState] = useState({loading: true, error: null, data: null}); |
| 68 | + |
| 69 | + useEffect(() => { |
| 70 | + const controller = new AbortController(); |
| 71 | + async function load() { |
| 72 | + try { |
| 73 | + const response = await fetch(catalogUrl, { |
| 74 | + headers: {Accept: 'application/json'}, |
| 75 | + signal: controller.signal, |
| 76 | + }); |
| 77 | + if (!response.ok) { |
| 78 | + throw new Error(`GET catalog returned ${response.status}`); |
| 79 | + } |
| 80 | + const data = await response.json(); |
| 81 | + setState({loading: false, error: null, data}); |
| 82 | + } catch (error) { |
| 83 | + if (error.name === 'AbortError') return; |
| 84 | + setState({loading: false, error: error.message, data: null}); |
| 85 | + } |
| 86 | + } |
| 87 | + load(); |
| 88 | + return () => controller.abort(); |
| 89 | + }, [catalogUrl]); |
| 90 | + |
| 91 | + const rows = useMemo(() => meteredEndpoints(state.data), [state.data]); |
| 92 | + const billing = state.data?.billing; |
| 93 | + const market = state.data?.market; |
| 94 | + const version = state.data?.version; |
| 95 | + |
| 96 | + return ( |
| 97 | + <div className={styles.wrapper}> |
| 98 | + <div className={styles.header}> |
| 99 | + <div> |
| 100 | + <strong>Metered routes (live)</strong> |
| 101 | + <p> |
| 102 | + Fetched from <code>api/endpoints.json</code> — same catalog the resolver and{' '} |
| 103 | + <code>bds-agent</code> use for route matching and credit weights. |
| 104 | + </p> |
| 105 | + </div> |
| 106 | + <a href={catalogUrl} target="_blank" rel="noreferrer"> |
| 107 | + Open catalog JSON |
| 108 | + </a> |
| 109 | + </div> |
| 110 | + |
| 111 | + {state.loading && <p className={styles.status}>Loading catalog…</p>} |
| 112 | + |
| 113 | + {state.error && ( |
| 114 | + <div className={styles.error}> |
| 115 | + Could not load the live catalog: <code>{state.error}</code> |
| 116 | + <br /> |
| 117 | + Open the catalog JSON above for the full route list. |
| 118 | + </div> |
| 119 | + )} |
| 120 | + |
| 121 | + {!state.loading && !state.error && rows.length === 0 && ( |
| 122 | + <div className={styles.error}>No metered endpoints found in the catalog.</div> |
| 123 | + )} |
| 124 | + |
| 125 | + {!state.loading && !state.error && rows.length > 0 && ( |
| 126 | + <> |
| 127 | + <div className={styles.meta}> |
| 128 | + {market && <span>{market}</span>} |
| 129 | + {Number.isFinite(version) && <span>catalog v{version}</span>} |
| 130 | + <span>{rows.length} metered route(s)</span> |
| 131 | + {billing?.credit_per_epoch != null && ( |
| 132 | + <span>base GET ≈ 1/7200 credit</span> |
| 133 | + )} |
| 134 | + </div> |
| 135 | + <div className={styles.tableWrap}> |
| 136 | + <table> |
| 137 | + <thead> |
| 138 | + <tr> |
| 139 | + <th>Metered route</th> |
| 140 | + <th> |
| 141 | + <code>credit_weight</code> |
| 142 | + </th> |
| 143 | + <th>Typical credits</th> |
| 144 | + </tr> |
| 145 | + </thead> |
| 146 | + <tbody> |
| 147 | + {rows.map((ep) => ( |
| 148 | + <tr key={`${ep.method || 'GET'}:${ep.path}`}> |
| 149 | + <td className={styles.pathCell}> |
| 150 | + <code>{ep.path}</code> |
| 151 | + </td> |
| 152 | + <td>{weightColumn(ep)}</td> |
| 153 | + <td>{billingColumn(ep, billing)}</td> |
| 154 | + </tr> |
| 155 | + ))} |
| 156 | + </tbody> |
| 157 | + </table> |
| 158 | + </div> |
| 159 | + </> |
| 160 | + )} |
| 161 | + </div> |
| 162 | + ); |
| 163 | +} |
0 commit comments