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:
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 {