From cb6b32d6176e5dd8f5357b0f8a3ecce77861b197 Mon Sep 17 00:00:00 2001
From: poka <poka@p2p.legal>
Date: Sat, 17 May 2025 03:06:32 +0200
Subject: [PATCH 1/7] Add Nostr profile support

---
 Cargo.lock              |  55 +++
 Cargo.toml              |   5 +
 src/commands.rs         |   1 +
 src/commands/profile.rs | 976 ++++++++++++++++++++++++++++++++++++++++
 src/conf.rs             |  11 +-
 src/main.rs             |   4 +
 6 files changed, 1051 insertions(+), 1 deletion(-)
 create mode 100644 src/commands/profile.rs

diff --git a/Cargo.lock b/Cargo.lock
index 4299623..74fb42f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1596,6 +1596,12 @@ dependencies = [
  "parking_lot_core",
 ]
 
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
 [[package]]
 name = "der"
 version = "0.7.9"
@@ -2351,6 +2357,7 @@ version = "0.4.2"
 dependencies = [
  "age",
  "anyhow",
+ "bech32",
  "bip39",
  "bs58",
  "clap",
@@ -2371,12 +2378,16 @@ dependencies = [
  "rstest",
  "scrypt",
  "sea-orm",
+ "secp256k1",
  "serde",
  "serde_json",
+ "sha2 0.10.8",
  "sp-core",
  "sp-runtime",
  "subxt",
  "tokio",
+ "tokio-tungstenite",
+ "url",
 ]
 
 [[package]]
@@ -5182,6 +5193,7 @@ version = "0.28.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
 dependencies = [
+ "rand",
  "secp256k1-sys",
 ]
 
@@ -6752,6 +6764,22 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "tokio-tungstenite"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tungstenite",
+ "webpki-roots 0.26.6",
+]
+
 [[package]]
 name = "tokio-util"
 version = "0.7.12"
@@ -6979,6 +7007,27 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
+[[package]]
+name = "tungstenite"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http 1.1.0",
+ "httparse",
+ "log",
+ "rand",
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror",
+ "url",
+ "utf-8",
+]
+
 [[package]]
 name = "twox-hash"
 version = "1.6.3"
@@ -7124,6 +7173,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
 [[package]]
 name = "utf8parse"
 version = "0.2.2"
diff --git a/Cargo.toml b/Cargo.toml
index 2e952bc..8091b73 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,12 +41,17 @@ inquire = "^0.7.5"
 serde = { version = "^1.0", features = ["derive"] }
 serde_json = "^1.0.128"
 tokio = { version = "^1.40.0", features = ["macros"] }
+tokio-tungstenite = { version = "0.21.0", features = ["rustls-tls-webpki-roots"] }
 confy = "^0.5.1"
 bs58 = "^0.5.1"
 directories = "^5.0.1"
 comfy-table = "^7.1.1"
 sea-orm = { version = "1.1.0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ] }
 indoc = "2.0.5"
+bech32 = "^0.9.1"
+sha2 = "0.10.8"
+secp256k1 = { version = "0.28.2", features = ["rand", "recovery"] }
+url = "2.5.0"
 
 # crypto
 scrypt = { version = "^0.11", default-features = false } # for old-style key generation
diff --git a/src/commands.rs b/src/commands.rs
index 15f96ba..a1e0dc8 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -8,6 +8,7 @@ pub mod expire;
 pub mod identity;
 pub mod net_test;
 pub mod oneshot;
+pub mod profile;
 pub mod publish;
 pub mod revocation;
 pub mod runtime;
diff --git a/src/commands/profile.rs b/src/commands/profile.rs
new file mode 100644
index 0000000..22b6e7d
--- /dev/null
+++ b/src/commands/profile.rs
@@ -0,0 +1,976 @@
+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 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 ----
+    
+    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(())
+} 
\ No newline at end of file
diff --git a/src/conf.rs b/src/conf.rs
index 509e0c5..8809c39 100644
--- a/src/conf.rs
+++ b/src/conf.rs
@@ -15,6 +15,8 @@ pub struct Config {
 	/// user address
 	/// to perform actions, user must provide secret
 	pub address: Option<AccountId>,
+	/// nostr relay endpoint
+	pub relay: Option<String>,
 }
 
 impl std::default::Default for Config {
@@ -23,6 +25,7 @@ impl std::default::Default for Config {
 			duniter_endpoint: String::from(data::LOCAL_DUNITER_ENDPOINT),
 			indexer_endpoint: String::from(data::LOCAL_INDEXER_ENDPOINT),
 			address: None,
+			relay: Some(String::from("wss://relay.copylaradio.com")),
 		}
 	}
 }
@@ -34,10 +37,16 @@ impl std::fmt::Display for Config {
 		} else {
 			"(no address)".to_string()
 		};
+		let relay = if let Some(relay) = &self.relay {
+			relay.clone()
+		} else {
+			"(no relay)".to_string()
+		};
 		writeln!(f, "Ğcli config")?;
 		writeln!(f, "duniter endpoint {}", self.duniter_endpoint)?;
 		writeln!(f, "indexer endpoint {}", self.indexer_endpoint)?;
-		write!(f, "address {address}")
+		writeln!(f, "address {address}")?;
+		write!(f, "nostr relay {relay}")
 	}
 }
 
diff --git a/src/main.rs b/src/main.rs
index 8e417d6..7368e9a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -155,6 +155,9 @@ pub enum Subcommand {
 	/// Key management (import, generate, list...)
 	#[clap(subcommand)]
 	Vault(commands::vault::Subcommand),
+	/// Nostr profile management (get, set...)
+	#[clap(subcommand)]
+	Profile(commands::profile::Subcommand),
 	/// Cesium
 	#[clap(subcommand, hide = true)]
 	Cesium(commands::cesium::Subcommand),
@@ -233,6 +236,7 @@ async fn main() -> Result<(), GcliError> {
 		Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await,
 		Subcommand::Config(subcommand) => conf::handle_command(data, subcommand).await,
 		Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand).await,
+		Subcommand::Profile(subcommand) => commands::profile::handle_command(data, subcommand).await,
 		Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await,
 		Subcommand::Publish => commands::publish::handle_command().await,
 	};
-- 
GitLab


From daee09e31afe6038b50f55ad744f17086fcd2b30 Mon Sep 17 00:00:00 2001
From: poka <poka@p2p.legal>
Date: Sat, 17 May 2025 03:09:41 +0200
Subject: [PATCH 2/7] fix warn

---
 src/commands/profile.rs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/commands/profile.rs b/src/commands/profile.rs
index 22b6e7d..bab33d5 100644
--- a/src/commands/profile.rs
+++ b/src/commands/profile.rs
@@ -749,7 +749,6 @@ async fn set_profile(
     let nsec = get_nostr_nsec(&keypair)?;
     
     // ---- 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 {
@@ -757,7 +756,7 @@ async fn set_profile(
         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);
+    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),
-- 
GitLab


From 3f0456d7bb95f06f9fbfd335086d47f2f3e855ab Mon Sep 17 00:00:00 2001
From: fred <support@qo-op.com>
Date: Sun, 18 May 2025 20:03:06 +0200
Subject: [PATCH 3/7] NIP-39 compatibility : need some test

---
 src/commands/profile.rs | 149 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 130 insertions(+), 19 deletions(-)

