commit dc41a3b3cd84b5da3b1ba8c2f94550b51e7c51b4
parent b9bf25b4972dc36688fb823feafb0046c86602ce
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:
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")
));