ddk 1.0.11

application tooling for DLCs 🌊
Documentation
use crate::logger::{log_error, log_info, WriteLog};
use bip39::{Language, Mnemonic};
use bitcoin::key::rand::Fill;
use bitcoin::Network;
use ddk_manager::manager::Manager;
use ddk_manager::SystemTimeProvider;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};

use crate::chain::EsploraClient;
use crate::ddk::{DlcDevKit, DlcManagerMessage};
use crate::error::{BuilderError, Error};
use crate::logger::{LogLevel, Logger};
use crate::wallet::address::AddressGenerator;
use crate::wallet::DlcDevKitWallet;
use crate::{Oracle, Storage, Transport};

const DEFAULT_ESPLORA_HOST: &str = "https://mutinynet.com/api";
const DEFAULT_NETWORK: Network = Network::Signet;
const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Info;

/// Configuration for the seed bytes for the wallet.
#[derive(Debug, Clone)]
pub enum SeedConfig {
    /// Generates a random seed everytime ddk is run.
    Random,
    /// The first string is the mnemonic, the second is the passphrase.
    Mnemonic(String, String),
    /// The bytes to use for the seed.
    Bytes([u8; 64]),
}

/// Builder pattern for creating a [`crate::ddk::DlcDevKit`] process.
#[derive(Clone)]
pub struct Builder<T, S, O> {
    name: Option<String>,
    transport: Option<Arc<T>>,
    storage: Option<Arc<S>>,
    oracle: Option<Arc<O>>,
    contract_address_generator: Option<Arc<dyn AddressGenerator + Send + Sync + 'static>>,
    esplora_host: String,
    network: Network,
    seed_bytes: [u8; 64],
    logger: Option<Arc<Logger>>,
}

/// Defaults when creating a DDK application
/// Transport, storage, and oracle is set to none.
///
/// esplora_host: <https://mutinynet.com/api>
/// network: Network::Signet
impl<T: Transport, S: Storage, O: Oracle> Default for Builder<T, S, O> {
    fn default() -> Self {
        Self {
            name: None,
            transport: None,
            storage: None,
            oracle: None,
            contract_address_generator: None,
            esplora_host: DEFAULT_ESPLORA_HOST.to_string(),
            network: DEFAULT_NETWORK,
            seed_bytes: [0u8; 64],
            logger: None,
        }
    }
}

impl<T: Transport, S: Storage, O: Oracle> Builder<T, S, O> {
    /// Create a new, default DDK builder.
    pub fn new() -> Self {
        Builder::default()
    }

    /// Set the name of the DDK process. Used as an identifier for the process created.
    /// Creates a directory for the process with the name specifed. All file-based components
    /// will be stored in a directory under the storage path set in the `DdkConfig` and the `name`.
    /// If no name is set, defaults to a generated `uuid`.
    pub fn set_name(&mut self, name: &str) -> &mut Self {
        self.name = Some(name.into());
        self
    }

    /// The communication layer of DDK. Type MUST implement [crate::Transport].
    /// Transport sets up listeners, communicates with counterparties, and passes
    /// DLC messages to the `Manager`.
    pub fn set_transport(&mut self, transport: Arc<T>) -> &mut Self {
        self.transport = Some(transport);
        self
    }

    /// DLC contract storage. Storage is used by the [`ddk_manager::manager::Manager`] to create, update, retrieve, and
    /// delete contracts. MUST implement [`crate::Storage`].
    pub fn set_storage(&mut self, storage: Arc<S>) -> &mut Self {
        self.storage = Some(storage);
        self
    }

    /// Oracle implementation for the [ddk_manager::manager::Manager] to retrieve oracle attestations and announcements.
    /// MUST implement [`crate::Oracle`].
    pub fn set_oracle(&mut self, oracle: Arc<O>) -> &mut Self {
        self.oracle = Some(oracle);
        self
    }

    /// Wallet implementation for the [ddk_manager::manager::Manager] to retrieve wallet keys and sign transactions.
    /// For now, just uses the [`DlcDevKitWallet`] implementation. Optionally can use a custom address generation.
    pub fn set_contract_address_generator(
        &mut self,
        contract_address_generator: Arc<dyn AddressGenerator + Send + Sync + 'static>,
    ) -> &mut Self {
        self.contract_address_generator = Some(contract_address_generator);
        self
    }

    /// Set the esplora server to connect to.
    pub fn set_esplora_host(&mut self, host: String) -> &mut Self {
        self.esplora_host = host;
        self
    }

    /// Set the network DDK connects to.
    pub fn set_network(&mut self, network: Network) -> &mut Self {
        self.network = network;
        self
    }

