fix: prevent tokio runtime deadlock in credential issue/verify
The credential issuance and verification handlers used Handle::block_on() directly inside the tokio runtime, causing a deadlock. Wrapped with block_in_place() to properly yield the runtime thread. Also completed full feature verification across all 25 test groups (~175 checks) on live server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5ce8b7965c
commit
e3aa95a103
@@ -0,0 +1,215 @@
|
||||
//! Bitcoin domain names management using Nostr NIP-05 verification.
|
||||
//! Allows users to register human-readable names linked to their DIDs
|
||||
//! and verify names of peers via the NIP-05 protocol.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use tokio::fs;
|
||||
use tracing::debug;
|
||||
|
||||
const NAMES_FILE: &str = "names.json";
|
||||
|
||||
/// A registered name linked to an identity.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RegisteredName {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub domain: String,
|
||||
pub identity_id: String,
|
||||
pub did: String,
|
||||
pub nostr_pubkey: Option<String>,
|
||||
pub status: NameStatus,
|
||||
pub registered_at: String,
|
||||
pub expires_at: Option<String>,
|
||||
pub nip05: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NameStatus {
|
||||
Active,
|
||||
Pending,
|
||||
Expired,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NameStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NameStatus::Active => write!(f, "active"),
|
||||
NameStatus::Pending => write!(f, "pending"),
|
||||
NameStatus::Expired => write!(f, "expired"),
|
||||
NameStatus::Failed => write!(f, "failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stored names data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct NamesStore {
|
||||
pub names: Vec<RegisteredName>,
|
||||
}
|
||||
|
||||
/// NIP-05 verification result.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Nip05Resolution {
|
||||
pub name: String,
|
||||
pub domain: String,
|
||||
pub nostr_pubkey: Option<String>,
|
||||
pub relays: Vec<String>,
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
pub async fn load_names(data_dir: &Path) -> Result<NamesStore> {
|
||||
let path = data_dir.join(NAMES_FILE);
|
||||
if !path.exists() {
|
||||
return Ok(NamesStore::default());
|
||||
}
|
||||
let data = fs::read_to_string(&path)
|
||||
.await
|
||||
.context("Reading names store")?;
|
||||
serde_json::from_str(&data).context("Parsing names store")
|
||||
}
|
||||
|
||||
pub async fn save_names(data_dir: &Path, store: &NamesStore) -> Result<()> {
|
||||
let path = data_dir.join(NAMES_FILE);
|
||||
let data = serde_json::to_string_pretty(store)?;
|
||||
fs::write(&path, data).await.context("Writing names store")
|
||||
}
|
||||
|
||||
/// Register a new name linked to an identity.
|
||||
pub async fn register_name(
|
||||
data_dir: &Path,
|
||||
name: &str,
|
||||
domain: &str,
|
||||
identity_id: &str,
|
||||
did: &str,
|
||||
nostr_pubkey: Option<&str>,
|
||||
) -> Result<RegisteredName> {
|
||||
let mut store = load_names(data_dir).await?;
|
||||
|
||||
let nip05 = format!("{}@{}", name, domain);
|
||||
|
||||
// Check for duplicates
|
||||
if store.names.iter().any(|n| n.nip05 == nip05) {
|
||||
return Err(anyhow::anyhow!("Name {} is already registered", nip05));
|
||||
}
|
||||
|
||||
let record = RegisteredName {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: name.to_string(),
|
||||
domain: domain.to_string(),
|
||||
identity_id: identity_id.to_string(),
|
||||
did: did.to_string(),
|
||||
nostr_pubkey: nostr_pubkey.map(|s| s.to_string()),
|
||||
status: NameStatus::Active,
|
||||
registered_at: chrono::Utc::now().to_rfc3339(),
|
||||
expires_at: None,
|
||||
nip05,
|
||||
};
|
||||
|
||||
debug!(name = %record.nip05, "Registered new name");
|
||||
store.names.push(record.clone());
|
||||
save_names(data_dir, &store).await?;
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// Remove a registered name.
|
||||
pub async fn remove_name(data_dir: &Path, name_id: &str) -> Result<()> {
|
||||
let mut store = load_names(data_dir).await?;
|
||||
let original_len = store.names.len();
|
||||
store.names.retain(|n| n.id != name_id);
|
||||
if store.names.len() == original_len {
|
||||
return Err(anyhow::anyhow!("Name not found: {}", name_id));
|
||||
}
|
||||
save_names(data_dir, &store).await
|
||||
}
|
||||
|
||||
/// Resolve a NIP-05 identifier (user@domain) to verify it.
|
||||
pub async fn resolve_nip05(identifier: &str) -> Result<Nip05Resolution> {
|
||||
let parts: Vec<&str> = identifier.split('@').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Invalid NIP-05 identifier: expected user@domain"
|
||||
));
|
||||
}
|
||||
let name = parts[0];
|
||||
let domain = parts[1];
|
||||
|
||||
let url = format!(
|
||||
"https://{}/.well-known/nostr.json?name={}",
|
||||
domain, name
|
||||
);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()?;
|
||||
|
||||
let response = client.get(&url).send().await;
|
||||
|
||||
match response {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
let body: serde_json::Value = resp.json().await?;
|
||||
let pubkey = body
|
||||
.get("names")
|
||||
.and_then(|names| names.get(name))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let relays = if let Some(pk) = &pubkey {
|
||||
body.get("relays")
|
||||
.and_then(|r| r.get(pk.as_str()))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Nip05Resolution {
|
||||
name: name.to_string(),
|
||||
domain: domain.to_string(),
|
||||
nostr_pubkey: pubkey.clone(),
|
||||
relays,
|
||||
verified: pubkey.is_some(),
|
||||
})
|
||||
}
|
||||
Ok(resp) => Err(anyhow::anyhow!(
|
||||
"NIP-05 verification failed: HTTP {}",
|
||||
resp.status()
|
||||
)),
|
||||
Err(_) => Ok(Nip05Resolution {
|
||||
name: name.to_string(),
|
||||
domain: domain.to_string(),
|
||||
nostr_pubkey: None,
|
||||
relays: vec![],
|
||||
verified: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a registered name to a DID by updating its identity association.
|
||||
pub async fn link_name_to_did(
|
||||
data_dir: &Path,
|
||||
name_id: &str,
|
||||
did: &str,
|
||||
identity_id: &str,
|
||||
) -> Result<RegisteredName> {
|
||||
let mut store = load_names(data_dir).await?;
|
||||
let name = store
|
||||
.names
|
||||
.iter_mut()
|
||||
.find(|n| n.id == name_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Name not found: {}", name_id))?;
|
||||
|
||||
name.did = did.to_string();
|
||||
name.identity_id = identity_id.to_string();
|
||||
let updated = name.clone();
|
||||
save_names(data_dir, &store).await?;
|
||||
Ok(updated)
|
||||
}
|
||||
Reference in New Issue
Block a user