p2panda 0.6.1

Out-of-the-box p2panda Node API for application developers
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::collections::HashSet;
use std::fmt::Debug;

use p2panda_core::{SigningKey, Topic};
use p2panda_net::address_book::AddressBookError;
use p2panda_net::addrs::{NodeInfo, TrustedTransportInfo};
use p2panda_net::discovery::{DiscoveryConfig, DiscoveryError};
use p2panda_net::gossip::{GossipConfig, GossipError};
use p2panda_net::iroh_endpoint::{EndpointAddr, EndpointError, IrohConfig, RelayUrl};
use p2panda_net::iroh_mdns::MdnsDiscoveryError;
use p2panda_net::sync::LogSyncError;
use p2panda_net::utils::from_verifying_key;
use p2panda_net::{
    AddressBook, DEFAULT_NETWORK_ID, Discovery, Endpoint, Gossip, LogSync, MdnsDiscovery,
    NetworkId, NodeId,
};
use p2panda_store::SqliteStore;
use thiserror::Error;

use crate::operation::Extensions;

#[derive(Clone, Debug)]
pub(crate) struct Network {
    pub address_book: AddressBook,
    #[allow(unused)]
    pub mdns: Option<MdnsDiscovery>,
    pub endpoint: Endpoint,
    pub discovery: Discovery,
    pub gossip: Gossip,
    pub log_sync: LogSync<SqliteStore, Topic, Extensions>,
}

impl Network {
    pub async fn spawn(
        config: NetworkConfig,
        signing_key: SigningKey,
        store: SqliteStore,
    ) -> Result<Self, NetworkError> {
        let address_book = AddressBook::builder().store(store.clone()).spawn().await?;

        for (node_id, transport_info) in config.bootstraps {
            let mut node_info = NodeInfo::new(node_id).bootstrap();
            if node_info.update_transports(transport_info.into()).is_ok() {
                address_book.insert_node_info(node_info).await?;
            }
        }

        let mut endpoint = Endpoint::builder(address_book.clone())
            .config(config.iroh)
            .signing_key(signing_key)
            .network_id(config.network_id);

        for url in &config.relay_urls {
            endpoint = endpoint.relay_url(url.clone());
        }

        let endpoint = endpoint.spawn().await?;

        let mdns = match config.mdns_mode {
            MdnsDiscoveryMode::Active | MdnsDiscoveryMode::Passive => {
                let mdns = MdnsDiscovery::builder(address_book.clone(), endpoint.clone())
                    .mode(config.mdns_mode.into())
                    .spawn()
                    .await?;

                Some(mdns)
            }
            MdnsDiscoveryMode::Disabled => None,
        };

        let discovery = Discovery::builder(address_book.clone(), endpoint.clone())
            .config(config.discovery)
            .spawn()
            .await?;

        let gossip = Gossip::builder(address_book.clone(), endpoint.clone())
            .config(config.gossip)
            .spawn()
            .await?;

        let log_sync = LogSync::builder(store.clone(), endpoint.clone(), gossip.clone())
            .spawn()
            .await?;

        Ok(Self {
            address_book,
            endpoint,
            mdns,
            discovery,
            gossip,
            log_sync,
        })
    }

    #[allow(unused)]
    pub fn id(&self) -> NodeId {
        self.endpoint.node_id()
    }

    pub fn network_id(&self) -> NetworkId {
        self.endpoint.network_id()
    }

    pub async fn insert_bootstrap(
        &self,
        node_id: NodeId,
        relay_url: RelayUrl,
    ) -> Result<(), NetworkError> {
        let mut node_info = NodeInfo::new(node_id).bootstrap();
        let endpoint_addr =
            EndpointAddr::new(from_verifying_key(node_id)).with_relay_url(relay_url);
        let transport_info = TrustedTransportInfo::from(endpoint_addr);

        if node_info.update_transports(transport_info.into()).is_ok() {
            self.address_book.insert_node_info(node_info).await?;
        }
        Ok(())
    }
}

/// mDNS discovery mode.
///
/// By default this is set to "active" meaning we are actively advertising our address and public
/// key on local-area networks.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum MdnsDiscoveryMode {
    /// mDNS discovery disabled.
    Disabled,

    /// Advertise our own and listen for others' discovery announcements.
    #[default]
    Active,

    /// Listen for others' discovery announcements but don't advertise our own.
    Passive,
}

impl From<MdnsDiscoveryMode> for p2panda_net::iroh_mdns::MdnsDiscoveryMode {
    fn from(value: MdnsDiscoveryMode) -> Self {
        match value {
            MdnsDiscoveryMode::Disabled => unreachable!(),
            MdnsDiscoveryMode::Active => Self::Active,
            MdnsDiscoveryMode::Passive => Self::Passive,
        }
    }
}

#[derive(Clone, Debug)]
pub(crate) struct NetworkConfig {
    pub network_id: NetworkId,
    pub relay_urls: HashSet<RelayUrl>,
    pub bootstraps: HashSet<(NodeId, TrustedTransportInfo)>,
    pub mdns_mode: MdnsDiscoveryMode,
    pub discovery: DiscoveryConfig,
    pub gossip: GossipConfig,
    pub iroh: IrohConfig,
}

impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            network_id: DEFAULT_NETWORK_ID,
            relay_urls: HashSet::new(),
            bootstraps: HashSet::new(),
            mdns_mode: MdnsDiscoveryMode::Active,
            discovery: DiscoveryConfig::default(),
            gossip: GossipConfig::default(),
            iroh: IrohConfig::default(),
        }
    }
}

/// Errors coming from the networking layer.
#[derive(Debug, Error)]
pub enum NetworkError {
    #[error(transparent)]
    AddressBook(#[from] AddressBookError),

    #[error(transparent)]
    Endpoint(#[from] EndpointError),

    #[error(transparent)]
    Mdns(#[from] MdnsDiscoveryError),

    #[error(transparent)]
    Discovery(#[from] DiscoveryError),

    #[error(transparent)]
    Gossip(#[from] GossipError),

    #[error(transparent)]
    LogSync(#[from] LogSyncError<Extensions>),
}