diff --git a/nomen/src/config/cfg.rs b/nomen/src/config/cfg.rs index b25bbbb..8cec781 100644 --- a/nomen/src/config/cfg.rs +++ b/nomen/src/config/cfg.rs @@ -8,6 +8,8 @@ use nostr_sdk::{ }; use sqlx::{sqlite, SqlitePool}; +use crate::util::Nsec; + use super::{Cli, ConfigFile}; #[derive(Clone, Debug)] @@ -71,6 +73,19 @@ impl Config { Ok((keys, client)) } + pub async fn nostr_keys_client( + &self, + keys: &nostr_sdk::Keys, + ) -> anyhow::Result { + let client = nostr_sdk::Client::with_opts(keys, Options::new().wait_for_send(true)); + let relays = self.relays(); + for relay in relays { + client.add_relay(relay, None).await?; + } + client.connect().await; + Ok(client) + } + pub async fn nostr_random_client( &self, ) -> anyhow::Result<(nostr_sdk::Keys, nostr_sdk::Client)> { @@ -98,14 +113,18 @@ impl Config { (self.publish_index() || self.well_known()) && self.file.nostr.secret.is_none() } - fn publish_index(&self) -> bool { + pub fn publish_index(&self) -> bool { self.file.nostr.publish.unwrap_or_default() } - fn well_known(&self) -> bool { + pub fn well_known(&self) -> bool { self.file.nostr.well_known.unwrap_or_default() } + pub fn secret_key(&self) -> Option { + self.file.nostr.secret + } + fn rpc_cookie(&self) -> Option { self.file.rpc.cookie.clone() } diff --git a/nomen/src/db/mod.rs b/nomen/src/db/mod.rs index e96306d..7fd66f2 100644 --- a/nomen/src/db/mod.rs +++ b/nomen/src/db/mod.rs @@ -13,9 +13,10 @@ use sqlx::{Sqlite, SqlitePool}; mod index; mod name; mod raw; +pub mod relay_index; pub mod stats; -static MIGRATIONS: [&str; 14] = [ +static MIGRATIONS: [&str; 15] = [ "CREATE TABLE event_log (id INTEGER PRIMARY KEY, created_at, type, data);", "CREATE TABLE index_height (blockheight INTEGER PRIMARY KEY, blockhash);", "CREATE TABLE raw_blockchain (id INTEGER PRIMARY KEY, blockhash, txid, blocktime, blockheight, txheight, vout, data, indexed_at);", @@ -49,6 +50,7 @@ static MIGRATIONS: [&str; 14] = [ "CREATE TABLE name_events (name, fingerprint, nsid, pubkey, created_at, event_id, records, indexed_at, raw_event);", "CREATE UNIQUE INDEX name_events_unique_idx ON name_events(name, pubkey);", "CREATE INDEX name_events_created_at_idx ON name_events(created_at);", + "CREATE TABLE relay_index_queue (name);" ]; pub async fn initialize(config: &Config) -> anyhow::Result { diff --git a/nomen/src/db/relay_index.rs b/nomen/src/db/relay_index.rs new file mode 100644 index 0000000..02b9935 --- /dev/null +++ b/nomen/src/db/relay_index.rs @@ -0,0 +1,39 @@ +use sqlx::Sqlite; + +pub async fn queue( + conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy, + name: &str, +) -> anyhow::Result<()> { + sqlx::query("INSERT INTO relay_index_queue (name) VALUES (?)") + .bind(name) + .execute(conn) + .await?; + Ok(()) +} + +#[derive(sqlx::FromRow, Debug)] +pub struct Name { + pub name: String, + pub pubkey: String, + pub records: String, +} + +pub async fn fetch_all( + conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy, +) -> anyhow::Result> { + let results = sqlx::query_as::<_, Name>( + "SELECT vnr.name, vnr.pubkey, COALESCE(vnr.records, '{}') as records + FROM valid_names_records_vw vnr + JOIN relay_index_queue riq ON vnr.name = riq.name;", + ) + .fetch_all(conn) + .await?; + Ok(results) +} + +pub async fn clear(conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy) -> anyhow::Result<()> { + sqlx::query("DELETE FROM relay_index_queue;") + .execute(conn) + .await?; + Ok(()) +} diff --git a/nomen/src/subcommands/index/blockchain.rs b/nomen/src/subcommands/index/blockchain.rs index cb67d99..ce59fbb 100644 --- a/nomen/src/subcommands/index/blockchain.rs +++ b/nomen/src/subcommands/index/blockchain.rs @@ -289,6 +289,7 @@ async fn index_output(conn: &SqlitePool, index: BlockchainIndex) -> anyhow::Resu } } } + db::relay_index::queue(conn, name).await?; } } else { db::insert_blockchain_index(conn, &index).await?; diff --git a/nomen/src/subcommands/index/events/mod.rs b/nomen/src/subcommands/index/events/mod.rs index 723211b..f246b1f 100644 --- a/nomen/src/subcommands/index/events/mod.rs +++ b/nomen/src/subcommands/index/events/mod.rs @@ -1,5 +1,6 @@ mod event_data; mod records; +pub mod relay_index; pub use event_data::*; pub use records::*; diff --git a/nomen/src/subcommands/index/events/records.rs b/nomen/src/subcommands/index/events/records.rs index 82229ea..808cec6 100644 --- a/nomen/src/subcommands/index/events/records.rs +++ b/nomen/src/subcommands/index/events/records.rs @@ -49,6 +49,8 @@ async fn save_event(pool: &SqlitePool, ed: EventData) -> anyhow::Result<()> { db::update_v0_index(pool, name.as_ref(), &pubkey, calculated_nsid).await?; + db::relay_index::queue(pool, name.as_ref()).await?; + Ok(()) } diff --git a/nomen/src/subcommands/index/events/relay_index.rs b/nomen/src/subcommands/index/events/relay_index.rs new file mode 100644 index 0000000..222882f --- /dev/null +++ b/nomen/src/subcommands/index/events/relay_index.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +use nostr_sdk::{EventBuilder, Keys, Tag}; +use secp256k1::SecretKey; +use serde::Serialize; +use sqlx::SqlitePool; + +use crate::{ + config::Config, + db::{self, relay_index::Name}, +}; + +pub async fn publish(config: &Config, pool: &SqlitePool) -> anyhow::Result<()> { + if !config.publish_index() { + return Ok(()); + } + let sk: SecretKey = config + .secret_key() + .expect("Missing config validation for secret") + .into(); + let keys = Keys::new(sk); + let client = config.nostr_keys_client(&keys).await?; + tracing::info!("Publishing relay index."); + let names = db::relay_index::fetch_all(pool).await?; + + send_event(names, keys, &client).await?; + + db::relay_index::clear(pool).await?; + client.disconnect().await.ok(); + Ok(()) +} + +async fn send_event( + names: Vec, + keys: Keys, + client: &nostr_sdk::Client, +) -> Result<(), anyhow::Error> { + for name in names { + let records: HashMap = serde_json::from_str(&name.records)?; + let content = Content { + name: name.name.clone(), + pubkey: name.pubkey, + records, + }; + let content_serialize = serde_json::to_string(&content)?; + let event = EventBuilder::new( + nostr_sdk::Kind::ParameterizedReplaceable(38301), + content_serialize, + &[Tag::Identifier(name.name.clone())], + ) + .to_event(&keys)?; + + if client.send_event(event).await.is_err() { + tracing::error!("Unable to broadcast event during relay index publish"); + } + } + Ok(()) +} + +#[derive(Serialize)] +struct Content { + name: String, + pubkey: String, + records: HashMap, +} diff --git a/nomen/src/subcommands/index/mod.rs b/nomen/src/subcommands/index/mod.rs index 3668c89..b29075a 100644 --- a/nomen/src/subcommands/index/mod.rs +++ b/nomen/src/subcommands/index/mod.rs @@ -7,6 +7,7 @@ pub async fn index(config: &Config) -> anyhow::Result<()> { let pool = config.sqlite().await?; blockchain::index(config, &pool).await?; events::records(config, &pool).await?; + events::relay_index::publish(config, &pool).await?; db::save_event(&pool, "index", "").await?; Ok(()) diff --git a/nomen/src/subcommands/server/explorer.rs b/nomen/src/subcommands/server/explorer.rs index a541f83..1e5bce3 100644 --- a/nomen/src/subcommands/server/explorer.rs +++ b/nomen/src/subcommands/server/explorer.rs @@ -14,7 +14,7 @@ use serde::Deserialize; use crate::{ db::{self, NameDetails}, subcommands::util::{extend_psbt, name_event}, - util::{format_time, KeyVal, Pubkey}, + util::{format_time, KeyVal, Npub}, }; use super::{AppState, WebError}; @@ -129,7 +129,7 @@ pub struct NewNameTemplate { pub struct NewNameForm { upgrade: bool, name: String, - pubkey: Pubkey, + pubkey: Npub, psbt: String, } @@ -197,7 +197,7 @@ pub struct NewRecordsTemplate { #[derive(Deserialize)] pub struct NewRecordsQuery { name: Option, - pubkey: Option, + pubkey: Option, } pub async fn new_records_form( @@ -240,7 +240,7 @@ async fn records_from_query(query: &NewRecordsQuery, state: &AppState) -> Result pub struct NewRecordsForm { records: String, name: String, - pubkey: Pubkey, + pubkey: Npub, } #[allow(clippy::unused_async)] @@ -297,7 +297,7 @@ pub mod transfer { use crate::{ subcommands::{AppState, WebError}, - util::Pubkey, + util::Npub, }; #[derive(askama::Template)] @@ -307,8 +307,8 @@ pub mod transfer { #[derive(Deserialize)] pub struct InitiateTransferForm { name: String, - pubkey: Pubkey, - old_pubkey: Pubkey, + pubkey: Npub, + old_pubkey: Npub, } #[allow(clippy::unused_async)] @@ -320,8 +320,8 @@ pub mod transfer { #[template(path = "transfer/sign.html")] pub struct SignEventTemplate { name: String, - pubkey: Pubkey, - old_pubkey: Pubkey, + pubkey: Npub, + old_pubkey: Npub, event: String, } @@ -346,7 +346,7 @@ pub mod transfer { #[derive(Deserialize)] pub struct FinalTransferForm { name: String, - pubkey: Pubkey, + pubkey: Npub, sig: Signature, } @@ -376,3 +376,27 @@ pub mod transfer { }) } } + +pub mod well_known { + use axum::{extract::State, Json}; + use nostr_sdk::Keys; + + use crate::subcommands::{AppState, WebError}; + + #[allow(clippy::unused_async)] + pub async fn nomen( + State(state): State, + ) -> anyhow::Result, WebError> { + let sk = state.config.secret_key().ok_or(anyhow::anyhow!( + "Config: secret key required for .well-known" + ))?; + let pk = Keys::new(*sk.as_ref()).public_key(); + let result = serde_json::json!({ + "indexer": { + "pubkey": pk.to_string() + } + }); + + Ok(Json(result)) + } +} diff --git a/nomen/src/subcommands/server/mod.rs b/nomen/src/subcommands/server/mod.rs index 6f43f21..f3046a1 100644 --- a/nomen/src/subcommands/server/mod.rs +++ b/nomen/src/subcommands/server/mod.rs @@ -64,6 +64,10 @@ pub async fn start(config: &Config, conn: &SqlitePool) -> anyhow::Result<()> { .route("/stats", get(explorer::index_stats)); } + if config.well_known() { + app = app.route("/.well-known/nomen.json", get(explorer::well_known::nomen)); + } + if config.api() { let api_router = Router::new() .route("/names", get(api::names)) diff --git a/nomen/src/util/mod.rs b/nomen/src/util/mod.rs index fff52e7..dd0c71f 100644 --- a/nomen/src/util/mod.rs +++ b/nomen/src/util/mod.rs @@ -1,10 +1,10 @@ mod keyval; +mod npub; mod nsec; -mod pubkey; pub use keyval::*; +pub use npub::*; pub use nsec::*; -pub use pubkey::*; use time::{macros::format_description, OffsetDateTime}; diff --git a/nomen/src/util/pubkey.rs b/nomen/src/util/npub.rs similarity index 67% rename from nomen/src/util/pubkey.rs rename to nomen/src/util/npub.rs index 88fdb09..b8e00b4 100644 --- a/nomen/src/util/pubkey.rs +++ b/nomen/src/util/npub.rs @@ -6,24 +6,24 @@ use serde::Serialize; #[derive(Debug, Clone, Copy, Serialize, serde_with::DeserializeFromStr)] -pub struct Pubkey(XOnlyPublicKey); +pub struct Npub(XOnlyPublicKey); -impl AsRef for Pubkey { +impl AsRef for Npub { fn as_ref(&self) -> &XOnlyPublicKey { &self.0 } } -impl FromStr for Pubkey { +impl FromStr for Npub { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let keys = Keys::from_pk_str(s)?; - Ok(Pubkey(keys.public_key())) + Ok(Npub(keys.public_key())) } } -impl Display for Pubkey { +impl Display for Npub { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0.to_string()) } @@ -35,14 +35,14 @@ mod tests { #[test] fn test_npub() { - let _pubkey: Pubkey = "npub1u50q2x85utgcgqrmv607crvmk8x3k2nvyun84dxlj6034kajje0s2cm3r0" + let _pubkey: Npub = "npub1u50q2x85utgcgqrmv607crvmk8x3k2nvyun84dxlj6034kajje0s2cm3r0" .parse() .unwrap(); } #[test] fn test_hex() { - let _pubkey: Pubkey = "e51e0518f4e2d184007b669fec0d9bb1cd1b2a6c27267ab4df969f1adbb2965f" + let _pubkey: Npub = "e51e0518f4e2d184007b669fec0d9bb1cd1b2a6c27267ab4df969f1adbb2965f" .parse() .unwrap(); } diff --git a/nomen/src/util/nsec.rs b/nomen/src/util/nsec.rs index c2f65d1..468c9d9 100644 --- a/nomen/src/util/nsec.rs +++ b/nomen/src/util/nsec.rs @@ -35,6 +35,12 @@ impl From for Nsec { } } +impl From for SecretKey { + fn from(value: Nsec) -> Self { + value.0 + } +} + #[cfg(test)] mod tests { use super::*;