Skip to content

feat(api): native /api/v1/wallet lifecycle + key-derivation surface - #113

Merged
arkadianet merged 2 commits into
mainfrom
feat/native-wallet-lifecycle
Jun 20, 2026
Merged

feat(api): native /api/v1/wallet lifecycle + key-derivation surface#113
arkadianet merged 2 commits into
mainfrom
feat/native-wallet-lifecycle

Conversation

@arkadianet

@arkadianet arkadianet commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What & why

Second phase of the native /api/v1/wallet/* API — the lifecycle & key-derivation (write) surface — stacked on #112 (the read surface). All nine endpoints are thin adapters over the existing WalletAdmin trait methods, so there are no new bridge commands; the headline new infrastructure is the centralized strict-JSON body extractor that the write surface needs.

Base this PR on fix/wallet-reemission-balance (#112). Review #112 first.

Endpoints (api-key gated; strict bodies)

Method Path
POST /init create wallet; mnemonic returned once (no-store); strength validated
POST /restore restore; explicit DerivationMode required
POST /unlock /lock load / drop the master key
POST /mnemonic/verify {matched}; 409 on an uninitialized wallet
POST /addresses derive (tagged next|path)
GET/PUT /change-address read / set (PUT not unlock-gated — public metadata)
POST /rescan full rebuild; rescan_unavailable on a non-replay backend

Key points

  • StrictJson<T> extractor: deny_unknown_fields → the native {reason:"bad_request"} envelope (never Axum's default 400); empty body treated as {}; a no_store helper for secret-bearing responses (init mnemonic, mnemonic/verify).
  • Bridge correctness fixes (shared with compat; strictly more correct, no compat test depended on the old codes): init/restore now refuse to overwrite an existing wallet (WalletExists) instead of writing a second secret file — a real data-loss guard; derive bad-path → 400, duplicate → 409; rescan unsupported/in-progress → 409 (all were 500).
  • Tagged request enums (DeriveKeyRequest/DerivationMode) use manual Deserialize so unknown sibling fields are rejected and per-variant fields validated (serde can't deny_unknown_fields an internally-tagged enum).

Review process

Design + a dedicated codex review of P2 in dev-docs/ (gitignored). Every finding folded in — most importantly the init/restore overwrite guard codex caught, plus mnemonic/verify-uninitialized → 409, derive error classification, rescan 409s, the fallible address index, and the tagged-enum strictness.

Test plan

  • cargo fmt --all -- --check — clean
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — clean
  • cargo test --workspace259 test binaries, 0 failures

New tests: init-twice → WalletExists, strict-JSON native envelope, init bad-strength, bodyless rescan, POST gating, tagged-enum strictness, all new DTO shapes.

Next

P3 — transaction construction (build/sign/send + the burn-aware builder), the second half of closing the original invalid-tx bug. Its own gated, codex-reviewed PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

New Features

  • Added native wallet API endpoints for initialization, restoration, unlock, and lock operations
  • Mnemonic verification functionality
  • Address derivation and change-address management
  • Wallet rescanning with optional starting height

Bug Fixes

  • Prevents accidental wallet overwriting when already initialized
  • Improved derivation-path error reporting and validation
  • Enhanced rescan error handling for unavailable scenarios

Tests

  • Added integration tests for wallet initialization, security validation, and error handling

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 47 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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 credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

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, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc45c50c-2ce7-4148-985c-3a7ebfcb9583

📥 Commits

Reviewing files that changed from the base of the PR and between aa4be13 and dced23f.

📒 Files selected for processing (2)
  • ergo-api/src/wallet/native/mod.rs
  • ergo-api/tests/fixtures/openapi_native.yaml
📝 Walkthrough

Walkthrough

This PR adds native wallet lifecycle, address derivation, change-address management, and rescan endpoints to ergo-api under /api/v1/wallet/*, backed by new DTOs with strict serde validation, a StrictJson extractor, and OpenAPI registration. It also hardens ergo-node's wallet bridge with typed WalletExists, RescanUnavailable, and DerivationPathExists error variants replacing internal string errors, and adds overwrite-prevention guards to init and restore.

Changes

Native Wallet API Endpoints

Layer / File(s) Summary
Wallet lifecycle DTOs with strict serde validation
ergo-api/src/wallet/native/dto.rs
Adds all new wallet request/response structs and enums (UnlockRequest, MnemonicVerify*, InitRequest/Response, DerivationMode, RestoreRequest, DeriveKeyRequest, DerivedAddress, ChangeAddressDto, SetChangeAddressRequest, RescanRequest) with custom Deserialize enforcing tagged-union constraints, deny_unknown_fields, and field defaults, plus serde unit tests.
StrictJson extractor and NoStoreJson wrapper
ergo-api/src/wallet/native/mod.rs
Adds StrictJson<T> implementing FromRequest (empty body → {}, parse errors → native bad_request envelope) and NoStoreJson/no_store for Cache-Control: no-store on secret-bearing responses.
Unlock, lock, mnemonic_verify, init, restore handlers
ergo-api/src/wallet/native/mod.rs
Implements POST handlers for wallet unlock, lock, mnemonic verify (with 409 wallet_uninitialized guard), init (mnemonic strength validation), and restore, using StrictJson for bodies and NoStoreJson for secret responses.
Address derivation, change-address, rescan handlers and router wiring
ergo-api/src/wallet/native/mod.rs
Adds POST /wallet/addresses (derive_address with derivation-path index validation), GET/PUT /wallet/change-address, POST /wallet/rescan handlers, and registers all new routes in router_with_security.
OpenAPI spec fixture and server.rs registration
ergo-api/src/server.rs, ergo-api/tests/fixtures/openapi_native.yaml
Registers all new endpoint functions in paths(...) and components(schemas(...)) in server.rs, and documents every new route, request body, response shape, error references, and ApiKeyAuth security in the YAML fixture.
Runtime mount integration tests
ergo-api/tests/openapi_native_runtime_mount.rs
Adds tokio tests covering StrictJson unknown-field rejection, API-key gate on POST routes, init invalid-strength rejection, and empty-body rescan reaching the handler.

ergo-node Wallet Bridge Error Hardening

Layer / File(s) Summary
Typed derivation errors in wallet_bridge.rs
ergo-node/src/node/wallet_bridge.rs
Changes derive_key_impl to return WalletAdminError::BadRequest for parse failures and WalletAdminError::DerivationPathExists for duplicate paths; updates derive_next_key_impl similarly.
WalletExists and RescanUnavailable guards in admin.rs
ergo-node/src/node/wallet_bridge/commands/admin.rs, ergo-node/tests/wallet_admin_roundtrip.rs
Adds early-exit WalletAdminError::WalletExists checks to init and restore when storage is not Uninitialized; upgrades rescan error branches from Internal to WalletAdminError::RescanUnavailable; integration test validates the init-twice guard.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • arkadianet/ergo#75: The backend /wallet/rescan implementation changes in that PR (scan rebuild/matching behavior) are directly related to the RescanRequest DTO and POST /api/v1/wallet/rescan handler introduced here.

Poem

🐇 Hoppity-hop, new routes appear,
Wallets init, lock, unlock — have no fear!
StrictJson guards each body tight,
No unknown fields shall pass tonight.
WalletExists stops the double-init deed,
The rabbit's API grows — plant that seed! 🌱

🚥 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 main change: implementing the native wallet lifecycle and key-derivation endpoints for the /api/v1/wallet API surface.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/native-wallet-lifecycle

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 and usage tips.

Base automatically changed from fix/wallet-reemission-balance to main June 20, 2026 11:35
Second phase of the native wallet API (stacked on the read surface). Adds the
write side of lifecycle + keys and the centralized strict-JSON body extractor,
all over the EXISTING WalletAdmin trait methods (no new bridge commands).

Endpoints (api-key gated; bodies via the strict extractor):
- POST init (mnemonic returned once, no-store; strength validated 12/15/18/21/24),
  POST restore (explicit DerivationMode required), POST unlock, POST lock,
  POST mnemonic/verify (no-store; 409 on an uninitialized wallet).
- POST /addresses (derive: tagged next|path), GET/PUT /change-address
  (PUT not unlock-gated; change address is public metadata, shown while locked),
  POST rescan (real full-rebuild; rescan_unavailable on a non-replay backend).

StrictJson<T> extractor: deny_unknown_fields -> the native {reason:"bad_request"}
envelope (never Axum's default 400); empty body treated as `{}`; no-store helper
for secret-bearing responses.

Bridge correctness fixes (shared with the compat surface, strictly more correct;
no compat test depended on the old codes):
- init/restore now REFUSE to overwrite an existing wallet (WalletExists) instead
  of persisting a second secret file -- a real data-loss guard.
- derive: bad path -> bad_request(400), duplicate path -> derivation_path_exists(409)
  (were 500); rescan unsupported/in-progress -> rescan_unavailable(409) (was 500).

The tagged request enums (DeriveKeyRequest/DerivationMode) use manual Deserialize
so unknown sibling fields are rejected and per-variant fields validated (serde
can't deny_unknown_fields an internally-tagged enum).

Design + codex review (P2) in dev-docs (gitignored); every finding folded in
(init/restore overwrite guard, mnemonic-verify-uninitialized 409, derive error
classification, rescan 409s, fallible address index, tagged-enum strictness).

Test plan: cargo fmt --all -- --check; cargo clippy --workspace --all-targets
--all-features -- -D warnings; cargo test --workspace (259 test binaries, 0
failures). New: init-twice -> WalletExists, strict-JSON native envelope,
init-bad-strength, bodyless-rescan, POST gating, tagged-enum strictness, DTO shapes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@arkadianet
arkadianet force-pushed the feat/native-wallet-lifecycle branch from cd4fab5 to aa4be13 Compare June 20, 2026 11:41
@arkadianet

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 20, 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: 1

🧹 Nitpick comments (1)
ergo-node/tests/wallet_admin_roundtrip.rs (1)

575-611: ⚡ Quick win

Add a symmetric restore overwrite regression test.

This test locks in the new init guard well; adding a restore-twice case would similarly protect the WalletExists contract added for restore.

Proposed test shape
+#[tokio::test]
+async fn restore_twice_returns_wallet_exists() {
+    // setup identical to init_twice_returns_wallet_exists
+    // first restore succeeds
+    // second restore expect_err and assert matches!(err, WalletAdminError::WalletExists)
+}
🤖 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-node/tests/wallet_admin_roundtrip.rs` around lines 575 - 611, Create a
new async test function named restore_twice_returns_wallet_exists that mirrors
the structure of init_twice_returns_wallet_exists. Set up identical test
infrastructure with temporary directory, storage, state, database, and wallet
admin components. Instead of calling admin.init() twice, call admin.restore()
twice with appropriate parameters and verify that the second restore call
returns a WalletAdminError::WalletExists error using the same assertion pattern.
This protects the WalletExists contract for the restore function similar to how
the existing test_init_twice_returns_wallet_exists guards the init function.
🤖 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/wallet/native/mod.rs`:
- Around line 576-579: The utoipa::path annotation for the POST
/api/v1/wallet/rescan endpoint currently marks the request body
(dto::RescanRequest) as required in the OpenAPI spec, but the handler
implementation allows it to be optional. Modify the utoipa::path annotation by
adding the appropriate parameter to mark the request_body as optional (using the
required property or wrapper). After making this change to the annotation,
regenerate the OpenAPI fixture to reflect the updated contract where the
RescanRequest body is optional.

---

Nitpick comments:
In `@ergo-node/tests/wallet_admin_roundtrip.rs`:
- Around line 575-611: Create a new async test function named
restore_twice_returns_wallet_exists that mirrors the structure of
init_twice_returns_wallet_exists. Set up identical test infrastructure with
temporary directory, storage, state, database, and wallet admin components.
Instead of calling admin.init() twice, call admin.restore() twice with
appropriate parameters and verify that the second restore call returns a
WalletAdminError::WalletExists error using the same assertion pattern. This
protects the WalletExists contract for the restore function similar to how the
existing test_init_twice_returns_wallet_exists guards the init function.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7282edef-2c53-40af-a580-5f63150d42ca

📥 Commits

Reviewing files that changed from the base of the PR and between c63d2cf and aa4be13.

📒 Files selected for processing (8)
  • ergo-api/src/server.rs
  • ergo-api/src/wallet/native/dto.rs
  • ergo-api/src/wallet/native/mod.rs
  • ergo-api/tests/fixtures/openapi_native.yaml
  • ergo-api/tests/openapi_native_runtime_mount.rs
  • ergo-node/src/node/wallet_bridge.rs
  • ergo-node/src/node/wallet_bridge/commands/admin.rs
  • ergo-node/tests/wallet_admin_roundtrip.rs

Comment thread ergo-api/src/wallet/native/mod.rs
CodeRabbit (PR #113): the strict extractor treats an empty body as `{}` (a
bodyless rescan POST does a full rebuild, covered by the runtime-mount test), but
the utoipa annotation generated `requestBody.required: true` — a contract mismatch
that would make generated SDKs require the body. Use `request_body =
Option<dto::RescanRequest>` so the spec marks it optional; golden regenerated.

Gate: fmt + clippy --workspace --all-targets --all-features -D warnings clean;
cargo test --workspace green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@arkadianet
arkadianet merged commit 8525857 into main Jun 20, 2026
8 checks passed
@arkadianet
arkadianet deleted the feat/native-wallet-lifecycle branch June 20, 2026 12:56
arkadianet added a commit that referenced this pull request Jun 21, 2026
…lease cut (#126)

0.4.3 was version-cut at #117, but #106#114 and #118#125 landed at the same
version without a CHANGELOG entry — so [0.4.3] documented only #115/#116/#117
while the deployed 0.4.3 binary actually contains all 18 PRs since the v0.4.2
cut. Backfills the section to match what shipped:

- New ### Added subsection for the native /api/v1/wallet surface (#112, #113,
  #114).
- ### Fixed now covers the signed-byte parity fixes (#106, #107), the
  Coll-equality cost short-circuit (#108), EIP-27 re-emission enforcement
  (#109 + #111, merged — the P0 fork fix, now across block/mempool/mining), and
  the full v6/EIP-50 + ErgoTree wire-deserialization cluster (#118#125).
- Broadened the release intro to name the EIP-27 enforcement and the native
  wallet surface alongside the v6/EIP-50 conformance work.
- Added the missing (#117) reference to the box-deserialize entry.

Docs only; no code or behavior change. Entries were drafted from each PR's own
commit/description and accuracy-reviewed (e.g. the EIP-27 soak figure is the
corrected 172 reward-box burns, not the repudiated 1,173).

Co-authored-by: arkadianet <rkadias@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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