diff --git a/src/commands/profile.rs b/src/commands/profile.rs
index bab33d5..8c0d868 100644
--- a/src/commands/profile.rs
+++ b/src/commands/profile.rs
@@ -114,6 +114,10 @@ pub enum Subcommand {
         /// Profile picture URL
         #[clap(short = 'p', long)]
         picture: Option<String>,
+
+        /// Profile banner URL
+        #[clap(short = 'b', long)]
+        banner: Option<String>,
         
         /// Profile about/description
         #[clap(short, long)]
@@ -126,6 +130,38 @@ pub enum Subcommand {
         /// Profile NIP-05 identifier
         #[clap(short = 'i', long)]
         nip05: Option<String>,
+
+        /// GitHub username
+        #[clap(long)]
+        github: Option<String>,
+
+        /// Twitter username
+        #[clap(long)]
+        twitter: Option<String>,
+
+        /// Mastodon identity (format: instance/@username)
+        #[clap(long)]
+        mastodon: Option<String>,
+
+        /// Telegram user ID
+        #[clap(long)]
+        telegram: Option<String>,
+
+        /// IPFS Gateway URL
+        #[clap(long)]
+        ipfs_gw: Option<String>,
+
+        /// NOSTR Card IPNS vault key
+        #[clap(long)]
+        ipns_vault: Option<String>,
+
+        /// ZenCard wallet address
+        #[clap(long)]
+        zencard: Option<String>,
+
+        /// TW Feed IPNS key
+        #[clap(long)]
+        tw_feed: Option<String>,
         
         /// Relay URL to publish profile to
         #[clap(short, long)]
@@ -139,9 +175,11 @@ pub struct NostrProfile {
     pub name: Option<String>,
     pub display_name: Option<String>,
     pub picture: Option<String>,
+    pub banner: Option<String>,
     pub about: Option<String>,
     pub website: Option<String>,
     pub nip05: Option<String>,
+    pub bot: Option<bool>,
     #[serde(flatten)]
     pub additional_fields: HashMap<String, String>,
 }
@@ -152,9 +190,11 @@ impl Default for NostrProfile {
             name: None,
             display_name: None,
             picture: None,
+            banner: None,
             about: None,
             website: None,
             nip05: None,
+            bot: Some(false),
             additional_fields: HashMap::new(),
         }
     }
@@ -471,9 +511,18 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
             name,
             display_name,
             picture,
+            banner,
             about,
             website,
             nip05,
+            github,
+            twitter,
+            mastodon,
+            telegram,
+            ipfs_gw,
+            ipns_vault,
+            zencard,
+            tw_feed,
             relay,
         } => {
             set_profile(
@@ -481,9 +530,18 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
                 name,
                 display_name,
                 picture,
+                banner,
                 about,
                 website,
                 nip05,
+                github,
+                twitter,
+                mastodon,
+                telegram,
+                ipfs_gw,
+                ipns_vault,
+                zencard,
+                tw_feed,
                 relay,
             )
             .await
@@ -715,28 +773,46 @@ async fn set_profile(
     name: Option<String>,
     display_name: Option<String>,
     picture: Option<String>,
+    banner: Option<String>,
     about: Option<String>,
     website: Option<String>,
     nip05: Option<String>,
+    github: Option<String>,
+    twitter: Option<String>,
+    mastodon: Option<String>,
+    telegram: Option<String>,
+    ipfs_gw: Option<String>,
+    ipns_vault: Option<String>,
+    zencard: Option<String>,
+    tw_feed: 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() {
+    // 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() {
          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!("  -b, --banner <BANNER>             Set profile banner 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!("  -i, --nip05 <NIP05>              Set profile NIP-05 identifier");
+         println!("      --github <USERNAME>           Set GitHub username");
+         println!("      --twitter <USERNAME>          Set Twitter username");
+         println!("      --mastodon <IDENTITY>         Set Mastodon identity (instance/@username)");
+         println!("      --telegram <USER_ID>          Set Telegram user ID");
+         println!("      --ipfs-gw <URL>               Set IPFS Gateway URL");
+         println!("      --ipns-vault <KEY>            Set NOSTR Card IPNS vault key");
+         println!("      --zencard <ADDRESS>           Set ZenCard wallet address");
+         println!("      --tw-feed <KEY>               Set TW Feed IPNS key");
          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)
@@ -747,51 +823,86 @@ async fn set_profile(
     
     // Get Nostr private key in bech32 format (for display)
     let nsec = get_nostr_nsec(&keypair)?;
-    
-    // ---- START: Calculate g1pubv2 and g1pub for tags ----
+
+    // Calculate g1pubv2 (gdev SS58) and g1pub (Duniter v1)
     let mut g1pub_for_tag: Option<String> = None;
 
+    // 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 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);
+    let g1pubv2_for_tag: Option<String> = Some(account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(42)));
 
+    // Calculate g1pub (Duniter v1 pubkey)
     match cesium::compute_g1v1_public_key(&keypair) {
         Ok(pubkey_g1) => g1pub_for_tag = Some(pubkey_g1),
-        Err(_e) => {
+        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.");
+                log::warn!("Failed to compute g1pub (Duniter v1 pubkey) for tag: {}", e);
             }
         }
     }
-    // ---- END: Calculate g1pubv2 and g1pub for tags ----
-    
-    let mut profile_content_obj = NostrProfile::default(); // This is for the 'content' field
+
+    // Create profile content
+    let mut profile_content_obj = NostrProfile::default();
     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) = banner { profile_content_obj.banner = 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); }
-    
+    profile_content_obj.bot = Some(false);
+
     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
+    event.tags = Vec::new();
+
+    // Add NIP-39 external identity tags
+    if let Some(gh) = &github {
+        event.tags.push(vec!["i".to_string(), format!("github:{}", gh), "".to_string()]);
+    }
+    if let Some(tw) = &twitter {
+        event.tags.push(vec!["i".to_string(), format!("twitter:{}", tw), "".to_string()]);
+    }
+    if let Some(mstd) = &mastodon {
+        event.tags.push(vec!["i".to_string(), format!("mastodon:{}", mstd), "".to_string()]);
+    }
+    if let Some(tg) = &telegram {
+        event.tags.push(vec!["i".to_string(), format!("telegram:{}", tg), "".to_string()]);
+    }
 
-    // Add g1pubv2 and g1pub as tags
+    // Add g1pubv2 and g1pub tags
     if let Some(g1_ss58) = &g1pubv2_for_tag {
         event.tags.push(vec!["g1pubv2".to_string(), g1_ss58.clone()]);
+        // Also add as an external identity for compatibility
+        event.tags.push(vec!["i".to_string(), format!("g1pubv2:{}", g1_ss58), "".to_string()]);
     }
     if let Some(g1_key) = &g1pub_for_tag {
         event.tags.push(vec!["g1pub".to_string(), g1_key.clone()]);
+        // Also add as an external identity for compatibility
+        event.tags.push(vec!["i".to_string(), format!("g1pub:{}", g1_key), "".to_string()]);
     }
-    
+
+    // Add additional custom tags
+    if let Some(gw) = &ipfs_gw {
+        event.tags.push(vec!["i".to_string(), format!("ipfs_gw:{}", gw), "".to_string()]);
+    }
+    if let Some(vault) = &ipns_vault {
+        event.tags.push(vec!["i".to_string(), format!("ipns_vault:{}", vault), "".to_string()]);
+    }
+    if let Some(card) = &zencard {
+        event.tags.push(vec!["i".to_string(), format!("zencard:{}", card), "".to_string()]);
+    }
+    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
-- 
GitLab


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 4/7] 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


From be08bcc3fd47194ea3a966eee52734175e064c83 Mon Sep 17 00:00:00 2001
From: fred <support@qo-op.com>
Date: Sat, 31 May 2025 13:24:03 +0200
Subject: [PATCH 5/7] adding -o json --no-password behaviour

---
 examples/profile.examples.md  | 193 ++++++++++++++++++++++
 examples/vault.examples.md    | 137 ++++++++++++++++
 src/commands/profile.rs       | 299 ++++++++++++++++++++++++++++------
 src/commands/vault.rs         | 230 +++++++++++++++++++-------
 src/entities/vault_account.rs |  14 +-
 src/main.rs                   |   3 +
 6 files changed, 762 insertions(+), 114 deletions(-)
 create mode 100644 examples/profile.examples.md
 create mode 100644 examples/vault.examples.md

