feat(api): v1 chain/* + transactions/* reads — first product-API route group - #169
Conversation
|
Warning Review limit reached
Next review available in: 28 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughThis PR introduces a new native ChangesNative v1 Product API
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Router as v1_router
participant Governor
participant AuthMw as require_tier
participant Handler as chain/transactions handler
participant State as V1State
Client->>Router: HTTP request /api/v1/*
Router->>Governor: governor_mw charge tokens for RouteClass
Governor-->>Router: allow or 429 rate_limited
Router->>AuthMw: require_tier(Tier)
AuthMw-->>Router: allow, unauthorized, or SensitiveOpDisabled
Router->>Handler: dispatch to handler
Handler->>State: read chain/mempool/indexer data
State-->>Handler: data or error
Handler-->>Client: JSON response or v1_error envelope
sequenceDiagram
participant Server
participant Posture as warn_startup_posture
participant Listener
participant Router as v1_router
Server->>Listener: bind and get local_addr
Server->>Posture: warn_startup_posture(security, bind_addr)
Posture-->>Server: log WARN if insecure posture
Server->>Router: mount v1_router(V1State, Governor)
Server->>Server: serve via make_service_with_connect_info
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (2)
ergo-api/tests/v1_chain_tx_routes.rs (2)
676-733: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMisleading test name / missing direct
/submitrejection coverage.
submit_rejection_maps_to_v1_envelope(line 709) actually posts to/api/v1/transactions/check, not/submit. The/submitpath only has success (submit_ok_returns_tx_id) and disabled-bridge (submit_without_bridge_is_submit_disabled) coverage — there's no test verifying a business-error rejection (e.g.double_spend) through/submititself, only through/check.Consider either renaming this test to reflect it exercises
/check, or adding a parallel case that posts the same rejection through/submitto lock both paths independently.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ergo-api/tests/v1_chain_tx_routes.rs` around lines 676 - 733, The test name is misleading because submit_rejection_maps_to_v1_envelope sends the request to /api/v1/transactions/check instead of the /submit endpoint. Either rename this test to match the /check route, or add a separate submit-path rejection test in v1_chain_tx_routes.rs that uses the same StubSubmit error case through app(Deps) and send(..., "/api/v1/transactions/submit", ...) to cover the /submit business-error behavior independently.
350-353: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winNo route-level coverage for auth-tier gating or active rate limiting.
This file's stated purpose is to be the "convention-lock" template for later route groups, and the PR objectives explicitly call out auth-tier handling and the per-IP governor as part of this group. However, every request here is sent from a loopback
ConnectInfo(governor-exempt) and no test exercisesrequire_tierrejection (e.g. missing/invalid API key → 401/403) or governor throttling behavior for a non-exempt IP against these mounted routes.Unit tests for
Governor/require_tierpresumably exist at the middleware layer, but that doesn't confirm the middleware is actually wired correctly onto these specific routes. Worth adding at least one integration case per concern (non-loopback IP hitting a rate limit, and a tier-gated route rejecting an unauthorized caller) to close this gap for the "template" this file is meant to establish.Also applies to: 658-733
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ergo-api/tests/v1_chain_tx_routes.rs` around lines 350 - 353, Add integration coverage in the route tests so this template verifies the middleware wiring, not just the happy path. In v1_chain_tx_routes.rs, keep the existing loopback-based cases but add at least one request against a non-loopback ConnectInfo to exercise Governor throttling, and another request to a require_tier-protected route with missing/invalid API key to confirm 401/403 rejection. Use the existing route setup helpers and request-building patterns in the test module so the new cases target the mounted endpoints directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ergo-api/src/v1/auth.rs`:
- Around line 132-140: The tiered auth check in auth middleware is failing open
because `check_key` returns `NoKeyConfigured` as allowed for both
`Tier::Operator` and `Tier::Admin`. Update the `Tier::Operator`/`Tier::Admin`
branches in `auth.rs` so that `NoKeyConfigured` is treated as a rejection and
returns an unauthorized response instead of calling `next.run`, and apply the
same change to the duplicate tiered-route logic referenced by the other affected
block.
In `@ergo-api/src/v1/error.rs`:
- Line 156: The RouteDisabled error variant is mapped inconsistently with the
`_disabled` status contract, so update the `ErrorReason` handling in `error.rs`
to keep `RouteDisabled` on the 409 `_disabled` path unless it is truly
transient. Adjust the match arm used by the status mapping logic (including the
related branches around the error/status conversion helpers) so `RouteDisabled`
resolves the same way as other disabled reasons, or rename it to an
`_unavailable`-style reason if you intend to keep 503 semantics.
In `@ergo-api/src/v1/governor.rs`:
- Around line 142-145: Governor::new currently accepts unvalidated
GovernorConfig values, allowing negative or non-finite knobs to break throttling
and even add tokens during cost subtraction. Add validation in Governor::new
(and any shared config initialization used by the related constructors around
the referenced locations) to reject negative route weights and zero/non-finite
refill or burst values before creating the Arc<Self>, returning or panicking
consistently with the existing API. Use the Governor and GovernorConfig symbols
to locate the constructor and enforce these checks at the boundary so invalid
configs never reach bucket/token logic.
- Around line 189-191: In the pruning path inside the governor bucket management
logic, the retain check is using stale token counts for idle entries, so one-off
IP buckets never become eligible for removal. Update the pruning flow around the
map.retain call to recompute each bucket’s virtual refill state before
evaluating whether it should be kept, using the same refill logic as the
current-IP path in the governor code. Make sure the condition in this pruning
branch uses the refreshed tokens value, so buckets idle past idle_prune_after
can actually be dropped and max_tracked_ips remains an effective bound.
In `@ergo-api/src/v1/mod.rs`:
- Around line 48-55: The client_ip() helper currently trusts only
ConnectInfo<SocketAddr>, which can make all traffic from a local reverse proxy
appear as loopback and bypass the governor exemption and Admin loopback checks.
Update the v1 request source handling in client_ip() and the related
startup/config path to support a trusted-proxy or PROXY-protocol source of the
real peer address, or explicitly reject deployments where the app is behind a
local reverse proxy. Make sure the fix preserves the security boundary around
loopback rather than inferring it from the proxy’s socket address.
In `@ergo-api/src/v1/routes/transactions.rs`:
- Around line 42-47: The parse_id32 helper currently accepts any 64-character
hex string, including uppercase or mixed-case IDs, which bypasses the
lowercase-only validation used elsewhere. Update parse_id32 to mirror
valid_modifier_id by checking that the input is ASCII lowercase before calling
hex::decode, while keeping the existing length and decode/try_into flow intact.
---
Nitpick comments:
In `@ergo-api/tests/v1_chain_tx_routes.rs`:
- Around line 676-733: The test name is misleading because
submit_rejection_maps_to_v1_envelope sends the request to
/api/v1/transactions/check instead of the /submit endpoint. Either rename this
test to match the /check route, or add a separate submit-path rejection test in
v1_chain_tx_routes.rs that uses the same StubSubmit error case through app(Deps)
and send(..., "/api/v1/transactions/submit", ...) to cover the /submit
business-error behavior independently.
- Around line 350-353: Add integration coverage in the route tests so this
template verifies the middleware wiring, not just the happy path. In
v1_chain_tx_routes.rs, keep the existing loopback-based cases but add at least
one request against a non-loopback ConnectInfo to exercise Governor throttling,
and another request to a require_tier-protected route with missing/invalid API
key to confirm 401/403 rejection. Use the existing route setup helpers and
request-building patterns in the test module so the new cases target the mounted
endpoints directly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 94078387-9529-416a-988d-19367c154857
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (17)
ergo-api/Cargo.tomlergo-api/src/auth.rsergo-api/src/blockchain.rsergo-api/src/blockchain/boxes.rsergo-api/src/blockchain/transactions.rsergo-api/src/lib.rsergo-api/src/server.rsergo-api/src/v1/auth.rsergo-api/src/v1/cursor.rsergo-api/src/v1/error.rsergo-api/src/v1/governor.rsergo-api/src/v1/mod.rsergo-api/src/v1/routes/chain.rsergo-api/src/v1/routes/dto.rsergo-api/src/v1/routes/mod.rsergo-api/src/v1/routes/transactions.rsergo-api/tests/v1_chain_tx_routes.rs
SECURITY: - auth.rs: require_tier fails CLOSED on NoKeyConfigured for Operator/Admin (401 unauthorized), Admin denies before the loopback check. Public unaffected. Matches the compat require_api_key credential-reject posture; an always-mounted admin gate (shutdown/secret-export) must DENY when no key is configured. The boot-warn stays a signal, not the gate. - mod.rs: is_trusted_loopback() is the single loopback-privilege authority; new local_reverse_proxy flag (default false) on GovernorConfig + V1AuthConfig withdraws socket-derived loopback trust (governor exemption AND Admin loopback-preferred) when an operator declares a local reverse proxy — no longer inferable from a proxy's socket. XFF not consulted. CORRECTNESS: - governor.rs: Governor::new -> Result<_, GovernorConfigError>; GovernorConfig:: validate rejects non-finite/non-positive refill+burst and non-finite/negative weights before any bucket math. - governor.rs: prune recomputes each bucket's virtual refill before retain, so idle-drained buckets that virtually refilled to full actually drop (max_tracked_ips is now an effective bound). - error.rs: Reason::RouteDisabled -> RouteUnavailable (wire route_unavailable, 503) — resolves the _disabled->409 vs _unavailable->503 suffix-rule inconsistency; 'route/bridge not wired' is transient = _unavailable. parse_id32 lowercase-only SKIPPED (superseded by #171 case-insensitive; also doesn't exist on this base branch). +14 unit tests (auth fail-closed matrix, governor validation/pruning/proxy, is_trusted_loopback). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
c447f44 to
3e338c1
Compare
…governor, auth tiers (G2) (#168) * feat(api): v1 error envelope + canonical reason enum (G2a) The first of the four G2 shared primitives every /api/v1/* endpoint inherits. A nested, machine-first error envelope {error:{reason,message,detail}} (design v1-api-design.md §1.3) replacing the frozen flat {reason,detail} compat shape, plus the canonical Reason enum (§1.4 / coherence Part A) with a pure reason->HTTP-status mapping. One enum, one spelling per concept, so a disabled subsystem answers its *_disabled reason instead of a bare 404. 111 variants (110 canonical + invalid_cursor, required by the §1.5 cursor codec). Status overrides off the mechanical suffix rule are documented at each arm (unsupported_intent ->422, compiler/oracle_unavailable->501, timeout->504) and pinned by an oracle-parity table test against the spec. Not mounted on any route; the first route-group PR consumes it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * feat(api): v1 opaque versioned cursor codec + page builder (G2b) The one cursor-pagination codec (design §1.5, coherence C.4): every v1 collection is cursor-paginated (?limit=&cursor=, never offset on the wire). Wire form is base64url-nopad(version-byte || compact-JSON payload), generic over a small CursorPayload each group picks (height key, global index, mempool keyset, ...). Opaqueness is load-bearing (locked decision D5): because clients treat the cursor as opaque, Phase-2 can swap a Phase-1 offset-alias payload for a stable-seek key without breaking clients; the version byte makes an incompatible future encoding fail closed (invalid_cursor) rather than mis-seek. Includes clamp_limit (per-group default/max from §2.2) and Page::from_overfetch (overfetch-by-one has_more, no total needed). Adds base64 (0.22, already in Cargo.lock via ergo-compiler) to ergo-api — no new crate. Tamper/version/shape rejections tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * feat(api): v1 per-IP rate/cost governor middleware (G2c) The load-bearing control behind the T0 'public but bounded' contract (design §2.1–§2.2): every open surface sits behind a per-IP token bucket whose refill/burst are config knobs, and each RouteClass (cheap read / heavy read / compute) consumes a class-weighted number of tokens. A depleted bucket answers 429 rate_limited (the §1.3 envelope) with a Retry-After header; the operator's own UI (loopback) is exempt by default. Dependency-light: a std HashMap<IpAddr,Bucket> behind a std Mutex, refilled lazily on access (no timer task), with opportunistic pruning of idle full buckets. Client IP comes from ConnectInfo (added shared client_ip helper; XFF deliberately untrusted); a connect-info gap falls back to one shared 'unknown' bucket, never an exemption. Attaches per subtree via from_fn_with_state(gov.state(class), governor_mw). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * feat(api): v1 T0/T1/T2 auth tier split + boot-warn (G2d) The exposure-tier gate (design §2.1): Public (no gate — governor-bounded), Operator (valid api_key), Admin (api_key AND loopback-preferred). Reuses the EXISTING api-key scheme — factored the Blake2b-256 + constant-time compare into ApiSecurity::verify and pointed the frozen compat gate (require_api_key) at it too, so there is one credential path node-wide. The v1 gate answers the §1.3 envelope (unauthorized/401) rather than the legacy 403 shape. Admin is warn-and-allow off-loopback by default (logged loudly), with a config flag to hard-deny remote admin ops (sensitive_op_disabled). Adds the startup boot-warn: assess_posture (pure, tested predicate) + warn_startup_posture, which screams when T1/T2 are network-reachable under no key or a known weak/default key (the shipped 'hello' template value). Not wired into server startup here (this PR changes no server behavior) — exported with its call site documented for the next PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * chore(review): address CodeRabbit #169 G2-foundation findings SECURITY: - auth.rs: require_tier fails CLOSED on NoKeyConfigured for Operator/Admin (401 unauthorized), Admin denies before the loopback check. Public unaffected. Matches the compat require_api_key credential-reject posture; an always-mounted admin gate (shutdown/secret-export) must DENY when no key is configured. The boot-warn stays a signal, not the gate. - mod.rs: is_trusted_loopback() is the single loopback-privilege authority; new local_reverse_proxy flag (default false) on GovernorConfig + V1AuthConfig withdraws socket-derived loopback trust (governor exemption AND Admin loopback-preferred) when an operator declares a local reverse proxy — no longer inferable from a proxy's socket. XFF not consulted. CORRECTNESS: - governor.rs: Governor::new -> Result<_, GovernorConfigError>; GovernorConfig:: validate rejects non-finite/non-positive refill+burst and non-finite/negative weights before any bucket math. - governor.rs: prune recomputes each bucket's virtual refill before retain, so idle-drained buckets that virtually refilled to full actually drop (max_tracked_ips is now an effective bound). - error.rs: Reason::RouteDisabled -> RouteUnavailable (wire route_unavailable, 503) — resolves the _disabled->409 vs _unavailable->503 suffix-rule inconsistency; 'route/bridge not wired' is transient = _unavailable. parse_id32 lowercase-only SKIPPED (superseded by #171 case-insensitive; also doesn't exist on this base branch). +14 unit tests (auth fail-closed matrix, governor validation/pruning/proxy, is_trusted_loopback). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx --------- Co-authored-by: arkadianet <rkadias@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The v1 `transactions/{tx_id}` handler reuses the existing extra-index
projection pipeline (`build_indexed_tx_response` / `build_indexed_box_response`)
rather than re-deriving box dereferencing and confirmation math, so the
confirmed-tx read shape can never drift from `/blockchain/transaction/byId`.
Widen the two builders from `pub(super)` to `pub(crate)` so the `v1` module
can call them; no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…e group
Mount the highest-priority self-sufficiency surface (`dev-docs/v1-api-design.md`
§3.5–§3.6) on the G2 shared primitives, and wire `/api/v1` into the server for
real. This is the first consumer of the v1 error envelope, cursor page builder,
and rate/cost governor, so it sets the handler/DTO/test template later groups
copy.
Endpoints (all T0, Phase-1, `HeavyRead` governor class):
- chain/* (11): blocks list (cursor-paginated full history), block-by-id,
block transactions, blocks at-height, blocks by-ids (POST, cap 200),
headers list, header-by-id, headers at-height, modifiers-by-id (adds the
explicit `kind` discriminant the untagged Scala BlockSection lacks),
ad-proofs section, and the tx Merkle membership proof (side byte → left/right).
- transactions/{tx_id}: unified confirmed (extra-index) + unconfirmed (mempool
overlay) read; `transactions/{submit,check}`: canonical submit paths (O1).
Conventions enforced: snake_case glossary field names verbatim, `value` as
string, `<name>_unix_ms` + `<name>_iso` timestamps (self-contained ISO helper,
oracle-pinned to known unix epochs), `{items, page}` collection envelope,
`{error:{reason,message,detail}}` with canonical reasons, and honest
`*_unavailable` / `*_disabled` gating instead of a bare 404 for a subsystem
that is off. The fee-proposition ErgoTree is oracle-pinned to the mainnet
test-vector fixture (no `ergo-mempool` dep pulled into the API crate).
Server wiring: `v1_router` merged under `/api/v1` with one shared governor per
node (later groups reuse the same per-IP budget); `warn_startup_posture` called
at the documented startup seam; the server now serves with
`into_make_service_with_connect_info` so the governor / auth tier can read the
real peer IP.
24 route-level integration tests lock the envelope, page, reason, and exact
field-name contracts. Gate green: fmt + clippy (-D warnings) + tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…available) Governor::new now returns Result (config validation); default config is statically valid → expect. Reason::RouteDisabled renamed RouteUnavailable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…Rabbit #169) parse_id32 length-checked then hex::decode'd, which accepts uppercase/mixed-case hex — decoding to identical bytes but leaving the tx-id validator misaligned with the chain routes' `valid_modifier_id` (64-char LOWERCASE hex). Reuse that shared validator so a non-canonical id is a `400 invalid_tx_id`, never a lookup. Adds a unit test pinning lowercase-accept / uppercase-and-mixed-reject / bad-len / non-hex. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
3e338c1 to
2cfcc40
Compare
…dr chain guard, token capped Four verified findings on the boxes/tokens/addresses group: - mod.rs parse_id32: enforce canonical lowercase hex via the shared `valid_modifier_id`, so `/boxes/*` and `/tokens/*` reject uppercase/mixed-case consistently with the tx routes (`hex::decode` alone accepts uppercase). - boxes.rs render_unspent_page: the offset cursor advances the CONFIRMED window, but `has_more` was read from the post-`exclude_spent` merged count — a pool-spent view filter could drop the (limit+1)th sentinel and report no next page, silently truncating paging. Capture the confirmed-overfetch signal BEFORE the retain and page from it (new `offset_page_explicit`; `offset_page` now delegates to it). - addresses.rs address transactions: guard the chain reader when there ARE txs to build, so a missing reader is an honest `chain_reader_unavailable` instead of a 500 from `build_indexed_tx_response`. Empty results need no chain (authoritative from the index), so the guard is scoped to the non-empty case. - tokens.rs scan_token_holders: base `capped` on the actual unspent scan hitting the cap, not `token_total_boxes` (which counts SPENT + unspent) — the latter wrongly flagged a COMPLETE scan as capped for tokens with many spent but few unspent boxes. Conservative: an exactly-cap unspent set may over-report, never under. Skipped (with reason): the tx_by_id/indexer() dedup nitpick (pure refactor of already-merged #169 code, out of scope for this PR) and the token_holders scan cache (a perf feature needing keyed+height-invalidated cached state, not a minimal fix; the scan is already bounded by HOLDER_SCAN_CAP). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
…hape) (#170) * refactor(api): expose blockchain pool-overlay + address helpers as pub(crate) The v1 boxes/tokens/addresses group reuses the confirmed-side readers and the unconfirmed pool-output overlays already proven on the /blockchain/* surface, rather than re-deriving them. Widen the (non-compat) helpers the new group needs to pub(crate) and re-export them — mirroring the prior group's builder exposure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * feat(api): extend shared V1Box + add box/token/address v1 DTOs The boxes group owns the canonical box shape, so it extends the existing shared V1Box rather than defining a second: `decoded` becomes omit-unless-requested (§0.3), and a canonical projector detects the pool sentinel (inclusion_height==0) to render an unconfirmed box with null on-chain metadata (§0.4). Adds V1Token, V1Balance, V1AddressTxSummary, the holders/stats DTOs, and CollectionMeta (the {items,page,meta} envelope, Part D). Shape locked by unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * feat(api): v1 boxes/* + tokens/* + addresses/* reads — second route group Implements §3.7 Phase-1 endpoints on the G2 primitives: box by-id/by-address/ by-ergo-tree/by-template/by-token (+unspent variants) + global-index range; token by-id/holders/stats + honest state_unavailable for the not-yet-indexable mint-order list (G3); address balance/transactions, with boxes/unspent dual- mounted at addresses/* (O10, one handler). v1 mounts unconditionally and gates inside each handler (indexer_disabled/_syncing/_halted), never a bare 404 for a disabled subsystem. Single by-id reads are CheapRead; paginated/scan/range are HeavyRead. 22 route-level tests lock envelope/page/reason/field-names + the dual-mount agreement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * chore(rebase): adapt boxes group to G2 Governor::new Result Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx * fix(api): address CodeRabbit #170 — parse_id32 case, boxes paging, addr chain guard, token capped Four verified findings on the boxes/tokens/addresses group: - mod.rs parse_id32: enforce canonical lowercase hex via the shared `valid_modifier_id`, so `/boxes/*` and `/tokens/*` reject uppercase/mixed-case consistently with the tx routes (`hex::decode` alone accepts uppercase). - boxes.rs render_unspent_page: the offset cursor advances the CONFIRMED window, but `has_more` was read from the post-`exclude_spent` merged count — a pool-spent view filter could drop the (limit+1)th sentinel and report no next page, silently truncating paging. Capture the confirmed-overfetch signal BEFORE the retain and page from it (new `offset_page_explicit`; `offset_page` now delegates to it). - addresses.rs address transactions: guard the chain reader when there ARE txs to build, so a missing reader is an honest `chain_reader_unavailable` instead of a 500 from `build_indexed_tx_response`. Empty results need no chain (authoritative from the index), so the guard is scoped to the non-empty case. - tokens.rs scan_token_holders: base `capped` on the actual unspent scan hitting the cap, not `token_total_boxes` (which counts SPENT + unspent) — the latter wrongly flagged a COMPLETE scan as capped for tokens with many spent but few unspent boxes. Conservative: an exactly-cap unspent set may over-report, never under. Skipped (with reason): the tx_by_id/indexer() dedup nitpick (pure refactor of already-merged #169 code, out of scope for this PR) and the token_holders scan cache (a perf feature needing keyed+height-invalidated cached state, not a minimal fix; the scan is already bounded by HOLDER_SCAN_CAP). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx --------- Co-authored-by: arkadianet <rkadias@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
What this is
The first route group of
/api/v1/*and the PR that mounts/api/v1for real —chain/*+transactions/*reads, the highest-priority self-sufficiency gap (today a v1 client must drop to the compat surface for these). It's the first consumer of the G2 primitives, so it sets the handler/envelope/tier/test pattern every later group copies.Endpoints (all T0, Phase-1, governor
HeavyReadclass)chain/*(all 11): blocks list / by-id / by-id txs / at-height / by-ids; headers list / by-id / at-height; modifiers (explicitkind); ad-proofs; tx Merkle proof (left/right).transactions/*reads:{tx_id}(unified confirmed + unconfirmed, output-derived fee,confirmedflag);{submit,check}canonical (Appendix A O1).{tx_id}/detailalready shipped — left as-is.Everything uses the v1 nested error envelope + canonical reasons, cursor page builder, snake_case glossary names, and the shared governor. Server now serves with
into_make_service_with_connect_infoso the governor/auth read the real peer IP, andwarn_startup_postureis called at the startup seam.Honest, not stubbed
Where a capability isn't wired yet the endpoint returns the true reason, never fake data or new schema:
chain_reader_unavailable,indexer_disabled(+indexer_syncing/indexer_halted),submit_disabled,ad_proofs_unavailable(pruned),delivered_by: nulloutside the near-tip ring.Design/hook corrections (flagged for the record)
timestamp_unix_ms+timestamp_iso/size_bytesper the synthesis doc (overrides the pre-coherence fragment'stimestamp/size).ergo_ser::address::encode_p2pk_from_pubkey(ergo-api has no ergo-wallet dep — the cite was stale).test-vectors/mainnet/fee_proposition.hex(avoids pullingergo-mempoolinto the API crate).nest("/api/v1")— the operator router already carries flat/api/v1/*routes a nest would conflict with; behaviorally identical.modifier_not_found→ missing modifier answersblock_not_found, missing prooftx_not_in_block.The
V1Boxshape here is the transactions-group projection; the futureboxes/*group owns the canonical box shape and MUST match these field names (they're already glossary-exact:box_id/value-as-string/ergo_tree/creation_height/registers/output_index/spent_by/global_index/assets).Test plan
ergo-api/tests/v1_chain_tx_routes.rs— 24 route-level tests locking envelope/page/reason/exact snake_case field names against the design schemas (the convention-lock, enforced), plus lib unit tests (ISO-timestamp oracle vectors, fee-proposition oracle-pin, submit-reason map). Compat routes untouched.Deferred (scoped)
tx build/simulate/fee-estimate/status (Phase-2 tx-intelligence group);
mempool/{submit,check}alias (ships with the mempool group); OpenAPI aggregator registration of the new routes (§1.7 follow-up pass).🤖 Generated with Claude Code
https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
Summary by CodeRabbit
New Features
/api/v1API surface for blocks, headers, proofs, and transactions.Bug Fixes