Skip to content

feat(api): v1 scan/* + watch-only + account-abstraction + tiered secret export (final route group) - #185

Merged
arkadianet merged 2 commits into
mainfrom
feat/v1-api-wallet-scan
Jul 9, 2026
Merged

feat(api): v1 scan/* + watch-only + account-abstraction + tiered secret export (final route group)#185
arkadianet merged 2 commits into
mainfrom
feat/v1-api-wallet-scan

Conversation

@arkadianet

@arkadianet arkadianet commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Stacked on #184 (operator) → the full v1 stack. Merge those first. Final v1 route group.

What this is

The last v1 group: scan/*, watch-only, account-abstraction, and the correctly-tiered secret export.

Load-bearing discovery: the pre-existing native /api/v1/wallet/* surface already OWNS that whole prefix via a *rest catch-all — so merging any new /api/v1/wallet/* sibling makes Router::merge panic on the matchit catch-all (same shape as the operator group's node/shutdown). This group therefore mounts only prefixes it fully owns (scan/*, accounts/*) and does NOT re-mount wallet reads (no collision, no duplication).

REAL vs seam

  • scan/ (T1) — REAL:* register / list (cursor) / get (404 not compat-400) / deregister / {id}/unspent (the -1 legality rule ported) / transactions / boxes attach+detach, over the existing WalletAdmin scan methods.
  • watch-only — REAL: register → scan_p2s_rule (T1); list → scans filtered to wallet_interaction=off (T0); {id}/unspent → scan_unspent_boxes (T0); delete (T1).
  • private-key export (T2) — REAL, and the exemplar: POST /api/v1/accounts/private-key, get_private_key-backed, acknowledge-gated, no-store. Proven Admin + loopback gate: no-key → 401; valid key from non-loopback under hard-deny → 409 at the gate, before get_private_key runs, no private_key in the body; loopback+key → passes. No-key node-wide → both tiers fail closed.
  • named accounts + PSBT (T1) — honest seams: full path surfaces answering route_unavailable (no account_id model / durable PSBT session yet). No fabricated balances or signatures.

⚠️ Security gap flagged (separate change, still owed)

The pre-existing native init/restore/rescan/getPrivateKey routes sit behind a single weak api-key gate with no tier/loopback check — the real "secret at the wrong tier" exposure (design §0 drift #4). Retrofitting require_tier onto the native surface risks its pinned tests, so it's a separate change — this group ships the properly-gated export as the model for it.

Other flags

Spec path deviations forced by the matchit catch-all (watch at /accounts/watch*, PSBT at /transactions-psbt*); v1 Reason enum lacks wallet_locked/wallet_exists (111-reason contract locked → mapped to closest with disambiguating detail, Internal never leaks); watch registry has no label persistence (Phase-2).

Test plan

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p ergo-api   # router_layout=87 (collision-free assembly), openapi_native_runtime_mount, +12 new

🤖 Generated with Claude Code

https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx

Summary by CodeRabbit

  • New Features

    • Added new scan and account management endpoints for listing, registering, viewing, and removing scans.
    • Added watch-only account actions for tracking addresses and checking unspent funds.
    • Added a protected option to export a private key with stronger access checks.
  • Bug Fixes

    • Improved error responses for missing scans and unsupported routes.
    • Added clearer paging support for scan and transaction lists.

@arkadianet
arkadianet changed the base branch from main to feat/v1-api-operator July 7, 2026 22:31
@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: 42 minutes

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: 31c89308-6bf7-4464-b1b6-b36e995c6619

📥 Commits

Reviewing files that changed from the base of the PR and between 5af6b95 and 22944b3.

📒 Files selected for processing (3)
  • ergo-api/src/v1/accounts/mod.rs
  • ergo-api/src/v1/accounts/scan.rs
  • ergo-api/tests/v1_scan_accounts_routes.rs
📝 Walkthrough

Walkthrough

Adds a new v1 "scan/accounts" API surface: an AccountsState/accounts_router with tiered (T0/T1/T2) endpoints for watch-only scans, scan registry operations, private-key export, and honest "route unavailable" seams for unwired accounts/PSBT routes, wired into server.rs and v1::mod, plus end-to-end tests.

Changes

Scan/Accounts v1 API

Layer / File(s) Summary
Module registration and server wiring
ergo-api/src/v1/mod.rs, ergo-api/src/server.rs
Declares/re-exports the new accounts module and merges accounts_router into the assembled native operator API using a cloned wallet_admin handle.
AccountsState, error mapping, and seams
ergo-api/src/v1/accounts/mod.rs
Defines AccountsState, map_wallet_err translating WalletAdminError into v1 Reason responses, and a seam helper returning honest RouteUnavailable for unwired subsystems (accounts, PSBT).
Watch-only endpoints and private-key export
ergo-api/src/v1/accounts/mod.rs
Implements watch scan register/list/delete and a private-key export handler requiring acknowledgement, address validation, and Cache-Control: no-store.
Tiered router assembly
ergo-api/src/v1/accounts/mod.rs
Builds accounts_router merging T0 (governor CheapRead), T1 (require_tier(Operator)), and T2 (require_tier(Admin)) sub-routers.
Scan registry handlers
ergo-api/src/v1/accounts/scan.rs
Adds DTOs, cursor/pagination types, and handlers for registering/listing/fetching/deregistering scans, listing unspent boxes and transactions, and attaching/detaching boxes.
End-to-end route tests
ergo-api/tests/v1_scan_accounts_routes.rs
Adds a mocked WalletAdmin and tests covering T0/T1/T2 auth gating, watch-only reads, seam responses, and private-key export acknowledgement/loopback rules.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AccountsRouter
  participant Governor
  participant WalletAdmin
  Client->>AccountsRouter: GET /api/v1/accounts/watch (T0)
  AccountsRouter->>Governor: check CheapRead
  AccountsRouter->>WalletAdmin: list watch scans
  WalletAdmin-->>AccountsRouter: scan rows
  AccountsRouter-->>Client: 200 items+page

  Client->>AccountsRouter: POST /api/v1/scan/scans (T1)
  AccountsRouter->>AccountsRouter: require_tier(Operator)
  AccountsRouter->>WalletAdmin: register scan
  WalletAdmin-->>AccountsRouter: scan_id
  AccountsRouter-->>Client: 201 scan_id

  Client->>AccountsRouter: POST /api/v1/accounts/private-key (T2)
  AccountsRouter->>AccountsRouter: require_tier(Admin)
  AccountsRouter->>WalletAdmin: get_private_key
  WalletAdmin-->>AccountsRouter: secret scalar
  AccountsRouter-->>Client: 200 private_key
Loading

Possibly related PRs

  • arkadianet/ergo#60: Introduces WalletAdmin scan management (stopTracking/addBox) that the new scan box attach/detach handlers depend on.
  • arkadianet/ergo#61: Changes WalletAdmin::transactions_by_scan_id error semantics directly consumed by the new scan transactions endpoint.
  • arkadianet/ergo#168: Provides the v1 tier middleware, error envelope, and pagination primitives reused by the new accounts/scan router.
🚥 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 accurately summarizes the new v1 scan, watch-only, account, and tiered secret-export route group.
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-wallet-scan

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.

@arkadianet
arkadianet force-pushed the feat/v1-api-operator branch from 19d7fdd to dbf038d Compare July 8, 2026 15:57
@arkadianet
arkadianet force-pushed the feat/v1-api-wallet-scan branch from e8ca23b to 992c0d5 Compare July 8, 2026 15:57
@arkadianet
arkadianet force-pushed the feat/v1-api-operator branch from dbf038d to 589d596 Compare July 8, 2026 21:09
@arkadianet
arkadianet force-pushed the feat/v1-api-wallet-scan branch from 992c0d5 to a0056f8 Compare July 8, 2026 21:09
@arkadianet
arkadianet force-pushed the feat/v1-api-operator branch from 589d596 to 0b81c27 Compare July 8, 2026 22:58
@arkadianet
arkadianet force-pushed the feat/v1-api-wallet-scan branch from a0056f8 to 0bbfc28 Compare July 8, 2026 22:58
@arkadianet
arkadianet force-pushed the feat/v1-api-operator branch from 0b81c27 to 0f5974c Compare July 9, 2026 02:05
@arkadianet
arkadianet force-pushed the feat/v1-api-wallet-scan branch from 0bbfc28 to c2be87f Compare July 9, 2026 02:05
… secret gate (§3.10–§3.11)

The final v1 route group. Adds `/api/v1/scan/*` and `/api/v1/accounts/*` as a
new router that reuses the existing WalletAdmin machinery — never reimplements
scan matching or key derivation — and mounts ONLY prefixes it fully owns.

Why the new prefixes and not `/api/v1/wallet/*`: the pre-existing native wallet
surface owns that whole prefix via a `*rest` catch-all (like compat
`node/shutdown` for the operator group), so a sibling router adding wallet paths
would make `Router::merge` panic on the overlapping catch-all. Reads/build/sign/
send therefore stay on the native surface; this group delivers the genuinely-new
scan + account-abstraction surfaces and the correctly-tiered secret export.

- scan/* (T1, REAL): register/list/get/deregister, {id}/unspent, {id}/transactions,
  {id}/boxes attach + detach — snake_case + cursor + nested-error adaptation over
  the WalletAdmin scan trait methods; the ScanBoxFilter -1 legality rule is kept.
- watch-only (T0 read, T1 write, REAL): register via scan_p2s_rule, list via the
  wallet_interaction="off" scan marker, unspent read via scan_unspent_boxes.
- named accounts + PSBT (T1, honest seams): designed surfaces answering
  route_unavailable — no node backing exists (no account_id model, no durable
  PSBT session / hints-bag sign variant). Never a fabricated balance or signature.
- private-key export (T2, REAL): `POST /api/v1/accounts/private-key`, admin
  api-key + loopback-preferred via the fail-closed require_tier(Admin) gate,
  acknowledge-gated, Cache-Control: no-store, backed by get_private_key.

Secret material is unreachable at T0/T1 and (under admin_hard_deny) from any
non-loopback caller — the gate rejects before the handler runs. 12 route tests
cover the T1 scan gate, watch-only reads, the seams, and the full T2 gate matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
@arkadianet
arkadianet force-pushed the feat/v1-api-wallet-scan branch from c2be87f to 5af6b95 Compare July 9, 2026 05:10
@arkadianet
arkadianet changed the base branch from feat/v1-api-operator to main July 9, 2026 05:10
@arkadianet

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 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.

@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: 2

🧹 Nitpick comments (1)
ergo-api/tests/v1_scan_accounts_routes.rs (1)

119-252: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding tests for untested scan/watch endpoints.

Several T1 endpoints have no test coverage: DELETE /scan/scans/:id (deregister), GET /scan/scans/:id/unspent, GET /scan/scans/:id/transactions, POST /scan/scans/:id/boxes (attach), DELETE /scan/scans/:id/boxes/:box_id (detach), and DELETE /accounts/watch/:id (watch delete). The mock already implements scan_add_box, scan_stop_tracking, and transactions_by_scan_id, suggesting these were intended to be exercised. Adding at least a smoke test for each would prevent regressions in route wiring or tier gating.

Also applies to: 70-91, 55-69

🤖 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_scan_accounts_routes.rs` around lines 119 - 252, Add smoke
test coverage for the missing scan/watch routes using the existing test mock in
the `v1_scan_accounts_routes` suite. Exercise the route handlers for
`scan_add_box`, `scan_stop_tracking`, and `transactions_by_scan_id` through
`DELETE /scan/scans/:id`, `GET /scan/scans/:id/unspent`, `GET
/scan/scans/:id/transactions`, `POST /scan/scans/:id/boxes`, `DELETE
/scan/scans/:id/boxes/:box_id`, and `DELETE /accounts/watch/:id` to verify
wiring and tier gating. Keep the tests focused on confirming the endpoints
respond and dispatch to the expected mock methods rather than deep business
logic.
🤖 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/accounts/mod.rs`:
- Around line 418-427: The T0 endpoint for scan unspent lookup currently passes
any scan_id through to scan::unspent, unlike watch_list which already enforces
watch-only scans. Update the scan::unspent path in
ergo-api/src/v1/accounts/mod.rs to verify the scan’s wallet_interaction is "off"
before returning results, and return scan_not_found for operator/non-watch
scans. Keep the check aligned with the existing watch_list filtering so the
/api/v1/accounts/watch/:scan_id/unspent route is scoped consistently.

In `@ergo-api/src/v1/accounts/scan.rs`:
- Around line 216-227: The ScanBoxFilter overfetch path is pushing a valid
?limit=2500 request past the validator cap because the code adds 1 to the
clamped limit before calling filter.validate(). Update the limit handling in the
scan logic around ScanBoxFilter so the internal overfetch stays at or below
2500, either by clamping the user-facing limit to 2499 before the
saturating_add(1) or by adjusting ScanBoxFilter::validate() to allow this
internal overfetch case.

---

Nitpick comments:
In `@ergo-api/tests/v1_scan_accounts_routes.rs`:
- Around line 119-252: Add smoke test coverage for the missing scan/watch routes
using the existing test mock in the `v1_scan_accounts_routes` suite. Exercise
the route handlers for `scan_add_box`, `scan_stop_tracking`, and
`transactions_by_scan_id` through `DELETE /scan/scans/:id`, `GET
/scan/scans/:id/unspent`, `GET /scan/scans/:id/transactions`, `POST
/scan/scans/:id/boxes`, `DELETE /scan/scans/:id/boxes/:box_id`, and `DELETE
/accounts/watch/:id` to verify wiring and tier gating. Keep the tests focused on
confirming the endpoints respond and dispatch to the expected mock methods
rather than deep business logic.
🪄 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: b592dc6a-8117-492d-a726-3da1c480a69d

📥 Commits

Reviewing files that changed from the base of the PR and between a0ee59d and 5af6b95.

📒 Files selected for processing (5)
  • ergo-api/src/server.rs
  • ergo-api/src/v1/accounts/mod.rs
  • ergo-api/src/v1/accounts/scan.rs
  • ergo-api/src/v1/mod.rs
  • ergo-api/tests/v1_scan_accounts_routes.rs

Comment thread ergo-api/src/v1/accounts/mod.rs
Comment thread ergo-api/src/v1/accounts/scan.rs
…unspent mount + limit-cap regression

- The T0 /accounts/watch/{scan_id}/unspent mount used the unscoped
  scan::unspent handler, so a PUBLIC caller could read any operator
  (wallet-interacting) scan's boxes. New watch_unspent wrapper enforces
  the same wallet_interaction="off" scoping watch_list uses; non-watch
  scans answer scan_not_found on the public mount (the T1 api-key mount
  keeps the unscoped read).
- SCAN_BOX_MAX_LIMIT 2500 → 2499: the overfetch-by-one probe pushed a
  valid ?limit=2500 past the ScanBoxFilter 1..=2500 validator (400).
  Capping the probe instead would silently break has_more on exact-cap
  pages; one-below keeps clamp semantics and honest pagination.
- tests: smoke coverage for deregister/unspent/transactions/attach/
  detach/watch-delete wiring + 401 tier gating, plus regressions for
  both fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUh2DBnAPqThdYFZW5D8wx
@arkadianet
arkadianet merged commit 41661de into main Jul 9, 2026
9 checks passed
@arkadianet
arkadianet deleted the feat/v1-api-wallet-scan branch July 9, 2026 06:42
arkadianet pushed a commit that referenced this pull request Jul 13, 2026
Bump workspace version to 0.5.2 and promote the changelog: the complete
v1 product API (#168-#185, #188), shadow validation as a production mode
(#193-#195), the operator observability wave (#187, #190, #192, #194),
two live accept-invalid consensus fixes (#176, #179), ErgoScript
compiler byte-parity completion (#165-#167, #175), and the #160-#163
sync/recovery fixes. Full workspace gate run on the merge result.

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