diff --git a/examples/profile.examples.md b/examples/profile.examples.md
new file mode 100644
index 0000000..ec2a23c
--- /dev/null
+++ b/examples/profile.examples.md
@@ -0,0 +1,193 @@
+# Exemples d'utilisation de la commande `gcli profile`
+
+## Notes importantes
+- L'option `-o json` doit toujours être placée au début de la commande, juste après `gcli`
+- L'option `--no-password` doit être placée avant la sous-commande `profile`
+- Le relais par défaut est `wss://relay.copylaradio.com`
+- Les clés sont stockées dans un coffre-fort local
+
+## Exemple complet : Gestion du profil de TOTO
+
+### 1. Import de la clé G1v1
+```bash
+# Import de la clé G1v1 de TOTO
+gcli -o json --no-password vault import -S g1v1 --g1v1-id "toto" --g1v1-password "toto" --no-password -n TOTO
+```
+
+### 2. Configuration du profil
+```bash
+# Définition du profil avec informations personnelles
+gcli -o json --no-password profile set \
+  --name "TOTO" \
+  --display-name "TOTO le Super" \
+  --about "Développeur passionné de cryptographie" \
+  --picture "https://example.com/toto.jpg" \
+  --website "https://toto.dev" \
+  --github "toto-dev" \
+  --twitter "toto_crypto" \
+  --mastodon "mastodon.social/@toto" \
+  --ipfs-gw "https://ipfs.io" \
+  --ipns-vault "/ipns/QmNostrVaultKey" \
+  --zencard "zencard_address" \
+  --tw-feed "tw_feed_key"
+```
+
+### 3. Vérification du profil
+```bash
+# Vérification que le profil a bien été publié
+gcli -o json --no-password profile get
+```
+
+### 4. Export des clés jumelles
+```bash
+# Export des clés IPFS
+gcli -o json --no-password profile export --format ipfs
+
+# Export des clés Nostr
+gcli -o json --no-password profile export --format nostr
+
+# Export des clés Bitcoin
+gcli -o json --no-password profile export --format bitcoin
+
+# Export des clés Monero
+gcli -o json --no-password profile export --format monero
+
+# Export des clés Duniter v1
+gcli -o json --no-password profile export --format g1v1
+
+# Export des clés Duniter v2
+gcli -o json --no-password profile export --format g1v2
+```
+
+### 5. Suppression du profil (si nécessaire)
+```bash
+# Suppression du profil
+gcli -o json --no-password profile remove
+```
+
+### Champs supplémentaires créés par make_NOSTRCARD.sh
+
+Le script `make_NOSTRCARD.sh` ajoute automatiquement plusieurs champs spéciaux au profil Nostr :
+
+1. **Clés jumelles** (twin_keys) :
+   - `ipns` : Clé IPNS pour le stockage personnel
+   - `bitcoin` : Adresse Bitcoin jumelle
+   - `monero` : Adresse Monero jumelle
+
+2. **Tags NIP-39** pour les identités externes :
+   - `g1pubv2` : Clé publique Duniter v2 (format SS58)
+   - `g1pub` : Clé publique Duniter v1
+   - `ipfs_gw` : URL de la passerelle IPFS
+   - `ipns_vault` : Clé IPNS du coffre-fort
+   - `zencard` : Adresse ZenCard
+   - `tw_feed` : Clé IPNS du flux
+
+3. **Métadonnées de localisation** :
+   - `ZUMAP` : Coordonnées géographiques (latitude/longitude)
+   - `GPS` : Données GPS formatées
+
+4. **Informations de sécurité** :
+   - `TODATE` : Horodatage de création
+   - `LANG` : Langue préférée
+   - `HEX` : Clé publique en format hexadécimal
+   - `NPUB` : Clé publique en format bech32
+
+5. **QR Codes générés** :
+   - `IPNS.QR.png` : QR code pour l'accès au coffre-fort IPNS
+   - `G1PUBNOSTR.QR.png` : QR code de l'identité G1
+   - `scan_${MOATS}.png` : Photo de profil scannée
+
+Ces champs sont automatiquement ajoutés lors de la création du profil via `make_NOSTRCARD.sh` et sont visibles dans la sortie JSON de la commande `profile get`.
+
+## 1. Récupérer un profil (`get`)
+
+```bash
+# Récupérer le profil depuis le relais par défaut
+gcli profile get
+
+# Récupérer le profil depuis un relais spécifique
+gcli profile get --relay wss://relay.example.com
+
+# Récupérer le profil au format JSON
+gcli -o json profile get
+
+# Récupérer le profil sans mot de passe
+gcli --no-password profile get
+```
+
+## 2. Définir un profil (`set`)
+
+```bash
+# Définir le nom et la description
+gcli profile set --name "Alice" --about "Nostr user"
+
+# Définir l'image de profil et la bannière
+gcli profile set --picture "https://example.com/avatar.jpg" --banner "https://example.com/banner.jpg"
+
+# Définir les identités sociales
+gcli profile set --github "alice" --twitter "alice" --mastodon "mastodon.social/@alice"
+
+# Définir les clés jumelles
+gcli profile set --ipfs-gw "https://ipfs.io" --ipns-vault "key123" --zencard "address123"
+
+# Publier sur un relais spécifique
+gcli profile set --name "Alice" --relay wss://relay.example.com
+
+# Publier sans mot de passe
+gcli --no-password profile set --name "Alice"
+
+# Publier au format JSON
+gcli -o json profile set --name "Alice"
+```
+
+## 3. Supprimer un profil (`remove`)
+
+```bash
+# Supprimer le profil du relais par défaut
+gcli profile remove
+
+# Supprimer le profil de plusieurs relais
+gcli profile remove --relays "wss://relay1.com,wss://relay2.com"
+
+# Supprimer sans mot de passe
+gcli --no-password profile remove
+
+# Supprimer au format JSON
+gcli -o json profile remove
+```
+
+## 4. Exporter les clés (`export`)
+
+```bash
+# Exporter les clés Nostr
+gcli profile export --format nostr
+
+# Exporter les clés IPFS
+gcli profile export --format ipfs
+
+# Exporter les clés Bitcoin
+gcli profile export --format bitcoin
+
+# Exporter les clés Monero
+gcli profile export --format monero
+
+# Exporter les clés Duniter v1
+gcli profile export --format g1v1
+
+# Exporter les clés Duniter v2
+gcli profile export --format g1v2
+
+# Exporter sans mot de passe
+gcli --no-password profile export --format nostr
+
+# Exporter au format JSON
+gcli -o json profile export --format nostr
+
+# Exporter sans mot de passe au format JSON
+gcli -o json --no-password profile export --format nostr
+```
+
+## Notes supplémentaires
+- Pour la commande `set`, si aucune option n'est fournie, l'aide s'affiche
+- Pour la commande `export`, si le format n'est pas reconnu, la liste des formats disponibles s'affiche
+- Les formats d'export disponibles sont : `ipfs`, `nostr`, `bitcoin`, `monero`, `g1v1`, `g1v2` 
\ No newline at end of file
diff --git a/examples/vault.examples.md b/examples/vault.examples.md
new file mode 100644
index 0000000..8233632
--- /dev/null
+++ b/examples/vault.examples.md
@@ -0,0 +1,137 @@
+# Exemples d'utilisation de la commande `vault`
+
+La commande `vault` permet de gérer les clés et les comptes dans le coffre-fort. Voici les différentes sous-commandes disponibles :
+
+## Lister les comptes
+
+### Lister tous les comptes
+```bash
+# Format humain (par défaut)
+gcli vault list all
+
+# Afficher les clés publiques G1v1 pour les clés ed25519
+gcli vault list all --show-g1v1
+```
+
+### Lister uniquement les comptes de base
+```bash
+# Format humain (par défaut)
+gcli vault list base
+
+# Afficher les clés publiques G1v1 pour les clés ed25519
+gcli vault list base --show-g1v1
+```
+
+### Lister les comptes liés à une adresse spécifique
+```bash
+# Par adresse SS58
+gcli vault list for -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+
+# Par nom de compte
+gcli vault list for -v MonCompte
+
+# Afficher les clés publiques G1v1
+gcli vault list for -v MonCompte --show-g1v1
+```
+
+## Importer une clé
+
+### Import en mode interactif
+```bash
+# Format Substrate (par défaut)
+gcli vault import
+
+# Format Seed
+gcli vault import -S seed
+
+# Format G1v1
+gcli vault import -S g1v1
+```
+
+### Import en mode non-interactif
+```bash
+# Format Substrate avec URI
+gcli vault import -u "bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"
+
+# Format G1v1 avec ID et mot de passe
+gcli vault import -S g1v1 --g1v1-id "mon_id" --g1v1-password "mon_mot_de_passe"
+
+# Sans mot de passe
+gcli vault import -u "bottom drive obey lake curtain smoke basket hold race lonely fit walk" --no-password
+
+# Avec un nom personnalisé
+gcli vault import -u "bottom drive obey lake curtain smoke basket hold race lonely fit walk" -n "MonCompte"
+```
+
+## Dériver un compte
+
+### Dérivation en mode interactif
+```bash
+# Par adresse SS58
+gcli vault derive -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+
+# Par nom de compte
+gcli vault derive -v MonCompte
+```
+
+### Dérivation en mode non-interactif
+```bash
+# Par adresse SS58 avec chemin de dérivation
+gcli vault derive -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV -d "//0"
+
+# Par nom de compte avec chemin de dérivation
+gcli vault derive -v MonCompte -d "//0"
+
+# Sans mot de passe
+gcli vault derive -v MonCompte -d "//0" --no-password
+
+# Avec un nom personnalisé
+gcli vault derive -v MonCompte -d "//0" -n "MonCompteDerive"
+```
+
+## Renommer un compte
+```bash
+gcli vault rename 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+```
+
+## Supprimer un compte
+```bash
+# Par adresse SS58
+gcli vault remove -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+
+# Par nom de compte
+gcli vault remove -v MonCompte
+```
+
+## Inspecter un compte
+```bash
+# Par adresse SS58
+gcli vault inspect -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+
+# Par nom de compte
+gcli vault inspect -v MonCompte
+```
+
+## Utiliser un compte
+```bash
+# Par adresse SS58
+gcli vault use -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+
+# Par nom de compte
+gcli vault use -v MonCompte
+```
+
+## Générer une mnémonique
+```bash
+gcli vault generate
+```
+
+## Voir l'emplacement du coffre-fort
+```bash
+gcli vault where
+```
+
+## Migrer les anciens fichiers de clés
+```bash
+gcli vault migrate
+``` 
\ No newline at end of file
diff --git a/src/commands/profile.rs b/src/commands/profile.rs
index 83f6145..3797013 100644
--- a/src/commands/profile.rs
+++ b/src/commands/profile.rs
@@ -15,8 +15,7 @@ use bech32::{self, ToBase32, Variant};
 use sp_core::crypto::Ss58Codec;
 use crate::commands::cesium;
 use bs58;
