Browse Source

Add Payment Requests to FFI (#1351)

* Add Payment Requests to FFI


---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
David Caseria 2 tháng trước cách đây
mục cha
commit
74204e4aa1

+ 1 - 1
crates/cdk-ffi/Cargo.toml

@@ -15,7 +15,7 @@ name = "cdk_ffi"
 [dependencies]
 async-trait = { workspace = true }
 bip39 = { workspace = true }
-cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "bip353"] }
+cdk = { workspace = true, default-features = false, features = ["wallet", "auth", "bip353", "nostr"] }
 cdk-sqlite = { workspace = true }
 cdk-postgres = { workspace = true, optional = true }
 futures = { workspace = true }

+ 94 - 0
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -14,6 +14,9 @@ use cdk::wallet::multi_mint_wallet::{
 
 use crate::error::FfiError;
 use crate::token::Token;
+use crate::types::payment_request::{
+    CreateRequestParams, CreateRequestResult, NostrWaitInfo, PaymentRequest,
+};
 use crate::types::*;
 
 /// FFI-compatible MultiMintWallet
@@ -661,6 +664,97 @@ impl MultiMintWallet {
     }
 }
 
+/// Payment request methods for MultiMintWallet
+#[uniffi::export(async_runtime = "tokio")]
+impl MultiMintWallet {
+    /// Create a NUT-18 payment request
+    ///
+    /// Creates a payment request that can be shared to receive Cashu tokens.
+    /// The request can include optional amount, description, and spending conditions.
+    ///
+    /// # Arguments
+    ///
+    /// * `params` - Parameters for creating the payment request
+    ///
+    /// # Transport Options
+    ///
+    /// - `"nostr"` - Uses Nostr relays for privacy-preserving delivery (requires nostr_relays)
+    /// - `"http"` - Uses HTTP POST for delivery (requires http_url)
+    /// - `"none"` - No transport; token must be delivered out-of-band
+    ///
+    /// # Example
+    ///
+    /// ```ignore
+    /// let params = CreateRequestParams {
+    ///     amount: Some(100),
+    ///     unit: "sat".to_string(),
+    ///     description: Some("Coffee payment".to_string()),
+    ///     transport: "http".to_string(),
+    ///     http_url: Some("https://example.com/callback".to_string()),
+    ///     ..Default::default()
+    /// };
+    /// let result = wallet.create_request(params).await?;
+    /// println!("Share this request: {}", result.payment_request.to_string_encoded());
+    ///
+    /// // If using Nostr transport, wait for payment:
+    /// if let Some(nostr_info) = result.nostr_wait_info {
+    ///     let amount = wallet.wait_for_nostr_payment(nostr_info).await?;
+    ///     println!("Received {} sats", amount);
+    /// }
+    /// ```
+    pub async fn create_request(
+        &self,
+        params: CreateRequestParams,
+    ) -> Result<CreateRequestResult, FfiError> {
+        let (payment_request, nostr_wait_info) = self.inner.create_request(params.into()).await?;
+        Ok(CreateRequestResult {
+            payment_request: Arc::new(PaymentRequest::from_inner(payment_request)),
+            nostr_wait_info: nostr_wait_info.map(|info| Arc::new(NostrWaitInfo::from_inner(info))),
+        })
+    }
+
+    /// Wait for a Nostr payment and receive it into the wallet
+    ///
+    /// This method connects to the Nostr relays specified in the `NostrWaitInfo`,
+    /// subscribes for incoming payment events, and receives the first valid
+    /// payment into the wallet.
+    ///
+    /// # Arguments
+    ///
+    /// * `info` - The Nostr wait info returned from `create_request` when using Nostr transport
+    ///
+    /// # Returns
+    ///
+    /// The amount received from the payment.
+    ///
+    /// # Example
+    ///
+    /// ```ignore
+    /// let result = wallet.create_request(params).await?;
+    /// if let Some(nostr_info) = result.nostr_wait_info {
+    ///     let amount = wallet.wait_for_nostr_payment(nostr_info).await?;
+    ///     println!("Received {} sats", amount);
+    /// }
+    /// ```
+    pub async fn wait_for_nostr_payment(
+        &self,
+        info: Arc<NostrWaitInfo>,
+    ) -> Result<Amount, FfiError> {
+        // We need to clone the inner NostrWaitInfo since we can't consume the Arc
+        let info_inner = cdk::wallet::payment_request::NostrWaitInfo {
+            keys: info.inner().keys.clone(),
+            relays: info.inner().relays.clone(),
+            pubkey: info.inner().pubkey,
+        };
+        let amount = self
+            .inner
+            .wait_for_nostr_payment(info_inner)
+            .await
+            .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
+        Ok(amount.into())
+    }
+}
+
 /// Auth methods for MultiMintWallet
 #[uniffi::export(async_runtime = "tokio")]
 impl MultiMintWallet {

+ 2 - 0
crates/cdk-ffi/src/types/mod.rs

@@ -8,6 +8,7 @@ pub mod amount;
 pub mod invoice;
 pub mod keys;
 pub mod mint;
+pub mod payment_request;
 pub mod proof;
 pub mod quote;
 pub mod subscription;
@@ -19,6 +20,7 @@ pub use amount::*;
 pub use invoice::*;
 pub use keys::*;
 pub use mint::*;
+pub use payment_request::*;
 pub use proof::*;
 pub use quote::*;
 pub use subscription::*;

+ 370 - 0
crates/cdk-ffi/src/types/payment_request.rs

@@ -0,0 +1,370 @@
+//! Payment Request FFI types (NUT-18)
+
+use std::sync::Arc;
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, CurrencyUnit};
+use crate::error::FfiError;
+
+/// Transport type for payment request delivery
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
+pub enum TransportType {
+    /// Nostr transport (privacy-preserving)
+    Nostr,
+    /// HTTP POST transport
+    HttpPost,
+}
+
+impl From<cdk::nuts::TransportType> for TransportType {
+    fn from(t: cdk::nuts::TransportType) -> Self {
+        match t {
+            cdk::nuts::TransportType::Nostr => TransportType::Nostr,
+            cdk::nuts::TransportType::HttpPost => TransportType::HttpPost,
+        }
+    }
+}
+
+impl From<TransportType> for cdk::nuts::TransportType {
+    fn from(t: TransportType) -> Self {
+        match t {
+            TransportType::Nostr => cdk::nuts::TransportType::Nostr,
+            TransportType::HttpPost => cdk::nuts::TransportType::HttpPost,
+        }
+    }
+}
+
+/// Transport for payment request delivery
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct Transport {
+    /// Transport type
+    pub transport_type: TransportType,
+    /// Target (e.g., nprofile for Nostr, URL for HTTP)
+    pub target: String,
+    /// Optional tags
+    pub tags: Option<Vec<Vec<String>>>,
+}
+
+impl From<cdk::nuts::Transport> for Transport {
+    fn from(t: cdk::nuts::Transport) -> Self {
+        Self {
+            transport_type: t._type.into(),
+            target: t.target,
+            tags: t.tags,
+        }
+    }
+}
+
+impl From<Transport> for cdk::nuts::Transport {
+    fn from(t: Transport) -> Self {
+        Self {
+            _type: t.transport_type.into(),
+            target: t.target,
+            tags: t.tags,
+        }
+    }
+}
+
+/// NUT-18 Payment Request
+///
+/// A payment request that can be shared to request Cashu tokens.
+/// Encoded as a string with the `creqA` prefix.
+#[derive(uniffi::Object)]
+pub struct PaymentRequest {
+    inner: cdk::nuts::PaymentRequest,
+}
+
+impl PaymentRequest {
+    /// Create from inner CDK type
+    pub(crate) fn from_inner(inner: cdk::nuts::PaymentRequest) -> Self {
+        Self { inner }
+    }
+
+    /// Get inner reference
+    pub(crate) fn inner(&self) -> &cdk::nuts::PaymentRequest {
+        &self.inner
+    }
+}
+
+#[uniffi::export]
+impl PaymentRequest {
+    /// Parse a payment request from its encoded string representation
+    #[uniffi::constructor]
+    pub fn from_string(encoded: String) -> Result<Arc<Self>, FfiError> {
+        use std::str::FromStr;
+        let inner = cdk::nuts::PaymentRequest::from_str(&encoded)
+            .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
+        Ok(Arc::new(Self { inner }))
+    }
+
+    /// Encode the payment request to a string
+    pub fn to_string_encoded(&self) -> String {
+        self.inner.to_string()
+    }
+
+    /// Get the payment ID
+    pub fn payment_id(&self) -> Option<String> {
+        self.inner.payment_id.clone()
+    }
+
+    /// Get the requested amount
+    pub fn amount(&self) -> Option<Amount> {
+        self.inner.amount.map(|a| a.into())
+    }
+
+    /// Get the currency unit
+    pub fn unit(&self) -> Option<CurrencyUnit> {
+        self.inner.unit.clone().map(|u| u.into())
+    }
+
+    /// Get whether this is a single-use request
+    pub fn single_use(&self) -> Option<bool> {
+        self.inner.single_use
+    }
+
+    /// Get the list of acceptable mint URLs
+    pub fn mints(&self) -> Option<Vec<String>> {
+        self.inner
+            .mints
+            .as_ref()
+            .map(|mints| mints.iter().map(|m| m.to_string()).collect())
+    }
+
+    /// Get the description
+    pub fn description(&self) -> Option<String> {
+        self.inner.description.clone()
+    }
+
+    /// Get the transports for delivering the payment
+    pub fn transports(&self) -> Vec<Transport> {
+        self.inner
+            .transports
+            .iter()
+            .cloned()
+            .map(|t| t.into())
+            .collect()
+    }
+}
+
+/// Parameters for creating a NUT-18 payment request
+#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
+pub struct CreateRequestParams {
+    /// Optional amount to request (in smallest unit for the currency)
+    pub amount: Option<u64>,
+    /// Currency unit (e.g., "sat", "msat", "usd")
+    pub unit: String,
+    /// Optional description for the request
+    pub description: Option<String>,
+    /// Optional public keys for P2PK spending conditions (hex-encoded)
+    pub pubkeys: Option<Vec<String>>,
+    /// Required number of signatures for multisig (defaults to 1)
+    pub num_sigs: u64,
+    /// Optional HTLC hash (hex-encoded SHA-256)
+    pub hash: Option<String>,
+    /// Optional HTLC preimage (alternative to hash)
+    pub preimage: Option<String>,
+    /// Transport type: "nostr", "http", or "none"
+    pub transport: String,
+    /// HTTP URL for HTTP transport (required if transport is "http")
+    pub http_url: Option<String>,
+    /// Nostr relay URLs (required if transport is "nostr")
+    pub nostr_relays: Option<Vec<String>>,
+}
+
+impl Default for CreateRequestParams {
+    fn default() -> Self {
+        Self {
+            amount: None,
+            unit: "sat".to_string(),
+            description: None,
+            pubkeys: None,
+            num_sigs: 1,
+            hash: None,
+            preimage: None,
+            transport: "none".to_string(),
+            http_url: None,
+            nostr_relays: None,
+        }
+    }
+}
+
+impl From<CreateRequestParams> for cdk::wallet::payment_request::CreateRequestParams {
+    fn from(params: CreateRequestParams) -> Self {
+        Self {
+            amount: params.amount,
+            unit: params.unit,
+            description: params.description,
+            pubkeys: params.pubkeys,
+            num_sigs: params.num_sigs,
+            hash: params.hash,
+            preimage: params.preimage,
+            transport: params.transport,
+            http_url: params.http_url,
+            nostr_relays: params.nostr_relays,
+        }
+    }
+}
+
+impl From<cdk::wallet::payment_request::CreateRequestParams> for CreateRequestParams {
+    fn from(params: cdk::wallet::payment_request::CreateRequestParams) -> Self {
+        Self {
+            amount: params.amount,
+            unit: params.unit,
+            description: params.description,
+            pubkeys: params.pubkeys,
+            num_sigs: params.num_sigs,
+            hash: params.hash,
+            preimage: params.preimage,
+            transport: params.transport,
+            http_url: params.http_url,
+            nostr_relays: params.nostr_relays,
+        }
+    }
+}
+
+/// Decode a payment request from its encoded string representation
+#[uniffi::export]
+pub fn decode_payment_request(encoded: String) -> Result<Arc<PaymentRequest>, FfiError> {
+    PaymentRequest::from_string(encoded)
+}
+
+/// Encode CreateRequestParams to JSON string
+#[uniffi::export]
+pub fn encode_create_request_params(params: CreateRequestParams) -> Result<String, FfiError> {
+    Ok(serde_json::to_string(&params)?)
+}
+
+/// Decode CreateRequestParams from JSON string
+#[uniffi::export]
+pub fn decode_create_request_params(json: String) -> Result<CreateRequestParams, FfiError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Information needed to wait for an incoming Nostr payment
+///
+/// Returned by `create_request` when the transport is `nostr`. Pass this to
+/// `wait_for_nostr_payment` to connect, subscribe, and receive the incoming
+/// payment on the specified relays.
+#[derive(uniffi::Object)]
+pub struct NostrWaitInfo {
+    inner: cdk::wallet::payment_request::NostrWaitInfo,
+}
+
+impl NostrWaitInfo {
+    /// Create from inner CDK type
+    pub(crate) fn from_inner(inner: cdk::wallet::payment_request::NostrWaitInfo) -> Self {
+        Self { inner }
+    }
+
+    /// Get inner reference
+    pub(crate) fn inner(&self) -> &cdk::wallet::payment_request::NostrWaitInfo {
+        &self.inner
+    }
+}
+
+#[uniffi::export]
+impl NostrWaitInfo {
+    /// Get the Nostr relays to connect to
+    pub fn relays(&self) -> Vec<String> {
+        self.inner.relays.clone()
+    }
+
+    /// Get the recipient public key as a hex string
+    pub fn pubkey(&self) -> String {
+        self.inner.pubkey.to_hex()
+    }
+}
+
+/// Result of creating a payment request
+///
+/// Contains the payment request and optionally the Nostr wait info
+/// if the transport was set to "nostr".
+#[derive(uniffi::Record)]
+pub struct CreateRequestResult {
+    /// The payment request to share with the payer
+    pub payment_request: Arc<PaymentRequest>,
+    /// Nostr wait info (present when transport is "nostr")
+    pub nostr_wait_info: Option<Arc<NostrWaitInfo>>,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
+
+    #[test]
+    fn test_decode_payment_request() {
+        let req = PaymentRequest::from_string(PAYMENT_REQUEST.to_string()).unwrap();
+
+        assert_eq!(req.payment_id().unwrap(), "b7a90176");
+        assert_eq!(req.amount().unwrap().value, 10);
+        assert!(matches!(req.unit().unwrap(), CurrencyUnit::Sat));
+
+        let mints = req.mints().unwrap();
+        assert_eq!(mints.len(), 1);
+        assert_eq!(mints[0], "https://nofees.testnut.cashu.space");
+
+        let transports = req.transports();
+        assert_eq!(transports.len(), 1);
+        assert!(matches!(transports[0].transport_type, TransportType::Nostr));
+    }
+
+    #[test]
+    fn test_roundtrip_payment_request() {
+        let req = PaymentRequest::from_string(PAYMENT_REQUEST.to_string()).unwrap();
+        let encoded = req.to_string_encoded();
+        let decoded = PaymentRequest::from_string(encoded).unwrap();
+
+        assert_eq!(req.payment_id(), decoded.payment_id());
+        assert_eq!(
+            req.amount().map(|a| a.value),
+            decoded.amount().map(|a| a.value)
+        );
+    }
+
+    #[test]
+    fn test_transport_conversion() {
+        let ffi_transport = Transport {
+            transport_type: TransportType::Nostr,
+            target: "nprofile1...".to_string(),
+            tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
+        };
+
+        let cdk_transport: cdk::nuts::Transport = ffi_transport.clone().into();
+        let back: Transport = cdk_transport.into();
+
+        assert_eq!(ffi_transport.transport_type, back.transport_type);
+        assert_eq!(ffi_transport.target, back.target);
+        assert_eq!(ffi_transport.tags, back.tags);
+    }
+
+    #[test]
+    fn test_create_request_params_default() {
+        let params = CreateRequestParams::default();
+
+        assert_eq!(params.unit, "sat");
+        assert_eq!(params.num_sigs, 1);
+        assert_eq!(params.transport, "none");
+        assert!(params.amount.is_none());
+    }
+
+    #[test]
+    fn test_create_request_params_serialization() {
+        let params = CreateRequestParams {
+            amount: Some(100),
+            unit: "sat".to_string(),
+            description: Some("Test payment".to_string()),
+            transport: "http".to_string(),
+            http_url: Some("https://example.com/callback".to_string()),
+            ..Default::default()
+        };
+
+        let json = encode_create_request_params(params.clone()).unwrap();
+        let decoded = decode_create_request_params(json).unwrap();
+
+        assert_eq!(params.amount, decoded.amount);
+        assert_eq!(params.unit, decoded.unit);
+        assert_eq!(params.description, decoded.description);
+    }
+}

