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