rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

commit 8f7dc600e8603d64d6a031e5897adfc4070c0b86
parent d7a493da239527b4117e5bba495210ba8c2ff859
Author: triesap <triesap@radroots.dev>
Date:   Wed, 31 Dec 2025 12:22:33 +0000

nostr: validate farm dependencies and update event timestamp handling

- Bump nostr crates to 0.44.x and refresh Cargo.lock deps
- Fix published_at extraction to use created_at.as_secs()
- Add farm profile/record dependency checks during listing validation
- Publish identity profile on startup before falling back to metadata

Diffstat:
MCargo.lock | 49+++++++++++++++++++++++++++++++------------------
Msrc/adapters/nostr/event.rs | 2+-
Msrc/features/trade_listing/handlers/dvm.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/lib.rs | 13+++++++++++--
4 files changed, 131 insertions(+), 25 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -319,6 +319,12 @@ dependencies = [ ] [[package]] +name = "btreecap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6160c957d8aa33d0a8ba1dbab98e3cb57023ad9374c501441e88559f99e6c4c9" + +[[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -546,7 +552,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -831,6 +836,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] name = "hex-conservative" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1154,9 +1165,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", ] [[package]] @@ -1395,9 +1403,7 @@ dependencies = [ [[package]] name = "nostr" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30e6dcb36d88017587b0b5578d1ed3398afe8e4f45fdb910e48b8675aaf6f68" +version = "0.44.1" dependencies = [ "aes", "base64 0.22.1", @@ -1407,8 +1413,10 @@ dependencies = [ "cbc", "chacha20", "chacha20poly1305", - "getrandom 0.2.15", + "hex", "instant", + "once_cell", + "rand 0.9.0", "scrypt", "secp256k1", "serde", @@ -1419,24 +1427,29 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c75a8c2175d2785ba73cfddef21d1e30da5fbbdf158569b6808ba44973a15b" +version = "0.44.0" dependencies = [ + "btreecap", "lru", "nostr", "tokio", ] [[package]] +name = "nostr-gossip" +version = "0.44.0" +dependencies = [ + "nostr", +] + +[[package]] name = "nostr-relay-pool" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265d9b44771ed15db93b183a0c93dbb703b2b0d0b74dffb5c2a081be52373a5a" +version = "0.44.0" dependencies = [ "async-utility", "async-wsocket", "atomic-destructor", + "hex", "lru", "negentropy", "nostr", @@ -1447,15 +1460,15 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599f8963d6a1522a13b1a2b0ea6e168acfc367706606f1d33fa595e91fa22db0" +version = "0.44.1" dependencies = [ "async-utility", "nostr", "nostr-database", + "nostr-gossip", "nostr-relay-pool", "tokio", + "tracing", ] [[package]] @@ -1793,6 +1806,7 @@ name = "radroots-identity" version = "0.1.0" dependencies = [ "nostr", + "radroots-events", "radroots-runtime", "serde", "serde_json", @@ -2186,7 +2200,6 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand 0.8.5", "secp256k1-sys", "serde", ] diff --git a/src/adapters/nostr/event.rs b/src/adapters/nostr/event.rs @@ -58,7 +58,7 @@ impl JobEventLike for NostrEventAdapter<'_> { self.author_hex.clone() } fn raw_published_at(&self) -> u32 { - self.evt.created_at.as_u64() as u32 + self.evt.created_at.as_secs() as u32 } fn raw_kind(&self) -> u32 { match self.evt.kind { diff --git a/src/features/trade_listing/handlers/dvm.rs b/src/features/trade_listing/handlers/dvm.rs @@ -16,6 +16,8 @@ use radroots_nostr::prelude::{ RadrootsNostrKeys, RadrootsNostrTag, }; +use radroots_events::kinds::KIND_FARM; +use radroots_events::listing::RadrootsListingFarmRef; use radroots_trade::listing::{ dvm::{ TradeListingEnvelope, TradeListingEnvelopeError, TradeListingMessageType, @@ -323,10 +325,13 @@ async fn handle_listing_validate_request( let errors = if let Some(event) = listing_event { let rr_event = radroots_event_from_nostr(&event); match validate_listing_event(&rr_event) { - Ok(_) => { - let mut state = state.lock().await; - state.mark_listing_validated(listing_addr); - Vec::new() + Ok(listing) => { + let errors = validate_farm_dependencies(client, &listing.listing.farm).await?; + if errors.is_empty() { + let mut state = state.lock().await; + state.mark_listing_validated(listing_addr); + } + errors } Err(err) => vec![err], } @@ -937,6 +942,85 @@ async fn fetch_listing_by_addr( Ok(latest) } +async fn fetch_latest_event_by_kind( + client: &RadrootsNostrClient, + filter: RadrootsNostrFilter, + kind: RadrootsNostrKind, +) -> Result<Option<RadrootsNostrEvent>, TradeListingDvmError> { + let events = client.fetch_events(filter, Duration::from_secs(10)).await?; + let mut latest: Option<RadrootsNostrEvent> = None; + for ev in events { + if ev.kind != kind { + continue; + } + match &latest { + Some(cur) if ev.created_at <= cur.created_at => {} + _ => latest = Some(ev), + } + } + Ok(latest) +} + +async fn validate_farm_dependencies( + client: &RadrootsNostrClient, + farm: &RadrootsListingFarmRef, +) -> Result<Vec<TradeListingValidationError>, TradeListingDvmError> { + let mut errors = Vec::new(); + let farm_pubkey = farm.pubkey.trim(); + let farm_d_tag = farm.d_tag.trim(); + let author = match radroots_nostr_parse_pubkey(farm_pubkey) { + Ok(author) => author, + Err(_) => { + errors.push(TradeListingValidationError::MissingFarmProfile); + errors.push(TradeListingValidationError::MissingFarmRecord); + return Ok(errors); + } + }; + + let profile_filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::Metadata) + .author(author.clone()); + let profile_event = + match fetch_latest_event_by_kind(client, profile_filter, RadrootsNostrKind::Metadata).await + { + Ok(event) => event, + Err(_) => None, + }; + let has_profile = profile_event + .map(|event| { + let rr_event = radroots_event_from_nostr(&event); + tag_has_value(&rr_event.tags, "t", "radroots:type:farm") + }) + .unwrap_or(false); + if !has_profile { + errors.push(TradeListingValidationError::MissingFarmProfile); + } + + if !farm_d_tag.is_empty() { + let record_filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::Custom(KIND_FARM as u16)) + .author(author) + .identifier(farm_d_tag.to_string()); + let record_event = match fetch_latest_event_by_kind( + client, + record_filter, + RadrootsNostrKind::Custom(KIND_FARM as u16), + ) + .await + { + Ok(event) => event, + Err(_) => None, + }; + if record_event.is_none() { + errors.push(TradeListingValidationError::MissingFarmRecord); + } + } else { + errors.push(TradeListingValidationError::MissingFarmRecord); + } + + Ok(errors) +} + fn parse_payload<T: DeserializeOwned>(value: serde_json::Value) -> Result<T, TradeListingDvmError> { serde_json::from_value(value).map_err(|e| TradeListingDvmError::InvalidPayload(e.to_string())) } diff --git a/src/lib.rs b/src/lib.rs @@ -17,7 +17,7 @@ use crate::{ rhi::{Rhi, start_subscriber}, }; use radroots_identity::RadrootsIdentity; -use radroots_nostr::prelude::RadrootsNostrMetadata; +use radroots_nostr::prelude::{radroots_nostr_publish_identity_profile, RadrootsNostrMetadata}; use tracing::{info, warn}; fn metadata_has_fields(md: &RadrootsNostrMetadata) -> bool { @@ -54,7 +54,16 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> if !relays.is_empty() { client.connect().await; client.wait_for_connection(Duration::from_secs(5)).await; - if has_metadata { + let profile_published = match radroots_nostr_publish_identity_profile(&client, &identity).await + { + Ok(Some(_)) => true, + Ok(None) => false, + Err(e) => { + warn!("Failed to publish identity profile: {e}"); + false + } + }; + if has_metadata && !profile_published { if let Err(e) = client.set_metadata(&md).await { warn!("Failed to publish metadata on startup: {e}"); } else {