diff --git a/src/commands/profile.rs b/src/commands/profile.rs index cbbd426519385d202b85275c92f2e2ad62dd2107..67913baf323f500c467123aea985fc3cbdffda46 100644 --- a/src/commands/profile.rs +++ b/src/commands/profile.rs @@ -11,6 +11,8 @@ 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> { @@ -159,7 +161,7 @@ impl Default for NostrProfile { } /// Nostr event structure -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct NostrEvent { pub id: String, pub pubkey: String, @@ -501,7 +503,8 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr // Get Nostr pubkey in bech32 format (for display) let npub = get_nostr_npub(&keypair)?; - println!("Searching for profile with pubkey: {}", pubkey); + 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(|| { @@ -545,8 +548,10 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr // Wait for response with timeout let mut profile_found = false; - let mut profile = NostrProfile::default(); - let mut all_messages: Vec<String> = Vec::new(); // Store all received messages for debugging + 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 @@ -564,7 +569,7 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr ).await { Ok(Some(Ok(msg))) => { if let Message::Text(text) = msg { - all_messages.push(text.clone()); + event_received = Some(serde_json::from_str::<NostrEvent>(&text).unwrap()); // Parse the message if let Ok(json) = serde_json::from_str::<Value>(&text) { @@ -573,25 +578,34 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr 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() { - - if let Some(content) = json[2]["content"].as_str() { - - // Try to parse the profile - match serde_json::from_str::<NostrProfile>(content) { - Ok(parsed_profile) => { - profile = parsed_profile; - profile_found = true; - - // Close the subscription - 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; - }, - Err(_) => { + // 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 @@ -630,25 +644,32 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr match data.args.output_format { OutputFormat::Human => { println!("Profile for npub: {}", npub); - if let Some(name) = &profile.name { + if let Some(name) = &profile_content.name { println!("Name: {}", name); } - if let Some(display_name) = &profile.display_name { + if let Some(display_name) = &profile_content.display_name { println!("Display Name: {}", display_name); } - if let Some(picture) = &profile.picture { + if let Some(picture) = &profile_content.picture { println!("Picture: {}", picture); } - if let Some(about) = &profile.about { + if let Some(about) = &profile_content.about { println!("About: {}", about); } - if let Some(website) = &profile.website { + if let Some(website) = &profile_content.website { println!("Website: {}", website); } - if let Some(nip05) = &profile.nip05 { + if let Some(nip05) = &profile_content.nip05 { println!("NIP-05: {}", nip05); } - for (key, value) in &profile.additional_fields { + // 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); } } @@ -656,7 +677,12 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr let output = json!({ "pubkey_hex": pubkey, "pubkey_bech32": npub, - "profile": profile + "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()); } @@ -670,7 +696,12 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr let output = json!({ "pubkey_hex": pubkey, "pubkey_bech32": npub, - "profile": NostrProfile::default() + "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()); } @@ -719,60 +750,57 @@ async fn set_profile( // Get Nostr private key in bech32 format (for display) let nsec = get_nostr_nsec(&keypair)?; - // Create profile data - let mut profile = NostrProfile::default(); - - if let Some(name) = name { - profile.name = Some(name); - } - if let Some(display_name) = display_name { - profile.display_name = Some(display_name); - } - if let Some(picture) = picture { - profile.picture = Some(picture); - } - if let Some(about) = about { - profile.about = Some(about); - } - if let Some(website) = website { - profile.website = Some(website); - } - if let Some(nip05) = nip05 { - profile.nip05 = Some(nip05); + // ---- START: Calculate g1pubv2 and g1pub for tags ---- + let mut g1pubv2_for_tag: Option<String> = None; + 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)); + g1pubv2_for_tag = 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 ---- - // Serialize profile to JSON - let profile_json = serde_json::to_string(&profile) - .map_err(|e| anyhow!("Failed to serialize profile: {}", e))?; + 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); } - // Create and sign Nostr event - let mut event = NostrEvent::new(pubkey.clone(), 0, profile_json); + let profile_content_json = serde_json::to_string(&profile_content_obj) + .map_err(|e| anyhow!("Failed to serialize profile content: {}", e))?; - // Make sure tags is initialized as an empty array, not null - event.tags = Vec::new(); + 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); - - // Calculate ID and sign + log::debug!("Event tags: {:?}", event.tags); // Log the tags + event.calculate_id()?; - log::debug!("Calculated event ID: {}", event.id); - event.sign(&keypair)?; - log::debug!("Signed event with signature: {}", event.sig); - - // Log the complete event for debugging - log::debug!("Event to publish: {}", serde_json::to_string_pretty(&event).unwrap()); - - // Verify the event signature - match verify_nostr_event(&event) { - Ok(true) => log::debug!("Event signature verified successfully"), - Ok(false) => { - log::error!("Event signature verification failed - relay will likely reject this event"); - return Err(anyhow!("Event signature verification failed - cannot proceed").into()); - }, - Err(e) => log::warn!("Error verifying event signature: {}", e), - } // Use default relay if none provided let relay = relay_url.unwrap_or_else(|| { @@ -902,25 +930,22 @@ async fn set_profile( match data.args.output_format { OutputFormat::Human => { - println!("Profile data published to npub: {}", npub); + println!("Profile data published to npub (bech32): {}", npub); + println!("Nostr pubkey (hex): {}", pubkey); - if let Some(name) = &profile.name { - println!("Name: {}", name); - } - if let Some(display_name) = &profile.display_name { - println!("Display Name: {}", display_name); - } - if let Some(picture) = &profile.picture { - println!("Picture: {}", picture); - } - if let Some(about) = &profile.about { - println!("About: {}", about); - } - if let Some(website) = &profile.website { - println!("Website: {}", website); + 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(nip05) = &profile.nip05 { - println!("NIP-05: {}", nip05); + if let Some(g1_key) = &g1pub_for_tag { + println!("g1pub (Tag to be published): {}", g1_key); } println!("\nPublished to relay: {}", relay); @@ -936,8 +961,12 @@ async fn set_profile( "pubkey_hex": pubkey, "pubkey_bech32": npub, "seckey_bech32": nsec, - "profile": profile, - "event": event, + "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