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