Quellcode durchsuchen

feat: optional transport and nut10 secret on payment request (#744)

* feat: optional transport on payment request

* feat: create token for payment rquest

* feat: create payment request

* feat: arg append
thesimplekid vor 1 Monat
Ursprung
Commit
385ec4d295

+ 161 - 7
crates/cashu/src/nuts/nut18.rs

@@ -3,15 +3,18 @@
 //! <https://github.com/cashubtc/nuts/blob/main/18.md>
 
 use std::fmt;
+use std::ops::Not;
 use std::str::FromStr;
 
 use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
 use bitcoin::base64::{alphabet, Engine};
+use serde::ser::{SerializeTuple, Serializer};
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
-use super::{CurrencyUnit, Proofs};
+use super::{CurrencyUnit, Nut10Secret, Proofs, SpendingConditions};
 use crate::mint_url::MintUrl;
+use crate::nuts::nut10::Kind;
 use crate::Amount;
 
 const PAYMENT_REQUEST_PREFIX: &str = "creqA";
@@ -146,6 +149,91 @@ impl AsRef<String> for Transport {
     }
 }
 
+/// Secret Data without nonce for payment requests
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct SecretDataRequest {
+    /// Expresses the spending condition specific to each kind
+    pub data: String,
+    /// Additional data committed to and can be used for feature extensions
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub tags: Option<Vec<Vec<String>>>,
+}
+
+/// Nut10Secret without nonce for payment requests
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
+pub struct Nut10SecretRequest {
+    /// Kind of the spending condition
+    pub kind: Kind,
+    /// Secret Data without nonce
+    pub secret_data: SecretDataRequest,
+}
+
+impl Nut10SecretRequest {
+    /// Create a new Nut10SecretRequest
+    pub fn new<S, V>(kind: Kind, data: S, tags: Option<V>) -> Self
+    where
+        S: Into<String>,
+        V: Into<Vec<Vec<String>>>,
+    {
+        let secret_data = SecretDataRequest {
+            data: data.into(),
+            tags: tags.map(|v| v.into()),
+        };
+
+        Self { kind, secret_data }
+    }
+}
+
+impl From<Nut10Secret> for Nut10SecretRequest {
+    fn from(secret: Nut10Secret) -> Self {
+        let secret_data = SecretDataRequest {
+            data: secret.secret_data.data,
+            tags: secret.secret_data.tags,
+        };
+
+        Self {
+            kind: secret.kind,
+            secret_data,
+        }
+    }
+}
+
+impl From<Nut10SecretRequest> for Nut10Secret {
+    fn from(value: Nut10SecretRequest) -> Self {
+        Self::new(value.kind, value.secret_data.data, value.secret_data.tags)
+    }
+}
+
+impl From<SpendingConditions> for Nut10SecretRequest {
+    fn from(conditions: SpendingConditions) -> Self {
+        match conditions {
+            SpendingConditions::P2PKConditions { data, conditions } => {
+                Self::new(Kind::P2PK, data.to_hex(), conditions)
+            }
+            SpendingConditions::HTLCConditions { data, conditions } => {
+                Self::new(Kind::HTLC, data.to_string(), conditions)
+            }
+        }
+    }
+}
+
+impl Serialize for Nut10SecretRequest {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        // Create a tuple representing the struct fields
+        let secret_tuple = (&self.kind, &self.secret_data);
+
+        // Serialize the tuple as a JSON array
+        let mut s = serializer.serialize_tuple(2)?;
+
+        s.serialize_element(&secret_tuple.0)?;
+        s.serialize_element(&secret_tuple.1)?;
+        s.end()
+    }
+}
+
 /// Payment Request
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct PaymentRequest {
@@ -169,7 +257,10 @@ pub struct PaymentRequest {
     pub description: Option<String>,
     /// Transport
     #[serde(rename = "t")]
-    pub transports: Vec<Transport>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub transports: Option<Vec<Transport>>,
+    /// Nut10
+    pub nut10: Option<Nut10SecretRequest>,
 }
 
 impl PaymentRequest {
@@ -189,6 +280,7 @@ pub struct PaymentRequestBuilder {
     mints: Option<Vec<MintUrl>>,
     description: Option<String>,
     transports: Vec<Transport>,
+    nut10: Option<Nut10SecretRequest>,
 }
 
 impl PaymentRequestBuilder {
@@ -252,8 +344,16 @@ impl PaymentRequestBuilder {
         self
     }
 
+    /// Set Nut10 secret
+    pub fn nut10(mut self, nut10: Nut10SecretRequest) -> Self {
+        self.nut10 = Some(nut10);
+        self
+    }
+
     /// Build the PaymentRequest
     pub fn build(self) -> PaymentRequest {
+        let transports = self.transports.is_empty().not().then_some(self.transports);
+
         PaymentRequest {
             payment_id: self.payment_id,
             amount: self.amount,
@@ -261,7 +361,8 @@ impl PaymentRequestBuilder {
             single_use: self.single_use,
             mints: self.mints,
             description: self.description,
-            transports: self.transports,
+            transports,
+            nut10: self.nut10,
         }
     }
 }
@@ -334,7 +435,8 @@ mod tests {
         );
         assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
 
-        let transport = req.transports.first().unwrap();
+        let transport = req.transports.unwrap();
+        let transport = transport.first().unwrap();
 
         let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
 
@@ -354,7 +456,8 @@ mod tests {
                 .parse()
                 .expect("valid mint url")]),
             description: None,
-            transports: vec![transport.clone()],
+            transports: Some(vec![transport.clone()]),
+            nut10: None,
         };
 
         let request_str = request.to_string();
@@ -370,7 +473,8 @@ mod tests {
         );
         assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
 
-        let t = req.transports.first().unwrap();
+        let t = req.transports.unwrap();
+        let t = t.first().unwrap();
         assert_eq!(&transport, t);
     }
 
