/** * The few things the socket does not push. * * A miner's own history is a query, not a stream — it is asked for once when * someone opens their page and does not change until the next block they win. * Pushing it to every connected tab would be strictly worse. */ import type { AccountDetail } from './generated/AccountDetail' import type { AccountEvent } from './generated/AccountEvent' import type { AccountRow } from './generated/AccountRow' import type { ApiError } from './generated/ApiError' import type { BlockDetail } from './generated/BlockDetail' import type { BlockExtrinsic } from './generated/BlockExtrinsic' import type { CallIndex } from './generated/CallIndex' import type { ChainSeries } from './generated/ChainSeries' import type { ChainRoles } from './generated/ChainRoles' import type { ChainState } from './generated/ChainState' import type { GenesisDetail } from './generated/GenesisDetail' import type { IntentsInventory } from './generated/IntentsInventory' import type { IntentsPrice } from './generated/IntentsPrice' import type { IntentsSeries } from './generated/IntentsSeries' import type { IntentsSwaps } from './generated/IntentsSwaps' import type { Markets } from './generated/Markets' import type { RefPools } from './generated/RefPools' import type { RefSwaps } from './generated/RefSwaps' import type { ReversibleState } from './generated/ReversibleState' import type { RecentBlock } from './generated/RecentBlock' import type { MinerDetail } from './generated/MinerDetail' import type { Distribution } from './generated/Distribution' import type { NetworkSummary } from './generated/NetworkSummary' import type { VestingSummary } from './generated/VestingSummary' import type { WormholeSummary } from './generated/WormholeSummary' import type { NodeIndex } from './generated/NodeIndex' import type { NodeInfo } from './generated/NodeInfo' import type { RuntimeDetail } from './generated/RuntimeDetail' import type { RuntimeSummary } from './generated/RuntimeSummary' import type { Window as WindowName } from './generated/Window' const BASE = import.meta.env.VITE_API_BASE_URL ?? '/v1' /** An error the API described, as opposed to a network failure. */ export class RequestFailed extends Error { constructor( readonly status: number, readonly code: string, message: string, ) { super(message) this.name = 'RequestFailed' } } async function get(path: string, signal?: AbortSignal): Promise { const response = await fetch(`${BASE}${path}`, signal ? { signal } : {}) if (!response.ok) { // The API answers failures with a typed body; fall back to the status text // for anything that got as far as nginx but not as far as the API. const body = (await response.json().catch(() => null)) as ApiError | null throw new RequestFailed( response.status, body?.code ?? 'http_error', body?.message ?? response.statusText, ) } return (await response.json()) as T } /** * The history behind the headline tiles. * * A query rather than a stream, like a miner's page: pushing fifty buckets to * every connected tab on every block would be the whole page's bandwidth spent * on a line whose newest point moves once a bucket. The caller refetches when * the chain crosses a bucket boundary, which is the only moment it changes. */ export function fetchSeries( chain: string, windowName: WindowName, signal?: AbortSignal, ): Promise { return get( `/chains/${encodeURIComponent(chain)}/series?window=${windowName}`, signal, ) } /** * One block, by height or by hash. * * A height names whatever currently holds it and a hash names one block for * good, so the caller redirects to the hash once the height resolves — see * `BlockPanel`. The API accepts either spelling at the same path. */ export function fetchBlock( chain: string, block: string, signal?: AbortSignal, ): Promise { return get( `/chains/${encodeURIComponent(chain)}/blocks/${encodeURIComponent(block)}`, signal, ) } /** * One account: what it was paid, and everything else it appears in. * * Paged with a keyset rather than an offset — pass the `(height, event_index)` * of the last row already shown. New blocks arrive at the front of this * ordering while a reader is paging through it, and an offset would show them * the same row twice. */ export function fetchAccount( chain: string, address: string, before?: { height: number; index: number }, signal?: AbortSignal, ): Promise { const page = before ? `?before_height=${before.height}&before_index=${before.index}` : '' return get( `/chains/${encodeURIComponent(chain)}/accounts/${encodeURIComponent(address)}${page}`, signal, ) } /** * Every dispatchable the runtime declares, with what has been done with it. * * The declared half comes from metadata and the used half from the index; the * join is the thing worth looking at, so it arrives as one document. */ export function fetchCalls(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${encodeURIComponent(chain)}/calls`, signal) } /** Every firing of one kind of event, newest first. */ export function fetchEventFeed( chain: string, pallet: string, variant: string, before?: number, signal?: AbortSignal, ): Promise { const cursor = before === undefined ? '' : `?before=${before}` return get( `/chains/${encodeURIComponent(chain)}/events/${encodeURIComponent(pallet)}/${encodeURIComponent(variant)}${cursor}`, signal, ) } /** Every dispatch of one kind of call, newest first. */ export function fetchCallFeed( chain: string, pallet: string, call: string, before?: number, signal?: AbortSignal, ): Promise { const cursor = before === undefined ? '' : `?before=${before}` return get( `/chains/${encodeURIComponent(chain)}/calls/${encodeURIComponent(pallet)}/${encodeURIComponent(call)}${cursor}`, signal, ) } /** * Every storage entry readable without a key, decoded. * * Live on every call. State is what is true now, and caching now produces a * copy of some earlier now that is indistinguishable from the real thing. */ export function fetchState(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${encodeURIComponent(chain)}/state`, signal) } /** * Scheduled transfers that have not yet executed. * * Live, and re-read rather than counted down locally: the deadline is a block * height, and blocks do not arrive on a timer. */ export function fetchReversible(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${encodeURIComponent(chain)}/reversible`, signal) } /** Every account the chain names, and how. */ export function fetchRoles(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${encodeURIComponent(chain)}/roles`, signal) } /** Block zero, and what it handed out. */ export function fetchGenesis(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${encodeURIComponent(chain)}/genesis`, signal) } /** Accounts ranked by what they have been paid, most first. */ export function fetchAccounts(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${encodeURIComponent(chain)}/accounts`, signal) } /** * A page of block history, newest first. * * Without `before` this is the live ticker out of the API's memory, which is * what the front of the list is; with it, the record. Same question, and the * boundary is whether the caller is at the front. */ export function fetchBlockPage( chain: string, before?: number, signal?: AbortSignal, ): Promise { const cursor = before === undefined ? '' : `?before=${before}` return get(`/chains/${encodeURIComponent(chain)}/blocks${cursor}`, signal) } /** Every runtime this chain has been seen running, oldest first. */ export function fetchRuntimes(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${encodeURIComponent(chain)}/runtimes`, signal) } /** * One runtime, described by itself. * * Served from the cached metadata rather than the node — which is the whole * reason it was cached: a runtime's description outlives the state the node * would need to regenerate it. */ export function fetchRuntime( chain: string, spec: number, signal?: AbortSignal, ): Promise { return get(`/chains/${encodeURIComponent(chain)}/runtimes/${spec}`, signal) } /** One miner's standing and history. */ export function fetchMiner( chain: string, miner: string, windowName: WindowName, signal?: AbortSignal, ): Promise { return get( `/chains/${encodeURIComponent(chain)}/miners/${encodeURIComponent(miner)}?window=${windowName}`, signal, ) } /** Every node the chain's telemetry feed reports. */ export async function fetchNodes(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${chain}/nodes`, signal) } /** One node, by peer id. */ export async function fetchNode( chain: string, peer: string, signal?: AbortSignal, ): Promise { return get(`/chains/${chain}/nodes/${encodeURIComponent(peer)}`, signal) } /** High-level network statistics: supply, capability and activity. */ export async function fetchNetwork(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${chain}/network`, signal) } /** Who won the blocks, and what became of the rewards. */ export async function fetchDistribution( chain: string, window: WindowName, signal?: AbortSignal, ): Promise { return get(`/chains/${chain}/distribution?window=${window}`, signal) } /** What enters the wormhole, what leaves, and what is still inside. */ export async function fetchWormhole(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${chain}/wormhole`, signal) } /** Every grant out of the vesting pot, and the curve they release on. */ export async function fetchVesting(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${chain}/vesting`, signal) } /** * The chain's token as NEAR Intents settles it: per counter token, from trades * only. A chain that tracks nothing there answers 404 `no_intents`. */ export async function fetchIntentsPrice( chain: string, signal?: AbortSignal, ): Promise { return get(`/chains/${chain}/intents/price`, signal) } /** The venues a chain's token is followed on; empty for none. */ export async function fetchMarkets(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${chain}/markets`, signal) } /** * Settled swaps, newest first: the latest page without a cursor, then each * older page by passing the previous response's `next` as `before`. */ export async function fetchSwapsNewest( chain: string, before: string | null, signal?: AbortSignal, ): Promise { const cursor = before ? `&before=${encodeURIComponent(before)}` : '' return get(`/chains/${chain}/intents/swaps?order=newest&limit=50${cursor}`, signal) } /** The asset's price in one counter token, bucketed over its latest trades. */ export async function fetchIntentsSeries( chain: string, counter: string, signal?: AbortSignal, ): Promise { return get( `/chains/${chain}/intents/series?counter=${encodeURIComponent(counter)}`, signal, ) } /** Solver inventory inside intents.near; null until first read. */ export async function fetchIntentsInventory( chain: string, signal?: AbortSignal, ): Promise { return get(`/chains/${chain}/intents/inventory`, signal) } /** The asset's Ref DCL pools: state, ladder, coverage. Null until first read. */ export async function fetchRefPools(chain: string, signal?: AbortSignal): Promise { return get(`/chains/${chain}/ref/pools`, signal) } /** Swaps in the asset's Ref DCL pools, newest first. */ export async function fetchRefSwaps( chain: string, before: string | null, signal?: AbortSignal, ): Promise { const cursor = before ? `?before=${encodeURIComponent(before)}` : '' return get(`/chains/${chain}/ref/swaps${cursor}`, signal) }