Skip to content

Add yTranche indexing and REST support - #445

Open
murderteeth wants to merge 11 commits into
mainfrom
tronche
Open

Add yTranche indexing and REST support#445
murderteeth wants to merge 11 commits into
mainfrom
tronche

Conversation

@murderteeth

@murderteeth murderteeth commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Indexes the yTranche deployment: Kong discovers tranches through the controller,
retains controller state, enriches tranche snapshots with Hook state, generates
controller-backed PPS and performance history, and exposes it over REST.

The controller is the authoritative source for a tranche's assets and price per
share — it holds what the tranche has accrued and what losses it has absorbed,
where the tranche's own totalAssets() reports its ERC-4626 balance. So tvl-c,
pps and every APY observation resolve through one shared reader:
trancheController.liveAssets(vault) for tranches, totalAssets() and
pricePerShare() for ordinary vaults, which keep the paths they already had.

Closes #444

How to review

Read the commits in order — each is one self-contained step, and the messages carry
the reasoning that doesn't fit in code comments.

Start with docs/ytranche.md: it covers the accounting semantics that are easy to
get wrong (pendingExcess is assigned profit awaiting a report, coverage is an
accounting measure while capacity is what the Hook will deliver, rate limits are
fixed windows that expire at windowStart + rateLimitWindow) and the hook vs
hookState snapshot distinction.

Then the three places where a wrong call would be expensive:

  • packages/ingest/abis/yearn/lib/assets.ts — the shared asset/PPS readers, and
    the refactor of lib/tvl.ts, lib/apy.ts and the v2/v3 pps hook onto them.
    This is the only change that reaches non-tranche vaults.
  • .../tranche/controller/snapshot/hook.ts — discovery and the ordered tranches
    accounting array, all read at the automatic snapshot's block.
  • packages/web/app/api/rest/tranche/db.ts — payload units and derivations.

The two generated abi.ts files are etherscan-verified sources, one line each —
skim or skip.

Test plan

  • Automated: bun --filter ingest test, bun --filter web test, bun --filter lib test.
    20 new tests. Ingest carries 10 failures that also fail on main
    (archive-RPC-dependent specs) — the same set before and after this branch.
  • Manual: full local run against mainnet, controller-only config, discovery
    through to REST. Three tranches found in priority order; 9 things with correct
    base/locked types; hookState on each tranche alongside the raw hook
    address; five output labels populated with the legacy tvl label appearing
    for the main vault alone; both REST routes 200 with matching payloads; unknown
    controller 404, bad chainId 400.
  • Verified two tranche-accounting / tranche-system observations against
    direct eth_call at their stored block numbers — exact to the wei, confirming
    each observation is a point-in-time read.

Risk / impact

Runs on the existing schema, reusing the thing, snapshot and output tables.

The one change with shared blast radius routes every yearn vault's tvl-c and pps
through the new readers. Ordinary vaults issue the same totalAssets() and
pricePerShare() calls they did before — the branch is keyed on a
trancheController default that tranches alone carry — and the existing tvl and
pps specs pass unchanged. Rollback is a straight revert; every write lands in
existing tables and columns.

Also adds a refresh-cache workflow step for the tranche cache and widens the
timeseries refresh to controller-addressed series. Both are additive; the tranche
endpoints begin serving once their first refresh runs.

🤖 Generated with Claude Code

murderteeth and others added 11 commits July 26, 2026 17:34
Adds the Ethereum yTranche deployment to the indexer's configuration.

- `yearn/3/tranche/controller` — the TrancheController ABI, registered as a
  static chain-1 source at its creation block. Its zero-argument state
  (VAULT, ASSET, reserveVault, tranchesLength, totalClaims, vaultAssets,
  reserveAssets, backingAssets, ...) is picked up by the automatic snapshot,
  and its events index through automatic event extraction.
- `yearn/3/tranche/vault` — the tranche ABI: the TokenizedStrategy interface
  unioned with the base and locked tranche implementations, so one ABI covers
  both deployed variants. Registered as `tranche` things.
- `yearn/3/tranche/hook` — a read-only interface for the Hook contract. The
  Hook owns no thing and no snapshot; the tranche snapshot hook reads it
  through this interface.

Tranches also carry `vault` and `strategy` labels, which the generic
`yearn/3/vault` and `yearn/3/strategy` paths would otherwise process. Both
filters now exclude things marked `tranche`, so every tranche address has
exactly one snapshot owner.

