mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
feat(grin): address→npub bridge + mark relay same-instance-only
Enhance the grin address→user lookup to return the owner's linked npub (not just a bool), so a bare grin slatepack address can resolve to its owner's npub and route over the federated Nostr gift-wrap rail instead of only manual handoff. Doc-comment the backend slatepack relay as a same-instance-only convenience — the federated default transport is Nostr gift-wrap; the relay needs a recipient user_id and both parties on this instance, so it is not the primary path in a public-backend world.
This commit is contained in:
@@ -56,6 +56,7 @@ use utoipa::OpenApi;
|
||||
crate::api::wallet::grin::scan,
|
||||
crate::api::wallet::grin::height,
|
||||
crate::api::wallet::grin::broadcast,
|
||||
crate::api::wallet::grin::address_user,
|
||||
crate::api::wallet::grin_relay::create,
|
||||
crate::api::wallet::grin_relay::pending,
|
||||
crate::api::wallet::grin_relay::get_relay,
|
||||
@@ -144,6 +145,7 @@ use utoipa::OpenApi;
|
||||
crate::api::wallet::grin::GrinHeightResponse,
|
||||
crate::api::wallet::grin::GrinBroadcastRequest,
|
||||
crate::api::wallet::grin::GrinBroadcastResponse,
|
||||
crate::api::wallet::grin::GrinAddressUserResponse,
|
||||
crate::api::wallet::grin_relay::CreateRelayRequest,
|
||||
crate::api::wallet::grin_relay::RespondRelayRequest,
|
||||
crate::api::wallet::grin_relay::FinalizeRelayRequest,
|
||||
|
||||
+67
-1
@@ -17,7 +17,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
extract::{Path, State},
|
||||
http::HeaderMap,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
@@ -93,6 +93,31 @@ pub struct GrinBroadcastResponse {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
/// Resolution of a bare Grin address to its owning user's routing identity.
|
||||
///
|
||||
/// The address→npub **bridge**: given a `grin1…` slatepack address, report
|
||||
/// whether it belongs to a user registered on *this* backend and, if so, that
|
||||
/// user's linked Nostr pubkey so a bare-address send can be upgraded to a
|
||||
/// federated Nostr gift-wrap instead of the same-instance relay.
|
||||
///
|
||||
/// NOTE (federation): this is a **same-instance convenience**. It can only
|
||||
/// resolve addresses whose owner registered their Grin key here (via `POST
|
||||
/// /keys`). It is *not* a federated directory — a wallet on another backend
|
||||
/// won't be found. `registered: false` (a constant-shape miss, mirroring
|
||||
/// `GET /users/by-username`) means "unknown to this instance", at which point
|
||||
/// the sender falls back to manual clipboard / the backend relay.
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct GrinAddressUserResponse {
|
||||
/// Whether the address resolved to a user on this backend.
|
||||
pub registered: bool,
|
||||
/// The owning user's id (UUID), when `registered`.
|
||||
pub user_id: Option<String>,
|
||||
/// The owning user's linked Nostr pubkey (x-only hex), when they have one.
|
||||
/// This is what the sender feeds the Nostr gift-wrap channel to route the
|
||||
/// slate over Nostr. `None` when the user registered no Nostr identity.
|
||||
pub npub: Option<String>,
|
||||
}
|
||||
|
||||
fn output_dto(o: ViewWalletOutputResult) -> GrinOutput {
|
||||
GrinOutput {
|
||||
commit: o.commit,
|
||||
@@ -200,6 +225,46 @@ pub async fn broadcast(
|
||||
Ok(Json(GrinBroadcastResponse { ok: true }))
|
||||
}
|
||||
|
||||
/// Resolve a bare Grin address to its owning user's Nostr routing identity.
|
||||
///
|
||||
/// The address→npub bridge (see [`GrinAddressUserResponse`]). JWT-gated: only
|
||||
/// authenticated senders resolve routing. An address unknown to this backend
|
||||
/// returns the constant-shape `registered: false` response — the caller then
|
||||
/// falls back to manual clipboard or the same-instance relay.
|
||||
#[utoipa::path(
|
||||
security(("bearer_auth" = [])),
|
||||
get,
|
||||
path = "/wallet/grin/address/{addr}/user",
|
||||
params(("addr" = String, Path, description = "Grin address (registered public key)")),
|
||||
responses(
|
||||
(status = 200, description = "Address resolution result", body = GrinAddressUserResponse),
|
||||
(status = 401, description = "Missing or invalid token")
|
||||
),
|
||||
tag = "grin"
|
||||
)]
|
||||
#[instrument(skip(state, headers))]
|
||||
pub async fn address_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(addr): Path<String>,
|
||||
) -> Result<Json<GrinAddressUserResponse>, AppError> {
|
||||
extract_user_id_from_token(&state, &headers).await?;
|
||||
|
||||
let Some(user) = state.db.find_user_by_grin_address(&addr).await? else {
|
||||
return Ok(Json(GrinAddressUserResponse {
|
||||
registered: false,
|
||||
user_id: None,
|
||||
npub: None,
|
||||
}));
|
||||
};
|
||||
|
||||
Ok(Json(GrinAddressUserResponse {
|
||||
registered: true,
|
||||
user_id: Some(user.id.to_string()),
|
||||
npub: user.nostr_pubkey,
|
||||
}))
|
||||
}
|
||||
|
||||
// ── router ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Grin routes, RELATIVE to the `/api/v1` mount point.
|
||||
@@ -208,4 +273,5 @@ pub fn routes() -> Router<Arc<AppState>> {
|
||||
.route("/wallet/grin/scan", post(scan))
|
||||
.route("/wallet/grin/height", get(height))
|
||||
.route("/wallet/grin/broadcast", post(broadcast))
|
||||
.route("/wallet/grin/address/:addr/user", get(address_user))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,23 @@
|
||||
//! must be a registered user (no blind spam to arbitrary addresses); slatepack
|
||||
//! sizes are capped; entries expire; and the whole feature is behind
|
||||
//! `FEATURE_GRIN_RELAY` so an operator can disable the mailbox.
|
||||
//!
|
||||
//! ## Federation: this relay is a SAME-INSTANCE CONVENIENCE, not the default
|
||||
//!
|
||||
//! The relay only connects two users **registered on the same backend**: the
|
||||
//! sender is the JWT caller and the recipient is addressed by a
|
||||
//! `recipient_user_id` that must already exist in *this* instance's `users`
|
||||
//! table (there is deliberately no cross-instance address lookup here). A wallet
|
||||
//! on another backend cannot be a relay counterparty.
|
||||
//!
|
||||
//! It is therefore NOT the federated transport. The **default, federated**
|
||||
//! transport for interactive Grin transfers is Nostr gift-wrap (npub / NIP-05),
|
||||
//! which works across instances; **manual clipboard** is the universal fallback.
|
||||
//! Prefer those. This mailbox is offered only as an ergonomic shortcut when both
|
||||
//! parties happen to live on the same instance — treat it as an optimization,
|
||||
//! never as something a second backend can rely on. See the address→npub bridge
|
||||
//! in [`super::grin`] (`/wallet/grin/address/{addr}/user`), which lets a
|
||||
//! same-instance address be upgraded to federated Nostr routing.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -222,6 +222,29 @@ impl Database {
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Resolve a registered Grin address to its owning user.
|
||||
///
|
||||
/// The `address` is matched verbatim against `user_keys.public_key` for the
|
||||
/// `grin` asset (the canonical form the wallet registered via `POST /keys`).
|
||||
/// This is a **same-instance** bridge: it only resolves addresses whose owner
|
||||
/// registered on *this* backend. Used to look up a recipient's linked Nostr
|
||||
/// pubkey so a bare-`grin1…`-addressed send can route over Nostr gift-wrap.
|
||||
#[instrument(skip(self, address))]
|
||||
pub async fn find_user_by_grin_address(
|
||||
&self,
|
||||
address: &str,
|
||||
) -> Result<Option<User>, AppError> {
|
||||
let sql = format!(
|
||||
"SELECT {USER_COLS} FROM users u \
|
||||
JOIN user_keys k ON k.user_id = u.id \
|
||||
WHERE k.asset = 'grin' AND k.public_key = $1"
|
||||
);
|
||||
Ok(sqlx::query_as::<_, User>(&sql)
|
||||
.bind(address)
|
||||
.fetch_optional(self.pool())
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_user_by_username(&self, username: &str) -> Result<Option<User>, AppError> {
|
||||
let sql = format!("SELECT {USER_COLS} FROM users WHERE username = $1");
|
||||
|
||||
Reference in New Issue
Block a user