feat: Grin slatepack relay (non-custodial async mailbox, feature-flagged)

Final wallet-handler slice. An async store-and-forward mailbox for interactive
Grin transfers between two registered Smirk users, over the grin_slatepacks db.
Reworked clean from the legacy relay — NOT a port.

Endpoints (under /api/v1/wallet/grin/relay, JWT-gated):
- POST create   {recipient_user_id, slate_id, slatepack, amount_nanogrin}
- GET  pending  -> the caller's inbox
- POST get      {slate_id} -> poll a relay's state (sender/recipient)
- POST respond  {slate_id, response_slatepack} (recipient)
- POST finalize {slate_id, tx_hash} (sender; RECORDS the txid only)
- POST cancel   {slate_id} (sender or recipient)

Non-custodial: stores only opaque (encrypted) slatepacks + lifecycle status;
never holds keys, finalizes, or broadcasts. The wallet broadcasts locally (via
/wallet/grin/broadcast) and reports the txid here — diverging from the legacy
server-side post_tx (see docs/private/OWN_BACKEND_CONVERGENCE.md).

Controls + hardening (incl. fixes from the adversarial review workflow):
- Per-action authz: caller must be the slate's sender/recipient; a non-party
  gets 404 (no existence oracle).
- slate_id is UNIQUE (+ validated as a UUID) and mutations key on the
  authorized row's PRIMARY KEY, not slate_id — so a duplicate slate_id can't
  brick a relay (fetch_optional-on-2-rows 500) or let a slate_id-keyed UPDATE
  touch another row. create maps the unique violation to 409.
- State transitions are ATOMIC: the status + expiry preconditions live in the
  UPDATE WHERE clause (not a Rust read-check-write), so respond-twice and
  cancel-races-finalize cannot occur; a non-qualifying transition -> 409.
- TTL enforced at the action boundary (respond/finalize require expires_at >
  now) + a periodic expire_old_slatepacks sweep in main.rs (relay-enabled only).
- Recipient must be a registered user; slatepack sizes capped (64 KiB).
- Behind FEATURE_GRIN_RELAY (default on).

6 paths + schemas + grin_relay tag in openapi (drift-clean). 4 L1 integration
tests against live Postgres: full lifecycle, unregistered-recipient 400, auth
gate, non-party/non-recipient 404s.
This commit is contained in:
jwinterm
2026-06-23 17:39:13 -04:00
parent e62f1bec8e
commit 586f681287
9 changed files with 1076 additions and 31 deletions
+14 -1
View File
@@ -49,6 +49,12 @@ use utoipa::OpenApi;
crate::api::wallet::grin::scan,
crate::api::wallet::grin::height,
crate::api::wallet::grin::broadcast,
crate::api::wallet::grin_relay::create,
crate::api::wallet::grin_relay::pending,
crate::api::wallet::grin_relay::get_relay,
crate::api::wallet::grin_relay::respond,
crate::api::wallet::grin_relay::finalize,
crate::api::wallet::grin_relay::cancel,
crate::api::nip05::well_known_nostr,
),
components(schemas(
@@ -112,6 +118,12 @@ use utoipa::OpenApi;
crate::api::wallet::grin::GrinHeightResponse,
crate::api::wallet::grin::GrinBroadcastRequest,
crate::api::wallet::grin::GrinBroadcastResponse,
crate::api::wallet::grin_relay::CreateRelayRequest,
crate::api::wallet::grin_relay::RespondRelayRequest,
crate::api::wallet::grin_relay::FinalizeRelayRequest,
crate::api::wallet::grin_relay::SlateIdRequest,
crate::api::wallet::grin_relay::RelayEntry,
crate::api::wallet::grin_relay::PendingRelaysResponse,
crate::api::nip05::WellKnownResponse,
)),
tags(
@@ -119,7 +131,8 @@ use utoipa::OpenApi;
(name = "auth", description = "Wallet authentication: registration, restore, refresh, sessions, Nostr identity."),
(name = "btc_ltc", description = "Bitcoin/Litecoin chain access via Electrum (balance, UTXOs, history, tip, fee, broadcast)."),
(name = "xmr_wow", description = "Monero/Wownero chain access via LWS (register, balance, history, unspent/random outs, submit, height, confirmations)."),
(name = "grin", description = "Grin view-only chain access (rewind_hash scan, tip height, broadcast).")
(name = "grin", description = "Grin view-only chain access (rewind_hash scan, tip height, broadcast)."),
(name = "grin_relay", description = "Grin slatepack relay: async non-custodial mailbox for interactive Grin transfers (feature-flagged).")
)
)]
pub struct ApiDoc;