-use base64;
-use bitcoin::{PrivateKey, Network, Address};
+use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
 
 /// Derive a Nostr key from a Substrate key
 fn derive_nostr_keys_from_substrate(keypair: &KeyPair) -> Result<(SecretKey, PublicKey), GcliError> {
@@ -591,7 +590,21 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 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?;
+    let keypair = if data.args.no_password {
+        // If no_password is set, try to get the keypair without prompting
+        match fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                if data.args.output_format == OutputFormat::Json {
+                    return Err(anyhow!("Failed to get keypair without password: {}", e).into());
+                } else {
+                    return Err(anyhow!("Failed to get keypair without password. Try removing --no-password option.").into());
+                }
+            }
+        }
+    } else {
+        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)?;
@@ -599,8 +612,10 @@ 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 Nostr pubkey (hex): {}", pubkey);
-    println!("Nostr pubkey (bech32): {}", npub);
+    if data.args.output_format == OutputFormat::Human {
+        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(|| {
@@ -621,11 +636,15 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr
     let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?;
     
     // Connect to the WebSocket
-    println!("Connecting to relay {}...", relay);
+    if data.args.output_format == OutputFormat::Human {
+        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...");
+    if data.args.output_format == OutputFormat::Human {
+        println!("Connected to relay, requesting profile...");
+    }
     
     // Create subscription request - CORRECTED FORMAT
     // According to NIP-01, REQ should be an array: ["REQ", <subscription_id>, <filter>, ...]
@@ -638,7 +657,9 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr
     // Send the request in the correct format
     let req_msg = json!(["REQ", "profile-request", filter]);
     
-    println!("Sending request: {}", req_msg.to_string());
+    if data.args.output_format == OutputFormat::Human {
+        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))?;
     
@@ -768,15 +789,50 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr
                 }
             }
             OutputFormat::Json => {
+                // Create a clean JSON structure
                 let output = json!({
+                    "vault": {
+                        "address": data.cfg.address.clone(),
+                        "g1v1_pub_key": match &keypair {
+                            KeyPair::Ed25519(_pair) => {
+                                match cesium::compute_g1v1_public_key(&keypair) {
+                                    Ok(pubkey) => Some(pubkey),
+                                    Err(_) => None
+                                }
+                            },
+                            _ => None
+                        },
+                        "crypto_scheme": match &keypair {
+                            KeyPair::Sr25519(_) => Some("Sr25519"),
+                            KeyPair::Ed25519(_) => Some("Ed25519"),
+                        }
+                    },
                     "pubkey_hex": pubkey,
                     "pubkey_bech32": npub,
-                    "profile_content": profile_content, // The content part
+                    "profile_content": {
+                        "name": profile_content.name,
+                        "display_name": profile_content.display_name,
+                        "picture": profile_content.picture,
+                        "banner": profile_content.banner,
+                        "about": profile_content.about,
+                        "website": profile_content.website,
+                        "nip05": profile_content.nip05,
+                        "bot": profile_content.bot,
+                        "twin_keys": profile_content.twin_keys
+                    },
                     "tags_custom": {
                         "g1pubv2": g1pubv2_from_tags,
                         "g1pub": g1pub_from_tags
                     },
-                    "full_event": event_received // Include the full event if available
+                    "full_event": {
+                        "id": event_received.as_ref().map(|e| &e.id),
+                        "pubkey": event_received.as_ref().map(|e| &e.pubkey),
+                        "created_at": event_received.as_ref().map(|e| e.created_at),
+                        "kind": event_received.as_ref().map(|e| e.kind),
+                        "tags": event_received.as_ref().map(|e| &e.tags),
+                        "content": event_received.as_ref().map(|e| &e.content),
+                        "sig": event_received.as_ref().map(|e| &e.sig)
+                    }
                 });
                 println!("{}", serde_json::to_string_pretty(&output).unwrap());
             }
@@ -788,14 +844,40 @@ async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliEr
             }
             OutputFormat::Json => {
                 let output = json!({
+                    "vault": {
+                        "address": data.cfg.address.clone(),
+                        "g1v1_pub_key": match &keypair {
+                            KeyPair::Ed25519(_pair) => {
+                                match cesium::compute_g1v1_public_key(&keypair) {
+                                    Ok(pubkey) => Some(pubkey),
+                                    Err(_) => None
+                                }
+                            },
+                            _ => None
+                        },
+                        "crypto_scheme": match &keypair {
+                            KeyPair::Sr25519(_) => Some("Sr25519"),
+                            KeyPair::Ed25519(_) => Some("Ed25519"),
+                        }
+                    },
                     "pubkey_hex": pubkey,
                     "pubkey_bech32": npub,
-                    "profile_content": NostrProfile::default(), // Default empty profile content
+                    "profile_content": {
+                        "name": null,
+                        "display_name": null,
+                        "picture": null,
+                        "banner": null,
+                        "about": null,
+                        "website": null,
+                        "nip05": null,
+                        "bot": null,
+                        "twin_keys": null
+                    },
                     "tags_custom": {
-                        "g1pubv2": Option::<String>::None,
-                        "g1pub": Option::<String>::None
+                        "g1pubv2": null,
+                        "g1pub": null
                     },
-                    "full_event": Option::<NostrEvent>::None
+                    "full_event": null
                 });
                 println!("{}", serde_json::to_string_pretty(&output).unwrap());
             }
