From d50b52d97bc21fdfe3fa7aafb0b97ffa9f03203b Mon Sep 17 00:00:00 2001 From: "mostronatorcoder[bot]" <263173566+mostronatorcoder[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:07:36 +0000 Subject: [PATCH 1/2] feat: include Mostro pubkey in order event source tag Adds the Mostro daemon's pubkey to the source tag in kind 38383 order events, enabling clients to identify which Mostro instance published the order. ## Changes ### src/nip33.rs - `create_source_tag()`: Added `mostro_pubkey` parameter, appended as `&mostro={pubkey}` query parameter to the source URL - `order_to_tags()`: Derives pubkey via `get_keys()` and passes it to `create_source_tag()` ### docs/SOURCE_TAG_PUBKEY.md - Documents the new format, backward compatibility, and client behavior ## Source Tag Format Before: `mostro:{order_id}?relays={relay1},{relay2}` After: `mostro:{order_id}?relays={relay1},{relay2}&mostro={pubkey}` ## Backward Compatibility Clients that do not understand the `mostro` query parameter can safely ignore it. The `relays` parameter remains unchanged. Closes #678 Related: MostroP2P/mobile#541 --- docs/SOURCE_TAG_PUBKEY.md | 45 +++++++++++++++++++++++++++++++++++++++ src/nip33.rs | 21 +++++++++++++----- 2 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 docs/SOURCE_TAG_PUBKEY.md diff --git a/docs/SOURCE_TAG_PUBKEY.md b/docs/SOURCE_TAG_PUBKEY.md new file mode 100644 index 00000000..129a5bc9 --- /dev/null +++ b/docs/SOURCE_TAG_PUBKEY.md @@ -0,0 +1,45 @@ +# Source Tag: Mostro Pubkey + +## Overview + +The `source` tag in kind 38383 order events now includes the Mostro daemon's pubkey, +allowing clients to identify which Mostro instance published the order. + +## Format + +### Before + +``` +mostro:{order_id}?relays={relay1},{relay2} +``` + +### After + +``` +mostro:{order_id}?relays={relay1},{relay2}&mostro={pubkey} +``` + +### Example + +``` +mostro:e215c07e-b1f9-45b0-9640-0295067ee99a?relays=wss://relay.mostro.network,wss://nos.lol&mostro=82fa8cb978b43c79b2156585bac2c011176a21d2aead6d9f7c575c005be88390 +``` + +## Backward Compatibility + +Clients that do not understand the `mostro` query parameter can safely ignore it. +The `relays` parameter remains in the same position and format as before. + +## Client Behavior + +When a client receives a deep link with a `mostro` parameter: + +1. If the pubkey matches the currently selected Mostro instance → open order directly +2. If different → prompt user to switch instances, then navigate to order detail + +See [MostroP2P/mobile#541](https://github.com/MostroP2P/mobile/issues/541) for client implementation. + +## Related + +- [Order Event spec](https://mostro.network/protocol/order_event.html) +- Issue: [#678](https://github.com/MostroP2P/mostro/issues/678) diff --git a/src/nip33.rs b/src/nip33.rs index c19257ae..a1524c43 100644 --- a/src/nip33.rs +++ b/src/nip33.rs @@ -1,6 +1,6 @@ use crate::config::settings::Settings; use crate::lightning::LnStatus; -use crate::util::get_expiration_timestamp_for_kind; +use crate::util::{get_expiration_timestamp_for_kind, get_keys}; use crate::LN_STATUS; use mostro_core::prelude::*; use nostr::event::builder::Error; @@ -237,16 +237,25 @@ fn create_status_tags(order: &Order) -> Result<(bool, Status), MostroError> { /// includes: /// - Order ID /// - List of relays where the event can be found +/// - Mostro daemon's pubkey (so clients can identify the instance) /// -/// The resulting reference uses a custom format: `mostro:{order_id}?{relay1,relay2,...}` +/// The resulting reference uses a custom format: +/// `mostro:{order_id}?relays={relay1,relay2,...}&mostro={pubkey}` /// fn create_source_tag( order: &Order, mostro_relays: &[String], + mostro_pubkey: &str, ) -> Result, MostroError> { if order.status == Status::Pending.to_string() { // Create a mostro: custom source reference for pending orders - let custom_ref = format!("mostro:{}?relays={}", order.id, mostro_relays.join(",")); + // Include the Mostro pubkey so clients can identify the instance + let custom_ref = format!( + "mostro:{}?relays={}&mostro={}", + order.id, + mostro_relays.join(","), + mostro_pubkey + ); Ok(Some(custom_ref)) } else { @@ -291,7 +300,7 @@ fn create_source_tag( /// - `y`: "mostro" platform identifier, plus optional Mostro instance name from settings /// - `z`: Always "order" (event type) /// - `rating`: User reputation data (if available) -/// - `source`: mostro: scheme link to pending orders (`mostro:{order_id}?{relay1,relay2,...}`) +/// - `source`: mostro: scheme link to pending orders (`mostro:{order_id}?relays={...}&mostro={pubkey}`) /// pub fn order_to_tags( order: &Order, @@ -304,7 +313,9 @@ pub fn order_to_tags( // Check if the order is pending/in-progress/success/canceled let (create_event, status) = create_status_tags(order)?; // Create mostro: scheme link in case of pending order creation - let mostro_link = create_source_tag(order, &Settings::get_nostr().relays)?; + // Include the Mostro pubkey so clients can identify the instance + let mostro_pubkey = get_keys()?.public_key().to_hex(); + let mostro_link = create_source_tag(order, &Settings::get_nostr().relays, &mostro_pubkey)?; // Send just in case the order is pending/in-progress/success/canceled if create_event { From 5ba67fd46e485df0c2a9289cf014a1f6c0055c60 Mon Sep 17 00:00:00 2001 From: "mostronatorcoder[bot]" <263173566+mostronatorcoder[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:14:14 +0000 Subject: [PATCH 2/2] fix: properly mock Mostro pubkey in tests Instead of silently skipping the source tag when keys are unavailable, properly pass the Mostro pubkey as an optional parameter to order_to_tags(). ## Changes ### API signature change - order_to_tags() now takes an optional `mostro_pubkey: Option<&str>` - If None, derives pubkey from get_keys() (production behavior) - If Some, uses the provided pubkey (test behavior) ### Updated call sites - src/app/release.rs: Pass my_keys.public_key().to_hex() - src/util.rs: get_tags_for_new_order() now takes mostro_keys parameter - src/util.rs: update_order_event() passes keys.public_key().to_hex() ### New test - order_to_tags_source_tag_includes_mostro_pubkey: Verifies the source tag contains the correct Mostro pubkey in the expected format ## Verification - 195 tests pass (was 194, +1 for new source tag test) - cargo fmt: clean - cargo clippy: clean --- src/app/release.rs | 4 ++- src/nip33.rs | 63 +++++++++++++++++++++++++++++++++++++++++++--- src/util.rs | 12 ++++++--- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/app/release.rs b/src/app/release.rs index e918f82d..d6b43e6f 100644 --- a/src/app/release.rs +++ b/src/app/release.rs @@ -633,12 +633,14 @@ async fn create_order_event( // If user has sent the order with his identity key means that he wants to be rate so we can just // check if we have identity key in db - if present we have to send reputation tags otherwise no. + let mostro_pubkey = my_keys.public_key().to_hex(); let tags = match crate::db::is_user_present(pool, identity_pubkey.to_string()).await { Ok(user) => order_to_tags( new_order, Some((user.total_rating, user.total_reviews, user.created_at)), + Some(&mostro_pubkey), )?, - Err(_) => order_to_tags(new_order, Some((0.0, 0, 0)))?, + Err(_) => order_to_tags(new_order, Some((0.0, 0, 0)), Some(&mostro_pubkey))?, }; // Prepare new child order event for sending (kind 38383 for orders) diff --git a/src/nip33.rs b/src/nip33.rs index a1524c43..e8e2ce7d 100644 --- a/src/nip33.rs +++ b/src/nip33.rs @@ -302,9 +302,17 @@ fn create_source_tag( /// - `rating`: User reputation data (if available) /// - `source`: mostro: scheme link to pending orders (`mostro:{order_id}?relays={...}&mostro={pubkey}`) /// +/// # Arguments +/// +/// * `order` - The order to transform into tags +/// * `reputation_data` - Optional reputation data for the maker +/// * `mostro_pubkey` - Optional Mostro pubkey override. If None, derived from get_keys(). +/// Pass Some() in tests to avoid global state dependencies. +/// pub fn order_to_tags( order: &Order, reputation_data: Option<(f64, i64, i64)>, + mostro_pubkey: Option<&str>, ) -> Result, MostroError> { // Position of the tags in the list const RATING_TAG_INDEX: usize = 7; @@ -314,8 +322,11 @@ pub fn order_to_tags( let (create_event, status) = create_status_tags(order)?; // Create mostro: scheme link in case of pending order creation // Include the Mostro pubkey so clients can identify the instance - let mostro_pubkey = get_keys()?.public_key().to_hex(); - let mostro_link = create_source_tag(order, &Settings::get_nostr().relays, &mostro_pubkey)?; + let pubkey = match mostro_pubkey { + Some(pk) => pk.to_string(), + None => get_keys()?.public_key().to_hex(), + }; + let mostro_link = create_source_tag(order, &Settings::get_nostr().relays, &pubkey)?; // Send just in case the order is pending/in-progress/success/canceled if create_event { @@ -519,6 +530,10 @@ mod tests { // ── Shared test helpers ────────────────────────────────────────────────────── + /// Test Mostro pubkey (derived from the test nsec in test_settings) + const TEST_MOSTRO_PUBKEY: &str = + "9a0e40e008c6dcfdb3c608a65ddf1c4e72eed7eeefbe1eb88ea0f1ea8b43dc4d"; + /// Initialize global settings once per test binary run using the canonical /// test_settings() helper from AppContext test_utils — consistent with the /// rest of the test infrastructure. @@ -604,7 +619,7 @@ mod tests { init_test_settings(); let order = make_pending_order(); - let tags = order_to_tags(&order, None) + let tags = order_to_tags(&order, None, Some(TEST_MOSTRO_PUBKEY)) .expect("order_to_tags must not error") .expect("pending order must produce Some(tags)"); @@ -618,7 +633,7 @@ mod tests { init_test_settings(); let order = make_pending_order(); - let tags = order_to_tags(&order, None) + let tags = order_to_tags(&order, None, Some(TEST_MOSTRO_PUBKEY)) .expect("order_to_tags must not error") .expect("pending order must produce Some(tags)"); @@ -631,6 +646,46 @@ mod tests { ); } + // ── order_to_tags: source tag with Mostro pubkey (kind 38383) ─────────────── + + /// Extract the value of the "source" tag from a Tags collection. + fn get_source_tag_value(tags: &Tags) -> Option { + tags.iter().find_map(|tag| { + let vec = tag.clone().to_vec(); + if vec.first().map(|s| s.as_str()) == Some("source") { + vec.get(1).cloned() + } else { + None + } + }) + } + + #[test] + fn order_to_tags_source_tag_includes_mostro_pubkey() { + init_test_settings(); + let order = make_pending_order(); + + let tags = order_to_tags(&order, None, Some(TEST_MOSTRO_PUBKEY)) + .expect("order_to_tags must not error") + .expect("pending order must produce Some(tags)"); + + let source = get_source_tag_value(&tags).expect("pending order must have source tag"); + + // Verify the source tag format: mostro:{order_id}?relays={...}&mostro={pubkey} + assert!( + source.starts_with("mostro:"), + "source must start with 'mostro:' scheme" + ); + assert!( + source.contains("&mostro="), + "source must contain '&mostro=' parameter" + ); + assert!( + source.contains(&format!("&mostro={}", TEST_MOSTRO_PUBKEY)), + "source must contain the correct Mostro pubkey" + ); + } + // ── info_to_tags: end-to-end y-tag emission (kind 38385) ──────────────────── #[test] diff --git a/src/util.rs b/src/util.rs index c2646df2..78deffad 100644 --- a/src/util.rs +++ b/src/util.rs @@ -287,7 +287,7 @@ pub fn get_expiration_timestamp_for_kind(kind: u16) -> Option { /// let identity_pubkey = PublicKey::from_str("02abcdef...").unwrap(); /// let trade_pubkey = identity_pubkey.clone(); /// -/// let tags = get_tags_for_new_order(&order, &pool, &identity_pubkey, &trade_pubkey).await?; +/// let tags = get_tags_for_new_order(&order, &pool, &identity_pubkey, &trade_pubkey, &keys).await?; /// // Use `tags` for further event processing. /// # Ok(()) /// # } @@ -296,19 +296,22 @@ pub async fn get_tags_for_new_order( pool: &SqlitePool, identity_pubkey: &PublicKey, trade_pubkey: &PublicKey, + mostro_keys: &Keys, ) -> Result, MostroError> { + let mostro_pubkey = mostro_keys.public_key().to_hex(); match is_user_present(pool, identity_pubkey.to_string()).await { Ok(user) => { // We transform the order fields to tags to use in the event order_to_tags( new_order_db, Some((user.total_rating, user.total_reviews, user.created_at)), + Some(&mostro_pubkey), ) } Err(_) => { // We transform the order fields to tags to use in the event if identity_pubkey == trade_pubkey { - order_to_tags(new_order_db, Some((0.0, 0, 0))) + order_to_tags(new_order_db, Some((0.0, 0, 0)), Some(&mostro_pubkey)) } else { Err(MostroInternalErr(ServiceError::InvalidPubkey)) } @@ -388,7 +391,7 @@ pub async fn publish_order( // Get tags for new order in case of full privacy or normal order // nip33 kind with order fields as tags and order id as identifier (kind 38383 for orders) let event = if let Some(tags) = - get_tags_for_new_order(&new_order_db, pool, &identity_pubkey, &trade_pubkey).await? + get_tags_for_new_order(&new_order_db, pool, &identity_pubkey, &trade_pubkey, keys).await? { new_order_event(keys, "", order_id.to_string(), tags) .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))? @@ -728,7 +731,8 @@ pub async fn update_order_event( let reputation_data = get_ratings_for_pending_order(&order_updated, status).await?; // We transform the order fields to tags to use in the event - if let Some(tags) = order_to_tags(&order_updated, reputation_data)? { + let mostro_pubkey = keys.public_key().to_hex(); + if let Some(tags) = order_to_tags(&order_updated, reputation_data, Some(&mostro_pubkey))? { // nip33 kind with order id as identifier and order fields as tags (kind 38383 for orders) let event = new_order_event(keys, "", order.id.to_string(), tags) .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;