The controller and tranche paths are siblings rather than nested under a
shared `yearn/3/tranche` hook path: kong's hook resolver applies a parent
path's hooks to all child paths, which would cross-wire the two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ting

The controller snapshot hook walks `tranchesByPriority(index)` for
`tranchesLength()` slots, so tranches are found through the controller with no
hardcoded addresses and each entry keeps its priority. For every tranche it
reads `tranches()`, `liveAssets()` and `trancheCoverage()` and appends an
ordered `tranches` array to the snapshot's hook data. All appended reads use
the automatic snapshot's block, so the stored row describes one point in time.

Discovery also projects the things the rest of the pipeline needs. Each tranche
gets `tranche`, `vault` and `strategy` things sharing one set of defaults:
controller, priority, type, asset, main vault, inception and the erc4626 / v3 /
yearn flags. `trancheType` records the implementation — base or locked, decided
by whether the tranche exposes cooldown configuration — rather than the A/B/E
deployment nicknames, which are configuration and not protocol types.

Four defaults go beyond the discovery set because existing shared code requires
them: `decimals` and `apiVersion` (the tvl and apy calculations parse them off
the thing), and `name`/`symbol`, which keep the vault list cache from seeing a
null name in the window between a tranche's first thing and its first snapshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hook state is parameterized by tranche, so the automatic snapshot can't reach
it — it only captures zero-argument reads, which for a tranche means the raw
`hook` address. This hook reads that Hook at the same block with the tranche as
target and appends `hookState`: the global gate and rate-limit window, the
tranche's deposit limit, its fixed-window deposit and withdrawal counters, and
the derived deposit and withdrawal caps.

`hook` stays the address and `hookState` stays separate, so a reader can always
tell which Hook produced the state. Counters are stored exactly as reported;
whether a window has expired depends on the timestamp a consumer is answering
for, so that derivation belongs to the consumer. The Hook gets no thing and no
snapshot of its own, and no account-scoped state is read.

A tranche pointing at a hook that doesn't implement the interface logs and
skips enrichment rather than failing the snapshot — the raw address is stored
either way, and that's what a reader needs to investigate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A vault's asset balance has one authoritative source, and for a tranche it is
not the vault: the controller holds the accounting. A tranche's own
totalAssets() reports its ERC-4626 balance, while the controller knows what it
has accrued and what losses it has absorbed. New `abis/yearn/lib/assets`
resolves both cases from a vault thing and a block — totalAssets() normally,
`trancheController.liveAssets(vault)` when the thing carries a controller — so
tvl, pps and apy can no longer disagree about how much a vault holds.

The same module owns the shared pps reader. Ordinary vaults keep reading
pricePerShare(); tranches are priced as authoritative assets × share scale ÷
totalSupply, falling back to the share scale when supply is zero. tvl-c,
`pps` and every apy observation (current, weekly, monthly, inception) now route
through these two functions, so a tranche and an ordinary vault are measured by
the same rule at the same blocks.

Tranches get `pps`, `apy-bwd-delta-pps` and `tvl-c` hooks and no `tvl` hook:
tranche tvl is a claim on the main vault's backing rather than additional
protocol assets, and the legacy label is the one naive aggregates sum. Labels,
components, pricing, sampling, annualization, compounding and storage are all
unchanged, and non-tranche vaults follow exactly the paths they did before —
`liveAssets` already excludes pendingExcess, so no pending profit enters pps or
tvl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tranche accounting reads decode to fixed tuples that viem types as a union
including address[], so the direct cast is not provable. Route both through
unknown, as the codebase does elsewhere for multicall tuple decodes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two daily series make the controller's accounting historical rather than
only current.

`tranche-accounting`, one series per tranche: baseline assets, pending excess,
live assets, claim, covered, coverage ratio, target rate, excess share and
whether accrual is paused. Everything comes from the controller, because the
controller — not the tranche's own ERC-4626 view — knows what a tranche has
accrued and how much of its claim is actually covered.

`tranche-system`, one series at the controller: total claims against vault
assets, reserve assets and backing assets, plus their ratio. That ratio is
accounting coverage and nothing more — what can be withdrawn right now is a
separate question, bounded by hook limits and deliverable vault liquidity.