@@ -400,7 +504,8 @@ mod tests {
         assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
         assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
 
-        let t = request.transports.first().unwrap();
+        let t = request.transports.clone().unwrap();
+        let t = t.first().unwrap();
         assert_eq!(&transport, t);
 
         // Test serialization and deserialization
@@ -434,4 +539,53 @@ mod tests {
         let result = TransportBuilder::default().build();
         assert!(result.is_err());
     }
+
+    #[test]
+    fn test_nut10_secret_request() {
+        use crate::nuts::nut10::Kind;
+
+        // Create a Nut10SecretRequest
+        let secret_request = Nut10SecretRequest::new(
+            Kind::P2PK,
+            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
+            Some(vec![vec!["key".to_string(), "value".to_string()]]),
+        );
+
+        // Convert to a full Nut10Secret
+        let full_secret: Nut10Secret = secret_request.clone().into();
+
+        // Check conversion
+        assert_eq!(full_secret.kind, Kind::P2PK);
+        assert_eq!(
+            full_secret.secret_data.data,
+            "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
+        );
+        assert_eq!(
+            full_secret.secret_data.tags,
+            Some(vec![vec!["key".to_string(), "value".to_string()]])
+        );
+
+        // Convert back to Nut10SecretRequest
+        let converted_back = Nut10SecretRequest::from(full_secret);
+
+        // Check round-trip conversion
+        assert_eq!(converted_back.kind, secret_request.kind);
+        assert_eq!(
+            converted_back.secret_data.data,
+            secret_request.secret_data.data
+        );
+        assert_eq!(
+            converted_back.secret_data.tags,
+            secret_request.secret_data.tags
+        );
+
+        // Test in PaymentRequest builder
+        let payment_request = PaymentRequest::builder()
+            .payment_id("test123")
+            .amount(Amount::from(100))
+            .nut10(secret_request.clone())
+            .build();
+
+        assert_eq!(payment_request.nut10, Some(secret_request));
+    }
 }

+ 248 - 49
crates/cdk-cli/src/sub_commands/create_request.rs

@@ -1,5 +1,10 @@
-use anyhow::Result;
-use cdk::nuts::nut18::TransportType;
+use std::str::FromStr;
+
+use anyhow::{bail, Result};
+use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use cdk::nuts::nut01::PublicKey;
+use cdk::nuts::nut11::{Conditions, SigFlag, SpendingConditions};
+use cdk::nuts::nut18::{Nut10SecretRequest, TransportType};
 use cdk::nuts::{CurrencyUnit, PaymentRequest, PaymentRequestPayload, Token, Transport};
 use cdk::wallet::{MultiMintWallet, ReceiveOptions};
 use clap::Args;
