sdk

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

commit 2e45a4bd5bc70263cf3bfbce73345452b5c292d8
parent 630651c55900455d1c8acd3aa671dd919bfb45f8
Author: triesap <tyson@radroots.org>
Date:   Wed,  8 Jul 2026 05:01:24 +0000

transport: reject duplicate SDK targets

- stop deduping normalized Nostr relay inputs in target sets
- reject duplicate explicit transport targets through the shared target-set error
- update product runtime idempotency tests to use unique relay sets
- cover duplicate target failures and unique relay normalization

Diffstat:
Mcrates/sdk/src/transport.rs | 12+++++-------
Mcrates/sdk/tests/farms_runtime.rs | 2+-
Mcrates/sdk/tests/listings_runtime.rs | 2+-
Mcrates/sdk/tests/orders_runtime.rs | 2+-
Mcrates/sdk/tests/runtime_foundation.rs | 23++++++++++++-----------
Mcrates/sdk/tests/unit/transport_tests.rs | 45+++++++++++++++++++++++++++++++++++++++------
6 files changed, 59 insertions(+), 27 deletions(-)

diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -132,15 +132,12 @@ impl TargetSet { S: AsRef<str>, { let mut targets = Vec::new(); - let mut seen = BTreeSet::new(); for relay in relays { let normalized = normalized_nostr_relay_url(relay.as_ref(), policy)?; - if seen.insert(normalized.clone()) { - targets.push(RadrootsTransportTarget::new( - RadrootsTransportKind::Nostr, - normalized, - )?); - } + targets.push(RadrootsTransportTarget::new( + RadrootsTransportKind::Nostr, + normalized, + )?); } Self::from_transport_targets(targets) } @@ -229,6 +226,7 @@ impl TargetSet { }); } } + RadrootsTransportTargetSet::new(targets.clone())?; let canonical_targets = targets .iter() .map(|target| { diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs @@ -723,7 +723,7 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() { farm(FARM_D_D_TAG, "North Farm"), TargetPolicy::use_transport_profile(), ) - .try_with_nostr_targets([RELAY_B, RELAY, RELAY], NostrRelayUrlPolicy::Public) + .try_with_nostr_targets([RELAY_B, RELAY], NostrRelayUrlPolicy::Public) .expect("first transport targets"); let second = FarmEnqueuePublishRequest::new( farmer_actor(), diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -743,7 +743,7 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() { listing(LISTING_F_D_TAG, "Coffee"), TargetPolicy::use_transport_profile(), ) - .try_with_nostr_targets([RELAY_B, RELAY, RELAY], NostrRelayUrlPolicy::Public) + .try_with_nostr_targets([RELAY_B, RELAY], NostrRelayUrlPolicy::Public) .expect("first transport targets"); let second = ListingEnqueuePublishRequest::new( actor(), diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs @@ -3386,7 +3386,7 @@ async fn order_submit_enqueue_derives_order_independent_idempotency_key() { PublishMode::EnqueueOnly, SatisfactionPolicy::NoWait, ) - .try_with_nostr_targets([RELAY_B, RELAY, RELAY], NostrRelayUrlPolicy::Public) + .try_with_nostr_targets([RELAY_B, RELAY], NostrRelayUrlPolicy::Public) .expect("first transport targets"); let second = TradeSubmitEnqueueRequest::new( buyer_actor(), diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs @@ -48,11 +48,7 @@ async fn sdk_builder_validates_configured_relay_targets() { let sdk = RadrootsClient::builder() .transport_profile( nostr_profile( - [ - " wss://relay-b.example.com/ ", - "wss://relay-a.example.com", - "wss://relay-a.example.com", - ], + [" wss://relay-b.example.com/ ", "wss://relay-a.example.com"], NostrRelayUrlPolicy::Public, ) .expect("profile"), @@ -625,13 +621,9 @@ fn sdk_error_contract_methods_cover_all_variants() { } #[test] -fn relay_target_set_validates_normalizes_dedupes_preserves_order_and_caps() { +fn relay_target_set_validates_normalizes_preserves_order_and_caps() { let targets = TargetSet::new( - [ - " wss://relay-b.example.com/ ", - "wss://relay-a.example.com", - "wss://relay-a.example.com", - ], + [" wss://relay-b.example.com/ ", "wss://relay-a.example.com"], NostrRelayUrlPolicy::Public, ) .expect("targets"); @@ -675,6 +667,15 @@ fn relay_target_set_validates_normalizes_dedupes_preserves_order_and_caps() { ); assert!(matches!( + TargetSet::new( + ["wss://relay-a.example.com", "WSS://RELAY-A.EXAMPLE.COM/"], + NostrRelayUrlPolicy::Public, + ), + Err(RadrootsSdkError::Transport { ref message }) + if message == "transport target set contains duplicate fingerprints" + )); + + assert!(matches!( TargetSet::new(Vec::<String>::new(), NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::EmptyTransportTargets { .. }) )); diff --git a/crates/sdk/tests/unit/transport_tests.rs b/crates/sdk/tests/unit/transport_tests.rs @@ -88,7 +88,7 @@ fn transport_profile_policy_serializes_as_kind_only() { } #[test] -fn target_set_accessors_and_configured_relays_cover_empty_and_dedupe_paths() { +fn target_set_accessors_and_configured_relays_cover_empty_paths() { assert!( TransportProfile::local_only() .configured_nostr_relay_urls() @@ -96,11 +96,7 @@ fn target_set_accessors_and_configured_relays_cover_empty_and_dedupe_paths() { ); let targets = TargetSet::new( - [ - "wss://relay-a.example.com", - "wss://relay-a.example.com", - "wss://relay-b.example.com", - ], + ["wss://relay-a.example.com", "wss://relay-b.example.com"], NostrRelayUrlPolicy::Public, ) .expect("targets"); @@ -171,6 +167,43 @@ fn target_set_accessors_and_configured_relays_cover_empty_and_dedupe_paths() { } #[test] +fn target_sets_reject_duplicate_transport_fingerprints() { + let duplicate_relays = TargetSet::new( + [ + "wss://relay-a.example.com/path", + "WSS://RELAY-A.EXAMPLE.COM/path", + ], + NostrRelayUrlPolicy::Public, + ) + .expect_err("duplicate relays"); + + assert!(matches!( + duplicate_relays, + RadrootsSdkError::Transport { ref message } + if message == "transport target set contains duplicate fingerprints" + )); + + let first = RadrootsTransportTarget::new( + RadrootsTransportKind::Nostr, + "wss://relay-a.example.com/path", + ) + .expect("first target"); + let second = RadrootsTransportTarget::new( + RadrootsTransportKind::Nostr, + "WSS://RELAY-A.EXAMPLE.COM/path", + ) + .expect("second target"); + let duplicate_targets = + TargetSet::transport_targets(vec![first, second]).expect_err("duplicate targets"); + + assert!(matches!( + duplicate_targets, + RadrootsSdkError::Transport { ref message } + if message == "transport target set contains duplicate fingerprints" + )); +} + +#[test] fn reticulum_preview_profile_uses_canonical_endpoint_and_behavior_names() { let profile = ReticulumPreviewProfile::preview_unavailable();