Each observation reads at a single historical block. Blocks before a tranche's
registration read as an unregistered tranche rather than reverting, so those
early zeros are real; a failed read emits nothing instead, so rpc trouble can't
be mistaken for empty accounting. Coverage ratio is left null rather than 1 when
there is no claim to cover — no claim is not evidence of full coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`GET /api/rest/tranche/:chainId` returns the deployment as one document:
controller, asset, main vault, reserve vault, system accounting and the tranches
in priority order. Each tranche carries its metadata, its raw `hook` address,
`hookState`, the controller's accounting for it, coverage, current
controller-backed price per share, and deliverable deposit and withdrawal
capacity.

Amounts are normalized to the asset's decimals, with `asset.decimals` in the
payload so raw units can be recovered, and price per share keeps both raw and
humanized forms to match the `pps` output. Everything is stamped with the block
it came from: system fields from the controller snapshot's block, each tranche
from its own, since the two are snapshotted independently.

Capacity is reported as the Hook derives it rather than re-derived here, plus
effective rate-limit usage: a fixed window has expired once the snapshot's
timestamp passes windowStart + rateLimitWindow, at which point nothing is used.
That is deliverable capacity, and it is deliberately separate from the coverage
ratio, which is an accounting measure.

Served from Redis like the other REST resources, with a refresh script wired
into the existing refresh-cache workflow. An unset reserve vault reads as the
zero address on chain and is reported as absent; a chain with more than one
controller keeps the larger deployment and logs the rest rather than dropping it
silently.

`tranche-accounting` and `tranche-system` join `pps` and `apy-historical` in the
timeseries mechanism. Both are narrower than "every vault", so labels now
declare their address scope and the refresh scripts give the narrow ones their
own pass instead of adding a query per vault. No GraphQL changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixed-block specs against the deployed Ethereum system, so expected values are
reproducible rather than moving targets:

- authoritative assets pick totalAssets() for an ordinary vault and
  liveAssets(vault) for a tranche, each checked against a direct read of the
  deployed contracts
- tranche pps equals authoritative assets × scale ÷ supply, ordinary pps still
  equals pricePerShare(), and zero supply returns the share scale
- apy's current, weekly, monthly and inception observations each equal the shared
  reader's value at the block apy sampled — apy has no pps path of its own
- tvl-c consumes controller-backed assets while keeping its five components, and
  the tranche path exposes no legacy `tvl` label
- controller discovery returns the three deployed tranches in priority order,
  with their accounting and base/locked implementation type
- tranche snapshot enrichment appends hookState with representative limit data,
  leaves the raw `hook` address alone, and skips enrichment for a missing hook or
  one that doesn't implement the interface
- both accounting series emit their components normalized to asset decimals
- the REST payload's units, priority ordering, effective rate-limit usage and
  pre-snapshot fallbacks, unit-tested off captured row shapes

Verifying apy's observations by equality against the reader rather than by
mocking it is deliberate: specs share one module registry, so a module mock
either leaks or gets bypassed depending on file order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
New docs/ytranche.md covers what the code can't say on its own: the deployment,
why the controller and tranche abi paths are siblings, why a tranche carries
three things and what the `tranche` default is protecting, the `hook` vs
`hookState` snapshot distinction, and the accounting semantics that are easy to
get wrong — pendingExcess is not live NAV, coverage is not withdrawal capacity,
target rate is not realized yield, rate limits are fixed windows rather than
sliding lookbacks, and tranche claims are not additional protocol TVL.

docs/rest.md gains the tranche endpoint with a full response and notes on units,
per-tranche block stamping and the capacity-vs-coverage distinction, plus the two
new timeseries segments. docs/outputs.md gains both new labels with their
components and the tranche pps formula, and its coverage table now records that
tranches deliberately emit no legacy `tvl`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tranche controller is bound to one asset class by construction: its ASSET and
VAULT are constructor arguments with no setters, so it can never be repointed. A
chain therefore holds one controller per asset class — USD, BTC, ETH — and the
previous single document per chain could only ever describe one of them. The
refresh script papered over this by keeping the controller with the most tranches
and logging the rest, which would have silently dropped a whole deployment from
REST the moment a second one shipped.

Replaces that with a collection and a member:

  GET /api/rest/tranche/:chainId/controllers   every system on the chain
  GET /api/rest/tranche/:chainId/:controller   one system, same shape as before

