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        // tnt-core v0.18.0 removed `operatorCount` from the Blueprint struct;
238        // the count is derived from the operator set via a dedicated getter.
239        let operator_count = self.get_blueprint_operator_count(blueprint_id).await?;
240
241        Ok(BlueprintInfo {
242            owner: result.owner,
243            manager: result.manager,
244            created_at: result.createdAt,
245            operator_count,
246            membership: ITangleTypes::MembershipModel::from_underlying(result.membership).into(),
247            pricing: ITangleTypes::PricingModel::from_underlying(result.pricing).into(),
248            active: result.active,
249        })
250    }
251
252    /// Get full blueprint configuration
253    pub async fn get_blueprint_config_info(&self, blueprint_id: u64) -> Result<BlueprintConfig> {
254        let result = self.get_blueprint_config(blueprint_id).await?;
255
256        Ok(BlueprintConfig {
257            membership: ITangleTypes::MembershipModel::from_underlying(result.membership).into(),
258            pricing: ITangleTypes::PricingModel::from_underlying(result.pricing).into(),
259            min_operators: result.minOperators,
260            max_operators: result.maxOperators,
261            subscription_rate: result.subscriptionRate,
262            subscription_interval: result.subscriptionInterval,
263            event_rate: result.eventRate,
264        })
265    }
266
267    /// Get services for an operator
268    ///
269    /// Queries all services and filters for those where the operator is active
270    pub async fn get_operator_services(&self, operator: Address) -> Result<Vec<u64>> {
271        let contract = self.tangle_contract();
272        let service_count: u64 = contract
273            .serviceCount()
274            .call()
275            .await
276            .map_err(|e| Error::Contract(e.to_string()))?;
277
278        let mut services = Vec::new();
279
280        for service_id in 0..service_count {
281            if self.is_service_operator(service_id, operator).await? {
282                services.push(service_id);
283            }
284        }
285
286        Ok(services)
287    }
288
289    /// Check if current operator is registered for the configured blueprint
290    pub async fn is_registered_for_blueprint(&self) -> Result<bool> {
291        let blueprint_id = self.config.settings.blueprint_id;
292        self.is_operator_registered(blueprint_id, self.account())
293            .await
294    }
295
296    /// Check if current operator is active in the restaking system
297    pub async fn is_operator_active_in_restaking(&self) -> Result<bool> {
298        self.is_operator_active(self.account()).await
299    }
300
301    /// Get current operator's stake
302    pub async fn get_own_stake(&self) -> Result<U256> {
303        self.get_operator_stake(self.account()).await
304    }
305
306    /// Fetch all blueprint summaries.
307    pub async fn list_blueprints(&self) -> Result<Vec<(u64, BlueprintInfo)>> {
308        let total = self.blueprint_count().await?;
309        let capacity = usize::try_from(total).unwrap_or(usize::MAX);
310        let mut blueprints = Vec::with_capacity(capacity);
311
312        for blueprint_id in 0..total {
313            match self.get_blueprint_info(blueprint_id).await {
314                Ok(info) => blueprints.push((blueprint_id, info)),
315                Err(err) => {
316                    tracing::warn!(
317                        %blueprint_id,
318                        error = %err,
319                        "failed to fetch blueprint info"
320                    );
321                }
322            }
323        }
324
325        Ok(blueprints)
326    }
327
328    /// Fetch all services registered on-chain.
329    pub async fn list_services(&self) -> Result<Vec<(u64, ServiceInfo)>> {
330        let total = self.service_count().await?;
331        let capacity = usize::try_from(total).unwrap_or(usize::MAX);
332        let mut services = Vec::with_capacity(capacity);
333
334        for service_id in 0..total {
335            match self.get_service_info(service_id).await {
336                Ok(info) => services.push((service_id, info)),
337                Err(err) => {
338                    tracing::warn!(
339                        %service_id,
340                        error = %err,
341                        "failed to fetch service info"
342                    );
343                }
344            }
345        }
346
347        Ok(services)
348    }
349
350    /// Fetch a single service request.
351    pub async fn get_service_request_info(&self, request_id: u64) -> Result<ServiceRequestInfo> {
352        let request = self.get_service_request(request_id).await?;
353        Ok(ServiceRequestInfo::from_contract(request_id, request))
354    }
355
356    /// List all service requests ever recorded on-chain.
357    pub async fn list_service_requests(&self) -> Result<Vec<ServiceRequestInfo>> {
358        let total = self.service_request_count().await?;
359        let capacity = usize::try_from(total).unwrap_or(usize::MAX);
360        let mut requests = Vec::with_capacity(capacity);
361
362        for request_id in 0..total {
363            match self.get_service_request(request_id).await {
364                Ok(info) => requests.push(ServiceRequestInfo::from_contract(request_id, info)),
365                Err(err) => {
366                    tracing::warn!(
367                        %request_id,
368                        error = %err,
369                        "failed to fetch service request"
370                    );
371                }
372            }
373        }
374
375        Ok(requests)
376    }
377}
378
379/// Operator security commitment for a service
380#[derive(Debug, Clone)]
381pub struct OperatorSecurityCommitment {
382    pub operator: Address,
383    pub exposure_bps: u16,
384}
385
386impl TangleClient {
387    /// Get operators with their security commitments for a service
388    pub async fn get_service_operators_with_exposure(
389        &self,
390        service_id: u64,
391    ) -> Result<Vec<OperatorSecurityCommitment>> {
392        let operators = self.get_service_operators(service_id).await?;
393
394        // For now, we return operators without exposure info
395        // The full implementation would query the contract for exposure data
396        Ok(operators
397            .into_iter()
398            .map(|operator| OperatorSecurityCommitment {
399                operator,
400                exposure_bps: 10000, // 100% default
401            })
402            .collect())
403    }
404}