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
286 lines
11 KiB
TypeScript
286 lines
11 KiB
TypeScript
/**
|
||
* The asset's Ref Finance DCL pools: what they offer, and what has traded.
|
||
*
|
||
* Kept apart from the NEAR Intents market on purpose. A user who buys through
|
||
* 1Click is filled by a solver (a trade on that market), and the solver then
|
||
* restocks from this pool (a swap here). Both are real, and summing them would
|
||
* count the same demand twice — so they are two markets and never one volume.
|
||
*
|
||
* The pool contract is on a shard our node does not track, so its state is
|
||
* read from a public endpoint and its swaps are found on intents.near's shard
|
||
* and fetched whole. A swap made by calling the pool directly is not read, and
|
||
* the coverage row says exactly how much of the pool's own recorded volume
|
||
* that leaves out (blackbeard/observer#37).
|
||
*/
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { Link } from 'react-router-dom'
|
||
|
||
import type { RefPools } from '../api/generated/RefPools'
|
||
import type { RefSwap } from '../api/generated/RefSwap'
|
||
import type { RefSwaps } from '../api/generated/RefSwaps'
|
||
import { RequestFailed, fetchRefPools, fetchRefSwaps } from '../api/rest'
|
||
import { ago, height as fmtHeight, price as fmtPrice, tokens, when } from '../lib/format'
|
||
import { href } from '../lib/routes'
|
||
import { Field } from './Field'
|
||
|
||
const EXPLORER = 'https://nearblocks.io/txns/'
|
||
const REFRESH_MS = 60_000
|
||
|
||
/** A share as a percentage, or a dash where there is nothing to divide. */
|
||
function pct(part: string, whole: string): string {
|
||
const w = Number(whole)
|
||
if (!w) return '—'
|
||
return `${((Number(part) / w) * 100).toFixed(2)}%`
|
||
}
|
||
|
||
/** How each kind of rung reads in the table. */
|
||
const KIND: Record<string, string> = {
|
||
asks: 'offers the asset',
|
||
bids: 'bids for the asset',
|
||
current: 'the current point',
|
||
}
|
||
|
||
export function RefDclMarket({
|
||
chain,
|
||
decimals,
|
||
symbol,
|
||
}: {
|
||
chain: string
|
||
decimals: number
|
||
symbol: string
|
||
}) {
|
||
const [pools, setPools] = useState<RefPools | null | undefined>(undefined)
|
||
const [swaps, setSwaps] = useState<RefSwaps | null>(null)
|
||
const [older, setOlder] = useState<RefSwap[]>([])
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
useEffect(() => {
|
||
let controller = new AbortController()
|
||
const load = () => {
|
||
controller = new AbortController()
|
||
Promise.all([
|
||
fetchRefPools(chain, controller.signal),
|
||
fetchRefSwaps(chain, null, controller.signal),
|
||
])
|
||
.then(([p, s]) => {
|
||
setPools(p)
|
||
setSwaps(s)
|
||
setError(null)
|
||
})
|
||
.catch((e: unknown) => {
|
||
if (controller.signal.aborted) return
|
||
setError(e instanceof RequestFailed ? e.message : 'Could not reach the observer.')
|
||
})
|
||
}
|
||
setOlder([])
|
||
load()
|
||
const id = window.setInterval(load, REFRESH_MS)
|
||
return () => {
|
||
window.clearInterval(id)
|
||
controller.abort()
|
||
}
|
||
}, [chain])
|
||
|
||
const asset = pools?.asset_symbol ?? swaps?.asset_symbol ?? symbol
|
||
const assetDecimals = pools?.asset_decimals ?? swaps?.asset_decimals ?? decimals
|
||
const cursor = swaps?.next ?? null
|
||
const loadOlder = () => {
|
||
if (!cursor) return
|
||
fetchRefSwaps(chain, cursor)
|
||
.then((page) => {
|
||
setOlder((rows) => [...rows, ...page.swaps])
|
||
setSwaps((s) => (s ? { ...s, next: page.next } : page))
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
const rows = swaps ? [...swaps.swaps, ...older] : []
|
||
|
||
return (
|
||
<>
|
||
<p className="panel-note" style={{ marginBottom: 12 }}>
|
||
<Link to={href({ chain, index: 'market' })}>Markets</Link> › Ref Finance DCL
|
||
</p>
|
||
|
||
{error && <p className="empty">{error}</p>}
|
||
{!error && pools === undefined && <p className="empty">Reading the pools…</p>}
|
||
{!error && pools === null && (
|
||
<p className="empty">
|
||
Not read yet — the first read is a minute after the observer starts.
|
||
</p>
|
||
)}
|
||
{pools && pools.pools.length === 0 && <p className="empty">No Ref DCL pool holds {asset}.</p>}
|
||
|
||
{pools?.pools.map((p) => {
|
||
const counter = p.counter_symbol ?? p.counter_token
|
||
const amount = (raw: string, d: number) => tokens(raw, d)
|
||
return (
|
||
<section key={p.pool_id} className="panel" style={{ marginBottom: 26 }}>
|
||
<div className="panel-head">
|
||
<h2 className="panel-title">
|
||
{asset}/{counter} pool
|
||
</h2>
|
||
<span className="eyebrow">
|
||
{p.contract} · {(p.fee / 10_000).toFixed(2)}% fee · {p.state} · read{' '}
|
||
{ago(pools.checked_at)}
|
||
</span>
|
||
</div>
|
||
<div className="node-grid">
|
||
<Field
|
||
label="Pool price"
|
||
value={`${fmtPrice(p.price)} ${counter}`}
|
||
note="where the next swap starts — not a trade"
|
||
/>
|
||
<Field
|
||
label={`${asset} in the pool`}
|
||
value={`${amount(p.asset_held, assetDecimals)} ${asset}`}
|
||
note="what buyers can take"
|
||
/>
|
||
<Field
|
||
label={`${counter} in the pool`}
|
||
value={`${tokens(p.counter_held, 6)} ${counter}`}
|
||
note="what sellers can be paid, and LP fees"
|
||
title={`${p.counter_held} base units`}
|
||
/>
|
||
<Field
|
||
label="Swaps indexed"
|
||
value={fmtHeight(p.indexed_swaps)}
|
||
note={`${pct(p.indexed_asset_out, p.asset_out)} of ${asset} bought out, ${pct(p.indexed_asset_in, p.asset_in)} sold in`}
|
||
/>
|
||
</div>
|
||
<div className="scroll-x">
|
||
<table className="board">
|
||
<caption className="visually-hidden">The pool's liquidity ladder</caption>
|
||
<thead>
|
||
<tr>
|
||
<th scope="col" className="left">
|
||
Price of {asset}
|
||
</th>
|
||
<th scope="col" className="left">
|
||
This range
|
||
</th>
|
||
<th scope="col">{asset}</th>
|
||
<th scope="col">{counter}</th>
|
||
<th scope="col">
|
||
{counter} to take all its {asset}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{p.rungs.map((r) => (
|
||
<tr key={`${r.price_from}-${r.price_to}-${r.kind}`}>
|
||
<td className="left numeral">
|
||
{r.kind === 'current'
|
||
? fmtPrice(r.price_from)
|
||
: `${fmtPrice(r.price_from)} – ${fmtPrice(r.price_to)}`}
|
||
</td>
|
||
<td className="left">{KIND[r.kind] ?? r.kind}</td>
|
||
<td className="numeral">
|
||
{r.asset > 0
|
||
? r.asset.toLocaleString('en-US', { maximumFractionDigits: 3 })
|
||
: '—'}
|
||
</td>
|
||
<td className="numeral">
|
||
{r.counter > 0
|
||
? r.counter.toLocaleString('en-US', { maximumFractionDigits: 4 })
|
||
: '—'}
|
||
</td>
|
||
<td className="numeral">
|
||
{r.counter_to_clear > 0
|
||
? r.counter_to_clear.toLocaleString('en-US', {
|
||
maximumFractionDigits: 0,
|
||
})
|
||
: '—'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p className="panel-note">
|
||
The ladder is the pool's liquidity, range by range: what it holds at each price
|
||
and what it would take to clear it. A range above the current price offers {asset};
|
||
one below it bids for {asset}. These are standing amounts, not trades — and{' '}
|
||
<strong>
|
||
“swaps indexed” is measured against the pool's own cumulative counters
|
||
</strong>
|
||
: anything under 100% is volume made by calling the pool directly, which this observer
|
||
does not read. Accounts allowed to swap here: {p.whitelist.join(', ')}.
|
||
</p>
|
||
</section>
|
||
)
|
||
})}
|
||
|
||
{pools && pools.pools.length > 0 && (
|
||
<section className="panel" style={{ marginBottom: 26 }}>
|
||
<div className="panel-head">
|
||
<h2 className="panel-title">Pool swaps</h2>
|
||
<span className="eyebrow">newest first</span>
|
||
</div>
|
||
{rows.length === 0 ? (
|
||
<p className="empty">No swap in these pools has been indexed yet.</p>
|
||
) : (
|
||
<div className="scroll-x">
|
||
<table className="board">
|
||
<caption className="visually-hidden">Swaps in the pools, newest first</caption>
|
||
<thead>
|
||
<tr>
|
||
<th scope="col" className="left">
|
||
Settled
|
||
</th>
|
||
<th scope="col" className="left">
|
||
Solver
|
||
</th>
|
||
<th scope="col" className="left">
|
||
Side
|
||
</th>
|
||
<th scope="col">{asset}</th>
|
||
<th scope="col">Price</th>
|
||
<th scope="col" className="left">
|
||
Tx
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.map((s) => (
|
||
<tr key={`${s.tx_hash}-${s.near_height}-${s.asset_amount}`}>
|
||
<td className="left numeral" title={s.at}>
|
||
{when(s.at)}
|
||
</td>
|
||
<td className="left">{s.trader}</td>
|
||
<td className="left">{s.side === 'buy' ? 'bought' : 'sold'}</td>
|
||
<td className="numeral">
|
||
{tokens(s.asset_amount, assetDecimals)} {asset}
|
||
</td>
|
||
<td className="numeral">{fmtPrice(s.price)}</td>
|
||
<td className="left">
|
||
<a href={`${EXPLORER}${s.tx_hash}`} target="_blank" rel="noreferrer">
|
||
{s.tx_hash.slice(0, 8)}…
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
{cursor && (
|
||
<div className="pager">
|
||
<button className="segmented panel-button" onClick={loadOlder}>
|
||
Older →
|
||
</button>
|
||
</div>
|
||
)}
|
||
<p className="panel-note">
|
||
A pool swap is a solver trading with the pool — typically restocking after filling a
|
||
user on NEAR Intents, which is why these are not added to that market's trades. The
|
||
price includes the pool's fee. The solver is named from its own intent; the pool
|
||
itself records the swapper as intents.near.
|
||
</p>
|
||
<p className="panel-note">What is read: {pools.reads}</p>
|
||
</section>
|
||
)}
|
||
</>
|
||
)
|
||
}
|