diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index f77b4c27..57461807 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -239,7 +239,7 @@ pub fn announce_refs(mut node: Node, rid: Id) -> Result<(), Error> { match node.announce_refs(rid) { Ok(_) => Ok(()), Err(e) if e.is_connection_err() => Ok(()), - Err(e) => return Err(e.into()), + Err(e) => Err(e.into()), } } diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index 9dc2af82..584e7599 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -11,7 +11,6 @@ use serde_json::Value; use time::OffsetDateTime; use tower::ServiceExt; -use radicle::cli; use radicle::cob::issue::Issues; use radicle::cob::patch::{MergeTarget, Patches}; use radicle::crypto::ssh::keystore::MemorySigner; @@ -69,10 +68,7 @@ pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile { storage, keystore, public_key: keypair.pk.into(), - config: profile::Config { - node: node::Config::new(alias), - cli: cli::Config::default(), - }, + config: profile::Config::new(alias), } } diff --git a/radicle-node/src/test/environment.rs b/radicle-node/src/test/environment.rs index 58d20a0b..df21cf18 100644 --- a/radicle-node/src/test/environment.rs +++ b/radicle-node/src/test/environment.rs @@ -114,6 +114,7 @@ impl Environment { let config = profile::Config { node: node::Config::new(alias.clone()), cli: cli::Config { hints: false }, + public_explorer: profile::Explorer::default(), }; config.write(&home.config()).unwrap(); diff --git a/radicle/src/node/config.rs b/radicle/src/node/config.rs index 08a2ee83..213297a9 100644 --- a/radicle/src/node/config.rs +++ b/radicle/src/node/config.rs @@ -12,6 +12,34 @@ use crate::node::{Address, Alias, NodeId}; /// Target number of peers to maintain connections to. pub const TARGET_OUTBOUND_PEERS: usize = 8; +/// Configured public seeds. +pub mod seeds { + use std::str::FromStr; + + use super::{ConnectAddress, PeerAddr}; + use once_cell::sync::Lazy; + + /// The radicle public community seed node. + pub static RADICLE_COMMUNITY_NODE: Lazy = Lazy::new(|| { + // SAFETY: `ConnectAddress` is known at compile time. + #[allow(clippy::unwrap_used)] + PeerAddr::from_str( + "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776", + ) + .unwrap() + .into() + }); + + /// The radicle team node. + pub static RADICLE_TEAM_NODE: Lazy = Lazy::new(|| { + // SAFETY: `ConnectAddress` is known at compile time. + #[allow(clippy::unwrap_used)] + PeerAddr::from_str("z6MksmpU5b1dS7oaqF2bHXhQi1DWy2hB7Mh9CuN7y1DN6QSz@seed.radicle.xyz:8776") + .unwrap() + .into() + }); +} + /// Peer-to-peer network. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] @@ -24,22 +52,13 @@ pub enum Network { impl Network { /// Bootstrap nodes for this network. pub fn bootstrap(&self) -> Vec<(Alias, ConnectAddress)> { - use std::str::FromStr; - match self { Self::Main => [ - ( - "seed.radicle.garden", - "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776", - ), - ( - "seed.radicle.xyz", - "z6MksmpU5b1dS7oaqF2bHXhQi1DWy2hB7Mh9CuN7y1DN6QSz@seed.radicle.xyz:8776", - ), + ("seed.radicle.garden", seeds::RADICLE_COMMUNITY_NODE.clone()), + ("seed.radicle.xyz", seeds::RADICLE_TEAM_NODE.clone()), ] .into_iter() - // SAFETY: These are valid addresses. - .map(|(a, s)| (Alias::new(a), PeerAddr::from_str(s).unwrap().into())) + .map(|(a, s)| (Alias::new(a), s)) .collect(), Self::Test => vec![], diff --git a/radicle/src/profile.rs b/radicle/src/profile.rs index 96404270..cbdbf23a 100644 --- a/radicle/src/profile.rs +++ b/radicle/src/profile.rs @@ -22,7 +22,7 @@ use crate::crypto::ssh::{keystore, Keystore, Passphrase}; use crate::crypto::{PublicKey, Signer}; use crate::node::{address, routing, tracking, Alias, AliasStore}; use crate::prelude::Did; -use crate::prelude::NodeId; +use crate::prelude::{Id, NodeId}; use crate::storage::git::transport; use crate::storage::git::Storage; use crate::{cli, git, node}; @@ -80,6 +80,55 @@ pub mod env { } } +#[derive(Debug, Error)] +pub enum ExplorerUrlError { + #[error("invalid explorer URL {0:?}: unknown protocol")] + UnknownProtocol(String), + #[error("invalid explorer URL {0:?}: missing `$host` component")] + MissingHost(String), + #[error("invalid explorer URL {0:?}: missing `$rid` component")] + MissingRid(String), +} + +/// A public explorer, eg. `https://app.radicle.xyz`. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +#[serde(transparent)] +pub struct Explorer(String); + +impl Default for Explorer { + fn default() -> Self { + Self(String::from("https://app.radicle.xyz/nodes/$host/$rid")) + } +} + +impl Explorer { + /// Get the explorer URL, filling in the host and RID. + pub fn url(&self, host: &str, rid: &Id) -> String { + self.0 + .replace("$host", host) + .replace("$rid", rid.urn().as_str()) + } +} + +impl FromStr for Explorer { + type Err = ExplorerUrlError; + + fn from_str(s: &str) -> Result { + let url = s.to_owned(); + + if !url.starts_with("http://") && !url.starts_with("https://") { + return Err(ExplorerUrlError::UnknownProtocol(url)); + } + if !url.contains("$host") { + return Err(ExplorerUrlError::MissingHost(url)); + } + if !url.contains("$rid") { + return Err(ExplorerUrlError::MissingRid(url)); + } + Ok(Explorer(url)) + } +} + #[derive(Debug, Error)] pub enum Error { #[error(transparent)] @@ -119,15 +168,29 @@ pub struct Config { /// CLI configuration. #[serde(default)] pub cli: cli::Config, + /// Public explorer. This is used for generating links. + #[serde(default)] + pub public_explorer: Explorer, + /// Preferred seeds. These seeds will be used for explorer links + /// and in other situations when a seed needs to be chosen. + #[serde(default)] + pub preferred_seeds: Vec, } impl Config { - /// Initialize a new configuration. Fails if the path already exists. - pub fn init(alias: Alias, path: &Path) -> io::Result { - let cfg = Self { + /// Create a new, default configuration. + pub fn new(alias: Alias) -> Self { + Self { node: node::Config::new(alias), cli: cli::Config::default(), - }; + public_explorer: Explorer::default(), + preferred_seeds: vec![node::config::seeds::RADICLE_COMMUNITY_NODE.clone()], + } + } + + /// Initialize a new configuration. Fails if the path already exists. + pub fn init(alias: Alias, path: &Path) -> io::Result { + let cfg = Config::new(alias); cfg.write(path)?; Ok(cfg) @@ -146,10 +209,7 @@ impl Config { let Ok(alias) = Alias::from_str(&user) else { return Err(ConfigError::Io(path.to_owned(), e)); }; - Ok(Config { - node: node::Config::new(alias), - cli: cli::Config::default(), - }) + Ok(Config::new(alias)) } } }