sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 0d21ded103d6bccf8087fc27b824d2cc758c4372
parent f987cde525c7752152579bbed03a818d4658e171
Author: triesap <tyson@radroots.org>
Date:   Fri, 10 Jul 2026 09:41:55 +0000

sdk: preserve required target proxy semantics

- send exact required target fingerprints to radrootsd proxy publish
- remove public relay-shaped Nostr adapter aliases
- replace TargetSet::new with explicit Nostr target construction
- add source-boundary guards for removed SDK transport APIs

Diffstat:
Mcrates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs | 2+-
Mcrates/sdk/src/adapters/nostr.rs | 32+++++++++++++-------------------
Mcrates/sdk/src/sync_runtime.rs | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mcrates/sdk/src/transport.rs | 16++++------------
Mcrates/sdk/tests/farms_runtime.rs | 2+-
Mcrates/sdk/tests/listings_runtime.rs | 2+-
Mcrates/sdk/tests/orders_runtime.rs | 4++--
Mcrates/sdk/tests/runtime_foundation.rs | 12++++++------
Mcrates/sdk/tests/source_boundary.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/trade_product_publish_runtime.rs | 2+-
Mcrates/sdk/tests/unit/adapters_nostr_tests.rs | 6++++--
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/sdk/tests/unit/transport_tests.rs | 14+++++++-------
13 files changed, 269 insertions(+), 74 deletions(-)

