11e092e342d393755aa3de8d3010f7e53326bbbc
92
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0378d0efcb
|
fix(web): say the probe's request was refused, not that nothing was quoted
The pause context showed 1Click filling two QTC buys (233 and 115 QTC, 14:18 and 14:57 UTC on 2026-10-07) while every probe was refused, from $100 to $100k and against both USDCs. The panel's heading, "Will anyone quote QTC?", and its "no quote" labels claimed more than the probe shows. It now names the request (exact input, no key, paying and receiving inside NEAR Intents, against USDC on NEAR or on Ethereum) and calls an outcome a refusal of that request. It points readers to the 1Click fills in each stretch's context, which show when other requests were filled. CLAUDE.md records the distinction. Refs #45 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC |
||
|
|
8892f0a399
|
feat: on-chain context for each pause in quoting, and pool settings kept
Each of the newest pauses now says what happened on-chain around QTC while it ran, so a reader can tell "not quoting but still trading" from "offline": - pool swaps by known solvers, with the QTC they sold into and bought out of the pool; - direct pool swaps by anyone else; - 1Click fills, which a pause should have none of; - bridge deposits arriving on NEAR; - the largest solver QTC holding at the pause's start and end, from the probe's own readings; - changes to the pool's state or whitelist. That last needs data that was not kept: the pool's state and whitelist were read every minute and held only in memory. ref_pool_settings (migration 0023) records them when either changes, and only a row with a predecessor counts as a change. The first row is how the pool was first seen. Context is computed for the 12 newest pauses, a few aggregate queries each. It is counts and amounts only, and the page says it does not explain why quotes stopped. Refs #45 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC |
||
|
|
ab2b59d178
|
feat: pause log, asymmetry and daily summary for 1Click quote probes
The probe record from
|
||
|
|
464bb498ca
|
feat: search box in the chain header
ci / gate (push) Successful in 4m21s
A pasted hash, address or account now opens its page. On this chain one
string can be several things: 0x and 64 hex is a block hash, a
transaction hash, an account id or a miner's preimage; 64 hex without
the prefix is also a NEAR implicit account. So nothing is decided by
shape.
GET /v1/chains/{chain}/search?q= looks for every kind the string could
be, and offers a page only when the data confirms it exists:
- a block that is recorded or that the node has;
- an indexed transaction hash;
- an account with a balance or with activity;
- a miner with blocks;
- a telemetry node;
- a cached runtime version;
- a NEAR Intents account the contract knows.
A NEAR transaction hash is the one exception. It is offered as a
candidate and labelled as looked up when opened, because confirming it
would spend a public RPC call on every search.
The header box searches on Enter, never while typing. One hit opens it;
several are listed to choose from with the arrow keys, never chosen for
the reader; none says what the string was checked as, and a transaction
hash missed while the #48 backfill runs says so.
The masthead's right-hand group now takes the free width, right
aligned, so the search box no longer wraps the connection light and
theme toggle onto a second line.
Closes #49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC
|
||
|
|
7c02840f91
|
feat: Quantus transaction page, addressed by hash, with history hashed
A transaction is addressed by its hash, which is what a wallet shows and
what the bridge writes into a NEAR mint. But chain_extrinsic.hash had
been written only since migration 0018: 4,045 of 285,708 mainnet
extrinsics had one. Links from the NEAR pages could therefore land only
on the block.
- Block page anchors: each extrinsic row has id x<index>. A link with
that fragment scrolls to the row and marks it, and the fragment
survives the height-to-hash redirect.
- Hash backfill (hashscan): the work list is the rows with no hash, and
it only shrinks, because live ingest hashes everything it writes. Each
block is read from our own node and hashed with the function the
bridge links use. Verified against the known deposit 0xc4444afc at
180,029/1, at about 6 ms a block. The client holds only our own
endpoints, so the job waits when bob's node is down rather than
walking history through a public fallback. Chains with no node of
ours (the testnets) are not backfilled and are hashed from now on
only.
- /quantus/tx/<hash> and GET /v1/chains/{chain}/tx/{hash}: the
extrinsic as the block page decodes it, its events, its fee, and for a
bridge deposit the NEAR mint that names it. A miss while the backfill
is running says how many extrinsics are still unhashed, not that the
transaction does not exist.
- Links from the NEAR transaction page, the Intents account page and the
bridge deposits table now go to the transaction, and the block page's
index cell links to it.
Closes #48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC
|
||
|
|
6b3bf76b7e
|
feat: probe whether 1Click will quote QTC, against what could have filled it
On NEAR Intents a price exists only when a solver chooses to quote, and nothing on-chain records a refusal. Every minute the observer now asks 1Click for a keyless dry quote on QTC pairs only, both ways, against native USDC and Ethereum USDC: the smallest size each minute and the full grid each quarter hour, averaging about 4 requests a minute against a published limit of 1,200. At the same moment it reads two things from its own nodes: the Ref pool's quote for the same input, and every solver's balance of the output token inside intents.near. So each "No liquidity available" is stored with whether one solver alone held enough to settle the trade. A probe that got no answer is recorded as `failed`, never as a refusal. Only our own observations are kept: the 1ClickSwap API Terms (s4.1(e)) forbid keeping API data past 24 hours, so a quote becomes a markup against the pool on arrival and raw responses are pruned hourly. The first live round, 2026-10-07: "No liquidity available" both ways. Selling 1 QTC, the pool would have paid 101.53 USDC and three solvers each held enough. Buying with 100 USDC, solver-ref-2.near held 2,308 QTC. The near-intents market page shows a strip per direction and counter token. It is an emphasis chart: the accent marks only "no quote while a solver could fill", the other states are quiet ink told apart by height, and the neutral token is validated against the accent in both themes. Beside it are a summary table and a list of the latest refusals. The pause log and the price-move statistics in #45 come later, once there is history to support them. Refs #45 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC |
||
|
|
6fa8cf342a
|
feat: NEAR calls and events read as sentences, JSON as the fallback
The transaction page showed every call's arguments and every event's payload as pretty-printed JSON. Most of those shapes are fixed by a standard (NEP-141, NEP-245, dip4) or are formats seen on this chain's paths (the bridge's ft_deposit, execute_intents in both its raw and NEP-413 encodings, Ref swap instructions). Those now read as sentences with typed parts: amounts in token units, Intents accounts linked, and Quantus transactions and addresses linked to this site. A sentence is produced only when it accounts for every field. Fields are read through a reader that records what was used. An unknown, missing or mistyped field, an intent kind nobody wrote a case for, or a msg in an unread format falls back to the JSON, with a note saying so. Fields left out on purpose (signatures, nonces, gas hints, intent hashes) are dropped by name in the formatter. The raw JSON stays one click away behind every sentence. Measured over the 12 transactions stored in production: every event and all but three calls read as sentences. The three that fall back are two private Ref callbacks and qcat.nearlytrade.near's ft_resolve_transfer, which carries a non-standard `tax` field. Closes #47 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC |
||
|
|
21ea353942
|
feat: NEAR transaction and Intents account pages
Adds /quantus/near/tx/<hash> and /quantus/near/intents/<account>, the last two parts of #44. A transaction is assembled from our own nodes where possible. The node tracking the signer's shard returns the transaction. Each receipt it lacks is fetched from the node tracking that receipt's receiver, using EXPERIMENTAL_receipt and a light client proof. A public node is used only when ours cannot serve all of it. The page says which source it used. Once a transaction is complete and every block it touched is final, it is stored in near_tx (migration 0020) as raw JSON and decoded on every read, so decoder fixes apply to stored transactions too. The decoder in blackbeard-core::near_tx orders the receipt tree, decodes function-call arguments, parses EVENT_JSON lines into amounts tied to their tokens, and reads the BRIDGED_FROM, WITHDRAW_TO and refund memos. It is tested against the bridge mint piPTXyoq and the failed swap with refund E4ysnVht. The account page shows balances in intents.near, read live from frankie, the account's intent signing keys, and the QTC activity this observer indexes. It says that activity is not the account's whole history. The bridge, fills and swap tables now link to these pages instead of nearblocks. Both pages still link out to nearblocks. Closes #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC |
||
|
|
084fd5ba2c
|
feat: link QTC bridge deposits and withdrawals across both chains
#44 part 3. A deposit is linked exactly: the NEAR mint is bridge-mng.near calling omft.near.ft_deposit, whose arguments carry the Intents account and BRIDGED_FROM's txHash — Blake2-256 of the depositor's Quantus transfer into their deposit address, length prefix included (checked: extrinsic 1 of block 180,029 hashes to 0xc4444afc…, the hash in mint piPTXyoq…). `chain_extrinsic.hash` is stored from now on and filled for the bridge's deposit transfers. `bridgescan` reads mints from gargravarr (bridge-mng.near is on shard 8), each confirmed executed before it is recorded. History before our nodes is found by bisecting the token's supply — 16 changes before gargravarr's oldest block, 204 archive calls — and a rise is accepted as mints only when the mints around it sum to it exactly. Withdrawals (ft_withdraw with WITHDRAW_TO) come from the intents scan, rewound to frankie's oldest block (migration 0018); custody's payout is matched by a strict, stated rule, as an inference, with the fee it implies — every pre-node withdrawal paid exactly 0.002 QTC less. `/bridge/deposits` and `/bridge/withdrawals`; a linked-transactions panel on the NEAR Intents page. Refs #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj |
||
|
|
69050a45c1
|
feat: index QTC's 1Click fills, which settle as transfers, not token_diff
Since launch the NEAR Intents page showed no price and no settlements while
QTC traded heavily: the scan counts `token_diff` trades, and 1Click settles
no QTC swap that way. A buy is a solver transferring QTC to a one-off
1Click account, passed on to a shared one; a sell is the mirror; the user's
payment is on their own chain and never on NEAR.
The scan now keeps every executed transfer of the asset (migration 0017,
cursor wound back to before the first QTC swap; pool swaps already recorded
are not re-queued), unclassified. `/v1/chains/{chain}/intents/fills`
classifies at read time against the solvers known then — the solver table
plus every Ref pool whitelist, since solver-ref-2.near never appears in a
token_diff — so a solver found later classifies its past fills without a
re-read. Solver to account is a buy, account to solver a sell; hops between
non-solvers (the pass-on, a flat 0.002 QTC per-swap fee to 5880ad…) are not
fills. Fills are pushed live; the NEAR Intents page lists them without a
price, and shows the Ref pool's restock price in its place.
Checked against the chain: the traced buy at #218,780,964 (2,991.6053 QTC,
solver-ref-2.near to d4524d…) and the sell at #218,758,672 classify as such,
and their pass-on hops do not.
Closes #43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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> |
||
|
|
578a54ef87
|
fix: the backfill was re-announcing blocks the console had already shown
The wormhole pulse was broadcast from inside `index_events`, which has two callers: `record`, for a block that just arrived, and `backfill`, walking history. So the backfill re-announced every block it re-read — and because it walks towards the tip, the blocks it re-announced were exactly the ones a viewer had just watched. The live console listed #43,446 three times. This is the live/backfill confusion this repository has already been bitten by twice, arriving by a third road, so the guard is a type rather than a comment: `Pass::Live` or `Pass::Backfill`, and a caller cannot forget to say which. The duplicates did more damage than repeating themselves. Console lines were keyed `height-index`, which looked unique and was not, and React given duplicate keys renders duplicates *and* stops honouring the list's own length cap — twelve lines had become eighteen on screen. Lines are keyed on a monotonic counter now, so a reorg re-recording a height cannot reproduce it. Neither half was caught by the build, the lint, or the tests. Both were obvious within a minute of watching the live page. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ad22866d3c
|
feat(ui): say what each mark was
The stage drew a dot per note and left the reader to guess. A dot travelling inward could be anybody paying anybody, and without text beside it the animation is an ornament rather than a reading of the chain. Each pulse now carries a sample of the individual notes — kind, recipient, amount — and a console below the stage lists them newest-first, the way the block ticker reads, which also means no auto-scroll to fight with and the line a reader is looking for never moves. `kind` is the third split this codebase makes on the producing extrinsic and the first that separates all three cases: a reward has no extrinsic at all because it is paid in block initialisation, a transfer has an ordinary one, and an exit has a `verify_*_batch`. The recorded sender distinguishes none of them. Two caps, and they turned out to interact. `MAX_PULSE_NOTES` bounds the wire — a block has carried hundreds of notes and sending every one for a handful of lines is kilobytes a block of text nobody reads. `CONSOLE_LINES_PER_BLOCK` bounds one block's share of the console, because without it a single busy block fills every line and twelve lines from one block look exactly like twelve blocks of one, which loses the reader all sense of rate. The count of what is not shown hangs on the block's first line rather than its last: the list is newest-first, so a count on the last line is the first thing trimmed, and a block of 260 showed four notes while saying nothing about it. That one reached a screenshot before it was caught. The console fills before the reduced-motion guard, so a viewer who asked for less motion still gets the whole reading — twelve lines, no moving marks, population still drawn, caption still live. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4f59af9847
|
feat(ui): the wormhole holds what it has swallowed
The stage drew the flow and nothing else, which on a chain committing about 1.9 notes a block and settling an exit every fifth one meant it was nearly always still. That was honest and it was also nearly empty, while the page's headline figure — tens of thousands of notes committed and not yet spent — sat in a box beside it. The mouth now holds a sample of that population, turning slowly, with arriving notes passing in front of it on their way in. The stock is read, never inferred. A pulse's `notes_out` is an exit crediting an account, and `credit_and_record` inserts a fresh leaf for that credit, so an exit adds a note rather than retiring one; what retires a note is a nullifier, and those are not counted per block. Accumulating pulses into the figure would drift from the truth while looking authoritative, so it is re-read from the chain every two minutes instead. It turns in CSS rather than in the animation loop. The flight marks stop when the chain is quiet, but the population is always there, and a JS loop running forever on a page left open all day is a cost this does not need to pay. Each shell is a ring of marks on a circle inside a vertically squashed group, so one composited rotation traces an ellipse in the mouth's own plane. Reduced motion stops the turning and keeps the marks: how much is inside is the information. One mark stands for many and the caption says how many, whether or not a block has arrived yet — forty-five turning marks with nothing explaining them would be worse than none. Flight time goes from 1.5s to 5s, measured against the real rate rather than the mock that made the first version look busy: at twelve-second blocks the few notes there are now overlap instead of appearing alone. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c08face7ad
|
feat(ui): a live wormhole, drawn from the blocks as they land
The wormhole page described a pool that was never shown doing anything. It now opens with a stage where a note arriving falls inward and is swallowed, and a note leaving climbs out and escapes — driven entirely by blocks this observer has actually decoded. Nothing is on a loop: a still stage means a still chain. `index_events` now reports what each block did to the pool, split by the producing extrinsic exactly as the daily flow query and `exit_cohorts` are — a settled batch releases value, anything else commits it, and the recorded sender cannot say because it is the same sentinel either way. The exit extrinsics are collected inside `index_extrinsics`, the one place the body is already decoded; finding them in a second pass would re-run `decode_extrinsic` over Dilithium signatures at 5.3 KiB apiece. It is broadcast after the block rather than inside it, because the counts cost two more round trips and the site's ticker must not wait for one panel. A block that touched no notes broadcasts nothing. On the front end the pulse takes a side channel — `Observer.onWormhole`, apart from the state listeners — because a pulse is an event, not a fact about the page, and routing it through `ObserverState` would re-render the leaderboard, the ticker and the whole tree once per block for one animation. Marks are pooled SVG nodes mutated from a loop that starts on a pulse and ends itself. Honest by construction: marks are capped and the caption is not, so a block committing 900 notes draws 28 and says nine hundred. `prefers-reduced-motion` stops the motion and keeps the information — no marks, caption still live, aria-label still describing the last block. The stage takes the headline stat row's place on this page. Hashrate, difficulty, block time, miners and height are on every other page and say nothing about the pool. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
864a76ffb6
|
feat(ui): show what goes into the wormhole and what comes out
The wormhole page counted the pool but never drew the flow through it. It now leads with a meter — 42% of every note ever created is still unspent — and a per-day chart of value arriving against value leaving. Direction is read from the producing extrinsic, never from the sender, which is the same sentinel whichever way the value is going: anything settled by `verify_public_batch` or `verify_private_batch` is leaving, everything else is arriving. `entered + left` reconciles to the day's total exactly. **The chart is diverging with a single hue.** In and out want two hues either side of a neutral midpoint, and this site has one data colour. The two-shade alternative was measured rather than assumed and fails outright — `#bd8829` against `#e0a63a` scores a normal-vision ΔE of 9.9, under the 15 floor. So direction is carried by which side of the zero rule a bar sits on, which survives greyscale, print, forced-colors and every form of colour vision, and leaves no categorical pair to validate. Rendering it is what found the rest. Same hue plus touching bars drew one continuous mark through the rule and the encoding disappeared, so a surface gap is held open at zero. Five days across a full-width panel drew 170px slabs, so bar width is capped and the columns space out instead. And `rx` rounds all four corners of a rect, so bars read as detached lozenges until they became paths with rounded data-ends only. Ships with the hover layer, direct labels on the busiest day each way, and a visually-hidden table so the series exists outside the picture. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f81ebd4746
|
feat: who the wormhole exits pay, and how much of it is the protocol itself
The premise was that pools are the best surface for observing wormhole transfers, because payouts must leave a pattern in the batches. The pattern is there. Most of it is not payouts. Two things break the obvious reading. Batch *size* is aggregation: the pallet documents a public batch as one segment per inner private batch and a segment as one client, so a 120-output batch is an aggregator bundling unrelated people. And the most recurrent recipient is the aggregator itself — one address is credited in 91.5% of every public batch anyone submitted and in none of the private ones, which is the rebate minted to the proof's `aggregator_address`. QUANPOOL's mining address appears in 501 batches for 7.54 QTC total, 0.015 each: the fee share paid to whoever included the batch. Classified, three addresses take 76% of everything the exits pay. A cohort table that did not say so would be describing aggregator plumbing and calling it an economy, so the share is the section's eyebrow and its first sentence. What survives the subtraction is the finding worth shipping: among genuine recipients, 35 addresses paid twenty or more times take two thirds of the remaining value at twice everyone else's average credit. That is a recurring-payment shape and the page says it is no more than that — the chain records no sender, so a regular payee is as plausibly an exchange deposit or one person on a schedule as a pool meeting its obligations. Batch size is not rendered anywhere, on purpose. Closes #20 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d9d41888a
|
fix: a miner's balance is a receipt total, and the supply figures were wrong
Two claims this repository shipped, both disproved by measuring mainnet. **A mining address is keyless, so its balance can only rise.** `retained` was documented here as "the one measure privacy cannot hide", reasoning that a payment debits the account whichever route it takes. It does not. `mining-rewards` pays `derive_wormhole_address(preimage)`, which no Dilithium keypair signs for, and `Wormhole::credit_and_record` credits an exit's destination with `Unbalanced::increase_balance` while never debiting the note's source. QUANPOOL's balance rose at all twelve samples between blocks 1,000 and 42,400 and fell at none, nonce 0 throughout — as every miner's must. So `retained` read ~100% for everyone because it had to, and a number identical for all reads as an accusation aimed at whoever mined most. The column is gone, `balance` is `received`, and the table says plainly that a pool paying out in full and one paying nothing produce the same row. **Four empty lock maps are not "nothing is withheld".** Locks, Holds, Freezes and Reserves are all empty — counted, not assumed — and the network page concluded circulating equals total issuance. Wrong by a factor of 436: this runtime vests from a *pot*, not with locks, and `modlqvesting` holds 5,669,940 QTC, 99.54% of every unit of balance on the chain. The page now reports the pot and issuance-minus-pot, and still declines the word "circulating" because the release schedule is not read here. The pot address is derived from `Vesting::PalletId` in metadata rather than a hard-coded `qvesting`: it is a consequence of a runtime constant, and a runtime that changed it would leave the site reporting an empty pot while looking perfectly healthy. Both steps are pinned against the real mainnet metadata fixture. Also recorded: never sum account balances and call it supply. They exceed `TotalIssuance` by 13,427 QTC at block 42,400 and by exactly zero at genesis, bisecting to block 4,800 and a `verify_public_batch` — private value is counted twice by any such total. Single blocks reconcile exactly, so a spot check says everything is fine. Refs #17 Closes #19 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e3c95d4945
|
feat: a wormhole page — what enters, what leaves, what is still inside
The observer had nothing to say about this chain's central feature, which is
why the distribution table's outflow column could ship reading zero for every
miner and look like evidence.
A transfer inserts a commitment as a note; spending one proves a nullifier,
which the runtime records so it cannot be spent twice. That makes the pool
countable even though its movements are not:
ZkTree::LeafCount 82,691 notes created
Wormhole::UsedNullifiers 47,796 notes spent
outstanding 34,895 unspent — still inside
Outstanding is the headline. It is the part of the chain's activity that has not
moved, and every one of those notes will name a recipient and an amount and not
a sender when it does.
The routes table separates transfers by the extrinsic that produced them,
because a hidden sender means two different things. In block initialisation it
is the chain paying a miner and the "sender" is the runtime; inside
`verify_public_batch` or `verify_private_batch` it is somebody spending a note
and the origin genuinely is not recorded. Collapsing those would make mining
rewards look like anonymous payments — 23,876 of them.
Notes per day come from the `leaf_index` each transfer carries rather than from
`ZkTree::LeafInserted`, which is in `SKIPPED_EVENTS` because it fires every block
and names no account. Same fact, cheaper road.
`SegmentsDenied` and `ExitMintFailed` have never fired. Shown anyway, because a
proof rejection nobody has ever seen is a finding rather than a gap — though it
is a weaker statement than it looks, and the note says so: never refused is not
the same as never attempted.
Closes #18
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
365cd7a803
|
fix(ui): "sent nothing" was a claim this chain cannot support
The distribution table shipped with a `sent on` column reading zero for every
miner, which implies nothing left those addresses. On this chain that is not
what a zero means, and the correction came from the observation that payouts are
themselves wormhole transactions.
`Wormhole::NativeTransferred` records `to` and `amount` and does not record who
sent it. Splitting the events by the extrinsic that produced them shows what
that costs:
(no extrinsic — initialisation) hidden 23,876 events 72 recipients
Utility::batch_all visible 15,303 2,145
Wormhole::verify_public_batch hidden 8,136 538
Balances::transfer_allow_death visible 6,941 651
Wormhole::verify_private_batch hidden 1,758 116
Hidden-sender transfers in block initialisation are mining rewards. The 9,894
produced by `verify_public_batch` and `verify_private_batch`, reaching 654
recipients, are real payments with no origin recorded — the chain's privacy
working as designed. So the column was blind to exactly the mechanism a
wormhole-held balance would use to pay out, while reading as proof it had not.
It is `sent visibly` now, the note leads with the limit rather than burying it,
and a `retained` column carries the one measure privacy cannot hide: balance
over what the chain paid in. A payment debits the account whichever route it
takes, so a balance that has never fallen is a balance nothing has left.
A second correction, from running the table over the full index rather than a
dev slice: *every* one of the top 25 miners shows zero visible outflow and holds
almost exactly what it mined, including another address calling itself
`quantus-mainnet-pool`. Zero outflow is universal among mining addresses and
distinguishes nobody. The accounts distributing to hundreds of recipients have
never mined a block — they are funded from the genesis endowment — so setting a
miner beside them is a category error, and one this repository made before the
numbers came in.
Refs #17
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|