1extern 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
67fn 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
79async 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
137pub type TangleProvider = DynProvider<Ethereum>;
139
140pub type EcdsaPublicKey = [u8; 65];
142
143pub type CompressedEcdsaPublicKey = [u8; 33];
145
146#[derive(Debug, Clone)]
148pub struct RestakingMetadata {
149 pub stake: U256,
151 pub delegation_count: u32,
153 pub status: RestakingStatus,
155 pub leaving_round: u64,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum RestakingStatus {
162 Active,
164 Inactive,
166 Leaving,
168 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum DelegationMode {
188 Disabled,
190 Whitelist,
192 Open,
194 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum AssetKind {
223 Native,
225 Erc20,
227 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum BlueprintSelectionMode {
254 All,
256 Fixed,
258 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum LockMultiplier {
285 None,
287 OneMonth,
289 TwoMonths,
291 ThreeMonths,
293 SixMonths,
295 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#[derive(Debug, Clone)]
327pub struct AssetInfo {
328 pub kind: AssetKind,
330 pub token: Address,
332}
333
334#[derive(Debug, Clone)]
336pub struct DepositInfo {
337 pub amount: U256,
339 pub delegated_amount: U256,
341}
342
343#[derive(Debug, Clone)]
345pub struct LockInfo {
346 pub amount: U256,
348 pub multiplier: LockMultiplier,
350 pub expiry_block: u64,
352}
353
354#[derive(Debug, Clone)]
356pub struct DelegationInfo {
357 pub operator: Address,
359 pub shares: U256,
361 pub asset: AssetInfo,
363 pub selection_mode: BlueprintSelectionMode,
365}
366
367#[derive(Debug, Clone)]
369pub struct DelegationRecord {
370 pub info: DelegationInfo,
372 pub blueprint_ids: Vec<u64>,
374}
375
376#[derive(Debug, Clone)]
378pub struct PendingUnstake {
379 pub operator: Address,
381 pub asset: AssetInfo,
383 pub shares: U256,
385 pub requested_round: u64,
387 pub selection_mode: BlueprintSelectionMode,
389 pub slash_factor_snapshot: U256,
391}
392
393#[derive(Debug, Clone)]
395pub struct PendingWithdrawal {
396 pub asset: AssetInfo,
398 pub amount: U256,
400 pub requested_round: u64,
402}
403
404#[derive(Debug, Clone)]
406pub struct OperatorMetadata {
407 pub public_key: EcdsaPublicKey,
409 pub rpc_endpoint: String,
411 pub restaking: RestakingMetadata,
413}
414
415#[derive(Debug, Clone)]
417pub struct OperatorStatusSnapshot {
418 pub service_id: u64,
420 pub operator: Address,
422 pub status_code: u8,
424 pub last_heartbeat: u64,
426 pub online: bool,
428}
429
430#[derive(Clone, Debug)]
432pub struct TangleEvent {
433 pub block_number: u64,
435 pub block_hash: B256,
437 pub timestamp: u64,
439 pub logs: Vec<Log>,
441}
442
443#[derive(Clone)]
445pub struct TangleClient {
446 provider: Arc<TangleProvider>,
448 tangle_address: Address,
450 restaking_address: Address,
452 status_registry_address: Address,
454 account: Address,
456 pub config: TangleClientConfig,
458 keystore: Arc<Keystore>,
460 latest_block: Arc<Mutex<Option<TangleEvent>>>,
462 block_subscription: Arc<Mutex<Option<u64>>>,
464}
465
466#[allow(clippy::missing_fields_in_debug)] impl 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 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 pub async fn with_keystore(config: TangleClientConfig, keystore: Keystore) -> Result<Self> {
500 let rpc_url = config.http_rpc_endpoint.as_str();
501
502 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 let ecdsa_key = keystore
512 .first_local::<K256Ecdsa>()
513 .map_err(Error::Keystore)?;
514
515 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 pub fn tangle_contract(&self) -> ITangleInstance<Arc<TangleProvider>> {
535 ITangleInstance::new(self.tangle_address, Arc::clone(&self.provider))
536 }
537
538 pub fn staking_contract(&self) -> IMultiAssetDelegationInstance<Arc<TangleProvider>> {
540 IMultiAssetDelegation::new(self.restaking_address, Arc::clone(&self.provider))
541 }
542
543 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 #[must_use]
553 pub fn account(&self) -> Address {
554 self.account
555 }
556
557 #[must_use]
559 pub fn keystore(&self) -> &Arc<Keystore> {
560 &self.keystore
561 }
562
563 #[must_use]
565 pub fn provider(&self) -> &Arc<TangleProvider> {
566 &self.provider
567 }
568
569 #[must_use]
571 pub fn tangle_address(&self) -> Address {
572 self.tangle_address
573 }
574
575 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 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 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 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 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 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 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 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 *self.latest_block.lock().await = Some(event.clone());
702
703 return Some(event);
704 }
705 }
706
707 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 pub async fn now(&self) -> Option<B256> {
721 Some(self.latest_event().await?.block_hash)
722 }
723
724 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn increase_stake(&self, amount: U256) -> Result<TransactionResult> {
2370 let bond_token = self.operator_bond_token().await?;
2371
2372 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 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 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 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 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 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 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), };
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), }
2487 }
2488
2489 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)), };
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)), }
2513 }
2514
2515 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
2911pub struct TransactionResult {
2912 pub tx_hash: B256,
2914 pub block_number: Option<u64>,
2916 pub gas_used: u64,
2918 pub success: bool,
2920}
2921
2922#[derive(Debug, Clone)]
2924pub struct JobSubmissionResult {
2925 pub tx: TransactionResult,
2927 pub call_id: u64,
2929}
2930
2931#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2933pub enum ThresholdType {
2934 CountBased,
2936 StakeWeighted,
2938}
2939
2940#[derive(Debug, Clone)]
2942pub struct AggregationConfig {
2943 pub required: bool,
2945 pub threshold_bps: u16,
2947 pub threshold_type: ThresholdType,
2949}
2950
2951fn ecdsa_public_key_to_address(pubkey: &[u8]) -> Result<Address> {
2953 use alloy_primitives::keccak256;
2954
2955 let uncompressed = if pubkey.len() == 33 {
2957 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 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 let hash = keccak256(&uncompressed[1..]);
2984
2985 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
3051impl BlueprintServicesClient for TangleClient {
3056 type PublicApplicationIdentity = EcdsaPublicKey;
3057 type PublicAccountIdentity = Address;
3058 type Id = u64;
3059 type Error = Error;
3060
3061 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 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 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 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 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 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 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 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}