@@ -825,33 +907,71 @@ 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() {
-         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!("  -b, --banner <BANNER>             Set profile banner 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!("      --github <USERNAME>           Set GitHub username");
-         println!("      --twitter <USERNAME>          Set Twitter username");
-         println!("      --mastodon <IDENTITY>         Set Mastodon identity (instance/@username)");
-         println!("      --telegram <USER_ID>          Set Telegram user ID");
-         println!("      --ipfs-gw <URL>               Set IPFS Gateway URL");
-         println!("      --ipns-vault <KEY>            Set NOSTR Card IPNS vault key");
-         println!("      --zencard <ADDRESS>           Set ZenCard wallet address");
-         println!("      --tw-feed <KEY>               Set TW Feed IPNS key");
-         println!("  -r, --relay <RELAY>               Specify relay URL to publish to");
-         println!("\nExample: gcli profile set --name \"Alice\" --about \"Nostr user\"");
-         return Ok(());
+        if data.args.output_format == OutputFormat::Human {
+            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!("  -b, --banner <BANNER>             Set profile banner 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!("      --github <USERNAME>           Set GitHub username");
+            println!("      --twitter <USERNAME>          Set Twitter username");
+            println!("      --mastodon <IDENTITY>         Set Mastodon identity (instance/@username)");
+            println!("      --telegram <USER_ID>          Set Telegram user ID");
+            println!("      --ipfs-gw <URL>               Set IPFS Gateway URL");
+            println!("      --ipns-vault <KEY>            Set NOSTR Card IPNS vault key");
+            println!("      --zencard <ADDRESS>           Set ZenCard wallet address");
+            println!("      --tw-feed <KEY>               Set TW Feed IPNS key");
+            println!("  -r, --relay <RELAY>               Specify relay URL to publish to");
+            println!("\nExample: gcli profile set --name \"Alice\" --about \"Nostr user\"");
+        } else {
+            println!("{}", serde_json::to_string_pretty(&json!({
+                "error": "No profile options specified",
+                "available_options": {
+                    "name": "Set profile name",
+                    "display_name": "Set profile display name",
+                    "picture": "Set profile picture URL",
+                    "banner": "Set profile banner URL",
+                    "about": "Set profile about/description",
+                    "website": "Set profile website URL",
+                    "nip05": "Set profile NIP-05 identifier",
+                    "github": "Set GitHub username",
+                    "twitter": "Set Twitter username",
+                    "mastodon": "Set Mastodon identity (instance/@username)",
+                    "telegram": "Set Telegram user ID",
+                    "ipfs_gw": "Set IPFS Gateway URL",
+                    "ipns_vault": "Set NOSTR Card IPNS vault key",
+                    "zencard": "Set ZenCard wallet address",
+                    "tw_feed": "Set TW Feed IPNS key",
+                    "relay": "Specify relay URL to publish to"
+                }
+            })).unwrap());
+        }
+        return Ok(());
     }
 
     // Get keypair for signing
-    let keypair = fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?;
+    let keypair = if data.args.no_password {
+        // If no_password is set, try to get the keypair without prompting
+        match fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                if data.args.output_format == OutputFormat::Json {
+                    return Err(anyhow!("Failed to get keypair without password: {}", e).into());
+                } else {
+                    return Err(anyhow!("Failed to get keypair without password. Try removing --no-password option.").into());
+                }
+            }
+        }
+    } else {
+        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)?;
@@ -877,9 +997,9 @@ async fn set_profile(
         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.");
+                log::info!("Cannot compute g1pub (Duniter v1 pubkey) as the key is not Ed25519.");
             } else {
-                log::warn!("Failed to compute g1pub (Duniter v1 pubkey) for tag: {}", e);
+                log::warn!("Failed to compute g1pub (Duniter v1 pubkey): {}", e);
             }
         }
     }
