b3dc4ef4f693fbdb6a3005b4f56afedc85bdad13
129
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b3dc4ef4f6
|
feat: list Ref's classic exchange for pools holding the asset
#39 item 2. Every pool on v2.ref-finance.near is listed every ten minutes — nine thousand-pool pages, three seconds on quadbrat, which tracks the exchange's shard and is asked first while at the tip — and each pool naming the asset is reported on the Ref market page, empty or not. Today that is pool 8900, the empty QTC/QTEST test pool #39 found by hand. A listing that fails part way publishes nothing. The exchange's own balance of the asset joins the supply reconciliation as a named holder, so a venue opening there is never "unaccounted". Refs #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
6152ba85c2
|
fix(api): queue only direct pool calls about the asset, and re-read the rest
The direct-call scan matched any transaction reaching the DCL contract, which hosts hundreds of pools: 4,881 strangers' swaps queued in the first hour after launch, from 176 signers, each to be fetched from a rate-limited public endpoint — enough on its own to keep the market page starved. A call now counts only when it moves the asset itself or its arguments name the asset's contract, which every pool id holding it does; a buy with any token names the pool, a sell is a transfer of the asset. ref_pending keeps no arguments, so the bad entries cannot be filtered in place: migration 0016 drops every entry not signed by the relay and the scan's cursor, and the scan reads the span again from our node. Refs #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
6116954f13
|
feat: QTC into and out of NEAR Intents, block by block
#39 item 4, as replaced: the original check (intents.near's balance against indexed events) could not see the trades it was for, because a trade moves QTC inside intents.near and leaves its total alone. What the balance does move with is deposits and withdrawals, so that is what is recorded. `intentsflow` reads intents.near's `ft_balance_of` on qtc.omft.near at every NEAR block from gargravarr, which tracks the token's shard, forward to its final block and back to the oldest it holds, and records each change (migration 0015). `view_at` treats only UNKNOWN_BLOCK as a skipped height: a garbage-collected block is an error, never "nothing changed". `/v1/chains/{chain}/intents/flows` sums deposits and withdrawals over a span; the NEAR Intents market page shows it after the bridge. Verified on the live chain through a tunnel to gargravarr: the scan's balance at #218,805,000 (36,044.097 QTC) and at #218,811,064 (72,671.542) match direct reads, and across 1,044 rows every balance less its change is the previous balance. Refs #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
0851b244ba
|
fix(api): read the pools already known while discovery is rate-limited
After the last deploy every pool listing came back FastNEAR 429 for twenty minutes, and with nothing discovered nothing was read: the market page had no ladder at all. The pools swaps have been recorded in certainly hold the asset, so the loop now starts from those and reads them while discovery retries; discovery still finds any pool nobody has swapped in. Refs #37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
5361dec713
|
feat: record the Ref pool's bid side over time
Each pool read now writes a snapshot — totals, swap volumes, and what
selling 10, 100, 1,000 and 5,000 of the asset would fetch from the
pool's own `quote` view — and the order book whenever it changed
(migration 0014). `/v1/chains/{chain}/ref/bids` turns that into the
series #41 asks for: posted orders, money held otherwise, net swapped
in, the quotes; the book now against N hours ago; and every level
change, classified by `dcl::level_changes` — a shrink the pool's payout
in the same minute accounts for is "taken by a swap" (consistent with
one, not proof), the rest "withdrawn", allocated nearest the price
first. The `ref-dcl` page shows it as four one-hue sparkline tiles and
two tables, never one chart in several colours.
Also records in CLAUDE.md why three deploys were cancelled: Gitea
cancels runs by commit when a branch at that commit is deleted.
Refs #41
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
|
||
|
|
1d25689e3d
|
feat(data): ask our own NEAR node for view calls while it is at the tip
FastNEAR kept answering 429 after the pacer shipped: its limit is per address, and every host behind the office IP shares it. gargravarr tracks the pool's and the token's shards, so `pool_rpc_urls` now also serves view calls first — pool state, order book, quotes, supply, balances — but only while its newest block is under two minutes old and it is not syncing, re-checked every 30 s. Behind the tip it would answer `final` from its own final block: on 2026-10-06, 11,536 blocks behind, it gave qtc.omft.near's supply as of three hours earlier, which would have replaced "unreadable" with a figure that looked current and was not. Any call it cannot answer falls back to the public endpoint. Refs #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
26dfdc956c
|
fix: pace public NEAR view calls, and never publish "no pools" from a failed read
From 17:32 every Ref pool read came back FastNEAR 429, minute after minute: a restart fires discovery pages, pool and order-book views, supply and balance reads and inventory at once, from one office address, against an endpoint measured at about two requests a second sustained. Every call to a public view endpoint now passes one process-wide gate, 600 ms apart. Worse than the outage, the pool loop read an empty pool list after a failed discovery and published it, so the market page said no Ref DCL pool holds QTC — "could not list" turned into "there are none". The slot now stays unread until a discovery has succeeded; one that succeeds and finds none is still published as none. Refs #37, #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
1afb99503b
|
fix(bridge): alarm on a shortfall only when both figures describe one moment
ci / gate (push) Successful in 4m20s
A deposit locks in custody before NEAR mints, and a withdrawal burns on NEAR before custody releases, so between a movement and the next NEAR supply read (every five minutes) `locked_less_minted` can sit either side of zero with nothing wrong. `gap_settled` says whether the supply was read at least a minute after the newest custody movement; the panel raises its alert only then, and explains a transient gap instead of alarming on it. Refs #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
8eb812a19a
|
feat: read the NEAR bridge from the Quantus side — custody against minted
`/v1/chains/{chain}/bridge` sets the bridge's custody account against
qtc.omft.near's supply on NEAR, and lists every movement through custody,
newest first and paged. The bridge's two accounts, custody and the fee payer
that funds each deposit, are named in `[chains.bridge]` with the evidence
beside them and validated as addresses at startup; neither is inferred.
`blackbeard_core::bridge::classify` tells a deposit by who and when: a
transfer into custody from an address the fee payer funded at most
`max_sweep_lag` blocks before (every mainnet deposit so far: one). Not by
amount — the funding varies (0.0010166025, 0.0010169025 before the 150k
sweep, 0.0010168025 before the 5,990) — and not by one-time addresses,
which #39 assumed: one address deposited three times. Anything else into
custody is an other inflow; locked is custody's balance less those, which
derives the 5 QTC seed instead of configuring it. Tested against every real
transfer on both accounts.
Each transfer is counted once. Every ordinary transfer emits both
`Balances::Transfer` and `Wormhole::NativeTransferred`; the query takes the
first, plus the second only for a hidden-sender exit.
Verified end to end against the live chain (custody balance read from bob)
and NEAR (the listing's supply): locked 155,994.701, minted 155,994.698,
locked − minted +0.003 QTC; 13 deposits from 11 addresses, 3 withdrawals.
The market page shows it beside the backing ceiling, reporting flows and
saying what they cannot establish: an address is not a person, and summed
flows are only the whole history if the index covers it.
Refs #39
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
|
||
|
|
9becf2629e
|
fix(data): count each visible transfer once, not once per pallet
ci / gate (push) Successful in 4m29s
Every visible transfer emits `Balances::Transfer` and `Wormhole::NativeTransferred` with the same payload in the same extrinsic (193,686 of 193,691 on mainnet), and `account_flows`, `flows_for` and `wormhole_routes` added the two: every account page's sent and received, the distribution table's `sent on`, and the wormhole page's visible routes were doubled. A `Balances::Transfer` now counts only when it has no such twin, which keeps the five that stand alone. Checked on production data: the bridge custody account's `received` goes from 372,003.992 over 30 receipts to 186,001.996 over 15. Closes #42 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
b55d323456
|
fix: no unaccounted figure until the whitelists are known; no mining row on market pages
The listing pass runs at startup, a minute before the first pool read, so its named set lacked every whitelisted account and it reported the sum as complete: production read 0.3695 QTC unaccounted against a true 0.269 — exactly liq.selfevident.near's 0.1005 — until the next pass. Absent now until the whitelists have been read. The market pages drop the hashrate/difficulty/block time/miners/height row, as the wormhole page already did: they are about the token on another chain, and mining figures above them read as though they bore on a price. Keyed on `sectionOf`, so the index and every market page agree. Refs #39, #41 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
35c7a2cf97
|
fix: show the Ref pool's limit-order bids in its ladder
The ladder was built from `get_liquidity_range` alone, so no limit order appeared in it: with 109,988 USDC posted in ten bid levels from $20 down to $0.50, it showed about 1,001 USDC of bids and told a QTC seller there was almost nothing to sell into. `get_pointorder_range` is now read on the same pass; each order becomes a one-point rung (`dcl::order_rungs`, filled at its own point's price, tested against #41's hand-worked levels), and every rung carries its `source` — liquidity, or an order its owner can cancel at any moment — and `asset_to_clear`, the QTC it absorbs. Orders plus liquidity reconcile to the pool's `total_x` less its uncollected fees (110,989.60 of 110,991.62 USDC, read 2026-10-06). Refs #41 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
016c0424cc
|
feat: reconcile QTC's NEAR supply against every holder we name
The listing pass now reads `ft_balance_of` for intents.near, each Ref DCL pool contract and every account on a pool's whitelist, and reports `unaccounted` — supply less all of them — beside the backing ceiling. It is #39's first blind spot: nothing noticed QTC moving to an account or venue this observer does not name. Absent unless every balance was read, because a partial sum overstates it while looking exact. Checked by hand against the chain before shipping: 0.269 QTC, the figure #39 reconciled this morning. The price response's `reads` now says Confidential Intents settle on NEAR's private shard and are in no figure here (#39 item 6). Refs #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
e78e627152
|
feat(api): read direct calls to the asset's pools on the shards they are signed on
The relay's pool swaps are found on intents.near's shard; a whitelisted account calling the pool itself signs on its own shard, which nothing read — #37's coverage figure measured the gap at 99.75% of buys. `poolscan` follows the pools' and the token's shards on gargravarr (`pool_rpc_urls`), which tracks shards 4 and 8, and queues every transaction that calls a pool or `ft_transfer_call`s tokens into one. `refdcl::fetch` reads each whole, as it does the relay's, so a swap is parsed one way and recorded once whichever road it came by. Reads only what the node holds; history before it synced stays the coverage figure's to account for. Records the three-node shard split and the Prometheus neard scrape config (installed on golgafrinchans) in the repo. Refs #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
c483d51dc7
|
fix(web): mark a section's nav link on its subjects' pages, not only its index
`/quantus/market/near-intents` marked nothing in the section nav, nor did a block, account or runtime page: the nav tested the index plus a hand-kept list of exceptions that every newer section had missed. `sectionOf` in lib/routes.ts now answers it once, over every subject field. Block zero stays with Genesis alone. Closes #40 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
7cd2197997
|
feat: chart the Ref DCL pool's swap price on its own market page
`/v1/chains/{chain}/ref/series?counter=nep141:…` buckets the latest pool
swaps exactly as `/intents/series` buckets intents trades, through one shared
`price_series`, and `ref-dcl` draws it with the same plot and figures table
(`SeriesBody`, pulled out of `PriceChart`). It is a separate chart, never a
second line on the intents one: a pool swap is usually a solver restocking
after an intents fill, and one line over both would draw one demand twice.
`RefPool::counter_token` was documented as a contract but carries the
`nep141:` prefix; the doc now says so — the first cut of the chart prefixed
it again and drew nothing.
Tracking shard 8 (option B), the other half left open on #37, is item 5 of
#39 and waits for the new NEAR host.
Closes #37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
|
||
|
|
f6b1e8ad14
|
test(web): assert dates and hours, not one ICU build's word order
The runner's Node carries English-only ICU and formats en-GB as en-US
("Oct 5", "EDT"); browsers carry full ICU. What #38 guards is which day
and which hour, so that is what the test checks.
Refs #38
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
|
||
|
|
21ec54bd24
|
fix(web): label local times with their zone and keep UTC days on their own date
ci / gate (push) Failing after 4m31s
Instants (blocks, swaps, node starts) render in the viewer's zone and say which: tables carry the zone, charts name it once, titles give UTC. Daily buckets the API cut at UTC midnight render in UTC and are headed "(UTC)" — FlowChart formatted them as local dates, putting every bar a day early anywhere west of Greenwich. All date formatting goes through lib/format.ts (when, utc, clock, utcDay, utcMonth, zone), one locale and a 24-hour clock. `pnpm test` runs Node's built-in runner under America/New_York, where the old formatter turns 5 Oct into 4 Oct, and is part of the shared CI gate. Closes #38 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
b85e74f1ca
|
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 |
||
|
|
d6654ac4b0
|
feat(api): flag QTC on NEAR that exceeds what exists outside the vesting pot
qtc.omft.near is minted only against QTC locked on Quantus by the bridge, so its supply can never legitimately exceed issuance outside the vesting pot — 221,838 QTC today. The listing poll now reads that ceiling every five minutes through one helper, issuance_outside_pot, using the same storage reads and the same subtraction the network page uses, rescales it to the NEAR token's decimals (rounding down, so the check can only get stricter), and compares it with ft_total_supply. A breach is a logged warning and, on the market page, a sentence before any figure: unbacked, or one of the two misread. Either read failing gives no verdict rather than a false all-clear. The panel also shows the ceiling itself as "most there could be". Checked live: QTC at 0 against 221,837.97 with no breach, and the breach path forced by pointing the dev config at wNEAR (35.4M on NEAR), which logged and rendered the warning as intended. In place before QTC's announced NEAR Intents launch on 2026-10-05. Closes #36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
a13a6ef1a5
|
fix(api): pace the intents backfill only for someone else's endpoint
The one-second pause between backfill blocks was set when every read went to FastNEAR's free RPC or its archive. Since #27 was enabled the live endpoint is our own node and start_height the oldest block it holds, so the whole backfill was served by a host we run and the pause only stretched it to about six hours — stalling the tip side for each descending batch, so live pushes were missed meanwhile. The pause now follows who serves the read: heights at or above the live endpoint's earliest block go to rpc_urls, older ones to the archive, and only a set that is not ours is paced. Ours is decided from the URL host — a `.internal` name, localhost, or a loopback, private, link-local or unique-local address — and a set counts as ours only when every endpoint in it is, since failover can reach any of them. Measured against near.internal: the backfill walked about 15,000 blocks in 60 s (≈250/s, up from 1/s) while the top end kept pace with the tip. Closes #35 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
a07a3bc580
|
fix(api): sort solver inventory with sort_by_key
The lint the previous commit was pushed with; caught before CI ran. Refs #34 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
2d80325c3b
|
feat(api): solver inventory per side of each pair, read from our node
ci / gate (push) Failing after 1m34s
NEAR Intents has no order book: solvers quote privately per request and nothing on chain carries a price level. What is on chain is the inventory they fill from, inside intents.near — and intents.near is on the shard our node tracks, so mt_batch_balance_of is answered locally in about 40 ms. The scan now records every solver it sees filling any pair — a named account with no referral, which the #30 survey found to be solvers without exception — in intents_solver (migration 0011), so the list survives restarts and is there before QTC has a trade of its own. Once a minute the observer asks intents.near what each holds of the asset and every counter traded against it, and serves it at /intents/inventory: the asset as the side that serves buyers, each counter as the side that serves sellers, with how many solvers hold it and the largest holder. The market page shows it as a table, the asset and the ten counters most solvers hold with the rest folded, under a note that says what it is not: no price levels, shared across pairs so the rows do not add up, and not a ceiling. Checked live against our node with wNEAR configured: 11 solvers, 74 tokens read in one pass. Before QTC is listed the panel's job is its one row — when solvers start stocking QTC, it shows here before the first trade. Closes #34 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
fb7148cc98
|
feat(ui): a price chart on the market page, and its placeholder
The asset's price in one counter token over its latest trades, read against the dataviz skill before it was drawn. One series in the data hue and no legend: a token trades against dozens of counters, in units no axis could share, so the counter is a control above the chart rather than a hue per line. Buckets come from blackbeard_core::intents::bucket_prices over /intents/series — volume-weighted, with low and high, empty buckets left out so the line breaks where nothing traded instead of drawing a price nobody paid. The y-axis is fitted to the prices; the window is a count of trades, so the time axis covers only what they span. A table view sits under it. Until anything has traded the frame keeps the chart's height — nothing moves when the first trade lands — with a baseline and the reason, and no axes, since a scale over no data is a range nobody measured. For QTC today that reads "no QTC is bridged to NEAR yet, so none can have traded there". Rendering it caught three things the build did not. The line never drew: the API reported the bucket width truncated to whole seconds, so no two buckets looked adjacent — widths are now whole seconds by construction. The chart painted at its 720px fallback until a ResizeObserver reported, centred in a full-width panel; it is measured before first paint now. And on the market page the seventy-row counter table pushed the chart below the fold; it folds. Checked against real wNEAR trades and the QTC placeholder, light and dark. Closes #33 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
5d22137a43
|
feat(ui): a market route — /:chain/market and /:chain/market/near-intents
The market index lists the venues a chain's token is followed on, asked of a
new /v1/chains/{chain}/markets so a chain followed nowhere says so and a
second venue needs no frontend change. The near-intents page carries the
price figures and listing evidence, swaps as they settle (from the
IntentsSwap push), and the settled history, paged newest first through
/intents/swaps?order=newest&before=… — the existing `after` stays for readers
going forward, and mixing the two directions is a 400.
`market` is a subject in lib/routes.ts like `node`: in parse, href and
isStandings together. `markets` parses too and is rewritten to the singular
every other kind uses.
Rendered against 964 real wNEAR trades before it shipped, which is what
showed the panel did not scale: seventy counter tokens became seventy tiles,
NFT swaps sat in the grid as prices with no number, and "wNEAR in USDC"
appeared six times for six different bridged tokens. Now only counters with
decimals are prices, most traded first — four tiles on the network page,
eight and a table of the rest on the market page — NFT and multi-token swaps
are a count, and a bridged token is labelled by its origin (`USDC · base`),
the contract it is, not the symbol it chose.
Closes #32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
|
||
|
|
ef2afa8e4e
|
fix(data): a skipped NEAR height no longer stalls the intents scan
NEAR does not produce a block at every height, and 218,363,444 was one it skipped. The backfill asked for its shard layout at the batch's first height, got UNKNOWN_BLOCK from every endpoint, and asked the same question every 15 s from then on: production's indexed_from stopped at 218,363,445. The tip side runs the same code and would have frozen the first time a batch began on a skip. The layout now comes from the first height in the batch that exists. The second half of it: neard sends JSON-RPC errors with HTTP 422 where FastNEAR's proxy sent them with 200, and call_one checked the status before reading the body, so the node's own "no such block" arrived as a transport failure. chunk() could not see a skip as a skip, deep() never asked the archive, and on a chain with several endpoints a node's valid answer would have been failed over. parse_response reads the body whatever the status: a JSON-RPC error is the node answering; a non-success status without one is the host not answering, as before. Checked against near.internal with the scan seeded to begin a backfill batch on the skipped height: it walked past it with no warnings. Closes #31 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
40bc2f67d1
|
fix(core): one swap per taker leg, and solver hops are never trades
A 3,000-block survey of intents.near from our own node (350 outcomes, 1,026 legs) showed the referral leg is the user — per-quote implicit accounts, none on a solver — and that its own diff mirrors the rest of the outcome to within the protocol fee in all 576 amounts checked. So each taker leg is now its own trade, priced from what that user gave and got. It also showed the pooling rule counted solver routing as trading: in 45% of outcomes with a taker the solvers traded through a pair no taker touched, usually USDT on Ethereum, and each such leg on the tracked asset became a phantom trade against a token the user never saw. Two takers on one pair — an account round-tripping wNEAR, in the case captured — were merged into one trade with a nonsense price. Now an outcome with any taker yields only the takers' trades; only an outcome with no referral at all (solvers trading with each other) is pooled, as `kind: unattributed`, with no side. `kind` is stored (migration 0010, no re-scan: production held no QTC rows) and on the wire, and the panel says how many of a figure's trades were users'. Tests run on the three captured outcomes. Checked live against near.internal with wNEAR: 18 trades from 18 outcomes, every taker an implicit account, and no counter that was only a hop. Closes #30 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
ec615cac52
|
ci: gate every branch, and make a "validate" dispatch ship nothing
deploy.yaml was the only workflow and ran only on main, so a pushed branch started no run at all: the first time CI saw a change was the push that deployed it. And its `mode: validate` input, offered as "check only", was read by no job — a validate dispatch built and deployed whatever ref it ran on. ci.yaml now runs on every branch but main, on the `rust` runner, with no deploy job, no secrets and no artifact. The gate itself moves into a local composite action, .gitea/actions/gate, which both ci.yaml and deploy.yaml's `build` job use, so the gate a branch passes is the gate main runs rather than a copy that can drift. Its commands are CLAUDE.md's "Run the gate CI runs" verbatim. One deliberate change on the way: the drift check is `git status --porcelain`, as CLAUDE.md and #28 state it, not `git diff --quiet`, which cannot see the new untracked file a new DTO generates. Both deploy jobs now require `github.ref == 'refs/heads/main'` and `inputs.mode != 'validate'` (empty on a push, so the push path and its "what changed" gating are unchanged). A validate dispatch on main runs the build and gate plus a new `validate` job: the read-only checks from the end of deploy-api and deploy-web — API health from bob, reachability from the edge, the chain list through the vhost — against what is already deployed. Those checks are copied rather than shared, because the deploy jobs run from the build artifact without a checkout and restructuring them is a change only a production deploy would test; the comment says they must change together. Closes #28 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
b05b208dfa
|
feat(config): follow QTC on NEAR Intents through our own node
Enables [chains.intents] for quantus: asset nep141:qtc.omft.near, read from https://near.internal (neard on frankie, tracking intents.near's shard), with view calls left on FastNEAR. start_height is the oldest block our node holds, 218,346,736 — a sound floor because qtc.omft.near held no supply at 218,369,028, so nothing can have traded before it, and it keeps the whole backfill on our node rather than on a throttled archive. The gate test that kept this off becomes a parse test that expects it on. Closes #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
cf227bf421
|
fix(data): read tx outcomes at INCLUDED_FINAL, not EXECUTED
Our NEAR node tracks only intents.near's shard. Against a swap that withdrew through omni.bridge.near, EXECUTED and EXECUTED_OPTIMISTIC both waited for the receipts on shards the node never sees and timed out with a 408 after ten seconds — the scan would have retried that block forever, stalling on exactly the trades it exists to read. INCLUDED_FINAL answered in 10 ms with all five token_diff events and the intents_executed event, matching FastNEAR's full view, because intents.near's first receipt executes in the inclusion block. Verified end to end against https://near.internal with wNEAR configured: the scan followed the tip with no errors, recorded trades and pushed one live. Refs #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
100d508588
|
feat(ws): push each swap as it settles on NEAR Intents
`ServerMessage::IntentsSwap` carries the trade exactly as /intents/swaps returns it — one conversion, `intents::to_swap`, serves both, so the route and the push cannot describe the same trade differently. Only trades read within LIVE_WITHIN_BLOCKS (~a minute) of NEAR's final tip are pushed. The backfill never broadcasts, and the threshold is what keeps an upward catch-up after a restart from announcing an hour of history as news — the confusion the wormhole pulse shipped once already. The market panel re-reads its figures on each push rather than adjusting a copy: a VWAP over the latest N trades drops its oldest as it gains one. Verified by replaying the trade at NEAR 218,261,895 with the threshold opened locally and catching the frame on /v1/ws. Refs #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
0d5ebd19ee
|
feat(ui): a market panel on the network page, from settled swaps only
Shows the token's price on NEAR Intents per counter token, volume-weighted over its latest trades with the trade count beside every figure, and in that token rather than in dollars. Before anything has settled it shows the evidence instead: supply bridged to NEAR, what intents.near holds, whether 1Click lists it, and zero swaps over the NEAR blocks read — so a rate quoted elsewhere has something true to stand against. Renders nothing when the chain answers `no_intents`, which it does while the integration is off. The asset is labelled by its own ft_metadata symbol, returned as `asset_symbol`, rather than assumed to be the chain's token: checked by rendering it with ZEC configured, where the first cut called 143,730 ZEC "QTC". Refs #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
d40e17bb1f
|
docs(config): point the intents example at near.internal
Refs #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
d2567e5584
|
fix(data): send NEAR view calls to their own endpoints
The node the scan will follow tracks only intents.near's shard, and the token contracts it asks about — qtc.omft.near for supply, every counter token for ft_metadata — live on other shards, which such a node cannot answer for. Views are a few an hour, so they go to `view_urls`, FastNEAR by default, and the per-block reads stay on `rpc_urls`. Refs #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
8128201ad9
|
feat(api): index swaps settled by NEAR Intents, off by default
The price of a token on NEAR Intents is the ratio inside a trade that
`intents.near` actually executed; there is no pool, oracle or stored price,
and a 1Click quote is an offer. This reads `execute_intents` transactions on
the contract's shard, keeps the `token_diff` legs whose receipt succeeded and
whose intent is in `intents_executed`, and records one trade per asset/counter
pair per outcome, counting each unit once whichever side the taker was on.
- core: event parsing, trade building, shard lookup, price and VWAP, tested
against the logs of mainnet tx 85uy9uCF (USDT on Tron for ZEC).
- data: a NEAR client over the existing RpcClient, live then archival;
migration 0009 (intents_trade, intents_scan, near_token).
- api: a contiguous-cursor scan and a supply/listing poll per chain, and
GET /v1/chains/{chain}/intents/{swaps,price}. Prices are in counter-token
units; no USD until a stablecoin is named by contract.
Gated on `[chains.intents]`, which the config template leaves commented out:
FastNEAR's free RPC sustains about two requests a second against ~1.7 needed
to follow the tip, and neardata allows 30 blocks a minute. It waits for our
own node. A test renders the template and fails if the block is enabled.
Verified end to end against mainnet with ZEC configured: the trade at NEAR
218,261,895 was recorded and served by both routes.
Refs #27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
|
||
|
|
dc68fe3be3
|
infra: publish the NEAR node as near.blackbeard.observer
Through the existing nodes role: a Cloudflare CNAME to the site indirection, a Let's Encrypt cert by DNS-01 on oolon, and a vhost beside the Quantus and Planck endpoints. The upstream is https://near.internal rather than frankie, so next week's move to new hardware changes one internal vhost and nothing here, and frankie's firewall still admits only the hanzalova proxy and bob. Throttled harder than the Quantus endpoint, because the node is a four-core stopgap and the swap indexer reads it too (via near.internal, outside these limits): 5 r/s per client with a burst of 20, 8 connections per client, and a 30 r/s ceiling across all clients so many addresses together cannot starve it. Only POST / and GET /status, /health pass; bodies over 64 KiB are refused. Hetzner and OVH hosting networks get the site's 403, and the name has its own JSON log. Checked from outside after install: 405 for other methods and paths, 413 for a 100 KB body, 32 of 40 parallel requests answered 429, and the log carries the real client address. /health answers 503 because the node is not synced; that is neard's own answer, passed through. Closes #29 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
0de203c819
|
fix(ui): link the repositories at their new paths
Both moved from quantus/ to blackbeard/: the observer to blackbeard/observer, the miner to blackbeard/miner. Gitea 301s the old paths, but a redirect survives only until something else takes the name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4Ldjix2ofMdQ7dzqgnYA8 |
||
|
|
6c79718ff1
|
docs: issues live at blackbeard/observer
The repository moved from quantus/blackbeard.observer. Gitea redirects the web URL, but the MCP issue API does not follow it, so filing #24 against the old path failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4Ldjix2ofMdQ7dzqgnYA8 |
||
|
|
6d8c3fe3ea
|
infra: a dedicated access log for the public vhost, and deny hosting networks
The proxy's shared access.log mixes every site on oolon and records neither Host nor Origin. Origin is the only field that separates our UI's WebSocket from another site's, because browsers send no Referer on the upgrade. The public vhost now writes its own JSON-lines log carrying both. Ten days of the shared log showed no third-party site using the API, and two headless pollers: an OVH host polling summary once a minute (88% of all no-referer REST) and a Hetzner host identifying as `stardust-devops`. The public vhost now refuses the hosting networks of both providers, from the prefixes RIPEstat reports as announced by AS24940, AS213230, AS212317 and AS16276, generated by script/deny-networks.py and committed so the list is reviewed. AS35540 is OVH's residential broadband and is left out. Against the same ten days, the list matches the two pollers and no visitor. Refusals are a plain-text 403 and are still logged with "denied":1, so a VPN user caught by it shows up rather than disappearing. Closes #24 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4Ldjix2ofMdQ7dzqgnYA8 |
||
|
|
501a9e6ad4
|
fix(data): re-read event history under the current keep policy
Until
|
||
|
|
82a4ac9c59
|
feat(api): wormhole deposits by recipient
GET /v1/chains/{chain}/wormhole/deposits?to=<addr>,…&after=<h>:<i>&limit=<n>
Every Wormhole::NativeTransferred to up to 100 addresses (SS58 or 0x hex),
oldest first, with a height:event_index cursor. A deposit carries its
recipient, its amount as a decimal string, its transfer_count and its
leaf_index: what a wallet needs to work out what it can still spend.
A wormhole address's System::Account balance is a receipt total. An exit
credits the exit account and records a nullifier, and never debits the
wormhole address. A deposit is spent when the nullifier its secret and
transfer_count derive is used. The wallet computes and checks that itself;
nothing here asks for nullifiers, because naming one pairs a deposit with its
exit.
This is for the Quantus extension's wormhole balance (quantus/extension#14),
which would otherwise ask Quantus's Subsquid and tell Quantus which addresses
are one person's.
- The query reads chain_event through the accounts GIN index, then keeps only
rows whose `to` is one of the addresses: accounts holds the sender too, and
a transfer from a wormhole address is not a deposit to it.
- The response carries indexed_from/indexed_to. On a chain still backfilling,
a client can't take the sum as a balance without checking it against
Wormhole::TransferCount.
- CORS on this route alone allows any origin, GET only, merged outside the
site's allowlist so that allowlist can never answer a preflight for it.
Extension origins can't be listed (Chrome ids vary by install, Firefox's are
random per install), and the data is public.
The integration test covers the ways this is silently wrong: a deposit from an
address counted as one to it, another event kind leaking in, a cursor that
skips a same-block sibling, and another chain's rows. Unit tests cover both
address spellings, the cursor, and u128 amounts written as strings.
Closes #22
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
|
||
|
|
54db950957
|
infra: public node endpoints quantus. and planck.blackbeard.observer
One name per chain for the blackbeard wallet (blackbeard/wallet #53), on the same edge proxy as the site: a CNAME to the site indirection, a Let's Encrypt certificate, and a vhost that proxies JSON-RPC over HTTP and WebSocket to the chain's upstream. quantus_node is our node on bob across the mesh (bob's firewalld now admits oolon by name, lair/quantus deploy); planck_node is Quantus's two official testnet hosts over TLS with SNI and verification, since we run no testnet node. Upstreams and the per-client throttle zones (20 requests a second with a burst of 40, 16 concurrent connections, 429 on excess) live in conf.d/blackbeard-nodes.conf so each vhost stands alone. script/infra-setup.sh gains a nodes role that does the three steps per name, enables the vhosts only after nginx -t passes and disables them again if it does not, and ends by asking each name for system_chain. Run today: both names carry certificates to 2026-12-15 and Planck answers; the quantus record was still propagating when the script asked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW |
||
|
|
efaf48711c
|
feat(ui): a caution mark where the tagline's bullet was
The masthead's separator between "quantus mining" and "this is sparta" is now the caution mark rather than a middle dot, scaled to the line it sits on. `.mark-tagline` gains an explicit `line-height` for that reason: the image is sized from it, and a line height inherited from `body` would quietly resize the mark the day the body's changed. The height is declared twice — `1.4em` then `1lh` — which is the same number here precisely because the line height above is stated, so a browser that does not know the unit lands in the same place rather than near it. `alt` is empty and `aria-hidden` is set, on purpose. The mark stands in for punctuation and the words either side already say what it says; announcing it would be reading the separator aloud. Imported as a hashed asset like the portrait, and it keeps its own transparency rather than being given a ground — the masthead sits on a radial glow, not a flat surface, so a background would show as a rectangle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b04a45f54a
|
feat(ui): a face on the about page
The about panel's prose is capped at 68 characters a line, which is a measure worth keeping whatever width the panel happens to be — and left the right-hand half of it empty. A portrait of the author now sits there, linked to rob.tn, taking the space the measure was already leaving rather than any of it. Imported as a module asset rather than dropped in `public/`. Vite hashes it by content, so a replaced photograph invalidates its own cache instead of being served stale from behind a long max-age; `public/` is for the favicon, which needs a fixed path, and for nothing else here. The name is in both `alt` and the link's `title`. The image is the only content of that link, so its `alt` is the link's accessible name — an empty one would leave a screen reader announcing an unlabelled link to an unexplained destination. It stacks under the prose below the breakpoint, and the photograph carries an alpha channel so it gets a ground of its own rather than sitting on whichever surface the theme supplies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6ed9a6c35a
|
feat: an about route, and somewhere to say thanks
Adds `/:chain/about`, linked from the header just before the connection light and the theme toggle — it is about the site rather than the chain, so it sits there instead of in the section nav, and `header: true` on its `SECTIONS` entry keeps it out while still letting the URL parse. The page says what the observer is, that it and the CUDA-optimised miner are built and freely sourced by a member of the Quantus user community rather than by the project, and takes donations in QTC. **The QR is generated in the browser from the address printed beside it, never shipped as an image.** A supplied PNG next to a typed address is two independent claims about where money goes, and nothing on the page could tell a reader if they ever stopped agreeing — a mismatch sends funds somewhere unrecoverable. Verified rather than trusted. The address was put through this crate's own SS58 decoder: checksum and network prefix both hold, and it round-trips to the same string. The QR matrix was then decoded from first principles — function-pattern map, mask 3 unapplied, codewords de-interleaved across version 4-M's two blocks — and reads back the address exactly. A note for whoever checks this again: an independent encoder produces a different matrix for the same string at every mask, because mode segmentation differs, so comparing matrices proves nothing and only a decode does. `the_donation_address_on_the_site_is_a_valid_quantus_address` reads the literal out of the frontend source and validates it, so a typo fails CI rather than costing somebody their money. Confirmed to fail on one changed character. Also on the vesting page, from verifying it live: a "claimable now" figure and a per-grant claimable column. Two grants began vesting at launch while the other forty-six wait for 2027, so 68,871 QTC is already claimable and none of it has been taken — a page that only said "first claimable" would have missed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2bbe8e4d5d
|
feat: a vesting route — when 99.5% of the supply actually arrives
The network page said 5,669,940 QTC sits in the vesting pot and stopped there, which is the largest economic fact about this chain rendered as a single number. `/:chain/vesting` now carries the schedules, the release curve and the evidence. It is the only forward-looking page on the site and it is exact rather than projected: the runtime vests nothing before a grant's cliff, the whole grant from its end, and linearly between, so summing that across the schedules is arithmetic on chain state. `blackbeard-core::vesting` restates those three branches with tests, because until 2027 nothing on chain will contradict them — covering the cliff, the mid-window, the `end` branch that makes the final figure exactly `total` rather than `total` less a rounding crumb, the `start < cliff` case that unlocks a chunk at once, and a grant large enough that `total × elapsed` would wrap a `u128` if the product were not split. Decoded by field name against the metadata, never by offset. The figures that justified building this were read positionally out of 89 bytes and summed to the pot exactly — good evidence the layout was guessed right, and exactly the shortcut this repository forbids elsewhere. A captured mainnet entry now pins the names, so a reordered struct fails the build rather than quietly paying the wrong address. It also retires a weak claim. "Held in the vesting pot" rested on an eight-byte `PalletId` spelling `qvesting` and a `type PalletId` line read from a source tree that may not be the deployed commit. `matches_pot` replaces that with arithmetic: the grants sum to the pot's balance to within the existential deposit, and if a grant is ended or the pot topped up the page says the two have parted company rather than going on describing one as the other. Measured: 48 grants, 5,669,940.000 QTC scheduled against 5,669,940.001 held, nothing claimed, first unlock 2027-09-09, fully vested 2030-09-08. `Field` becomes one component instead of four. Going to add a fourth copy found that the three had already parted company — two rendered `note` as a visible caption, the third passed it to `title` as a hover — so the shared one takes both as separate props rather than silently changing a page. The same lesson `isStandings` records, found the same way. Closes #21 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
da7f773564
|
feat: how concentrated the balance is, once the treasury is set aside
The last piece of #17. It was specified as "the share of issuance held by the top 1, 10 and 100" plus a Gini, and done literally that produces a number that is true and says nothing — because the top account is the vesting pot holding 99.5% of the chain. The figure would sit near 1.000 and move only when the pot paid out, describing an undistributed reserve rather than an economy. So the pot is reported first and then excluded, by its own storage key rather than by being the largest: "the biggest account" is a description that would silently start meaning something else the day the pot drains. Everything else is computed over what is left and labelled as being outside it. The denominator is the counted balance, not issuance. The two differ, and in the direction that matters: a wormhole exit credits its destination without debiting the note's source, so balances summed across the chain exceed issuance, and shares of issuance would add up to more than everything. Measured on mainnet: 1,949 accounts, 27,245 QTC outside the pot, top 1 at 39.6%, top 10 at 72.2%, top 100 at 92.2%, Gini 0.965. That top holder is a mining address — keyless, its balance a cumulative receipt that cannot fall however much its operator pays out — so the panel says in its own words that this is partly a statement about who mined most rather than who holds most. The Gini lives in `blackbeard-core` with its own tests, per the rule about statistics being easy to overstate and hard to notice: pinned against the published coefficient for 1,2,3,4, and against a dust holder beside two whales, which is the case an `f64` accumulator would round away and report as perfect equality between the whales. `RpcClient::storage_values` is new — `state_queryStorageAt`, which turns two thousand round trips into four. Chunked at 500 rather than sent whole because nothing documents a ceiling here, and an undocumented ceiling is one that gets discovered in production. Nothing renders unless the account walk reached the end of the map. Every share divides by a total, and a partial sum makes all of them wrong while looking entirely reasonable. Closes #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
67e7b2ec24
|
fix: the waiting counter was never once visible
It refused to count until a block arrived while the page was open, on the grounds that the block the snapshot hands over on load may be a minute old and starting it at zero would lie. That was honest and completely useless: on mainnet the block that triggers the counter is the same block that puts the first row in the console, so it became visible and unnecessary in the same tick. Reported from the live site after a refresh — the placeholder was still there, because the counter had no window in which to exist. It now seeds from the snapshot's block. The ticking is still `performance.now()`, which cannot be skewed or stepped; only the seed can be inexact, and only for that first block, whose age nothing but the server's `observed_at` can attest. A seed outside a plausible range is refused outright and the plain text stands — no counter beats a wrong one. Measured against the live API: the counter appears within 800ms of load reading "26,378 ms since #43,588", climbs, and gives way to rows about two seconds later, which is how long the console is honestly empty. Also recorded, because it wasted a verification: `/v1/healthz`'s `commit` is stamped into the API binary, so a frontend-only push leaves it on the previous sha *forever*. A watcher polling it for the sha just pushed waits for something that will never arrive and reports "still deploying" over a site that finished minutes ago. Compare the served bundle name to the local build instead — Vite hashes it by content, so equal names mean byte-identical. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a245caef99
|
feat(ui): count the wait instead of describing it
The empty console explained what would eventually appear there, which is the least interesting thing it could say while the chain is visibly producing blocks a few feet away. It now counts milliseconds since the last block, with the height linked, and resets each time one lands. The clock is `performance.now()`, not `observed_at`. That field is the server's wall clock, and subtracting it from the browser's would fold in whatever the two disagree by — a viewer whose machine runs fast would watch the counter go backwards. "Since this page saw it" is both unfakeable and the honest claim. Counting starts at the first block that arrives while the page is open, not at the first the ticker hands over: that one came from the snapshot and may be a minute old, so starting from when it was rendered would begin at zero and lie about it. The number is written straight to the DOM. Sixty React renders a second would redraw the mouth, the population and the whole console for one span of text — the same reason the flight marks are not React state. Under reduced motion it updates four times a second instead of sixty. The caption's idle line changes with it: "watching for the next block" beside a counter timing a block the page has plainly seen was a contradiction, so it now says which of the two is actually missing. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ee575ed33f
|
feat(ui): two columns for the stage, and a console that links out
Stacked, the mouth and the lines describing it could not be read at the same time, which is the whole point of having both. They sit side by side once there is room: the figure takes the space left over and the console takes a fixed column. The breakpoint is measured rather than chosen. At 1000px the console came out 360px wide and *every* line wrapped to two — worse than stacking — because one line needs about 354px for its height, kind and amount columns plus a shortened address and three gaps. The column's minimum is 400px and the breakpoint is where that can exist without starving the figure. The console now links out. A block height goes to the block, an address to the account, and the count of what is not shown names its block instead of saying "this block" — so the line that tells you 897 notes went unlisted is also the way to go and read them. Links fall back to plain text before the chain is known, because `href` would otherwise render `//block/43466`, which looks clickable and goes nowhere. They are marked with a dotted underline rather than a colour, following `a.ticker-height`: the console's columns already carry meaning through colour — muted for the height and address, bright for the kind — and tinting a link would make it momentarily read as a different column. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |