commit dfcc32733075b2e6d0cb5214a237504540af6e4f
parent 6527cc9fbd1748846498f504d8408095346eab38
Author: triesap <tyson@radroots.org>
Date: Thu, 7 May 2026 17:01:11 +0000
sdk: add profile relay publish
- expose profile local-identity relay publish APIs
- reuse shared relay-direct signing and receipt handling
- prove profile draft publish against an ACK relay
- cover wrong transport and signer mode failures
Diffstat:
2 files changed, 166 insertions(+), 0 deletions(-)
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -1880,6 +1880,43 @@ impl<'a> ProfileClient<'a> {
profile::build_draft(profile_value, profile_type)
}
+ #[cfg(all(
+ feature = "identity-models",
+ feature = "relay-client",
+ feature = "signing"
+ ))]
+ pub async fn publish_with_identity(
+ &self,
+ identity: &RadrootsIdentity,
+ profile_value: &RadrootsProfile,
+ profile_type: Option<RadrootsProfileType>,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ let parts = profile::build_draft(profile_value, profile_type)
+ .map_err(|err| SdkPublishError::Encode(err.to_string()))?;
+ self.client
+ .publish_parts_via_relay_with_identity(identity, parts, "profile.publish_with_identity")
+ .await
+ }
+
+ #[cfg(all(
+ feature = "identity-models",
+ feature = "relay-client",
+ feature = "signing"
+ ))]
+ pub async fn publish_draft_with_identity(
+ &self,
+ identity: &RadrootsIdentity,
+ draft: WireEventParts,
+ ) -> Result<SdkPublishReceipt, SdkPublishError> {
+ self.client
+ .publish_parts_via_relay_with_identity(
+ identity,
+ draft,
+ "profile.publish_draft_with_identity",
+ )
+ .await
+ }
+
#[cfg(feature = "radrootsd-client")]
pub async fn publish_profile_via_radrootsd(
&self,
diff --git a/crates/sdk/tests/relay_direct.rs b/crates/sdk/tests/relay_direct.rs
@@ -17,6 +17,7 @@ use radroots_sdk::listing::{
RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct,
RadrootsListingStatus,
};
+use radroots_sdk::profile::{RadrootsProfile, RadrootsProfileType};
use radroots_sdk::{
RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkEnvironment, SdkPublishError,
SdkTransportMode, SdkTransportReceipt, SignerConfig,
@@ -161,6 +162,134 @@ fn sample_listing() -> RadrootsListing {
}
}
+fn sample_profile() -> RadrootsProfile {
+ RadrootsProfile {
+ name: "north-farm".into(),
+ display_name: Some("North Farm".into()),
+ nip05: None,
+ about: Some("Farm profile".into()),
+ website: None,
+ picture: None,
+ banner: None,
+ lud06: None,
+ lud16: None,
+ bot: None,
+ }
+}
+
+#[tokio::test]
+async fn relay_direct_profile_publish_accepts_sdk_built_draft() -> TestResult<()> {
+ let relay = AckRelay::spawn().await?;
+ let identity = RadrootsIdentity::generate();
+ let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom);
+ config.transport = SdkTransportMode::RelayDirect;
+ config.signer = SignerConfig::LocalIdentity;
+ config.relay = RelayConfig {
+ urls: vec![relay.url().to_owned()],
+ };
+ let client = RadrootsSdkClient::from_config(config)?;
+ let draft = client
+ .profile()
+ .build_draft(&sample_profile(), Some(RadrootsProfileType::Farm))?;
+
+ let receipt = client
+ .profile()
+ .publish_draft_with_identity(&identity, draft)
+ .await?;
+
+ assert_eq!(receipt.transport, SdkTransportMode::RelayDirect);
+ assert_eq!(receipt.event_kind, Some(0));
+ assert!(receipt.event_id.is_some());
+ match receipt.transport_receipt {
+ SdkTransportReceipt::RelayDirect(relay_receipt) => {
+ assert_eq!(
+ receipt.event_id.as_deref(),
+ Some(relay_receipt.event_id.as_str())
+ );
+ assert_eq!(relay_receipt.event.kind, 0);
+ assert_eq!(relay_receipt.event.author, identity.public_key_hex());
+ assert_eq!(
+ relay_receipt.event.tags,
+ vec![vec!["t".to_owned(), "radroots:type:farm".to_owned()]]
+ );
+ assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]);
+ assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]);
+ assert_eq!(
+ relay_receipt.acknowledged_relays,
+ vec![relay.url().to_owned()]
+ );
+ assert!(relay_receipt.failed_relays.is_empty());
+ }
+ SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"),
+ }
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn relay_direct_profile_publish_rejects_radrootsd_transport_mode() -> TestResult<()> {
+ let identity = RadrootsIdentity::generate();
+ let mut config = RadrootsSdkConfig::production();
+ config.transport = SdkTransportMode::Radrootsd;
+ config.signer = SignerConfig::LocalIdentity;
+ let client = RadrootsSdkClient::from_config(config)?;
+
+ let error = client
+ .profile()
+ .publish_with_identity(
+ &identity,
+ &sample_profile(),
+ Some(RadrootsProfileType::Farm),
+ )
+ .await
+ .expect_err("unsupported transport");
+
+ assert!(matches!(
+ error,
+ SdkPublishError::UnsupportedTransport {
+ transport: SdkTransportMode::Radrootsd,
+ operation: "profile.publish_with_identity",
+ }
+ ));
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn relay_direct_profile_publish_rejects_draft_only_signer_mode() -> TestResult<()> {
+ let relay = AckRelay::spawn().await?;
+ let identity = RadrootsIdentity::generate();
+ let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom);
+ config.transport = SdkTransportMode::RelayDirect;
+ config.signer = SignerConfig::DraftOnly;
+ config.relay = RelayConfig {
+ urls: vec![relay.url().to_owned()],
+ };
+ let client = RadrootsSdkClient::from_config(config)?;
+
+ let error = client
+ .profile()
+ .publish_with_identity(
+ &identity,
+ &sample_profile(),
+ Some(RadrootsProfileType::Farm),
+ )
+ .await
+ .expect_err("unsupported signer mode");
+
+ assert!(matches!(
+ error,
+ SdkPublishError::UnsupportedSignerMode {
+ transport: SdkTransportMode::RelayDirect,
+ signer: SignerConfig::DraftOnly,
+ required: SignerConfig::LocalIdentity,
+ operation: "profile.publish_with_identity",
+ }
+ ));
+
+ Ok(())
+}
+
#[tokio::test]
async fn relay_direct_listing_publish_accepts_sdk_built_draft() -> TestResult<()> {
let relay = AckRelay::spawn().await?;