Skip to main content

blueprint_client_tangle/
services.rs

1//! Tangle Services Client
2//!
3//! Service-specific queries and operations for Tangle contracts.
4
5#![allow(missing_docs)]
6
7extern crate alloc;
8
9use alloc::string::ToString;
10use alloy_primitives::{Address, Bytes, U256};
11use blueprint_std::vec::Vec;
12
13use crate::client::TangleClient;
14use crate::contracts::ITangleTypes;
15use crate::error::{Error, Result};
16
17/// Service information from the Tangle contract
18#[derive(Debug, Clone)]
19pub struct ServiceInfo {
20    pub blueprint_id: u64,
21    pub owner: Address,
22    pub created_at: u64,
23    pub ttl: u64,
24    pub terminated_at: u64,
25    pub last_payment_at: u64,
26    pub operator_count: u32,
27    pub min_operators: u32,
28    pub max_operators: u32,
29    pub membership: MembershipModel,
30    pub pricing: PricingModel,
31    pub status: ServiceStatus,
32}
33
34/// Blueprint information from the Tangle contract
35#[derive(Debug, Clone)]
36pub struct BlueprintInfo {
37    pub owner: Address,
38    pub manager: Address,
39    pub created_at: u64,
40    pub operator_count: u32,
41    pub membership: MembershipModel,
42    pub pricing: PricingModel,
43    pub active: bool,
44}
45
46/// Blueprint configuration
47#[derive(Debug, Clone)]
48pub struct BlueprintConfig {
49    pub membership: MembershipModel,
50    pub pricing: PricingModel,
51    pub min_operators: u32,
52    pub max_operators: u32,
53    pub subscription_rate: U256,
54    pub subscription_interval: u64,
55    pub event_rate: U256,
56}
57
58/// Membership model
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum MembershipModel {
61    Fixed,
62    Dynamic,
63}
64
65/// Pricing model
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum PricingModel {
68    PayOnce,
69    Subscription,
70    EventDriven,
71}
72
73/// Service status
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum ServiceStatus {
76    Pending,
77    Active,
78    Terminated,
79}
80
81/// Parameters for submitting a new service request.
82#[derive(Debug, Clone)]
83pub struct ServiceRequestParams {
84    /// Blueprint being requested.
85    pub blueprint_id: u64,
86    /// Candidate operators for the service.
87    pub operators: Vec<Address>,
88    /// Optional exposure (in basis points) for each operator.
89    pub operator_exposures: Option<Vec<u16>>,
90    /// Additional permitted callers (beyond the requester).
91    pub permitted_callers: Vec<Address>,
92    /// Arbitrary configuration blob expected by the blueprint manager.
93    pub config: Bytes,
94    /// Time-to-live in blocks.
95    pub ttl: u64,
96    /// Payment token (zero address for ETH).
97    pub payment_token: Address,
98    /// Payment amount.
99    pub payment_amount: U256,
100    /// Optional security requirements (falls back to simple exposure flow when empty).
101    pub security_requirements: Vec<ITangleTypes::AssetSecurityRequirement>,
102}
103
104impl ServiceRequestParams {
105    /// Create a new parameter set without security requirements.
106    pub fn new(
107        blueprint_id: u64,
108        operators: Vec<Address>,
109        permitted_callers: Vec<Address>,
110        config: Bytes,
111        ttl: u64,
112        payment_token: Address,
113        payment_amount: U256,
114    ) -> Self {
115        Self {
116            blueprint_id,
117            operators,
118            operator_exposures: None,
119            permitted_callers,
120            config,
121            ttl,
122            payment_token,
123            payment_amount,
124            security_requirements: Vec::new(),
125        }
126    }
127}
128
129/// Details about a service request stored on-chain.
130#[derive(Debug, Clone)]
131pub struct ServiceRequestInfo {
132    /// Request identifier.
133    pub request_id: u64,
134    /// Blueprint being requested.
135    pub blueprint_id: u64,
136    /// Address that created the request.
137    pub requester: Address,
138    /// Block timestamp when request was created.
139    pub created_at: u64,
140    /// Request time-to-live in blocks.
141    pub ttl: u64,
142    /// Number of operators requested.
143    pub operator_count: u32,
144    /// Number of approvals the request has received.
145    pub approval_count: u32,
146    /// ERC-20 token used for payment (zero address for ETH).
147    pub payment_token: Address,
148    /// Payment amount for the request.
149    pub payment_amount: U256,
150    /// Membership model requested.
151    pub membership: MembershipModel,
152    /// Minimum operators allowed.
153    pub min_operators: u32,
154    /// Maximum operators allowed.
155    pub max_operators: u32,
156    /// Whether the request has been rejected.
157    pub rejected: bool,
158}
159
160impl ServiceRequestInfo {
161    fn from_contract(id: u64, request: ITangleTypes::ServiceRequest) -> Self {
162        Self {
163            request_id: id,
164            blueprint_id: request.blueprintId,
165            requester: request.requester,
166            created_at: request.createdAt,
167            ttl: request.ttl,
168            operator_count: request.operatorCount,
169            approval_count: request.approvalCount,
170            payment_token: request.paymentToken,
171            payment_amount: request.paymentAmount,
172            membership: ITangleTypes::MembershipModel::from_underlying(request.membership).into(),
173            min_operators: request.minOperators,
174            max_operators: request.maxOperators,
175            rejected: request.rejected,
176        }
177    }
178}
179
180impl From<ITangleTypes::MembershipModel> for MembershipModel {
181    fn from(model: ITangleTypes::MembershipModel) -> Self {
182        match model.into_underlying() {
183            0 => MembershipModel::Fixed,
184            1 => MembershipModel::Dynamic,
185            _ => MembershipModel::Fixed,
186        }
187    }
188}
189
190impl From<ITangleTypes::PricingModel> for PricingModel {
191    fn from(model: ITangleTypes::PricingModel) -> Self {
192        match model.into_underlying() {
193            0 => PricingModel::PayOnce,
194            1 => PricingModel::Subscription,
195            2 => PricingModel::EventDriven,
196            _ => PricingModel::PayOnce,
197        }
198    }
199}
200
201impl From<ITangleTypes::ServiceStatus> for ServiceStatus {
202    fn from(status: ITangleTypes::ServiceStatus) -> Self {
203        match status.into_underlying() {
204            0 => ServiceStatus::Pending,
205            1 => ServiceStatus::Active,
206            2 => ServiceStatus::Terminated,
207            _ => ServiceStatus::Pending,
208        }
209    }
210}
211
212/// Extension trait for service-related operations on TangleClient
213impl TangleClient {
214    /// Get full service information
215    pub async fn get_service_info(&self, service_id: u64) -> Result<ServiceInfo> {
216        let result = self.get_service(service_id).await?;
217
218        Ok(ServiceInfo {
219            blueprint_id: result.blueprintId,
220            owner: result.owner,
221            created_at: result.createdAt,
222            ttl: result.ttl,
223            terminated_at: result.terminatedAt,
224            last_payment_at: result.lastPaymentAt,
225            operator_count: result.operatorCount,
226            min_operators: result.minOperators,
227            max_operators: result.maxOperators,
228            membership: ITangleTypes::MembershipModel::from_underlying(result.membership).into(),
229            pricing: ITangleTypes::PricingModel::from_underlying(result.pricing).into(),
230            status: ITangleTypes::ServiceStatus::from_underlying(result.status).into(),
231        })
232    }
233
234    /// Get full blueprint information
235    pub async fn get_blueprint_info(&self, blueprint_id: u64) -> Result<BlueprintInfo> {
236        let result = self.get_blueprint(blueprint_id).await?;
237
238        Ok(BlueprintInfo {
239            owner: result.owner,
240            manager: result.manager,
241            created_at: result.createdAt,
242            operator_count: result.operatorCount,
243            membership: ITangleTypes::MembershipModel::from_underlying(result.membership).into(),
244            pricing: ITangleTypes::PricingModel::from_underlying(result.pricing).into(),
245            active: result.active,
246        })
247    }
248
249    /// Get full blueprint configuration
250    pub async fn get_blueprint_config_info(&self, blueprint_id: u64) -> Result<BlueprintConfig> {
251        let result = self.get_blueprint_config(blueprint_id).await?;
252
253        Ok(BlueprintConfig {
254            membership: ITangleTypes::MembershipModel::from_underlying(result.membership).into(),
255            pricing: ITangleTypes::PricingModel::from_underlying(result.pricing).into(),
256            min_operators: result.minOperators,
257            max_operators: result.maxOperators,
258            subscription_rate: result.subscriptionRate,
259            subscription_interval: result.subscriptionInterval,
260            event_rate: result.eventRate,
261        })
262    }
263
264    /// Get services for an operator
265    ///
266    /// Queries all services and filters for those where the operator is active
267    pub async fn get_operator_services(&self, operator: Address) -> Result<Vec<u64>> {
268        let contract = self.tangle_contract();
269        let service_count: u64 = contract
270            .serviceCount()
271            .call()
272            .await
273            .map_err(|e| Error::Contract(e.to_string()))?;
274
275        let mut services = Vec::new();
276
277        for service_id in 0..service_count {
278            if self.is_service_operator(service_id, operator).await? {
279                services.push(service_id);
280            }
281        }
282
283        Ok(services)
284    }
285
286    /// Check if current operator is registered for the configured blueprint
287    pub async fn is_registered_for_blueprint(&self) -> Result<bool> {
288        let blueprint_id = self.config.settings.blueprint_id;
289        self.is_operator_registered(blueprint_id, self.account())
290            .await
291    }
292
293    /// Check if current operator is active in the restaking system
294    pub async fn is_operator_active_in_restaking(&self) -> Result<bool> {
295        self.is_operator_active(self.account()).await
296    }
297
298    /// Get current operator's stake
299    pub async fn get_own_stake(&self) -> Result<U256> {
300        self.get_operator_stake(self.account()).await
301    }
302
303    /// Fetch all blueprint summaries.
304    pub async fn list_blueprints(&self) -> Result<Vec<(u64, BlueprintInfo)>> {
305        let total = self.blueprint_count().await?;
306        let capacity = usize::try_from(total).unwrap_or(usize::MAX);
307        let mut blueprints = Vec::with_capacity(capacity);
308
309        for blueprint_id in 0..total {
310            match self.get_blueprint_info(blueprint_id).await {
311                Ok(info) => blueprints.push((blueprint_id, info)),
312                Err(err) => {
313                    tracing::warn!(
314                        %blueprint_id,
315                        error = %err,
316                        "failed to fetch blueprint info"
317                    );
318                }
319            }
320        }
321
322        Ok(blueprints)
323    }
324
325    /// Fetch all services registered on-chain.
326    pub async fn list_services(&self) -> Result<Vec<(u64, ServiceInfo)>> {
327        let total = self.service_count().await?;
328        let capacity = usize::try_from(total).unwrap_or(usize::MAX);
329        let mut services = Vec::with_capacity(capacity);
330
331        for service_id in 0..total {
332            match self.get_service_info(service_id).await {
333                Ok(info) => services.push((service_id, info)),
334                Err(err) => {
335                    tracing::warn!(
336                        %service_id,
337                        error = %err,
338                        "failed to fetch service info"
339                    );
340                }
341            }
342        }
343
344        Ok(services)
345    }
346
347    /// Fetch a single service request.
348    pub async fn get_service_request_info(&self, request_id: u64) -> Result<ServiceRequestInfo> {
349        let request = self.get_service_request(request_id).await?;
350        Ok(ServiceRequestInfo::from_contract(request_id, request))
351    }
352
353    /// List all service requests ever recorded on-chain.
354    pub async fn list_service_requests(&self) -> Result<Vec<ServiceRequestInfo>> {
355        let total = self.service_request_count().await?;
356        let capacity = usize::try_from(total).unwrap_or(usize::MAX);
357        let mut requests = Vec::with_capacity(capacity);
358
359        for request_id in 0..total {
360            match self.get_service_request(request_id).await {
361                Ok(info) => requests.push(ServiceRequestInfo::from_contract(request_id, info)),
362                Err(err) => {
363                    tracing::warn!(
364                        %request_id,
365                        error = %err,
366                        "failed to fetch service request"
367                    );
368                }
369            }
370        }
371
372        Ok(requests)
373    }
374}
375
376/// Operator security commitment for a service
377#[derive(Debug, Clone)]
378pub struct OperatorSecurityCommitment {
379    pub operator: Address,
380    pub exposure_bps: u16,
381}
382
383impl TangleClient {
384    /// Get operators with their security commitments for a service
385    pub async fn get_service_operators_with_exposure(
386        &self,
387        service_id: u64,
388    ) -> Result<Vec<OperatorSecurityCommitment>> {
389        let operators = self.get_service_operators(service_id).await?;
390
391        // For now, we return operators without exposure info
392        // The full implementation would query the contract for exposure data
393        Ok(operators
394            .into_iter()
395            .map(|operator| OperatorSecurityCommitment {
396                operator,
397                exposure_bps: 10000, // 100% default
398            })
399            .collect())
400    }
401}