`controllers` is a static route segment, so Next resolves it ahead of the sibling
member route and it can never be mistaken for an address — controllers are hex, so
the reverse collision is impossible too. The member route accepts either address
casing; the cache key lowercases. `/api/rest/tranche/:chainId` is now an unrouted
prefix rather than a second URL for the same payload.

The dedupe branch is gone: every controller gets its own cache entry and each chain
gets a collection entry holding all of them. Nothing else needed changing —
discovery, the tranche-system series and the timeseries refresh were already keyed
by controller address.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The controller is a configured source rather than a discovered address, so
nothing created a thing for it and it was absent from every thing-shaped surface
— including the dashboard's label counts, where the tranches show up but the
deployment they belong to did not.

It now gets a `trancheController` thing carrying asset, main vault, inception and
the v3 / yearn flags. No `reserveVault` default: that one is settable, so its
current value belongs to the snapshot rather than to defaults, where it would go
stale. The thing is created whether or not any tranches are registered, since a
freshly deployed controller with an empty registry still exists.

That also removes a duplicated derivation. Both controller lookups in web were
reconstructing the set of controllers from the `trancheController` default on the
tranches pointing at them, which needed at least one registered tranche to find a
deployment at all. They now read the label directly.

No abi path claims the `trancheController` label, so the new thing is not fanned
out and the controller keeps exactly one snapshot owner. Label counts on the
dashboard are grouped straight from the thing table, so it appears with no web
changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
kong Ready Ready Preview, Comment Jul 26, 2026 9:32pm

Request Review

@murderteeth
murderteeth marked this pull request as ready for review August 3, 2026 15:01
@murderteeth
murderteeth requested review from matheus1lva and rossgalloway and removed request for matheus1lva August 3, 2026 16:08

@rossgalloway rossgalloway left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Logic and rationale for how the system works looks sound. Expected output and data about the tranches and overall system look comprehensive and clear. Well documented and as easy to understand as possible for 2 complicated systems coming together. Nice work!

Below is the automated codex review with a few minor comments that I will let you determine whether are worth picking up before merge.


Automated Review

Summary

Adds yTranche controller discovery, tranche and Hook-state snapshots, controller-backed asset/PPS calculations, accounting time series, and REST endpoints. Two correctness
issues remain around null accounting semantics and stale Hook state.

Issues

  • packages/web/app/api/rest/timeseries/db.ts:75 — The new accounting series cannot preserve the required coverageRatio: null when there is no claim. Both daily
    aggregation queries convert an all-null bucket to zero with COALESCE(AVG(NULLIF(value, 0)), 0).

    • Change: For coverageRatio in tranche-accounting and tranche-system, preserve SQL NULL in both getFullTimeseries and getRecentTimeseries; keep the existing aggregation
      and zero behavior for every pre-existing label and component.

    • Keep: Controller/tranche address scoping, route segments, legacy timeseries responses, and aggregation for other components must remain unchanged.

    • Done when: Latest and historical REST responses return coverageRatio: null for zero-claim observations in both new labels, with focused tests covering both queries.

  • packages/ingest/abis/yearn/3/tranche/vault/snapshot/hook.ts:39 — Removing a Hook or switching to an incompatible Hook retains the previous hookState. The hook returns
    {}, while upsertSnapshot merges that over the existing state; REST then pairs the current raw Hook address—including the zero address—with stale limits and capacities.

    • Change: Return an explicit hookState: null when the raw Hook is absent, zero, or cannot be enriched, and ensure the snapshot loader preserves that null as a clearing
      value.

    • Keep: Preserve the raw automatic hook address, keep enrichment failures non-fatal, and do not create a Hook thing or standalone Hook snapshot.

    • Done when: A valid snapshot followed by a zero-address or incompatible-Hook snapshot persists hookState: null, and REST returns null Hook state and capacity rather than
      the previous values.

Suggestions

  • Consider extracting the duplicated scoped-series traversal from refresh.ts and refresh-historical.ts into a shared helper, while retaining their respective readers, Redis
    keys, and payload shapes.

Coverage this review did not reach:

  • Controller-hook side effects that create/update controller, tranche, vault, strategy, and ERC-20 things—including a controller with zero tranches.
  • ABI/config integration, automatic event extraction, sibling hook resolution, and generic snapshot-owner exclusions.
  • End-to-end REST database, Redis, collection/member route, multi-controller, and 200/400/404 behavior.
  • Executable coverage of scoped timeseries discovery and cache population.
  • Non-tranche regression coverage across the broader V2/V3 and yield-bearing asset/PPS paths.