@@ -16,23 +21,41 @@ pub struct CreateRequestSubCommand {
     unit: String,
     /// Quote description
     description: Option<String>,
+    /// P2PK: Public key(s) for which the token can be spent with valid signature(s)
+    /// Can be specified multiple times for multiple pubkeys
+    #[arg(long, action = clap::ArgAction::Append)]
+    pubkey: Option<Vec<String>>,
+    /// Number of required signatures (for multiple pubkeys)
+    /// Defaults to 1 if not specified
+    #[arg(long, default_value = "1")]
+    num_sigs: u64,
+    /// HTLC: Hash for hash time locked contract
+    #[arg(long, conflicts_with = "preimage")]
+    hash: Option<String>,
+    /// HTLC: Preimage of the hash (to be used instead of hash)
+    #[arg(long, conflicts_with = "hash")]
+    preimage: Option<String>,
+    /// Transport type to use (nostr, http, or none)
+    /// - nostr: Use Nostr transport and listen for payment
+    /// - http: Use HTTP transport but only print the request
+    /// - none: Don't use any transport, just print the request
+    #[arg(long, default_value = "nostr")]
+    transport: String,
+    /// URL for HTTP transport (only used when transport=http)
+    #[arg(long)]
+    http_url: Option<String>,
+    /// Nostr relays to use (only used when transport=nostr)
+    /// Can be specified multiple times for multiple relays
+    /// If not provided, defaults to standard relays
+    #[arg(long, action = clap::ArgAction::Append)]
+    nostr_relay: Option<Vec<String>>,
 }
 
 pub async fn create_request(
     multi_mint_wallet: &MultiMintWallet,
     sub_command_args: &CreateRequestSubCommand,
 ) -> Result<()> {
-    let keys = Keys::generate();
-    let relays = vec!["wss://relay.nos.social", "wss://relay.damus.io"];
-
-    let nprofile = Nip19Profile::new(keys.public_key, relays.clone())?;
-
-    let nostr_transport = Transport {
-        _type: TransportType::Nostr,
-        target: nprofile.to_bech32()?,
-        tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
-    };
-
+    // Get available mints from the wallet
     let mints: Vec<cdk::mint_url::MintUrl> = multi_mint_wallet
         .get_balances(&CurrencyUnit::Sat)
         .await?
@@ -40,58 +63,234 @@ pub async fn create_request(
         .cloned()
         .collect();
 
+    // Process transport based on command line args
+    let transport_type = sub_command_args.transport.to_lowercase();
+    let transports = match transport_type.as_str() {
+        "nostr" => {
+            let keys = Keys::generate();
+
+            // Use custom relays if provided, otherwise use defaults
+            let relays = if let Some(custom_relays) = &sub_command_args.nostr_relay {
+                if !custom_relays.is_empty() {
+                    println!("Using custom Nostr relays: {:?}", custom_relays);
+                    custom_relays.clone()
+                } else {
+                    // Empty vector provided, fall back to defaults
+                    vec![
+                        "wss://relay.nos.social".to_string(),
+                        "wss://relay.damus.io".to_string(),
+                    ]
+                }
+            } else {
+                // No relays provided, use defaults
+                vec![
+                    "wss://relay.nos.social".to_string(),
+                    "wss://relay.damus.io".to_string(),
+                ]
+            };
+
+            let nprofile = Nip19Profile::new(keys.public_key, relays.clone())?;
+
+            let nostr_transport = Transport {
+                _type: TransportType::Nostr,
+                target: nprofile.to_bech32()?,
+                tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
+            };
+
+            // We'll need the Nostr keys and relays later for listening
+            let transport_info = Some((keys, relays, nprofile.public_key));
+
+            (Some(vec![nostr_transport]), transport_info)
+        }
+        "http" => {
+            if let Some(url) = &sub_command_args.http_url {
+                let http_transport = Transport {
+                    _type: TransportType::HttpPost,
+                    target: url.clone(),
+                    tags: None,
+                };
+
+                (Some(vec![http_transport]), None)
+            } else {
+                println!(
+                    "Warning: HTTP transport selected but no URL provided, skipping transport"
+                );
+                (None, None)
+            }
+        }
+        "none" => (None, None),
+        _ => {
+            println!(
+                "Warning: Unknown transport type '{}', defaulting to none",
+                transport_type
+            );
+            (None, None)
+        }
+    };
+
+    // Create spending conditions based on provided arguments
+    // Handle the following cases:
+    // 1. Only P2PK condition
+    // 2. Only HTLC condition with hash
+    // 3. Only HTLC condition with preimage
+    // 4. Both P2PK and HTLC conditions
+
+    let spending_conditions = if let Some(pubkey_strings) = &sub_command_args.pubkey {
+        // Parse all pubkeys
+        let mut parsed_pubkeys = Vec::new();
+        for pubkey_str in pubkey_strings {
+            match PublicKey::from_str(pubkey_str) {
+                Ok(pubkey) => parsed_pubkeys.push(pubkey),
+                Err(err) => {
+                    println!("Error parsing pubkey {}: {}", pubkey_str, err);
+                    // Continue with other pubkeys
+                }
+            }
+        }
+
+        if parsed_pubkeys.is_empty() {
+            println!("No valid pubkeys provided");
+            None
+        } else {
+            // We have pubkeys for P2PK condition
+            let num_sigs = sub_command_args.num_sigs.min(parsed_pubkeys.len() as u64);
+
+            // Check if we also have an HTLC condition
+            if let Some(hash_str) = &sub_command_args.hash {
+                // Create conditions with the pubkeys
+                let conditions = Conditions {
+                    locktime: None,
+                    pubkeys: Some(parsed_pubkeys),
+                    refund_keys: None,
+                    num_sigs: Some(num_sigs),
+                    sig_flag: SigFlag::SigInputs,
+                };
+
+                // Try to parse the hash
+                match Sha256Hash::from_str(hash_str) {
+                    Ok(hash) => {
+                        // Create HTLC condition with P2PK in the conditions
+                        Some(SpendingConditions::HTLCConditions {
+                            data: hash,
+                            conditions: Some(conditions),
+                        })
+                    }
+                    Err(err) => {
+                        println!("Error parsing hash: {}", err);
+                        // Fallback to just P2PK with multiple pubkeys
+                        bail!("Error parsing hash");
+                    }
+                }
+            } else if let Some(preimage) = &sub_command_args.preimage {
+                // Create conditions with the pubkeys
+                let conditions = Conditions {
+                    locktime: None,
+                    pubkeys: Some(parsed_pubkeys),
+                    refund_keys: None,
+                    num_sigs: Some(num_sigs),
+                    sig_flag: SigFlag::SigInputs,
+                };
+
+                // Create HTLC conditions with the hash and pubkeys in conditions
+                Some(SpendingConditions::new_htlc(
+                    preimage.to_string(),
+                    Some(conditions),
+                )?)
+            } else {
+                // Only P2PK condition with multiple pubkeys
+                Some(SpendingConditions::new_p2pk(
+                    *parsed_pubkeys.first().unwrap(),
+                    Some(Conditions {
+                        locktime: None,
+                        pubkeys: Some(parsed_pubkeys[1..].to_vec()),
+                        refund_keys: None,
+                        num_sigs: Some(num_sigs),
+                        sig_flag: SigFlag::SigInputs,
+                    }),
+                ))
+            }
+        }
+    } else if let Some(hash_str) = &sub_command_args.hash {
+        // Only HTLC condition with provided hash
+        match Sha256Hash::from_str(hash_str) {
+            Ok(hash) => Some(SpendingConditions::HTLCConditions {
+                data: hash,
+                conditions: None,
+            }),
+            Err(err) => {
+                println!("Error parsing hash: {}", err);
+                None
+            }
+        }
+    } else if let Some(preimage) = &sub_command_args.preimage {
+        // Only HTLC condition with provided preimage
+        // For HTLC, create the hash from the preimage and use it directly
+        Some(SpendingConditions::new_htlc(preimage.to_string(), None)?)
+    } else {
+        None
+    };
+
+    // Convert SpendingConditions to Nut10SecretRequest
+    let nut10 = spending_conditions.map(Nut10SecretRequest::from);
+
+    // Extract the transports option from our match result
+    let (transports_option, nostr_info) = transports;
+
     let req = PaymentRequest {
         payment_id: None,
         amount: sub_command_args.amount.map(|a| a.into()),
-        unit: None,
+        unit: Some(CurrencyUnit::from_str(&sub_command_args.unit)?),
         single_use: Some(true),
         mints: Some(mints),
         description: sub_command_args.description.clone(),
-        transports: vec![nostr_transport],
+        transports: transports_option,
+        nut10,
     };
 
+    // Always print the request
     println!("{req}");
 
-    let client = NostrClient::new(keys);
-
-    let filter = Filter::new().pubkey(nprofile.public_key);
-
-    for relay in relays {
-        client.add_read_relay(relay).await?;
-    }
-
-    client.connect().await;
-
-    client.subscribe(vec![filter], None).await?;
+    // Only listen for Nostr payment if Nostr transport was selected
+    if let Some((keys, relays, pubkey)) = nostr_info {
+        println!("Listening for payment via Nostr...");
 
-    // Handle subscription notifications with `handle_notifications` method
-    client
-        .handle_notifications(|notification| async {
-            let mut exit = false;
-            if let RelayPoolNotification::Event {
-                subscription_id: _,
-                event,
-                ..
-            } = notification
-            {
-                let unwrapped = client.unwrap_gift_wrap(&event).await?;
+        let client = NostrClient::new(keys);
+        let filter = Filter::new().pubkey(pubkey);
 
-                let rumor = unwrapped.rumor;
+        for relay in relays {
+            client.add_read_relay(relay).await?;
+        }
 
-                let payload: PaymentRequestPayload = serde_json::from_str(&rumor.content)?;
+        client.connect().await;
+        client.subscribe(vec![filter], None).await?;
 
-                let token = Token::new(payload.mint, payload.proofs, payload.memo, payload.unit);
+        // Handle subscription notifications with `handle_notifications` method
+        client
+            .handle_notifications(|notification| async {
+                let mut exit = false;
+                if let RelayPoolNotification::Event {
+                    subscription_id: _,
+                    event,
+                    ..
+                } = notification
+                {
+                    let unwrapped = client.unwrap_gift_wrap(&event).await?;
+                    let rumor = unwrapped.rumor;
+                    let payload: PaymentRequestPayload = serde_json::from_str(&rumor.content)?;
+                    let token =
+                        Token::new(payload.mint, payload.proofs, payload.memo, payload.unit);
 
-                let amount = multi_mint_wallet
-                    .receive(&token.to_string(), ReceiveOptions::default())
-                    .await?;
+                    let amount = multi_mint_wallet
+                        .receive(&token.to_string(), ReceiveOptions::default())
+                        .await?;
 
-                println!("Received {amount}");
-                exit = true;
-            }
-            Ok(exit) // Set to true to exit from the loop
-        })
-        .await?;
+                    println!("Received {amount}");
+                    exit = true;
+                }
+                Ok(exit) // Set to true to exit from the loop
+            })
+            .await?;
+    }
 
     Ok(())
 }

+ 81 - 68
crates/cdk-cli/src/sub_commands/pay_request.rs

@@ -2,7 +2,7 @@ use std::io::{self, Write};
 
 use anyhow::{anyhow, Result};
 use cdk::nuts::nut18::TransportType;
-use cdk::nuts::{PaymentRequest, PaymentRequestPayload};
+use cdk::nuts::{PaymentRequest, PaymentRequestPayload, Token};
 use cdk::wallet::{MultiMintWallet, SendOptions};
 use clap::Args;
 use nostr_sdk::nips::nip19::Nip19Profile;
@@ -67,18 +67,20 @@ pub async fn pay_request(
 
     let matching_wallet = matching_wallets.first().unwrap();
 
-    // We prefer nostr transport if it is available to hide ip.
-    let transport = payment_request
+    let transports = payment_request
         .transports
+        .clone()
+        .ok_or(anyhow!("Cannot pay request without transport"))?;
+
+    // We prefer nostr transport if it is available to hide ip.
+    let transport = transports
         .iter()
         .find(|t| t._type == TransportType::Nostr)
         .or_else(|| {
-            payment_request
-                .transports
+            transports
                 .iter()
                 .find(|t| t._type == TransportType::HttpPost)
-        })
-        .ok_or(anyhow!("No supported transport method found"))?;
+        });
 
     let prepared_send = matching_wallet
         .prepare_send(
@@ -91,81 +93,92 @@ pub async fn pay_request(
         .await?;
     let proofs = matching_wallet.send(prepared_send, None).await?.proofs();
 
-    let payload = PaymentRequestPayload {
-        id: payment_request.payment_id.clone(),
-        memo: None,
-        mint: matching_wallet.mint_url.clone(),
-        unit: matching_wallet.unit.clone(),
-        proofs,
-    };
-
-    match transport._type {
-        TransportType::Nostr => {
-            let keys = Keys::generate();
-            let client = NostrClient::new(keys);
-            let nprofile = Nip19Profile::from_bech32(&transport.target)?;
-
-            println!("{:?}", nprofile.relays);
-
-            let rumor = EventBuilder::new(
-                nostr_sdk::Kind::from_u16(14),
-                serde_json::to_string(&payload)?,
-                [],
-            );
-
-            let relays = nprofile.relays;
+    if let Some(transport) = transport {
+        let payload = PaymentRequestPayload {
+            id: payment_request.payment_id.clone(),
+            memo: None,
+            mint: matching_wallet.mint_url.clone(),
+            unit: matching_wallet.unit.clone(),
+            proofs,
+        };
+
+        match transport._type {
+            TransportType::Nostr => {
+                let keys = Keys::generate();
+                let client = NostrClient::new(keys);
+                let nprofile = Nip19Profile::from_bech32(&transport.target)?;
+
+                println!("{:?}", nprofile.relays);
+
+                let rumor = EventBuilder::new(
+                    nostr_sdk::Kind::from_u16(14),
+                    serde_json::to_string(&payload)?,
+                    [],
+                );
 
-            for relay in relays.iter() {
-                client.add_write_relay(relay).await?;
-            }
+                let relays = nprofile.relays;
 
-            client.connect().await;
+                for relay in relays.iter() {
+                    client.add_write_relay(relay).await?;
+                }
 
-            let gift_wrap = client
-                .gift_wrap_to(relays, &nprofile.public_key, rumor, None)
-                .await?;
+                client.connect().await;
 
-            println!(
-                "Published event {} succufully to {}",
-                gift_wrap.val,
-                gift_wrap
-                    .success
-                    .iter()
-                    .map(|s| s.to_string())
-                    .collect::<Vec<_>>()
-                    .join(", ")
-            );
+                let gift_wrap = client
+                    .gift_wrap_to(relays, &nprofile.public_key, rumor, None)
+                    .await?;
 
-            if !gift_wrap.failed.is_empty() {
                 println!(
-                    "Could not publish to {:?}",
+                    "Published event {} succufully to {}",
+                    gift_wrap.val,
                     gift_wrap
-                        .failed
-                        .keys()
-                        .map(|relay| relay.to_string())
+                        .success
+                        .iter()
+                        .map(|s| s.to_string())
                         .collect::<Vec<_>>()
                         .join(", ")
                 );
+
+                if !gift_wrap.failed.is_empty() {
+                    println!(
+                        "Could not publish to {:?}",
+                        gift_wrap
+                            .failed
+                            .keys()
+                            .map(|relay| relay.to_string())
+                            .collect::<Vec<_>>()
+                            .join(", ")
+                    );
+                }
             }
-        }
 
-        TransportType::HttpPost => {
-            let client = Client::new();
-
-            let res = client
-                .post(transport.target.clone())
-                .json(&payload)
-                .send()
-                .await?;
-
-            let status = res.status();
-            if status.is_success() {
-                println!("Successfully posted payment");
-            } else {
-                println!("{res:?}");
-                println!("Error posting payment");
+            TransportType::HttpPost => {
+                let client = Client::new();
+
+                let res = client
+                    .post(transport.target.clone())
+                    .json(&payload)
+                    .send()
+                    .await?;
+
+                let status = res.status();
+                if status.is_success() {
+                    println!("Successfully posted payment");
+                } else {
+                    println!("{res:?}");
+                    println!("Error posting payment");
+                }
             }
         }
+    } else {
+        // If no transport is available, print the token
+        let token = Token::new(
+            matching_wallet.mint_url.clone(),
+            proofs,
+            None,
+            matching_wallet.unit.clone(),
+        );
+        println!("Token: {token}");
     }
 
     Ok(())