Files
observer/web/src/api/rest.ts
T
grenadeandClaude Opus 5.5 b85e74f1ca
ci / gate (push) Successful in 5m7s
deploy / validate (push) Skipped
deploy / build (push) Successful in 9m3s
deploy / deploy-api (push) Successful in 42s
deploy / deploy-web (push) Successful in 12s
feat: index QTC's Ref DCL pool — its swaps, its ladder, and how much we see
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
2026-10-06 09:50:34 +03:00

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)
}