    /// Set the seed bytes for the wallet.
    pub fn set_seed_bytes(&mut self, seed_config: SeedConfig) -> Result<&mut Self, BuilderError> {
        match seed_config {
            SeedConfig::Random => {
                let mut seed = [0u8; 64];
                seed.try_fill(&mut bitcoin::key::rand::thread_rng())
                    .map_err(|_| BuilderError::SeedGenerationFailed)?;
                self.seed_bytes = seed
            }
            SeedConfig::Mnemonic(mnemonic, passphrase) => {
                let mnemonic = Mnemonic::parse_in_normalized(Language::English, &mnemonic).unwrap();
                self.seed_bytes = mnemonic.to_seed(passphrase)
            }
            SeedConfig::Bytes(bytes) => self.seed_bytes = bytes,
        }
        Ok(self)
    }

    /// Set the logger for the DDK instance.
    pub fn set_logger(&mut self, logger: Arc<Logger>) -> &mut Self {
        self.logger = Some(logger);
        self
    }

    /// Setup the logger based on the provided logger or use default console logging
    fn setup_logger(&self, name: &str) -> Result<Arc<Logger>, Error> {
        match &self.logger {
            Some(logger) => Ok(logger.clone()),
            None => {
                // Default to console logging with Info level
                Ok(Arc::new(Logger::console(
                    name.to_string(),
                    DEFAULT_LOG_LEVEL,
                )))
            }
        }
    }

    /// Builds the `DlcDevKit` instance. Fails if any components are missing.
    #[tracing::instrument(name = "builder", skip(self))]
    pub async fn finish(&self) -> Result<DlcDevKit<T, S, O>, Error> {
        let transport = self
            .transport
            .as_ref()
            .map_or_else(|| Err(BuilderError::NoTransport), |t| Ok(t.clone()))?;

        let storage = self
            .storage
            .as_ref()
            .map_or_else(|| Err(BuilderError::NoStorage), |s| Ok(s.clone()))?;

        let oracle = self
            .oracle
            .as_ref()
            .map_or_else(|| Err(BuilderError::NoOracle), |o| Ok(o.clone()))?;

        let name = self
            .name
            .clone()
            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());

        let logger = self.setup_logger(&name)?;

        let esplora_client = Arc::new(EsploraClient::new(
            &self.esplora_host,
            self.network,
            logger.clone(),
        )?);

        let wallet = match &self.contract_address_generator {
            Some(w) => {
                let wallet = DlcDevKitWallet::new(
                    &self.seed_bytes,
                    esplora_client.clone(),
                    self.network,
                    storage.clone(),
                    Some(w.clone()),
                    logger.clone(),
                )
                .await?;
                Arc::new(wallet)
            }
            None => Arc::new(
                DlcDevKitWallet::new(
                    &self.seed_bytes,
                    esplora_client.clone(),
                    self.network,
                    storage.clone(),
                    None,
                    logger.clone(),
                )
                .await?,
            ),
        };

        let mut oracles = HashMap::new();
        oracles.insert(oracle.get_public_key(), oracle.clone());

        let (sender, mut receiver) = tokio::sync::mpsc::channel(100);
        let (stop_signal_sender, stop_signal) = tokio::sync::watch::channel(false);

        let manager = Arc::new(
            Manager::new(
                wallet.clone(),
                wallet.clone(),
                esplora_client.clone(),
                storage.clone(),
                oracles,
                Arc::new(SystemTimeProvider {}),
                wallet.clone(),
                logger.clone(),
            )
            .await?,
        );

        let manager_clone = manager.clone();
        let logger_clone = logger.clone();
        tokio::spawn(async move {
            while let Some(msg) = receiver.recv().await {
                match msg {
                    DlcManagerMessage::OfferDlc {
                        contract_input,
                        counter_party,
                        oracle_announcements,
                        responder,
                    } => {
                        let offer = manager_clone
                            .send_offer_with_announcements(
                                &contract_input,
                                counter_party,
                                vec![oracle_announcements],
                            )
                            .await;

                        let _ = responder.send(offer).map_err(|e| {
                            log_error!(logger_clone.clone(), "Error sending offer: {:?}", e);
                        });
                    }
                    DlcManagerMessage::AcceptDlc {
                        contract,
                        responder,
                    } => {
                        let accept_dlc = manager_clone.accept_contract_offer(&contract).await;

                        let _ = responder.send(accept_dlc).map_err(|e| {
                            log_error!(logger_clone.clone(), "Error sending accept DLC: {:?}", e);
                        });
                    }
                    DlcManagerMessage::PeriodicCheck => {
                        let _ = manager_clone.periodic_check(false).await;
                    }
                }
            }
        });

        log_info!(
            logger.clone(),
            "DDK runtime created. name={}, esplora={}, network={}, transport={}, oracle={}",
            name,
            self.esplora_host,
            self.network,
            transport.name(),
            oracle.get_public_key()
        );

        Ok(DlcDevKit {
            runtime: Arc::new(RwLock::new(None)),
            wallet,
            manager,
            sender,
            transport,
            storage,
            oracle,
            network: self.network,
            stop_signal,
            stop_signal_sender,
            logger,
        })
    }
}