This commit is contained in:
Ren Amamiya
2026-04-02 13:10:21 +07:00
parent 1b4aa02cc0
commit 216c877ebf
4 changed files with 370 additions and 229 deletions

View File

@@ -91,7 +91,7 @@ impl AccountSelector {
fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) { fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let task = nostr.read(cx).get_signer(&public_key, cx); let task = nostr.read(cx).get_secret(public_key, cx);
// Mark the public key as being logged in // Mark the public key as being logged in
self.set_logging_in(public_key, cx); self.set_logging_in(public_key, cx);
@@ -117,7 +117,7 @@ impl AccountSelector {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| { nostr.update(cx, |this, cx| {
this.remove_signer(&public_key, cx); this.remove_secret(&public_key, cx);
}); });
} }

View File

@@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -79,16 +80,19 @@ pub struct NostrRegistry {
/// Nostr signer /// Nostr signer
signer: Arc<CoopSigner>, signer: Arc<CoopSigner>,
/// Local public keys /// All local stored identities
npubs: Entity<Vec<PublicKey>>, npubs: Entity<Vec<PublicKey>>,
/// App keys /// Keys directory
key_dir: PathBuf,
/// Master app keys used for various operations.
/// ///
/// Used for Nostr Connect and NIP-4e operations /// Example: Nostr Connect and NIP-4e operations
app_keys: Keys, app_keys: Keys,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
tasks: Vec<Task<()>>, tasks: Vec<Task<Result<(), Error>>>,
} }
impl EventEmitter<StateEvent> for NostrRegistry {} impl EventEmitter<StateEvent> for NostrRegistry {}
@@ -106,12 +110,20 @@ impl NostrRegistry {
/// Create a new nostr instance /// Create a new nostr instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let key_dir = config_dir().join("keys");
let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate());
// Construct the nostr signer // Construct the nostr signer
let app_keys = get_or_init_app_keys().unwrap_or(Keys::generate());
let signer = Arc::new(CoopSigner::new(app_keys.clone())); let signer = Arc::new(CoopSigner::new(app_keys.clone()));
// Construct the nostr npubs entity // Get all local stored npubs
let npubs = cx.new(|_| vec![]); let npubs = cx.new(|_| match Self::discover(&key_dir) {
Ok(npubs) => npubs,
Err(e) => {
log::error!("Failed to discover npubs: {e}");
vec![]
}
});
// Construct the nostr lmdb instance // Construct the nostr lmdb instance
let lmdb = cx.foreground_executor().block_on(async move { let lmdb = cx.foreground_executor().block_on(async move {
@@ -148,6 +160,7 @@ impl NostrRegistry {
client, client,
signer, signer,
npubs, npubs,
key_dir,
app_keys, app_keys,
tasks: vec![], tasks: vec![],
} }
@@ -173,6 +186,33 @@ impl NostrRegistry {
self.app_keys.clone() self.app_keys.clone()
} }
/// Discover all npubs in the keys directory
fn discover(dir: &PathBuf) -> Result<Vec<PublicKey>, Error> {
// Ensure keys directory exists
std::fs::create_dir_all(dir)?;
let files = std::fs::read_dir(dir)?;
let mut entries = Vec::new();
let mut npubs: Vec<PublicKey> = Vec::new();
for file in files.flatten() {
let metadata = file.metadata()?;
let modified_time = metadata.modified()?;
let name = file.file_name().into_string().unwrap().replace(".npub", "");
entries.push((modified_time, name));
}
// Sort by modification time (most recent first)
entries.sort_by(|a, b| b.0.cmp(&a.0));
for (_, name) in entries {
let public_key = PublicKey::parse(&name)?;
npubs.push(public_key);
}
Ok(npubs)
}
/// Connect to the bootstrapping relays /// Connect to the bootstrapping relays
fn connect(&mut self, cx: &mut Context<Self>) { fn connect(&mut self, cx: &mut Context<Self>) {
let client = self.client(); let client = self.client();
@@ -211,105 +251,139 @@ impl NostrRegistry {
// Emit connecting event // Emit connecting event
cx.emit(StateEvent::Connecting); cx.emit(StateEvent::Connecting);
self.tasks
.push(cx.spawn(async move |this, cx| match task.await {
Ok(_) => {
this.update(cx, |this, cx| {
cx.emit(StateEvent::Connected);
this.get_npubs(cx);
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::error(e.to_string()));
})
.ok();
}
}));
}
/// Get all used npubs
fn get_npubs(&mut self, cx: &mut Context<Self>) {
let npubs = self.npubs.downgrade();
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
let dir = config_dir().join("keys");
// Ensure keys directory exists
smol::fs::create_dir_all(&dir).await?;
let mut files = smol::fs::read_dir(&dir).await?;
let mut entries = Vec::new();
while let Some(Ok(entry)) = files.next().await {
let metadata = entry.metadata().await?;
let modified_time = metadata.modified()?;
let name = entry
.file_name()
.into_string()
.unwrap()
.replace(".npub", "");
entries.push((modified_time, name));
}
// Sort by modification time (most recent first)
entries.sort_by(|a, b| b.0.cmp(&a.0));
let mut npubs = Vec::new();
for (_, name) in entries {
let public_key = PublicKey::parse(&name)?;
npubs.push(public_key);
}
Ok(npubs)
});
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
match task.await { if let Err(e) = task.await {
Ok(public_keys) => match public_keys.is_empty() { this.update(cx, |_this, cx| {
true => { cx.emit(StateEvent::error(e.to_string()));
this.update(cx, |this, cx| { })?;
this.create_identity(cx); } else {
}) this.update(cx, |_this, cx| {
.ok(); cx.emit(StateEvent::Connected);
} })?;
false => {
// TODO: auto login
npubs
.update(cx, |this, cx| {
this.extend(public_keys);
cx.notify();
})
.ok();
}
},
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::error(e.to_string()));
})
.ok();
}
} }
Ok(())
})); }));
} }
/// Get the secret for a given npub.
pub fn get_secret(
&self,
public_key: PublicKey,
cx: &App,
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
let npub = public_key.to_bech32().unwrap();
let key_path = self.key_dir.join(format!("{}.npub", npub));
let app_keys = self.app_keys.clone();
if let Ok(payload) = std::fs::read_to_string(key_path) {
cx.background_spawn(async move {
let decrypted = app_keys.nip44_decrypt(&public_key, &payload).await?;
let secret = SecretKey::parse(&decrypted)?;
let keys = Keys::new(secret);
Ok(keys.into_nostr_signer())
})
} else {
self.get_secret_keyring(&npub, cx)
}
}
/// Get the secret for a given npub in the OS credentials store.
#[deprecated = "Use get_secret instead"]
fn get_secret_keyring(
&self,
user: &str,
cx: &App,
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
let read = cx.read_credentials(user);
let app_keys = self.app_keys.clone();
cx.background_spawn(async move {
let (_, secret) = read
.await
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
// Try to parse as a direct secret key first
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
return Ok(Keys::new(secret_key).into_nostr_signer());
}
// Convert the secret into string
let sec = String::from_utf8(secret)
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
// Try to parse as a NIP-46 URI
let uri =
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
// Set the auth URL handler
nip46.auth_url_handler(CoopAuthUrlHandler);
Ok(nip46.into_nostr_signer())
})
}
/// Add a new npub to the keys directory
fn write_secret(
&self,
public_key: PublicKey,
secret: String,
cx: &App,
) -> Task<Result<(), Error>> {
let npub = public_key.to_bech32().unwrap();
let key_path = self.key_dir.join(format!("{}.npub", npub));
let app_keys = self.app_keys.clone();
cx.background_spawn(async move {
// If the secret starts with "bunker://" (nostr connect), use it directly; otherwise, encrypt it
let content = if secret.starts_with("bunker://") {
secret
} else {
app_keys.nip44_encrypt(&public_key, &secret).await?
};
// Write the encrypted secret to the keys directory
smol::fs::write(key_path, &content).await?;
Ok(())
})
}
/// Remove a secret
pub fn remove_secret(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
let public_key = public_key.to_owned();
let npub = public_key.to_bech32().unwrap();
let keys_dir = config_dir().join("keys");
let key_path = keys_dir.join(format!("{}.npub", npub));
// Remove the secret file from the keys directory
std::fs::remove_file(key_path).ok();
self.npubs.update(cx, |this, cx| {
this.retain(|k| k != &public_key);
cx.notify();
});
}
/// Create a new identity /// Create a new identity
fn create_identity(&mut self, cx: &mut Context<Self>) { pub fn create_identity(&mut self, cx: &mut Context<Self>) {
let client = self.client(); let client = self.client();
let keys = Keys::generate(); let keys = Keys::generate();
let async_keys = keys.clone(); let async_keys = keys.clone();
let username = keys.public_key().to_bech32().unwrap();
let secret = keys.secret_key().to_secret_bytes();
// Create a write credential task
let write_credential = cx.write_credentials(&username, &username, &secret);
// Emit creating event // Emit creating event
cx.emit(StateEvent::Creating); cx.emit(StateEvent::Creating);
// Create the write secret task
let write_secret =
self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx);
// Run async tasks in background // Run async tasks in background
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = async_keys.into_nostr_signer(); let signer = async_keys.into_nostr_signer();
@@ -361,7 +435,7 @@ impl NostrRegistry {
.await?; .await?;
// Write user's credentials to the system keyring // Write user's credentials to the system keyring
write_credential.await?; write_secret.await?;
Ok(()) Ok(())
}); });
@@ -371,58 +445,19 @@ impl NostrRegistry {
Ok(_) => { Ok(_) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_signer(keys, cx); this.set_signer(keys, cx);
}) })?;
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(StateEvent::error(e.to_string())); cx.emit(StateEvent::error(e.to_string()));
}) })?;
.ok();
} }
}; };
Ok(())
})); }));
} }
/// Get the signer in keyring by username
pub fn get_signer(
&self,
public_key: &PublicKey,
cx: &App,
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
let username = public_key.to_bech32().unwrap();
let app_keys = self.app_keys.clone();
let read_credential = cx.read_credentials(&username);
cx.spawn(async move |_cx| {
let (_, secret) = read_credential
.await
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
// Try to parse as a direct secret key first
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
return Ok(Keys::new(secret_key).into_nostr_signer());
}
// Convert the secret into string
let sec = String::from_utf8(secret)
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
// Try to parse as a NIP-46 URI
let uri =
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
// Set the auth URL handler
nip46.auth_url_handler(CoopAuthUrlHandler);
Ok(nip46.into_nostr_signer())
})
}
/// Set the signer for the nostr client and verify the public key /// Set the signer for the nostr client and verify the public key
pub fn set_signer<T>(&mut self, new: T, cx: &mut Context<Self>) pub fn set_signer<T>(&mut self, new: T, cx: &mut Context<Self>)
where where
@@ -430,6 +465,7 @@ impl NostrRegistry {
{ {
let client = self.client(); let client = self.client();
let signer = self.signer(); let signer = self.signer();
let key_dir = self.key_dir.clone();
// Create a task to update the signer and verify the public key // Create a task to update the signer and verify the public key
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move { let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
@@ -441,15 +477,6 @@ impl NostrRegistry {
let signer = client.signer().context("Signer not found")?; let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let npub = public_key.to_bech32().unwrap();
let keys_dir = config_dir().join("keys");
// Ensure keys directory exists
smol::fs::create_dir_all(&keys_dir).await?;
let key_path = keys_dir.join(format!("{}.npub", npub));
smol::fs::write(key_path, "").await?;
log::info!("Signer's public key: {}", public_key); log::info!("Signer's public key: {}", public_key);
Ok(public_key) Ok(public_key)
}); });
@@ -457,9 +484,7 @@ impl NostrRegistry {
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
match task.await { match task.await {
Ok(public_key) => { Ok(public_key) => {
// Update states
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.ensure_relay_list(&public_key, cx);
// Add public key to npubs if not already present // Add public key to npubs if not already present
this.npubs.update(cx, |this, cx| { this.npubs.update(cx, |this, cx| {
if !this.contains(&public_key) { if !this.contains(&public_key) {
@@ -467,65 +492,43 @@ impl NostrRegistry {
cx.notify(); cx.notify();
} }
}); });
// Emit signer changed event // Emit signer changed event
cx.emit(StateEvent::SignerSet); cx.emit(StateEvent::SignerSet);
}) })?;
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(StateEvent::error(e.to_string())); cx.emit(StateEvent::error(e.to_string()));
}) })?;
.ok();
} }
}; };
}));
}
/// Remove a signer from the keyring Ok(())
pub fn remove_signer(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
let public_key = public_key.to_owned();
let npub = public_key.to_bech32().unwrap();
let keys_dir = config_dir().join("keys");
self.tasks.push(cx.spawn(async move |this, cx| {
let key_path = keys_dir.join(format!("{}.npub", npub));
smol::fs::remove_file(key_path).await.ok();
this.update(cx, |this, cx| {
this.npubs().update(cx, |this, cx| {
this.retain(|k| k != &public_key);
cx.notify();
});
})
.ok();
})); }));
} }
/// Add a key signer to keyring /// Add a key signer to keyring
pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context<Self>) { pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context<Self>) {
let keys = keys.clone(); let keys = keys.clone();
let username = keys.public_key().to_bech32().unwrap(); let write_secret =
let secret = keys.secret_key().to_secret_bytes(); self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx);
// Write the credential to the keyring
let write_credential = cx.write_credentials(&username, "keys", &secret);
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
match write_credential.await { match write_secret.await {
Ok(_) => { Ok(_) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_signer(keys, cx); this.set_signer(keys, cx);
}) })?;
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(StateEvent::error(e.to_string())); cx.emit(StateEvent::error(e.to_string()));
}) })?;
.ok();
} }
}; };
Ok(())
})); }));
} }
@@ -546,39 +549,32 @@ impl NostrRegistry {
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
match task.await { match task.await {
Ok((public_key, uri)) => { Ok((public_key, uri)) => {
let username = public_key.to_bech32().unwrap(); // Create the write secret task
let write_credential = this let write_secret = this.read_with(cx, |this, cx| {
.read_with(cx, |_this, cx| { this.write_secret(public_key, uri.to_string(), cx)
cx.write_credentials( })?;
&username,
"nostrconnect",
uri.to_string().as_bytes(),
)
})
.unwrap();
match write_credential.await { match write_secret.await {
Ok(_) => { Ok(_) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_signer(nip46, cx); this.set_signer(nip46, cx);
}) })?;
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(StateEvent::error(e.to_string())); cx.emit(StateEvent::error(e.to_string()));
}) })?;
.ok();
} }
} }
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(StateEvent::error(e.to_string())); cx.emit(StateEvent::error(e.to_string()));
}) })?;
.ok();
} }
}; };
Ok(())
})); }));
} }
@@ -594,17 +590,17 @@ impl NostrRegistry {
Ok(event) => { Ok(event) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.ensure_connection(&event, cx); this.ensure_connection(&event, cx);
}) })?;
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(StateEvent::RelayNotConfigured); cx.emit(StateEvent::RelayNotConfigured);
cx.emit(StateEvent::error(e.to_string())); cx.emit(StateEvent::error(e.to_string()));
}) })?;
.ok();
} }
}; };
Ok(())
})); }));
} }
@@ -653,17 +649,17 @@ impl NostrRegistry {
Ok(_) => { Ok(_) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(StateEvent::RelayConnected); cx.emit(StateEvent::RelayConnected);
}) })?;
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(StateEvent::RelayNotConfigured); cx.emit(StateEvent::RelayNotConfigured);
cx.emit(StateEvent::error(e.to_string())); cx.emit(StateEvent::error(e.to_string()));
}) })?;
.ok();
} }
}; };
Ok(())
})); }));
} }
@@ -849,29 +845,33 @@ impl NostrRegistry {
} }
} }
/// Get or create a new app keys /// Get or create new app keys
fn get_or_init_app_keys() -> Result<Keys, Error> { fn get_or_init_app_keys(cx: &App) -> Result<Keys, Error> {
let dir = config_dir().join(".app_keys"); let read = cx.read_credentials(CLIENT_NAME);
let stored_keys: Option<Keys> = cx.foreground_executor().block_on(async move {
let content = match std::fs::read(&dir) { if let Ok(Some((_, secret))) = read.await {
Ok(content) => content, SecretKey::from_slice(&secret).map(Keys::new).ok()
Err(_) => { } else {
// Generate new keys if file doesn't exist None
let keys = Keys::generate();
let secret_key = keys.secret_key();
// Create directory and write secret key
std::fs::create_dir_all(dir.parent().unwrap())?;
std::fs::write(&dir, secret_key.to_secret_bytes())?;
return Ok(keys);
} }
}; });
let secret_key = SecretKey::from_slice(&content)?; if let Some(keys) = stored_keys {
let keys = Keys::new(secret_key); Ok(keys)
} else {
let keys = Keys::generate();
let user = keys.public_key().to_hex();
let secret = keys.secret_key().to_secret_bytes();
let write = cx.write_credentials(CLIENT_NAME, &user, &secret);
Ok(keys) cx.foreground_executor().block_on(async move {
if let Err(e) = write.await {
log::error!("Keyring not available or panic: {e}")
}
});
Ok(keys)
}
} }
fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> { fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {

141
crates/state/src/npubs.rs Normal file
View File

@@ -0,0 +1,141 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Error, anyhow};
use common::config_dir;
use gpui::{App, Context};
use nostr_connect::prelude::*;
use crate::{CLIENT_NAME, NOSTR_CONNECT_TIMEOUT};
#[derive(Debug)]
pub struct NostrRing {
/// Keys directory
dir: PathBuf,
/// Master app keys used for various operations.
///
/// Example: Nostr Connect and NIP-4e operations
app_keys: Keys,
/// All local stored identities
npubs: Vec<PublicKey>,
}
impl NostrRing {
pub fn new(cx: &mut Context<Self>) -> Self {
let dir = config_dir().join("keys");
let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate());
// Get all local stored npubs
let npubs = match Self::discover(&dir) {
Ok(npubs) => npubs,
Err(e) => {
log::error!("Failed to discover npubs: {e}");
vec![]
}
};
Self {
dir,
app_keys,
npubs,
}
}
/// Get the secret for a given npub, if it exists
fn get_secret(&self, public_key: &PublicKey, cx: &App) -> Result<Arc<dyn NostrSigner>, Error> {
let npub = public_key.to_bech32()?;
let key_path = self.dir.join(format!("{}.npub", npub));
if let Ok(secret) = std::fs::read_to_string(key_path) {
let secret = SecretKey::parse(&secret)?;
let keys = Keys::new(secret);
Ok(keys.into_nostr_signer())
} else {
self.get_secret_keyring(&npub, cx)
}
}
/// Get the secret for a given npub in the os credentials store
#[deprecated = "Use get_secret instead"]
fn get_secret_keyring(&self, user: &str, cx: &App) -> Result<Arc<dyn NostrSigner>, Error> {
let read = cx.read_credentials(user);
let app_keys = self.app_keys.clone();
cx.foreground_executor().block_on(async move {
let (_, secret) = read
.await
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
// Try to parse as a direct secret key first
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
return Ok(Keys::new(secret_key).into_nostr_signer());
}
// Convert the secret into string
let sec = String::from_utf8(secret)
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
// Try to parse as a NIP-46 URI
let uri =
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
// Set the auth URL handler
nip46.auth_url_handler(CoopAuthUrlHandler);
Ok(nip46.into_nostr_signer())
})
}
/// Add a new npub to the keys directory
fn add(&mut self, public_key: PublicKey, secret: &str) -> Result<(), Error> {
let npub = public_key.to_bech32()?;
let key_path = self.dir.join(format!("{}.npub", npub));
std::fs::write(key_path, secret)?;
Ok(())
}
/// Remove a npub from the keys directory
fn remove(&self, public_key: &PublicKey) -> Result<(), Error> {
let npub = public_key.to_bech32()?;
let key_path = self.dir.join(format!("{}.npub", npub));
std::fs::remove_file(key_path)?;
Ok(())
}
/// Discover all npubs in the keys directory
fn discover(dir: &PathBuf) -> Result<Vec<PublicKey>, Error> {
// Ensure keys directory exists
std::fs::create_dir_all(dir)?;
let files = std::fs::read_dir(dir)?;
let mut entries = Vec::new();
let mut npubs: Vec<PublicKey> = Vec::new();
for file in files.flatten() {
let metadata = file.metadata()?;
let modified_time = metadata.modified()?;
let name = file.file_name().into_string().unwrap().replace(".npub", "");
entries.push((modified_time, name));
}
// Sort by modification time (most recent first)
entries.sort_by(|a, b| b.0.cmp(&a.0));
for (_, name) in entries {
let public_key = PublicKey::parse(&name)?;
npubs.push(public_key);
}
Ok(npubs)
}
}

View File

@@ -42,7 +42,7 @@ impl CoopSigner {
/// Get public key /// Get public key
/// ///
/// Ensure to call this method after the signer has been initialized. /// Ensure to call this method after the signer has been initialized.
/// Otherwise, this method will panic. /// Otherwise, it will panic.
pub fn public_key(&self) -> Option<PublicKey> { pub fn public_key(&self) -> Option<PublicKey> {
*self.signer_pkey.read_blocking() *self.signer_pkey.read_blocking()
} }