From e66bcb07bb29a84948b20e616223e68674493502 Mon Sep 17 00:00:00 2001
From: fred <support@qo-op.com>
Date: Mon, 19 May 2025 13:22:20 +0200
Subject: [PATCH] Add twinkey support

---
 Cargo.lock              |  33 ++-
 Cargo.toml              |   2 +
 src/commands/profile.rs | 531 ++++++++++++++++++++++++++++++++++++++--
 3 files changed, 550 insertions(+), 16 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 74fb42f..15ebd51 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -54,7 +54,7 @@ checksum = "edeef7d7b199195a2d7d7a8155d2d04aee736e60c5c7bdd7097d115369a8817d"
 dependencies = [
  "age-core",
  "base64 0.21.7",
- "bech32",
+ "bech32 0.9.1",
  "chacha20poly1305",
  "cookie-factory",
  "hmac 0.12.1",
@@ -806,6 +806,12 @@ version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
 
+[[package]]
+name = "bech32"
+version = "0.10.0-beta"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea"
+
 [[package]]
 name = "beef"
 version = "0.5.2"
@@ -851,6 +857,20 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "bitcoin"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae"
+dependencies = [
+ "bech32 0.10.0-beta",
+ "bitcoin-internals",
+ "bitcoin_hashes 0.13.0",
+ "hex-conservative",
+ "hex_lit",
+ "secp256k1",
+]
+
 [[package]]
 name = "bitcoin-internals"
 version = "0.2.0"
@@ -2357,8 +2377,10 @@ version = "0.4.2"
 dependencies = [
  "age",
  "anyhow",
- "bech32",
+ "base64 0.21.7",
+ "bech32 0.9.1",
  "bip39",
+ "bitcoin",
  "bs58",
  "clap",
  "clap_complete",
@@ -2629,6 +2651,12 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
 
+[[package]]
+name = "hex_lit"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
+
 [[package]]
 name = "hkdf"
 version = "0.12.4"
@@ -5193,6 +5221,7 @@ version = "0.28.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
 dependencies = [
+ "bitcoin_hashes 0.13.0",
  "rand",
  "secp256k1-sys",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 8091b73..cef19f6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,8 @@ subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.37.0-duni
     "native",
     "jsonrpsee",
 ] }
+base64 = "0.21.0" 
+bitcoin = "0.31.1" # ou une version compatible
 
 # substrate primitives dependencies
 sp-core = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.14.0" }
diff --git a/src/commands/profile.rs b/src/commands/profile.rs
index 8c0d868..83f6145 100644
--- a/src/commands/profile.rs
+++ b/src/commands/profile.rs
@@ -5,6 +5,7 @@ use serde_json::{json, Value};
 use std::collections::HashMap;
 use std::time::{SystemTime, UNIX_EPOCH};
 use sp_core::crypto::Pair;
+use sp_core::ByteArray;
 use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
 use futures::{SinkExt, StreamExt};
 use url::Url;
@@ -13,6 +14,9 @@ use secp256k1::{Secp256k1, SecretKey, PublicKey};
 use bech32::{self, ToBase32, Variant};
 use sp_core::crypto::Ss58Codec;
 use crate::commands::cesium;
+use bs58;
+use base64;
+use bitcoin::{PrivateKey, Network, Address};
 
 /// Derive a Nostr key from a Substrate key
 fn derive_nostr_keys_from_substrate(keypair: &KeyPair) -> Result<(SecretKey, PublicKey), GcliError> {
@@ -40,9 +44,13 @@ fn derive_nostr_keys_from_substrate(keypair: &KeyPair) -> Result<(SecretKey, Pub
         }
     };
     
-    // Create a secp256k1 secret key from the seed
+    // Derive a seed for secp256k1 by hashing the Ed25519 seed with SHA256, like keygen.py
     let secp = Secp256k1::new();
-    let secret_key = SecretKey::from_slice(&seed[0..32])
+    let mut hasher = Sha256::new();
+    hasher.update(&seed);
+    let secp256k1_seed = hasher.finalize().to_vec();
+
+    let secret_key = SecretKey::from_slice(&secp256k1_seed[0..32])
         .map_err(|e| anyhow!("Failed to create secp256k1 secret key: {}", e))?;
     
     // Derive the public key
@@ -167,6 +175,18 @@ pub enum Subcommand {
         #[clap(short, long)]
         relay: Option<String>,
     },
+    /// Remove Nostr profile and associated events
+    Remove {
+        /// List of relay URLs to remove profile from (comma-separated)
+        #[clap(short, long)]
+        relays: Option<String>,
+    },
+    /// Export keys in different formats
+    Export {
+        /// Export format (ipfs, nostr, bitcoin, monero)
+        #[clap(short, long)]
+        format: String,
+    },
 }
 
 /// Nostr profile metadata (NIP-01 kind 0)
