Skip to content

feat(api): v1 mempool/* group + O4 depth ring — closes the read-gap (+ UI migration) - #171

Merged
arkadianet merged 5 commits into
mainfrom
feat/v1-api-mempool
Jul 8, 2026
Merged

feat(api): v1 mempool/* group + O4 depth ring — closes the read-gap (+ UI migration)#171
arkadianet merged 5 commits into
mainfrom
feat/v1-api-mempool

Conversation

@arkadianet

@arkadianet arkadianet commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Stacked on #170#169#168. Merge those first.

What this is

The mempool/* v1 route group — the last read-gap piece. After this, a v1 client never needs the compat surface for reads.

⚠️ This group REPLACES pre-existing routes (read before merging)

Unlike the prior additive groups, some flat /api/v1/mempool/* handlers already existed in server.rs (bare Json, no envelope/cursor — a pre-v1 stopgap). They collided with the v1 paths, so they're removed and ownership moves to v1_router; the OpenAPI golden was regenerated and 3 coupled tests migrated to the v1 shapes. Compat surface untouched. The only externally-visible shape change is mempool/transactions: flat {transactions} → v1 {items, page}; the web UI mempool panel is migrated in the same PR (one-line .transactions.items; summary field names are unchanged, so the rest of the panel is untouched). Nothing breaks on merge.

Endpoints (§3.8, all T0)

summary (+ utilization, weight_function, and the O4 history), transactions (keyset-cursor list, ?order=), transactions/{tx_id} (row + io_box), by-address, by-ergo-tree, by-box-id, by-token-id, fee-histogram, and submit/check as O1 aliases (the same canonical handler from the chain/tx group, second mount — no duplicated logic).

O4 — mempool-depth ring (shared infra)

New dependency-light mempool_depth.rs: a 512-sample FIFO ring (Mutex<VecDeque>, monotonic seq) fed by a background sampler wired in server.rs (guarded on a live Tokio runtime so test router builds don't spawn it). Lives on V1State, re-exported from ergo_api::v1, projected via V1MempoolDepthPointthe exact shape the future stats/mempool-depth endpoint will consume from the same Arc (documented).

Design/hook corrections (relay)

  • source emits the real ApiTxSource taxonomy (peer|api|wallet|demoted_from_block) — the fragment's propagated|local|… never matched the impl.
  • first_seen uses the §1.2 flat *_unix_ms/*_iso rule (not the fragment's nested object).
  • fee-histogram fee_per_byte_min/max is honestly null (the frozen hook is wait-time-keyed, no per-bin fee bounds).
  • submit/check: always-mounted 409 submit_disabled (was conditional-404); by-*mempool_view_disabled when the chain bridge is absent.

Test plan

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace     # 5767 passed / 0 failed

ergo-api/tests/v1_mempool_routes.rs (17 tests) + the migrated mempool_source_schema/submit_routes/openapi_native_runtime_mount. Compat untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

Summary by CodeRabbit

  • New Features
    • Rolled out the /api/v1 product API with standardized error envelopes, cursor pagination, per-IP rate limiting, and tiered authentication.
    • Added v1 browsing for chain, transactions, boxes, tokens, addresses, and mempool (including summary, depth sampling, fee histogram, and filtered mempool listings).
  • Bug Fixes
    • Updated API security verification and aligned mempool/transactions response and schema behavior.
    • Fixed the web mempool list to use the correct response field.
  • Tests
    • Expanded v1 routing and mempool regression coverage for the new wire formats and pagination rules.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@arkadianet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c2d247e2-b76e-4106-9a2e-628bf429621b

📥 Commits

Reviewing files that changed from the base of the PR and between 32437b3 and 8398c1c.

📒 Files selected for processing (22)
  • ergo-api/src/server.rs
  • ergo-api/src/traits.rs
  • ergo-api/src/v1/mempool_depth.rs
  • ergo-api/src/v1/mod.rs
  • ergo-api/src/v1/routes/addresses.rs
  • ergo-api/src/v1/routes/boxes.rs
  • ergo-api/src/v1/routes/chain.rs
  • ergo-api/src/v1/routes/dto.rs
  • ergo-api/src/v1/routes/extract.rs
  • ergo-api/src/v1/routes/mempool.rs
  • ergo-api/src/v1/routes/mod.rs
  • ergo-api/src/v1/routes/tokens.rs
  • ergo-api/src/v1/routes/transactions.rs
  • ergo-api/tests/fixtures/openapi_native.yaml
  • ergo-api/tests/mempool_source_schema.rs
  • ergo-api/tests/openapi_native_runtime_mount.rs
  • ergo-api/tests/submit_routes.rs
  • ergo-api/tests/v1_boxes_tokens_addresses_routes.rs
  • ergo-api/tests/v1_chain_tx_routes.rs
  • ergo-api/tests/v1_mempool_routes.rs
  • ergo-api/web/js/mempool.js
  • ergo-node/src/api_bridge.rs
📝 Walkthrough

Walkthrough

This PR adds a new /api/v1/* product API in ergo-api, with shared error, cursor, auth, governor, and mempool-depth primitives, new route modules for chain, transactions, mempool, boxes, tokens, and addresses, server/router wiring, and updated tests and UI handling for the new response shapes.

Changes

V1 Product API

Layer / File(s) Summary
Shared primitives
ergo-api/src/lib.rs, ergo-api/src/traits.rs, ergo-api/src/auth.rs, ergo-api/src/blockchain*, ergo-node/src/api_bridge.rs, ergo-api/src/v1/cursor.rs, ergo-api/src/v1/error.rs, ergo-api/src/v1/auth.rs, ergo-api/src/v1/governor.rs, ergo-api/src/v1/mempool_depth.rs
Adds the v1 module, shared cursor/error/auth/governor/depth primitives, refactors API-key verification, widens blockchain helper visibility, and adds the mempool weight-function trait method plus node implementation.
v1 route surface
ergo-api/src/v1/mod.rs, ergo-api/src/v1/routes/mod.rs, ergo-api/src/v1/routes/extract.rs, ergo-api/src/v1/routes/dto.rs, ergo-api/src/v1/routes/chain.rs, ergo-api/src/v1/routes/transactions.rs, ergo-api/src/v1/routes/mempool.rs, ergo-api/src/v1/routes/boxes.rs, ergo-api/src/v1/routes/tokens.rs, ergo-api/src/v1/routes/addresses.rs
Defines shared v1 state and extractors, canonical DTOs/projections, and the chain/transactions/mempool/boxes/tokens/addresses handlers under /api/v1/*.
Server wiring and UI
ergo-api/src/server.rs, ergo-api/web/js/mempool.js
Mounts the v1 router with connect-info, starts depth sampling, removes native mempool/submit/check routing, and switches the mempool UI to the new items envelope.
Tests and fixtures
ergo-api/tests/fixtures/openapi_native.yaml, ergo-api/tests/openapi_native_runtime_mount.rs, ergo-api/tests/submit_routes.rs, ergo-api/tests/mempool_source_schema.rs, ergo-api/tests/v1_chain_tx_routes.rs, ergo-api/tests/v1_mempool_routes.rs, ergo-api/tests/v1_boxes_tokens_addresses_routes.rs
Updates OpenAPI/runtime-mount expectations and adds integration coverage for the new v1 route groups and alias behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server as server.rs
  participant V1Router as v1_router
  participant Governor as governor_mw
  participant Route as v1 handler
  Client->>Server: request /api/v1/...
  Server->>V1Router: merge mounted router
  V1Router->>Governor: apply RouteClass middleware
  Governor->>Route: allow request
  Route-->>Client: JSON response or v1_error
Loading

Possibly related PRs

  • arkadianet/ergo#88: Both changes touch ergo-api v1 auth and operator routing, including API-key verification and protected v1 endpoints.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the v1 mempool route group and O4 depth ring, plus the UI migration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v1-api-mempool

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (6)
ergo-api/src/v1/routes/chain.rs (1)

144-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the discard pattern.

if let Some(bad) = ... { let _ = bad; return invalid_hex(); } can be replaced with .any(...) since the matched id is unused.

♻️ Proposed simplification
-    if let Some(bad) = ids.iter().find(|id| !valid_modifier_id(id)) {
-        let _ = bad;
-        return invalid_hex();
-    }
+    if ids.iter().any(|id| !valid_modifier_id(id)) {
+        return invalid_hex();
+    }
🤖 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/src/v1/routes/chain.rs` around lines 144 - 147, The
`ids.iter().find(...)` result is only used to check for the presence of an
invalid modifier id, and the matched value is discarded in `chain.rs`’s
validation block. Replace the `if let Some(bad)` pattern in that section with a
direct `.any(...)` check against `valid_modifier_id` so the code expresses the
intent without binding an unused variable, while keeping the same
`invalid_hex()` early return behavior.
ergo-api/src/v1/routes/transactions.rs (1)

285-292: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Unnecessary intermediate allocation in fee computation.

Collecting into a Vec<(String, u64)> then re-mapping to (&str, u64) just to satisfy fee_from_hex_values's lifetime bound is more roundabout than needed; hex::encode output could be sliced directly with .as_str() inline in a single pass, or fee_from_hex_values could take an owned-string iterator.

♻️ Proposed simplification
-    let fee = fee_from_hex_values(
-        tx.output_candidates
-            .iter()
-            .map(|c| (hex::encode(c.ergo_tree_bytes()), c.value))
-            .collect::<Vec<_>>()
-            .iter()
-            .map(|(t, v)| (t.as_str(), *v)),
-    );
+    let fee: u64 = tx
+        .output_candidates
+        .iter()
+        .filter(|c| hex::encode(c.ergo_tree_bytes()) == FEE_PROPOSITION_ERGO_TREE_HEX)
+        .map(|c| c.value)
+        .sum();
🤖 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/src/v1/routes/transactions.rs` around lines 285 - 292, The fee
calculation in transactions::fee_from_hex_values is doing an unnecessary Vec
allocation and second pass just to convert encoded trees into borrowed strings.
Simplify the tx.output_candidates iterator pipeline so it feeds
fee_from_hex_values in a single pass without collecting first, either by passing
the hex::encode result directly as a borrowed string where possible or by
changing fee_from_hex_values to accept owned String values. Keep the change
localized around the fee computation in the transactions route and preserve the
existing fee behavior.
ergo-api/src/v1/routes/mod.rs (1)

1-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Router doc omits the mempool/* group it mounts.

The module and v1_router doc comments describe chain/*, transactions/*, boxes/*, tokens/*, addresses/*, but never mention mempool/* — even though lines 299-367 mount an extensive mempool route set (summary, transactions, by-*, fee-histogram, submit/check aliases). Given this PR's stated purpose is adding the mempool group, the docs should reflect it.

Also applies to: 284-293

🤖 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/src/v1/routes/mod.rs` around lines 1 - 13, The route-group
documentation in the `mod.rs` module comment and the `v1_router` docs is missing
the newly mounted `mempool/*` group. Update the comments near `v1_router` and
the module header to explicitly include `mempool/*` alongside the other mounted
groups, reflecting the full set of routes now returned by `v1_router` (including
the mempool summary, transactions, by-*, fee-histogram, and submit/check
aliases).
ergo-api/tests/v1_mempool_routes.rs (1)

436-445: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

io_box population is never tested with real data.

detail_returns_row_plus_io_arrays only exercises NoopMempoolView, so inputs/outputs are always empty. The transactions/{tx_id} io_box resolution — called out as a headline new feature in the PR objectives — has no test asserting real input/output rows are rendered.

Consider adding a minimal MempoolView stub returning a populated input/output for at least one tx id to cover the resolution path.

🤖 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_mempool_routes.rs` around lines 436 - 445, The existing
`detail_returns_row_plus_io_arrays` test only covers `NoopMempoolView`, so it
never verifies real `io_box` resolution for `transactions/{tx_id}`. Add a
focused test alongside this one that uses a minimal `MempoolView` stub returning
populated input/output data for a known tx id, and assert that `inputs` and
`outputs` in the response contain rendered rows rather than just empty arrays.
Use the `app()` and `get(...)` route setup from the current test as the
reference point, and target the `io_box`/transaction detail path exercised by
`detail_returns_row_plus_io_arrays`.
ergo-api/tests/v1_boxes_tokens_addresses_routes.rs (1)

377-469: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared StubRead (and harness helpers) into a common test-support module.

This StubRead implementation of NodeReadState is duplicated almost verbatim across v1_boxes_tokens_addresses_routes.rs, v1_chain_tx_routes.rs, and v1_mempool_routes.rs (same fields, same boilerplate values). The send/get/reason harness functions are likewise near-identical across the three files. Any future change to NodeReadState requires updating every copy in lockstep.

Consider hoisting these into a shared tests/common/mod.rs (or tests/support.rs included via #[path]) and importing it from each integration test binary.

🤖 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_boxes_tokens_addresses_routes.rs` around lines 377 - 469,
The `StubRead` implementation and the `send`/`get`/`reason` harness helpers are
duplicated across multiple route test files, so factor them into a shared
test-support module and reuse them from each integration test. Move the common
`NodeReadState` stub and helper functions into a shared location such as
`tests/common/mod.rs` or a `tests/support.rs` module, then update
`v1_boxes_tokens_addresses_routes`, `v1_chain_tx_routes`, and
`v1_mempool_routes` to import the shared symbols instead of maintaining separate
copies.
ergo-api/src/v1/routes/tokens.rs (1)

24-30: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Cache the bounded holder scan for repeated /holders and /stats calls
scan_token_holders runs independently in both handlers, so pagination and stats requests replay the same bounded O(n) scan. Caching the (token_id, as_of_height) result would avoid repeating the CPU/I/O work on hot tokens.

🤖 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/src/v1/routes/tokens.rs` around lines 24 - 30, `scan_token_holders`
is being executed separately by the `/holders` and `/stats` handlers, so
repeated requests re-run the same bounded scan work. Add a shared cache keyed by
`(token_id, as_of_height)` around the bounded holder scan path and have both
handlers read from it before invoking `scan_token_holders`; keep the cache
lookup/store near the existing `HOLDER_SCAN_CAP`/`SCAN_BATCH` flow so hot-token
pagination and stats reuse the same result.
🤖 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/server.rs`:
- Around line 1194-1223: The mempool-depth sampler is being started from the
router assembly path and its JoinHandle is dropped immediately, so repeated
calls to `server.rs` router setup can spawn duplicate detached tasks. Update the
`spawn_depth_sampler` usage in the `v1_mempool_depth` setup to keep the returned
handle somewhere owned by the process or to guard startup so it runs only once
per process, using the existing `MempoolDepthRing`/`spawn_depth_sampler` symbols
to locate the code.

In `@ergo-api/src/v1/routes/boxes.rs`:
- Around line 11-17: The v1 box routes still let extractor failures bypass the
standard error shape, so malformed JSON or invalid query parameters can return
Axum’s default rejection body instead of v1_error(...). Update the extraction
path used by boxes_by_ergo_tree and boxes_unspent_by_ergo_tree to wrap
body/query parsing in the v1 error envelope, either by introducing a v1
extractor wrapper or by explicitly handling JsonRejection and QueryRejection in
the route handlers so all failures return the same response format.
- Around line 211-261: The `render_unspent_page` flow can underfill pagination
when `exclude_mempool_spent` filters the overfetched confirmed results before
`offset_page`, causing `has_more` to be computed incorrectly. Fix this by
applying the spent-box filter inside the confirmed paging/fetch path (or by
continuing to overfetch until you have `limit + 1` surviving `IndexedErgoBox`
items) so `offset_page` receives a correctly sized window. Keep the change
localized around `render_unspent_page`, `fetch_confirmed`, and the
`exclude_spent` handling.

In `@ergo-api/src/v1/routes/mempool.rs`:
- Around line 90-97: MempoolCursor currently only stores the keyset values and
can be replayed across different order= requests, which can cause skipped or
duplicated rows. Update the cursor payload in mempool.rs’s MempoolCursor to
include the selected order, then validate that the decoded cursor order matches
the requested order before using it in the mempool route logic so mismatched
cursors fail closed.

In `@ergo-api/src/v1/routes/mod.rs`:
- Around line 143-150: The 64-char id validation in valid_modifier_id is
inconsistent with parse_id32, since one path enforces lowercase-only while the
other accepts mixed case. Update the route-level validation and related
docs/contracts used by the box/token/tx parsing flow so they all follow the same
rule: either reject uppercase everywhere or explicitly allow case-insensitive
ids everywhere, using valid_modifier_id and parse_id32 as the key entry points.

In `@ergo-api/src/v1/routes/tokens.rs`:
- Around line 107-165: The scan cap flag in scan_token_holders is being derived
from token_total_boxes, which includes spent boxes and can misreport capped
scans for high-turnover tokens. Update HolderScan/scan_token_holders so the
capped flag is determined from the unspent pagination behavior in
token_unspent_paged and the scan loop result, while leaving box_count based on
token_total_boxes; use scan_token_holders and HolderScan as the main places to
adjust.

---

Nitpick comments:
In `@ergo-api/src/v1/routes/chain.rs`:
- Around line 144-147: The `ids.iter().find(...)` result is only used to check
for the presence of an invalid modifier id, and the matched value is discarded
in `chain.rs`’s validation block. Replace the `if let Some(bad)` pattern in that
section with a direct `.any(...)` check against `valid_modifier_id` so the code
expresses the intent without binding an unused variable, while keeping the same
`invalid_hex()` early return behavior.

In `@ergo-api/src/v1/routes/mod.rs`:
- Around line 1-13: The route-group documentation in the `mod.rs` module comment
and the `v1_router` docs is missing the newly mounted `mempool/*` group. Update
the comments near `v1_router` and the module header to explicitly include
`mempool/*` alongside the other mounted groups, reflecting the full set of
routes now returned by `v1_router` (including the mempool summary, transactions,
by-*, fee-histogram, and submit/check aliases).

In `@ergo-api/src/v1/routes/tokens.rs`:
- Around line 24-30: `scan_token_holders` is being executed separately by the
`/holders` and `/stats` handlers, so repeated requests re-run the same bounded
scan work. Add a shared cache keyed by `(token_id, as_of_height)` around the
bounded holder scan path and have both handlers read from it before invoking
`scan_token_holders`; keep the cache lookup/store near the existing
`HOLDER_SCAN_CAP`/`SCAN_BATCH` flow so hot-token pagination and stats reuse the
same result.

In `@ergo-api/src/v1/routes/transactions.rs`:
- Around line 285-292: The fee calculation in transactions::fee_from_hex_values
is doing an unnecessary Vec allocation and second pass just to convert encoded
trees into borrowed strings. Simplify the tx.output_candidates iterator pipeline
so it feeds fee_from_hex_values in a single pass without collecting first,
either by passing the hex::encode result directly as a borrowed string where
possible or by changing fee_from_hex_values to accept owned String values. Keep
the change localized around the fee computation in the transactions route and
preserve the existing fee behavior.

In `@ergo-api/tests/v1_boxes_tokens_addresses_routes.rs`:
- Around line 377-469: The `StubRead` implementation and the
`send`/`get`/`reason` harness helpers are duplicated across multiple route test
files, so factor them into a shared test-support module and reuse them from each
integration test. Move the common `NodeReadState` stub and helper functions into
a shared location such as `tests/common/mod.rs` or a `tests/support.rs` module,
then update `v1_boxes_tokens_addresses_routes`, `v1_chain_tx_routes`, and
`v1_mempool_routes` to import the shared symbols instead of maintaining separate
copies.

In `@ergo-api/tests/v1_mempool_routes.rs`:
- Around line 436-445: The existing `detail_returns_row_plus_io_arrays` test
only covers `NoopMempoolView`, so it never verifies real `io_box` resolution for
`transactions/{tx_id}`. Add a focused test alongside this one that uses a
minimal `MempoolView` stub returning populated input/output data for a known tx
id, and assert that `inputs` and `outputs` in the response contain rendered rows
rather than just empty arrays. Use the `app()` and `get(...)` route setup from
the current test as the reference point, and target the `io_box`/transaction
detail path exercised by `detail_returns_row_plus_io_arrays`.
🪄 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: fb714446-3047-48af-a349-1b277d444d62

📥 Commits

Reviewing files that changed from the base of the PR and between 39cb0d6 and 2afd04c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • ergo-api/Cargo.toml
  • ergo-api/src/auth.rs
  • ergo-api/src/blockchain.rs
  • ergo-api/src/blockchain/balance.rs
  • ergo-api/src/blockchain/boxes.rs
  • ergo-api/src/blockchain/bytemplate.rs
  • ergo-api/src/blockchain/tokens.rs
  • ergo-api/src/blockchain/transactions.rs
  • ergo-api/src/blockchain/unspent_byaddress.rs
  • ergo-api/src/lib.rs
  • ergo-api/src/server.rs
  • ergo-api/src/traits.rs
  • ergo-api/src/v1/auth.rs
  • ergo-api/src/v1/cursor.rs
  • ergo-api/src/v1/error.rs
  • ergo-api/src/v1/governor.rs
  • ergo-api/src/v1/mempool_depth.rs
  • ergo-api/src/v1/mod.rs
  • ergo-api/src/v1/routes/addresses.rs
  • ergo-api/src/v1/routes/boxes.rs
  • ergo-api/src/v1/routes/chain.rs
  • ergo-api/src/v1/routes/dto.rs
  • ergo-api/src/v1/routes/mempool.rs
  • ergo-api/src/v1/routes/mod.rs
  • ergo-api/src/v1/routes/tokens.rs
  • ergo-api/src/v1/routes/transactions.rs
  • ergo-api/tests/fixtures/openapi_native.yaml
  • ergo-api/tests/mempool_source_schema.rs
  • ergo-api/tests/openapi_native_runtime_mount.rs
  • ergo-api/tests/submit_routes.rs
  • ergo-api/tests/v1_boxes_tokens_addresses_routes.rs
  • ergo-api/tests/v1_chain_tx_routes.rs
  • ergo-api/tests/v1_mempool_routes.rs
  • ergo-api/web/js/mempool.js
  • ergo-node/src/api_bridge.rs

Comment thread ergo-api/src/server.rs
Comment thread ergo-api/src/v1/governor.rs
Comment thread ergo-api/src/v1/routes/boxes.rs Outdated
Comment thread ergo-api/src/v1/routes/boxes.rs
Comment thread ergo-api/src/v1/routes/mempool.rs Outdated
Comment thread ergo-api/src/v1/routes/mod.rs Outdated
Comment thread ergo-api/src/v1/routes/tokens.rs
arkadianet pushed a commit that referenced this pull request Jul 7, 2026
- extractor envelope (swept): shared V1Query/V1Json wrappers (v1/routes/extract.rs)
  replace 19 Query + 3 Json sites so malformed body/query returns the v1 envelope,
  not axum's default rejection.
- pagination underfill: render_unspent_page overfetches until limit+1 rows SURVIVE
  the exclude-mempool-spent filter; cursor advances by rows consumed (no underfill/dupe).
- cursor/order replay: MempoolCursor stamps the order tag, fails closed (invalid_cursor)
  on mismatch.
- depth-sampler duplicate-spawn: AtomicBool once-guard (test suite builds routers many
  times under a live runtime).
- scan_capped derived from the unspent scan hitting the cap, not spent-inclusive
  token_total_boxes.
- id-case: valid_modifier_id relaxed to case-insensitive to match parse_id32 + the
  compat/wallet surfaces (ecosystem sends mixed-case hex).
- chain.rs .any(); mod.rs docs (+mempool group); single-pass fee_from_hex_values;
  populated-MempoolView io_box detail test.
- SKIPPED: holder-scan cache (design-deferred, #170); test-support dedup (premise false
  — stubs/ helpers diverge, a shared module trips -D warnings dead_code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
arkadianet pushed a commit that referenced this pull request Jul 7, 2026
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
arkadianet pushed a commit that referenced this pull request Jul 7, 2026
- extractor envelope (swept): shared V1Query/V1Json wrappers (v1/routes/extract.rs)
  replace 19 Query + 3 Json sites so malformed body/query returns the v1 envelope,
  not axum's default rejection.
- pagination underfill: render_unspent_page overfetches until limit+1 rows SURVIVE
  the exclude-mempool-spent filter; cursor advances by rows consumed (no underfill/dupe).
- cursor/order replay: MempoolCursor stamps the order tag, fails closed (invalid_cursor)
  on mismatch.
- depth-sampler duplicate-spawn: AtomicBool once-guard (test suite builds routers many
  times under a live runtime).
- scan_capped derived from the unspent scan hitting the cap, not spent-inclusive
  token_total_boxes.
- id-case: valid_modifier_id relaxed to case-insensitive to match parse_id32 + the
  compat/wallet surfaces (ecosystem sends mixed-case hex).
- chain.rs .any(); mod.rs docs (+mempool group); single-pass fee_from_hex_values;
  populated-MempoolView io_box detail test.
- SKIPPED: holder-scan cache (design-deferred, #170); test-support dedup (premise false
  — stubs/ helpers diverge, a shared module trips -D warnings dead_code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
@arkadianet
arkadianet force-pushed the feat/v1-api-mempool branch from 8235a68 to 32437b3 Compare July 7, 2026 18:13
arkadianet pushed a commit that referenced this pull request Jul 7, 2026
The #171 CodeRabbit wave added a populated-MempoolView detail test after the
realtime group forked; its V1State construction needs the realtime: None field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
arkadianet added a commit that referenced this pull request Jul 8, 2026
…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>
arkadianet and others added 5 commits July 8, 2026 17:56
Shared infra for the mempool/* group and the future stats/mempool-depth
endpoint (Appendix A O4 — one ring, two surfaces).

- MempoolDepthRing: bounded (512) FIFO ring with monotonic per-sample seq,
  mirroring the operator event ring; dependency-light (std VecDeque + Mutex),
  a passive store fed fully-formed observations so it is trivially unit-tested.
- spawn_depth_sampler: background sampler (30s cadence) fed from mempool_summary
  + min-fee; the server wiring guards the spawn on a live runtime so non-async
  router builds never touch it. sample_into is exported for the stats consumer.
- NodeReadState::mempool_weight_function(): defaulted (reads mempool_transactions
  so no implementer breaks), overridden O(1) in the node's SnapshotReadState so
  mempool/summary + fee-histogram don't clone the pool list for one enum field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
The last read-gap group: summary (+utilization, weight_function, O4 depth
history), cursor-paginated transactions, single-tx detail with io_box, the four
by-* filtered views, and the fee histogram — all on the v1 envelope + keyset
(priority_weight, tx_id) cursor + canonical reasons.

This REPLACES the pre-v1 native /api/v1/mempool/* handlers (they collided with
the v1 paths): summary/transactions[/{tx_id}] and the conditional submit/check
routes are removed from server.rs (handlers + utoipa paths + OpenAPI derive,
golden fixture regenerated) and ownership moves to v1_router. submit/check are
Overlap-O1 aliases: the SAME transactions::{submit,check} handlers, second
mount, now always-mounted answering 409 submit_disabled instead of a bare 404.

Corrections vs the fragment: source emits the REAL ApiTxSource taxonomy
(peer|api|wallet|demoted_from_block) as a flat string, not the speculative
propagated|local|... set; first_seen uses the §1.2 flat *_unix_ms/*_iso rule;
the fee-histogram fee_per_byte band is honestly null (the frozen wait-time hook
carries no per-bin fee bounds); by-* answer mempool_view_disabled when the
chain-reader bridge is absent.

Migrated the coupled tests to the v1 shapes (mempool_source_schema, submit_routes,
openapi_native_runtime_mount) + new tests/v1_mempool_routes.rs. Compat frozen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
/api/v1/mempool/transactions now returns {items,page} (v1 collection shape)
instead of the flat {transactions}; the summary fields (size/capacity_count/
total_bytes/capacity_bytes) are unchanged, so only the tx-list unwrap moves
.transactions -> .items. Keeps the mempool panel working across the API change
(design §6 UI migration, done incrementally per endpoint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
- extractor envelope (swept): shared V1Query/V1Json wrappers (v1/routes/extract.rs)
  replace 19 Query + 3 Json sites so malformed body/query returns the v1 envelope,
  not axum's default rejection.
- pagination underfill: render_unspent_page overfetches until limit+1 rows SURVIVE
  the exclude-mempool-spent filter; cursor advances by rows consumed (no underfill/dupe).
- cursor/order replay: MempoolCursor stamps the order tag, fails closed (invalid_cursor)
  on mismatch.
- depth-sampler duplicate-spawn: AtomicBool once-guard (test suite builds routers many
  times under a live runtime).
- scan_capped derived from the unspent scan hitting the cap, not spent-inclusive
  token_total_boxes.
- id-case: valid_modifier_id relaxed to case-insensitive to match parse_id32 + the
  compat/wallet surfaces (ecosystem sends mixed-case hex).
- chain.rs .any(); mod.rs docs (+mempool group); single-pass fee_from_hex_values;
  populated-MempoolView io_box detail test.
- SKIPPED: holder-scan cache (design-deferred, #170); test-support dedup (premise false
  — stubs/ helpers diverge, a shared module trips -D warnings dead_code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
@arkadianet
arkadianet force-pushed the feat/v1-api-mempool branch from 32437b3 to 8398c1c Compare July 8, 2026 08:07
@arkadianet

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@arkadianet
arkadianet merged commit c25fd2c into main Jul 8, 2026
9 checks passed
arkadianet pushed a commit that referenced this pull request Jul 8, 2026
The #171 CodeRabbit wave added a populated-MempoolView detail test after the
realtime group forked; its V1State construction needs the realtime: None field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
arkadianet added a commit that referenced this pull request Jul 8, 2026
…webhooks follow-up) (#172)

* feat(api): v1 real-time subscriptions — RealtimeBus + WS /api/v1/ws (§4.1, G5)

The single biggest net-new of the v1 API: the push surface every Ergo node
lacks today (everything polls). This ships the WS core + the load-bearing
RealtimeBus fan-out hub; webhooks (the durable sibling) are a separate
follow-up and are deliberately NOT built here.

RealtimeBus (ergo-api/src/v1/realtime/) is a concrete shared type, mirroring
the O4 depth-ring precedent rather than the fragment's "consumed trait"
framing — the fan-out hub's drop policy, per-key filtering, and backfill window
are directly unit-testable, and node-side taps simply call `publish`. Per
subscriber it holds a bounded tokio mpsc queue (256 frames) + a shared filter;
`publish` assigns the one global monotonic `seq`, retains the event in an
8192-entry resume window, and `try_send`s to every matching subscriber under a
never-block drop policy: a full queue drops the event for that consumer and
raises its `lagged` flag (the socket then closes `slow_consumer`), so one slow
client can never stall the fan-out or the node.

WS /api/v1/ws is a thin axum WebSocketUpgrade adapter over a transport-free
Session state machine: the full frame protocol (welcome/ack/event/pong/
heartbeat/close/error/subscribe_rejected/unsubscribed/resync), subscribe/
unsubscribe/resume/ping/auth ops, per-channel partial-success rejects,
selector validation (invalid_selector before the channel_unavailable liveness
gate), terminal box:/tx: channels that fire once then auto-unsubscribe,
control-op rate limiting, heartbeat/idle timeout, and binary rejection. T0 but
bounded: a per-IP/global ConnLimiter checked pre-upgrade (429 connection_limit)
plus the send-queue + rate caps.

Upstream tap (honest Phase-1): a server-seam bridge task (runtime-guarded like
the depth sampler) polls the existing coarse operator event ring and
republishes block_applied/reorg into the bus `blocks` channel — one push path,
no second event source. The fine-grained address/box/token/tx taps live in node
internals and are a follow-up; until they land those classes are gated
channel_unavailable. block_applied frames carry confirmed:true; the retraction
event model (box_reverted/box_unspent/tx_dropped:reorged, previous_seq) is built
and tested but only emitted once the fine-grained taps exist — reorg guarantee
today is best-effort coarse-grained (blocks-channel reorg frame, no dropped-
branch enumeration).

Reuses the G2 primitives verbatim: the canonical Reason enum (realtime_disabled,
channel_unavailable, invalid_selector, channel_limit, connection_limit,
slow_consumer, idle_timeout, binary_unsupported, unknown_op, rate_limited), the
error envelope, and the v1 DTO field names + unix_ms_to_iso timestamp rule.

Test plan: cargo fmt --all --check (clean); cargo clippy --workspace
--all-targets --all-features -D warnings (clean, no #[allow]); cargo test
--workspace (green). 34 realtime tests: bus fan-out / per-key filtering /
slow-consumer drop / backfill-gap / conn-limiter, the pure Session protocol
(subscribe/reject/terminal/rate-limit/on_event), the coarse→bus projection +
boot-seed, and a real end-to-end WS transport test (upgrade → welcome →
subscribe → publish → event → ping/pong) over tokio-tungstenite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* chore(rebase): reconcile V1State realtime field with the #171-wave test

The #171 CodeRabbit wave added a populated-MempoolView detail test after the
realtime group forked; its V1State construction needs the realtime: None field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* fix(api): saturate the resume gap check for client-supplied since=u64::MAX

`since` comes straight from the WS Resume frame, so `since + 1` was a
remotely-triggerable overflow panic in debug builds (wrap in release).
Saturating add keeps the gap semantics identical for all real cursors.
Skipped the publish-lock nitpick: fan-out under the mutex is what keeps
per-subscriber seq ordering, and every step is non-blocking.

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>
arkadianet pushed a commit that referenced this pull request Jul 8, 2026
…scoping, v1 envelope extractors, id/helper dedup, mempool.js v1 fields

- spawn_event_bridge_once: repeated router assembly in one runtime stacked
  bridge pollers; same AtomicBool idempotence as the depth sampler/webhook
  worker.
- validate_url: allow_loopback waived require_https for ALL hosts; plaintext
  http is now excused only for loopback targets (+ regression test).
- webhooks list/deliveries/patch_active: stock Query/Json answered malformed
  requests outside the v1 envelope; switched to V1Query/V1Json per §1.4.
- is_id64 delegates to valid_modifier_id — it accepted uppercase hex the
  rest of v1 (and its own error text) rejects.
- transactions.rs/tx_intel.rs: drop the duplicated parse_id32 and inline
  BlockchainState constructions for the shared helpers.
- mempool.js: finish the #171 v1 migration — fee/fee_per_byte (strings) and
  summary.weight_function; the panel's fee columns and histogram read
  fields the v1 endpoint never serves.

Skipped: tx-intel detail whitespace (already fixed in 8a0a6f1); chain.rs
list_blocks overfetch-vs-pruned-bodies has_more (real, needs a bounded-scan
design — follow-up); token-holders scan memoization (cache+invalidation
complexity for an already-capped scan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
arkadianet added a commit that referenced this pull request Jul 8, 2026
)

* feat(api): tx-intelligence node seams — non-mutating simulate + keyless builder (§4.2)

Add the two boundaries the v1 transactions/* intelligence group needs, both
honest-unavailable by default so a node that hasn't wired them answers a clean
503 rather than fabricating a result:

- NodeSubmit::simulate — a NON-mutating dry-run (G8). Unlike SubmitMode::CheckOnly,
  which still mutates the mempool anti-DoS bookkeeping (invariant #7), this MUST
  NOT touch node state; a cleanly-invalid tx is a successful Ok(valid:false).
  Default impl returns route_disabled so existing NodeSubmit impls keep compiling.
- NodeTxBuilder — the ONE keyless builder seam (O7). The production impl will
  delegate to the same selection/change/fee core the keyed wallet build uses; a
  second builder is never forked. Carries the keyless request/response/error types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* feat(api): v1 transactions/* intelligence — build/simulate/fee-estimate/status (§3.6)

The "help me transact" endpoints on the v1 transactions group:

- POST /transactions/build — keyless tx_intent -> unsigned tx over the O7
  NodeTxBuilder seam; intent-shape gate rejects mint / payment-registers / raw
  boxes as 422 unsupported_intent, enforces caps + address/id validation, and
  answers route_unavailable when the builder is unwired (never fake selection).
- POST /transactions/simulate — accept/reject + cost/fee/conflicts, no broadcast
  and no mempool mutation (G8); Ok(valid:false) is a 200.
- GET /transactions/fee-estimate — REAL: mempool-derived 1/3/10-block fee tiers
  (nanoERG/byte strings) from the chain reader's pool_recommended_fee + floor.
- GET /transactions/{tx_id}/status — REAL: confirmed (extra-index) precedes
  pooled (rank / ahead-bytes / competitiveness / eta); unknown id is a 200.

build/simulate mount at the governor Compute class, fee-estimate/status at
HeavyRead. 16 route tests (envelope/reason/field names, build happy-path with a
stub builder, simulate accept+reject, fee-estimate shape, status
pending/confirmed/unknown). V1State gains an Option tx_builder (prod None).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* fix(api): collapse literal whitespace runs in tx-intel error detail strings (CodeRabbit #178)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* fix(api): address CodeRabbit #178 — bridge once-guard, http-loopback scoping, v1 envelope extractors, id/helper dedup, mempool.js v1 fields

- spawn_event_bridge_once: repeated router assembly in one runtime stacked
  bridge pollers; same AtomicBool idempotence as the depth sampler/webhook
  worker.
- validate_url: allow_loopback waived require_https for ALL hosts; plaintext
  http is now excused only for loopback targets (+ regression test).
- webhooks list/deliveries/patch_active: stock Query/Json answered malformed
  requests outside the v1 envelope; switched to V1Query/V1Json per §1.4.
- is_id64 delegates to valid_modifier_id — it accepted uppercase hex the
  rest of v1 (and its own error text) rejects.
- transactions.rs/tx_intel.rs: drop the duplicated parse_id32 and inline
  BlockchainState constructions for the shared helpers.
- mempool.js: finish the #171 v1 migration — fee/fee_per_byte (strings) and
  summary.weight_function; the panel's fee columns and histogram read
  fields the v1 endpoint never serves.

Skipped: tx-intel detail whitespace (already fixed in 8a0a6f1); chain.rs
list_blocks overfetch-vs-pruned-bodies has_more (real, needs a bounded-scan
design — follow-up); token-holders scan memoization (cache+invalidation
complexity for an already-capped scan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

* fix(api): chain list pagination pages by height, not collected-row count

A pruned/missing block body inside the overfetch window shrank the item
count, so Page::from_overfetch reported has_more=false and falsely ended
the listing (CodeRabbit #178). CodeRabbit's suggested scan-until-full is
unbounded on a heavily pruned node, so instead has_more/next_cursor now
derive from the height domain: the cursor advances to the last SCANNED
height and the overfetched (limit+1)-th height is probed for a header —
headers are dense on a synced chain, so presence there is exactly
"another candidate row exists". Pruned gaps yield short (even empty)
pages that keep advancing, bounded to limit+1 height probes per request.
Applied to both list_blocks and list_headers via height_window_page.

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 Fable 5 <noreply@anthropic.com>
@arkadianet
arkadianet deleted the feat/v1-api-mempool branch July 9, 2026 06:41
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.

1 participant