+ 24 - 0
crates/cdk-ffi/src/wallet.rs

@@ -8,6 +8,7 @@ use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};
 
 use crate::error::FfiError;
 use crate::token::Token;
+use crate::types::payment_request::PaymentRequest;
 use crate::types::*;
 
 /// FFI-compatible Wallet
@@ -475,6 +476,29 @@ impl Wallet {
             .await?;
         Ok(fee.into())
     }
+
+    /// Pay a NUT-18 payment request
+    ///
+    /// This method prepares and sends a payment for the given payment request.
+    /// It will use the Nostr or HTTP transport specified in the request.
+    ///
+    /// # Arguments
+    ///
+    /// * `payment_request` - The NUT-18 payment request to pay
+    /// * `custom_amount` - Optional amount to pay (required if request has no amount)
+    pub async fn pay_request(
+        &self,
+        payment_request: std::sync::Arc<PaymentRequest>,
+        custom_amount: Option<Amount>,
+    ) -> Result<(), FfiError> {
+        self.inner
+            .pay_request(
+                payment_request.inner().clone(),
+                custom_amount.map(Into::into),
+            )
+            .await?;
+        Ok(())
+    }
 }
 
 /// BIP353 methods for Wallet

+ 4 - 0
crates/cdk/Cargo.toml