@@ -180,10 +200,18 @@ pub struct NostrProfile {
     pub website: Option<String>,
     pub nip05: Option<String>,
     pub bot: Option<bool>,
+    pub twin_keys: Option<TwinKeys>,
     #[serde(flatten)]
     pub additional_fields: HashMap<String, String>,
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TwinKeys {
+    pub ipns: Option<String>,
+    pub bitcoin: Option<String>,
+    pub monero: Option<String>,
+}
+
 impl Default for NostrProfile {
     fn default() -> Self {
         Self {
@@ -195,6 +223,11 @@ impl Default for NostrProfile {
             website: None,
             nip05: None,
             bot: Some(false),
+            twin_keys: Some(TwinKeys {
+                ipns: None,
+                bitcoin: None,
+                monero: None,
+            }),
             additional_fields: HashMap::new(),
         }
     }
@@ -325,9 +358,12 @@ impl NostrEvent {
                 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());
+                // For Ed25519, take the first 32 bytes (the seed/private key part in raw format)
+                // and hash it with SHA256, like keygen.py does with its 'seed'.
+                let raw_bytes = pair.to_raw_vec();
+                let ed25519_seed = &raw_bytes[..32];
+                let mut hasher = sha2::Sha256::new();
+                hasher.update(ed25519_seed);
                 hasher.finalize().to_vec()
             }
         };
@@ -546,6 +582,8 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
             )
             .await
         }
+        Subcommand::Remove { relays } => remove_profile(data, relays).await,
+        Subcommand::Export { format } => export_keys(data, format).await,
     }
 }
 
