From 15bf0d0b6befae6c81631c0e5d0dc2947dd3318a Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 11 Feb 2026 09:20:48 +0000 Subject: feat: use fallback relays for bootstrapping only - Add --relay flag to 'ngit account create' allowing users to specify relay URLs (repeatable). Defaults to relay-default-set when not provided. - Remove fallback relays from fetch when repo context exists (repo coordinate provided). Only use them for bootstrapping (profile discovery with no repo context). - Remove fallback relays from publish when repo or user relays exist. Only use them when neither is available (e.g. new account signup). - Update --customize help text to reflect new relay-default-set behavior. --- src/bin/ngit/cli.rs | 2 +- src/bin/ngit/sub_commands/create.rs | 30 +++++-- src/lib/client.rs | 20 ++++- src/lib/login/fresh.rs | 17 ++-- src/lib/login/user.rs | 2 + tests/ngit_init.rs | 140 ++++++++++++++++++++++++++++-- tests/ngit_send.rs | 167 +++++++++++++++++++++++++++--------- 7 files changed, 315 insertions(+), 63 deletions(-) diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs index d2246d7..47f4b27 100644 --- a/src/bin/ngit/cli.rs +++ b/src/bin/ngit/cli.rs @@ -53,7 +53,7 @@ ngit settings are managed through the git config. Currently the only settings not reachable through standard commands relate to default hardcoded relays: - nostr.grasp-default-set - only used during `ngit init` - - nostr.relay-default-set - must have at least 1 value, all events send to repo relays, user write and default relays + - nostr.relay-default-set - used for profile discovery and account bootstrapping - nostr.relay-blaster-set - only used for repo announcement events - nostr.relay-signer-fallback-set diff --git a/src/bin/ngit/sub_commands/create.rs b/src/bin/ngit/sub_commands/create.rs index e0d89b5..1c2b8db 100644 --- a/src/bin/ngit/sub_commands/create.rs +++ b/src/bin/ngit/sub_commands/create.rs @@ -16,6 +16,11 @@ pub struct SubCommandArgs { #[arg(long, required = true)] pub name: String, + /// Relay URLs for the new account's relay list (can be specified multiple + /// times). Defaults to the relay-default-set if not provided. + #[arg(long = "relay", value_parser, num_args = 1)] + pub relays: Vec, + /// Don't publish metadata to relays (offline mode) #[arg(long)] pub offline: bool, @@ -28,20 +33,31 @@ pub struct SubCommandArgs { pub async fn launch(_cli: &Cli, args: &SubCommandArgs) -> Result<()> { let git_repo = Repo::discover().ok(); + let params = Params::with_git_config_relay_defaults(&git_repo.as_ref()); + + let relay_urls = if args.relays.is_empty() { + params.relay_default_set.clone() + } else { + args.relays.clone() + }; + let client = if args.offline { None } else { - Some(Client::new(Params::with_git_config_relay_defaults( - &git_repo.as_ref(), - ))) + Some(Client::new(params)) }; let publish = !args.offline; - let (_signer, public_key, _signer_info, keys) = - signup_non_interactive(args.name.clone(), client.as_ref(), args.local, publish) - .await - .context("failed to create account")?; + let (_signer, public_key, _signer_info, keys) = signup_non_interactive( + args.name.clone(), + client.as_ref(), + args.local, + publish, + relay_urls, + ) + .await + .context("failed to create account")?; // Display the generated nsec prominently println!("\n✓ Account created successfully!"); diff --git a/src/lib/client.rs b/src/lib/client.rs index 9c49653..89fcaf7 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -1577,7 +1577,14 @@ async fn create_relays_request( }; let relays = { - let mut relays = fallback_relays; + // Only use fallback relays for bootstrapping (no repo context). + // When we have a repo coordinate, rely on repo relays and coordinate + // hint relays instead of always merging in the default set. + let mut relays = if trusted_maintainer_coordinate.is_none() { + fallback_relays + } else { + HashSet::new() + }; if let Some(repo_ref) = &repo_ref { for r in repo_ref.relays.clone() { relays.insert(r); @@ -1588,6 +1595,8 @@ async fn create_relays_request( relays.insert(r.clone()); } } + // When bootstrapping with no repo context and no coordinate hints, + // we need at least the fallback relays to discover the user profile. relays }; @@ -2238,8 +2247,15 @@ pub async fn send_events( animate: bool, silent: bool, ) -> Result<()> { + // Only include default relays as fallback when there are no repo relays + // (bootstrapping case, e.g. new account signup). When repo relays exist, + // trust the repo and user relay configuration. let fallback = [ - client.get_relay_default_set().clone(), + if repo_read_relays.is_empty() && my_write_relays.is_empty() { + client.get_relay_default_set().clone() + } else { + vec![] + }, if events.iter().any(|e| e.kind.eq(&Kind::GitRepoAnnouncement)) { client.get_blaster_relays().clone() } else { diff --git a/src/lib/login/fresh.rs b/src/lib/login/fresh.rs index 886b0e4..8e49085 100644 --- a/src/lib/login/fresh.rs +++ b/src/lib/login/fresh.rs @@ -715,6 +715,7 @@ pub async fn signup_non_interactive( #[cfg(not(test))] client: Option<&Client>, save_local: bool, publish: bool, + relay_urls: Vec, ) -> Result<(Arc, PublicKey, SignerInfo, Keys)> { // Generate new keypair let keys = nostr::Keys::generate(); @@ -783,10 +784,9 @@ pub async fn signup_non_interactive( if let Some(client) = client { let profile = EventBuilder::metadata(&Metadata::new().name(name)).sign_with_keys(&keys)?; let relay_list = EventBuilder::relay_list( - client - .get_relay_default_set() + relay_urls .iter() - .map(|s| (RelayUrl::parse(s).unwrap(), None)), + .filter_map(|s| RelayUrl::parse(s).ok().map(|url| (url, None))), ) .sign_with_keys(&keys)?; @@ -799,7 +799,7 @@ pub async fn signup_non_interactive( client, git_repo_path, vec![profile, relay_list], - client.get_relay_default_set().clone(), + relay_urls, vec![], true, false, @@ -848,12 +848,19 @@ async fn signup( } } - // Call the non-interactive function + // Call the non-interactive function, using relay_default_set as the + // relay list for interactive signup + let relay_urls = if let Some(c) = client { + c.get_relay_default_set().clone() + } else { + vec![] + }; let (signer, public_key, signer_info, _keys) = signup_non_interactive( name.clone(), client, false, // save_local = false (will be saved globally by caller) true, // publish = true (always publish in interactive mode) + relay_urls, ) .await?; diff --git a/src/lib/login/user.rs b/src/lib/login/user.rs index 0b702ef..b273363 100644 --- a/src/lib/login/user.rs +++ b/src/lib/login/user.rs @@ -113,6 +113,8 @@ pub async fn get_user_details( } Ok(user_ref) } else { + // No cached profile found. Fall back to fetching from default relays + // (bootstrapping). let empty = UserRef { public_key: public_key.to_owned(), metadata: extract_user_metadata(public_key, &[])?, diff --git a/tests/ngit_init.rs b/tests/ngit_init.rs index 5483315..f70bc2e 100644 --- a/tests/ngit_init.rs +++ b/tests/ngit_init.rs @@ -337,7 +337,21 @@ mod state_b_coordinate_only { ), Relay::new(8052, None, None), Relay::new(8053, None, None), - Relay::new(8055, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], + )?; + Ok(()) + }), + ), Relay::new(8056, None, None), ); @@ -420,7 +434,21 @@ mod state_b_coordinate_only { ), Relay::new(8052, None, None), Relay::new(8053, None, None), - Relay::new(8055, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], + )?; + Ok(()) + }), + ), Relay::new(8056, None, None), ); @@ -537,7 +565,22 @@ mod state_c_my_announcement { ), Relay::new(8052, None, None), Relay::new(8053, None, None), - Relay::new(8055, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + generate_repo_ref_event(), + ], + )?; + Ok(()) + }), + ), Relay::new(8056, None, None), ); @@ -595,7 +638,22 @@ mod state_c_my_announcement { ), Relay::new(8052, None, None), Relay::new(8053, None, None), - Relay::new(8055, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + generate_repo_ref_event(), + ], + )?; + Ok(()) + }), + ), Relay::new(8056, None, None), ); @@ -657,7 +715,22 @@ mod state_c_my_announcement { ), Relay::new(8052, None, None), Relay::new(8053, None, None), - Relay::new(8055, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + generate_repo_ref_event(), + ], + )?; + Ok(()) + }), + ), Relay::new(8056, None, None), ); @@ -836,7 +909,24 @@ mod state_d_co_maintainer { ), Relay::new(8052, None, None), Relay::new(8053, None, None), - Relay::new(8055, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + generate_test_key_2_metadata_event("carole"), + generate_test_key_2_relay_list_event(), + generate_repo_ref_event_as_key_2_listing_key_1(), + ], + )?; + Ok(()) + }), + ), Relay::new(8056, None, None), ); @@ -1021,7 +1111,24 @@ mod state_e_not_listed { ), Relay::new(8052, None, None), Relay::new(8053, None, None), - Relay::new(8055, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + generate_test_key_2_metadata_event("carole"), + generate_test_key_2_relay_list_event(), + generate_repo_ref_event_as_key_2_not_listing_key_1(), + ], + )?; + Ok(()) + }), + ), Relay::new(8056, None, None), ); @@ -1104,7 +1211,24 @@ mod state_e_not_listed { ), Relay::new(8052, None, None), Relay::new(8053, None, None), - Relay::new(8055, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + generate_test_key_2_metadata_event("carole"), + generate_test_key_2_relay_list_event(), + generate_repo_ref_event_as_key_2_not_listing_key_1(), + ], + )?; + Ok(()) + }), + ), Relay::new(8056, None, None), ); diff --git a/tests/ngit_send.rs b/tests/ngit_send.rs index 7946aef..7170f84 100644 --- a/tests/ngit_send.rs +++ b/tests/ngit_send.rs @@ -56,6 +56,29 @@ mod when_commits_behind_ask_to_proceed { )) } + fn create_relay_55() -> Result> { + Ok(Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], + )?; + Ok(()) + }), + )) + } + + fn create_relay_56() -> Result> { + Ok(Relay::new(8056, None, None)) + } + fn expect_confirm_prompt(p: &'_ mut CliTester) -> Result> { p.expect("fetching updates...\r\n")?; p.expect_eventually("\r\n")?; // may be 'no updates' or some updates @@ -73,17 +96,25 @@ mod when_commits_behind_ask_to_proceed { async fn asked_with_default_no() -> Result<()> { let test_repo = prep_test_repo()?; let mut r51 = create_relay_51()?; + let mut r55 = create_relay_55()?; + let mut r56 = create_relay_56()?; // // check relay had the right number of events let cli_tester_handle = std::thread::spawn(move || -> Result<()> { let mut p = CliTester::new_from_dir(&test_repo.dir, ["-i", "send", "HEAD~2"]); expect_confirm_prompt(&mut p)?; p.exit()?; - relay::shutdown_relay(8051)?; + for p in [51, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } Ok(()) }); // launch relay - r51.listen_until_close().await?; + let _ = join!( + r51.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); cli_tester_handle.join().unwrap()?; Ok(()) } @@ -93,16 +124,24 @@ mod when_commits_behind_ask_to_proceed { async fn when_response_is_false_aborts() -> Result<()> { let test_repo = prep_test_repo()?; let mut r51 = create_relay_51()?; + let mut r55 = create_relay_55()?; + let mut r56 = create_relay_56()?; let cli_tester_handle = std::thread::spawn(move || -> Result<()> { let mut p = CliTester::new_from_dir(&test_repo.dir, ["-i", "send", "HEAD~2"]); expect_confirm_prompt(&mut p)?.succeeds_with(Some(false))?; p.expect_end_with("Error: aborting so commits can be rebased\r\n")?; - relay::shutdown_relay(8051)?; + for p in [51, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } Ok(()) }); // launch relay - r51.listen_until_close().await?; + let _ = join!( + r51.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); cli_tester_handle.join().unwrap()?; Ok(()) } @@ -112,17 +151,25 @@ mod when_commits_behind_ask_to_proceed { async fn when_response_is_true_proceeds() -> Result<()> { let test_repo = prep_test_repo()?; let mut r51 = create_relay_51()?; + let mut r55 = create_relay_55()?; + let mut r56 = create_relay_56()?; let cli_tester_handle = std::thread::spawn(move || -> Result<()> { let mut p = CliTester::new_from_dir(&test_repo.dir, ["-i", "send", "HEAD~2"]); expect_confirm_prompt(&mut p)?.succeeds_with(Some(true))?; p.expect("? include cover letter")?; p.exit()?; - relay::shutdown_relay(8051)?; + for p in [51, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } Ok(()) }); // launch relay - r51.listen_until_close().await?; + let _ = join!( + r51.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); cli_tester_handle.join().unwrap()?; Ok(()) } @@ -245,7 +292,11 @@ async fn prep_run_create_proposal( relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -319,13 +370,11 @@ mod when_cover_letter_details_specified_with_range_of_head_2_sends_cover_letter_ #[tokio::test] #[serial] - async fn only_1_cover_letter_event_sent_to_fallback_relays() -> Result<()> { + async fn no_events_sent_to_fallback_relays() -> Result<()> { let (r51, r52, _, _, _) = prep_run_create_proposal(true).await?; + // Fallback relays should not receive events when repo relays exist for relay in [&r51, &r52] { - assert_eq!( - relay.events.iter().filter(|e| is_cover_letter(e)).count(), - 1, - ); + assert_eq!(relay.events.len(), 0); } Ok(()) } @@ -333,8 +382,9 @@ mod when_cover_letter_details_specified_with_range_of_head_2_sends_cover_letter_ #[tokio::test] #[serial] async fn only_2_patch_kind_events_sent_to_each_relay() -> Result<()> { - let (r51, r52, r53, r55, r56) = prep_run_create_proposal(true).await?; - for relay in [&r51, &r52, &r53, &r55, &r56] { + let (_, _, r53, r55, r56) = prep_run_create_proposal(true).await?; + // Only user and repo relays should receive patches, not fallback relays + for relay in [&r53, &r55, &r56] { assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 2,); } Ok(()) @@ -832,7 +882,11 @@ mod when_cover_letter_details_specified_with_range_of_head_2_sends_cover_letter_ relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -848,10 +902,8 @@ mod when_cover_letter_details_specified_with_range_of_head_2_sends_cover_letter_ &mut p, vec![ (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), - (" [my-relay] ws://localhost:8053", true, ""), (" [repo-relay] ws://localhost:8056", true, ""), - (" [default] ws://localhost:8051", true, ""), - (" [default] ws://localhost:8052", true, ""), + (" [my-relay] ws://localhost:8053", true, ""), ], 3, )?; @@ -912,7 +964,11 @@ mod when_cover_letter_details_specified_with_range_of_head_2_sends_cover_letter_ relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -986,7 +1042,11 @@ mod when_cover_letter_details_specified_with_range_of_head_2_sends_cover_letter_ relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -1009,14 +1069,12 @@ mod when_cover_letter_details_specified_with_range_of_head_2_sends_cover_letter_ &mut p, vec![ (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), - (" [my-relay] ws://localhost:8053", true, ""), ( " [repo-relay] ws://localhost:8056", false, "error: Payment Required", ), - (" [default] ws://localhost:8051", true, ""), - (" [default] ws://localhost:8052", true, ""), + (" [my-relay] ws://localhost:8053", true, ""), ], 3, )?; @@ -1080,7 +1138,11 @@ mod when_no_cover_letter_flag_set_with_range_of_head_2_sends_2_patches_without_c relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -1097,10 +1159,8 @@ mod when_no_cover_letter_flag_set_with_range_of_head_2_sends_2_patches_without_c &mut p, vec![ (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), - (" [my-relay] ws://localhost:8053", true, ""), (" [repo-relay] ws://localhost:8056", true, ""), - (" [default] ws://localhost:8051", true, ""), - (" [default] ws://localhost:8052", true, ""), + (" [my-relay] ws://localhost:8053", true, ""), ], 2, )?; @@ -1303,7 +1363,11 @@ mod when_range_ommited_prompts_for_selection_defaulting_ahead_of_main { relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -1366,7 +1430,11 @@ mod when_range_ommited_prompts_for_selection_defaulting_ahead_of_main { relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -1382,10 +1450,8 @@ mod when_range_ommited_prompts_for_selection_defaulting_ahead_of_main { &mut p, vec![ (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), - (" [my-relay] ws://localhost:8053", true, ""), (" [repo-relay] ws://localhost:8056", true, ""), - (" [default] ws://localhost:8051", true, ""), - (" [default] ws://localhost:8052", true, ""), + (" [my-relay] ws://localhost:8053", true, ""), ], 2, )?; @@ -1504,7 +1570,12 @@ mod root_proposal_specified_using_in_reply_to_with_range_of_head_2_and_cover_let relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event(), get_pretend_proposal_root_event()], + &vec![ + generate_repo_ref_event(), + get_pretend_proposal_root_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -1567,7 +1638,12 @@ mod root_proposal_specified_using_in_reply_to_with_range_of_head_2_and_cover_let relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event(), get_pretend_proposal_root_event()], + &vec![ + generate_repo_ref_event(), + get_pretend_proposal_root_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -1583,10 +1659,8 @@ mod root_proposal_specified_using_in_reply_to_with_range_of_head_2_and_cover_let &mut p, vec![ (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), - (" [my-relay] ws://localhost:8053", true, ""), (" [repo-relay] ws://localhost:8056", true, ""), - (" [default] ws://localhost:8051", true, ""), - (" [default] ws://localhost:8052", true, ""), + (" [my-relay] ws://localhost:8053", true, ""), ], 3, )?; @@ -1765,7 +1839,12 @@ mod in_reply_to_mentions_issue { relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event(), get_pretend_issue_event()], + &vec![ + generate_repo_ref_event(), + get_pretend_issue_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -1885,7 +1964,11 @@ mod in_reply_to_mentions_npub_and_nprofile_which_get_mentioned_in_proposal_root relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), @@ -2037,7 +2120,11 @@ mod non_interactive_validation { relay.respond_events( client_id, &subscription_id, - &vec![generate_repo_ref_event()], + &vec![ + generate_repo_ref_event(), + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], )?; Ok(()) }), -- cgit v1.2.3