@@ -143,6 +143,10 @@ name = "human_readable_payment"
 required-features = ["wallet", "bip353"]
 
 [[example]]
+name = "payment_request"
+required-features = ["wallet", "nostr"]
+
+[[example]]
 name = "token-proofs"
 required-features = ["wallet"]
 

+ 252 - 0
crates/cdk/examples/payment_request.rs

@@ -0,0 +1,252 @@
+//! # Payment Request Example (NUT-18)
+//!
+//! This example demonstrates how to create and receive payments using NUT-18
+//! payment requests with the MultiMintWallet. It shows both HTTP and Nostr
+//! transport options.
+//!
+//! ## Payment Request Flow
+//!
+//! 1. Receiver creates a payment request with desired parameters
+//! 2. Receiver shares the encoded payment request string with the payer
+//! 3. Payer decodes the request and sends tokens via the specified transport
+//! 4. Receiver waits for and receives the payment
+//!
+//! ## Transport Options
+//!
+//! - **Nostr**: Privacy-preserving delivery via Nostr relays (gift-wrapped events)
+//! - **HTTP**: Direct delivery to a specified callback URL
+//! - **None**: Out-of-band delivery (receiver must receive tokens manually)
+//!
+//! ## Usage
+//!
+//! ```bash
+//! cargo run --example payment_request --features="wallet nostr"
+//! ```
+
+use std::sync::Arc;
+use std::time::Duration;
+
+use anyhow::anyhow;
+use cdk::amount::SplitTarget;
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::multi_mint_wallet::MultiMintWallet;
+use cdk::wallet::payment_request::CreateRequestParams;
+use cdk_sqlite::wallet::memory;
+use rand::random;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    println!("NUT-18 Payment Request Example");
+    println!("===============================\n");
+
+    // Generate a random seed for the wallet
+    let seed: [u8; 64] = random();
+
+    // Mint URL and currency unit
+    let mint_url = "https://fake.thesimplekid.dev";
+    let unit = CurrencyUnit::Sat;
+    let initial_amount = cdk::Amount::from(100);
+
+    // Initialize the memory store
+    let localstore = Arc::new(memory::empty().await?);
+
+    // Create a new MultiMintWallet
+    let wallet = MultiMintWallet::new(localstore, seed, unit.clone()).await?;
+
+    // Add the mint to our wallet
+    wallet.add_mint(mint_url.parse()?).await?;
+
+    println!("Step 1: Funding the wallet");
+    println!("---------------------------");
+
+    // Get a wallet for our mint to create a mint quote
+    let mint_wallet = wallet
+        .get_wallet(&mint_url.parse()?)
+        .await
+        .ok_or_else(|| anyhow!("Wallet not found for mint"))?;
+    let mint_quote = mint_wallet.mint_quote(initial_amount, None).await?;
+
+    println!(
+        "Pay this invoice to fund the wallet:\n{}",
+        mint_quote.request
+    );
+    println!("\nQuote ID: {}", mint_quote.id);
+
+    // Wait for payment and mint tokens
+    println!("\nWaiting for payment...");
+    let _proofs = mint_wallet
+        .wait_and_mint_quote(
+            mint_quote,
+            SplitTarget::default(),
+            None,
+            Duration::from_secs(300),
+        )
+        .await?;
+
+    let balance = wallet.total_balance().await?;
+    println!("Wallet funded with {} sats\n", balance);
+
+    // ============================================================================
+    // Example 1: Create a Payment Request with Nostr Transport
+    // ============================================================================
+
+    println!("\n╔════════════════════════════════════════════════════════════════╗");
+    println!("║ Example 1: Payment Request with Nostr Transport               ║");
+    println!("╚════════════════════════════════════════════════════════════════╝\n");
+
+    println!("Creating a payment request for 10 sats via Nostr...\n");
+
+    let nostr_params = CreateRequestParams {
+        amount: Some(10),
+        unit: "sat".to_string(),
+        description: Some("Coffee payment".to_string()),
+        pubkeys: None,
+        num_sigs: 1,
+        hash: None,
+        preimage: None,
+        transport: "nostr".to_string(),
+        http_url: None,
+        nostr_relays: Some(vec![
+            "wss://relay.damus.io".to_string(),
+            "wss://nos.lol".to_string(),
+        ]),
+    };
+
+    let (payment_request, nostr_wait_info) = wallet.create_request(nostr_params).await?;
+
+    println!("Payment Request Created!");
+    println!("------------------------");
+    println!("Encoded: {}\n", payment_request);
+
+    println!("Request Details:");
+    println!("  Amount: {:?}", payment_request.amount);
+    println!("  Unit: {:?}", payment_request.unit);
+    println!("  Description: {:?}", payment_request.description);
+    println!("  Mints: {:?}", payment_request.mints);
+    println!("  Transports: {:?}", payment_request.transports);
+
+    if let Some(ref info) = nostr_wait_info {
+        println!("\nNostr Wait Info:");
+        println!("  Relays: {:?}", info.relays);
+        println!("  Pubkey: {}", info.pubkey);
+
+        println!("\nTo receive payment, call:");
+        println!("  let amount = wallet.wait_for_nostr_payment(nostr_wait_info).await?;");
+        println!("\nThis will:");
+        println!("  1. Connect to the specified Nostr relays");
+        println!("  2. Subscribe for gift-wrapped payment events");
+        println!("  3. Receive and process the first valid payment");
+        println!("  4. Return the received amount");
+
+        // Uncomment to actually wait for a payment:
+        // println!("\nWaiting for Nostr payment...");
+        // let received = wallet.wait_for_nostr_payment(info.clone()).await?;
+        // println!("Received {} sats via Nostr!", received);
+    }
+
+    // ============================================================================
+    // Example 2: Create a Payment Request with HTTP Transport
+    // ============================================================================
+
+    println!("\n\n╔════════════════════════════════════════════════════════════════╗");
+    println!("║ Example 2: Payment Request with HTTP Transport                ║");
+    println!("╚════════════════════════════════════════════════════════════════╝\n");
+
+    println!("Creating a payment request for 21 sats via HTTP...\n");
+
+    let http_params = CreateRequestParams {
+        amount: Some(21),
+        unit: "sat".to_string(),
+        description: Some("Tip jar".to_string()),
+        pubkeys: None,
+        num_sigs: 1,
+        hash: None,
+        preimage: None,
+        transport: "http".to_string(),
+        http_url: Some("https://example.com/cashu/callback".to_string()),
+        nostr_relays: None,
+    };
+
+    let (http_request, _) = wallet.create_request(http_params).await?;
+
+    println!("Payment Request Created!");
+    println!("------------------------");
+    println!("Encoded: {}\n", http_request);
+
+    println!("Request Details:");
+    println!("  Amount: {:?}", http_request.amount);
+    println!("  Unit: {:?}", http_request.unit);
+    println!("  Description: {:?}", http_request.description);
+    println!("  Transports: {:?}", http_request.transports);
+
+    println!("\nWith HTTP transport:");
+    println!("  - Payer will POST tokens to: https://example.com/cashu/callback");
+    println!("  - Your server receives the token and calls wallet.receive()");
+
+    // ============================================================================
+    // Example 3: Create a Payment Request with P2PK Spending Conditions
+    // ============================================================================
+
+    println!("\n\n╔════════════════════════════════════════════════════════════════╗");
+    println!("║ Example 3: Payment Request with P2PK Lock                     ║");
+    println!("╚════════════════════════════════════════════════════════════════╝\n");
+
+    println!("Creating a P2PK-locked payment request...\n");
+
+    // Generate a secret key for the spending condition
+    let secret = cdk::nuts::SecretKey::generate();
+    let pubkey_hex = secret.public_key().to_string();
+
+    let p2pk_params = CreateRequestParams {
+        amount: Some(50),
+        unit: "sat".to_string(),
+        description: Some("Locked payment".to_string()),
+        pubkeys: Some(vec![pubkey_hex.clone()]),
+        num_sigs: 1,
+        hash: None,
+        preimage: None,
+        transport: "nostr".to_string(),
+        http_url: None,
+        nostr_relays: Some(vec!["wss://relay.damus.io".to_string()]),
+    };
+
+    let (p2pk_request, _) = wallet.create_request(p2pk_params).await?;
+
+    println!("P2PK Payment Request Created!");
+    println!("-----------------------------");
+    println!("Encoded: {}\n", p2pk_request);
+
+    println!("Security:");
+    println!("  - Tokens sent to this request will be locked to pubkey:");
+    println!("    {}", pubkey_hex);
+    println!("  - Only the holder of the corresponding secret key can spend");
+
+    // ============================================================================
+    // Example 4: Paying a Payment Request
+    // ============================================================================
+
+    println!("\n\n╔════════════════════════════════════════════════════════════════╗");
+    println!("║ Example 4: Paying a Payment Request                           ║");
+    println!("╚════════════════════════════════════════════════════════════════╝\n");
+
+    println!("To pay a payment request from another wallet:\n");
+
+    println!("```rust");
+    println!("// Decode the payment request");
+    println!("let request = PaymentRequest::from_str(\"creqA...\")?;");
+    println!();
+    println!("// Pay the request (sends tokens via the specified transport)");
+    println!("let result = wallet.pay_request(request).await?;");
+    println!();
+    println!("println!(\"Sent {{}} sats\", result.amount_sent);");
+    println!("```\n");
+
+    println!("The pay_request method will:");
+    println!("  1. Select proofs matching the requested amount and unit");
+    println!("  2. Apply any spending conditions from the request");
+    println!("  3. Deliver the token via the request's transport (Nostr/HTTP)");
+
+    println!("\n✓ Example complete!");
+
+    Ok(())
+}