@@ -787,7 +825,7 @@ async fn set_profile(
     tw_feed: Option<String>,
     relay_url: Option<String>,
 ) -> Result<(), GcliError> {
-    // Check if no options were provided, and if so, display help
+        // Check if no options were provided, and if so, display help
     if name.is_none() && display_name.is_none() && picture.is_none() && banner.is_none() &&
         about.is_none() && website.is_none() && nip05.is_none() && github.is_none() &&
         twitter.is_none() && mastodon.is_none() && telegram.is_none() {
@@ -823,7 +861,7 @@ async fn set_profile(
     
     // Get Nostr private key in bech32 format (for display)
     let nsec = get_nostr_nsec(&keypair)?;
-
+    
     // Calculate g1pubv2 (gdev SS58) and g1pub (Duniter v1)
     let mut g1pub_for_tag: Option<String> = None;
 
@@ -845,7 +883,7 @@ async fn set_profile(
             }
         }
     }
-
+    
     // Create profile content
     let mut profile_content_obj = NostrProfile::default();
     if let Some(val) = name { profile_content_obj.name = Some(val); }
@@ -857,12 +895,84 @@ async fn set_profile(
     if let Some(val) = nip05 { profile_content_obj.nip05 = Some(val); }
     profile_content_obj.bot = Some(false);
 
+    // Generate twin keys
+    let ipns_key = {
+        // First get the public key bytes
+        let pub_bytes = match &keypair {
+            KeyPair::Sr25519(pair) => pair.public().to_raw_vec(),
+            KeyPair::Ed25519(pair) => pair.public().to_raw_vec(),
+        };
+        
+        // Create protobuf format for IPFS public key
+        let mut protobuf = Vec::new();
+        protobuf.extend_from_slice(&[0x08, 0x01]); // Ed25519 key type
+        protobuf.extend_from_slice(&[0x12, 0x20]); // Length prefix
+        protobuf.extend_from_slice(&pub_bytes);
+        
+        // Add multihash prefix
+        let mut peer_id_bytes = Vec::new();
+        peer_id_bytes.extend_from_slice(&[0x00, 0x24]); // Multihash identity prefix
+        peer_id_bytes.extend_from_slice(&protobuf);
+        
+        // Encode as base58
+        format!("12D3Koo{}", bs58::encode(&peer_id_bytes).into_string())
+    };
+
+    // Bitcoin address (using the same derivation as in keygen.py)
+    let bitcoin_address = match &keypair {
+        KeyPair::Sr25519(pair) => {
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(&pair.to_raw_vec());
+            let seed = hasher.finalize();
+            let secp = secp256k1::Secp256k1::new();
+            let secret_key = secp256k1::SecretKey::from_slice(&seed[0..32])
+                .map_err(|e| anyhow!("Failed to create Bitcoin secret key: {}", e))?;
+            let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
+            hex::encode(&public_key.serialize())
+        },
+        KeyPair::Ed25519(pair) => {
+            // For Ed25519, take the first 32 bytes (the seed/private key part in raw format)
+            // and hash it with SHA256, like keygen.py does with its 'seed'.
+            let raw_bytes = pair.to_raw_vec();
+            let ed25519_seed = &raw_bytes[..32];
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(ed25519_seed);
+            let seed = hasher.finalize();
+            let secp = secp256k1::Secp256k1::new();
+            let secret_key = secp256k1::SecretKey::from_slice(&seed[0..32])
+                .map_err(|e| anyhow!("Failed to create Bitcoin secret key: {}", e))?;
+            let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
+            hex::encode(&public_key.serialize())
+        }
+    };
+
+    // Monero address (using the same derivation as in keygen.py)
+    let monero_address = match &keypair {
+        KeyPair::Sr25519(pair) => {
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(&pair.to_raw_vec());
+            let seed = hasher.finalize();
+            hex::encode(&seed[0..32])
+        },
+        KeyPair::Ed25519(pair) => {
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(&pair.to_raw_vec());
+            let seed = hasher.finalize();
+            hex::encode(&seed[0..32])
+        }
+    };
+    
     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();
 
+    // Add twin keys as tags with proper formats
+    event.tags.push(vec!["twin_key".to_string(), "ipns".to_string(), ipns_key]);
+    event.tags.push(vec!["twin_key".to_string(), "bitcoin".to_string(), bitcoin_address]);
+    event.tags.push(vec!["twin_key".to_string(), "monero".to_string(), monero_address]);
+
     // Add NIP-39 external identity tags
     if let Some(gh) = &github {
         event.tags.push(vec!["i".to_string(), format!("github:{}", gh), "".to_string()]);
@@ -902,7 +1012,7 @@ async fn set_profile(
     if let Some(feed) = &tw_feed {
         event.tags.push(vec!["i".to_string(), format!("tw_feed:{}", feed), "".to_string()]);
     }
-
+    
     log::debug!("Created event with pubkey: {}", event.pubkey);
     log::debug!("Event content: {}", event.content);
     log::debug!("Event tags: {:?}", event.tags); // Log the tags
@@ -1050,7 +1160,7 @@ async fn set_profile(
 
             // Print calculated tag values for user feedback
             if let Some(g1_ss58) = &g1pubv2_for_tag {
-                println!("g1pubv2 (Tag to be published): {}", g1_ss58);
+                println!("\ng1pubv2 (Tag to be published): {}", g1_ss58);
             }
             if let Some(g1_key) = &g1pub_for_tag {
                 println!("g1pub (Tag to be published): {}", g1_key);
@@ -1069,12 +1179,17 @@ async fn set_profile(
                 "pubkey_hex": pubkey,
                 "pubkey_bech32": npub,
                 "seckey_bech32": nsec,
-                "profile_content_sent": profile_content_obj, // what was in content
+                "profile_content_sent": profile_content_obj,
                 "calculated_tags": {
                     "g1pubv2": g1pubv2_for_tag,
-                    "g1pub": g1pub_for_tag
+                    "g1pub": g1pub_for_tag,
+                    "twin_keys": {
+                        "ipns": profile_content_obj.twin_keys.as_ref().and_then(|tk| tk.ipns.clone()),
+                        "bitcoin": profile_content_obj.twin_keys.as_ref().and_then(|tk| tk.bitcoin.clone()),
+                        "monero": profile_content_obj.twin_keys.as_ref().and_then(|tk| tk.monero.clone())
+                    }
                 },
-                "event_sent": event, // The full event, including new tags
+                "event_sent": event,
                 "relay": relay,
                 "success": success,
                 "response": response_message
@@ -1082,5 +1197,393 @@ async fn set_profile(
         }
     }
     
+    Ok(())
+}
+
+/// Remove Nostr profile and associated events from relays
+async fn remove_profile(data: Data, relay_urls: Option<String>) -> Result<(), GcliError> {
+    // Get keypair for signing
+    let keypair = fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?;
+    
+    // Get Nostr pubkey in hex format
+    let pubkey = get_nostr_pubkey(&keypair)?;
+    
+    // Get Nostr pubkey in bech32 format (for display)
+    let npub = get_nostr_npub(&keypair)?;
+    
+    // Get list of relays
+    let relays = relay_urls
+        .map(|urls| urls.split(',').map(String::from).collect::<Vec<_>>())
+        .unwrap_or_else(|| vec![data.cfg
+            .relay
+            .clone()
+            .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string())]);
+
+    println!("Removing profile with Nostr pubkey (hex): {}", pubkey);
+    println!("Nostr pubkey (bech32): {}", npub);
+    
+    for relay in &relays {
+        println!("Sending deletion requests to {}", relay);
+        
+        // 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.clone()
+        };
+        
+        // Parse the URL
+        let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?;
+        
+        // Connect to the WebSocket
+        let (mut ws_stream, _) = connect_async(url).await
+            .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;
+        
+        // Create deletion event for relay list (kind 10002)
+        let mut deletion_event = NostrEvent::new(
+            pubkey.clone(),
+            5, // NIP-09 deletion event kind
+            String::new()
+        );
+        deletion_event.tags = vec![
+            vec!["e".to_string(), pubkey.clone()], // Delete relay list
+            vec!["p".to_string(), pubkey.clone()]  // Delete all events by this pubkey
+        ];
+        
+        // Sign the event
+        deletion_event.sign(&keypair)?;
+        
+        // Create event message
+        let event_msg = json!(["EVENT", deletion_event]);
+        
+        // Send the deletion event
+        ws_stream.send(Message::Text(event_msg.to_string())).await
+            .map_err(|e| anyhow!("Failed to send deletion event: {}", e))?;
+        
+        // Wait for confirmation with timeout
+        let timeout = tokio::time::Duration::from_secs(5);
+        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;
+            
+            match tokio::time::timeout(
+                tokio::time::Duration::from_millis(100),
+                ws_stream.next()
+            ).await {
+                Ok(Some(Ok(msg))) => {
+                    if let Message::Text(text) = msg {
+                        if let Ok(json) = serde_json::from_str::<Value>(&text) {
+                            if let Some(msg_type) = json.get(0).and_then(|v| v.as_str()) {
+                                if msg_type == "OK" {
+                                    println!("Deletion request confirmed by relay");
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                },
+                Ok(Some(Err(e))) => {
+                    println!("Error receiving confirmation: {}", e);
+                    break;
+                },
+                Ok(None) => {
+                    println!("Connection closed by relay");
+                    break;
+                },
+                Err(_) => continue,
+            }
+        }
+        
+        // Close the WebSocket connection
+        ws_stream.close(None).await.ok();
+    }
+    
+    println!("Deletion requests sent to all specified relays");
+    Ok(())
+}
+
+/// Export keys in different formats
+async fn export_keys(data: Data, format: String) -> Result<(), GcliError> {
+    // Get keypair for deriving other keys
+    let keypair = fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?;
+
+    match format.to_lowercase().as_str() {
+        "g1v1" => {
+            // Calculate g1pub (Duniter v1 pubkey)
+            let g1pub = match cesium::compute_g1v1_public_key(&keypair) {
+                Ok(pubkey) => pubkey,
+                Err(e) => {
+                    if !matches!(keypair, KeyPair::Ed25519(_)) {
+                        return Err(anyhow!("Cannot compute g1pub (Duniter v1 pubkey) as the key is not Ed25519.").into());
+                    } else {
+                        return Err(anyhow!("Failed to compute g1pub (Duniter v1 pubkey): {}", e).into());
+                    }
+                }
+            };
+
+            // Get raw key bytes and convert to pubsec format
+            let (secret_pubsec, _) = match &keypair {
+                KeyPair::Ed25519(pair) => {
+                    // Get the raw bytes
+                    let raw_bytes = pair.to_raw_vec();
+                    
+                    // In Ed25519, the first 32 bytes are the private key
+                    let private_key = &raw_bytes[..32];
+                    
+                    // The next 32 bytes are the public key
+                    let public_key = &raw_bytes[32..];
+
+                    // Create the seed from private key
+                    let mut hasher = sha2::Sha512::new();
+                    hasher.update(private_key);
+                    let hash = hasher.finalize();
+                    let seed = &hash[..32];
+
+                    // Format according to duniterpy's SigningKey format
+                    let mut signing_key = Vec::new();
+                    signing_key.extend_from_slice(&[0x01]); // Version
+                    signing_key.extend_from_slice(seed);    // Seed
+                    signing_key.extend_from_slice(public_key); // Public key
+
+                    (bs58::encode(&signing_key).into_string(), raw_bytes)
+                },
+                _ => return Err(anyhow!("G1v1 format only supports Ed25519 keys").into()),
+            };
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Duniter v1 Keys:");
+                    println!("Public Key: {}", g1pub);
+                    println!("Private Key: {}", secret_pubsec);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "g1v1",
+                        "public_key": g1pub,
+                        "private_key": secret_pubsec,
+                        "type": "PubSec",
+                        "version": "1"
+                    })).unwrap());
+                }
+            }
+        },
+        "g1v2" => {
+            // Calculate g1pubv2 (SS58 address)
+            let account_id: sp_core::crypto::AccountId32 = match &keypair {
+                KeyPair::Sr25519(pair) => pair.public().into(),
+                KeyPair::Ed25519(pair) => pair.public().into(),
+            };
+            let g1pubv2 = account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(42));
+
+            // Get raw key bytes for secret
+            let secret = match &keypair {
+                KeyPair::Sr25519(pair) => hex::encode(pair.to_raw_vec()),
+                KeyPair::Ed25519(pair) => hex::encode(pair.to_raw_vec()),
+            };
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Duniter v2 (gdev) Keys:");
+                    println!("SS58 Address: {}", g1pubv2);
+                    println!("Private Key: {}", secret);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "g1v2",
+                        "ss58_address": g1pubv2,
+                        "private_key": secret
+                    })).unwrap());
+                }
+            }
+        },
+        "ipfs" => {
+            // Get public key bytes
+            let pub_bytes = match &keypair {
+                KeyPair::Sr25519(pair) => pair.public().to_raw(),
+                KeyPair::Ed25519(pair) => pair.public().to_raw(),
+            };
+            
+            // Create protobuf format for IPFS key
+            let mut protobuf = Vec::new();
+            protobuf.extend_from_slice(&[0x08, 0x01]); // Ed25519 key type
+            protobuf.extend_from_slice(&[0x12, 0x20]); // Length prefix
+            protobuf.extend_from_slice(&pub_bytes);
+            
+            // Add multihash prefix for PeerID
+            let mut peer_id_bytes = Vec::new();
+            peer_id_bytes.extend_from_slice(&[0x00, 0x24]); // Multihash identity prefix
+            peer_id_bytes.extend_from_slice(&protobuf);
+            
+            // Create private key protobuf
+            let mut priv_protobuf = Vec::new();
+            priv_protobuf.extend_from_slice(&[0x08, 0x01]); // Ed25519 key type
+            priv_protobuf.extend_from_slice(&[0x12, 0x40]); // Length prefix
+            match &keypair {
+                KeyPair::Sr25519(pair) => priv_protobuf.extend_from_slice(&pair.to_raw_vec()),
+                KeyPair::Ed25519(pair) => priv_protobuf.extend_from_slice(&pair.to_raw_vec()),
+            }
+
+            let peer_id = format!("12D3Koo{}", bs58::encode(&peer_id_bytes).into_string());
+            let private_key = base64::encode(&priv_protobuf);
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("IPFS Keys:");
+                    println!("PeerID: {}", peer_id);
+                    println!("Private Key (base64): {}", private_key);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "ipfs",
+                        "peer_id": peer_id,
+                        "private_key": private_key
+                    })).unwrap());
+                }
+            }
+        },
+        "nostr" => {
+            // Get Nostr keys
+            let pubkey = get_nostr_pubkey(&keypair)?;
+            let npub = get_nostr_npub(&keypair)?;
+            let nsec = get_nostr_nsec(&keypair)?;
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Nostr Keys:");
+                    println!("Public Key (hex): {}", pubkey);
+                    println!("Public Key (npub): {}", npub);
+                    println!("Private Key (nsec): {}", nsec);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "nostr",
+                        "public_key_hex": pubkey,
+                        "public_key_bech32": npub,
+                        "private_key_bech32": nsec
+                    })).unwrap());
+                }
+            }
+        },
+        "bitcoin" => {
+            // Generate Bitcoin keys using seed from keypair
+            let seed = match &keypair {
+                KeyPair::Sr25519(pair) => {
+                    let mut hasher = sha2::Sha256::new();
+                    hasher.update(&pair.to_raw_vec());
+                    hasher.finalize()
+                },
+                KeyPair::Ed25519(pair) => {
+                    // For Ed25519, take the first 32 bytes (the seed/private key part in raw format)
+                    // and hash it with SHA256, like keygen.py does with its 'seed'.
+                    let raw_bytes = pair.to_raw_vec();
+                    let ed25519_seed = &raw_bytes[..32];
+                    let mut hasher = sha2::Sha256::new();
+                    hasher.update(ed25519_seed);
+                    hasher.finalize()
+                }
+            };
+
+            let secp = secp256k1::Secp256k1::new();
+            let secret_key = secp256k1::SecretKey::from_slice(&seed[0..32])
+                .map_err(|e| anyhow!("Failed to create Bitcoin secret key: {}", e))?;
+            let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
+
+            // Derive the corresponding public key
+            let public_key_secp = secp256k1::PublicKey::from_secret_key(&secp, &secret_key);
+
+            // Use the 'bitcoin' and 'bitcoin-addr' crates for proper formatting
+            use bitcoin::{PrivateKey, Network, Address};
+            // use bitcoin_addr::AddressType; // Removed
+
+            // Create a PrivateKey object (for mainnet or testnet, let's use Testnet for example)
+            // You might want to make the network configurable
+            let private_key_btc = PrivateKey::new(secret_key, Network::Bitcoin);
+
+            // Get the WIF format private key
+            let private_key_wif = private_key_btc.to_wif();
+
+            // Get the public key hex (compressed format is standard)
+            let public_key_hex = hex::encode(public_key_secp.serialize());
+
+            // Derive the P2PKH address (most common)
+            // Note: This assumes P2PKH. Other address types (P2SH, Bech32) are possible.
+            // keygen.py seems to derive a simple address from compressed pubkey.
+            // Convert secp256k1::PublicKey to bitcoin::PublicKey (using compressed format)
+            let public_key_btc = bitcoin::PublicKey::from_slice(&public_key_secp.serialize()).map_err(|e| anyhow!("Failed to create bitcoin::PublicKey: {}", e))?;
+
+            // Derive the P2PKH address using the bitcoin::PublicKey
+            let address = Address::p2pkh(&public_key_btc, Network::Bitcoin).to_string();
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Bitcoin Keys:");
+                    println!("Private Key (WIF): {}", private_key_wif);
+                    println!("Public Key (hex): {}", public_key_hex);
+                    println!("Address: {}", address);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "bitcoin",
+                        "private_key_wif": private_key_wif,
+                        "public_key_hex": public_key_hex,
+                        "address_p2pkh": address
+                    })).unwrap());
+                }
+            }
+        },
+        "monero" => {
+            // Generate Monero keys using seed from keypair
+            let seed = match &keypair {
+                KeyPair::Sr25519(pair) => {
+                    let mut hasher = sha2::Sha256::new();
+                    hasher.update(&pair.to_raw_vec());
+                    hasher.finalize()
+                },
+                KeyPair::Ed25519(pair) => {
+                    let mut hasher = sha2::Sha256::new();
+                    hasher.update(&pair.to_raw_vec());
+                    hasher.finalize()
+                }
+            };
+
+            let private_spend_key = hex::encode(&seed[0..32]);
+
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Monero Keys:");
+                    println!("Private Spend Key: {}", private_spend_key);
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "format": "monero",
+                        "private_spend_key": private_spend_key
+                    })).unwrap());
+                }
+            }
+        },
+        _ => {
+            let available_formats = vec!["ipfs", "nostr", "bitcoin", "monero", "g1v1", "g1v2"];
+            match data.args.output_format {
+                OutputFormat::Human => {
+                    println!("Available export formats:");
+                    println!("  ipfs    - IPFS PeerID and private key");
+                    println!("  nostr   - Nostr npub/nsec keys");
+                    println!("  bitcoin - Bitcoin WIF and address");
+                    println!("  monero  - Monero keys");
+                    println!("  g1v1    - Duniter v1 public/private keys");
+                    println!("  g1v2    - Duniter v2 (gdev) SS58/private keys");
+                },
+                OutputFormat::Json => {
+                    println!("{}", serde_json::to_string_pretty(&json!({
+                        "available_formats": available_formats
+                    })).unwrap());
+                }
+            }
+            return Ok(());
+        }
+    }
+    
     Ok(())
 } 
\ No newline at end of file
-- 
GitLab