Skip to main content

trident_sdk/
builder.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use tonic::transport::Endpoint;
5
6use crate::chain_handler::{ChainHandler, ChainHandlerBuildError, ChainHandlerBuilder};
7use crate::proto::delivery_v1;
8use crate::proto::delivery_v1::delivery_service_client::DeliveryServiceClient;
9use crate::{Domain, TridentClient};
10
11/// Errors returned while building and connecting a [`TridentClient`].
12///
13/// Covers transport and gRPC failures, missing contract addresses in the
14/// delivery service's deployment response, per-chain-family build failures,
15/// and mismatches between locally registered domains and server-reported
16/// deployments.
17#[derive(Debug, thiserror::Error)]
18pub enum TridentClientBuilderError {
19    /// gRPC transport could not be established against the configured endpoint.
20    #[error("Failed to connect to delivery service: {0}")]
21    Transport(#[from] tonic::transport::Error),
22
23    /// The delivery service rejected the `list_deployments` call.
24    #[error("Grpc: {0}")]
25    Grpc(#[from] tonic::Status),
26
27    /// The server's deployment response for `domain` was missing its source
28    /// `deposit_contract` address.
29    #[error("Missing deposit_contract for domain {0}")]
30    MissingDepositContract(Domain),
31
32    /// The server's deployment response for `domain` was missing its
33    /// destination `delivery_contract` address.
34    #[error("Missing delivery_contract for domain {0}")]
35    MissingDeliveryContract(Domain),
36
37    /// A chain-family-specific handler builder rejected the deployment data
38    /// (e.g. address decoding failure).
39    #[error(transparent)]
40    ChainHandlerBuild(#[from] ChainHandlerBuildError),
41
42    /// A domain was registered locally but the server returned no matching
43    /// deployment for it.
44    #[error("No deployment returned for registered domain {domain}")]
45    UnregisteredDeployment { domain: Domain },
46}
47
48/// Builds a [`TridentClient`] by registering one chain-handler builder per
49/// domain and connecting to the delivery service.
50///
51/// Chain-family-specific entry points (e.g. `with_evm_chain` from the `evm`
52/// module) wrap the generic internal chain-registration call.
53pub struct TridentClientBuilder {
54    endpoint: Endpoint,
55    pending: HashMap<Domain, Box<dyn ChainHandlerBuilder>>,
56    expiry_margins: HashMap<Domain, u64>,
57}
58
59impl TridentClientBuilder {
60    /// Creates an empty builder targeting the given delivery-service gRPC
61    /// endpoint.
62    pub fn new(endpoint: Endpoint) -> Self {
63        Self {
64            endpoint,
65            pending: HashMap::new(),
66            expiry_margins: HashMap::new(),
67        }
68    }
69
70    /// Registers a [`ChainHandlerBuilder`] for `domain`.
71    ///
72    /// Chain-family-specific convenience methods (e.g. `with_evm_chain` from
73    /// the `evm` module) call this internally.
74    pub(crate) fn with_chain<B>(mut self, domain: Domain, builder: B) -> Self
75    where
76        B: ChainHandlerBuilder + 'static,
77    {
78        self.pending.insert(domain, Box::new(builder));
79        self
80    }
81
82    /// Sets the flat number of blocks added on top of the server's minimum
83    /// spend-intent expiry height for `domain` when the SDK auto-resolves
84    /// the expiry. Domains without an entry fall back to 10% of the server's
85    /// minimum.
86    pub fn with_expiry_margin(mut self, domain: Domain, margin: u64) -> Self {
87        self.expiry_margins.insert(domain, margin);
88        self
89    }
90
91    /// Connects to the delivery service and materializes a [`TridentClient`].
92    ///
93    /// Fetches the deployment list from the server and, for each registered
94    /// domain, invokes the corresponding chain-handler builder with the
95    /// server-reported deposit and delivery contract addresses. Fails if a
96    /// deployment is missing either contract address, if a chain-handler
97    /// builder rejects its inputs, or if a registered domain has no matching
98    /// deployment.
99    pub async fn connect(self) -> Result<TridentClient, TridentClientBuilderError> {
100        let channel = self.endpoint.connect().await?;
101        let mut delivery = DeliveryServiceClient::new(channel);
102        let deployments = delivery
103            .list_deployments(delivery_v1::ListDeploymentsRequest {})
104            .await?
105            .into_inner();
106
107        let mut pending = self.pending;
108        let mut chains: HashMap<Domain, Arc<dyn ChainHandler>> = HashMap::new();
109
110        for deployment in deployments.deployments {
111            let Some(builder) = pending.remove(&deployment.domain) else {
112                continue;
113            };
114            let deposit_contract = deployment.deposit_contract.ok_or(
115                TridentClientBuilderError::MissingDepositContract(deployment.domain),
116            )?;
117            let delivery_contract = deployment.delivery_contract.ok_or(
118                TridentClientBuilderError::MissingDeliveryContract(deployment.domain),
119            )?;
120
121            let handler = builder.build(delivery_contract, deposit_contract).await?;
122            chains.insert(deployment.domain, Arc::from(handler));
123        }
124
125        if let Some(&domain) = pending.keys().next() {
126            return Err(TridentClientBuilderError::UnregisteredDeployment { domain });
127        }
128
129        Ok(TridentClient {
130            chains,
131            delivery,
132            expiry_margins: self.expiry_margins,
133        })
134    }
135}