Skip to content
Snippets Groups Projects
Select Git revision
  • 83b5f7ce7a09ec477ec7a91d75a56472322e481b
  • master default protected
  • dev
  • appimage
  • fix_gitlab
  • fixappveyor
  • gitlab
  • fix_ci
  • fix_dbus_error
  • fix_ci_osx
  • sakia020
  • fix_travis#1105
  • feature/backend
  • check_uniq_node_by_endpoints
  • qt5.7
  • feature/agent_architecture
  • translations
  • pyqt5.6
  • qtwebengine
  • pyinstaller
  • landscape
  • 0.53.2
  • 0.53.1
  • 0.53.0
  • 0.52.0
  • 0.51.1
  • 0.51.0
  • 0.50.5
  • 0.50.4
  • 0.50.3
  • 0.50.2
  • 0.50.1
  • 0.50.0
  • 0.33.0rc7
  • 0.33.0rc6
  • 0.33.0rc5
  • 0.33.0rc4
  • 0.33.0rc3
  • 0.33.0rc2
  • 0.33.0rc1
  • 0.32.10post1
41 results

transfer.py

Blame
  • profile.rs 37.23 KiB
    use crate::*;
    use anyhow::{anyhow, Result};
    use serde::{Deserialize, Serialize};
    use serde_json::{json, Value};
    use std::collections::HashMap;
    use std::time::{SystemTime, UNIX_EPOCH};
    use sp_core::crypto::Pair;
    use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
    use futures::{SinkExt, StreamExt};
    use url::Url;
    use sha2::{Sha256, Digest};
    use secp256k1::{Secp256k1, SecretKey, PublicKey};
    use bech32::{self, ToBase32, Variant};
    use sp_core::crypto::Ss58Codec;
    use crate::commands::cesium;
    
    /// Derive a Nostr key from a Substrate key
    fn derive_nostr_keys_from_substrate(keypair: &KeyPair) -> Result<(SecretKey, PublicKey), GcliError> {
        // Get the seed from the keypair - use a more direct approach
        let seed = match keypair {
            KeyPair::Sr25519(pair) => {
                // For Sr25519, we'll use the raw bytes directly
                let key_bytes = pair.to_raw_vec();
                // Take the first 32 bytes as the seed
                let mut seed = [0u8; 32];
                for i in 0..std::cmp::min(32, key_bytes.len()) {
                    seed[i] = key_bytes[i];
                }
                seed.to_vec()
            }
            KeyPair::Ed25519(pair) => {
                // For Ed25519, we'll use the raw bytes directly
                let key_bytes = pair.to_raw_vec();
                // Take the first 32 bytes as the seed
                let mut seed = [0u8; 32];
                for i in 0..std::cmp::min(32, key_bytes.len()) {
                    seed[i] = key_bytes[i];
                }
                seed.to_vec()
            }
        };
        
        // Create a secp256k1 secret key from the seed
        let secp = Secp256k1::new();
        let secret_key = SecretKey::from_slice(&seed[0..32])
            .map_err(|e| anyhow!("Failed to create secp256k1 secret key: {}", e))?;
        
        // Derive the public key
        let public_key = PublicKey::from_secret_key(&secp, &secret_key);
        
        Ok((secret_key, public_key))
    }
    
    /// Convert a hex string to bech32 format with the given prefix (npub/nsec)
    fn hex_to_bech32(hex_key: &str, prefix: &str) -> Result<String, GcliError> {
        // Decode the hex string to bytes
        let bytes = hex::decode(hex_key)
            .map_err(|e| anyhow!("Failed to decode hex key: {}", e))?;
        
        // Convert bytes to base32
        let base32_data = bytes.to_base32();
        
        // Encode as bech32
        let bech32_str = bech32::encode(prefix, base32_data, Variant::Bech32)
            .map_err(|e| anyhow!("Failed to encode bech32: {}", e))?;
        
        Ok(bech32_str)
    }
    
    /// Get Nostr public key in hex format
    fn get_nostr_pubkey(keypair: &KeyPair) -> Result<String, GcliError> {
        let (_, public_key) = derive_nostr_keys_from_substrate(keypair)?;
        // Nostr uses the x-only public key (32 bytes)
        let serialized = public_key.serialize();
        // Skip the first byte (format byte) and take only the x coordinate
        let pubkey = hex::encode(&serialized[1..33]);
        Ok(pubkey)
    }
    
    /// Get Nostr public key in bech32 format (npub)
    fn get_nostr_npub(keypair: &KeyPair) -> Result<String, GcliError> {
        let hex_pubkey = get_nostr_pubkey(keypair)?;
        let npub = hex_to_bech32(&hex_pubkey, "npub")?;
        Ok(npub)
    }
    
    /// Get Nostr private key in bech32 format (nsec)
    fn get_nostr_nsec(keypair: &KeyPair) -> Result<String, GcliError> {
        let (secret_key, _) = derive_nostr_keys_from_substrate(keypair)?;
        let hex_seckey = hex::encode(secret_key.secret_bytes());
        let nsec = hex_to_bech32(&hex_seckey, "nsec")?;
        Ok(nsec)
    }
    
    /// Profile subcommands
    #[derive(Clone, Debug, clap::Parser)]
    pub enum Subcommand {
        /// Get Nostr profile data from a relay
        Get {
            /// Relay URL to fetch profile from
            #[clap(short, long)]
            relay: Option<String>,
        },
        /// Set Nostr profile data (NIP-01 kind 0 metadata)
        Set {
            /// Profile name
            #[clap(short, long)]
            name: Option<String>,
            
            /// Profile display name
            #[clap(short = 'd', long)]
            display_name: Option<String>,
            
            /// Profile picture URL
            #[clap(short = 'p', long)]
            picture: Option<String>,
            
            /// Profile about/description
            #[clap(short, long)]
            about: Option<String>,
            
            /// Profile website
            #[clap(short = 'w', long)]
            website: Option<String>,
            
            /// Profile NIP-05 identifier
            #[clap(short = 'i', long)]
            nip05: Option<String>,
            
            /// Relay URL to publish profile to
            #[clap(short, long)]
            relay: Option<String>,
        },
    }
    
    /// Nostr profile metadata (NIP-01 kind 0)
    #[derive(Debug, Serialize, Deserialize)]
    pub struct NostrProfile {
        pub name: Option<String>,
        pub display_name: Option<String>,
        pub picture: Option<String>,
        pub about: Option<String>,
        pub website: Option<String>,
        pub nip05: Option<String>,
        #[serde(flatten)]
        pub additional_fields: HashMap<String, String>,
    }
    
    impl Default for NostrProfile {
        fn default() -> Self {
            Self {
                name: None,
                display_name: None,
                picture: None,
                about: None,
                website: None,
                nip05: None,
                additional_fields: HashMap::new(),
            }
        }
    }
    
    /// Nostr event structure
    #[derive(Debug, Serialize, Deserialize, Clone)]
    pub struct NostrEvent {
        pub id: String,
        pub pubkey: String,
        pub created_at: u64,
        pub kind: u32,
        pub tags: Vec<Vec<String>>,
        pub content: String,
        pub sig: String,
    }
    
    impl NostrEvent {
        /// Create a new unsigned Nostr event
        pub fn new(pubkey: String, kind: u32, content: String) -> Self {
            let created_at = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_secs();
            
            Self {
                id: String::new(), // Will be set after serialization
                pubkey,
                created_at,
                kind,
                tags: Vec::new(),
                content,
                sig: String::new(), // Will be set after signing
            }
        }
        
        /// Calculate the event ID (SHA-256 hash of the serialized event)
        pub fn calculate_id(&mut self) -> Result<(), GcliError> {
            // Create a temporary event for serialization exactly as specified by NIP-01
            // The order is important: [0, pubkey, created_at, kind, tags, content]
            let temp_event = json!([
                0,
                self.pubkey,
                self.created_at,
                self.kind,
                self.tags,
                self.content
            ]);
            
            // Serialize the event with no whitespace and canonical ordering
            // Using to_string() instead of to_string_pretty() to avoid whitespace
            let serialized = serde_json::to_string(&temp_event)
                .map_err(|e| anyhow!("Failed to serialize event: {}", e))?;
            
            // Calculate SHA-256 hash
            let mut hasher = Sha256::new();
            hasher.update(serialized.as_bytes());
            let result = hasher.finalize();
            
            // Set the ID
            self.id = hex::encode(result);
            Ok(())
        }
        
        /// Sign the event with the given keypair
        pub fn sign(&mut self, keypair: &KeyPair) -> Result<(), GcliError> {
            // Calculate ID if not already set
            if self.id.is_empty() {
                self.calculate_id()?;
            }
            
            // Derive Nostr keys from Substrate keypair
            let (secret_key, public_key) = derive_nostr_keys_from_substrate(keypair)?;
            
            // Verify that the pubkey in the event matches our derived pubkey
            let derived_pubkey = hex::encode(&public_key.serialize()[1..33]);
            if self.pubkey != derived_pubkey {
                // Update the pubkey to match
                self.pubkey = derived_pubkey;
                // Recalculate ID with the correct pubkey
                self.calculate_id()?;
            }
            
            // Create a secp256k1 context for Schnorr signatures
            let secp = Secp256k1::new();
            
            // Create a message from the event ID
            let id_bytes = hex::decode(&self.id)
                .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?;
            
            let message = secp256k1::Message::from_digest_slice(&id_bytes)
                .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?;
            
            // Sign the message with Schnorr
            let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures
            let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key);
            let signature = secp256k1::schnorr::Signature::from_slice(
                secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref()
            ).unwrap();
            
            // Set the signature
            self.sig = hex::encode(signature.as_ref());
            
            // Verify the signature
            let verify_result = verify_nostr_event(self);
            
            match verify_result {
                Ok(true) => {
                    Ok(())
                },
                Ok(false) => {
                    self.try_alternative_signing(keypair)
                },
                Err(_e) => {
                    self.try_alternative_signing(keypair)
                }
            }
        }
        
        /// Try an alternative signing method if the first one fails
        fn try_alternative_signing(&mut self, keypair: &KeyPair) -> Result<(), GcliError> {
            // Use a different approach for key derivation
            let seed = match keypair {
                KeyPair::Sr25519(pair) => {
                    // Use a hash of the key bytes
                    let mut hasher = Sha256::new();
                    hasher.update(&pair.to_raw_vec());
                    hasher.finalize().to_vec()
                }
                KeyPair::Ed25519(pair) => {
                    // Use a hash of the key bytes
                    let mut hasher = Sha256::new();
                    hasher.update(&pair.to_raw_vec());
                    hasher.finalize().to_vec()
                }
            };
            
            // Create a secp256k1 secret key from the seed
            let secp = Secp256k1::new();
            let secret_key = SecretKey::from_slice(&seed[0..32])
                .map_err(|e| anyhow!("Failed to create alternative secp256k1 secret key: {}", e))?;
            
            // Derive the public key
            let public_key = PublicKey::from_secret_key(&secp, &secret_key);
            
            // Update the pubkey in the event
            let derived_pubkey = hex::encode(&public_key.serialize()[1..33]);
            self.pubkey = derived_pubkey;
            
            // Recalculate ID with the new pubkey
            self.calculate_id()?;
            
            // Create a message from the event ID
            let id_bytes = hex::decode(&self.id)
                .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?;
            
            let message = secp256k1::Message::from_digest_slice(&id_bytes)
                .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?;
            
            // Sign the message with Schnorr
            let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures
            let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key);
            let signature = secp256k1::schnorr::Signature::from_slice(
                secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref()
            ).unwrap();
            
            // Set the signature
            self.sig = hex::encode(signature.as_ref());
            
            // Verify the signature
            let verify_result = verify_nostr_event(self);
            
            match verify_result {
                Ok(true) => {
                    Ok(())
                },
                Ok(false) => {
                    self.try_third_signing_approach(keypair)
                },
                Err(_e) => {
                    self.try_third_signing_approach(keypair)
                }
            }
        }
    
        /// Try a third signing approach if the first two fail
        fn try_third_signing_approach(&mut self, keypair: &KeyPair) -> Result<(), GcliError> {
            // Use a more complex derivation approach
            let seed = match keypair {
                KeyPair::Sr25519(pair) => {
                    // Use multiple rounds of hashing
                    let mut hasher1 = Sha256::new();
                    hasher1.update("nostr".as_bytes());
                    hasher1.update(&pair.to_raw_vec());
                    let first_hash = hasher1.finalize();
                    
                    let mut hasher2 = Sha256::new();
                    hasher2.update(first_hash);
                    hasher2.finalize().to_vec()
                }
                KeyPair::Ed25519(pair) => {
                    // Use multiple rounds of hashing
                    let mut hasher1 = Sha256::new();
                    hasher1.update("nostr".as_bytes());
                    hasher1.update(&pair.to_raw_vec());
                    let first_hash = hasher1.finalize();
                    
                    let mut hasher2 = Sha256::new();
                    hasher2.update(first_hash);
                    hasher2.finalize().to_vec()
                }
            };
            
            // Create a secp256k1 secret key from the seed
            let secp = Secp256k1::new();
            let secret_key = SecretKey::from_slice(&seed[0..32])
                .map_err(|e| anyhow!("Failed to create third secp256k1 secret key: {}", e))?;
            
            // Derive the public key
            let public_key = PublicKey::from_secret_key(&secp, &secret_key);
            
            // Update the pubkey in the event
            let derived_pubkey = hex::encode(&public_key.serialize()[1..33]);
            self.pubkey = derived_pubkey;
            
            // Recalculate ID with the new pubkey
            self.calculate_id()?;
            
            // Create a message from the event ID
            let id_bytes = hex::decode(&self.id)
                .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?;
            
            let message = secp256k1::Message::from_digest_slice(&id_bytes)
                .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?;
            
            // Sign the message with Schnorr
            let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures
            let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key);
            let signature = secp256k1::schnorr::Signature::from_slice(
                secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref()
            ).unwrap();
            
            // Set the signature
            self.sig = hex::encode(signature.as_ref());
            
            // Verify the signature
            let verify_result = verify_nostr_event(self);
            
            match verify_result {
                Ok(true) => {
                    Ok(())
                },
                Ok(false) => {
                    Err(anyhow!("All signature approaches failed").into())
                },
                Err(e) => {
                    Err(anyhow!("Error verifying third signature: {}", e).into())
                }
            }
        }
    }
    
    /// Verify a Nostr event signature
    fn verify_nostr_event(event: &NostrEvent) -> Result<bool, GcliError> {
        // Decode the pubkey
        let pubkey_bytes = hex::decode(&event.pubkey)
            .map_err(|e| anyhow!("Failed to decode pubkey: {}", e))?;
        
        // Create a secp256k1 context
        let secp = Secp256k1::new();
        
        // Create x-only public key from the 32-byte pubkey
        let xonly_pubkey = secp256k1::XOnlyPublicKey::from_slice(&pubkey_bytes)
            .map_err(|e| anyhow!("Failed to create x-only public key: {}", e))?;
        
        // Decode the ID
        let id_bytes = hex::decode(&event.id)
            .map_err(|e| anyhow!("Failed to decode ID: {}", e))?;
        
        // Create a message from the ID
        let message = secp256k1::Message::from_digest_slice(&id_bytes)
            .map_err(|e| anyhow!("Failed to create message: {}", e))?;
        
        // Decode the signature
        let sig_bytes = hex::decode(&event.sig)
            .map_err(|e| anyhow!("Failed to decode signature: {}", e))?;
        
        // Create a Schnorr signature from the signature bytes
        let signature = secp256k1::schnorr::Signature::from_slice(&sig_bytes)
            .map_err(|e| anyhow!("Failed to create Schnorr signature: {}", e))?;
        
        // Verify the Schnorr signature
        match secp.verify_schnorr(&signature, &message, &xonly_pubkey) {
            Ok(_) => {
                Ok(true)
            },
            Err(_e) => {
                Ok(false)
            }
        }
    }
    
    /// Handle profile commands
    pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
        // Enable debug logging for this module if not already set
        if std::env::var("RUST_LOG").is_err() {
            std::env::set_var("RUST_LOG", "debug");
            log::debug!("RUST_LOG environment variable set to debug");
        }
        
        match command {
            Subcommand::Get { relay } => get_profile(data, relay).await,
            Subcommand::Set {
                name,
                display_name,
                picture,
                about,
                website,
                nip05,
                relay,
            } => {
                set_profile(
                    data,
                    name,
                    display_name,
                    picture,
                    about,
                    website,
                    nip05,
                    relay,
                )
                .await
            }
        }
    }
    
    /// Get Nostr profile from a relay
    async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliError> {
        // Get keypair for signing
        // Use the configured address or prompt the user if not set
        let keypair = fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?;
        
        // Get Nostr pubkey in hex format (used for protocol)
        let pubkey = get_nostr_pubkey(&keypair)?;
        
        // Get Nostr pubkey in bech32 format (for display)
        let npub = get_nostr_npub(&keypair)?;
        
        println!("Searching for profile with Nostr pubkey (hex): {}", pubkey);
        println!("Nostr pubkey (bech32): {}", npub);
        
        // Use default relay if none provided
        let relay = relay_url.unwrap_or_else(|| {
            data.cfg
                .relay
                .clone()
                .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string())
        });
        
        // Ensure the relay URL starts with ws:// or wss://
        let relay = if !relay.starts_with("ws://") && !relay.starts_with("wss://") {
            format!("wss://{}", relay)
        } else {
            relay
        };
        
        // Parse the URL
        let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?;
        
        // Connect to the WebSocket
        println!("Connecting to relay {}...", relay);
        let (mut ws_stream, _) = connect_async(url).await
            .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;
        
        println!("Connected to relay, requesting profile...");
        
        // Create subscription request - CORRECTED FORMAT
        // According to NIP-01, REQ should be an array: ["REQ", <subscription_id>, <filter>, ...]
        let filter = json!({
            "kinds": [0],
            "authors": [pubkey],
            "limit": 1
        });
        
        // Send the request in the correct format
        let req_msg = json!(["REQ", "profile-request", filter]);
        
        println!("Sending request: {}", req_msg.to_string());
        ws_stream.send(Message::Text(req_msg.to_string())).await
            .map_err(|e| anyhow!("Failed to send request: {}", e))?;
        
        // Wait for response with timeout
        let mut profile_found = false;
        let mut profile_content = NostrProfile::default(); // To store parsed content
        let mut g1pubv2_from_tags: Option<String> = None;
        let mut g1pub_from_tags: Option<String> = None;
        let mut event_received: Option<NostrEvent> = None; // Store the whole event for JSON output
        
        // Set a timeout for receiving messages
        let timeout = tokio::time::Duration::from_secs(10); // Increased timeout
        let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100));
        
        let start_time = tokio::time::Instant::now();
        
        while start_time.elapsed() < timeout {
            interval.tick().await;
            
            // Check for messages with a timeout
            match tokio::time::timeout(
                tokio::time::Duration::from_millis(100),
                ws_stream.next()
            ).await {
                Ok(Some(Ok(msg))) => {
                    if let Message::Text(text) = msg {
                        // Parse the message
                        if let Ok(json) = serde_json::from_str::<Value>(&text) {
                            
                            // Check if it's an EVENT message
                            if let Some(event_type) = json.get(0).and_then(|v| v.as_str()) {
                                
                                if event_type == "EVENT" && json.get(1).is_some() && json.get(2).is_some() {
                                    // Attempt to deserialize the whole event first
                                    if let Ok(full_event_obj) = serde_json::from_value::<NostrEvent>(json[2].clone()) {
                                        event_received = Some(full_event_obj.clone()); // Clone for later JSON output
    
                                        // Then parse the content string into NostrProfile
                                        if let Ok(parsed_profile_content) = serde_json::from_str::<NostrProfile>(&full_event_obj.content) {
                                            profile_content = parsed_profile_content;
                                            profile_found = true;
    
                                            // Extract g1pubv2 and g1pub from tags
                                            for tag_vec in &full_event_obj.tags {
                                                if tag_vec.len() >= 2 {
                                                    match tag_vec[0].as_str() {
                                                        "g1pubv2" => g1pubv2_from_tags = Some(tag_vec[1].clone()),
                                                        "g1pub" => g1pub_from_tags = Some(tag_vec[1].clone()),
                                                        _ => {}
                                                    }
                                                }
                                            }
                                            let close_msg = json!(["CLOSE", "profile-request"]);
                                            ws_stream.send(Message::Text(close_msg.to_string())).await
                                                .map_err(|e| anyhow!("Failed to close subscription: {}", e))?;
                                            break;
                                        } else {
                                            log::warn!("Failed to parse NostrProfile content from event content string.");
                                        }
                                    } else {
                                        log::warn!("Failed to parse full NostrEvent object from relay message.");
                                    }
                                } else if event_type == "EOSE" {
                                    // End of stored events
                                    if !profile_found {
                                        // No profile found, close the connection
                                        let close_msg = json!(["CLOSE", "profile-request"]);
                                        ws_stream.send(Message::Text(close_msg.to_string())).await
                                            .map_err(|e| anyhow!("Failed to close subscription: {}", e))?;
                                        
                                        break;
                                    }
                                }
                            }
                        }
                    }
                },
                Ok(Some(Err(_e))) => {
                    break;
                },
                Ok(None) => {
                    // Connection closed
                    break;
                },
                Err(_) => {
                    // Timeout, continue
                    continue;
                }
            }
        }
        
        // Close the WebSocket connection
        ws_stream.close(None).await.ok();
        
        // Display the profile or a message if not found
        if profile_found {
            match data.args.output_format {
                OutputFormat::Human => {
                    println!("Profile for npub: {}", npub);
                    if let Some(name) = &profile_content.name {
                        println!("Name: {}", name);
                    }
                    if let Some(display_name) = &profile_content.display_name {
                        println!("Display Name: {}", display_name);
                    }
                    if let Some(picture) = &profile_content.picture {
                        println!("Picture: {}", picture);
                    }
                    if let Some(about) = &profile_content.about {
                        println!("About: {}", about);
                    }
                    if let Some(website) = &profile_content.website {
                        println!("Website: {}", website);
                    }
                    if let Some(nip05) = &profile_content.nip05 {
                        println!("NIP-05: {}", nip05);
                    }
                    // Display from tags
                    if let Some(g1pubv2) = &g1pubv2_from_tags {
                        println!("g1pubv2 (gdev SS58 Tag): {}", g1pubv2);
                    }
                    if let Some(g1pub) = &g1pub_from_tags {
                        println!("g1pub (Duniter v1 Pubkey Tag): {}", g1pub);
                    }
                    for (key, value) in &profile_content.additional_fields {
                        println!("{}: {}", key, value);
                    }
                }
                OutputFormat::Json => {
                    let output = json!({
                        "pubkey_hex": pubkey,
                        "pubkey_bech32": npub,
                        "profile_content": profile_content, // The content part
                        "tags_custom": {
                            "g1pubv2": g1pubv2_from_tags,
                            "g1pub": g1pub_from_tags
                        },
                        "full_event": event_received // Include the full event if available
                    });
                    println!("{}", serde_json::to_string_pretty(&output).unwrap());
                }
            }
        } else {
            match data.args.output_format {
                OutputFormat::Human => {
                    println!("No profile found for npub: {}", npub);
                }
                OutputFormat::Json => {
                    let output = json!({
                        "pubkey_hex": pubkey,
                        "pubkey_bech32": npub,
                        "profile_content": NostrProfile::default(), // Default empty profile content
                        "tags_custom": {
                            "g1pubv2": Option::<String>::None,
                            "g1pub": Option::<String>::None
                        },
                        "full_event": Option::<NostrEvent>::None
                    });
                    println!("{}", serde_json::to_string_pretty(&output).unwrap());
                }
            }
        }
        
        Ok(())
    }
    
    /// Set Nostr profile and publish to a relay
    async fn set_profile(
        data: Data,
        name: Option<String>,
        display_name: Option<String>,
        picture: Option<String>,
        about: Option<String>,
        website: Option<String>,
        nip05: Option<String>,
        relay_url: Option<String>,
    ) -> Result<(), GcliError> {
            // Check if no options were provided, and if so, display help
        if name.is_none() && display_name.is_none() && picture.is_none() && 
            about.is_none() && website.is_none() && nip05.is_none() {
             println!("No profile options specified. Available options:");
             println!("  -n, --name <NAME>                 Set profile name");
             println!("  -d, --display-name <DISPLAY_NAME> Set profile display name");
             println!("  -p, --picture <PICTURE>           Set profile picture URL");
             println!("  -a, --about <ABOUT>               Set profile about/description");
             println!("  -w, --website <WEBSITE>           Set profile website URL");
             println!("  -i, --nip05 <NIP05>               Set profile NIP-05 identifier");
             println!("  -r, --relay <RELAY>               Specify relay URL to publish to");
             println!("\nExample: gcli profile set --name \"Alice\" --about \"Nostr user\"");
             return Ok(());
        }
    
        // Get keypair for signing
        // Use the configured address or prompt the user if not set
        let keypair = fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?;
        
        // Get Nostr pubkey in hex format (used for protocol)
        let pubkey = get_nostr_pubkey(&keypair)?;
        
        // Get Nostr pubkey in bech32 format (for display)
        let npub = get_nostr_npub(&keypair)?;
        
        // Get Nostr private key in bech32 format (for display)
        let nsec = get_nostr_nsec(&keypair)?;
        
        // ---- START: Calculate g1pubv2 and g1pub for tags ----
        let mut g1pub_for_tag: Option<String> = None;
    
        let account_id: sp_core::crypto::AccountId32 = match &keypair {
            KeyPair::Sr25519(pair) => pair.public().into(),
            KeyPair::Ed25519(pair) => pair.public().into(),
        };
        let gdev_ss58_address = account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(42));
        let g1pubv2_for_tag: Option<String> = Some(gdev_ss58_address);
    
        match cesium::compute_g1v1_public_key(&keypair) {
            Ok(pubkey_g1) => g1pub_for_tag = Some(pubkey_g1),
            Err(_e) => {
                if !matches!(keypair, KeyPair::Ed25519(_)) {
                    log::info!("Cannot compute g1pub (Duniter v1 pubkey) for tag as the key is not Ed25519.");
                } else {
                    log::warn!("Failed to compute g1pub (Duniter v1 pubkey) for tag.");
                }
            }
        }
        // ---- END: Calculate g1pubv2 and g1pub for tags ----
        
        let mut profile_content_obj = NostrProfile::default(); // This is for the 'content' field
        if let Some(val) = name { profile_content_obj.name = Some(val); }
        if let Some(val) = display_name { profile_content_obj.display_name = Some(val); }
        if let Some(val) = picture { profile_content_obj.picture = Some(val); }
        if let Some(val) = about { profile_content_obj.about = Some(val); }
        if let Some(val) = website { profile_content_obj.website = Some(val); }
        if let Some(val) = nip05 { profile_content_obj.nip05 = Some(val); }
        
        let profile_content_json = serde_json::to_string(&profile_content_obj)
            .map_err(|e| anyhow!("Failed to serialize profile content: {}", e))?;
        
        let mut event = NostrEvent::new(pubkey.clone(), 0, profile_content_json);
        event.tags = Vec::new(); // Initialize tags
    
        // Add g1pubv2 and g1pub as tags
        if let Some(g1_ss58) = &g1pubv2_for_tag {
            event.tags.push(vec!["g1pubv2".to_string(), g1_ss58.clone()]);
        }
        if let Some(g1_key) = &g1pub_for_tag {
            event.tags.push(vec!["g1pub".to_string(), g1_key.clone()]);
        }
        
        log::debug!("Created event with pubkey: {}", event.pubkey);
        log::debug!("Event content: {}", event.content);
        log::debug!("Event tags: {:?}", event.tags); // Log the tags
    
        event.calculate_id()?;
        event.sign(&keypair)?;
        
        // Use default relay if none provided
        let relay = relay_url.unwrap_or_else(|| {
            data.cfg
                .relay
                .clone()
                .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string())
        });
        
        // Ensure the relay URL starts with ws:// or wss://
        let relay = if !relay.starts_with("ws://") && !relay.starts_with("wss://") {
            format!("wss://{}", relay)
        } else {
            relay
        };
        
        log::debug!("Using relay URL: {}", relay);
        
        // Parse the URL
        let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?;
        
        // Connect to the WebSocket
        println!("Connecting to relay {}...", relay);
        let (mut ws_stream, _) = connect_async(url).await
            .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;
        
        println!("Connected to relay, publishing profile...");
        
        // Create event message - CORRECTED FORMAT
        // According to NIP-01, EVENT should be an array: ["EVENT", <event_object>]
        let event_msg = json!([
            "EVENT",
            event
        ]);
        
        let event_msg_str = event_msg.to_string();
        log::debug!("Sending message to relay: {}", event_msg_str);
        
        // Send the event
        ws_stream.send(Message::Text(event_msg_str)).await
            .map_err(|e| anyhow!("Failed to send event: {}", e))?;
        
        // Wait for OK message with timeout
        let mut success = false;
        let mut response_message = String::new();
        
        // Set a timeout for receiving messages
        let timeout = tokio::time::Duration::from_secs(10); // Increased timeout
        let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100));
        
        log::debug!("Waiting for relay response with timeout of {} seconds", timeout.as_secs());
        
        let start_time = tokio::time::Instant::now();
        
        while start_time.elapsed() < timeout {
            interval.tick().await;
            
            // Check for messages with a timeout
            match tokio::time::timeout(
                tokio::time::Duration::from_millis(100),
                ws_stream.next()
            ).await {
                Ok(Some(Ok(msg))) => {
                    if let Message::Text(text) = msg {
                        log::debug!("Received message from relay: {}", text);
                        response_message = text.clone();
                        
                        // Parse the message
                        if let Ok(json) = serde_json::from_str::<Value>(&text) {
                            log::debug!("Parsed JSON response: {}", json);
                            
                            // Check if it's an OK message
                            if let Some(msg_type) = json.get(0).and_then(|v| v.as_str()) {
                                log::debug!("Message type: {}", msg_type);
                                
                                if msg_type == "OK" && json.get(1).is_some() && json.get(2).is_some() {
                                    let success_status = json[2].as_bool().unwrap_or(false);
                                    log::debug!("OK status: {}", success_status);
                                    
                                    if success_status {
                                        success = true;
                                        log::debug!("Event accepted by relay");
                                        break;
                                    } else {
                                        // Error message
                                        if let Some(error_msg) = json.get(3).and_then(|v| v.as_str()) {
                                            log::error!("Relay rejected event: {}", error_msg);
                                            return Err(anyhow!("Relay rejected event: {}", error_msg).into());
                                        } else {
                                            log::error!("Relay rejected event without specific error message");
                                        }
                                        break;
                                    }
                                } else if msg_type == "NOTICE" && json.get(1).and_then(|v| v.as_str()).is_some() {
                                    log::debug!("Received NOTICE: {}", json[1].as_str().unwrap());
                                }
                            }
                        } else {
                            log::warn!("Failed to parse relay response as JSON: {}", text);
                        }
                    } else {
                        log::debug!("Received non-text message from relay");
                    }
                },
                Ok(Some(Err(e))) => {
                    log::error!("WebSocket error: {}", e);
                    break;
                },
                Ok(None) => {
                    log::debug!("Connection closed by relay");
                    break;
                },
                Err(_) => {
                    // Timeout, continue
                    continue;
                }
            }
        }
        
        if start_time.elapsed() >= timeout {
            log::warn!("Timeout waiting for relay response");
        }
        
        // Close the WebSocket connection
        log::debug!("Closing WebSocket connection");
        ws_stream.close(None).await.ok();
        
        match data.args.output_format {
            OutputFormat::Human => {
                println!("Profile data published to npub (bech32): {}", npub);
                println!("Nostr pubkey (hex): {}", pubkey);
                
                if let Some(val) = &profile_content_obj.name { println!("Name: {}", val); }
                if let Some(val) = &profile_content_obj.display_name { println!("Display Name: {}", val); }
                if let Some(val) = &profile_content_obj.picture { println!("Picture: {}", val); }
                if let Some(val) = &profile_content_obj.about { println!("About: {}", val); }
                if let Some(val) = &profile_content_obj.website { println!("Website: {}", val); }
                if let Some(val) = &profile_content_obj.nip05 { println!("NIP-05: {}", val); }
    
                // Print calculated tag values for user feedback
                if let Some(g1_ss58) = &g1pubv2_for_tag {
                    println!("g1pubv2 (Tag to be published): {}", g1_ss58);
                }
                if let Some(g1_key) = &g1pub_for_tag {
                    println!("g1pub (Tag to be published): {}", g1_key);
                }
                
                println!("\nPublished to relay: {}", relay);
                if success {
                    println!("Status: Success");
                } else {
                    println!("Status: No confirmation received from relay");
                    println!("Last response: {}", response_message);
                }
            }
            OutputFormat::Json => {
                println!("{}", serde_json::to_string_pretty(&json!({
                    "pubkey_hex": pubkey,
                    "pubkey_bech32": npub,
                    "seckey_bech32": nsec,
                    "profile_content_sent": profile_content_obj, // what was in content
                    "calculated_tags": {
                        "g1pubv2": g1pubv2_for_tag,
                        "g1pub": g1pub_for_tag
                    },
                    "event_sent": event, // The full event, including new tags
                    "relay": relay,
                    "success": success,
                    "response": response_message
                })).unwrap());
            }
        }
        
        Ok(())
    }