diff --git a/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs b/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs @@ -28,7 +28,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { .build() .await?; let actor = RadrootsActorContext::test(seller.as_str(), [RadrootsActorRole::Seller])?; - let targets = TargetSet::new([LOCAL_RELAY], NostrRelayUrlPolicy::Localhost)?; + let targets = TargetSet::nostr_relays([LOCAL_RELAY], NostrRelayUrlPolicy::Localhost)?; let target_policy = TargetPolicy::explicit(targets); let prepared = sdk diff --git a/crates/sdk/src/adapters/nostr.rs b/crates/sdk/src/adapters/nostr.rs @@ -7,31 +7,25 @@ use radroots_nostr::prelude::{ RadrootsNostrOutput, }; -pub type RelayClient = RadrootsNostrClient; -pub type RelayClientOptions = RadrootsNostrClientOptions; -pub type RelayError = RadrootsNostrError; -pub type RelayEventId = RadrootsNostrEventId; -pub type RelayOutput<T> = RadrootsNostrOutput<T>; - -pub fn signerless_client() -> RelayClient { - RelayClient::new_signerless() +pub fn signerless_client() -> RadrootsNostrClient { + RadrootsNostrClient::new_signerless() } pub fn signerless_client_with_options( - options: RelayClientOptions, -) -> Result<RelayClient, RelayError> { - RelayClient::new_signerless_with_options(options) + options: RadrootsNostrClientOptions, +) -> Result<RadrootsNostrClient, RadrootsNostrError> { + RadrootsNostrClient::new_signerless_with_options(options) } -pub fn client_from_identity(identity: &RadrootsIdentity) -> RelayClient { - RelayClient::from_identity(identity) +pub fn client_from_identity(identity: &RadrootsIdentity) -> RadrootsNostrClient { + RadrootsNostrClient::from_identity(identity) } pub async fn configure_write_relays( - client: &RelayClient, + client: &RadrootsNostrClient, relay_urls: &[String], connect_timeout: Duration, -) -> Result<(), RelayError> { +) -> Result<(), RadrootsNostrError> { for relay_url in relay_urls { client.add_write_relay(relay_url).await?; } @@ -44,13 +38,13 @@ pub async fn connected_client_from_identity( identity: &RadrootsIdentity, relay_urls: &[String], connect_timeout: Duration, -) -> Result<RelayClient, RelayError> { +) -> Result<RadrootsNostrClient, RadrootsNostrError> { let client = client_from_identity(identity); configure_write_relays(&client, relay_urls, connect_timeout).await?; Ok(client) } -pub async fn connected_relay_urls(client: &RelayClient) -> Vec<String> { +pub async fn connected_relay_urls(client: &RadrootsNostrClient) -> Vec<String> { let mut relay_urls = client .relays() .await @@ -63,9 +57,9 @@ pub async fn connected_relay_urls(client: &RelayClient) -> Vec<String> { } pub async fn publish_signed_event( - client: &RelayClient, + client: &RadrootsNostrClient, event: &SignedNostrEvent, -) -> Result<RelayOutput<RelayEventId>, RelayError> { +) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> { client.send_event(event).await } diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -31,6 +31,8 @@ use radroots_trade::projection::{ RadrootsProjectionRefreshReceipt, RadrootsProjectionRefreshRequest, refresh_product_projections, }; +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +use radroots_transport::RadrootsTransportTargetFingerprint; #[cfg(feature = "runtime")] use radroots_transport::{ RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportImplementationState, @@ -1193,28 +1195,36 @@ async fn proxy_delivery_policy( .iter() .filter(|target| target.delivery_plan_id == active_delivery_plan_id) .collect::<Vec<_>>(); - let satisfied_count = plan - .satisfaction_policy - .target_satisfaction_class() - .map(|satisfaction_class| { - active_targets - .iter() - .filter(|target| { - target - .status - .counts_as_transport_satisfaction(satisfaction_class) - }) - .count() - }) - .unwrap_or(0); + reject_delivered_proxy_satisfaction(&plan.satisfaction_policy)?; let ready_target_count = active_targets .iter() .filter(|target| target.status.is_ready_for_attempt()) .count(); - let required_remaining = (plan.required_success_count as usize).saturating_sub(satisfied_count); + let required_remaining_targets = + proxy_required_remaining_targets(&plan.satisfaction_policy, &active_targets)?; + let required_remaining = if let Some(targets) = required_remaining_targets.as_ref() { + targets.len() + } else { + let satisfied_count = plan + .satisfaction_policy + .target_satisfaction_class() + .map(|satisfaction_class| { + active_targets + .iter() + .filter(|target| { + target + .status + .counts_as_transport_satisfaction(satisfaction_class) + }) + .count() + }) + .unwrap_or(0); + (plan.required_success_count as usize).saturating_sub(satisfied_count) + }; proxy_delivery_policy_from_remaining( ready_target_count, required_remaining, + required_remaining_targets.as_deref(), &plan.satisfaction_policy, ) } @@ -1223,10 +1233,24 @@ async fn proxy_delivery_policy( fn proxy_delivery_policy_from_remaining( ready_target_count: usize, required_remaining: usize, + required_remaining_targets: Option<&[RadrootsTransportTargetFingerprint]>, satisfaction_policy: &RadrootsTransportSatisfactionPolicy, ) -> Result<TransportPublishDeliveryPolicy, RadrootsSdkError> { reject_delivered_proxy_satisfaction(satisfaction_policy)?; - if ready_target_count == 0 || required_remaining == 0 { + if required_remaining == 0 { + return Ok(TransportPublishDeliveryPolicy::Any); + } + if ready_target_count == 0 { + if matches!( + satisfaction_policy, + RadrootsTransportSatisfactionPolicy::RequiredTargets { .. } + ) { + return Err(RadrootsSdkError::InvalidRequest { + message: + "radrootsd proxy publish has unsatisfied required targets but no ready target" + .to_owned(), + }); + } return Ok(TransportPublishDeliveryPolicy::Any); } Ok(match satisfaction_policy { @@ -1234,7 +1258,17 @@ fn proxy_delivery_policy_from_remaining( RadrootsTransportSatisfactionPolicy::Any { .. } => TransportPublishDeliveryPolicy::Any, RadrootsTransportSatisfactionPolicy::All { .. } => TransportPublishDeliveryPolicy::All, RadrootsTransportSatisfactionPolicy::RequiredTargets { .. } => { - TransportPublishDeliveryPolicy::All + TransportPublishDeliveryPolicy::required_targets( + required_remaining_targets + .ok_or_else(|| RadrootsSdkError::InvalidRequest { + message: "radrootsd proxy publish missing required target fingerprints" + .to_owned(), + })? + .to_vec(), + ) + .map_err(|error| RadrootsSdkError::InvalidRequest { + message: error.to_string(), + })? } RadrootsTransportSatisfactionPolicy::Quorum { .. } => { if required_remaining >= ready_target_count { @@ -1251,6 +1285,41 @@ fn proxy_delivery_policy_from_remaining( } #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +fn proxy_required_remaining_targets( + satisfaction_policy: &RadrootsTransportSatisfactionPolicy, + active_targets: &[&RadrootsOutboxDeliveryTargetRecord], +) -> Result<Option<Vec<RadrootsTransportTargetFingerprint>>, RadrootsSdkError> { + let RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } = + satisfaction_policy + else { + return Ok(None); + }; + let mut remaining = Vec::new(); + for required in targets { + let target = active_targets + .iter() + .find(|target| target.endpoint_fingerprint == *required) + .ok_or_else(|| RadrootsSdkError::InvalidRequest { + message: format!( + "radrootsd proxy publish required target {required} is not present in active delivery plan" + ), + })?; + if target.status.counts_as_transport_satisfaction(*class) { + continue; + } + if !target.status.is_ready_for_attempt() { + return Err(RadrootsSdkError::InvalidRequest { + message: format!( + "radrootsd proxy publish required target {required} is not ready for publish" + ), + }); + } + remaining.push(required.clone()); + } + Ok(Some(remaining)) +} + +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] fn reject_delivered_proxy_satisfaction( satisfaction_policy: &RadrootsTransportSatisfactionPolicy, ) -> Result<(), RadrootsSdkError> { diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -268,7 +268,10 @@ pub struct TargetSet { } impl TargetSet { - pub fn new<I, S>(relays: I, policy: NostrRelayUrlPolicy) -> Result<Self, RadrootsSdkError> + pub fn nostr_relays<I, S>( + relays: I, + policy: NostrRelayUrlPolicy, + ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, @@ -284,17 +287,6 @@ impl TargetSet { Self::from_transport_targets(targets) } - pub fn nostr_relays<I, S>( - relays: I, - policy: NostrRelayUrlPolicy, - ) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - Self::new(relays, policy) - } - pub fn transport_targets( targets: Vec<RadrootsTransportTarget>, ) -> Result<Self, RadrootsSdkError> { diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs @@ -748,7 +748,7 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() { farmer_actor(), farm(FARM_D_D_TAG, "North Farm"), TargetPolicy::explicit( - TargetSet::new([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) + TargetSet::nostr_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("second transport targets"), ), ); diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -773,7 +773,7 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() { actor(), listing(LISTING_F_D_TAG, "Coffee"), TargetPolicy::explicit( - TargetSet::new([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) + TargetSet::nostr_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("second transport targets"), ), ); diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs @@ -564,7 +564,7 @@ fn listing_event_ptr() -> RadrootsNostrEventPtr { fn explicit_trade_relays() -> TargetPolicy { TargetPolicy::explicit( - TargetSet::new([RELAY], NostrRelayUrlPolicy::Public).expect("transport targets"), + TargetSet::nostr_relays([RELAY], NostrRelayUrlPolicy::Public).expect("transport targets"), ) } @@ -3393,7 +3393,7 @@ async fn order_submit_enqueue_derives_order_independent_idempotency_key() { listing_event_ptr(), order_request("order-submit-idempotent"), TargetPolicy::explicit( - TargetSet::new([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) + TargetSet::nostr_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("second transport targets"), ), PublishMode::EnqueueOnly, diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs @@ -79,7 +79,7 @@ async fn sdk_builder_rejects_ws_relay_without_localhost_policy() { #[test] fn invalid_relay_url_errors_redact_userinfo() { - let error = TargetSet::new( + let error = TargetSet::nostr_relays( ["wss://user:password@relay.example.com/path?token=secret#frag"], NostrRelayUrlPolicy::Public, ) @@ -408,7 +408,7 @@ fn sdk_error_contract_methods_cover_all_variants() { vec![RadrootsSdkRecoveryAction::ConfigureTransportTargets], ), ( - TargetSet::new(["wss://u:p@relay.example.com"], NostrRelayUrlPolicy::Public) + TargetSet::nostr_relays(["wss://u:p@relay.example.com"], NostrRelayUrlPolicy::Public) .expect_err("invalid relay"), "invalid_relay_url", RadrootsSdkErrorClass::Configuration, @@ -622,7 +622,7 @@ fn sdk_error_contract_methods_cover_all_variants() { #[test] fn relay_target_set_validates_normalizes_preserves_order_and_caps() { - let targets = TargetSet::new( + let targets = TargetSet::nostr_relays( [" wss://relay-b.example.com/ ", "wss://relay-a.example.com"], NostrRelayUrlPolicy::Public, ) @@ -671,7 +671,7 @@ fn relay_target_set_validates_normalizes_preserves_order_and_caps() { ); assert!(matches!( - TargetSet::new( + TargetSet::nostr_relays( ["wss://relay-a.example.com", "WSS://RELAY-A.EXAMPLE.COM/"], NostrRelayUrlPolicy::Public, ), @@ -680,7 +680,7 @@ fn relay_target_set_validates_normalizes_preserves_order_and_caps() { )); assert!(matches!( - TargetSet::new(Vec::<String>::new(), NostrRelayUrlPolicy::Public), + TargetSet::nostr_relays(Vec::<String>::new(), NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::EmptyTransportTargets { .. }) )); @@ -688,7 +688,7 @@ fn relay_target_set_validates_normalizes_preserves_order_and_caps() { .map(|index| format!("wss://relay-{index}.example.com")) .collect::<Vec<_>>(); assert!(matches!( - TargetSet::new(too_many, NostrRelayUrlPolicy::Public), + TargetSet::nostr_relays(too_many, NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::TransportTargetLimitExceeded { max: SDK_TRANSPORT_TARGET_MAX_COUNT, actual diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -581,6 +581,90 @@ fn sdk_transport_nostr_features_do_not_retain_relay_named_aliases() { } #[test] +fn sdk_transport_public_api_rejects_removed_relay_aliases_and_neutral_target_constructor() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let nostr_adapter = read_source(manifest_dir.join("src/adapters/nostr.rs").as_path()); + let transport_source = read_source(manifest_dir.join("src/transport.rs").as_path()); + let target_set_impl = source_between( + transport_source.as_str(), + "impl TargetSet {", + "\n}\n\nimpl serde::Serialize for TargetSet", + ); + + for required in [ + "RadrootsNostrClient", + "RadrootsNostrClientOptions", + "RadrootsNostrError", + "RadrootsNostrEventId", + "RadrootsNostrOutput", + "pub fn nostr_relays", + "pub fn transport_targets", + ] { + assert!( + nostr_adapter.contains(required) || transport_source.contains(required), + "SDK public transport API must retain explicit target-state witness `{required}`" + ); + } + + for forbidden in [ + "pub type RelayClient", + "pub type RelayClientOptions", + "pub type RelayError", + "pub type RelayEventId", + "pub type RelayOutput", + "RelayClient::", + "RelayOutput<", + "pub fn new<I, S>", + ] { + assert!( + !nostr_adapter.contains(forbidden) && !target_set_impl.contains(forbidden), + "SDK transport public API must not retain removed alias or constructor `{forbidden}`" + ); + } + + for relative_root in ["src", "tests", "examples"] { + let root = manifest_dir.join(relative_root); + if !root.exists() { + continue; + } + for path in rust_source_files(root.as_path()) { + if path.file_name().and_then(|file_name| file_name.to_str()) + == Some("source_boundary.rs") + { + continue; + } + let source = read_source(path.as_path()); + let relative_path = relative_manifest_path(manifest_dir, path.as_path()); + assert!( + !contains_sdk_target_set_new(source.as_str()), + "{relative_path} must use TargetSet::nostr_relays or TargetSet::transport_targets explicitly" + ); + for forbidden in [ + "RelayClient", + "RelayClientOptions", + "RelayError", + "RelayEventId", + "RelayOutput", + ] { + assert!( + !source.contains(forbidden), + "{relative_path} must not retain removed SDK Nostr adapter alias `{forbidden}`" + ); + } + } + } +} + +fn contains_sdk_target_set_new(source: &str) -> bool { + source.match_indices("TargetSet::new(").any(|(index, _)| { + source[..index] + .chars() + .next_back() + .is_none_or(|character| !is_rust_identifier_character(character)) + }) +} + +#[test] fn sdk_readme_documents_current_public_product_surface() { let readme_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("README"); let readme = read_source(readme_path.as_path()); diff --git a/crates/sdk/tests/trade_product_publish_runtime.rs b/crates/sdk/tests/trade_product_publish_runtime.rs @@ -226,7 +226,7 @@ fn trade_propose_request( fn explicit_trade_relays() -> TargetPolicy { TargetPolicy::explicit( - TargetSet::new([RELAY], NostrRelayUrlPolicy::Public).expect("target relays"), + TargetSet::nostr_relays([RELAY], NostrRelayUrlPolicy::Public).expect("target relays"), ) } diff --git a/crates/sdk/tests/unit/adapters_nostr_tests.rs b/crates/sdk/tests/unit/adapters_nostr_tests.rs @@ -6,6 +6,7 @@ use crate::adapters::signing::sign_parts_with_identity; use crate::identity::RadrootsIdentity; use core::time::Duration; use radroots_events_codec::wire::WireEventParts; +use radroots_nostr::prelude::RadrootsNostrClientOptions; use tokio::runtime::Runtime; #[test] @@ -13,8 +14,9 @@ fn client_constructors_build_without_runtime_net() { let identity = RadrootsIdentity::generate(); let _client = client_from_identity(&identity); let _signerless = signerless_client(); - let _signerless_with_options = signerless_client_with_options(super::RelayClientOptions::new()) - .expect("signerless client with options"); + let _signerless_with_options = + signerless_client_with_options(RadrootsNostrClientOptions::new()) + .expect("signerless client with options"); } #[test] diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs @@ -1,8 +1,8 @@ #[cfg(feature = "radrootsd-proxy")] use super::{ CLAIM_OWNER, complete_proxy_publish_attempt, proxy_delivery_policy_from_remaining, - proxy_error_message, proxy_outbox_idempotency_key, proxy_transport_error_receipt, - push_proxy_claimed_outbox_event, push_proxy_event_receipt, + proxy_error_message, proxy_outbox_idempotency_key, proxy_required_remaining_targets, + proxy_transport_error_receipt, push_proxy_claimed_outbox_event, push_proxy_event_receipt, transport_publish_target_from_outbox_target, }; use super::{ @@ -48,7 +48,8 @@ use radroots_outbox::{ #[cfg(feature = "radrootsd-proxy")] use radroots_transport::{ RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RadrootsTransportKind, RadrootsTransportMeshScopeId, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetLabel, + RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, + RadrootsTransportTarget, RadrootsTransportTargetLabel, }; use radroots_transport_nostr::{ RadrootsOutboxPublishReceipt, RadrootsOutboxPublishTargetReceipt, RadrootsRelayOutcomeKind, @@ -812,14 +813,20 @@ async fn proxy_push_empty_queue_and_private_helpers_are_deterministic() { assert_eq!(receipt.attempted_events, 0); assert_eq!( - proxy_delivery_policy_from_remaining(0, 0, &RadrootsTransportSatisfactionPolicy::no_wait()) - .expect("no-wait proxy policy"), + proxy_delivery_policy_from_remaining( + 0, + 0, + None, + &RadrootsTransportSatisfactionPolicy::no_wait() + ) + .expect("no-wait proxy policy"), TransportPublishDeliveryPolicy::Any ); assert_eq!( proxy_delivery_policy_from_remaining( 0, 0, + None, &RadrootsTransportSatisfactionPolicy::all_accepted() ) .expect("zero-target proxy policy"), @@ -829,6 +836,7 @@ async fn proxy_push_empty_queue_and_private_helpers_are_deterministic() { proxy_delivery_policy_from_remaining( 2, 2, + None, &RadrootsTransportSatisfactionPolicy::all_accepted() ) .expect("all-target proxy policy"), @@ -838,11 +846,49 @@ async fn proxy_push_empty_queue_and_private_helpers_are_deterministic() { proxy_delivery_policy_from_remaining( 2, 1, + None, &RadrootsTransportSatisfactionPolicy::any_accepted() ) .expect("any-target proxy policy"), TransportPublishDeliveryPolicy::Any ); + let first_required = + RadrootsTransportTarget::new(RadrootsTransportKind::Nostr, "wss://required-a.example.com") + .expect("first required target"); + let second_required = + RadrootsTransportTarget::new(RadrootsTransportKind::Nostr, "wss://required-b.example.com") + .expect("second required target"); + let optional = + RadrootsTransportTarget::new(RadrootsTransportKind::Nostr, "wss://optional.example.com") + .expect("optional target"); + let policy = RadrootsTransportSatisfactionPolicy::required_targets( + RadrootsTransportSatisfactionClass::Accepted, + vec![ + first_required.fingerprint.clone(), + second_required.fingerprint.clone(), + ], + ) + .expect("required target policy"); + let mut first_record = delivery_target_record(1, 7, &first_required); + first_record.status = RadrootsOutboxDeliveryTargetStatus::Accepted; + let second_record = delivery_target_record(2, 7, &second_required); + let mut optional_record = delivery_target_record(3, 7, &optional); + optional_record.status = RadrootsOutboxDeliveryTargetStatus::Accepted; + let active_targets = vec![&first_record, &second_record, &optional_record]; + let remaining = proxy_required_remaining_targets(&policy, &active_targets) + .expect("required remaining targets") + .expect("required target policy"); + assert_eq!(remaining, vec![second_required.fingerprint]); + assert_eq!( + proxy_delivery_policy_from_remaining(2, remaining.len(), Some(&remaining), &policy) + .expect("required target proxy policy"), + TransportPublishDeliveryPolicy::RequiredTargets { targets: remaining } + ); + assert!(matches!( + proxy_delivery_policy_from_remaining(0, 1, Some(&[]), &policy), + Err(RadrootsSdkError::InvalidRequest { message }) + if message.contains("unsatisfied required targets") + )); assert_eq!( proxy_outbox_idempotency_key(7, 3, "event-id", 5), "radroots-sdk-outbox-7-3-event-id-5" @@ -1699,6 +1745,14 @@ impl OutboxPublishReceiptFixture for RadrootsOutboxPublishReceipt { .push(RadrootsOutboxPublishTargetReceipt { delivery_target_id: 10, endpoint_uri: "wss://relay.example.com".to_owned(), + endpoint_fingerprint: RadrootsTransportTarget::new_with_metadata( + RadrootsTransportKind::Nostr, + "wss://relay.example.com", + Some(RadrootsTransportMeshScopeId::parse("farm.local").expect("scope")), + Some(RadrootsTransportTargetLabel::parse("Farm relay").expect("label")), + ) + .expect("target") + .fingerprint, target_scope: Some("farm.local".to_owned()), target_label: Some("Farm relay".to_owned()), attempted: true, diff --git a/crates/sdk/tests/unit/transport_tests.rs b/crates/sdk/tests/unit/transport_tests.rs @@ -121,7 +121,7 @@ fn target_set_accessors_and_configured_relays_cover_empty_paths() { .is_empty() ); - let targets = TargetSet::new( + let targets = TargetSet::nostr_relays( ["wss://relay-a.example.com", "wss://relay-b.example.com"], NostrRelayUrlPolicy::Public, ) @@ -158,7 +158,7 @@ fn target_set_accessors_and_configured_relays_cover_empty_paths() { ) .expect("explicit policy"), TargetPolicy::Explicit( - TargetSet::new(["wss://relay-c.example.com"], NostrRelayUrlPolicy::Public) + TargetSet::nostr_relays(["wss://relay-c.example.com"], NostrRelayUrlPolicy::Public) .expect("target set"), ) ); @@ -196,7 +196,7 @@ fn target_set_accessors_and_configured_relays_cover_empty_paths() { #[test] fn target_sets_reject_duplicate_transport_fingerprints() { - let duplicate_relays = TargetSet::new( + let duplicate_relays = TargetSet::nostr_relays( [ "wss://relay-a.example.com/path", "WSS://RELAY-A.EXAMPLE.COM/path", @@ -369,7 +369,7 @@ fn explicit_target_sets_reject_mixed_proxy_delegate_targets() { #[test] fn normalized_relays_reject_empty_and_over_limit_sets() { assert!(matches!( - TargetSet::new(Vec::<String>::new(), NostrRelayUrlPolicy::Public), + TargetSet::nostr_relays(Vec::<String>::new(), NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::EmptyTransportTargets { .. }) )); @@ -377,7 +377,7 @@ fn normalized_relays_reject_empty_and_over_limit_sets() { .map(|index| format!("wss://relay-{index}.example.com")) .collect::<Vec<_>>(); assert!(matches!( - TargetSet::new(too_many, NostrRelayUrlPolicy::Public), + TargetSet::nostr_relays(too_many, NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::TransportTargetLimitExceeded { actual, .. }) if actual == SDK_TRANSPORT_TARGET_MAX_COUNT + 1 )); @@ -391,12 +391,12 @@ fn local_ws_authority_parser_handles_ipv6_ports_and_non_ws_values() { assert!(!is_local_ws_relay("wss://relay.example.com")); assert!(!is_local_ws_relay("ws://relay.example.com")); assert!(matches!( - TargetSet::new(["ws://relay.example.com"], NostrRelayUrlPolicy::Localhost), + TargetSet::nostr_relays(["ws://relay.example.com"], NostrRelayUrlPolicy::Localhost), Err(RadrootsSdkError::InvalidRelayUrl { reason, .. }) if reason.contains("localhost") )); assert!(matches!( - TargetSet::new(["ws://relay.example.com"], NostrRelayUrlPolicy::Public), + TargetSet::nostr_relays(["ws://relay.example.com"], NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::InvalidRelayUrl { reason, .. }) if reason.contains("localhost") ));