@@ -1041,11 +1161,15 @@ async fn set_profile(
     let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?;
     
     // Connect to the WebSocket
-    println!("Connecting to relay {}...", relay);
+    if data.args.output_format == OutputFormat::Human {
+        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...");
+    if data.args.output_format == OutputFormat::Human {
+        println!("Connected to relay, publishing profile...");
+    }
     
     // Create event message - CORRECTED FORMAT
     // According to NIP-01, EVENT should be an array: ["EVENT", <event_object>]
@@ -1176,6 +1300,22 @@ async fn set_profile(
         }
         OutputFormat::Json => {
             println!("{}", serde_json::to_string_pretty(&json!({
+                "vault": {
+                    "address": data.cfg.address.clone(),
+                    "g1v1_pub_key": match &keypair {
+                        KeyPair::Ed25519(_pair) => {
+                            match cesium::compute_g1v1_public_key(&keypair) {
+                                Ok(pubkey) => Some(pubkey),
+                                Err(_) => None
+                            }
+                        },
+                        _ => None
+                    },
+                    "crypto_scheme": match &keypair {
+                        KeyPair::Sr25519(_) => Some("Sr25519"),
+                        KeyPair::Ed25519(_) => Some("Ed25519"),
+                    }
+                },
                 "pubkey_hex": pubkey,
                 "pubkey_bech32": npub,
                 "seckey_bech32": nsec,
@@ -1203,7 +1343,21 @@ async fn set_profile(
 /// 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?;
+    let keypair = if data.args.no_password {
+        // If no_password is set, try to get the keypair without prompting
+        match fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                if data.args.output_format == OutputFormat::Json {
+                    return Err(anyhow!("Failed to get keypair without password: {}", e).into());
+                } else {
+                    return Err(anyhow!("Failed to get keypair without password. Try removing --no-password option.").into());
+                }
+            }
+        }
+    } else {
+        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)?;
@@ -1219,11 +1373,17 @@ async fn remove_profile(data: Data, relay_urls: Option<String>) -> Result<(), Gc
             .clone()
             .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string())]);
 
-    println!("Removing profile with Nostr pubkey (hex): {}", pubkey);
-    println!("Nostr pubkey (bech32): {}", npub);
+    if data.args.output_format == OutputFormat::Human {
+        println!("Removing profile with Nostr pubkey (hex): {}", pubkey);
+        println!("Nostr pubkey (bech32): {}", npub);
+    }
+    
+    let mut results = Vec::new();
     
     for relay in &relays {
-        println!("Sending deletion requests to {}", relay);
+        if data.args.output_format == OutputFormat::Human {
+            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://") {
@@ -1264,6 +1424,8 @@ async fn remove_profile(data: Data, relay_urls: Option<String>) -> Result<(), Gc
         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();
+        let mut success = false;
+        let mut response_message = String::new();
         
         while start_time.elapsed() < timeout {
             interval.tick().await;
@@ -1274,10 +1436,14 @@ async fn remove_profile(data: Data, relay_urls: Option<String>) -> Result<(), Gc
             ).await {
                 Ok(Some(Ok(msg))) => {
                     if let Message::Text(text) = msg {
+                        response_message = text.clone();
                         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");
+                                    success = true;
+                                    if data.args.output_format == OutputFormat::Human {
+                                        println!("Deletion request confirmed by relay");
+                                    }
                                     break;
                                 }
                             }
@@ -1285,29 +1451,62 @@ async fn remove_profile(data: Data, relay_urls: Option<String>) -> Result<(), Gc
                     }
                 },
                 Ok(Some(Err(e))) => {
-                    println!("Error receiving confirmation: {}", e);
+                    if data.args.output_format == OutputFormat::Human {
+                        println!("Error receiving confirmation: {}", e);
+                    }
                     break;
                 },
                 Ok(None) => {
-                    println!("Connection closed by relay");
+                    if data.args.output_format == OutputFormat::Human {
+                        println!("Connection closed by relay");
+                    }
                     break;
                 },
                 Err(_) => continue,
             }
         }
         
+        results.push(json!({
+            "relay": relay,
+            "success": success,
+            "response": response_message
+        }));
+        
         // Close the WebSocket connection
         ws_stream.close(None).await.ok();
     }
     
-    println!("Deletion requests sent to all specified relays");
+    if data.args.output_format == OutputFormat::Human {
+        println!("Deletion requests sent to all specified relays");
+    } else {
+        println!("{}", serde_json::to_string_pretty(&json!({
+            "pubkey_hex": pubkey,
+            "pubkey_bech32": npub,
+            "results": results
+        })).unwrap());
+    }
+    
     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?;
+    let keypair = if data.args.no_password {
+        // If no_password is set, try to get the keypair without prompting
+        match fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await {
+            Ok(pair) => pair,
+            Err(e) => {
+                if data.args.output_format == OutputFormat::Json {
+                    return Err(anyhow!("Failed to get keypair without password: {}", e).into());
+                } else {
+                    return Err(anyhow!("Failed to get keypair without password. Try removing --no-password option.").into());
+                }
+            }
+        }
+    } else {
+        fetch_or_get_keypair(&data, data.cfg.address.clone(), Some(CryptoScheme::Ed25519)).await?
+    };
 
     match format.to_lowercase().as_str() {
         "g1v1" => {
@@ -1426,7 +1625,7 @@ async fn export_keys(data: Data, format: String) -> Result<(), GcliError> {
             }
 
             let peer_id = format!("12D3Koo{}", bs58::encode(&peer_id_bytes).into_string());
-            let private_key = base64::encode(&priv_protobuf);
+            let private_key = BASE64.encode(&priv_protobuf);
 
             match data.args.output_format {
                 OutputFormat::Human => {
@@ -1488,7 +1687,7 @@ async fn export_keys(data: Data, format: String) -> Result<(), GcliError> {
             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);
+            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);
diff --git a/src/commands/vault.rs b/src/commands/vault.rs
index b46606e..57b352e 100644
--- a/src/commands/vault.rs
+++ b/src/commands/vault.rs
@@ -15,6 +15,8 @@ use std::cell::RefCell;
 use std::io::{Read, Write};
 use std::path::PathBuf;
 use std::rc::Rc;
+use serde::Serialize;
+use std::fmt;
 
 /// vault subcommands
 #[derive(Clone, Debug, clap::Parser)]
@@ -221,25 +223,73 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 				let all_account_tree_node_hierarchies =
 					vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?;
 
-				let table = display::compute_vault_accounts_table_with_g1v1(
-					&all_account_tree_node_hierarchies,
-					show_g1v1,
-				)?;
-
-				println!("available SS58 Addresses:");
-				println!("{table}");
+				match data.args.output_format {
+					OutputFormat::Json => {
+						let accounts: Vec<_> = all_account_tree_node_hierarchies
+							.iter()
+							.map(|node| {
+								let node = node.borrow();
+								VaultAccountJson {
+									address: node.account.address.to_string(),
+									crypto_scheme: node.account.crypto_scheme.map(|s| s.to_string()),
+									path: node.account.path.clone(),
+									name: node.account.name.clone(),
+									g1v1_public_key: if show_g1v1 && node.account.crypto_scheme == Some(CryptoScheme::Ed25519.into()) {
+										Some(cesium::compute_g1v1_public_key_from_ed25519_account_id(&node.account.address.0))
+									} else {
+										None
+									}
+								}
+							})
+							.collect();
+						println!("{}", serde_json::to_string_pretty(&accounts)?);
+					}
+					OutputFormat::Human => {
+						let table = display::compute_vault_accounts_table_with_g1v1(
+							&all_account_tree_node_hierarchies,
+							show_g1v1,
+						)?;
+
+						println!("available SS58 Addresses:");
+						println!("{table}");
+					}
+				}
 			}
 			ListChoice::Base { show_g1v1 } => {
 				let base_account_tree_nodes =
 					vault_account::fetch_only_base_account_tree_nodes(db).await?;
 
-				let table = display::compute_vault_accounts_table_with_g1v1(
-					&base_account_tree_nodes,
-					show_g1v1,
-				)?;
-
-				println!("available <Base> SS58 Addresses:");
-				println!("{table}");
+				match data.args.output_format {
+					OutputFormat::Json => {
+						let accounts: Vec<_> = base_account_tree_nodes
+							.iter()
+							.map(|node| {
+								let node = node.borrow();
+								VaultAccountJson {
+									address: node.account.address.to_string(),
+									crypto_scheme: node.account.crypto_scheme.map(|s| s.to_string()),
+									path: node.account.path.clone(),
+									name: node.account.name.clone(),
+									g1v1_public_key: if show_g1v1 && node.account.crypto_scheme == Some(CryptoScheme::Ed25519.into()) {
+										Some(cesium::compute_g1v1_public_key_from_ed25519_account_id(&node.account.address.0))
+									} else {
+										None
+									}
+								}
+							})
+							.collect();
+						println!("{}", serde_json::to_string_pretty(&accounts)?);
+					}
+					OutputFormat::Human => {
+						let table = display::compute_vault_accounts_table_with_g1v1(
+							&base_account_tree_nodes,
+							show_g1v1,
+						)?;
+
+						println!("available <Base> SS58 Addresses:");
+						println!("{table}");
+					}
+				}
 			}
 			ListChoice::For {
 				address_or_vault_name,
@@ -251,16 +301,40 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 				let base_account_tree_node =
 					vault_account::get_base_account_tree_node(&account_tree_node);
 
-				let table = display::compute_vault_accounts_table_with_g1v1(
-					&[base_account_tree_node],
-					show_g1v1,
-				)?;
-
-				println!(
-					"available SS58 Addresses linked to {}:",
-					account_tree_node.borrow().account
-				);
-				println!("{table}");
+				match data.args.output_format {
+					OutputFormat::Json => {
+						let accounts: Vec<_> = vec![base_account_tree_node]
+							.iter()
+							.map(|node| {
+								let node = node.borrow();
+								VaultAccountJson {
+									address: node.account.address.to_string(),
+									crypto_scheme: node.account.crypto_scheme.map(|s| s.to_string()),
+									path: node.account.path.clone(),
+									name: node.account.name.clone(),
+									g1v1_public_key: if show_g1v1 && node.account.crypto_scheme == Some(CryptoScheme::Ed25519.into()) {
+										Some(cesium::compute_g1v1_public_key_from_ed25519_account_id(&node.account.address.0))
+									} else {
+										None
+									}
+								}
+							})
+							.collect();
+						println!("{}", serde_json::to_string_pretty(&accounts)?);
+					}
+					OutputFormat::Human => {
+						let table = display::compute_vault_accounts_table_with_g1v1(
+							&[base_account_tree_node],
+							show_g1v1,
+						)?;
+
+						println!(
+							"available SS58 Addresses linked to {}:",
+							account_tree_node.borrow().account
+						);
+						println!("{table}");
+					}
+				}
 			}
 		},
 		Subcommand::ListFiles => {
@@ -376,6 +450,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 				provided_password.as_ref(),
 				Some(crypto_scheme),
 				name,
+				&data,
 			)
 			.await?;
 
@@ -665,6 +740,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 					Some(&vault_data_from_file.password),
 					Some(CryptoScheme::Sr25519),
 					None,
+					&data,
 				)
 				.await;
 
@@ -947,6 +1023,7 @@ pub async fn create_base_account_for_vault_data_to_import<C>(
 	password_opt: Option<&String>,
 	crypto_scheme: Option<CryptoScheme>,
 	name_opt: Option<String>,
+	data: &Data,
 ) -> Result<vault_account::Model, GcliError>
 where
 	C: ConnectionTrait,
@@ -959,50 +1036,57 @@ where
 		vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await?
 	{
 		// Existing account
-		if existing_vault_account.is_base_account() {
-			println!("You are trying to add {address_to_import} as a <Base> account while it already exists as a <Base> account.");
-			println!();
-			println!("Do you want to:");
-			println!("1. keep the existing <Base> account and cancel import");
-			println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)");
+		let result = if data.args.no_password {
+			println!("Account already exists. Overwriting due to --no-password flag.");
+			"2".to_string()
 		} else {
-			// Existing derivation account
-			let account_tree_node_hierarchy =
-				vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
-					db_tx,
+			if existing_vault_account.is_base_account() {
+				println!("You are trying to add {} as a <Base> account while it already exists as a <Base> account.", address_to_import);
+				println!();
+				println!("Do you want to:");
+				println!("1. keep the existing <Base> account and cancel import");
+				println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)");
+			} else {
+				// Existing derivation account
+				let account_tree_node_hierarchy =
+					vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
+						db_tx,
+						&address_to_import,
+					)
+					.await?;
+				let account_tree_node_for_address = vault_account::get_account_tree_node_for_address(
+					&account_tree_node_hierarchy,
 					&address_to_import,
-				)
-				.await?;
-			let account_tree_node_for_address = vault_account::get_account_tree_node_for_address(
-				&account_tree_node_hierarchy,
-				&address_to_import,
-			);
-
-			let base_parent_hierarchy_account_tree_node =
-				vault_account::get_base_parent_hierarchy_account_tree_node(
-					&account_tree_node_for_address,
 				);
 
-			let parent_hierarchy_table =
-				display::compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node])?;
+				let base_parent_hierarchy_account_tree_node =
+					vault_account::get_base_parent_hierarchy_account_tree_node(
+						&account_tree_node_for_address,
+					);
 
