Skip to main content

blueprint_client_tangle/
client.rs

1//! Tangle Client
2//!
3//! Provides connectivity to Tangle contracts for blueprint operators.
4
5extern crate alloc;
6
7use alloc::format;
8use alloc::string::{String, ToString};
9use alloc::vec;
10use alloy_network::Ethereum;
11use alloy_primitives::{Address, B256, Bytes, TxKind, U256, keccak256};
12use alloy_provider::{DynProvider, Provider, ProviderBuilder};
13use alloy_rpc_types::{
14    Block, BlockNumberOrTag, Filter, Log, TransactionReceipt,
15    transaction::{TransactionInput, TransactionRequest},
16};
17use alloy_sol_types::SolType;
18use blueprint_client_core::{BlueprintServicesClient, OperatorSet};
19use blueprint_crypto::k256::K256Ecdsa;
20use blueprint_keystore::Keystore;
21use blueprint_keystore::backends::Backend;
22use blueprint_std::collections::BTreeMap;
23use blueprint_std::sync::Arc;
24use blueprint_std::vec::Vec;
25use core::fmt;
26use core::time::Duration;
27use k256::elliptic_curve::sec1::ToEncodedPoint;
28use tokio::sync::Mutex;
29
30use crate::config::TangleClientConfig;
31use crate::contracts::{
32    IBlueprintServiceManager, IMultiAssetDelegation, IMultiAssetDelegationTypes,
33    IOperatorStatusRegistry, ITangle, ITangleTypes,
34};
35use crate::error::{Error, Result};
36use crate::services::ServiceRequestParams;
37use IMultiAssetDelegation::IMultiAssetDelegationInstance;
38use IOperatorStatusRegistry::IOperatorStatusRegistryInstance;
39use ITangle::ITangleInstance;
40
41#[allow(missing_docs)]
42mod erc20 {
43    alloy_sol_types::sol! {
44        #[sol(rpc)]
45        interface IERC20 {
46            function approve(address spender, uint256 amount) external returns (bool);
47            function allowance(address owner, address spender) external view returns (uint256);
48            function balanceOf(address owner) external view returns (uint256);
49        }
50    }
51}
52
53use erc20::IERC20;
54
55/// Type alias for the dynamic provider
56pub type TangleProvider = DynProvider<Ethereum>;
57
58/// Type alias for ECDSA public key (uncompressed, 65 bytes)
59pub type EcdsaPublicKey = [u8; 65];
60
61/// Type alias for compressed ECDSA public key (33 bytes)
62pub type CompressedEcdsaPublicKey = [u8; 33];
63
64/// Restaking-specific metadata for an operator.
65#[derive(Debug, Clone)]
66pub struct RestakingMetadata {
67    /// Operator self-stake amount (in wei).
68    pub stake: U256,
69    /// Number of delegations attached to this operator.
70    pub delegation_count: u32,
71    /// Whether the operator is active inside MultiAssetDelegation.
72    pub status: RestakingStatus,
73    /// Round when the operator scheduled a voluntary exit.
74    pub leaving_round: u64,
75}
76
77/// Restaking status reported by MultiAssetDelegation.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum RestakingStatus {
80    /// Operator is active.
81    Active,
82    /// Operator is inactive (e.g., kicked or never joined).
83    Inactive,
84    /// Operator scheduled a leave operation.
85    Leaving,
86    /// Unknown status value (future-proofing).
87    Unknown(u8),
88}
89
90impl From<u8> for RestakingStatus {
91    fn from(value: u8) -> Self {
92        match value {
93            0 => RestakingStatus::Active,
94            1 => RestakingStatus::Inactive,
95            2 => RestakingStatus::Leaving,
96            other => RestakingStatus::Unknown(other),
97        }
98    }
99}
100
101/// Delegation mode for an operator.
102///
103/// Controls who can delegate to the operator.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum DelegationMode {
106    /// Disabled: Only operator can self-stake (default).
107    Disabled,
108    /// Whitelist: Only approved addresses can delegate.
109    Whitelist,
110    /// Open: Anyone can delegate.
111    Open,
112    /// Unknown mode value (future-proofing).
113    Unknown(u8),
114}
115
116impl From<u8> for DelegationMode {
117    fn from(value: u8) -> Self {
118        match value {
119            0 => DelegationMode::Disabled,
120            1 => DelegationMode::Whitelist,
121            2 => DelegationMode::Open,
122            other => DelegationMode::Unknown(other),
123        }
124    }
125}
126
127impl fmt::Display for DelegationMode {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            DelegationMode::Disabled => write!(f, "disabled"),
131            DelegationMode::Whitelist => write!(f, "whitelist"),
132            DelegationMode::Open => write!(f, "open"),
133            DelegationMode::Unknown(value) => write!(f, "unknown({value})"),
134        }
135    }
136}
137
138/// Asset kinds supported by MultiAssetDelegation.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum AssetKind {
141    /// Native asset (e.g. ETH).
142    Native,
143    /// ERC-20 token.
144    Erc20,
145    /// Unknown asset kind value.
146    Unknown(u8),
147}
148
149impl From<u8> for AssetKind {
150    fn from(value: u8) -> Self {
151        match value {
152            0 => AssetKind::Native,
153            1 => AssetKind::Erc20,
154            other => AssetKind::Unknown(other),
155        }
156    }
157}
158
159impl fmt::Display for AssetKind {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            AssetKind::Native => write!(f, "native"),
163            AssetKind::Erc20 => write!(f, "erc20"),
164            AssetKind::Unknown(value) => write!(f, "unknown({value})"),
165        }
166    }
167}
168
169/// Blueprint selection mode for a delegation.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum BlueprintSelectionMode {
172    /// Delegate across all blueprints.
173    All,
174    /// Delegate to a fixed set of blueprints.
175    Fixed,
176    /// Unknown selection mode value.
177    Unknown(u8),
178}
179
180impl From<u8> for BlueprintSelectionMode {
181    fn from(value: u8) -> Self {
182        match value {
183            0 => BlueprintSelectionMode::All,
184            1 => BlueprintSelectionMode::Fixed,
185            other => BlueprintSelectionMode::Unknown(other),
186        }
187    }
188}
189
190impl fmt::Display for BlueprintSelectionMode {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        match self {
193            BlueprintSelectionMode::All => write!(f, "all"),
194            BlueprintSelectionMode::Fixed => write!(f, "fixed"),
195            BlueprintSelectionMode::Unknown(value) => write!(f, "unknown({value})"),
196        }
197    }
198}
199
200/// Lock multiplier tier for a deposit.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum LockMultiplier {
203    /// No lock multiplier.
204    None,
205    /// One-month lock.
206    OneMonth,
207    /// Two-month lock.
208    TwoMonths,
209    /// Three-month lock.
210    ThreeMonths,
211    /// Six-month lock.
212    SixMonths,
213    /// Unknown multiplier value.
214    Unknown(u8),
215}
216
217impl From<u8> for LockMultiplier {
218    fn from(value: u8) -> Self {
219        match value {
220            0 => LockMultiplier::None,
221            1 => LockMultiplier::OneMonth,
222            2 => LockMultiplier::TwoMonths,
223            3 => LockMultiplier::ThreeMonths,
224            4 => LockMultiplier::SixMonths,
225            other => LockMultiplier::Unknown(other),
226        }
227    }
228}
229
230impl fmt::Display for LockMultiplier {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        match self {
233            LockMultiplier::None => write!(f, "none"),
234            LockMultiplier::OneMonth => write!(f, "one-month"),
235            LockMultiplier::TwoMonths => write!(f, "two-months"),
236            LockMultiplier::ThreeMonths => write!(f, "three-months"),
237            LockMultiplier::SixMonths => write!(f, "six-months"),
238            LockMultiplier::Unknown(value) => write!(f, "unknown({value})"),
239        }
240    }
241}
242
243/// Asset specification returned by MultiAssetDelegation.
244#[derive(Debug, Clone)]
245pub struct AssetInfo {
246    /// Asset kind identifier.
247    pub kind: AssetKind,
248    /// Token contract address (zero for native).
249    pub token: Address,
250}
251
252/// Deposit summary for a delegator and token.
253#[derive(Debug, Clone)]
254pub struct DepositInfo {
255    /// Total deposited amount.
256    pub amount: U256,
257    /// Portion already delegated.
258    pub delegated_amount: U256,
259}
260
261/// Lock details for a delegator and token.
262#[derive(Debug, Clone)]
263pub struct LockInfo {
264    /// Locked amount.
265    pub amount: U256,
266    /// Multiplier tier.
267    pub multiplier: LockMultiplier,
268    /// Block when lock expires.
269    pub expiry_block: u64,
270}
271
272/// Delegation info for a delegator.
273#[derive(Debug, Clone)]
274pub struct DelegationInfo {
275    /// Operator address.
276    pub operator: Address,
277    /// Delegated shares.
278    pub shares: U256,
279    /// Asset metadata.
280    pub asset: AssetInfo,
281    /// Blueprint selection mode.
282    pub selection_mode: BlueprintSelectionMode,
283}
284
285/// Delegation with optional blueprint selections.
286#[derive(Debug, Clone)]
287pub struct DelegationRecord {
288    /// Delegation metadata.
289    pub info: DelegationInfo,
290    /// Selected blueprint IDs (fixed mode only).
291    pub blueprint_ids: Vec<u64>,
292}
293
294/// Pending delegator unstake request.
295#[derive(Debug, Clone)]
296pub struct PendingUnstake {
297    /// Operator address.
298    pub operator: Address,
299    /// Asset metadata.
300    pub asset: AssetInfo,
301    /// Shares scheduled to unstake.
302    pub shares: U256,
303    /// Round when the unstake was requested.
304    pub requested_round: u64,
305    /// Blueprint selection mode.
306    pub selection_mode: BlueprintSelectionMode,
307    /// Slash factor snapshot at request time.
308    pub slash_factor_snapshot: U256,
309}
310
311/// Pending delegator withdrawal request.
312#[derive(Debug, Clone)]
313pub struct PendingWithdrawal {
314    /// Asset metadata.
315    pub asset: AssetInfo,
316    /// Amount requested for withdrawal.
317    pub amount: U256,
318    /// Round when the withdrawal was requested.
319    pub requested_round: u64,
320}
321
322/// Metadata associated with a registered operator.
323#[derive(Debug, Clone)]
324pub struct OperatorMetadata {
325    /// Operator's uncompressed ECDSA public key used for gossip/aggregation.
326    pub public_key: EcdsaPublicKey,
327    /// Operator-provided RPC endpoint.
328    pub rpc_endpoint: String,
329    /// Restaking information pulled from MultiAssetDelegation.
330    pub restaking: RestakingMetadata,
331}
332
333/// Snapshot of an operator's heartbeat/status entry.
334#[derive(Debug, Clone)]
335pub struct OperatorStatusSnapshot {
336    /// Service being inspected.
337    pub service_id: u64,
338    /// Operator address.
339    pub operator: Address,
340    /// Raw status code recorded on-chain.
341    pub status_code: u8,
342    /// Last heartbeat timestamp (Unix seconds).
343    pub last_heartbeat: u64,
344    /// Whether the operator is currently marked online.
345    pub online: bool,
346}
347
348/// Event from Tangle contracts
349#[derive(Clone, Debug)]
350pub struct TangleEvent {
351    /// Block number
352    pub block_number: u64,
353    /// Block hash
354    pub block_hash: B256,
355    /// Block timestamp
356    pub timestamp: u64,
357    /// Logs from the block
358    pub logs: Vec<Log>,
359}
360
361/// Tangle Client for interacting with Tangle v2 contracts
362#[derive(Clone)]
363pub struct TangleClient {
364    /// RPC provider
365    provider: Arc<TangleProvider>,
366    /// Tangle contract address
367    tangle_address: Address,
368    /// MultiAssetDelegation contract address
369    restaking_address: Address,
370    /// Operator status registry contract address
371    status_registry_address: Address,
372    /// Operator's account address
373    account: Address,
374    /// Client configuration
375    pub config: TangleClientConfig,
376    /// Keystore for signing
377    keystore: Arc<Keystore>,
378    /// Latest block tracking
379    latest_block: Arc<Mutex<Option<TangleEvent>>>,
380    /// Current block subscription
381    block_subscription: Arc<Mutex<Option<u64>>>,
382}
383
384#[allow(clippy::missing_fields_in_debug)] // provider/signer/subscription intentionally omitted
385impl fmt::Debug for TangleClient {
386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387        f.debug_struct("TangleClient")
388            .field("tangle_address", &self.tangle_address)
389            .field("restaking_address", &self.restaking_address)
390            .field("status_registry_address", &self.status_registry_address)
391            .field("account", &self.account)
392            .finish()
393    }
394}
395
396impl TangleClient {
397    /// Create a new Tangle client from configuration
398    ///
399    /// # Arguments
400    /// * `config` - Client configuration
401    ///
402    /// # Errors
403    /// Returns error if keystore initialization fails or RPC connection fails
404    pub async fn new(config: TangleClientConfig) -> Result<Self> {
405        let keystore = Keystore::new(config.keystore_config())?;
406        Self::with_keystore(config, keystore).await
407    }
408
409    /// Create a new Tangle client with an existing keystore
410    ///
411    /// # Arguments
412    /// * `config` - Client configuration
413    /// * `keystore` - Keystore instance
414    ///
415    /// # Errors
416    /// Returns error if RPC connection fails
417    pub async fn with_keystore(config: TangleClientConfig, keystore: Keystore) -> Result<Self> {
418        let rpc_url = config.http_rpc_endpoint.as_str();
419
420        // Create provider and wrap in DynProvider for type erasure
421        let provider = ProviderBuilder::new()
422            .connect(rpc_url)
423            .await
424            .map_err(|e| Error::Config(e.to_string()))?;
425
426        let dyn_provider = DynProvider::new(provider);
427
428        // Get operator's address from keystore (using ECDSA key)
429        let ecdsa_key = keystore
430            .first_local::<K256Ecdsa>()
431            .map_err(Error::Keystore)?;
432
433        // Convert ECDSA public key to Ethereum address
434        // The key.0 is a VerifyingKey - extract the bytes from it
435        let pubkey_bytes = ecdsa_key.0.to_encoded_point(false);
436        let account = ecdsa_public_key_to_address(pubkey_bytes.as_bytes())?;
437
438        Ok(Self {
439            provider: Arc::new(dyn_provider),
440            tangle_address: config.settings.tangle_contract,
441            restaking_address: config.settings.restaking_contract,
442            status_registry_address: config.settings.status_registry_contract,
443            account,
444            config,
445            keystore: Arc::new(keystore),
446            latest_block: Arc::new(Mutex::new(None)),
447            block_subscription: Arc::new(Mutex::new(None)),
448        })
449    }
450
451    /// Get the Tangle contract instance
452    pub fn tangle_contract(&self) -> ITangleInstance<Arc<TangleProvider>> {
453        ITangleInstance::new(self.tangle_address, Arc::clone(&self.provider))
454    }
455
456    /// Get the MultiAssetDelegation contract instance
457    pub fn restaking_contract(&self) -> IMultiAssetDelegationInstance<Arc<TangleProvider>> {
458        IMultiAssetDelegation::new(self.restaking_address, Arc::clone(&self.provider))
459    }
460
461    /// Get the operator status registry contract instance
462    pub fn status_registry_contract(&self) -> IOperatorStatusRegistryInstance<Arc<TangleProvider>> {
463        IOperatorStatusRegistryInstance::new(
464            self.status_registry_address,
465            Arc::clone(&self.provider),
466        )
467    }
468
469    /// Get the operator's account address
470    #[must_use]
471    pub fn account(&self) -> Address {
472        self.account
473    }
474
475    /// Get the keystore
476    #[must_use]
477    pub fn keystore(&self) -> &Arc<Keystore> {
478        &self.keystore
479    }
480
481    /// Get the provider
482    #[must_use]
483    pub fn provider(&self) -> &Arc<TangleProvider> {
484        &self.provider
485    }
486
487    /// Get the Tangle contract address
488    #[must_use]
489    pub fn tangle_address(&self) -> Address {
490        self.tangle_address
491    }
492
493    /// Get the ECDSA signing key from the keystore
494    ///
495    /// # Errors
496    /// Returns error if the key is not found in the keystore
497    pub fn ecdsa_signing_key(&self) -> Result<blueprint_crypto::k256::K256SigningKey> {
498        let public = self
499            .keystore
500            .first_local::<K256Ecdsa>()
501            .map_err(Error::Keystore)?;
502        self.keystore
503            .get_secret::<K256Ecdsa>(&public)
504            .map_err(Error::Keystore)
505    }
506
507    /// Get an Ethereum wallet for signing transactions
508    ///
509    /// # Errors
510    /// Returns error if the key is not found or wallet creation fails
511    pub fn wallet(&self) -> Result<alloy_network::EthereumWallet> {
512        let signing_key = self.ecdsa_signing_key()?;
513        let local_signer = signing_key
514            .alloy_key()
515            .map_err(|e| Error::Keystore(blueprint_keystore::Error::Other(e.to_string())))?;
516        Ok(alloy_network::EthereumWallet::from(local_signer))
517    }
518
519    /// Get the current block number
520    pub async fn block_number(&self) -> Result<u64> {
521        self.provider
522            .get_block_number()
523            .await
524            .map_err(Error::Transport)
525    }
526
527    /// Get a block by number
528    pub async fn get_block(&self, number: BlockNumberOrTag) -> Result<Option<Block>> {
529        self.provider
530            .get_block_by_number(number)
531            .await
532            .map_err(Error::Transport)
533    }
534
535    /// Get logs matching a filter
536    pub async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>> {
537        self.provider
538            .get_logs(filter)
539            .await
540            .map_err(Error::Transport)
541    }
542
543    /// Get the next event (polls for new blocks)
544    ///
545    /// On the first call, scans from block 0 to catch up on any historical
546    /// events (e.g. ServiceActivated) that were emitted before the client
547    /// started.  Subsequent calls only scan new blocks.
548    pub async fn next_event(&self) -> Option<TangleEvent> {
549        loop {
550            let current_block = self.block_number().await.ok()?;
551
552            let mut last_block = self.block_subscription.lock().await;
553            let from_block = last_block.map(|b| b + 1).unwrap_or(0);
554
555            if from_block > current_block {
556                drop(last_block);
557                tokio::time::sleep(Duration::from_secs(1)).await;
558                continue;
559            }
560
561            // Get block info
562            let block = self
563                .get_block(BlockNumberOrTag::Number(current_block))
564                .await
565                .ok()??;
566
567            // Create filter for Tangle contract events
568            let filter = Filter::new()
569                .address(self.tangle_address)
570                .from_block(from_block)
571                .to_block(current_block);
572
573            let logs = self.get_logs(&filter).await.ok()?;
574
575            *last_block = Some(current_block);
576
577            let event = TangleEvent {
578                block_number: current_block,
579                block_hash: block.header.hash,
580                timestamp: block.header.timestamp,
581                logs,
582            };
583
584            // Update latest
585            *self.latest_block.lock().await = Some(event.clone());
586
587            return Some(event);
588        }
589    }
590
591    /// Get the latest observed event
592    pub async fn latest_event(&self) -> Option<TangleEvent> {
593        let latest = self.latest_block.lock().await;
594        match &*latest {
595            Some(event) => Some(event.clone()),
596            None => {
597                drop(latest);
598                self.next_event().await
599            }
600        }
601    }
602
603    /// Get the current block hash
604    pub async fn now(&self) -> Option<B256> {
605        Some(self.latest_event().await?.block_hash)
606    }
607
608    // ═══════════════════════════════════════════════════════════════════════════
609    // BLUEPRINT QUERIES
610    // ═══════════════════════════════════════════════════════════════════════════
611
612    /// Get blueprint information
613    pub async fn get_blueprint(&self, blueprint_id: u64) -> Result<ITangleTypes::Blueprint> {
614        let contract = self.tangle_contract();
615        let result = contract
616            .getBlueprint(blueprint_id)
617            .call()
618            .await
619            .map_err(|e| Error::Contract(e.to_string()))?;
620        Ok(result)
621    }
622
623    /// Fetch the raw ABI-encoded blueprint definition bytes.
624    pub async fn get_raw_blueprint_definition(&self, blueprint_id: u64) -> Result<Vec<u8>> {
625        let mut data = Vec::with_capacity(4 + 32);
626        let method_hash = keccak256("getBlueprintDefinition(uint64)".as_bytes());
627        data.extend_from_slice(&method_hash[..4]);
628        let mut arg = [0u8; 32];
629        arg[24..].copy_from_slice(&blueprint_id.to_be_bytes());
630        data.extend_from_slice(&arg);
631
632        let mut request = TransactionRequest::default();
633        request.to = Some(TxKind::Call(self.tangle_address));
634        request.input = TransactionInput::new(Bytes::from(data));
635
636        let response = self
637            .provider
638            .call(request)
639            .await
640            .map_err(Error::Transport)?;
641
642        Ok(response.to_vec())
643    }
644
645    /// Get blueprint configuration
646    pub async fn get_blueprint_config(
647        &self,
648        blueprint_id: u64,
649    ) -> Result<ITangleTypes::BlueprintConfig> {
650        let contract = self.tangle_contract();
651        let result = contract
652            .getBlueprintConfig(blueprint_id)
653            .call()
654            .await
655            .map_err(|e| Error::Contract(e.to_string()))?;
656        Ok(result)
657    }
658
659    /// Get the full blueprint definition.
660    pub async fn get_blueprint_definition(
661        &self,
662        blueprint_id: u64,
663    ) -> Result<ITangleTypes::BlueprintDefinition> {
664        let contract = self.tangle_contract();
665        let result = contract
666            .getBlueprintDefinition(blueprint_id)
667            .call()
668            .await
669            .map_err(|e| Error::Contract(e.to_string()))?;
670        Ok(result)
671    }
672
673    /// Create a new blueprint from an encoded definition.
674    pub async fn create_blueprint(
675        &self,
676        encoded_definition: Vec<u8>,
677    ) -> Result<(TransactionResult, u64)> {
678        let definition = ITangleTypes::BlueprintDefinition::abi_decode(encoded_definition.as_ref())
679            .map_err(|err| {
680                Error::Contract(format!("failed to decode blueprint definition: {err}"))
681            })?;
682
683        let wallet = self.wallet()?;
684        let provider = ProviderBuilder::new()
685            .wallet(wallet)
686            .connect(self.config.http_rpc_endpoint.as_str())
687            .await
688            .map_err(Error::Transport)?;
689        let contract = ITangle::new(self.tangle_address, &provider);
690        let pending_tx = contract
691            .createBlueprint(definition)
692            .send()
693            .await
694            .map_err(|e| Error::Contract(e.to_string()))?;
695        let receipt = pending_tx.get_receipt().await?;
696        let blueprint_id = self.extract_blueprint_id(&receipt)?;
697
698        Ok((transaction_result_from_receipt(&receipt), blueprint_id))
699    }
700
701    /// Check if operator is registered for blueprint
702    pub async fn is_operator_registered(
703        &self,
704        blueprint_id: u64,
705        operator: Address,
706    ) -> Result<bool> {
707        let contract = self.tangle_contract();
708        contract
709            .isOperatorRegistered(blueprint_id, operator)
710            .call()
711            .await
712            .map_err(|e| Error::Contract(e.to_string()))
713    }
714
715    // ═══════════════════════════════════════════════════════════════════════════
716    // SERVICE QUERIES
717    // ═══════════════════════════════════════════════════════════════════════════
718
719    /// Get service information
720    pub async fn get_service(&self, service_id: u64) -> Result<ITangleTypes::Service> {
721        let contract = self.tangle_contract();
722        let result = contract
723            .getService(service_id)
724            .call()
725            .await
726            .map_err(|e| Error::Contract(e.to_string()))?;
727        Ok(result)
728    }
729
730    /// Get service operators
731    pub async fn get_service_operators(&self, service_id: u64) -> Result<Vec<Address>> {
732        let contract = self.tangle_contract();
733        contract
734            .getServiceOperators(service_id)
735            .call()
736            .await
737            .map_err(|e| Error::Contract(e.to_string()))
738    }
739
740    /// Check if address is a service operator
741    pub async fn is_service_operator(&self, service_id: u64, operator: Address) -> Result<bool> {
742        let contract = self.tangle_contract();
743        contract
744            .isServiceOperator(service_id, operator)
745            .call()
746            .await
747            .map_err(|e| Error::Contract(e.to_string()))
748    }
749
750    /// Get service operator info including exposure
751    ///
752    /// Returns the `ServiceOperator` struct which contains `exposureBps`.
753    pub async fn get_service_operator(
754        &self,
755        service_id: u64,
756        operator: Address,
757    ) -> Result<ITangleTypes::ServiceOperator> {
758        let contract = self.tangle_contract();
759        let result = contract
760            .getServiceOperator(service_id, operator)
761            .call()
762            .await
763            .map_err(|e| Error::Contract(e.to_string()))?;
764        Ok(result)
765    }
766
767    /// Get total exposure for a service
768    ///
769    /// Returns the sum of all operator exposureBps values.
770    pub async fn get_service_total_exposure(&self, service_id: u64) -> Result<U256> {
771        let mut total = U256::ZERO;
772        for operator in self.get_service_operators(service_id).await? {
773            let op_info = self.get_service_operator(service_id, operator).await?;
774            if op_info.active {
775                total = total.saturating_add(U256::from(op_info.exposureBps));
776            }
777        }
778        Ok(total)
779    }
780
781    /// Get operator weights (exposureBps) for all operators in a service
782    ///
783    /// Returns a map of operator address to their exposure in basis points.
784    /// This is useful for stake-weighted BLS signature threshold calculations.
785    pub async fn get_service_operator_weights(
786        &self,
787        service_id: u64,
788    ) -> Result<BTreeMap<Address, u16>> {
789        let operators = self.get_service_operators(service_id).await?;
790        let mut weights = BTreeMap::new();
791
792        for operator in operators {
793            let op_info = self.get_service_operator(service_id, operator).await?;
794            if op_info.active {
795                weights.insert(operator, op_info.exposureBps);
796            }
797        }
798
799        Ok(weights)
800    }
801
802    /// Register the current operator for a blueprint.
803    pub async fn register_operator(
804        &self,
805        blueprint_id: u64,
806        rpc_endpoint: impl Into<String>,
807        registration_inputs: Option<Bytes>,
808    ) -> Result<TransactionResult> {
809        let wallet = self.wallet()?;
810        let provider = ProviderBuilder::new()
811            .wallet(wallet)
812            .connect(self.config.http_rpc_endpoint.as_str())
813            .await
814            .map_err(Error::Transport)?;
815        let contract = ITangle::new(self.tangle_address, &provider);
816
817        let signing_key = self.ecdsa_signing_key()?;
818        let verifying = signing_key.verifying_key();
819        // Use uncompressed SEC1 format (65 bytes starting with 0x04)
820        // The contract expects uncompressed public keys, not compressed (33 bytes)
821        let encoded_point = verifying.0.to_encoded_point(false);
822        let ecdsa_bytes = Bytes::copy_from_slice(encoded_point.as_bytes());
823        let rpc_endpoint = rpc_endpoint.into();
824
825        let receipt = if let Some(inputs) = registration_inputs {
826            contract
827                .registerOperator_0(
828                    blueprint_id,
829                    ecdsa_bytes.clone(),
830                    rpc_endpoint.clone(),
831                    inputs,
832                )
833                .send()
834                .await
835                .map_err(|e| Error::Contract(e.to_string()))?
836                .get_receipt()
837                .await?
838        } else {
839            contract
840                .registerOperator_1(blueprint_id, ecdsa_bytes.clone(), rpc_endpoint.clone())
841                .send()
842                .await
843                .map_err(|e| Error::Contract(e.to_string()))?
844                .get_receipt()
845                .await?
846        };
847
848        Ok(transaction_result_from_receipt(&receipt))
849    }
850
851    /// Unregister the current operator from a blueprint.
852    pub async fn unregister_operator(&self, blueprint_id: u64) -> Result<TransactionResult> {
853        let wallet = self.wallet()?;
854        let provider = ProviderBuilder::new()
855            .wallet(wallet)
856            .connect(self.config.http_rpc_endpoint.as_str())
857            .await
858            .map_err(Error::Transport)?;
859        let contract = ITangle::new(self.tangle_address, &provider);
860
861        let receipt = contract
862            .unregisterOperator(blueprint_id)
863            .send()
864            .await
865            .map_err(|e| Error::Contract(e.to_string()))?
866            .get_receipt()
867            .await?;
868
869        Ok(transaction_result_from_receipt(&receipt))
870    }
871
872    /// Get the number of registered blueprints.
873    pub async fn blueprint_count(&self) -> Result<u64> {
874        let contract = self.tangle_contract();
875        contract
876            .blueprintCount()
877            .call()
878            .await
879            .map_err(|e| Error::Contract(e.to_string()))
880    }
881
882    /// Get the number of registered services.
883    pub async fn service_count(&self) -> Result<u64> {
884        let contract = self.tangle_contract();
885        contract
886            .serviceCount()
887            .call()
888            .await
889            .map_err(|e| Error::Contract(e.to_string()))
890    }
891
892    /// Get a service request by ID.
893    pub async fn get_service_request(
894        &self,
895        request_id: u64,
896    ) -> Result<ITangleTypes::ServiceRequest> {
897        let contract = self.tangle_contract();
898        contract
899            .getServiceRequest(request_id)
900            .call()
901            .await
902            .map_err(|e| Error::Contract(e.to_string()))
903    }
904
905    /// Get the total number of service requests ever created.
906    pub async fn service_request_count(&self) -> Result<u64> {
907        let mut data = Vec::with_capacity(4);
908        let selector = keccak256("serviceRequestCount()".as_bytes());
909        data.extend_from_slice(&selector[..4]);
910
911        let mut request = TransactionRequest::default();
912        request.to = Some(TxKind::Call(self.tangle_address));
913        request.input = TransactionInput::new(Bytes::from(data));
914
915        let response = self
916            .provider
917            .call(request)
918            .await
919            .map_err(Error::Transport)?;
920
921        if response.len() < 32 {
922            return Err(Error::Contract(
923                "serviceRequestCount returned malformed data".into(),
924            ));
925        }
926
927        let raw = response.as_ref();
928        let mut buf = [0u8; 8];
929        buf.copy_from_slice(&raw[24..32]);
930        Ok(u64::from_be_bytes(buf))
931    }
932
933    /// Fetch metadata recorded for a specific job call.
934    pub async fn get_job_call(
935        &self,
936        service_id: u64,
937        call_id: u64,
938    ) -> Result<ITangleTypes::JobCall> {
939        let contract = self.tangle_contract();
940        contract
941            .getJobCall(service_id, call_id)
942            .call()
943            .await
944            .map_err(|e| Error::Contract(e.to_string()))
945    }
946
947    /// Fetch operator metadata (ECDSA public key + RPC endpoint) for a blueprint.
948    pub async fn get_operator_metadata(
949        &self,
950        blueprint_id: u64,
951        operator: Address,
952    ) -> Result<OperatorMetadata> {
953        let contract = self.tangle_contract();
954        let prefs = contract
955            .getOperatorPreferences(blueprint_id, operator)
956            .call()
957            .await
958            .map_err(|e| Error::Contract(format!("getOperatorPreferences failed: {e}")))?;
959        let restaking_meta = self
960            .restaking_contract()
961            .getOperatorMetadata(operator)
962            .call()
963            .await
964            .map_err(|e| Error::Contract(format!("getOperatorMetadata failed: {e}")))?;
965        let public_key = normalize_public_key(&prefs.ecdsaPublicKey.0)?;
966        Ok(OperatorMetadata {
967            public_key,
968            rpc_endpoint: prefs.rpcAddress.to_string(),
969            restaking: RestakingMetadata {
970                stake: restaking_meta.stake,
971                delegation_count: restaking_meta.delegationCount,
972                status: RestakingStatus::from(restaking_meta.status),
973                leaving_round: restaking_meta.leavingRound,
974            },
975        })
976    }
977
978    /// Submit a service request.
979    #[allow(clippy::too_many_arguments)]
980    pub async fn request_service(
981        &self,
982        params: ServiceRequestParams,
983    ) -> Result<(TransactionResult, u64)> {
984        let wallet = self.wallet()?;
985        let provider = ProviderBuilder::new()
986            .wallet(wallet)
987            .connect(self.config.http_rpc_endpoint.as_str())
988            .await
989            .map_err(Error::Transport)?;
990        let contract = ITangle::new(self.tangle_address, &provider);
991
992        let ServiceRequestParams {
993            blueprint_id,
994            operators,
995            operator_exposures,
996            permitted_callers,
997            config,
998            ttl,
999            payment_token,
1000            payment_amount,
1001            security_requirements,
1002        } = params;
1003        let confidentiality = 0u8;
1004
1005        let is_native_payment = payment_token == Address::ZERO && payment_amount > U256::ZERO;
1006
1007        // Auto-approve ERC-20 spending before the contract call (matches restaking pattern).
1008        if payment_token != Address::ZERO && payment_amount > U256::ZERO {
1009            self.erc20_approve(payment_token, self.tangle_address, payment_amount)
1010                .await?;
1011        }
1012
1013        let request_id_hint = if !security_requirements.is_empty() {
1014            let mut call = contract.requestServiceWithSecurity(
1015                blueprint_id,
1016                operators.clone(),
1017                security_requirements.clone(),
1018                config.clone(),
1019                permitted_callers.clone(),
1020                ttl,
1021                payment_token,
1022                payment_amount,
1023                confidentiality,
1024            );
1025            call = call.from(self.account());
1026            if is_native_payment {
1027                call = call.value(payment_amount);
1028            }
1029            call.call().await.ok()
1030        } else if let Some(ref exposures) = operator_exposures {
1031            let mut call = contract.requestServiceWithExposure(
1032                blueprint_id,
1033                operators.clone(),
1034                exposures.clone(),
1035                config.clone(),
1036                permitted_callers.clone(),
1037                ttl,
1038                payment_token,
1039                payment_amount,
1040                confidentiality,
1041            );
1042            call = call.from(self.account());
1043            if is_native_payment {
1044                call = call.value(payment_amount);
1045            }
1046            call.call().await.ok()
1047        } else {
1048            let mut call = contract.requestService(
1049                blueprint_id,
1050                operators.clone(),
1051                config.clone(),
1052                permitted_callers.clone(),
1053                ttl,
1054                payment_token,
1055                payment_amount,
1056                confidentiality,
1057            );
1058            call = call.from(self.account());
1059            if is_native_payment {
1060                call = call.value(payment_amount);
1061            }
1062            call.call().await.ok()
1063        };
1064        let pre_count = self.service_request_count().await.ok();
1065
1066        let pending_tx = if !security_requirements.is_empty() {
1067            let mut call = contract.requestServiceWithSecurity(
1068                blueprint_id,
1069                operators.clone(),
1070                security_requirements.clone(),
1071                config.clone(),
1072                permitted_callers.clone(),
1073                ttl,
1074                payment_token,
1075                payment_amount,
1076                confidentiality,
1077            );
1078            if is_native_payment {
1079                call = call.value(payment_amount);
1080            }
1081            call.send().await
1082        } else if let Some(exposures) = operator_exposures {
1083            let mut call = contract.requestServiceWithExposure(
1084                blueprint_id,
1085                operators.clone(),
1086                exposures,
1087                config.clone(),
1088                permitted_callers.clone(),
1089                ttl,
1090                payment_token,
1091                payment_amount,
1092                confidentiality,
1093            );
1094            if is_native_payment {
1095                call = call.value(payment_amount);
1096            }
1097            call.send().await
1098        } else {
1099            let mut call = contract.requestService(
1100                blueprint_id,
1101                operators.clone(),
1102                config.clone(),
1103                permitted_callers.clone(),
1104                ttl,
1105                payment_token,
1106                payment_amount,
1107                confidentiality,
1108            );
1109            if is_native_payment {
1110                call = call.value(payment_amount);
1111            }
1112            call.send().await
1113        }
1114        .map_err(|e| Error::Contract(e.to_string()))?;
1115
1116        let receipt = pending_tx.get_receipt().await?;
1117        if !receipt.status() {
1118            return Err(Error::Contract(
1119                "requestService transaction reverted".into(),
1120            ));
1121        }
1122
1123        let request_id = match self.extract_request_id(&receipt, blueprint_id).await {
1124            Ok(id) => id,
1125            Err(err) => {
1126                if let Some(id) = request_id_hint {
1127                    return Ok((transaction_result_from_receipt(&receipt), id));
1128                }
1129                if let Some(count) = pre_count {
1130                    return Ok((transaction_result_from_receipt(&receipt), count));
1131                }
1132                return Err(err);
1133            }
1134        };
1135
1136        Ok((transaction_result_from_receipt(&receipt), request_id))
1137    }
1138
1139    /// Join a dynamic service with the requested exposure.
1140    pub async fn join_service(
1141        &self,
1142        service_id: u64,
1143        exposure_bps: u16,
1144    ) -> Result<TransactionResult> {
1145        let wallet = self.wallet()?;
1146        let provider = ProviderBuilder::new()
1147            .wallet(wallet)
1148            .connect(self.config.http_rpc_endpoint.as_str())
1149            .await
1150            .map_err(Error::Transport)?;
1151        let contract = ITangle::new(self.tangle_address, &provider);
1152
1153        let receipt = contract
1154            .joinService(service_id, exposure_bps)
1155            .send()
1156            .await
1157            .map_err(|e| Error::Contract(e.to_string()))?
1158            .get_receipt()
1159            .await?;
1160
1161        Ok(transaction_result_from_receipt(&receipt))
1162    }
1163
1164    /// Join a dynamic service with the requested exposure and explicit security commitments.
1165    ///
1166    /// Use this method when the service has security requirements that mandate operators
1167    /// provide asset commitments when joining.
1168    pub async fn join_service_with_commitments(
1169        &self,
1170        service_id: u64,
1171        exposure_bps: u16,
1172        commitments: Vec<ITangleTypes::AssetSecurityCommitment>,
1173    ) -> Result<TransactionResult> {
1174        let wallet = self.wallet()?;
1175        let provider = ProviderBuilder::new()
1176            .wallet(wallet)
1177            .connect(self.config.http_rpc_endpoint.as_str())
1178            .await
1179            .map_err(Error::Transport)?;
1180        let contract = ITangle::new(self.tangle_address, &provider);
1181
1182        let receipt = contract
1183            .joinServiceWithCommitments(service_id, exposure_bps, commitments)
1184            .send()
1185            .await
1186            .map_err(|e| Error::Contract(e.to_string()))?
1187            .get_receipt()
1188            .await?;
1189
1190        Ok(transaction_result_from_receipt(&receipt))
1191    }
1192
1193    /// Leave a dynamic service using the legacy immediate exit helper.
1194    ///
1195    /// Note: This only works when `exitQueueDuration == 0`. For services with
1196    /// an exit queue (default 7 days), use the exit queue workflow:
1197    /// 1. `schedule_exit()` - Enter the exit queue
1198    /// 2. Wait for exit queue duration
1199    /// 3. `execute_exit()` - Complete the exit
1200    pub async fn leave_service(&self, service_id: u64) -> Result<TransactionResult> {
1201        let wallet = self.wallet()?;
1202        let provider = ProviderBuilder::new()
1203            .wallet(wallet)
1204            .connect(self.config.http_rpc_endpoint.as_str())
1205            .await
1206            .map_err(Error::Transport)?;
1207        let contract = ITangle::new(self.tangle_address, &provider);
1208
1209        let receipt = contract
1210            .leaveService(service_id)
1211            .send()
1212            .await
1213            .map_err(|e| Error::Contract(e.to_string()))?
1214            .get_receipt()
1215            .await?;
1216
1217        Ok(transaction_result_from_receipt(&receipt))
1218    }
1219
1220    /// Schedule an exit from a dynamic service.
1221    ///
1222    /// This enters the operator into the exit queue. After the exit queue duration
1223    /// has passed (default 7 days), call `execute_exit()` to complete the exit.
1224    ///
1225    /// Requires that the operator has fulfilled the minimum commitment duration
1226    /// since joining the service.
1227    pub async fn schedule_exit(&self, service_id: u64) -> Result<TransactionResult> {
1228        let wallet = self.wallet()?;
1229        let provider = ProviderBuilder::new()
1230            .wallet(wallet)
1231            .connect(self.config.http_rpc_endpoint.as_str())
1232            .await
1233            .map_err(Error::Transport)?;
1234        let contract = ITangle::new(self.tangle_address, &provider);
1235
1236        let receipt = contract
1237            .scheduleExit(service_id)
1238            .send()
1239            .await
1240            .map_err(|e| Error::Contract(e.to_string()))?
1241            .get_receipt()
1242            .await?;
1243
1244        Ok(transaction_result_from_receipt(&receipt))
1245    }
1246
1247    /// Execute a previously scheduled exit from a dynamic service.
1248    ///
1249    /// This completes the exit after the exit queue duration has passed.
1250    /// Must be called after `schedule_exit()` and waiting for the queue duration.
1251    pub async fn execute_exit(&self, service_id: u64) -> Result<TransactionResult> {
1252        let wallet = self.wallet()?;
1253        let provider = ProviderBuilder::new()
1254            .wallet(wallet)
1255            .connect(self.config.http_rpc_endpoint.as_str())
1256            .await
1257            .map_err(Error::Transport)?;
1258        let contract = ITangle::new(self.tangle_address, &provider);
1259
1260        let receipt = contract
1261            .executeExit(service_id)
1262            .send()
1263            .await
1264            .map_err(|e| Error::Contract(e.to_string()))?
1265            .get_receipt()
1266            .await?;
1267
1268        Ok(transaction_result_from_receipt(&receipt))
1269    }
1270
1271    /// Cancel a previously scheduled exit from a dynamic service.
1272    ///
1273    /// This cancels the exit and keeps the operator in the service.
1274    /// Can only be called before `execute_exit()`.
1275    pub async fn cancel_exit(&self, service_id: u64) -> Result<TransactionResult> {
1276        let wallet = self.wallet()?;
1277        let provider = ProviderBuilder::new()
1278            .wallet(wallet)
1279            .connect(self.config.http_rpc_endpoint.as_str())
1280            .await
1281            .map_err(Error::Transport)?;
1282        let contract = ITangle::new(self.tangle_address, &provider);
1283
1284        let receipt = contract
1285            .cancelExit(service_id)
1286            .send()
1287            .await
1288            .map_err(|e| Error::Contract(e.to_string()))?
1289            .get_receipt()
1290            .await?;
1291
1292        Ok(transaction_result_from_receipt(&receipt))
1293    }
1294
1295    /// Approve a pending service request with a simple restaking percentage.
1296    pub async fn approve_service(
1297        &self,
1298        request_id: u64,
1299        restaking_percent: u8,
1300    ) -> Result<TransactionResult> {
1301        let wallet = self.wallet()?;
1302        let provider = ProviderBuilder::new()
1303            .wallet(wallet)
1304            .connect(self.config.http_rpc_endpoint.as_str())
1305            .await
1306            .map_err(Error::Transport)?;
1307        let contract = ITangle::new(self.tangle_address, &provider);
1308
1309        let receipt = contract
1310            .approveService(request_id, restaking_percent)
1311            .send()
1312            .await
1313            .map_err(|e| Error::Contract(e.to_string()))?
1314            .get_receipt()
1315            .await?;
1316
1317        Ok(transaction_result_from_receipt(&receipt))
1318    }
1319
1320    /// Approve a service request with explicit security commitments.
1321    pub async fn approve_service_with_commitments(
1322        &self,
1323        request_id: u64,
1324        commitments: Vec<ITangleTypes::AssetSecurityCommitment>,
1325    ) -> Result<TransactionResult> {
1326        let wallet = self.wallet()?;
1327        let provider = ProviderBuilder::new()
1328            .wallet(wallet)
1329            .connect(self.config.http_rpc_endpoint.as_str())
1330            .await
1331            .map_err(Error::Transport)?;
1332        let contract = ITangle::new(self.tangle_address, &provider);
1333
1334        let receipt = contract
1335            .approveServiceWithCommitments(request_id, commitments)
1336            .send()
1337            .await
1338            .map_err(|e| Error::Contract(e.to_string()))?
1339            .get_receipt()
1340            .await?;
1341
1342        Ok(transaction_result_from_receipt(&receipt))
1343    }
1344
1345    /// Reject a pending service request.
1346    pub async fn reject_service(&self, request_id: u64) -> Result<TransactionResult> {
1347        let wallet = self.wallet()?;
1348        let provider = ProviderBuilder::new()
1349            .wallet(wallet)
1350            .connect(self.config.http_rpc_endpoint.as_str())
1351            .await
1352            .map_err(Error::Transport)?;
1353        let contract = ITangle::new(self.tangle_address, &provider);
1354
1355        let receipt = contract
1356            .rejectService(request_id)
1357            .send()
1358            .await
1359            .map_err(|e| Error::Contract(e.to_string()))?
1360            .get_receipt()
1361            .await?;
1362
1363        Ok(transaction_result_from_receipt(&receipt))
1364    }
1365
1366    // ═══════════════════════════════════════════════════════════════════════════
1367    // OPERATOR QUERIES (Restaking)
1368    // ═══════════════════════════════════════════════════════════════════════════
1369
1370    /// Check if address is a registered operator
1371    pub async fn is_operator(&self, operator: Address) -> Result<bool> {
1372        let contract = self.restaking_contract();
1373        contract
1374            .isOperator(operator)
1375            .call()
1376            .await
1377            .map_err(|e| Error::Contract(e.to_string()))
1378    }
1379
1380    /// Check if operator is active
1381    pub async fn is_operator_active(&self, operator: Address) -> Result<bool> {
1382        let contract = self.restaking_contract();
1383        contract
1384            .isOperatorActive(operator)
1385            .call()
1386            .await
1387            .map_err(|e| Error::Contract(e.to_string()))
1388    }
1389
1390    /// Get operator's total stake
1391    pub async fn get_operator_stake(&self, operator: Address) -> Result<U256> {
1392        let contract = self.restaking_contract();
1393        contract
1394            .getOperatorStake(operator)
1395            .call()
1396            .await
1397            .map_err(|e| Error::Contract(e.to_string()))
1398    }
1399
1400    /// Get minimum operator stake requirement
1401    pub async fn min_operator_stake(&self) -> Result<U256> {
1402        let contract = self.restaking_contract();
1403        contract
1404            .minOperatorStake()
1405            .call()
1406            .await
1407            .map_err(|e| Error::Contract(e.to_string()))
1408    }
1409
1410    /// Fetch status registry metadata for an operator/service pair.
1411    pub async fn operator_status(
1412        &self,
1413        service_id: u64,
1414        operator: Address,
1415    ) -> Result<OperatorStatusSnapshot> {
1416        if self.status_registry_address.is_zero() {
1417            return Err(Error::MissingStatusRegistry);
1418        }
1419        let contract = self.status_registry_contract();
1420
1421        let last_heartbeat = contract
1422            .getLastHeartbeat(service_id, operator)
1423            .call()
1424            .await
1425            .map_err(|e| Error::Contract(e.to_string()))?;
1426        let status_code = contract
1427            .getOperatorStatus(service_id, operator)
1428            .call()
1429            .await
1430            .map_err(|e| Error::Contract(e.to_string()))?;
1431        let online = contract
1432            .isOnline(service_id, operator)
1433            .call()
1434            .await
1435            .map_err(|e| Error::Contract(e.to_string()))?;
1436
1437        let last_heartbeat = u64::try_from(last_heartbeat).unwrap_or(u64::MAX);
1438
1439        Ok(OperatorStatusSnapshot {
1440            service_id,
1441            operator,
1442            status_code,
1443            last_heartbeat,
1444            online,
1445        })
1446    }
1447
1448    /// Fetch restaking metadata for an operator.
1449    pub async fn get_restaking_metadata(&self, operator: Address) -> Result<RestakingMetadata> {
1450        let restaking_meta = self
1451            .restaking_contract()
1452            .getOperatorMetadata(operator)
1453            .call()
1454            .await
1455            .map_err(|e| Error::Contract(format!("getOperatorMetadata failed: {e}")))?;
1456        Ok(RestakingMetadata {
1457            stake: restaking_meta.stake,
1458            delegation_count: restaking_meta.delegationCount,
1459            status: RestakingStatus::from(restaking_meta.status),
1460            leaving_round: restaking_meta.leavingRound,
1461        })
1462    }
1463
1464    /// Get operator self stake from MultiAssetDelegation.
1465    pub async fn get_operator_self_stake(&self, operator: Address) -> Result<U256> {
1466        let contract = self.restaking_contract();
1467        contract
1468            .getOperatorSelfStake(operator)
1469            .call()
1470            .await
1471            .map_err(|e| Error::Contract(e.to_string()))
1472    }
1473
1474    /// Get operator delegated stake from MultiAssetDelegation.
1475    pub async fn get_operator_delegated_stake(&self, operator: Address) -> Result<U256> {
1476        let contract = self.restaking_contract();
1477        contract
1478            .getOperatorDelegatedStake(operator)
1479            .call()
1480            .await
1481            .map_err(|e| Error::Contract(e.to_string()))
1482    }
1483
1484    /// Get delegators for the operator.
1485    pub async fn get_operator_delegators(&self, operator: Address) -> Result<Vec<Address>> {
1486        let contract = self.restaking_contract();
1487        contract
1488            .getOperatorDelegators(operator)
1489            .call()
1490            .await
1491            .map_err(|e| Error::Contract(e.to_string()))
1492    }
1493
1494    /// Get delegator count for the operator.
1495    pub async fn get_operator_delegator_count(&self, operator: Address) -> Result<u64> {
1496        let contract = self.restaking_contract();
1497        let count = contract
1498            .getOperatorDelegatorCount(operator)
1499            .call()
1500            .await
1501            .map_err(|e| Error::Contract(e.to_string()))?;
1502        Ok(u64::try_from(count).unwrap_or(u64::MAX))
1503    }
1504
1505    /// Get current restaking round.
1506    pub async fn restaking_round(&self) -> Result<u64> {
1507        let contract = self.restaking_contract();
1508        contract
1509            .currentRound()
1510            .call()
1511            .await
1512            .map_err(|e| Error::Contract(e.to_string()))
1513    }
1514
1515    /// Get operator commission (basis points).
1516    pub async fn operator_commission_bps(&self) -> Result<u16> {
1517        let contract = self.restaking_contract();
1518        contract
1519            .operatorCommissionBps()
1520            .call()
1521            .await
1522            .map_err(|e| Error::Contract(e.to_string()))
1523    }
1524
1525    // ═══════════════════════════════════════════════════════════════════════════
1526    // OPERATOR DELEGATION CONFIG
1527    // ═══════════════════════════════════════════════════════════════════════════
1528
1529    /// Get operator's delegation mode.
1530    ///
1531    /// Returns the delegation policy for the operator:
1532    /// - Disabled: Only operator can self-stake
1533    /// - Whitelist: Only approved addresses can delegate
1534    /// - Open: Anyone can delegate
1535    pub async fn get_delegation_mode(&self, operator: Address) -> Result<DelegationMode> {
1536        let contract = self.restaking_contract();
1537        let mode = contract
1538            .getDelegationMode(operator)
1539            .call()
1540            .await
1541            .map_err(|e| Error::Contract(e.to_string()))?;
1542        Ok(DelegationMode::from(mode))
1543    }
1544
1545    /// Check if delegator is whitelisted for operator.
1546    pub async fn is_delegator_whitelisted(
1547        &self,
1548        operator: Address,
1549        delegator: Address,
1550    ) -> Result<bool> {
1551        let contract = self.restaking_contract();
1552        contract
1553            .isWhitelisted(operator, delegator)
1554            .call()
1555            .await
1556            .map_err(|e| Error::Contract(e.to_string()))
1557    }
1558
1559    /// Check if delegator can delegate to operator.
1560    ///
1561    /// This checks the operator's delegation mode and whitelist status.
1562    pub async fn can_delegate(&self, operator: Address, delegator: Address) -> Result<bool> {
1563        let contract = self.restaking_contract();
1564        contract
1565            .canDelegate(operator, delegator)
1566            .call()
1567            .await
1568            .map_err(|e| Error::Contract(e.to_string()))
1569    }
1570
1571    /// Set delegation mode for the calling operator.
1572    ///
1573    /// Changes take effect immediately for NEW delegations only.
1574    /// Existing delegations remain valid regardless of mode change.
1575    ///
1576    /// # Arguments
1577    /// * `mode` - Delegation mode: Disabled (0), Whitelist (1), or Open (2)
1578    pub async fn set_delegation_mode(&self, mode: DelegationMode) -> Result<TransactionResult> {
1579        let wallet = self.wallet()?;
1580        let provider = ProviderBuilder::new()
1581            .wallet(wallet)
1582            .connect(self.config.http_rpc_endpoint.as_str())
1583            .await
1584            .map_err(Error::Transport)?;
1585        let contract = IMultiAssetDelegation::new(self.restaking_address, provider);
1586
1587        let mode_value: u8 = match mode {
1588            DelegationMode::Disabled => 0,
1589            DelegationMode::Whitelist => 1,
1590            DelegationMode::Open => 2,
1591            DelegationMode::Unknown(v) => v,
1592        };
1593
1594        let receipt = contract
1595            .setDelegationMode(mode_value)
1596            .send()
1597            .await
1598            .map_err(|e| Error::Contract(e.to_string()))?
1599            .get_receipt()
1600            .await
1601            .map_err(|e| Error::Contract(e.to_string()))?;
1602
1603        Ok(transaction_result_from_receipt(&receipt))
1604    }
1605
1606    /// Update delegation whitelist for the calling operator.
1607    ///
1608    /// Whitelist only applies when delegation mode is set to Whitelist.
1609    /// Can be called regardless of current mode to pre-configure.
1610    ///
1611    /// # Arguments
1612    /// * `delegators` - Array of delegator addresses to update
1613    /// * `approved` - True to approve for delegation, false to revoke
1614    pub async fn set_delegation_whitelist(
1615        &self,
1616        delegators: Vec<Address>,
1617        approved: bool,
1618    ) -> Result<TransactionResult> {
1619        let wallet = self.wallet()?;
1620        let provider = ProviderBuilder::new()
1621            .wallet(wallet)
1622            .connect(self.config.http_rpc_endpoint.as_str())
1623            .await
1624            .map_err(Error::Transport)?;
1625        let contract = IMultiAssetDelegation::new(self.restaking_address, provider);
1626
1627        let receipt = contract
1628            .setDelegationWhitelist(delegators, approved)
1629            .send()
1630            .await
1631            .map_err(|e| Error::Contract(e.to_string()))?
1632            .get_receipt()
1633            .await
1634            .map_err(|e| Error::Contract(e.to_string()))?;
1635
1636        Ok(transaction_result_from_receipt(&receipt))
1637    }
1638
1639    /// Fetch ERC20 allowance for an owner/spender pair.
1640    pub async fn erc20_allowance(
1641        &self,
1642        token: Address,
1643        owner: Address,
1644        spender: Address,
1645    ) -> Result<U256> {
1646        let contract = IERC20::new(token, Arc::clone(&self.provider));
1647        contract
1648            .allowance(owner, spender)
1649            .call()
1650            .await
1651            .map_err(|e| Error::Contract(e.to_string()))
1652    }
1653
1654    /// Fetch ERC20 balance for an owner.
1655    pub async fn erc20_balance(&self, token: Address, owner: Address) -> Result<U256> {
1656        let contract = IERC20::new(token, Arc::clone(&self.provider));
1657        contract
1658            .balanceOf(owner)
1659            .call()
1660            .await
1661            .map_err(|e| Error::Contract(e.to_string()))
1662    }
1663
1664    /// Approve ERC20 spending for the given spender.
1665    pub async fn erc20_approve(
1666        &self,
1667        token: Address,
1668        spender: Address,
1669        amount: U256,
1670    ) -> Result<TransactionResult> {
1671        let wallet = self.wallet()?;
1672        let provider = ProviderBuilder::new()
1673            .wallet(wallet)
1674            .connect(self.config.http_rpc_endpoint.as_str())
1675            .await
1676            .map_err(Error::Transport)?;
1677        let contract = IERC20::new(token, &provider);
1678
1679        let receipt = contract
1680            .approve(spender, amount)
1681            .send()
1682            .await
1683            .map_err(|e| Error::Contract(e.to_string()))?
1684            .get_receipt()
1685            .await?;
1686
1687        Ok(transaction_result_from_receipt(&receipt))
1688    }
1689
1690    /// Fetch delegator deposit info for a token.
1691    pub async fn get_deposit_info(
1692        &self,
1693        delegator: Address,
1694        token: Address,
1695    ) -> Result<DepositInfo> {
1696        let contract = self.restaking_contract();
1697        let deposit = contract
1698            .getDeposit(delegator, token)
1699            .call()
1700            .await
1701            .map_err(|e| Error::Contract(e.to_string()))?;
1702        Ok(DepositInfo {
1703            amount: deposit.amount,
1704            delegated_amount: deposit.delegatedAmount,
1705        })
1706    }
1707
1708    /// Fetch lock info for a token.
1709    pub async fn get_locks(&self, delegator: Address, token: Address) -> Result<Vec<LockInfo>> {
1710        let contract = self.restaking_contract();
1711        let locks = contract
1712            .getLocks(delegator, token)
1713            .call()
1714            .await
1715            .map_err(|e| Error::Contract(e.to_string()))?;
1716        Ok(locks
1717            .into_iter()
1718            .map(|lock| LockInfo {
1719                amount: lock.amount,
1720                multiplier: LockMultiplier::from(lock.multiplier),
1721                expiry_block: lock.expiryBlock,
1722            })
1723            .collect())
1724    }
1725
1726    /// Fetch delegations for a delegator.
1727    pub async fn get_delegations(&self, delegator: Address) -> Result<Vec<DelegationInfo>> {
1728        let contract = self.restaking_contract();
1729        let delegations = contract
1730            .getDelegations(delegator)
1731            .call()
1732            .await
1733            .map_err(|e| Error::Contract(e.to_string()))?;
1734        Ok(delegations
1735            .into_iter()
1736            .map(|delegation| DelegationInfo {
1737                operator: delegation.operator,
1738                shares: delegation.shares,
1739                asset: asset_info_from_types(delegation.asset),
1740                selection_mode: BlueprintSelectionMode::from(delegation.selectionMode),
1741            })
1742            .collect())
1743    }
1744
1745    /// Fetch delegations with blueprint selections attached.
1746    pub async fn get_delegations_with_blueprints(
1747        &self,
1748        delegator: Address,
1749    ) -> Result<Vec<DelegationRecord>> {
1750        let delegations = self.get_delegations(delegator).await?;
1751        let mut records = Vec::with_capacity(delegations.len());
1752        for (idx, info) in delegations.into_iter().enumerate() {
1753            let blueprint_ids = if matches!(info.selection_mode, BlueprintSelectionMode::Fixed) {
1754                self.get_delegation_blueprints(delegator, idx as u64)
1755                    .await?
1756            } else {
1757                Vec::new()
1758            };
1759            records.push(DelegationRecord {
1760                info,
1761                blueprint_ids,
1762            });
1763        }
1764        Ok(records)
1765    }
1766
1767    /// Fetch blueprint IDs for a fixed delegation.
1768    pub async fn get_delegation_blueprints(
1769        &self,
1770        delegator: Address,
1771        index: u64,
1772    ) -> Result<Vec<u64>> {
1773        let contract = self.restaking_contract();
1774        let ids = contract
1775            .getDelegationBlueprints(delegator, U256::from(index))
1776            .call()
1777            .await
1778            .map_err(|e| Error::Contract(e.to_string()))?;
1779        Ok(ids)
1780    }
1781
1782    /// Fetch pending delegator unstakes.
1783    pub async fn get_pending_unstakes(&self, delegator: Address) -> Result<Vec<PendingUnstake>> {
1784        let contract = self.restaking_contract();
1785        let unstakes = contract
1786            .getPendingUnstakes(delegator)
1787            .call()
1788            .await
1789            .map_err(|e| Error::Contract(e.to_string()))?;
1790        Ok(unstakes
1791            .into_iter()
1792            .map(|request| PendingUnstake {
1793                operator: request.operator,
1794                asset: asset_info_from_types(request.asset),
1795                shares: request.shares,
1796                requested_round: request.requestedRound,
1797                selection_mode: BlueprintSelectionMode::from(request.selectionMode),
1798                slash_factor_snapshot: request.slashFactorSnapshot,
1799            })
1800            .collect())
1801    }
1802
1803    /// Fetch pending delegator withdrawals.
1804    pub async fn get_pending_withdrawals(
1805        &self,
1806        delegator: Address,
1807    ) -> Result<Vec<PendingWithdrawal>> {
1808        let contract = self.restaking_contract();
1809        let withdrawals = contract
1810            .getPendingWithdrawals(delegator)
1811            .call()
1812            .await
1813            .map_err(|e| Error::Contract(e.to_string()))?;
1814        Ok(withdrawals
1815            .into_iter()
1816            .map(|request| PendingWithdrawal {
1817                asset: asset_info_from_types(request.asset),
1818                amount: request.amount,
1819                requested_round: request.requestedRound,
1820            })
1821            .collect())
1822    }
1823
1824    /// Deposit and delegate in a single call.
1825    pub async fn deposit_and_delegate_with_options(
1826        &self,
1827        operator: Address,
1828        token: Address,
1829        amount: U256,
1830        selection_mode: BlueprintSelectionMode,
1831        blueprint_ids: Vec<u64>,
1832    ) -> Result<TransactionResult> {
1833        let wallet = self.wallet()?;
1834        let provider = ProviderBuilder::new()
1835            .wallet(wallet)
1836            .connect(self.config.http_rpc_endpoint.as_str())
1837            .await
1838            .map_err(Error::Transport)?;
1839        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
1840
1841        let mut call = contract.depositAndDelegateWithOptions(
1842            operator,
1843            token,
1844            amount,
1845            selection_mode_to_u8(selection_mode),
1846            blueprint_ids,
1847        );
1848        if token == Address::ZERO {
1849            call = call.value(amount);
1850        }
1851
1852        let receipt = call
1853            .send()
1854            .await
1855            .map_err(|e| Error::Contract(e.to_string()))?
1856            .get_receipt()
1857            .await?;
1858
1859        Ok(transaction_result_from_receipt(&receipt))
1860    }
1861
1862    /// Delegate existing deposits with explicit selection.
1863    pub async fn delegate_with_options(
1864        &self,
1865        operator: Address,
1866        token: Address,
1867        amount: U256,
1868        selection_mode: BlueprintSelectionMode,
1869        blueprint_ids: Vec<u64>,
1870    ) -> Result<TransactionResult> {
1871        let wallet = self.wallet()?;
1872        let provider = ProviderBuilder::new()
1873            .wallet(wallet)
1874            .connect(self.config.http_rpc_endpoint.as_str())
1875            .await
1876            .map_err(Error::Transport)?;
1877        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
1878
1879        let receipt = contract
1880            .delegateWithOptions(
1881                operator,
1882                token,
1883                amount,
1884                selection_mode_to_u8(selection_mode),
1885                blueprint_ids,
1886            )
1887            .send()
1888            .await
1889            .map_err(|e| Error::Contract(e.to_string()))?
1890            .get_receipt()
1891            .await?;
1892
1893        Ok(transaction_result_from_receipt(&receipt))
1894    }
1895
1896    /// Schedule a delegator unstake (bond-less).
1897    pub async fn schedule_delegator_unstake(
1898        &self,
1899        operator: Address,
1900        token: Address,
1901        amount: U256,
1902    ) -> Result<TransactionResult> {
1903        let wallet = self.wallet()?;
1904        let provider = ProviderBuilder::new()
1905            .wallet(wallet)
1906            .connect(self.config.http_rpc_endpoint.as_str())
1907            .await
1908            .map_err(Error::Transport)?;
1909        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
1910
1911        let receipt = contract
1912            .scheduleDelegatorUnstake(operator, token, amount)
1913            .send()
1914            .await
1915            .map_err(|e| Error::Contract(e.to_string()))?
1916            .get_receipt()
1917            .await?;
1918
1919        Ok(transaction_result_from_receipt(&receipt))
1920    }
1921
1922    /// Execute any matured delegator unstake requests.
1923    pub async fn execute_delegator_unstake(&self) -> Result<TransactionResult> {
1924        let wallet = self.wallet()?;
1925        let provider = ProviderBuilder::new()
1926            .wallet(wallet)
1927            .connect(self.config.http_rpc_endpoint.as_str())
1928            .await
1929            .map_err(Error::Transport)?;
1930        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
1931
1932        let receipt = contract
1933            .executeDelegatorUnstake()
1934            .send()
1935            .await
1936            .map_err(|e| Error::Contract(e.to_string()))?
1937            .get_receipt()
1938            .await?;
1939
1940        Ok(transaction_result_from_receipt(&receipt))
1941    }
1942
1943    /// Execute a specific delegator unstake and withdraw.
1944    pub async fn execute_delegator_unstake_and_withdraw(
1945        &self,
1946        operator: Address,
1947        token: Address,
1948        shares: U256,
1949        requested_round: u64,
1950        receiver: Address,
1951    ) -> Result<TransactionResult> {
1952        let wallet = self.wallet()?;
1953        let provider = ProviderBuilder::new()
1954            .wallet(wallet)
1955            .connect(self.config.http_rpc_endpoint.as_str())
1956            .await
1957            .map_err(Error::Transport)?;
1958        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
1959
1960        let receipt = contract
1961            .executeDelegatorUnstakeAndWithdraw(operator, token, shares, requested_round, receiver)
1962            .send()
1963            .await
1964            .map_err(|e| Error::Contract(e.to_string()))?
1965            .get_receipt()
1966            .await?;
1967
1968        Ok(transaction_result_from_receipt(&receipt))
1969    }
1970
1971    /// Schedule a withdrawal for a token.
1972    pub async fn schedule_withdraw(
1973        &self,
1974        token: Address,
1975        amount: U256,
1976    ) -> Result<TransactionResult> {
1977        let wallet = self.wallet()?;
1978        let provider = ProviderBuilder::new()
1979            .wallet(wallet)
1980            .connect(self.config.http_rpc_endpoint.as_str())
1981            .await
1982            .map_err(Error::Transport)?;
1983        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
1984
1985        let receipt = contract
1986            .scheduleWithdraw(token, amount)
1987            .send()
1988            .await
1989            .map_err(|e| Error::Contract(e.to_string()))?
1990            .get_receipt()
1991            .await?;
1992
1993        Ok(transaction_result_from_receipt(&receipt))
1994    }
1995
1996    /// Execute any matured withdrawal requests.
1997    pub async fn execute_withdraw(&self) -> Result<TransactionResult> {
1998        let wallet = self.wallet()?;
1999        let provider = ProviderBuilder::new()
2000            .wallet(wallet)
2001            .connect(self.config.http_rpc_endpoint.as_str())
2002            .await
2003            .map_err(Error::Transport)?;
2004        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2005
2006        let receipt = contract
2007            .executeWithdraw()
2008            .send()
2009            .await
2010            .map_err(|e| Error::Contract(e.to_string()))?
2011            .get_receipt()
2012            .await?;
2013
2014        Ok(transaction_result_from_receipt(&receipt))
2015    }
2016
2017    /// Schedule an operator unstake.
2018    pub async fn schedule_operator_unstake(&self, amount: U256) -> Result<TransactionResult> {
2019        let wallet = self.wallet()?;
2020        let provider = ProviderBuilder::new()
2021            .wallet(wallet)
2022            .connect(self.config.http_rpc_endpoint.as_str())
2023            .await
2024            .map_err(Error::Transport)?;
2025        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2026
2027        let receipt = contract
2028            .scheduleOperatorUnstake(amount)
2029            .send()
2030            .await
2031            .map_err(|e| Error::Contract(e.to_string()))?
2032            .get_receipt()
2033            .await?;
2034
2035        Ok(transaction_result_from_receipt(&receipt))
2036    }
2037
2038    /// Execute an operator unstake.
2039    pub async fn execute_operator_unstake(&self) -> Result<TransactionResult> {
2040        let wallet = self.wallet()?;
2041        let provider = ProviderBuilder::new()
2042            .wallet(wallet)
2043            .connect(self.config.http_rpc_endpoint.as_str())
2044            .await
2045            .map_err(Error::Transport)?;
2046        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2047
2048        let receipt = contract
2049            .executeOperatorUnstake()
2050            .send()
2051            .await
2052            .map_err(|e| Error::Contract(e.to_string()))?
2053            .get_receipt()
2054            .await?;
2055
2056        Ok(transaction_result_from_receipt(&receipt))
2057    }
2058
2059    /// Start leaving the operator set.
2060    pub async fn start_leaving(&self) -> Result<TransactionResult> {
2061        let wallet = self.wallet()?;
2062        let provider = ProviderBuilder::new()
2063            .wallet(wallet)
2064            .connect(self.config.http_rpc_endpoint.as_str())
2065            .await
2066            .map_err(Error::Transport)?;
2067        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2068
2069        let receipt = contract
2070            .startLeaving()
2071            .send()
2072            .await
2073            .map_err(|e| Error::Contract(e.to_string()))?
2074            .get_receipt()
2075            .await?;
2076
2077        Ok(transaction_result_from_receipt(&receipt))
2078    }
2079
2080    /// Complete leaving after delay.
2081    pub async fn complete_leaving(&self) -> Result<TransactionResult> {
2082        let wallet = self.wallet()?;
2083        let provider = ProviderBuilder::new()
2084            .wallet(wallet)
2085            .connect(self.config.http_rpc_endpoint.as_str())
2086            .await
2087            .map_err(Error::Transport)?;
2088        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2089
2090        let receipt = contract
2091            .completeLeaving()
2092            .send()
2093            .await
2094            .map_err(|e| Error::Contract(e.to_string()))?
2095            .get_receipt()
2096            .await?;
2097
2098        Ok(transaction_result_from_receipt(&receipt))
2099    }
2100
2101    // ═══════════════════════════════════════════════════════════════════════════
2102    // OPERATOR RESTAKING REGISTRATION
2103    // ═══════════════════════════════════════════════════════════════════════════
2104
2105    /// Get the operator bond token address.
2106    ///
2107    /// Returns `Address::ZERO` if native ETH is used for operator bonds,
2108    /// otherwise returns the ERC20 token address (e.g., TNT).
2109    pub async fn operator_bond_token(&self) -> Result<Address> {
2110        let contract = self.restaking_contract();
2111        contract
2112            .operatorBondToken()
2113            .call()
2114            .await
2115            .map_err(|e| Error::Contract(e.to_string()))
2116    }
2117
2118    /// Register as an operator on the restaking layer.
2119    ///
2120    /// Automatically uses the configured bond token (native ETH or ERC20 like TNT).
2121    /// For ERC20 tokens, automatically approves the restaking contract first.
2122    pub async fn register_operator_restaking(
2123        &self,
2124        stake_amount: U256,
2125    ) -> Result<TransactionResult> {
2126        let bond_token = self.operator_bond_token().await?;
2127
2128        // Auto-approve ERC20 bond token if needed
2129        if bond_token != Address::ZERO {
2130            self.erc20_approve(bond_token, self.restaking_address, stake_amount)
2131                .await?;
2132        }
2133
2134        let wallet = self.wallet()?;
2135        let provider = ProviderBuilder::new()
2136            .wallet(wallet)
2137            .connect(self.config.http_rpc_endpoint.as_str())
2138            .await
2139            .map_err(Error::Transport)?;
2140        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2141
2142        let receipt = if bond_token == Address::ZERO {
2143            // Native ETH bond
2144            contract
2145                .registerOperator()
2146                .value(stake_amount)
2147                .send()
2148                .await
2149                .map_err(|e| Error::Contract(e.to_string()))?
2150                .get_receipt()
2151                .await?
2152        } else {
2153            // ERC20 bond (e.g., TNT)
2154            contract
2155                .registerOperatorWithAsset(bond_token, stake_amount)
2156                .send()
2157                .await
2158                .map_err(|e| Error::Contract(e.to_string()))?
2159                .get_receipt()
2160                .await?
2161        };
2162
2163        Ok(transaction_result_from_receipt(&receipt))
2164    }
2165
2166    /// Increase operator stake.
2167    ///
2168    /// Automatically uses the configured bond token (native ETH or ERC20 like TNT).
2169    /// For ERC20 tokens, automatically approves the restaking contract first.
2170    pub async fn increase_stake(&self, amount: U256) -> Result<TransactionResult> {
2171        let bond_token = self.operator_bond_token().await?;
2172
2173        // Auto-approve ERC20 bond token if needed
2174        if bond_token != Address::ZERO {
2175            self.erc20_approve(bond_token, self.restaking_address, amount)
2176                .await?;
2177        }
2178
2179        let wallet = self.wallet()?;
2180        let provider = ProviderBuilder::new()
2181            .wallet(wallet)
2182            .connect(self.config.http_rpc_endpoint.as_str())
2183            .await
2184            .map_err(Error::Transport)?;
2185        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2186
2187        let receipt = if bond_token == Address::ZERO {
2188            // Native ETH bond
2189            contract
2190                .increaseStake()
2191                .value(amount)
2192                .send()
2193                .await
2194                .map_err(|e| Error::Contract(e.to_string()))?
2195                .get_receipt()
2196                .await?
2197        } else {
2198            // ERC20 bond (e.g., TNT)
2199            contract
2200                .increaseStakeWithAsset(bond_token, amount)
2201                .send()
2202                .await
2203                .map_err(|e| Error::Contract(e.to_string()))?
2204                .get_receipt()
2205                .await?
2206        };
2207
2208        Ok(transaction_result_from_receipt(&receipt))
2209    }
2210
2211    /// Deposit native ETH without delegating.
2212    ///
2213    /// Use this to pre-fund your account before delegating.
2214    pub async fn deposit_native(&self, amount: U256) -> Result<TransactionResult> {
2215        let wallet = self.wallet()?;
2216        let provider = ProviderBuilder::new()
2217            .wallet(wallet)
2218            .connect(self.config.http_rpc_endpoint.as_str())
2219            .await
2220            .map_err(Error::Transport)?;
2221        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2222
2223        let receipt = contract
2224            .deposit()
2225            .value(amount)
2226            .send()
2227            .await
2228            .map_err(|e| Error::Contract(e.to_string()))?
2229            .get_receipt()
2230            .await?;
2231
2232        Ok(transaction_result_from_receipt(&receipt))
2233    }
2234
2235    /// Deposit ERC20 tokens without delegating.
2236    ///
2237    /// Use this to pre-fund your account before delegating.
2238    pub async fn deposit_erc20(&self, token: Address, amount: U256) -> Result<TransactionResult> {
2239        let wallet = self.wallet()?;
2240        let provider = ProviderBuilder::new()
2241            .wallet(wallet)
2242            .connect(self.config.http_rpc_endpoint.as_str())
2243            .await
2244            .map_err(Error::Transport)?;
2245        let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2246
2247        let receipt = contract
2248            .depositERC20(token, amount)
2249            .send()
2250            .await
2251            .map_err(|e| Error::Contract(e.to_string()))?
2252            .get_receipt()
2253            .await?;
2254
2255        Ok(transaction_result_from_receipt(&receipt))
2256    }
2257
2258    // ═══════════════════════════════════════════════════════════════════════════
2259    // BLS AGGREGATION QUERIES
2260    // ═══════════════════════════════════════════════════════════════════════════
2261
2262    /// Get the blueprint manager address for a service
2263    pub async fn get_blueprint_manager(&self, service_id: u64) -> Result<Option<Address>> {
2264        let service = self.get_service(service_id).await?;
2265        let blueprint = self.get_blueprint(service.blueprintId).await?;
2266        if blueprint.manager == Address::ZERO {
2267            Ok(None)
2268        } else {
2269            Ok(Some(blueprint.manager))
2270        }
2271    }
2272
2273    /// Check if a job requires BLS aggregation
2274    ///
2275    /// Queries the blueprint's service manager contract to determine if the specified
2276    /// job index requires aggregated BLS signatures instead of individual results.
2277    pub async fn requires_aggregation(&self, service_id: u64, job_index: u8) -> Result<bool> {
2278        let manager = match self.get_blueprint_manager(service_id).await? {
2279            Some(m) => m,
2280            None => return Ok(false), // No manager means no aggregation required
2281        };
2282
2283        let bsm = IBlueprintServiceManager::new(manager, Arc::clone(&self.provider));
2284        match bsm.requiresAggregation(service_id, job_index).call().await {
2285            Ok(required) => Ok(required),
2286            Err(_) => Ok(false), // If call fails, assume no aggregation required
2287        }
2288    }
2289
2290    /// Get the aggregation threshold configuration for a job
2291    ///
2292    /// Returns (threshold_bps, threshold_type) where:
2293    /// - threshold_bps: Threshold in basis points (e.g., 6700 = 67%)
2294    /// - threshold_type: 0 = CountBased (% of operators), 1 = StakeWeighted (% of stake)
2295    pub async fn get_aggregation_threshold(
2296        &self,
2297        service_id: u64,
2298        job_index: u8,
2299    ) -> Result<(u16, u8)> {
2300        let manager = match self.get_blueprint_manager(service_id).await? {
2301            Some(m) => m,
2302            None => return Ok((6700, 0)), // Default: 67% count-based
2303        };
2304
2305        let bsm = IBlueprintServiceManager::new(manager, Arc::clone(&self.provider));
2306        match bsm
2307            .getAggregationThreshold(service_id, job_index)
2308            .call()
2309            .await
2310        {
2311            Ok(result) => Ok((result.thresholdBps, result.thresholdType)),
2312            Err(_) => Ok((6700, 0)), // Default if call fails
2313        }
2314    }
2315
2316    /// Get the aggregation configuration for a specific job
2317    ///
2318    /// Returns the full aggregation config including whether it's required and threshold settings
2319    pub async fn get_aggregation_config(
2320        &self,
2321        service_id: u64,
2322        job_index: u8,
2323    ) -> Result<AggregationConfig> {
2324        let requires_aggregation = self.requires_aggregation(service_id, job_index).await?;
2325        let (threshold_bps, threshold_type) = self
2326            .get_aggregation_threshold(service_id, job_index)
2327            .await?;
2328
2329        Ok(AggregationConfig {
2330            required: requires_aggregation,
2331            threshold_bps,
2332            threshold_type: if threshold_type == 0 {
2333                ThresholdType::CountBased
2334            } else {
2335                ThresholdType::StakeWeighted
2336            },
2337        })
2338    }
2339
2340    // ═══════════════════════════════════════════════════════════════════════════
2341    // TRANSACTION SUBMISSION
2342    // ═══════════════════════════════════════════════════════════════════════════
2343
2344    /// Submit a job invocation to the Tangle contract.
2345    pub async fn submit_job(
2346        &self,
2347        service_id: u64,
2348        job_index: u8,
2349        inputs: Bytes,
2350    ) -> Result<JobSubmissionResult> {
2351        self.submit_job_with_value(service_id, job_index, inputs, U256::ZERO)
2352            .await
2353    }
2354
2355    /// Submit a job invocation with native token payment.
2356    ///
2357    /// For `EventDriven` services the caller should send `event_rate` as `value`.
2358    /// For free jobs or services that don't charge per-event, pass `U256::ZERO`.
2359    pub async fn submit_job_with_value(
2360        &self,
2361        service_id: u64,
2362        job_index: u8,
2363        inputs: Bytes,
2364        value: U256,
2365    ) -> Result<JobSubmissionResult> {
2366        use crate::contracts::ITangle::submitJobCall;
2367        use alloy_sol_types::SolCall;
2368
2369        let wallet = self.wallet()?;
2370        let provider = ProviderBuilder::new()
2371            .wallet(wallet)
2372            .connect(self.config.http_rpc_endpoint.as_str())
2373            .await
2374            .map_err(Error::Transport)?;
2375
2376        let call = submitJobCall {
2377            serviceId: service_id,
2378            jobIndex: job_index,
2379            inputs,
2380        };
2381        let calldata = call.abi_encode();
2382
2383        let mut tx_request = TransactionRequest::default()
2384            .to(self.tangle_address)
2385            .input(calldata.into());
2386        if value > U256::ZERO {
2387            tx_request = tx_request.value(value);
2388        }
2389
2390        let pending_tx = provider
2391            .send_transaction(tx_request)
2392            .await
2393            .map_err(Error::Transport)?;
2394
2395        let receipt = pending_tx
2396            .get_receipt()
2397            .await
2398            .map_err(Error::PendingTransaction)?;
2399
2400        self.parse_job_submitted(&receipt)
2401    }
2402
2403    /// Submit a job from operator-signed quotes with payment.
2404    ///
2405    /// Calls the on-chain `submitJobFromQuote` function. The total quoted
2406    /// price is sent as native value with the transaction.
2407    pub async fn submit_job_from_quote(
2408        &self,
2409        service_id: u64,
2410        job_index: u8,
2411        inputs: Bytes,
2412        quotes: Vec<ITangleTypes::SignedJobQuote>,
2413    ) -> Result<JobSubmissionResult> {
2414        use crate::contracts::ITangle::submitJobFromQuoteCall;
2415        use alloy_sol_types::SolCall;
2416
2417        // Sum all quote prices to determine total native value to send.
2418        let total_value: U256 = quotes.iter().map(|q| q.details.price).sum();
2419
2420        let wallet = self.wallet()?;
2421        let provider = ProviderBuilder::new()
2422            .wallet(wallet)
2423            .connect(self.config.http_rpc_endpoint.as_str())
2424            .await
2425            .map_err(Error::Transport)?;
2426
2427        let call = submitJobFromQuoteCall {
2428            serviceId: service_id,
2429            jobIndex: job_index,
2430            inputs,
2431            quotes,
2432        };
2433        let calldata = call.abi_encode();
2434
2435        let mut tx_request = TransactionRequest::default()
2436            .to(self.tangle_address)
2437            .input(calldata.into());
2438        if total_value > U256::ZERO {
2439            tx_request = tx_request.value(total_value);
2440        }
2441
2442        let pending_tx = provider
2443            .send_transaction(tx_request)
2444            .await
2445            .map_err(Error::Transport)?;
2446
2447        let receipt = pending_tx
2448            .get_receipt()
2449            .await
2450            .map_err(Error::PendingTransaction)?;
2451
2452        self.parse_job_submitted(&receipt)
2453    }
2454
2455    /// Parse a `JobSubmitted` event from a transaction receipt.
2456    fn parse_job_submitted(&self, receipt: &TransactionReceipt) -> Result<JobSubmissionResult> {
2457        let tx = TransactionResult {
2458            tx_hash: receipt.transaction_hash,
2459            block_number: receipt.block_number,
2460            gas_used: receipt.gas_used,
2461            success: receipt.status(),
2462        };
2463
2464        let job_submitted_sig = keccak256("JobSubmitted(uint64,uint64,uint8,address,bytes)");
2465        let call_id = receipt
2466            .logs()
2467            .iter()
2468            .find_map(|log| {
2469                let topics = log.topics();
2470                if log.address() != self.tangle_address || topics.len() < 3 {
2471                    return None;
2472                }
2473                if topics[0].0 != job_submitted_sig {
2474                    return None;
2475                }
2476                let mut buf = [0u8; 32];
2477                buf.copy_from_slice(topics[2].as_slice());
2478                Some(U256::from_be_bytes(buf).to::<u64>())
2479            })
2480            .ok_or_else(|| {
2481                let status = receipt.status();
2482                let log_count = receipt.logs().len();
2483                let topics: Vec<String> = receipt
2484                    .logs()
2485                    .iter()
2486                    .map(|log| {
2487                        log.topics()
2488                            .iter()
2489                            .map(|topic| format!("{topic:#x}"))
2490                            .collect::<Vec<_>>()
2491                            .join(",")
2492                    })
2493                    .collect();
2494                Error::Contract(format!(
2495                    "submitJob receipt missing JobSubmitted event (status={status:?}, logs={log_count}, topics={topics:?})"
2496                ))
2497            })?;
2498
2499        Ok(JobSubmissionResult { tx, call_id })
2500    }
2501
2502    /// Submit a job result to the Tangle contract
2503    ///
2504    /// This sends a signed transaction to submit a single operator's result.
2505    ///
2506    /// # Arguments
2507    /// * `service_id` - The service ID
2508    /// * `call_id` - The call/job ID
2509    /// * `output` - The encoded result output
2510    ///
2511    /// # Returns
2512    /// The transaction hash and receipt on success
2513    pub async fn submit_result(
2514        &self,
2515        service_id: u64,
2516        call_id: u64,
2517        output: Bytes,
2518    ) -> Result<TransactionResult> {
2519        use crate::contracts::ITangle::submitResultCall;
2520        use alloy_sol_types::SolCall;
2521
2522        let wallet = self.wallet()?;
2523        let provider = ProviderBuilder::new()
2524            .wallet(wallet)
2525            .connect(self.config.http_rpc_endpoint.as_str())
2526            .await
2527            .map_err(Error::Transport)?;
2528
2529        let call = submitResultCall {
2530            serviceId: service_id,
2531            callId: call_id,
2532            result: output,
2533        };
2534        let calldata = call.abi_encode();
2535
2536        let tx_request = TransactionRequest::default()
2537            .to(self.tangle_address)
2538            .input(calldata.into());
2539
2540        let pending_tx = provider
2541            .send_transaction(tx_request)
2542            .await
2543            .map_err(Error::Transport)?;
2544
2545        let receipt = pending_tx
2546            .get_receipt()
2547            .await
2548            .map_err(Error::PendingTransaction)?;
2549
2550        Ok(TransactionResult {
2551            tx_hash: receipt.transaction_hash,
2552            block_number: receipt.block_number,
2553            gas_used: receipt.gas_used,
2554            success: receipt.status(),
2555        })
2556    }
2557
2558    /// Submit an aggregated BLS signature result to the Tangle contract
2559    ///
2560    /// This sends a signed transaction to submit an aggregated result with BLS signature.
2561    ///
2562    /// # Arguments
2563    /// * `service_id` - The service ID
2564    /// * `call_id` - The call/job ID
2565    /// * `output` - The encoded result output
2566    /// * `signer_bitmap` - Bitmap indicating which operators signed
2567    /// * `aggregated_signature` - The aggregated BLS signature [2]
2568    /// * `aggregated_pubkey` - The aggregated BLS public key [4]
2569    ///
2570    /// # Returns
2571    /// The transaction hash and receipt on success
2572    pub async fn submit_aggregated_result(
2573        &self,
2574        service_id: u64,
2575        call_id: u64,
2576        output: Bytes,
2577        signer_bitmap: U256,
2578        aggregated_signature: [U256; 2],
2579        aggregated_pubkey: [U256; 4],
2580    ) -> Result<TransactionResult> {
2581        use crate::contracts::ITangle::submitAggregatedResultCall;
2582        use alloy_sol_types::SolCall;
2583
2584        let wallet = self.wallet()?;
2585        let provider = ProviderBuilder::new()
2586            .wallet(wallet)
2587            .connect(self.config.http_rpc_endpoint.as_str())
2588            .await
2589            .map_err(Error::Transport)?;
2590
2591        let call = submitAggregatedResultCall {
2592            serviceId: service_id,
2593            callId: call_id,
2594            output,
2595            signerBitmap: signer_bitmap,
2596            aggregatedSignature: aggregated_signature,
2597            aggregatedPubkey: aggregated_pubkey,
2598        };
2599        let calldata = call.abi_encode();
2600
2601        let tx_request = TransactionRequest::default()
2602            .to(self.tangle_address)
2603            .input(calldata.into());
2604
2605        let pending_tx = provider
2606            .send_transaction(tx_request)
2607            .await
2608            .map_err(Error::Transport)?;
2609
2610        let receipt = pending_tx
2611            .get_receipt()
2612            .await
2613            .map_err(Error::PendingTransaction)?;
2614
2615        Ok(TransactionResult {
2616            tx_hash: receipt.transaction_hash,
2617            block_number: receipt.block_number,
2618            gas_used: receipt.gas_used,
2619            success: receipt.status(),
2620        })
2621    }
2622
2623    async fn extract_request_id(
2624        &self,
2625        receipt: &TransactionReceipt,
2626        blueprint_id: u64,
2627    ) -> Result<u64> {
2628        if let Some(event) = receipt.decoded_log::<ITangle::ServiceRequested>() {
2629            return Ok(event.data.requestId);
2630        }
2631        if let Some(event) = receipt.decoded_log::<ITangle::ServiceRequestedWithSecurity>() {
2632            return Ok(event.data.requestId);
2633        }
2634
2635        let requested_sig = keccak256("ServiceRequested(uint64,uint64,address)".as_bytes());
2636        let requested_with_security_sig = keccak256(
2637            "ServiceRequestedWithSecurity(uint64,uint64,address,address[],((uint8,address),uint16,uint16)[])"
2638                .as_bytes(),
2639        );
2640
2641        for log in receipt.logs() {
2642            let topics = log.topics();
2643            if topics.is_empty() {
2644                continue;
2645            }
2646            let sig = topics[0].0;
2647            if sig != requested_sig && sig != requested_with_security_sig {
2648                continue;
2649            }
2650            if topics.len() < 2 {
2651                continue;
2652            }
2653
2654            let mut buf = [0u8; 32];
2655            buf.copy_from_slice(topics[1].as_slice());
2656            let id = U256::from_be_bytes(buf).to::<u64>();
2657            return Ok(id);
2658        }
2659
2660        if let Some(block_number) = receipt.block_number {
2661            let filter = Filter::new()
2662                .select(block_number)
2663                .address(self.tangle_address)
2664                .event_signature(vec![requested_sig, requested_with_security_sig]);
2665            if let Ok(logs) = self.get_logs(&filter).await {
2666                for log in logs {
2667                    let topics = log.topics();
2668                    if topics.len() < 2 {
2669                        continue;
2670                    }
2671                    let mut buf = [0u8; 32];
2672                    buf.copy_from_slice(topics[1].as_slice());
2673                    let id = U256::from_be_bytes(buf).to::<u64>();
2674                    return Ok(id);
2675                }
2676            }
2677        }
2678
2679        let count = self.service_request_count().await?;
2680        if count == 0 {
2681            return Err(Error::Contract(
2682                "requestService receipt missing ServiceRequested event".into(),
2683            ));
2684        }
2685
2686        let account = self.account();
2687        let start = count.saturating_sub(5);
2688        for candidate in (start..count).rev() {
2689            if let Ok(request) = self.get_service_request(candidate).await
2690                && request.blueprintId == blueprint_id
2691                && request.requester == account
2692            {
2693                return Ok(candidate);
2694            }
2695        }
2696
2697        Ok(count - 1)
2698    }
2699
2700    fn extract_blueprint_id(&self, receipt: &TransactionReceipt) -> Result<u64> {
2701        for log in receipt.logs() {
2702            if let Ok(event) = log.log_decode::<ITangle::BlueprintCreated>() {
2703                return Ok(event.inner.blueprintId);
2704            }
2705        }
2706
2707        Err(Error::Contract(
2708            "createBlueprint receipt missing BlueprintCreated event".into(),
2709        ))
2710    }
2711}
2712
2713/// Result of a submitted transaction
2714#[derive(Debug, Clone)]
2715pub struct TransactionResult {
2716    /// Transaction hash
2717    pub tx_hash: B256,
2718    /// Block number the transaction was included in
2719    pub block_number: Option<u64>,
2720    /// Gas used by the transaction
2721    pub gas_used: u64,
2722    /// Whether the transaction succeeded
2723    pub success: bool,
2724}
2725
2726/// Result of submitting a job via `submitJob`.
2727#[derive(Debug, Clone)]
2728pub struct JobSubmissionResult {
2729    /// Transaction metadata.
2730    pub tx: TransactionResult,
2731    /// Call identifier assigned by the contract.
2732    pub call_id: u64,
2733}
2734
2735/// Threshold type for BLS aggregation
2736#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2737pub enum ThresholdType {
2738    /// Threshold based on number of operators (e.g., 67% of operators must sign)
2739    CountBased,
2740    /// Threshold based on stake weight (e.g., 67% of total stake must sign)
2741    StakeWeighted,
2742}
2743
2744/// Configuration for BLS signature aggregation
2745#[derive(Debug, Clone)]
2746pub struct AggregationConfig {
2747    /// Whether aggregation is required for this job
2748    pub required: bool,
2749    /// Threshold in basis points (e.g., 6700 = 67%)
2750    pub threshold_bps: u16,
2751    /// Type of threshold calculation
2752    pub threshold_type: ThresholdType,
2753}
2754
2755/// Convert ECDSA public key to Ethereum address
2756fn ecdsa_public_key_to_address(pubkey: &[u8]) -> Result<Address> {
2757    use alloy_primitives::keccak256;
2758
2759    // Handle both compressed (33 bytes) and uncompressed (65 bytes) keys
2760    let uncompressed = if pubkey.len() == 33 {
2761        // Decompress the key using k256
2762        use k256::EncodedPoint;
2763        use k256::elliptic_curve::sec1::FromEncodedPoint;
2764
2765        let point = EncodedPoint::from_bytes(pubkey)
2766            .map_err(|e| Error::InvalidAddress(format!("Invalid compressed key: {e}")))?;
2767
2768        let pubkey: k256::PublicKey = Option::from(k256::PublicKey::from_encoded_point(&point))
2769            .ok_or_else(|| Error::InvalidAddress("Failed to decompress public key".into()))?;
2770
2771        pubkey.to_encoded_point(false).as_bytes().to_vec()
2772    } else if pubkey.len() == 65 {
2773        pubkey.to_vec()
2774    } else if pubkey.len() == 64 {
2775        // Already without prefix
2776        let mut full = vec![0x04];
2777        full.extend_from_slice(pubkey);
2778        full
2779    } else {
2780        return Err(Error::InvalidAddress(format!(
2781            "Invalid public key length: {}",
2782            pubkey.len()
2783        )));
2784    };
2785
2786    // Skip the 0x04 prefix and hash the rest
2787    let hash = keccak256(&uncompressed[1..]);
2788
2789    // Take the last 20 bytes as the address
2790    Ok(Address::from_slice(&hash[12..]))
2791}
2792
2793fn normalize_public_key(raw: &[u8]) -> Result<EcdsaPublicKey> {
2794    match raw.len() {
2795        65 => {
2796            let mut key = [0u8; 65];
2797            key.copy_from_slice(raw);
2798            Ok(key)
2799        }
2800        64 => {
2801            let mut key = [0u8; 65];
2802            key[0] = 0x04;
2803            key[1..].copy_from_slice(raw);
2804            Ok(key)
2805        }
2806        33 => {
2807            use k256::EncodedPoint;
2808            use k256::elliptic_curve::sec1::FromEncodedPoint;
2809
2810            let point = EncodedPoint::from_bytes(raw)
2811                .map_err(|e| Error::InvalidAddress(format!("Invalid compressed key: {e}")))?;
2812            let public_key: k256::PublicKey =
2813                Option::from(k256::PublicKey::from_encoded_point(&point)).ok_or_else(|| {
2814                    Error::InvalidAddress("Failed to decompress public key".into())
2815                })?;
2816            let encoded = public_key.to_encoded_point(false);
2817            let bytes = encoded.as_bytes();
2818            let mut key = [0u8; 65];
2819            key.copy_from_slice(bytes);
2820            Ok(key)
2821        }
2822        0 => Err(Error::Other(
2823            "Operator has not published an ECDSA public key".into(),
2824        )),
2825        len => Err(Error::InvalidAddress(format!(
2826            "Unexpected operator key length: {len}"
2827        ))),
2828    }
2829}
2830
2831fn asset_info_from_types(asset: IMultiAssetDelegationTypes::Asset) -> AssetInfo {
2832    AssetInfo {
2833        kind: AssetKind::from(asset.kind),
2834        token: asset.token,
2835    }
2836}
2837
2838fn selection_mode_to_u8(mode: BlueprintSelectionMode) -> u8 {
2839    match mode {
2840        BlueprintSelectionMode::All => 0,
2841        BlueprintSelectionMode::Fixed => 1,
2842        BlueprintSelectionMode::Unknown(value) => value,
2843    }
2844}
2845
2846fn transaction_result_from_receipt(receipt: &TransactionReceipt) -> TransactionResult {
2847    TransactionResult {
2848        tx_hash: receipt.transaction_hash,
2849        block_number: receipt.block_number,
2850        gas_used: receipt.gas_used,
2851        success: receipt.status(),
2852    }
2853}
2854
2855// ═══════════════════════════════════════════════════════════════════════════════
2856// BLUEPRINT SERVICES CLIENT IMPLEMENTATION
2857// ═══════════════════════════════════════════════════════════════════════════════
2858
2859impl BlueprintServicesClient for TangleClient {
2860    type PublicApplicationIdentity = EcdsaPublicKey;
2861    type PublicAccountIdentity = Address;
2862    type Id = u64;
2863    type Error = Error;
2864
2865    /// Get all operators for the current service with their ECDSA keys
2866    async fn get_operators(
2867        &self,
2868    ) -> core::result::Result<
2869        OperatorSet<Self::PublicAccountIdentity, Self::PublicApplicationIdentity>,
2870        Self::Error,
2871    > {
2872        let service_id = self
2873            .config
2874            .settings
2875            .service_id
2876            .ok_or_else(|| Error::Other("No service ID configured".into()))?;
2877
2878        // Get service operators
2879        let operators = self.get_service_operators(service_id).await?;
2880
2881        let mut map = BTreeMap::new();
2882
2883        for operator in operators {
2884            let metadata = self
2885                .get_operator_metadata(self.config.settings.blueprint_id, operator)
2886                .await?;
2887            map.insert(operator, metadata.public_key);
2888        }
2889
2890        Ok(map)
2891    }
2892
2893    /// Get the current operator's ECDSA public key
2894    async fn operator_id(
2895        &self,
2896    ) -> core::result::Result<Self::PublicApplicationIdentity, Self::Error> {
2897        let key = self
2898            .keystore
2899            .first_local::<K256Ecdsa>()
2900            .map_err(Error::Keystore)?;
2901
2902        // Convert VerifyingKey to 65-byte uncompressed format
2903        let encoded = key.0.to_encoded_point(false);
2904        let bytes = encoded.as_bytes();
2905
2906        let mut uncompressed = [0u8; 65];
2907        uncompressed.copy_from_slice(bytes);
2908
2909        Ok(uncompressed)
2910    }
2911
2912    /// Get the current blueprint ID
2913    async fn blueprint_id(&self) -> core::result::Result<Self::Id, Self::Error> {
2914        Ok(self.config.settings.blueprint_id)
2915    }
2916}