rhi

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

transport_nostr_adapter.rs (22854B)


      1 //! Private source-locked Nostr transport composition for the RHI runtime.
      2 
      3 use core::fmt;
      4 use std::{collections::BTreeMap, error::Error, sync::Arc};
      5 
      6 use radroots_event_codec::Codec;
      7 use radroots_transport::{
      8     BoxFuture, DeliveryReceipt, DeliveryRequest, EventSink, EventSource, EventSubscriber,
      9     FetchPage, FetchRequest, SinkFailure, SinkStatus, SourceStatus, Target, TargetSet,
     10     outcome::{DeliveryOutcomeKind, FetchTargetState},
     11     policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
     12     sink::{DeliveryPayload, DeliveryTargetReceipt},
     13     source::{BoxSubscription, FetchBounds, FetchSelector, SubscriptionRequest},
     14     target::TargetFingerprint,
     15 };
     16 use radroots_transport_nostr::{
     17     Config, NostrTransport, PreparedDelivery, RelayAccess, RelayEndpoint, RelayProfile,
     18     RelayProfileKind, RelayUrlPolicy,
     19 };
     20 
     21 use crate::{
     22     RhiConfigDocumentV1, RhiConfigProfile, RhiExactPresenceSink, RhiExactPublicationSink,
     23     RhiPreparedPresenceAttempt, RhiPreparedPublicationAttempt, RhiPresenceAttemptOutcome,
     24     RhiPublicationAttemptOutcome, RhiTransportAdapters,
     25 };
     26 
     27 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     28 pub(crate) enum RhiNostrAdapterErrorKind {
     29     Configuration,
     30     Target,
     31     Payload,
     32     Preparation,
     33 }
     34 
     35 pub(crate) struct RhiNostrAdapterError {
     36     kind: RhiNostrAdapterErrorKind,
     37 }
     38 
     39 impl RhiNostrAdapterError {
     40     #[cfg(test)]
     41     pub(crate) const fn kind(&self) -> RhiNostrAdapterErrorKind {
     42         self.kind
     43     }
     44 }
     45 
     46 impl fmt::Debug for RhiNostrAdapterError {
     47     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     48         formatter
     49             .debug_struct("RhiNostrAdapterError")
     50             .field("kind", &self.kind)
     51             .finish()
     52     }
     53 }
     54 
     55 impl fmt::Display for RhiNostrAdapterError {
     56     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     57         formatter.write_str("RHI Nostr adapter failed")
     58     }
     59 }
     60 
     61 impl Error for RhiNostrAdapterError {}
     62 
     63 const fn adapter_error(kind: RhiNostrAdapterErrorKind) -> RhiNostrAdapterError {
     64     RhiNostrAdapterError { kind }
     65 }
     66 
     67 struct RelayDefinition {
     68     id: Box<str>,
     69     target: Target,
     70     kind: RelayProfileKind,
     71 }
     72 
     73 struct RelayBinding {
     74     transport: NostrTransport,
     75     target: Target,
     76 }
     77 
     78 /// One private adapter that routes a bounded request to exactly one configured
     79 /// public or simulator transport group.
     80 struct RhiNostrTransport {
     81     by_target: BTreeMap<TargetFingerprint, NostrTransport>,
     82 }
     83 
     84 impl RhiNostrTransport {
     85     fn for_targets(
     86         &self,
     87         targets: &TargetSet,
     88     ) -> Result<NostrTransport, radroots_transport::Error> {
     89         let mut selected: Option<NostrTransport> = None;
     90         for target in targets.targets() {
     91             let transport = self
     92                 .by_target
     93                 .get(target.fingerprint())
     94                 .ok_or(radroots_transport::Error::UnsupportedOperation)?;
     95             if let Some(existing) = &selected {
     96                 if existing.config() != transport.config() {
     97                     return Err(radroots_transport::Error::UnsupportedOperation);
     98                 }
     99             } else {
    100                 selected = Some(transport.clone());
    101             }
    102         }
    103         selected.ok_or(radroots_transport::Error::UnsupportedOperation)
    104     }
    105 }
    106 
    107 impl EventSource for RhiNostrTransport {
    108     fn status(&self) -> BoxFuture<'_, Result<SourceStatus, radroots_transport::Error>> {
    109         Box::pin(async { Err(radroots_transport::Error::UnsupportedOperation) })
    110     }
    111 
    112     fn fetch(
    113         &self,
    114         request: FetchRequest,
    115     ) -> BoxFuture<'_, Result<FetchPage, radroots_transport::Error>> {
    116         Box::pin(async move {
    117             let transport = self.for_targets(request.target_set())?;
    118             transport.fetch(request).await
    119         })
    120     }
    121 }
    122 
    123 impl EventSubscriber for RhiNostrTransport {
    124     fn subscribe(
    125         &self,
    126         request: SubscriptionRequest,
    127     ) -> BoxFuture<'_, Result<BoxSubscription, radroots_transport::Error>> {
    128         Box::pin(async move {
    129             let transport = self.for_targets(request.target_set())?;
    130             transport.subscribe(request).await
    131         })
    132     }
    133 }
    134 
    135 impl EventSink for RhiNostrTransport {
    136     fn status(&self) -> BoxFuture<'_, Result<SinkStatus, radroots_transport::Error>> {
    137         Box::pin(async { Err(radroots_transport::Error::UnsupportedOperation) })
    138     }
    139 
    140     fn deliver(
    141         &self,
    142         request: DeliveryRequest,
    143     ) -> BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>> {
    144         Box::pin(async move {
    145             let transport = self
    146                 .for_targets(request.target_set())
    147                 .map_err(|_| SinkFailure::invalid_contract(&request))?;
    148             transport.deliver(request).await
    149         })
    150     }
    151 }
    152 
    153 pub(crate) struct RhiNostrExactSink {
    154     relays: BTreeMap<Box<str>, RelayBinding>,
    155 }
    156 
    157 struct RhiPreparedNostrDelivery {
    158     transport: NostrTransport,
    159     prepared: PreparedDelivery,
    160 }
    161 
    162 impl fmt::Debug for RhiPreparedNostrDelivery {
    163     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    164         formatter.write_str("RhiPreparedNostrDelivery([redacted])")
    165     }
    166 }
    167 
    168 impl RhiNostrExactSink {
    169     fn prepare(
    170         &self,
    171         relay_id: &str,
    172         request_id: String,
    173         exact_event_bytes: &[u8],
    174         deadline_unix_ms: u64,
    175     ) -> Result<RhiPreparedNostrDelivery, RhiNostrAdapterError> {
    176         let binding = self
    177             .relays
    178             .get(relay_id)
    179             .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Target))?;
    180         let raw = core::str::from_utf8(exact_event_bytes)
    181             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Payload))?;
    182         let signed = Codec::decode_signed_event(raw)
    183             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Payload))?;
    184         if signed.raw_json().as_bytes() != exact_event_bytes {
    185             return Err(adapter_error(RhiNostrAdapterErrorKind::Payload));
    186         }
    187         let targets = TargetSet::new(vec![binding.target.clone()])
    188             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Target))?;
    189         let request = DeliveryRequest::new(
    190             request_id,
    191             DeliveryPayload::new(signed),
    192             targets,
    193             SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()),
    194             deadline_unix_ms,
    195         )
    196         .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Preparation))?;
    197         let prepared = binding
    198             .transport
    199             .prepare_delivery(request)
    200             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Preparation))?;
    201         Ok(RhiPreparedNostrDelivery {
    202             transport: binding.transport.clone(),
    203             prepared,
    204         })
    205     }
    206 
    207     async fn execute(
    208         &self,
    209         prepared: RhiPreparedNostrDelivery,
    210     ) -> Result<DeliveryOutcomeKind, RhiNostrAdapterError> {
    211         let RhiPreparedNostrDelivery {
    212             transport,
    213             prepared,
    214         } = prepared;
    215         let receipt = transport
    216             .execute_prepared_delivery(prepared)
    217             .await
    218             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Preparation))?;
    219         classify_receipt(receipt.target_receipts())
    220     }
    221 }
    222 
    223 impl RhiExactPublicationSink for RhiNostrExactSink {
    224     fn submit_exact<'a>(
    225         &'a self,
    226         attempt: &'a RhiPreparedPublicationAttempt,
    227     ) -> BoxFuture<'a, RhiPublicationAttemptOutcome> {
    228         Box::pin(async move {
    229             let request_id = request_id("publication", attempt.attempt_id().as_bytes());
    230             let prepared = match self.prepare(
    231                 attempt.relay_id(),
    232                 request_id,
    233                 attempt.exact_signed_event_bytes(),
    234                 attempt.deadline_at().get(),
    235             ) {
    236                 Ok(prepared) => prepared,
    237                 Err(_) => return RhiPublicationAttemptOutcome::Failed,
    238             };
    239             match self.execute(prepared).await {
    240                 Ok(DeliveryOutcomeKind::Accepted | DeliveryOutcomeKind::Delivered) => {
    241                     RhiPublicationAttemptOutcome::Accepted
    242                 }
    243                 Ok(DeliveryOutcomeKind::Rejected) => RhiPublicationAttemptOutcome::Rejected,
    244                 Ok(DeliveryOutcomeKind::Unavailable | DeliveryOutcomeKind::Failed) => {
    245                     RhiPublicationAttemptOutcome::Failed
    246                 }
    247                 Err(_) => RhiPublicationAttemptOutcome::Unknown,
    248             }
    249         })
    250     }
    251 }
    252 
    253 impl RhiExactPresenceSink for RhiNostrExactSink {
    254     fn submit_exact<'a>(
    255         &'a self,
    256         attempt: &'a RhiPreparedPresenceAttempt,
    257     ) -> BoxFuture<'a, RhiPresenceAttemptOutcome> {
    258         Box::pin(async move {
    259             let request_id = request_id("presence", attempt.attempt_id().as_bytes());
    260             let prepared = match self.prepare(
    261                 attempt.relay_id(),
    262                 request_id,
    263                 attempt.exact_signed_event_bytes(),
    264                 attempt.deadline_at().get(),
    265             ) {
    266                 Ok(prepared) => prepared,
    267                 Err(_) => return RhiPresenceAttemptOutcome::Failed,
    268             };
    269             match self.execute(prepared).await {
    270                 Ok(DeliveryOutcomeKind::Accepted | DeliveryOutcomeKind::Delivered) => {
    271                     RhiPresenceAttemptOutcome::Accepted
    272                 }
    273                 Ok(DeliveryOutcomeKind::Rejected) => RhiPresenceAttemptOutcome::Rejected,
    274                 Ok(DeliveryOutcomeKind::Unavailable | DeliveryOutcomeKind::Failed) => {
    275                     RhiPresenceAttemptOutcome::Failed
    276                 }
    277                 Err(_) => RhiPresenceAttemptOutcome::Unknown,
    278             }
    279         })
    280     }
    281 }
    282 
    283 fn classify_receipt(
    284     receipts: &[DeliveryTargetReceipt],
    285 ) -> Result<DeliveryOutcomeKind, RhiNostrAdapterError> {
    286     let [receipt] = receipts else {
    287         return Err(adapter_error(RhiNostrAdapterErrorKind::Preparation));
    288     };
    289     Ok(receipt.outcome().kind())
    290 }
    291 
    292 fn request_id(prefix: &str, bytes: &[u8; 32]) -> String {
    293     const HEX: &[u8; 16] = b"0123456789abcdef";
    294     let mut result = String::with_capacity(prefix.len() + 1 + bytes.len() * 2);
    295     result.push_str(prefix);
    296     result.push('-');
    297     for byte in bytes {
    298         result.push(char::from(HEX[usize::from(byte >> 4)]));
    299         result.push(char::from(HEX[usize::from(byte & 0x0f)]));
    300     }
    301     result
    302 }
    303 
    304 pub(crate) fn build_rhi_nostr_adapters(
    305     configuration: &RhiConfigDocumentV1,
    306 ) -> Result<(RhiTransportAdapters, Arc<RhiNostrExactSink>), RhiNostrAdapterError> {
    307     let relays = configuration
    308         .normalized()
    309         .pointer("/relays")
    310         .and_then(serde_json::Value::as_array)
    311         .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    312     let connect_timeout = configuration_integer(configuration, "/network/connect_deadline_ms")?;
    313     let source_timeout = configuration
    314         .normalized()
    315         .pointer("/evidence/sources")
    316         .and_then(serde_json::Value::as_array)
    317         .and_then(|sources| {
    318             sources
    319                 .iter()
    320                 .filter_map(|source| {
    321                     source
    322                         .pointer("/deadline_ms")
    323                         .and_then(serde_json::Value::as_u64)
    324                 })
    325                 .max()
    326         })
    327         .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    328     let publication_timeout = configuration
    329         .normalized()
    330         .pointer("/publication/retry/attempt_deadline_ms")
    331         .and_then(serde_json::Value::as_u64)
    332         .unwrap_or(source_timeout);
    333     let request_timeout = source_timeout.max(publication_timeout);
    334 
    335     let mut public = Vec::new();
    336     let mut simulator = Vec::new();
    337     let mut definitions = Vec::with_capacity(relays.len());
    338     for relay in relays {
    339         let id = relay
    340             .pointer("/id")
    341             .and_then(serde_json::Value::as_str)
    342             .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    343         let url = relay
    344             .pointer("/url")
    345             .and_then(serde_json::Value::as_str)
    346             .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    347         let read = relay
    348             .pointer("/read")
    349             .and_then(serde_json::Value::as_bool)
    350             .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    351         let write = relay
    352             .pointer("/write")
    353             .and_then(serde_json::Value::as_bool)
    354             .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    355         let access = if write {
    356             RelayAccess::ReadWrite
    357         } else if read {
    358             RelayAccess::ReadOnly
    359         } else {
    360             return Err(adapter_error(RhiNostrAdapterErrorKind::Configuration));
    361         };
    362         let (kind, policy) = if url.starts_with("wss://") {
    363             (RelayProfileKind::Public, RelayUrlPolicy::Public)
    364         } else if configuration.profile() == RhiConfigProfile::RepoLocal && url.starts_with("ws://")
    365         {
    366             (RelayProfileKind::Simulator, RelayUrlPolicy::Local)
    367         } else {
    368             return Err(adapter_error(RhiNostrAdapterErrorKind::Configuration));
    369         };
    370         let endpoint = RelayEndpoint::new(url, policy, access)
    371             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    372         match kind {
    373             RelayProfileKind::Public => public.push(endpoint),
    374             RelayProfileKind::Simulator => simulator.push(endpoint),
    375             _ => return Err(adapter_error(RhiNostrAdapterErrorKind::Configuration)),
    376         }
    377         definitions.push(RelayDefinition {
    378             id: Box::from(id),
    379             target: Target::nostr_relay(url)
    380                 .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Target))?,
    381             kind,
    382         });
    383     }
    384 
    385     let public_transport = build_transport(
    386         RelayProfileKind::Public,
    387         public,
    388         connect_timeout,
    389         request_timeout,
    390     )?;
    391     let simulator_transport = build_transport(
    392         RelayProfileKind::Simulator,
    393         simulator,
    394         connect_timeout,
    395         request_timeout,
    396     )?;
    397     let mut by_target = BTreeMap::new();
    398     let mut bindings = BTreeMap::new();
    399     for definition in definitions {
    400         let transport = match definition.kind {
    401             RelayProfileKind::Public => public_transport.clone(),
    402             RelayProfileKind::Simulator => simulator_transport.clone(),
    403             _ => None,
    404         }
    405         .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    406         if by_target
    407             .insert(definition.target.fingerprint().clone(), transport.clone())
    408             .is_some()
    409             || bindings
    410                 .insert(
    411                     definition.id,
    412                     RelayBinding {
    413                         transport,
    414                         target: definition.target,
    415                     },
    416                 )
    417                 .is_some()
    418         {
    419             return Err(adapter_error(RhiNostrAdapterErrorKind::Configuration));
    420         }
    421     }
    422     if by_target.is_empty() {
    423         return Err(adapter_error(RhiNostrAdapterErrorKind::Configuration));
    424     }
    425     let transport = Arc::new(RhiNostrTransport { by_target });
    426     let adapters = RhiTransportAdapters::new(
    427         transport.clone(),
    428         transport.clone(),
    429         transport as Arc<dyn EventSink>,
    430     );
    431     Ok((adapters, Arc::new(RhiNostrExactSink { relays: bindings })))
    432 }
    433 
    434 pub(crate) async fn probe_required_sources(
    435     configuration: &RhiConfigDocumentV1,
    436     deadline_unix_ms: u64,
    437 ) -> Result<(), RhiNostrAdapterError> {
    438     let relays = configuration
    439         .normalized()
    440         .pointer("/relays")
    441         .and_then(serde_json::Value::as_array)
    442         .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    443     let sources = configuration
    444         .normalized()
    445         .pointer("/evidence/sources")
    446         .and_then(serde_json::Value::as_array)
    447         .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    448     let connect_timeout = configuration_integer(configuration, "/network/connect_deadline_ms")?;
    449     let request_timeout = sources
    450         .iter()
    451         .filter_map(|source| {
    452             source
    453                 .pointer("/deadline_ms")
    454                 .and_then(serde_json::Value::as_u64)
    455         })
    456         .max()
    457         .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    458 
    459     let mut public = Vec::new();
    460     let mut public_targets = Vec::new();
    461     let mut simulator = Vec::new();
    462     let mut simulator_targets = Vec::new();
    463     for source in sources.iter().filter(|source| {
    464         source
    465             .pointer("/required")
    466             .and_then(serde_json::Value::as_bool)
    467             == Some(true)
    468     }) {
    469         let relay_id = source
    470             .pointer("/relay_id")
    471             .and_then(serde_json::Value::as_str)
    472             .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    473         let relay = relays
    474             .iter()
    475             .find(|relay| {
    476                 relay.pointer("/id").and_then(serde_json::Value::as_str) == Some(relay_id)
    477             })
    478             .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    479         let url = relay
    480             .pointer("/url")
    481             .and_then(serde_json::Value::as_str)
    482             .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    483         let target = Target::nostr_relay(url)
    484             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Target))?;
    485         let (kind, policy) = if url.starts_with("wss://") {
    486             (RelayProfileKind::Public, RelayUrlPolicy::Public)
    487         } else if configuration.profile() == RhiConfigProfile::RepoLocal && url.starts_with("ws://")
    488         {
    489             (RelayProfileKind::Simulator, RelayUrlPolicy::Local)
    490         } else {
    491             return Err(adapter_error(RhiNostrAdapterErrorKind::Configuration));
    492         };
    493         let endpoint = RelayEndpoint::new(url, policy, RelayAccess::ReadOnly)
    494             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    495         match kind {
    496             RelayProfileKind::Public => {
    497                 public.push(endpoint);
    498                 public_targets.push(target);
    499             }
    500             RelayProfileKind::Simulator => {
    501                 simulator.push(endpoint);
    502                 simulator_targets.push(target);
    503             }
    504             _ => return Err(adapter_error(RhiNostrAdapterErrorKind::Configuration)),
    505         }
    506     }
    507     if public_targets.is_empty() && simulator_targets.is_empty() {
    508         return Err(adapter_error(RhiNostrAdapterErrorKind::Configuration));
    509     }
    510     probe_group(
    511         RelayProfileKind::Public,
    512         public,
    513         public_targets,
    514         connect_timeout,
    515         request_timeout,
    516         deadline_unix_ms,
    517         "rhi-doctor-public",
    518     )
    519     .await?;
    520     probe_group(
    521         RelayProfileKind::Simulator,
    522         simulator,
    523         simulator_targets,
    524         connect_timeout,
    525         request_timeout,
    526         deadline_unix_ms,
    527         "rhi-doctor-simulator",
    528     )
    529     .await
    530 }
    531 
    532 async fn probe_group(
    533     kind: RelayProfileKind,
    534     endpoints: Vec<RelayEndpoint>,
    535     targets: Vec<Target>,
    536     connect_timeout: u64,
    537     request_timeout: u64,
    538     deadline_unix_ms: u64,
    539     request_id: &'static str,
    540 ) -> Result<(), RhiNostrAdapterError> {
    541     if targets.is_empty() {
    542         return Ok(());
    543     }
    544     let transport = build_transport(kind, endpoints, connect_timeout, request_timeout)?
    545         .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    546     let target_set =
    547         TargetSet::new(targets).map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Target))?;
    548     let selector = FetchSelector::all()
    549         .with_since_unix_seconds(u64::MAX)
    550         .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    551     let request = FetchRequest::new(
    552         request_id,
    553         target_set,
    554         FetchBounds::new(1, deadline_unix_ms)
    555             .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Configuration))?,
    556     )
    557     .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Configuration))?
    558     .with_selector(selector);
    559     let page = transport
    560         .fetch(request)
    561         .await
    562         .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Preparation))?;
    563     if page.target_outcomes().is_empty()
    564         || page.target_outcomes().iter().any(|outcome| {
    565             !matches!(
    566                 outcome.state(),
    567                 FetchTargetState::Complete | FetchTargetState::Partial
    568             )
    569         })
    570     {
    571         return Err(adapter_error(RhiNostrAdapterErrorKind::Preparation));
    572     }
    573     Ok(())
    574 }
    575 
    576 fn build_transport(
    577     kind: RelayProfileKind,
    578     endpoints: Vec<RelayEndpoint>,
    579     connect_timeout: u64,
    580     request_timeout: u64,
    581 ) -> Result<Option<NostrTransport>, RhiNostrAdapterError> {
    582     if endpoints.is_empty() {
    583         return Ok(None);
    584     }
    585     let maximum_connections = endpoints.len().min(8);
    586     let profile = RelayProfile::explicit(kind, endpoints)
    587         .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    588     let config = Config::from_profile(profile)
    589         .with_timeouts(connect_timeout, request_timeout, connect_timeout)
    590         .and_then(|config| config.with_max_connections(maximum_connections))
    591         .map_err(|_| adapter_error(RhiNostrAdapterErrorKind::Configuration))?;
    592     Ok(Some(NostrTransport::new(config)))
    593 }
    594 
    595 fn configuration_integer(
    596     configuration: &RhiConfigDocumentV1,
    597     pointer: &str,
    598 ) -> Result<u64, RhiNostrAdapterError> {
    599     configuration
    600         .normalized()
    601         .pointer(pointer)
    602         .and_then(serde_json::Value::as_u64)
    603         .ok_or_else(|| adapter_error(RhiNostrAdapterErrorKind::Configuration))
    604 }
    605 
    606 #[cfg(test)]
    607 mod tests {
    608     use std::error::Error as _;
    609 
    610     use super::*;
    611     use crate::{RhiConfigProfile, parse_rhi_config_v1};
    612 
    613     const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
    614 
    615     #[test]
    616     fn exact_configuration_builds_without_network_io_and_errors_are_redacted() {
    617         let configuration = parse_rhi_config_v1(CONFIG.as_bytes(), RhiConfigProfile::Production)
    618             .expect("configuration");
    619         let (_adapters, sink) =
    620             build_rhi_nostr_adapters(&configuration).expect("offline composition");
    621         assert_eq!(sink.relays.len(), 2);
    622         assert_eq!(request_id("publication", &[0xab; 32]).len(), 76);
    623         for kind in [
    624             RhiNostrAdapterErrorKind::Configuration,
    625             RhiNostrAdapterErrorKind::Target,
    626             RhiNostrAdapterErrorKind::Payload,
    627             RhiNostrAdapterErrorKind::Preparation,
    628         ] {
    629             let error = adapter_error(kind);
    630             assert_eq!(error.kind(), kind);
    631             assert!(error.source().is_none());
    632             assert!(!format!("{error} {error:?}").contains("relay.example"));
    633         }
    634     }
    635 }