Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/SOURCE_TAG_PUBKEY.md
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 3 additions & 1 deletion src/app/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
80 changes: 73 additions & 7 deletions src/nip33.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Option<String>, 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 {
Expand Down Expand Up @@ -291,11 +300,19 @@ 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}`)
///
/// # 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<Option<Tags>, MostroError> {
// Position of the tags in the list
const RATING_TAG_INDEX: usize = 7;
Expand All @@ -304,7 +321,12 @@ 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 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 {
Expand Down Expand Up @@ -508,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.
Expand Down Expand Up @@ -593,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)");

Expand All @@ -607,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)");

Expand All @@ -620,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<String> {
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]
Expand Down
12 changes: 8 additions & 4 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ pub fn get_expiration_timestamp_for_kind(kind: u16) -> Option<i64> {
/// 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(())
/// # }
Expand All @@ -296,19 +296,22 @@ pub async fn get_tags_for_new_order(
pool: &SqlitePool,
identity_pubkey: &PublicKey,
trade_pubkey: &PublicKey,
mostro_keys: &Keys,
) -> Result<Option<Tags>, 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))
}
Expand Down Expand Up @@ -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())))?
Expand Down Expand Up @@ -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())))?;
Expand Down
Loading