Verdict

COMMENT

———

How This Was Reviewed

Reviewed with the review-pr-workflow skill (https://github.com/yearn/webops/blob/main/skills/review-pr-workflow/SKILL.md) —
5 review lenses, each material finding independently verified by fresh native Codex session agents. 0 candidate findings were refuted and dropped.

Verification and preview

  • Lint: 0 errors, 34 existing/current warnings. The repository wrapper required unsupported terminal behavior, so its underlying package lint commands were run directly.

  • Web tests: 9/9 passed.

  • Ingest selection: 20/20 passed.

  • Production build: passed.

  • No UI files changed, so Playwright visual verification was skipped.

  • No source changes were made.

const addresses = await extractTranchesByPriority(chainId, address, snapshot.tranchesLength, blockNumber)
const tranches = await extractTrancheAccounting(chainId, address, addresses, blockNumber)

await projectControllerThing(chainId, address, snapshot)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we can do a promise.all here would be small tiny ways to make it faster.

@@ -0,0 +1,98 @@
import { EvmAddressSchema, Thing } from 'lib/types'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the file contains mostly "tranche" related things, im wondering if doesnt make sense rename to be under lib/traches/assets maybe?

block_number AS "blockNumber",
block_time AS "blockTime"
FROM snapshot
WHERE chain_id = $1 AND lower(address) = lower($2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
WHERE chain_id = $1 AND lower(address) = lower($2)
WHERE chain_id = $1 AND address = $2

and make sure controller is passed throug getAdress() from viem. All the addressess have been normalized already


const assetRows = assetAddress
? await db.query(
'SELECT defaults FROM thing WHERE chain_id = $1 AND lower(address) = lower($2) AND label = $3',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prob same here

@matheus1lva matheus1lva left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Verdict: COMMENT

No blockers. Four confirmed issues (two extend the prior automated review with new detail, two are new), three non-blocking suggestions.

Issues

  • packages/ingest/abis/yearn/3/tranche/vault/snapshot/hook.ts:39 — When a tranche's Hook is absent, zeroed, or unreadable, the hook returns {}; upsertSnapshot shallow-merges (packages/ingest/load/index.ts:104-108), so the previous hookState is retained. REST (tranche/db.ts:254, :280-281, :302-310) then serves the stale limits stamped with the current snapshot's blockNumber/blockTime, and derives effectiveUsed/remaining/capacity from them. setHook makes this reachable. Spec task 4 requires hookState read at the snapshot's block.

    • Change: Return { hookState: null } for the absent/zero/incompatible cases so the merge overwrites the stored value (not undefined — it can be dropped in queue serialization), and have buildTrancheSystem treat null as absent (hookState: null, capacity: null, which db.spec.ts:166-183 already exercises for the no-snapshot case). Add a test: populated state followed by a zero/incompatible-Hook snapshot persists null.
    • Keep: Raw hook address preserved; enrichment failures non-fatal; no second top-level hook field.
    • Done when: After a snapshot where the Hook is zero/unreadable, the stored row's hook.hookState is null and REST returns hookState: null / capacity: null instead of an older block's values.
  • packages/web/app/api/rest/timeseries/db.ts:75 (and :103) — COALESCE(AVG(NULLIF(value, 0)), 0) collapses an all-NULL daily bucket to 0. The SQL predates this PR, but #445 newly routes tranche-accounting/tranche-system through it, so "no claim to cover" is served as "0% covered" — the exact inversion spec task 5 forbids, and this PR's own docs (docs/outputs.md, docs/ytranche.md, docs/rest.md's ?components=coverageRatio) promise null. Ingest is correct (value: claim > 0n ? div(covered, claim) : undefined persists as NULL); the aggregation destroys it.

    • Change: Preserve NULL through aggregation for these series — AVG(value) in both getFullTimeseries and getRecentTimeseries; if the COALESCE must stay for existing vault labels, branch on the label.
    • Keep: Existing vault labels (pps, apy-bwd-delta-pps, apr-oracle, tvl-c) keep current bucket values and shape; daily bucketing and the historical/latest merge unchanged.
    • Done when: GET /api/rest/timeseries/tranche-accounting/1/<tranche>?components=coverageRatio returns value: null for a zero-claim day, and likewise for tranche-system.
  • packages/ingest/abis/yearn/3/tranche/controller/snapshot/hook.ts:132tranches.map((address, priority) => …) derives priority from the caller's array index, not from the controller. tranche-accounting/hook.ts:46 calls extractTrancheAccounting(chainId, controller, [address], blockNumber) with a single address, so every tranche comes back priority: 0. The spec cements the wrong behavior: hook.spec.ts:93-96 passes [a, e] and asserts [0, 1] while the discovery test above proves e sits at index 2. Currently latent (the accounting hook doesn't consume priority), but the field lies for any subset caller.

    • Change: Take { address, priority }[] (the controller snapshot knows the real index from extractTranchesByPriority) and have tranche-accounting/hook.ts:46 pass the tranche's stored defaults.priority. Update hook.spec.ts:96 to expect real priorities.
    • Keep: The controller snapshot's tranches array stays priority-ordered with the same field names/values.
    • Done when: extractTrancheAccounting cannot produce a priority disagreeing with tranchesByPriority(index); the [a, e] -> [0, 1] assertion is replaced by [0, 2].
  • packages/ingest/abis/yearn/3/tranche/controller/snapshot/hook.ts:171 (and :69) — estimateCreationBlock — a bytecode binary search (~25 archive calls, only a 10s cache; packages/lib/blocks.ts:98-127) — runs unconditionally for the controller and every tranche on every snapshot cycle. The repo's convention guards it: yearn/3/vault/snapshot/hook.ts:119 runs it only if (! await things.exist(...)). Three tranches ≈ ~100 archive getBytecode calls per controller snapshot for values that never change.

    • Change: Guard both calls with things.exist(chainId, address, label) and omit inceptBlock/inceptTime from defaults when the thing exists — upsertThingDefaults (packages/ingest/db.ts:124) is a right-wins merge, so omitting keys preserves stored values.
    • Keep: Tranche things still re-project priority, trancheType, and the rest of defaults each snapshot; only incept fields are skipped.
    • Done when: A repeat snapshot of an already-discovered controller issues no estimateCreationBlock calls and stored incept values are unchanged.

Suggestions

  • .github/workflows/refresh-cache.yml:31 — The refresh-cache step and the widening of refresh.ts/refresh-historical.ts to tranche/controller-addressed series are not requested in #444 (task 7 scopes to the two REST resources and existing timeseries REST). They are necessary plumbing — the routes read only Redis, so without them every tranche route 404s — and the PR body discloses them. Worth an explicit scope acknowledgment since they touch infrastructure shared with the vault caches.
  • packages/web/app/api/rest/tranche/db.ts:302capacity re-emits three hookState fields verbatim under different names (depositCap, depositLimit, withdrawCap) and ignores remaining, the rate-limit-aware number computed just above in toRateLimit (db.ts:122). Either drop capacity, or make it carry only derived deliverable numbers (e.g. Math.min(cap, remaining)) and stop restating limit.
  • packages/web/app/api/rest/timeseries/refresh-historical.ts:52 — The 34-line scoped-refresh pass is duplicated byte-for-byte with refresh.ts:52-82, differing only in fetcher and key function. Extract one refreshScopedSeries(fetch, key) helper so adding a fourth scoped label edits one place, not two.

Coverage this review did not reach (non-blocking): controller-discovery side effects (process/projectControllerThing/projectTrancheThings are never called by any spec, so zero-tranche controllers and thing/default projection are untested); ABI/config wiring (abis.yaml, sibling-path isolation, snapshot ownership, automatic event extraction); the non-tranche regression surface of lib/assets.ts/tvl.ts/apy.ts and the v2 PPS hook (no legacy-fallback or failed-read pinning); tranche-path historical APY (apy-shared-pps.spec uses an ordinary v2 vault); the REST DB layer beyond the pure payload builder (getTrancheControllers/getTrancheSystems, multi-controller, case handling); the two routes and cache refresh (no 200/400/404, header, or Redis-failure tests); timeseries label routing/refresh plumbing regression; and docs-vs-code cross-check of the negative-scope criteria.


How This Was Reviewed

Reviewed with the review-pr-workflow skill
5 review lenses (skeptic / architect / minimalist models per agent-and-subagents rules),
each material finding independently verified (auto panel: codex gpt-5.6-terra). 2 candidate findings were refuted and dropped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add yTranche indexing and REST support

3 participants