-			println!("You are trying to add {address_to_import} as a <Base> account");
-			println!(
-				"but it is already present as `{}` derivation of {} account.",
-				existing_vault_account.path.clone().unwrap(),
-				existing_vault_account.parent.clone().unwrap()
-			);
-			println!();
-			println!("Its parent hierarchy is this:");
-			println!("{parent_hierarchy_table}");
-			println!();
-			println!("Do you want to:");
-			println!("1. keep the existing derivation and cancel import");
-			println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)");
-		}
+				let parent_hierarchy_table =
+					display::compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node])?;
 
-		let result = inputs::select_action("Your choice?", vec!["1", "2"])?;
-		match result {
+				println!("You are trying to add {} as a <Base> account", address_to_import);
+				println!(
+					"but it is already present as `{}` derivation of '{}' account.",
+					existing_vault_account.path.clone().unwrap(),
+					existing_vault_account.parent.clone().unwrap()
+				);
+				println!();
+				println!("Its parent hierarchy is this:");
+				println!("{parent_hierarchy_table}");
+				println!();
+				println!("Do you want to:");
+				println!("1. keep the existing derivation and cancel import");
+				println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)");
+			}
+
+			let result = inputs::select_action("Your choice?", vec!["1", "2"])?;
+			result.to_string()
+		};
+
+		match result.as_str() {
 			"2" => {
 				let password = match password_opt {
 					Some(password) => password.clone(),
@@ -1259,7 +1343,12 @@ pub async fn try_fetch_key_pair(
 
 		println!("(Vault: {})", account_tree_node.borrow().account);
 
-		let password = inputs::prompt_password()?;
+		let password = if data.args.no_password {
+			"".to_string()
+		} else {
+			inputs::prompt_password()?
+		};
+
 		let secret_suri =
 			vault_account::compute_suri_account_tree_node(&account_tree_node, password)?;
 
@@ -1336,6 +1425,21 @@ pub fn try_fetch_vault_data_from_file(
 	}
 }
 
+#[derive(Serialize)]
+struct VaultAccountJson {
+	address: String,
+	crypto_scheme: Option<String>,
+	path: Option<String>,
+	name: Option<String>,
+	g1v1_public_key: Option<String>,
+}
+
+impl From<serde_json::Error> for GcliError {
+	fn from(err: serde_json::Error) -> Self {
+		GcliError::Input(err.to_string())
+	}
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs
index 60d663d..8151185 100644
--- a/src/entities/vault_account.rs
+++ b/src/entities/vault_account.rs
@@ -22,6 +22,7 @@ use std::future::Future;
 use std::pin::Pin;
 use std::rc::Rc;
 use std::str::FromStr;
+use std::fmt;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
 #[sea_orm(table_name = "vault_account")]
@@ -224,8 +225,19 @@ impl From<DbAccountId> for AccountId {
 	rename_all = "PascalCase"
 )]
 pub enum DbCryptoScheme {
-	Ed25519,
+	#[sea_orm(string_value = "sr25519")]
 	Sr25519,
+	#[sea_orm(string_value = "Ed25519")]
+	Ed25519,
+}
+
+impl fmt::Display for DbCryptoScheme {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match self {
+			DbCryptoScheme::Sr25519 => write!(f, "sr25519"),
+			DbCryptoScheme::Ed25519 => write!(f, "Ed25519"),
+		}
+	}
 }
 
 impl From<crate::keys::CryptoScheme> for DbCryptoScheme {
diff --git a/src/main.rs b/src/main.rs
index 7368e9a..2f84058 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -77,6 +77,9 @@ pub struct Args {
 	/// Output format (human, json, ...)
 	#[clap(short = 'o', long, default_value = OutputFormat::Human)]
 	output_format: OutputFormat,
+	/// Do not prompt for password
+	#[clap(long)]
+	no_password: bool,
 }
 
 // TODO derive the fromstr implementation
-- 
GitLab


From 1f16771664141a62b0f7d28a9f88021ddc76b865 Mon Sep 17 00:00:00 2001
From: fred <support@qo-op.com>
Date: Sat, 31 May 2025 13:29:41 +0200
Subject: [PATCH 6/7] examples for toto

---
 examples/vault.examples.md | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/examples/vault.examples.md b/examples/vault.examples.md
index 8233632..dec5d16 100644
--- a/examples/vault.examples.md
+++ b/examples/vault.examples.md
@@ -25,7 +25,7 @@ gcli vault list base --show-g1v1
 ### Lister les comptes liés à une adresse spécifique
 ```bash
 # Par adresse SS58
-gcli vault list for -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+gcli vault list for -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
 
 # Par nom de compte
 gcli vault list for -v MonCompte
@@ -68,7 +68,7 @@ gcli vault import -u "bottom drive obey lake curtain smoke basket hold race lone
 ### Dérivation en mode interactif
 ```bash
 # Par adresse SS58
-gcli vault derive -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+gcli vault derive -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
 
 # Par nom de compte
 gcli vault derive -v MonCompte
@@ -77,7 +77,7 @@ gcli vault derive -v MonCompte
 ### Dérivation en mode non-interactif
 ```bash
 # Par adresse SS58 avec chemin de dérivation
-gcli vault derive -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV -d "//0"
+gcli vault derive -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r -d "//0"
 
 # Par nom de compte avec chemin de dérivation
 gcli vault derive -v MonCompte -d "//0"
@@ -91,13 +91,13 @@ gcli vault derive -v MonCompte -d "//0" -n "MonCompteDerive"
 
 ## Renommer un compte
 ```bash
-gcli vault rename 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+gcli vault rename 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
 ```
 
 ## Supprimer un compte
 ```bash
 # Par adresse SS58
-gcli vault remove -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+gcli vault remove -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
 
 # Par nom de compte
 gcli vault remove -v MonCompte
@@ -106,7 +106,7 @@ gcli vault remove -v MonCompte
 ## Inspecter un compte
 ```bash
 # Par adresse SS58
-gcli vault inspect -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+gcli vault inspect -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
 
 # Par nom de compte
 gcli vault inspect -v MonCompte
@@ -115,7 +115,7 @@ gcli vault inspect -v MonCompte
 ## Utiliser un compte
 ```bash
 # Par adresse SS58
-gcli vault use -a 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
+gcli vault use -a 5GV2PGDLRJ5e5EiupV4myDUzf9WzTddSExQcjQmLfwprKC8r
 
 # Par nom de compte
 gcli vault use -v MonCompte
-- 
GitLab


From dfcc91f24cab71baf5150464438c3ffb5bf36618 Mon Sep 17 00:00:00 2001
From: fred <support@qo-op.com>
Date: Sat, 31 May 2025 14:09:47 +0200
Subject: [PATCH 7/7] ok

---
 src/indexer.rs | 140 +++++++++++++++++++++++++++++++------------------
 src/utils.rs   |   2 +
 2 files changed, 91 insertions(+), 51 deletions(-)

diff --git a/src/indexer.rs b/src/indexer.rs
index aa76c1f..93373a7 100644
--- a/src/indexer.rs
+++ b/src/indexer.rs
@@ -191,59 +191,97 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 			let i_latest_h = convert_hash_bytea(i_latest_block.hash);
 			let i_latest_n = i_latest_block.height;
 
-			fn color(x: bool) -> Color {
-				match x {
-					true => Color::Green,
-					false => Color::Red,
+			match data.args.output_format {
+				OutputFormat::Json => {
+					let output = serde_json::json!({
+						"duniter": {
+							"url": d_url,
+							"genesis_hash": d_gen_hash,
+							"finalized_block": {
+								"number": d_finalized_n,
+								"hash": d_finalized_h.to_string()
+							},
+							"latest_block": {
+								"number": d_latest_n,
+								"hash": d_latest_h.to_string()
+							}
+						},
+						"indexer": {
+							"url": i_url,
+							"genesis_hash": i_gen_hash,
+							"finalized_block": {
+								"number": i_finalized_n,
+								"hash": i_finalized_h.map(|h| h.to_string())
+							},
+							"latest_block": {
+								"number": i_latest_n,
+								"hash": i_latest_h.to_string()
+							}
+						},
+						"status": {
+							"genesis_hash_match": d_gen_hash == i_gen_hash,
+							"finalized_block_match": i_finalized_h.map_or(false, |h| h == d_finalized_h),
+							"latest_block_match": i_latest_n == d_latest_n as i64 && i_latest_h == d_latest_h
+						}
+					});
+					println!("{}", serde_json::to_string_pretty(&output).unwrap());
+				},
+				OutputFormat::Human => {
+					fn color(x: bool) -> Color {
+						match x {
+							true => Color::Green,
+							false => Color::Red,
+						}
+					}
+
+					let mut table = Table::new();
+					table
+						.load_preset(presets::UTF8_FULL)
+						.apply_modifier(modifiers::UTF8_ROUND_CORNERS)
+						.set_content_arrangement(ContentArrangement::Dynamic)
+						.set_width(120)
+						.set_header(vec!["Variable", "Duniter", "Indexer"])
+						.add_row(vec!["URL", d_url, i_url])
+						.add_row(vec![
+							Cell::new("genesis hash"),
+							Cell::new(d_gen_hash),
+							Cell::new(i_gen_hash).fg(color(d_gen_hash == i_gen_hash)),
+						])
+						.add_row(vec![
+							Cell::new("finalized block number"),
+							Cell::new(d_finalized_n),
+							match i_finalized_n {
+								None => Cell::new("not indexed").fg(Color::Yellow),
+								Some(n) => {
+									// if it exists, it must be the same
+									assert_eq!(n, d_finalized_n as i64);
+									Cell::new("")
+								}
+							},
+						])
+						.add_row(vec![
+							Cell::new("finalized block hash"),
+							Cell::new(d_finalized_h),
+							match i_finalized_h {
+								// number already tells it is not indexed
+								None => Cell::new(""),
+								Some(h) => Cell::new(h).fg(color(h == d_finalized_h)),
+							},
+						])
+						.add_row(vec![
+							Cell::new("latest block number"),
+							Cell::new(d_latest_n),
+							Cell::new(i_latest_n).fg(color(i_latest_n == d_latest_n as i64)),
+						])
+						.add_row(vec![
+							Cell::new("latest block hash"),
+							Cell::new(d_latest_h),
+							Cell::new(i_latest_h).fg(color(i_latest_h == d_latest_h)),
+						]);
+
+					println!("{table}");
 				}
 			}
-
-			let mut table = Table::new();
-			table
-				.load_preset(presets::UTF8_FULL)
-				.apply_modifier(modifiers::UTF8_ROUND_CORNERS)
-				.set_content_arrangement(ContentArrangement::Dynamic)
-				.set_width(120)
-				.set_header(vec!["Variable", "Duniter", "Indexer"])
-				.add_row(vec!["URL", d_url, i_url])
-				.add_row(vec![
-					Cell::new("genesis hash"),
-					Cell::new(d_gen_hash),
-					Cell::new(i_gen_hash).fg(color(d_gen_hash == i_gen_hash)),
-				])
-				.add_row(vec![
-					Cell::new("finalized block number"),
-					Cell::new(d_finalized_n),
-					match i_finalized_n {
-						None => Cell::new("not indexed").fg(Color::Yellow),
-						Some(n) => {
-							// if it exists, it must be the same
-							assert_eq!(n, d_finalized_n as i64);
-							Cell::new("")
-						}
-					},
-				])
-				.add_row(vec![
-					Cell::new("finalized block hash"),
-					Cell::new(d_finalized_h),
-					match i_finalized_h {
-						// number already tells it is not indexed
-						None => Cell::new(""),
-						Some(h) => Cell::new(h).fg(color(h == d_finalized_h)),
-					},
-				])
-				.add_row(vec![
-					Cell::new("latest block number"),
-					Cell::new(d_latest_n),
-					Cell::new(i_latest_n).fg(color(i_latest_n == d_latest_n as i64)),
-				])
-				.add_row(vec![
-					Cell::new("latest block hash"),
-					Cell::new(d_latest_h),
-					Cell::new(i_latest_h).fg(color(i_latest_h == d_latest_h)),
-				]);
-
-			println!("{table}");
 		}
 	};
 
diff --git a/src/utils.rs b/src/utils.rs
index 68ec011..bcff984 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -116,6 +116,8 @@ pub enum GcliError {
 	/// error coming from io
 	#[allow(dead_code)]
 	IoError(std::io::Error),
+	/// error coming from config
+	Config(String),
 }
 impl std::fmt::Display for GcliError {
 	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-- 
GitLab