trident-sdk 0.2.0

End-user SDK for Trident deposits and cross-chain spends
Documentation
use std::collections::HashMap;
use std::sync::Arc;

use tonic::transport::Endpoint;

use crate::chain_handler::{ChainHandler, ChainHandlerBuildError, ChainHandlerBuilder};
use crate::proto::delivery_v1;
use crate::proto::delivery_v1::delivery_service_client::DeliveryServiceClient;
use crate::{Domain, TridentClient};

/// Errors returned while building and connecting a [`TridentClient`].
///
/// Covers transport and gRPC failures, missing contract addresses in the
/// delivery service's deployment response, per-chain-family build failures,
/// and mismatches between locally registered domains and server-reported
/// deployments.
#[derive(Debug, thiserror::Error)]
pub enum TridentClientBuilderError {
    /// gRPC transport could not be established against the configured endpoint.
    #[error("Failed to connect to delivery service: {0}")]
    Transport(#[from] tonic::transport::Error),

    /// The delivery service rejected the `list_deployments` call.
    #[error("Grpc: {0}")]
    Grpc(#[from] tonic::Status),

    /// The server's deployment response for `domain` was missing its source
    /// `deposit_contract` address.
    #[error("Missing deposit_contract for domain {0}")]
    MissingDepositContract(Domain),

    /// The server's deployment response for `domain` was missing its
    /// destination `delivery_contract` address.
    #[error("Missing delivery_contract for domain {0}")]
    MissingDeliveryContract(Domain),

    /// A chain-family-specific handler builder rejected the deployment data
    /// (e.g. address decoding failure).
    #[error(transparent)]
    ChainHandlerBuild(#[from] ChainHandlerBuildError),

    /// A domain was registered locally but the server returned no matching
    /// deployment for it.
    #[error("No deployment returned for registered domain {domain}")]
    UnregisteredDeployment { domain: Domain },
}

/// Builds a [`TridentClient`] by registering one chain-handler builder per
/// domain and connecting to the delivery service.
///
/// Chain-family-specific entry points (e.g. `with_evm_chain` from the `evm`
/// module) wrap the generic internal chain-registration call.
pub struct TridentClientBuilder {
    endpoint: Endpoint,
    pending: HashMap<Domain, Box<dyn ChainHandlerBuilder>>,
    expiry_margins: HashMap<Domain, u64>,
}

impl TridentClientBuilder {
    /// Creates an empty builder targeting the given delivery-service gRPC
    /// endpoint.
    pub fn new(endpoint: Endpoint) -> Self {
        Self {
            endpoint,
            pending: HashMap::new(),
            expiry_margins: HashMap::new(),
        }
    }

    /// Registers a [`ChainHandlerBuilder`] for `domain`.
    ///
    /// Chain-family-specific convenience methods (e.g. `with_evm_chain` from
    /// the `evm` module) call this internally.
    pub(crate) fn with_chain<B>(mut self, domain: Domain, builder: B) -> Self
    where
        B: ChainHandlerBuilder + 'static,
    {
        self.pending.insert(domain, Box::new(builder));
        self
    }

    /// Sets the flat number of blocks added on top of the server's minimum
    /// spend-intent expiry height for `domain` when the SDK auto-resolves
    /// the expiry. Domains without an entry fall back to 10% of the server's
    /// minimum.
    pub fn with_expiry_margin(mut self, domain: Domain, margin: u64) -> Self {
        self.expiry_margins.insert(domain, margin);
        self
    }

    /// Connects to the delivery service and materializes a [`TridentClient`].
    ///
    /// Fetches the deployment list from the server and, for each registered
    /// domain, invokes the corresponding chain-handler builder with the
    /// server-reported deposit and delivery contract addresses. Fails if a
    /// deployment is missing either contract address, if a chain-handler
    /// builder rejects its inputs, or if a registered domain has no matching
    /// deployment.
    pub async fn connect(self) -> Result<TridentClient, TridentClientBuilderError> {
        let channel = self.endpoint.connect().await?;
        let mut delivery = DeliveryServiceClient::new(channel);
        let deployments = delivery
            .list_deployments(delivery_v1::ListDeploymentsRequest {})
            .await?
            .into_inner();

        let mut pending = self.pending;
        let mut chains: HashMap<Domain, Arc<dyn ChainHandler>> = HashMap::new();

        for deployment in deployments.deployments {
            let Some(builder) = pending.remove(&deployment.domain) else {
                continue;
            };
            let deposit_contract = deployment.deposit_contract.ok_or(
                TridentClientBuilderError::MissingDepositContract(deployment.domain),
            )?;
            let delivery_contract = deployment.delivery_contract.ok_or(
                TridentClientBuilderError::MissingDeliveryContract(deployment.domain),
            )?;

            let handler = builder.build(delivery_contract, deposit_contract).await?;
            chains.insert(deployment.domain, Arc::from(handler));
        }

        if let Some(&domain) = pending.keys().next() {
            return Err(TridentClientBuilderError::UnregisteredDeployment { domain });
        }

        Ok(TridentClient {
            chains,
            delivery,
            expiry_margins: self.expiry_margins,
        })
    }
}