commit 39b618c2b97454c5a66afd9e53ba68b120a8b1dd
parent b4c8b4680f1fabffc58c6629f9dbceb77cf55611
Author: triesap <tyson@radroots.org>
Date: Wed, 24 Jun 2026 06:18:34 +0000
cli: fix Myc NIP-46 request policy
- pass CLI Myc timeout into the SDK request policy
- reuse the same timeout for relay transport readiness
- reject response subscriptions with no relay acceptance
- cover timeout and subscription readiness regressions
Diffstat:
| M | src/runtime/sdk.rs | | | 110 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- |
1 file changed, 98 insertions(+), 12 deletions(-)
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -19,8 +19,9 @@ use radroots_nostr_connect::prelude::{
};
use radroots_sdk::{
RadrootsSdk, RadrootsSdkBuilder, RadrootsSdkError, RadrootsSdkLocalKeySigner,
- RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture,
- RadrootsSdkSignerProvider, RadrootsSdkStorageConfig, SdkPublishTransport, SdkRelayUrlPolicy,
+ RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport,
+ RadrootsSdkNip46TransportFuture, RadrootsSdkSignerProvider, RadrootsSdkStorageConfig,
+ SdkPublishTransport, SdkRelayUrlPolicy,
adapters::radrootsd::{RadrootsdAuth, RadrootsdProxyConfig as SdkRadrootsdProxyConfig},
};
use radroots_secret_vault::{RadrootsSecretVault, RadrootsSecretVaultOsKeyring};
@@ -343,22 +344,31 @@ async fn signer_provider(
target,
actor_pubkey,
} => {
+ let request_policy = myc_nip46_request_policy(config)?;
+ let request_timeout = request_policy.request_timeout();
let transport = Arc::new(
- CliSdkNip46RelayTransport::connect(
- &client_keys,
- &target,
- Duration::from_millis(config.myc.status_timeout_ms),
- )
- .await?,
+ CliSdkNip46RelayTransport::connect(&client_keys, &target, request_timeout).await?,
);
- let signer =
- RadrootsSdkMycNip46Signer::new(client_keys, target, actor_pubkey, transport)
- .map_err(|error| RuntimeError::Config(error.to_string()))?;
+ let signer = RadrootsSdkMycNip46Signer::new_with_request_policy(
+ client_keys,
+ target,
+ actor_pubkey,
+ transport,
+ request_policy,
+ )
+ .map_err(|error| RuntimeError::Config(error.to_string()))?;
Ok(RadrootsSdkSignerProvider::MycNip46(signer))
}
}
}
+fn myc_nip46_request_policy(
+ config: &RuntimeConfig,
+) -> Result<RadrootsSdkMycNip46RequestPolicy, RuntimeError> {
+ RadrootsSdkMycNip46RequestPolicy::new(Duration::from_millis(config.myc.status_timeout_ms))
+ .map_err(|error| RuntimeError::Config(error.to_string()))
+}
+
fn parse_myc_nip46_target(value: &str) -> Result<RadrootsNostrConnectBunkerUri, RuntimeError> {
let trimmed = value.trim();
if trimmed.starts_with("nostrconnect://") {
@@ -447,11 +457,18 @@ impl CliSdkNip46RelayTransport {
))
})?;
let notifications = client.notifications();
- client.subscribe(filter, None).await.map_err(|error| {
+ let subscribe_output = client.subscribe(filter, None).await.map_err(|error| {
RuntimeError::Network(format!(
"failed to subscribe to signer.remote_nip46 response relays: {error}"
))
})?;
+ validate_myc_response_subscription_acceptance(
+ subscribe_output.success.len(),
+ subscribe_output
+ .failed
+ .iter()
+ .map(|(relay, error)| (relay.to_string(), error.to_owned())),
+ )?;
Ok(Self {
client,
notifications: Mutex::new(notifications),
@@ -461,6 +478,30 @@ impl CliSdkNip46RelayTransport {
}
}
+fn validate_myc_response_subscription_acceptance<I>(
+ success_count: usize,
+ failed: I,
+) -> Result<(), RuntimeError>
+where
+ I: IntoIterator<Item = (String, String)>,
+{
+ if success_count > 0 {
+ return Ok(());
+ }
+ let failures = failed
+ .into_iter()
+ .map(|(relay, error)| format!("{relay}: {error}"))
+ .collect::<Vec<_>>()
+ .join("; ");
+ Err(RuntimeError::Network(if failures.is_empty() {
+ "signer.remote_nip46 response subscription was not accepted by any relay".to_owned()
+ } else {
+ format!(
+ "signer.remote_nip46 response subscription was not accepted by any relay: {failures}"
+ )
+ }))
+}
+
impl RadrootsSdkNip46Transport for CliSdkNip46RelayTransport {
fn publish_request_event<'a>(
&'a self,
@@ -633,6 +674,7 @@ mod tests {
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
+ use std::time::Duration;
use radroots_authority::RadrootsEventSigner;
use radroots_runtime_paths::RadrootsMigrationReport;
@@ -1066,6 +1108,50 @@ mod tests {
}
#[test]
+ fn myc_request_policy_uses_cli_timeout_config() {
+ let root = tempdir().expect("tempdir");
+ let mut config = sample_config(root.path(), Vec::new());
+ config.myc.status_timeout_ms = 12_345;
+
+ let policy = myc_nip46_request_policy(&config).expect("request policy");
+
+ assert_eq!(policy.request_timeout(), Duration::from_millis(12_345));
+ }
+
+ #[test]
+ fn myc_request_policy_rejects_zero_cli_timeout() {
+ let root = tempdir().expect("tempdir");
+ let mut config = sample_config(root.path(), Vec::new());
+ config.myc.status_timeout_ms = 0;
+
+ let error = myc_nip46_request_policy(&config).expect_err("zero timeout");
+
+ assert!(error.to_string().contains("must be greater than zero"));
+ }
+
+ #[test]
+ fn myc_response_subscription_requires_relay_acceptance() {
+ let error = validate_myc_response_subscription_acceptance(
+ 0,
+ [(
+ "ws://127.0.0.1:8080".to_owned(),
+ "subscription rejected".to_owned(),
+ )],
+ )
+ .expect_err("response subscription acceptance");
+
+ assert!(
+ error
+ .to_string()
+ .contains("response subscription was not accepted by any relay")
+ );
+ assert!(error.to_string().contains("subscription rejected"));
+
+ validate_myc_response_subscription_acceptance(1, std::iter::empty())
+ .expect("accepted response subscription");
+ }
+
+ #[test]
fn sdk_sources_do_not_import_cli_types() {
let sdk_src = Path::new(env!("CARGO_MANIFEST_DIR")).join("../sdk/crates/sdk/src");
let mut files = Vec::new();