On 2026-10-06 the team bridged 150,000 QTC to NEAR and put almost all of it in a Ref Finance DCL pool, not into solver balances, so the market page saw none of it. The pool contract (dclv2.ref-labs.near) is on shard 8 and our node tracks only intents.near's shard 7, so its swap events are never in anything the scan reads. What the scan does see is the relay transaction: the Verifier's outcome logs an ft_withdraw from the solver to the pool. Those are queued (ref_pending, migration 0012, persisted because the process restarts) and fetched whole from the public endpoint 20 blocks later, once the pool's receipt has run; its dcl.ref swaps are recorded with the solver as trader, since the pool itself records intents.near. Swaps made by calling the pool directly are missed — and the pool's own cumulative counters say by how much, so the page reports coverage instead of a disclaimer: today 99.75% of QTC bought out and 100% of QTC sold in, the gap one 0.000498 QTC buy. Pool state comes from view calls once a minute: price at the current point, holdings, whitelist, counters, and the liquidity ladder from blackbeard_core::dcl — discrete sums over points, with the current point split exactly by liquidity_x; tested against the pool's own totals (QTC to 0.01, and USDC short by exactly the LP fees total_x also holds). Pools are found by paging list_pools. It is its own market, /:chain/market/ref-dcl, never summed with NEAR Intents trades: a user's buy is a solver fill there and, after it, the solver's restock here. The pools' whitelists join the inventory poll, which now finds solver-ref-2.near holding 0.288 QTC. Migration 0013 pulls QTC's scan back to NEAR 218,700,000, before the pool, so the swaps production already passed are found on the re-read. Refs #37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
352 lines
12 KiB
TypeScript
352 lines
12 KiB
TypeScript
/**
|
|
* 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<T>(path: string, signal?: AbortSignal): Promise<T> {
|
|
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<ChainSeries> {
|
|
return get<ChainSeries>(
|
|
`/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<BlockDetail> {
|
|
return get<BlockDetail>(
|
|
`/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<AccountDetail> {
|
|
const page = before ? `?before_height=${before.height}&before_index=${before.index}` : ''
|
|
return get<AccountDetail>(
|
|
`/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<CallIndex> {
|
|
return get<CallIndex>(`/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<AccountEvent[]> {
|
|
const cursor = before === undefined ? '' : `?before=${before}`
|
|
return get<AccountEvent[]>(
|
|
`/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<BlockExtrinsic[]> {
|
|
const cursor = before === undefined ? '' : `?before=${before}`
|
|
return get<BlockExtrinsic[]>(
|
|
`/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<ChainState> {
|
|
return get<ChainState>(`/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<ReversibleState> {
|
|
return get<ReversibleState>(`/chains/${encodeURIComponent(chain)}/reversible`, signal)
|
|
}
|
|
|
|
/** Every account the chain names, and how. */
|
|
export function fetchRoles(chain: string, signal?: AbortSignal): Promise<ChainRoles> {
|
|
return get<ChainRoles>(`/chains/${encodeURIComponent(chain)}/roles`, signal)
|
|
}
|
|
|
|
/** Block zero, and what it handed out. */
|
|
export function fetchGenesis(chain: string, signal?: AbortSignal): Promise<GenesisDetail> {
|
|
return get<GenesisDetail>(`/chains/${encodeURIComponent(chain)}/genesis`, signal)
|
|
}
|
|
|
|
/** Accounts ranked by what they have been paid, most first. */
|
|
export function fetchAccounts(chain: string, signal?: AbortSignal): Promise<AccountRow[]> {
|
|
return get<AccountRow[]>(`/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<RecentBlock[]> {
|
|
const cursor = before === undefined ? '' : `?before=${before}`
|
|
return get<RecentBlock[]>(`/chains/${encodeURIComponent(chain)}/blocks${cursor}`, signal)
|
|
}
|
|
|
|
/** Every runtime this chain has been seen running, oldest first. */
|
|
export function fetchRuntimes(chain: string, signal?: AbortSignal): Promise<RuntimeSummary[]> {
|
|
return get<RuntimeSummary[]>(`/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<RuntimeDetail> {
|
|
return get<RuntimeDetail>(`/chains/${encodeURIComponent(chain)}/runtimes/${spec}`, signal)
|
|
}
|
|
|
|
/** One miner's standing and history. */
|
|
export function fetchMiner(
|
|
chain: string,
|
|
miner: string,
|
|
windowName: WindowName,
|
|
signal?: AbortSignal,
|
|
): Promise<MinerDetail> {
|
|
return get<MinerDetail>(
|
|
`/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<NodeIndex> {
|
|
return get<NodeIndex>(`/chains/${chain}/nodes`, signal)
|
|
}
|
|
|
|
/** One node, by peer id. */
|
|
export async function fetchNode(
|
|
chain: string,
|
|
peer: string,
|
|
signal?: AbortSignal,
|
|
): Promise<NodeInfo> {
|
|
return get<NodeInfo>(`/chains/${chain}/nodes/${encodeURIComponent(peer)}`, signal)
|
|
}
|
|
|
|
/** High-level network statistics: supply, capability and activity. */
|
|
export async function fetchNetwork(chain: string, signal?: AbortSignal): Promise<NetworkSummary> {
|
|
return get<NetworkSummary>(`/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<Distribution> {
|
|
return get<Distribution>(`/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<WormholeSummary> {
|
|
return get<WormholeSummary>(`/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<VestingSummary> {
|
|
return get<VestingSummary>(`/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<IntentsPrice> {
|
|
return get<IntentsPrice>(`/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<Markets> {
|
|
return get<Markets>(`/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<IntentsSwaps> {
|
|
const cursor = before ? `&before=${encodeURIComponent(before)}` : ''
|
|
return get<IntentsSwaps>(`/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<IntentsSeries> {
|
|
return get<IntentsSeries>(
|
|
`/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<IntentsInventory | null> {
|
|
return get<IntentsInventory | null>(`/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<RefPools | null> {
|
|
return get<RefPools | null>(`/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<RefSwaps> {
|
|
const cursor = before ? `?before=${encodeURIComponent(before)}` : ''
|
|
return get<RefSwaps>(`/chains/${chain}/ref/swaps${cursor}`, signal)
|
|
}
|