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::scan,
|
||||||
crate::api::wallet::grin::height,
|
crate::api::wallet::grin::height,
|
||||||
crate::api::wallet::grin::broadcast,
|
crate::api::wallet::grin::broadcast,
|
||||||
|
crate::api::wallet::grin::address_user,
|
||||||
crate::api::wallet::grin_relay::create,
|
crate::api::wallet::grin_relay::create,
|
||||||
crate::api::wallet::grin_relay::pending,
|
crate::api::wallet::grin_relay::pending,
|
||||||
crate::api::wallet::grin_relay::get_relay,
|
crate::api::wallet::grin_relay::get_relay,
|
||||||
@@ -144,6 +145,7 @@ use utoipa::OpenApi;
|
|||||||
crate::api::wallet::grin::GrinHeightResponse,
|
crate::api::wallet::grin::GrinHeightResponse,
|
||||||
crate::api::wallet::grin::GrinBroadcastRequest,
|
crate::api::wallet::grin::GrinBroadcastRequest,
|
||||||
crate::api::wallet::grin::GrinBroadcastResponse,
|
crate::api::wallet::grin::GrinBroadcastResponse,
|
||||||
|
crate::api::wallet::grin::GrinAddressUserResponse,
|
||||||
crate::api::wallet::grin_relay::CreateRelayRequest,
|
crate::api::wallet::grin_relay::CreateRelayRequest,
|
||||||
crate::api::wallet::grin_relay::RespondRelayRequest,
|
crate::api::wallet::grin_relay::RespondRelayRequest,
|
||||||
crate::api::wallet::grin_relay::FinalizeRelayRequest,
|
crate::api::wallet::grin_relay::FinalizeRelayRequest,
|
||||||
|
|||||||
+67
-1
@@ -17,7 +17,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::{Path, State},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
@@ -93,6 +93,31 @@ pub struct GrinBroadcastResponse {
|
|||||||
pub ok: bool,
|
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 {
|
fn output_dto(o: ViewWalletOutputResult) -> GrinOutput {
|
||||||
GrinOutput {
|
GrinOutput {
|
||||||
commit: o.commit,
|
commit: o.commit,
|
||||||
@@ -200,6 +225,46 @@ pub async fn broadcast(
|
|||||||
Ok(Json(GrinBroadcastResponse { ok: true }))
|
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 ────────────────────────────────────────────────────────────────────
|
// ── router ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Grin routes, RELATIVE to the `/api/v1` mount point.
|
/// 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/scan", post(scan))
|
||||||
.route("/wallet/grin/height", get(height))
|
.route("/wallet/grin/height", get(height))
|
||||||
.route("/wallet/grin/broadcast", post(broadcast))
|
.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
|
//! must be a registered user (no blind spam to arbitrary addresses); slatepack
|
||||||
//! sizes are capped; entries expire; and the whole feature is behind
|
//! sizes are capped; entries expire; and the whole feature is behind
|
||||||
//! `FEATURE_GRIN_RELAY` so an operator can disable the mailbox.
|
//! `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;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,29 @@ impl Database {
|
|||||||
.await?)
|
.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))]
|
#[instrument(skip(self))]
|
||||||
pub async fn get_user_by_username(&self, username: &str) -> Result<Option<User>, AppError> {
|
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");
|
let sql = format!("SELECT {USER_COLS} FROM users WHERE username = $1");
|
||||||
|
|||||||
Reference in New Issue
Block a user