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, SolEvent};
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, IMasterBlueprintServiceManager, IMultiAssetDelegation,
33 IMultiAssetDelegationTypes, IOperatorStatusRegistry, ITangle, ITangleBlueprints, 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;
48
49const DEFAULT_MAX_GETLOGS_RANGE: u64 = 2_000;
55
56fn resolve_max_getlogs_range() -> u64 {
59 std::env::var("MAX_GETLOGS_RANGE")
60 .ok()
61 .and_then(|v| v.parse::<u64>().ok())
62 .filter(|v| *v > 0)
63 .unwrap_or(DEFAULT_MAX_GETLOGS_RANGE)
64}
65const ERC20_APPROVE_MIN_GAS_LIMIT: u64 = 100_000;
66const REGISTER_OPERATOR_RESTAKING_MIN_GAS_LIMIT: u64 = 500_000;
67#[allow(missing_docs)]
68mod erc20 {
69 alloy_sol_types::sol! {
70 #[sol(rpc)]
71 interface IERC20 {
72 function approve(address spender, uint256 amount) external returns (bool);
73 function allowance(address owner, address spender) external view returns (uint256);
74 function balanceOf(address owner) external view returns (uint256);
75 }
76 }
77}
78
79use erc20::IERC20;
80
81use IMasterBlueprintServiceManager::BlueprintDefinitionRecorded;
82
83fn buffered_gas_limit(estimated_gas: Option<u64>, min_gas_limit: u64) -> u64 {
86 estimated_gas
87 .map(|gas| {
88 gas.saturating_mul(SUBMIT_RESULT_GAS_BUFFER_NUMERATOR)
89 / SUBMIT_RESULT_GAS_BUFFER_DENOMINATOR
90 })
91 .unwrap_or(min_gas_limit)
92 .max(min_gas_limit)
93}
94
95async fn send_transaction_with_fallback_gas<P>(
110 provider: &P,
111 from: Address,
112 tx_request: TransactionRequest,
113 min_gas_limit: u64,
114) -> Result<TransactionReceipt>
115where
116 P: Provider<Ethereum>,
117{
118 let tx_request = tx_request.from(from);
119 let (estimated_gas, estimate_error) = match provider.estimate_gas(tx_request.clone()).await {
120 Ok(gas) => (Some(gas), None),
121 Err(err) => {
122 let msg = err.to_string();
123 tracing::warn!(
124 "eth_estimateGas failed; falling back to min_gas_limit={min_gas_limit}: {msg}"
125 );
126 (None, Some(msg))
127 }
128 };
129 let gas_limit = buffered_gas_limit(estimated_gas, min_gas_limit);
130 let pending_tx = provider
131 .send_transaction(tx_request.gas_limit(gas_limit))
132 .await
133 .map_err(Error::Transport)?;
134
135 let receipt = pending_tx
136 .get_receipt()
137 .await
138 .map_err(Error::PendingTransaction)?;
139
140 if !receipt.status() {
141 let tail = estimate_error
142 .map(|e| format!(" (estimate_gas reported: {e})"))
143 .unwrap_or_default();
144 return Err(Error::Contract(format!(
145 "transaction {} reverted on-chain (block={:?}, gas_used={}){tail}",
146 receipt.transaction_hash, receipt.block_number, receipt.gas_used,
147 )));
148 }
149
150 Ok(receipt)
151}
152
153pub type TangleProvider = DynProvider<Ethereum>;
155
156pub type EcdsaPublicKey = [u8; 65];
158
159pub type CompressedEcdsaPublicKey = [u8; 33];
161
162#[derive(Debug, Clone)]
164pub struct RestakingMetadata {
165 pub stake: U256,
167 pub delegation_count: u32,
169 pub status: RestakingStatus,
171 pub leaving_round: u64,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum RestakingStatus {
178 Active,
180 Inactive,
182 Leaving,
184 Unknown(u8),
186}
187
188impl From<u8> for RestakingStatus {
189 fn from(value: u8) -> Self {
190 match value {
191 0 => RestakingStatus::Active,
192 1 => RestakingStatus::Inactive,
193 2 => RestakingStatus::Leaving,
194 other => RestakingStatus::Unknown(other),
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum DelegationMode {
204 Disabled,
206 Whitelist,
208 Open,
210 Unknown(u8),
212}
213
214impl From<u8> for DelegationMode {
215 fn from(value: u8) -> Self {
216 match value {
217 0 => DelegationMode::Disabled,
218 1 => DelegationMode::Whitelist,
219 2 => DelegationMode::Open,
220 other => DelegationMode::Unknown(other),
221 }
222 }
223}
224
225impl fmt::Display for DelegationMode {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 match self {
228 DelegationMode::Disabled => write!(f, "disabled"),
229 DelegationMode::Whitelist => write!(f, "whitelist"),
230 DelegationMode::Open => write!(f, "open"),
231 DelegationMode::Unknown(value) => write!(f, "unknown({value})"),
232 }
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum AssetKind {
239 Native,
241 Erc20,
243 Unknown(u8),
245}
246
247impl From<u8> for AssetKind {
248 fn from(value: u8) -> Self {
249 match value {
250 0 => AssetKind::Native,
251 1 => AssetKind::Erc20,
252 other => AssetKind::Unknown(other),
253 }
254 }
255}
256
257impl fmt::Display for AssetKind {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 match self {
260 AssetKind::Native => write!(f, "native"),
261 AssetKind::Erc20 => write!(f, "erc20"),
262 AssetKind::Unknown(value) => write!(f, "unknown({value})"),
263 }
264 }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum BlueprintSelectionMode {
270 All,
272 Fixed,
274 Unknown(u8),
276}
277
278impl From<u8> for BlueprintSelectionMode {
279 fn from(value: u8) -> Self {
280 match value {
281 0 => BlueprintSelectionMode::All,
282 1 => BlueprintSelectionMode::Fixed,
283 other => BlueprintSelectionMode::Unknown(other),
284 }
285 }
286}
287
288impl fmt::Display for BlueprintSelectionMode {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 match self {
291 BlueprintSelectionMode::All => write!(f, "all"),
292 BlueprintSelectionMode::Fixed => write!(f, "fixed"),
293 BlueprintSelectionMode::Unknown(value) => write!(f, "unknown({value})"),
294 }
295 }
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum LockMultiplier {
301 None,
303 OneMonth,
305 TwoMonths,
307 ThreeMonths,
309 SixMonths,
311 Unknown(u8),
313}
314
315impl From<u8> for LockMultiplier {
316 fn from(value: u8) -> Self {
317 match value {
318 0 => LockMultiplier::None,
319 1 => LockMultiplier::OneMonth,
320 2 => LockMultiplier::TwoMonths,
321 3 => LockMultiplier::ThreeMonths,
322 4 => LockMultiplier::SixMonths,
323 other => LockMultiplier::Unknown(other),
324 }
325 }
326}
327
328impl fmt::Display for LockMultiplier {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 match self {
331 LockMultiplier::None => write!(f, "none"),
332 LockMultiplier::OneMonth => write!(f, "one-month"),
333 LockMultiplier::TwoMonths => write!(f, "two-months"),
334 LockMultiplier::ThreeMonths => write!(f, "three-months"),
335 LockMultiplier::SixMonths => write!(f, "six-months"),
336 LockMultiplier::Unknown(value) => write!(f, "unknown({value})"),
337 }
338 }
339}
340
341#[derive(Debug, Clone)]
343pub struct AssetInfo {
344 pub kind: AssetKind,
346 pub token: Address,
348}
349
350#[derive(Debug, Clone)]
352pub struct DepositInfo {
353 pub amount: U256,
355 pub delegated_amount: U256,
357}
358
359#[derive(Debug, Clone)]
361pub struct LockInfo {
362 pub amount: U256,
364 pub multiplier: LockMultiplier,
366 pub expiry_timestamp: u64,
368}
369
370#[derive(Debug, Clone)]
372pub struct DelegationInfo {
373 pub operator: Address,
375 pub shares: U256,
377 pub asset: AssetInfo,
379 pub selection_mode: BlueprintSelectionMode,
381}
382
383#[derive(Debug, Clone)]
385pub struct DelegationRecord {
386 pub info: DelegationInfo,
388 pub blueprint_ids: Vec<u64>,
390}
391
392#[derive(Debug, Clone)]
394pub struct PendingUnstake {
395 pub operator: Address,
397 pub asset: AssetInfo,
399 pub shares: U256,
401 pub requested_round: u64,
403 pub selection_mode: BlueprintSelectionMode,
405 pub slash_factor_snapshot: U256,
407}
408
409#[derive(Debug, Clone)]
411pub struct PendingWithdrawal {
412 pub asset: AssetInfo,
414 pub amount: U256,
416 pub requested_round: u64,
418}
419
420#[derive(Debug, Clone)]
422pub struct OperatorMetadata {
423 pub public_key: EcdsaPublicKey,
425 pub rpc_endpoint: String,
427 pub restaking: RestakingMetadata,
429}
430
431#[derive(Debug, Clone)]
433pub struct OperatorStatusSnapshot {
434 pub service_id: u64,
436 pub operator: Address,
438 pub status_code: u8,
440 pub last_heartbeat: u64,
442 pub online: bool,
444}
445
446#[derive(Clone, Debug)]
448pub struct TangleEvent {
449 pub block_number: u64,
451 pub block_hash: B256,
453 pub timestamp: u64,
455 pub logs: Vec<Log>,
457}
458
459#[derive(Clone)]
461pub struct TangleClient {
462 provider: Arc<TangleProvider>,
464 tangle_address: Address,
466 restaking_address: Address,
468 status_registry_address: Address,
470 account: Address,
472 pub config: TangleClientConfig,
474 keystore: Arc<Keystore>,
476 latest_block: Arc<Mutex<Option<TangleEvent>>>,
478 block_subscription: Arc<Mutex<Option<u64>>>,
480 max_getlogs_range: u64,
482}
483
484#[allow(clippy::missing_fields_in_debug)] impl fmt::Debug for TangleClient {
486 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487 f.debug_struct("TangleClient")
488 .field("tangle_address", &self.tangle_address)
489 .field("restaking_address", &self.restaking_address)
490 .field("status_registry_address", &self.status_registry_address)
491 .field("account", &self.account)
492 .finish()
493 }
494}
495
496impl TangleClient {
497 pub async fn new(config: TangleClientConfig) -> Result<Self> {
505 let keystore = Keystore::new(config.keystore_config())?;
506 Self::with_keystore(config, keystore).await
507 }
508
509 pub async fn with_keystore(config: TangleClientConfig, keystore: Keystore) -> Result<Self> {
518 let rpc_url = config.http_rpc_endpoint.as_str();
519
520 let provider = ProviderBuilder::new()
522 .connect(rpc_url)
523 .await
524 .map_err(|e| Error::Config(e.to_string()))?;
525
526 let dyn_provider = DynProvider::new(provider);
527
528 let ecdsa_key = keystore
530 .first_local::<K256Ecdsa>()
531 .map_err(Error::Keystore)?;
532
533 let pubkey_bytes = ecdsa_key.0.to_encoded_point(false);
536 let account = ecdsa_public_key_to_address(pubkey_bytes.as_bytes())?;
537
538 Ok(Self {
539 provider: Arc::new(dyn_provider),
540 tangle_address: config.settings.tangle_contract,
541 restaking_address: config.settings.staking_contract,
542 status_registry_address: config.settings.status_registry_contract,
543 account,
544 config,
545 keystore: Arc::new(keystore),
546 latest_block: Arc::new(Mutex::new(None)),
547 block_subscription: Arc::new(Mutex::new(None)),
548 max_getlogs_range: resolve_max_getlogs_range(),
549 })
550 }
551
552 pub fn tangle_contract(&self) -> ITangleInstance<Arc<TangleProvider>> {
554 ITangleInstance::new(self.tangle_address, Arc::clone(&self.provider))
555 }
556
557 pub fn staking_contract(&self) -> IMultiAssetDelegationInstance<Arc<TangleProvider>> {
559 IMultiAssetDelegation::new(self.restaking_address, Arc::clone(&self.provider))
560 }
561
562 pub fn status_registry_contract(&self) -> IOperatorStatusRegistryInstance<Arc<TangleProvider>> {
564 IOperatorStatusRegistryInstance::new(
565 self.status_registry_address,
566 Arc::clone(&self.provider),
567 )
568 }
569
570 #[must_use]
572 pub fn account(&self) -> Address {
573 self.account
574 }
575
576 #[must_use]
578 pub fn keystore(&self) -> &Arc<Keystore> {
579 &self.keystore
580 }
581
582 #[must_use]
584 pub fn provider(&self) -> &Arc<TangleProvider> {
585 &self.provider
586 }
587
588 #[must_use]
590 pub fn tangle_address(&self) -> Address {
591 self.tangle_address
592 }
593
594 pub fn ecdsa_signing_key(&self) -> Result<blueprint_crypto::k256::K256SigningKey> {
599 let public = self
600 .keystore
601 .first_local::<K256Ecdsa>()
602 .map_err(Error::Keystore)?;
603 self.keystore
604 .get_secret::<K256Ecdsa>(&public)
605 .map_err(Error::Keystore)
606 }
607
608 pub fn wallet(&self) -> Result<alloy_network::EthereumWallet> {
613 let signing_key = self.ecdsa_signing_key()?;
614 let local_signer = signing_key
615 .alloy_key()
616 .map_err(|e| Error::Keystore(blueprint_keystore::Error::Other(e.to_string())))?;
617 Ok(alloy_network::EthereumWallet::from(local_signer))
618 }
619
620 pub async fn block_number(&self) -> Result<u64> {
622 self.provider
623 .get_block_number()
624 .await
625 .map_err(Error::Transport)
626 }
627
628 pub async fn get_block(&self, number: BlockNumberOrTag) -> Result<Option<Block>> {
630 self.provider
631 .get_block_by_number(number)
632 .await
633 .map_err(Error::Transport)
634 }
635
636 pub async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>> {
638 self.provider
639 .get_logs(filter)
640 .await
641 .map_err(Error::Transport)
642 }
643
644 pub async fn next_event(&self) -> Option<TangleEvent> {
652 loop {
653 let current_block = match self.block_number().await {
654 Ok(block) => block,
655 Err(err) => {
656 tracing::warn!(error = %err, "Failed to fetch current block number");
657 tokio::time::sleep(Duration::from_secs(1)).await;
658 continue;
659 }
660 };
661
662 let mut last_block = self.block_subscription.lock().await;
663 let from_block = last_block
664 .map(|block| block.saturating_add(1))
665 .unwrap_or_else(|| current_block.saturating_sub(9_999));
666
667 if from_block > current_block {
668 drop(last_block);
669 tokio::time::sleep(Duration::from_secs(1)).await;
670 continue;
671 }
672
673 let to_block = core::cmp::min(
677 from_block.saturating_add(self.max_getlogs_range.saturating_sub(1)),
678 current_block,
679 );
680
681 let Some(block) = (match self.get_block(BlockNumberOrTag::Number(to_block)).await {
683 Ok(block) => block,
684 Err(err) => {
685 tracing::warn!(error = %err, block = to_block, "Failed to fetch block");
686 drop(last_block);
687 tokio::time::sleep(Duration::from_secs(1)).await;
688 continue;
689 }
690 }) else {
691 tracing::warn!(block = to_block, "Latest block was unavailable");
692 drop(last_block);
693 tokio::time::sleep(Duration::from_secs(1)).await;
694 continue;
695 };
696
697 let filter = Filter::new()
699 .address(self.tangle_address)
700 .from_block(from_block)
701 .to_block(to_block);
702
703 let logs = match self.get_logs(&filter).await {
704 Ok(logs) => logs,
705 Err(err) => {
706 tracing::warn!(
707 error = %err,
708 from_block,
709 to_block,
710 "Failed to fetch Tangle logs"
711 );
712 drop(last_block);
713 tokio::time::sleep(Duration::from_secs(1)).await;
714 continue;
715 }
716 };
717
718 *last_block = Some(to_block);
719
720 let event = TangleEvent {
721 block_number: to_block,
722 block_hash: block.header.hash,
723 timestamp: block.header.timestamp,
724 logs,
725 };
726
727 *self.latest_block.lock().await = Some(event.clone());
729
730 return Some(event);
731 }
732 }
733
734 pub async fn latest_event(&self) -> Option<TangleEvent> {
736 let latest = self.latest_block.lock().await;
737 match &*latest {
738 Some(event) => Some(event.clone()),
739 None => {
740 drop(latest);
741 self.next_event().await
742 }
743 }
744 }
745
746 pub async fn now(&self) -> Option<B256> {
748 Some(self.latest_event().await?.block_hash)
749 }
750
751 pub async fn get_blueprint(&self, blueprint_id: u64) -> Result<ITangleTypes::Blueprint> {
757 let contract = self.tangle_contract();
758 let result = contract
759 .getBlueprint(blueprint_id)
760 .call()
761 .await
762 .map_err(|e| Error::Contract(e.to_string()))?;
763 Ok(result)
764 }
765
766 pub async fn get_blueprint_operator_count(&self, blueprint_id: u64) -> Result<u32> {
771 let contract = self.tangle_contract();
772 let count = contract
773 .blueprintOperatorCount(blueprint_id)
774 .call()
775 .await
776 .map_err(|e| Error::Contract(e.to_string()))?;
777 u32::try_from(count).map_err(|_| {
778 Error::Contract(format!(
779 "blueprintOperatorCount({blueprint_id}) = {count} exceeds u32"
780 ))
781 })
782 }
783
784 pub async fn get_raw_blueprint_definition(&self, blueprint_id: u64) -> Result<Vec<u8>> {
786 let mut data = Vec::with_capacity(4 + 32);
787 let method_hash = keccak256("getBlueprintDefinition(uint64)".as_bytes());
788 data.extend_from_slice(&method_hash[..4]);
789 let mut arg = [0u8; 32];
790 arg[24..].copy_from_slice(&blueprint_id.to_be_bytes());
791 data.extend_from_slice(&arg);
792
793 let mut request = TransactionRequest::default();
794 request.to = Some(TxKind::Call(self.tangle_address));
795 request.input = TransactionInput::new(Bytes::from(data));
796
797 let response = self
798 .provider
799 .call(request)
800 .await
801 .map_err(Error::Transport)?;
802
803 Ok(response.to_vec())
804 }
805
806 pub async fn get_raw_blueprint_definition_from_event(
823 &self,
824 blueprint_id: u64,
825 ) -> Result<Vec<u8>> {
826 let expected_hash = ITangleBlueprints::new(self.tangle_address, &self.provider)
827 .blueprintDefinitionHash(blueprint_id)
828 .call()
829 .await
830 .map_err(|e| Error::Contract(e.to_string()))?;
831 if expected_hash == B256::ZERO {
832 return Err(Error::Contract(format!(
833 "blueprint {blueprint_id} has no recorded definition hash on-chain"
834 )));
835 }
836
837 let mut id_topic = [0u8; 32];
839 id_topic[24..].copy_from_slice(&blueprint_id.to_be_bytes());
840 let id_topic = B256::from(id_topic);
841
842 let head = self.block_number().await?;
843 let mut to_block = head;
844 loop {
845 let from_block = to_block.saturating_sub(self.max_getlogs_range.saturating_sub(1));
846 let filter = Filter::new()
847 .event_signature(BlueprintDefinitionRecorded::SIGNATURE_HASH)
848 .topic1(id_topic)
849 .from_block(from_block)
850 .to_block(to_block);
851
852 let logs = self.get_logs(&filter).await?;
853 for log in logs.iter().rev() {
855 let Ok(decoded) = BlueprintDefinitionRecorded::decode_log(&log.inner) else {
859 continue;
860 };
861 let encoded = decoded.encodedDefinition.to_vec();
862 if keccak256(&encoded) == expected_hash {
863 return Ok(encoded);
864 }
865 }
866
867 if from_block == 0 {
868 break;
869 }
870 to_block = from_block.saturating_sub(1);
871 }
872
873 Err(Error::Contract(format!(
874 "no BlueprintDefinitionRecorded event matching the on-chain hash for blueprint {blueprint_id}"
875 )))
876 }
877
878 pub async fn get_blueprint_config(
880 &self,
881 blueprint_id: u64,
882 ) -> Result<ITangleTypes::BlueprintConfig> {
883 let contract = self.tangle_contract();
884 let result = contract
885 .getBlueprintConfig(blueprint_id)
886 .call()
887 .await
888 .map_err(|e| Error::Contract(e.to_string()))?;
889 Ok(result)
890 }
891
892 pub async fn get_blueprint_definition(
894 &self,
895 blueprint_id: u64,
896 ) -> Result<ITangleTypes::BlueprintDefinition> {
897 let contract = self.tangle_contract();
898 let result = contract
899 .getBlueprintDefinition(blueprint_id)
900 .call()
901 .await
902 .map_err(|e| Error::Contract(e.to_string()))?;
903 Ok(result)
904 }
905
906 pub async fn create_blueprint(&self, calldata: Vec<u8>) -> Result<(TransactionResult, u64)> {
908 let wallet = self.wallet()?;
909 let from_address = wallet.default_signer().address();
910 let provider = ProviderBuilder::new()
911 .wallet(wallet)
912 .connect(self.config.http_rpc_endpoint.as_str())
913 .await
914 .map_err(Error::Transport)?;
915 let tx_request = TransactionRequest::default()
916 .to(self.tangle_address)
917 .input(Bytes::from(calldata).into());
918 let receipt = send_transaction_with_fallback_gas(
919 &provider,
920 from_address,
921 tx_request,
922 CREATE_BLUEPRINT_MIN_GAS_LIMIT,
923 )
924 .await?;
925 let blueprint_id = self.extract_blueprint_id(&receipt)?;
926
927 Ok((transaction_result_from_receipt(&receipt), blueprint_id))
928 }
929
930 pub async fn is_operator_registered(
932 &self,
933 blueprint_id: u64,
934 operator: Address,
935 ) -> Result<bool> {
936 let contract = self.tangle_contract();
937 contract
938 .isOperatorRegistered(blueprint_id, operator)
939 .call()
940 .await
941 .map_err(|e| Error::Contract(e.to_string()))
942 }
943
944 pub async fn get_service(&self, service_id: u64) -> Result<ITangleTypes::Service> {
950 let contract = self.tangle_contract();
951 let result = contract
952 .getService(service_id)
953 .call()
954 .await
955 .map_err(|e| Error::Contract(e.to_string()))?;
956 Ok(result)
957 }
958
959 pub async fn get_service_operators(&self, service_id: u64) -> Result<Vec<Address>> {
961 let contract = self.tangle_contract();
962 contract
963 .getServiceOperators(service_id)
964 .call()
965 .await
966 .map_err(|e| Error::Contract(e.to_string()))
967 }
968
969 pub async fn is_service_operator(&self, service_id: u64, operator: Address) -> Result<bool> {
971 let contract = self.tangle_contract();
972 contract
973 .isServiceOperator(service_id, operator)
974 .call()
975 .await
976 .map_err(|e| Error::Contract(e.to_string()))
977 }
978
979 pub async fn get_service_operator(
983 &self,
984 service_id: u64,
985 operator: Address,
986 ) -> Result<ITangleTypes::ServiceOperator> {
987 let contract = self.tangle_contract();
988 let result = contract
989 .getServiceOperator(service_id, operator)
990 .call()
991 .await
992 .map_err(|e| Error::Contract(e.to_string()))?;
993 Ok(result)
994 }
995
996 pub async fn get_service_total_exposure(&self, service_id: u64) -> Result<U256> {
1000 let mut total = U256::ZERO;
1001 for operator in self.get_service_operators(service_id).await? {
1002 let op_info = self.get_service_operator(service_id, operator).await?;
1003 if op_info.active {
1004 total = total.saturating_add(U256::from(op_info.exposureBps));
1005 }
1006 }
1007 Ok(total)
1008 }
1009
1010 pub async fn get_service_operator_weights(
1015 &self,
1016 service_id: u64,
1017 ) -> Result<BTreeMap<Address, u16>> {
1018 let operators = self.get_service_operators(service_id).await?;
1019 let mut weights = BTreeMap::new();
1020
1021 for operator in operators {
1022 let op_info = self.get_service_operator(service_id, operator).await?;
1023 if op_info.active {
1024 weights.insert(operator, op_info.exposureBps);
1025 }
1026 }
1027
1028 Ok(weights)
1029 }
1030
1031 pub async fn register_operator(
1033 &self,
1034 blueprint_id: u64,
1035 rpc_endpoint: impl Into<String>,
1036 registration_inputs: Option<Bytes>,
1037 ) -> Result<TransactionResult> {
1038 use crate::contracts::ITangle::{registerOperator_0Call, registerOperator_1Call};
1039
1040 let wallet = self.wallet()?;
1041 let from_address = wallet.default_signer().address();
1042 let provider = ProviderBuilder::new()
1043 .wallet(wallet)
1044 .connect(self.config.http_rpc_endpoint.as_str())
1045 .await
1046 .map_err(Error::Transport)?;
1047
1048 let signing_key = self.ecdsa_signing_key()?;
1049 let verifying = signing_key.verifying_key();
1050 let encoded_point = verifying.0.to_encoded_point(false);
1053 let ecdsa_bytes = Bytes::copy_from_slice(encoded_point.as_bytes());
1054 let rpc_endpoint = rpc_endpoint.into();
1055
1056 let receipt = if let Some(inputs) = registration_inputs {
1057 let tx_request = TransactionRequest::default().to(self.tangle_address).input(
1058 registerOperator_0Call {
1059 blueprintId: blueprint_id,
1060 ecdsaPublicKey: ecdsa_bytes.clone(),
1061 rpcAddress: rpc_endpoint.clone(),
1062 registrationInputs: inputs,
1063 }
1064 .abi_encode()
1065 .into(),
1066 );
1067 send_transaction_with_fallback_gas(
1068 &provider,
1069 from_address,
1070 tx_request,
1071 REGISTER_BLUEPRINT_OPERATOR_MIN_GAS_LIMIT,
1072 )
1073 .await?
1074 } else {
1075 let tx_request = TransactionRequest::default().to(self.tangle_address).input(
1076 registerOperator_1Call {
1077 blueprintId: blueprint_id,
1078 ecdsaPublicKey: ecdsa_bytes.clone(),
1079 rpcAddress: rpc_endpoint.clone(),
1080 }
1081 .abi_encode()
1082 .into(),
1083 );
1084 send_transaction_with_fallback_gas(
1085 &provider,
1086 from_address,
1087 tx_request,
1088 REGISTER_BLUEPRINT_OPERATOR_MIN_GAS_LIMIT,
1089 )
1090 .await?
1091 };
1092
1093 Ok(transaction_result_from_receipt(&receipt))
1094 }
1095
1096 pub async fn unregister_operator(&self, blueprint_id: u64) -> Result<TransactionResult> {
1098 let wallet = self.wallet()?;
1099 let provider = ProviderBuilder::new()
1100 .wallet(wallet)
1101 .connect(self.config.http_rpc_endpoint.as_str())
1102 .await
1103 .map_err(Error::Transport)?;
1104 let contract = ITangle::new(self.tangle_address, &provider);
1105
1106 let receipt = contract
1107 .unregisterOperator(blueprint_id)
1108 .send()
1109 .await
1110 .map_err(|e| Error::Contract(e.to_string()))?
1111 .get_receipt()
1112 .await?;
1113
1114 Ok(transaction_result_from_receipt(&receipt))
1115 }
1116
1117 pub async fn blueprint_count(&self) -> Result<u64> {
1119 let contract = self.tangle_contract();
1120 contract
1121 .blueprintCount()
1122 .call()
1123 .await
1124 .map_err(|e| Error::Contract(e.to_string()))
1125 }
1126
1127 pub async fn service_count(&self) -> Result<u64> {
1129 let contract = self.tangle_contract();
1130 contract
1131 .serviceCount()
1132 .call()
1133 .await
1134 .map_err(|e| Error::Contract(e.to_string()))
1135 }
1136
1137 pub async fn get_service_request(
1139 &self,
1140 request_id: u64,
1141 ) -> Result<ITangleTypes::ServiceRequest> {
1142 let contract = self.tangle_contract();
1143 contract
1144 .getServiceRequest(request_id)
1145 .call()
1146 .await
1147 .map_err(|e| Error::Contract(e.to_string()))
1148 }
1149
1150 pub async fn service_request_count(&self) -> Result<u64> {
1152 let mut data = Vec::with_capacity(4);
1153 let selector = keccak256("serviceRequestCount()".as_bytes());
1154 data.extend_from_slice(&selector[..4]);
1155
1156 let mut request = TransactionRequest::default();
1157 request.to = Some(TxKind::Call(self.tangle_address));
1158 request.input = TransactionInput::new(Bytes::from(data));
1159
1160 let response = self
1161 .provider
1162 .call(request)
1163 .await
1164 .map_err(Error::Transport)?;
1165
1166 if response.len() < 32 {
1167 return Err(Error::Contract(
1168 "serviceRequestCount returned malformed data".into(),
1169 ));
1170 }
1171
1172 let raw = response.as_ref();
1173 let mut buf = [0u8; 8];
1174 buf.copy_from_slice(&raw[24..32]);
1175 Ok(u64::from_be_bytes(buf))
1176 }
1177
1178 pub async fn get_job_call(
1180 &self,
1181 service_id: u64,
1182 call_id: u64,
1183 ) -> Result<ITangleTypes::JobCall> {
1184 let contract = self.tangle_contract();
1185 contract
1186 .getJobCall(service_id, call_id)
1187 .call()
1188 .await
1189 .map_err(|e| Error::Contract(e.to_string()))
1190 }
1191
1192 pub async fn get_operator_rpc_endpoint(
1204 &self,
1205 blueprint_id: u64,
1206 operator: Address,
1207 ) -> Result<String> {
1208 use crate::contracts::ITangle::{OperatorPreferencesUpdated, OperatorRegistered};
1209
1210 let mut id_topic = [0u8; 32];
1211 id_topic[24..].copy_from_slice(&blueprint_id.to_be_bytes());
1212 let id_topic = B256::from(id_topic);
1213
1214 let head = self.block_number().await?;
1215 let mut to_block = head;
1216 loop {
1217 let from_block = to_block.saturating_sub(self.max_getlogs_range.saturating_sub(1));
1218 let filter = Filter::new()
1219 .address(self.tangle_address)
1220 .event_signature(vec![
1221 OperatorRegistered::SIGNATURE_HASH,
1222 OperatorPreferencesUpdated::SIGNATURE_HASH,
1223 ])
1224 .topic1(id_topic)
1225 .topic2(operator.into_word())
1226 .from_block(from_block)
1227 .to_block(to_block);
1228
1229 let logs = self.get_logs(&filter).await?;
1230 for log in logs.iter().rev() {
1232 let topic0 = log.topic0().copied();
1233 let rpc_address = if topic0 == Some(OperatorRegistered::SIGNATURE_HASH) {
1234 OperatorRegistered::decode_log(&log.inner)
1235 .map(|e| e.rpcAddress.clone())
1236 .ok()
1237 } else if topic0 == Some(OperatorPreferencesUpdated::SIGNATURE_HASH) {
1238 OperatorPreferencesUpdated::decode_log(&log.inner)
1239 .map(|e| e.rpcAddress.clone())
1240 .ok()
1241 } else {
1242 None
1243 };
1244 if let Some(rpc_address) = rpc_address {
1245 return Ok(rpc_address);
1246 }
1247 }
1248
1249 if from_block == 0 {
1250 break;
1251 }
1252 to_block = from_block.saturating_sub(1);
1253 }
1254
1255 Err(Error::Contract(format!(
1256 "no OperatorRegistered/OperatorPreferencesUpdated event found for blueprint \
1257 {blueprint_id} operator {operator} (unregistered, or logs pruned on this RPC)"
1258 )))
1259 }
1260
1261 pub async fn get_operator_metadata(
1269 &self,
1270 blueprint_id: u64,
1271 operator: Address,
1272 ) -> Result<OperatorMetadata> {
1273 let contract = self.tangle_contract();
1274 let prefs = contract
1275 .getOperatorPreferences(blueprint_id, operator)
1276 .call()
1277 .await
1278 .map_err(|e| Error::Contract(format!("getOperatorPreferences failed: {e}")))?;
1279 let restaking_meta = self
1280 .staking_contract()
1281 .getOperatorMetadata(operator)
1282 .call()
1283 .await
1284 .map_err(|e| Error::Contract(format!("getOperatorMetadata failed: {e}")))?;
1285 let public_key = normalize_public_key(&prefs.ecdsaPublicKey.0)?;
1286 let rpc_endpoint = match self.get_operator_rpc_endpoint(blueprint_id, operator).await {
1287 Ok(endpoint) => endpoint,
1288 Err(e) => {
1289 tracing::warn!(
1290 blueprint_id,
1291 %operator,
1292 error = %e,
1293 "operator rpc endpoint unavailable from events; returning empty endpoint"
1294 );
1295 String::new()
1296 }
1297 };
1298 Ok(OperatorMetadata {
1299 public_key,
1300 rpc_endpoint,
1301 restaking: RestakingMetadata {
1302 stake: restaking_meta.stake,
1303 delegation_count: restaking_meta.delegationCount,
1304 status: RestakingStatus::from(restaking_meta.status),
1305 leaving_round: restaking_meta.leavingRound,
1306 },
1307 })
1308 }
1309
1310 #[allow(clippy::too_many_arguments)]
1312 pub async fn request_service(
1313 &self,
1314 params: ServiceRequestParams,
1315 ) -> Result<(TransactionResult, u64)> {
1316 use crate::contracts::ITangle::{
1317 requestServiceCall, requestServiceWithExposureCall, requestServiceWithSecurityCall,
1318 };
1319
1320 let wallet = self.wallet()?;
1321 let from_address = wallet.default_signer().address();
1322 let provider = ProviderBuilder::new()
1323 .wallet(wallet)
1324 .connect(self.config.http_rpc_endpoint.as_str())
1325 .await
1326 .map_err(Error::Transport)?;
1327 let contract = ITangle::new(self.tangle_address, &provider);
1328
1329 let ServiceRequestParams {
1330 blueprint_id,
1331 operators,
1332 operator_exposures,
1333 permitted_callers,
1334 config,
1335 ttl,
1336 payment_token,
1337 payment_amount,
1338 security_requirements,
1339 } = params;
1340 let confidentiality = 0u8;
1341
1342 let is_native_payment = payment_token == Address::ZERO && payment_amount > U256::ZERO;
1343
1344 if payment_token != Address::ZERO && payment_amount > U256::ZERO {
1346 self.erc20_approve(payment_token, self.tangle_address, payment_amount)
1347 .await?;
1348 }
1349
1350 let request_id_hint = if !security_requirements.is_empty() {
1351 let mut call = contract.requestServiceWithSecurity(
1352 blueprint_id,
1353 operators.clone(),
1354 security_requirements.clone(),
1355 config.clone(),
1356 permitted_callers.clone(),
1357 ttl,
1358 payment_token,
1359 payment_amount,
1360 confidentiality,
1361 );
1362 call = call.from(self.account());
1363 if is_native_payment {
1364 call = call.value(payment_amount);
1365 }
1366 call.call().await.ok()
1367 } else if let Some(ref exposures) = operator_exposures {
1368 let mut call = contract.requestServiceWithExposure(
1369 blueprint_id,
1370 operators.clone(),
1371 exposures.clone(),
1372 config.clone(),
1373 permitted_callers.clone(),
1374 ttl,
1375 payment_token,
1376 payment_amount,
1377 confidentiality,
1378 );
1379 call = call.from(self.account());
1380 if is_native_payment {
1381 call = call.value(payment_amount);
1382 }
1383 call.call().await.ok()
1384 } else {
1385 let mut call = contract.requestService(
1386 blueprint_id,
1387 operators.clone(),
1388 config.clone(),
1389 permitted_callers.clone(),
1390 ttl,
1391 payment_token,
1392 payment_amount,
1393 confidentiality,
1394 );
1395 call = call.from(self.account());
1396 if is_native_payment {
1397 call = call.value(payment_amount);
1398 }
1399 call.call().await.ok()
1400 };
1401 let pre_count = self.service_request_count().await.ok();
1402
1403 let receipt = if !security_requirements.is_empty() {
1404 let mut tx_request = TransactionRequest::default().to(self.tangle_address).input(
1405 requestServiceWithSecurityCall {
1406 blueprintId: blueprint_id,
1407 operators: operators.clone(),
1408 securityRequirements: security_requirements.clone(),
1409 config: config.clone(),
1410 permittedCallers: permitted_callers.clone(),
1411 ttl,
1412 paymentToken: payment_token,
1413 paymentAmount: payment_amount,
1414 confidentiality,
1415 }
1416 .abi_encode()
1417 .into(),
1418 );
1419 if is_native_payment {
1420 tx_request = tx_request.value(payment_amount);
1421 }
1422 send_transaction_with_fallback_gas(
1423 &provider,
1424 from_address,
1425 tx_request,
1426 REQUEST_SERVICE_MIN_GAS_LIMIT,
1427 )
1428 .await
1429 } else if let Some(exposures) = operator_exposures {
1430 let mut tx_request = TransactionRequest::default().to(self.tangle_address).input(
1431 requestServiceWithExposureCall {
1432 blueprintId: blueprint_id,
1433 operators: operators.clone(),
1434 exposureBps: exposures,
1435 config: config.clone(),
1436 permittedCallers: permitted_callers.clone(),
1437 ttl,
1438 paymentToken: payment_token,
1439 paymentAmount: payment_amount,
1440 confidentiality,
1441 }
1442 .abi_encode()
1443 .into(),
1444 );
1445 if is_native_payment {
1446 tx_request = tx_request.value(payment_amount);
1447 }
1448 send_transaction_with_fallback_gas(
1449 &provider,
1450 from_address,
1451 tx_request,
1452 REQUEST_SERVICE_MIN_GAS_LIMIT,
1453 )
1454 .await
1455 } else {
1456 let mut tx_request = TransactionRequest::default().to(self.tangle_address).input(
1457 requestServiceCall {
1458 blueprintId: blueprint_id,
1459 operators: operators.clone(),
1460 config: config.clone(),
1461 permittedCallers: permitted_callers.clone(),
1462 ttl,
1463 paymentToken: payment_token,
1464 paymentAmount: payment_amount,
1465 confidentiality,
1466 }
1467 .abi_encode()
1468 .into(),
1469 );
1470 if is_native_payment {
1471 tx_request = tx_request.value(payment_amount);
1472 }
1473 send_transaction_with_fallback_gas(
1474 &provider,
1475 from_address,
1476 tx_request,
1477 REQUEST_SERVICE_MIN_GAS_LIMIT,
1478 )
1479 .await
1480 }
1481 .map_err(|e| Error::Contract(e.to_string()))?;
1482 if !receipt.status() {
1483 return Err(Error::Contract(
1484 "requestService transaction reverted".into(),
1485 ));
1486 }
1487
1488 let request_id = match self.extract_request_id(&receipt, blueprint_id).await {
1489 Ok(id) => id,
1490 Err(err) => {
1491 if let Some(id) = request_id_hint {
1492 return Ok((transaction_result_from_receipt(&receipt), id));
1493 }
1494 if let Some(count) = pre_count {
1495 return Ok((transaction_result_from_receipt(&receipt), count));
1496 }
1497 return Err(err);
1498 }
1499 };
1500
1501 Ok((transaction_result_from_receipt(&receipt), request_id))
1502 }
1503
1504 pub async fn join_service(
1506 &self,
1507 service_id: u64,
1508 exposure_bps: u16,
1509 ) -> Result<TransactionResult> {
1510 let wallet = self.wallet()?;
1511 let provider = ProviderBuilder::new()
1512 .wallet(wallet)
1513 .connect(self.config.http_rpc_endpoint.as_str())
1514 .await
1515 .map_err(Error::Transport)?;
1516 let contract = ITangle::new(self.tangle_address, &provider);
1517
1518 let receipt = contract
1519 .joinService(service_id, exposure_bps)
1520 .send()
1521 .await
1522 .map_err(|e| Error::Contract(e.to_string()))?
1523 .get_receipt()
1524 .await?;
1525
1526 Ok(transaction_result_from_receipt(&receipt))
1527 }
1528
1529 pub async fn join_service_with_commitments(
1534 &self,
1535 service_id: u64,
1536 exposure_bps: u16,
1537 commitments: Vec<ITangleTypes::AssetSecurityCommitment>,
1538 ) -> Result<TransactionResult> {
1539 let wallet = self.wallet()?;
1540 let provider = ProviderBuilder::new()
1541 .wallet(wallet)
1542 .connect(self.config.http_rpc_endpoint.as_str())
1543 .await
1544 .map_err(Error::Transport)?;
1545 let contract = ITangle::new(self.tangle_address, &provider);
1546
1547 let receipt = contract
1548 .joinServiceWithCommitments(service_id, exposure_bps, commitments)
1549 .send()
1550 .await
1551 .map_err(|e| Error::Contract(e.to_string()))?
1552 .get_receipt()
1553 .await?;
1554
1555 Ok(transaction_result_from_receipt(&receipt))
1556 }
1557
1558 pub async fn leave_service(&self, service_id: u64) -> Result<TransactionResult> {
1566 let wallet = self.wallet()?;
1567 let provider = ProviderBuilder::new()
1568 .wallet(wallet)
1569 .connect(self.config.http_rpc_endpoint.as_str())
1570 .await
1571 .map_err(Error::Transport)?;
1572 let contract = ITangle::new(self.tangle_address, &provider);
1573
1574 let receipt = contract
1575 .leaveService(service_id)
1576 .send()
1577 .await
1578 .map_err(|e| Error::Contract(e.to_string()))?
1579 .get_receipt()
1580 .await?;
1581
1582 Ok(transaction_result_from_receipt(&receipt))
1583 }
1584
1585 pub async fn schedule_exit(&self, service_id: u64) -> Result<TransactionResult> {
1593 let wallet = self.wallet()?;
1594 let provider = ProviderBuilder::new()
1595 .wallet(wallet)
1596 .connect(self.config.http_rpc_endpoint.as_str())
1597 .await
1598 .map_err(Error::Transport)?;
1599 let contract = ITangle::new(self.tangle_address, &provider);
1600
1601 let receipt = contract
1602 .scheduleExit(service_id)
1603 .send()
1604 .await
1605 .map_err(|e| Error::Contract(e.to_string()))?
1606 .get_receipt()
1607 .await?;
1608
1609 Ok(transaction_result_from_receipt(&receipt))
1610 }
1611
1612 pub async fn execute_exit(&self, service_id: u64) -> Result<TransactionResult> {
1617 let wallet = self.wallet()?;
1618 let provider = ProviderBuilder::new()
1619 .wallet(wallet)
1620 .connect(self.config.http_rpc_endpoint.as_str())
1621 .await
1622 .map_err(Error::Transport)?;
1623 let contract = ITangle::new(self.tangle_address, &provider);
1624
1625 let receipt = contract
1626 .executeExit(service_id)
1627 .send()
1628 .await
1629 .map_err(|e| Error::Contract(e.to_string()))?
1630 .get_receipt()
1631 .await?;
1632
1633 Ok(transaction_result_from_receipt(&receipt))
1634 }
1635
1636 pub async fn cancel_exit(&self, service_id: u64) -> Result<TransactionResult> {
1641 let wallet = self.wallet()?;
1642 let provider = ProviderBuilder::new()
1643 .wallet(wallet)
1644 .connect(self.config.http_rpc_endpoint.as_str())
1645 .await
1646 .map_err(Error::Transport)?;
1647 let contract = ITangle::new(self.tangle_address, &provider);
1648
1649 let receipt = contract
1650 .cancelExit(service_id)
1651 .send()
1652 .await
1653 .map_err(|e| Error::Contract(e.to_string()))?
1654 .get_receipt()
1655 .await?;
1656
1657 Ok(transaction_result_from_receipt(&receipt))
1658 }
1659
1660 pub async fn approve_service_with_params(
1668 &self,
1669 params: ITangleTypes::ApprovalParams,
1670 ) -> Result<TransactionResult> {
1671 use ITangle::approveServiceCall;
1672
1673 let wallet = self.wallet()?;
1674 let from_address = wallet.default_signer().address();
1675 let provider = ProviderBuilder::new()
1676 .wallet(wallet)
1677 .connect(self.config.http_rpc_endpoint.as_str())
1678 .await
1679 .map_err(Error::Transport)?;
1680
1681 let calldata = approveServiceCall { params }.abi_encode();
1682 let tx_request = TransactionRequest::default()
1683 .to(self.tangle_address)
1684 .input(Bytes::from(calldata).into());
1685
1686 let receipt = send_transaction_with_fallback_gas(
1689 &provider,
1690 from_address,
1691 tx_request,
1692 APPROVE_SERVICE_MIN_GAS_LIMIT,
1693 )
1694 .await?;
1695
1696 Ok(transaction_result_from_receipt(&receipt))
1697 }
1698
1699 pub async fn approve_service(&self, request_id: u64) -> Result<TransactionResult> {
1703 self.approve_service_with_params(ITangleTypes::ApprovalParams {
1704 requestId: request_id,
1705 securityCommitments: Vec::new(),
1706 blsPubkey: [U256::ZERO; 4],
1707 blsPopSignature: [U256::ZERO; 2],
1708 teeCommitments: Vec::new(),
1709 })
1710 .await
1711 }
1712
1713 pub async fn approve_service_with_commitments(
1715 &self,
1716 request_id: u64,
1717 commitments: Vec<ITangleTypes::AssetSecurityCommitment>,
1718 ) -> Result<TransactionResult> {
1719 self.approve_service_with_params(ITangleTypes::ApprovalParams {
1720 requestId: request_id,
1721 securityCommitments: commitments,
1722 blsPubkey: [U256::ZERO; 4],
1723 blsPopSignature: [U256::ZERO; 2],
1724 teeCommitments: Vec::new(),
1725 })
1726 .await
1727 }
1728
1729 pub async fn reject_service(&self, request_id: u64) -> Result<TransactionResult> {
1731 let wallet = self.wallet()?;
1732 let provider = ProviderBuilder::new()
1733 .wallet(wallet)
1734 .connect(self.config.http_rpc_endpoint.as_str())
1735 .await
1736 .map_err(Error::Transport)?;
1737 let contract = ITangle::new(self.tangle_address, &provider);
1738
1739 let receipt = contract
1740 .rejectService(request_id)
1741 .send()
1742 .await
1743 .map_err(|e| Error::Contract(e.to_string()))?
1744 .get_receipt()
1745 .await?;
1746
1747 Ok(transaction_result_from_receipt(&receipt))
1748 }
1749
1750 pub async fn is_operator(&self, operator: Address) -> Result<bool> {
1756 let contract = self.staking_contract();
1757 contract
1758 .isOperator(operator)
1759 .call()
1760 .await
1761 .map_err(|e| Error::Contract(e.to_string()))
1762 }
1763
1764 pub async fn is_operator_active(&self, operator: Address) -> Result<bool> {
1766 let contract = self.staking_contract();
1767 contract
1768 .isOperatorActive(operator)
1769 .call()
1770 .await
1771 .map_err(|e| Error::Contract(e.to_string()))
1772 }
1773
1774 pub async fn get_operator_stake(&self, operator: Address) -> Result<U256> {
1776 let contract = self.staking_contract();
1777 contract
1778 .getOperatorStake(operator)
1779 .call()
1780 .await
1781 .map_err(|e| Error::Contract(e.to_string()))
1782 }
1783
1784 pub async fn min_operator_stake(&self) -> Result<U256> {
1786 let contract = self.staking_contract();
1787 contract
1788 .minOperatorStake()
1789 .call()
1790 .await
1791 .map_err(|e| Error::Contract(e.to_string()))
1792 }
1793
1794 pub async fn operator_status(
1796 &self,
1797 service_id: u64,
1798 operator: Address,
1799 ) -> Result<OperatorStatusSnapshot> {
1800 if self.status_registry_address.is_zero() {
1801 return Err(Error::MissingStatusRegistry);
1802 }
1803 let contract = self.status_registry_contract();
1804
1805 let last_heartbeat = contract
1806 .getLastHeartbeat(service_id, operator)
1807 .call()
1808 .await
1809 .map_err(|e| Error::Contract(e.to_string()))?;
1810 let status_code = contract
1811 .getOperatorStatus(service_id, operator)
1812 .call()
1813 .await
1814 .map_err(|e| Error::Contract(e.to_string()))?;
1815 let online = contract
1816 .isOnline(service_id, operator)
1817 .call()
1818 .await
1819 .map_err(|e| Error::Contract(e.to_string()))?;
1820
1821 let last_heartbeat = u64::try_from(last_heartbeat).unwrap_or(u64::MAX);
1822
1823 Ok(OperatorStatusSnapshot {
1824 service_id,
1825 operator,
1826 status_code,
1827 last_heartbeat,
1828 online,
1829 })
1830 }
1831
1832 pub async fn get_restaking_metadata(&self, operator: Address) -> Result<RestakingMetadata> {
1834 let restaking_meta = self
1835 .staking_contract()
1836 .getOperatorMetadata(operator)
1837 .call()
1838 .await
1839 .map_err(|e| Error::Contract(format!("getOperatorMetadata failed: {e}")))?;
1840 Ok(RestakingMetadata {
1841 stake: restaking_meta.stake,
1842 delegation_count: restaking_meta.delegationCount,
1843 status: RestakingStatus::from(restaking_meta.status),
1844 leaving_round: restaking_meta.leavingRound,
1845 })
1846 }
1847
1848 pub async fn get_operator_self_stake(&self, operator: Address) -> Result<U256> {
1850 let contract = self.staking_contract();
1851 contract
1852 .getOperatorSelfStake(operator)
1853 .call()
1854 .await
1855 .map_err(|e| Error::Contract(e.to_string()))
1856 }
1857
1858 pub async fn get_operator_delegated_stake(&self, operator: Address) -> Result<U256> {
1860 let contract = self.staking_contract();
1861 contract
1862 .getOperatorDelegatedStake(operator)
1863 .call()
1864 .await
1865 .map_err(|e| Error::Contract(e.to_string()))
1866 }
1867
1868 pub async fn get_operator_delegators(&self, operator: Address) -> Result<Vec<Address>> {
1870 let contract = self.staking_contract();
1871 contract
1872 .getOperatorDelegators(operator)
1873 .call()
1874 .await
1875 .map_err(|e| Error::Contract(e.to_string()))
1876 }
1877
1878 pub async fn get_operator_delegator_count(&self, operator: Address) -> Result<u64> {
1880 let contract = self.staking_contract();
1881 let count = contract
1882 .getOperatorDelegatorCount(operator)
1883 .call()
1884 .await
1885 .map_err(|e| Error::Contract(e.to_string()))?;
1886 Ok(u64::try_from(count).unwrap_or(u64::MAX))
1887 }
1888
1889 pub async fn restaking_round(&self) -> Result<u64> {
1891 let contract = self.staking_contract();
1892 contract
1893 .currentRound()
1894 .call()
1895 .await
1896 .map_err(|e| Error::Contract(e.to_string()))
1897 }
1898
1899 pub async fn operator_commission_bps(&self) -> Result<u16> {
1901 let contract = self.staking_contract();
1902 contract
1903 .operatorCommissionBps()
1904 .call()
1905 .await
1906 .map_err(|e| Error::Contract(e.to_string()))
1907 }
1908
1909 pub async fn get_delegation_mode(&self, operator: Address) -> Result<DelegationMode> {
1920 let contract = self.staking_contract();
1921 let mode = contract
1922 .getDelegationMode(operator)
1923 .call()
1924 .await
1925 .map_err(|e| Error::Contract(e.to_string()))?;
1926 Ok(DelegationMode::from(mode))
1927 }
1928
1929 pub async fn is_delegator_whitelisted(
1931 &self,
1932 operator: Address,
1933 delegator: Address,
1934 ) -> Result<bool> {
1935 let contract = self.staking_contract();
1936 contract
1937 .isWhitelisted(operator, delegator)
1938 .call()
1939 .await
1940 .map_err(|e| Error::Contract(e.to_string()))
1941 }
1942
1943 pub async fn can_delegate(&self, operator: Address, delegator: Address) -> Result<bool> {
1947 let contract = self.staking_contract();
1948 contract
1949 .canDelegate(operator, delegator)
1950 .call()
1951 .await
1952 .map_err(|e| Error::Contract(e.to_string()))
1953 }
1954
1955 pub async fn set_delegation_mode(&self, mode: DelegationMode) -> Result<TransactionResult> {
1963 let wallet = self.wallet()?;
1964 let provider = ProviderBuilder::new()
1965 .wallet(wallet)
1966 .connect(self.config.http_rpc_endpoint.as_str())
1967 .await
1968 .map_err(Error::Transport)?;
1969 let contract = IMultiAssetDelegation::new(self.restaking_address, provider);
1970
1971 let mode_value: u8 = match mode {
1972 DelegationMode::Disabled => 0,
1973 DelegationMode::Whitelist => 1,
1974 DelegationMode::Open => 2,
1975 DelegationMode::Unknown(v) => v,
1976 };
1977
1978 let receipt = contract
1979 .setDelegationMode(mode_value)
1980 .send()
1981 .await
1982 .map_err(|e| Error::Contract(e.to_string()))?
1983 .get_receipt()
1984 .await
1985 .map_err(|e| Error::Contract(e.to_string()))?;
1986
1987 Ok(transaction_result_from_receipt(&receipt))
1988 }
1989
1990 pub async fn set_delegation_whitelist(
1999 &self,
2000 delegators: Vec<Address>,
2001 approved: bool,
2002 ) -> Result<TransactionResult> {
2003 let wallet = self.wallet()?;
2004 let provider = ProviderBuilder::new()
2005 .wallet(wallet)
2006 .connect(self.config.http_rpc_endpoint.as_str())
2007 .await
2008 .map_err(Error::Transport)?;
2009 let contract = IMultiAssetDelegation::new(self.restaking_address, provider);
2010
2011 let receipt = contract
2012 .setDelegationWhitelist(delegators, approved)
2013 .send()
2014 .await
2015 .map_err(|e| Error::Contract(e.to_string()))?
2016 .get_receipt()
2017 .await
2018 .map_err(|e| Error::Contract(e.to_string()))?;
2019
2020 Ok(transaction_result_from_receipt(&receipt))
2021 }
2022
2023 pub async fn erc20_allowance(
2025 &self,
2026 token: Address,
2027 owner: Address,
2028 spender: Address,
2029 ) -> Result<U256> {
2030 let contract = IERC20::new(token, Arc::clone(&self.provider));
2031 contract
2032 .allowance(owner, spender)
2033 .call()
2034 .await
2035 .map_err(|e| Error::Contract(e.to_string()))
2036 }
2037
2038 pub async fn erc20_balance(&self, token: Address, owner: Address) -> Result<U256> {
2040 let contract = IERC20::new(token, Arc::clone(&self.provider));
2041 contract
2042 .balanceOf(owner)
2043 .call()
2044 .await
2045 .map_err(|e| Error::Contract(e.to_string()))
2046 }
2047
2048 pub async fn erc20_approve(
2050 &self,
2051 token: Address,
2052 spender: Address,
2053 amount: U256,
2054 ) -> Result<TransactionResult> {
2055 use crate::client::IERC20::approveCall;
2056
2057 let wallet = self.wallet()?;
2058 let from_address = wallet.default_signer().address();
2059 let provider = ProviderBuilder::new()
2060 .wallet(wallet)
2061 .connect(self.config.http_rpc_endpoint.as_str())
2062 .await
2063 .map_err(Error::Transport)?;
2064 let tx_request = TransactionRequest::default()
2065 .to(token)
2066 .input(approveCall { spender, amount }.abi_encode().into());
2067 let receipt = send_transaction_with_fallback_gas(
2068 &provider,
2069 from_address,
2070 tx_request,
2071 ERC20_APPROVE_MIN_GAS_LIMIT,
2072 )
2073 .await?;
2074
2075 Ok(transaction_result_from_receipt(&receipt))
2076 }
2077
2078 pub async fn get_deposit_info(
2080 &self,
2081 delegator: Address,
2082 token: Address,
2083 ) -> Result<DepositInfo> {
2084 let contract = self.staking_contract();
2085 let deposit = contract
2086 .getDeposit(delegator, token)
2087 .call()
2088 .await
2089 .map_err(|e| Error::Contract(e.to_string()))?;
2090 Ok(DepositInfo {
2091 amount: deposit.amount,
2092 delegated_amount: deposit.delegatedAmount,
2093 })
2094 }
2095
2096 pub async fn get_locks(&self, delegator: Address, token: Address) -> Result<Vec<LockInfo>> {
2098 let contract = self.staking_contract();
2099 let locks = contract
2100 .getLocks(delegator, token)
2101 .call()
2102 .await
2103 .map_err(|e| Error::Contract(e.to_string()))?;
2104 Ok(locks
2105 .into_iter()
2106 .map(|lock| LockInfo {
2107 amount: lock.amount,
2108 multiplier: LockMultiplier::from(lock.multiplier),
2109 expiry_timestamp: lock.expiryTimestamp,
2110 })
2111 .collect())
2112 }
2113
2114 pub async fn get_delegations(&self, delegator: Address) -> Result<Vec<DelegationInfo>> {
2116 let contract = self.staking_contract();
2117 let delegations = contract
2118 .getDelegations(delegator)
2119 .call()
2120 .await
2121 .map_err(|e| Error::Contract(e.to_string()))?;
2122 Ok(delegations
2123 .into_iter()
2124 .map(|delegation| DelegationInfo {
2125 operator: delegation.operator,
2126 shares: delegation.shares,
2127 asset: asset_info_from_types(delegation.asset),
2128 selection_mode: BlueprintSelectionMode::from(delegation.selectionMode),
2129 })
2130 .collect())
2131 }
2132
2133 pub async fn get_delegations_with_blueprints(
2135 &self,
2136 delegator: Address,
2137 ) -> Result<Vec<DelegationRecord>> {
2138 let delegations = self.get_delegations(delegator).await?;
2139 let mut records = Vec::with_capacity(delegations.len());
2140 for (idx, info) in delegations.into_iter().enumerate() {
2141 let blueprint_ids = if matches!(info.selection_mode, BlueprintSelectionMode::Fixed) {
2142 self.get_delegation_blueprints(delegator, idx as u64)
2143 .await?
2144 } else {
2145 Vec::new()
2146 };
2147 records.push(DelegationRecord {
2148 info,
2149 blueprint_ids,
2150 });
2151 }
2152 Ok(records)
2153 }
2154
2155 pub async fn get_delegation_blueprints(
2157 &self,
2158 delegator: Address,
2159 index: u64,
2160 ) -> Result<Vec<u64>> {
2161 let contract = self.staking_contract();
2162 let ids = contract
2163 .getDelegationBlueprints(delegator, U256::from(index))
2164 .call()
2165 .await
2166 .map_err(|e| Error::Contract(e.to_string()))?;
2167 Ok(ids)
2168 }
2169
2170 pub async fn get_pending_unstakes(&self, delegator: Address) -> Result<Vec<PendingUnstake>> {
2172 let contract = self.staking_contract();
2173 let unstakes = contract
2174 .getPendingUnstakes(delegator)
2175 .call()
2176 .await
2177 .map_err(|e| Error::Contract(e.to_string()))?;
2178 Ok(unstakes
2179 .into_iter()
2180 .map(|request| PendingUnstake {
2181 operator: request.operator,
2182 asset: asset_info_from_types(request.asset),
2183 shares: request.shares,
2184 requested_round: request.requestedRound,
2185 selection_mode: BlueprintSelectionMode::from(request.selectionMode),
2186 slash_factor_snapshot: request.slashFactorSnapshot,
2187 })
2188 .collect())
2189 }
2190
2191 pub async fn get_pending_withdrawals(
2193 &self,
2194 delegator: Address,
2195 ) -> Result<Vec<PendingWithdrawal>> {
2196 let contract = self.staking_contract();
2197 let withdrawals = contract
2198 .getPendingWithdrawals(delegator)
2199 .call()
2200 .await
2201 .map_err(|e| Error::Contract(e.to_string()))?;
2202 Ok(withdrawals
2203 .into_iter()
2204 .map(|request| PendingWithdrawal {
2205 asset: asset_info_from_types(request.asset),
2206 amount: request.amount,
2207 requested_round: request.requestedRound,
2208 })
2209 .collect())
2210 }
2211
2212 pub async fn deposit_and_delegate_with_options(
2214 &self,
2215 operator: Address,
2216 token: Address,
2217 amount: U256,
2218 selection_mode: BlueprintSelectionMode,
2219 blueprint_ids: Vec<u64>,
2220 ) -> Result<TransactionResult> {
2221 let wallet = self.wallet()?;
2222 let provider = ProviderBuilder::new()
2223 .wallet(wallet)
2224 .connect(self.config.http_rpc_endpoint.as_str())
2225 .await
2226 .map_err(Error::Transport)?;
2227 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2228
2229 let mut call = contract.depositAndDelegateWithOptions(
2230 operator,
2231 token,
2232 amount,
2233 selection_mode_to_u8(selection_mode),
2234 blueprint_ids,
2235 );
2236 if token == Address::ZERO {
2237 call = call.value(amount);
2238 }
2239
2240 let receipt = call
2241 .send()
2242 .await
2243 .map_err(|e| Error::Contract(e.to_string()))?
2244 .get_receipt()
2245 .await?;
2246
2247 Ok(transaction_result_from_receipt(&receipt))
2248 }
2249
2250 pub async fn delegate_with_options(
2252 &self,
2253 operator: Address,
2254 token: Address,
2255 amount: U256,
2256 selection_mode: BlueprintSelectionMode,
2257 blueprint_ids: Vec<u64>,
2258 ) -> Result<TransactionResult> {
2259 let wallet = self.wallet()?;
2260 let provider = ProviderBuilder::new()
2261 .wallet(wallet)
2262 .connect(self.config.http_rpc_endpoint.as_str())
2263 .await
2264 .map_err(Error::Transport)?;
2265 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2266
2267 let receipt = contract
2268 .delegateWithOptions(
2269 operator,
2270 token,
2271 amount,
2272 selection_mode_to_u8(selection_mode),
2273 blueprint_ids,
2274 )
2275 .send()
2276 .await
2277 .map_err(|e| Error::Contract(e.to_string()))?
2278 .get_receipt()
2279 .await?;
2280
2281 Ok(transaction_result_from_receipt(&receipt))
2282 }
2283
2284 pub async fn schedule_delegator_unstake(
2286 &self,
2287 operator: Address,
2288 token: Address,
2289 amount: U256,
2290 ) -> Result<TransactionResult> {
2291 let wallet = self.wallet()?;
2292 let provider = ProviderBuilder::new()
2293 .wallet(wallet)
2294 .connect(self.config.http_rpc_endpoint.as_str())
2295 .await
2296 .map_err(Error::Transport)?;
2297 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2298
2299 let receipt = contract
2300 .scheduleDelegatorUnstake(operator, token, amount)
2301 .send()
2302 .await
2303 .map_err(|e| Error::Contract(e.to_string()))?
2304 .get_receipt()
2305 .await?;
2306
2307 Ok(transaction_result_from_receipt(&receipt))
2308 }
2309
2310 pub async fn execute_delegator_unstake(&self) -> Result<TransactionResult> {
2312 let wallet = self.wallet()?;
2313 let provider = ProviderBuilder::new()
2314 .wallet(wallet)
2315 .connect(self.config.http_rpc_endpoint.as_str())
2316 .await
2317 .map_err(Error::Transport)?;
2318 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2319
2320 let receipt = contract
2321 .executeDelegatorUnstake()
2322 .send()
2323 .await
2324 .map_err(|e| Error::Contract(e.to_string()))?
2325 .get_receipt()
2326 .await?;
2327
2328 Ok(transaction_result_from_receipt(&receipt))
2329 }
2330
2331 pub async fn execute_delegator_unstake_and_withdraw(
2333 &self,
2334 operator: Address,
2335 token: Address,
2336 shares: U256,
2337 requested_round: u64,
2338 receiver: Address,
2339 ) -> Result<TransactionResult> {
2340 let wallet = self.wallet()?;
2341 let provider = ProviderBuilder::new()
2342 .wallet(wallet)
2343 .connect(self.config.http_rpc_endpoint.as_str())
2344 .await
2345 .map_err(Error::Transport)?;
2346 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2347
2348 let receipt = contract
2349 .executeDelegatorUnstakeAndWithdraw(operator, token, shares, requested_round, receiver)
2350 .send()
2351 .await
2352 .map_err(|e| Error::Contract(e.to_string()))?
2353 .get_receipt()
2354 .await?;
2355
2356 Ok(transaction_result_from_receipt(&receipt))
2357 }
2358
2359 pub async fn schedule_withdraw(
2361 &self,
2362 token: Address,
2363 amount: U256,
2364 ) -> Result<TransactionResult> {
2365 let wallet = self.wallet()?;
2366 let provider = ProviderBuilder::new()
2367 .wallet(wallet)
2368 .connect(self.config.http_rpc_endpoint.as_str())
2369 .await
2370 .map_err(Error::Transport)?;
2371 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2372
2373 let receipt = contract
2374 .scheduleWithdraw(token, amount)
2375 .send()
2376 .await
2377 .map_err(|e| Error::Contract(e.to_string()))?
2378 .get_receipt()
2379 .await?;
2380
2381 Ok(transaction_result_from_receipt(&receipt))
2382 }
2383
2384 pub async fn execute_withdraw(&self) -> Result<TransactionResult> {
2386 let wallet = self.wallet()?;
2387 let provider = ProviderBuilder::new()
2388 .wallet(wallet)
2389 .connect(self.config.http_rpc_endpoint.as_str())
2390 .await
2391 .map_err(Error::Transport)?;
2392 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2393
2394 let receipt = contract
2395 .executeWithdraw()
2396 .send()
2397 .await
2398 .map_err(|e| Error::Contract(e.to_string()))?
2399 .get_receipt()
2400 .await?;
2401
2402 Ok(transaction_result_from_receipt(&receipt))
2403 }
2404
2405 pub async fn schedule_operator_unstake(&self, amount: U256) -> Result<TransactionResult> {
2407 let wallet = self.wallet()?;
2408 let provider = ProviderBuilder::new()
2409 .wallet(wallet)
2410 .connect(self.config.http_rpc_endpoint.as_str())
2411 .await
2412 .map_err(Error::Transport)?;
2413 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2414
2415 let receipt = contract
2416 .scheduleOperatorUnstake(amount)
2417 .send()
2418 .await
2419 .map_err(|e| Error::Contract(e.to_string()))?
2420 .get_receipt()
2421 .await?;
2422
2423 Ok(transaction_result_from_receipt(&receipt))
2424 }
2425
2426 pub async fn execute_operator_unstake(&self) -> Result<TransactionResult> {
2428 let wallet = self.wallet()?;
2429 let provider = ProviderBuilder::new()
2430 .wallet(wallet)
2431 .connect(self.config.http_rpc_endpoint.as_str())
2432 .await
2433 .map_err(Error::Transport)?;
2434 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2435
2436 let receipt = contract
2437 .executeOperatorUnstake()
2438 .send()
2439 .await
2440 .map_err(|e| Error::Contract(e.to_string()))?
2441 .get_receipt()
2442 .await?;
2443
2444 Ok(transaction_result_from_receipt(&receipt))
2445 }
2446
2447 pub async fn start_leaving(&self) -> Result<TransactionResult> {
2449 let wallet = self.wallet()?;
2450 let provider = ProviderBuilder::new()
2451 .wallet(wallet)
2452 .connect(self.config.http_rpc_endpoint.as_str())
2453 .await
2454 .map_err(Error::Transport)?;
2455 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2456
2457 let receipt = contract
2458 .startLeaving()
2459 .send()
2460 .await
2461 .map_err(|e| Error::Contract(e.to_string()))?
2462 .get_receipt()
2463 .await?;
2464
2465 Ok(transaction_result_from_receipt(&receipt))
2466 }
2467
2468 pub async fn complete_leaving(&self) -> Result<TransactionResult> {
2470 let wallet = self.wallet()?;
2471 let provider = ProviderBuilder::new()
2472 .wallet(wallet)
2473 .connect(self.config.http_rpc_endpoint.as_str())
2474 .await
2475 .map_err(Error::Transport)?;
2476 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2477
2478 let receipt = contract
2479 .completeLeaving()
2480 .send()
2481 .await
2482 .map_err(|e| Error::Contract(e.to_string()))?
2483 .get_receipt()
2484 .await?;
2485
2486 Ok(transaction_result_from_receipt(&receipt))
2487 }
2488
2489 pub async fn operator_bond_token(&self) -> Result<Address> {
2498 let contract = self.staking_contract();
2499 contract
2500 .operatorBondToken()
2501 .call()
2502 .await
2503 .map_err(|e| Error::Contract(e.to_string()))
2504 }
2505
2506 pub async fn register_operator_restaking(
2511 &self,
2512 stake_amount: U256,
2513 ) -> Result<TransactionResult> {
2514 use crate::contracts::IMultiAssetDelegation::{
2515 registerOperatorCall, registerOperatorWithAssetCall,
2516 };
2517
2518 let bond_token = self.operator_bond_token().await?;
2519
2520 if bond_token != Address::ZERO {
2522 self.erc20_approve(bond_token, self.restaking_address, stake_amount)
2523 .await?;
2524 }
2525
2526 let wallet = self.wallet()?;
2527 let from_address = wallet.default_signer().address();
2528 let provider = ProviderBuilder::new()
2529 .wallet(wallet)
2530 .connect(self.config.http_rpc_endpoint.as_str())
2531 .await
2532 .map_err(Error::Transport)?;
2533
2534 let receipt = if bond_token == Address::ZERO {
2535 let tx_request = TransactionRequest::default()
2536 .to(self.restaking_address)
2537 .input(registerOperatorCall {}.abi_encode().into())
2538 .value(stake_amount);
2539 send_transaction_with_fallback_gas(
2540 &provider,
2541 from_address,
2542 tx_request,
2543 REGISTER_OPERATOR_RESTAKING_MIN_GAS_LIMIT,
2544 )
2545 .await?
2546 } else {
2547 let tx_request = TransactionRequest::default()
2548 .to(self.restaking_address)
2549 .input(
2550 registerOperatorWithAssetCall {
2551 token: bond_token,
2552 amount: stake_amount,
2553 }
2554 .abi_encode()
2555 .into(),
2556 );
2557 send_transaction_with_fallback_gas(
2558 &provider,
2559 from_address,
2560 tx_request,
2561 REGISTER_OPERATOR_RESTAKING_MIN_GAS_LIMIT,
2562 )
2563 .await?
2564 };
2565
2566 Ok(transaction_result_from_receipt(&receipt))
2567 }
2568
2569 pub async fn increase_stake(&self, amount: U256) -> Result<TransactionResult> {
2574 let bond_token = self.operator_bond_token().await?;
2575
2576 if bond_token != Address::ZERO {
2578 self.erc20_approve(bond_token, self.restaking_address, amount)
2579 .await?;
2580 }
2581
2582 let wallet = self.wallet()?;
2583 let provider = ProviderBuilder::new()
2584 .wallet(wallet)
2585 .connect(self.config.http_rpc_endpoint.as_str())
2586 .await
2587 .map_err(Error::Transport)?;
2588 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2589
2590 let receipt = if bond_token == Address::ZERO {
2591 contract
2593 .increaseStake()
2594 .value(amount)
2595 .send()
2596 .await
2597 .map_err(|e| Error::Contract(e.to_string()))?
2598 .get_receipt()
2599 .await?
2600 } else {
2601 contract
2603 .increaseStakeWithAsset(bond_token, amount)
2604 .send()
2605 .await
2606 .map_err(|e| Error::Contract(e.to_string()))?
2607 .get_receipt()
2608 .await?
2609 };
2610
2611 Ok(transaction_result_from_receipt(&receipt))
2612 }
2613
2614 pub async fn deposit_native(&self, amount: U256) -> Result<TransactionResult> {
2618 let wallet = self.wallet()?;
2619 let provider = ProviderBuilder::new()
2620 .wallet(wallet)
2621 .connect(self.config.http_rpc_endpoint.as_str())
2622 .await
2623 .map_err(Error::Transport)?;
2624 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2625
2626 let receipt = contract
2627 .deposit()
2628 .value(amount)
2629 .send()
2630 .await
2631 .map_err(|e| Error::Contract(e.to_string()))?
2632 .get_receipt()
2633 .await?;
2634
2635 Ok(transaction_result_from_receipt(&receipt))
2636 }
2637
2638 pub async fn deposit_erc20(&self, token: Address, amount: U256) -> Result<TransactionResult> {
2642 let wallet = self.wallet()?;
2643 let provider = ProviderBuilder::new()
2644 .wallet(wallet)
2645 .connect(self.config.http_rpc_endpoint.as_str())
2646 .await
2647 .map_err(Error::Transport)?;
2648 let contract = IMultiAssetDelegation::new(self.restaking_address, &provider);
2649
2650 let receipt = contract
2651 .depositERC20(token, amount)
2652 .send()
2653 .await
2654 .map_err(|e| Error::Contract(e.to_string()))?
2655 .get_receipt()
2656 .await?;
2657
2658 Ok(transaction_result_from_receipt(&receipt))
2659 }
2660
2661 pub async fn get_blueprint_manager(&self, service_id: u64) -> Result<Option<Address>> {
2667 let service = self.get_service(service_id).await?;
2668 let blueprint = self.get_blueprint(service.blueprintId).await?;
2669 if blueprint.manager == Address::ZERO {
2670 Ok(None)
2671 } else {
2672 Ok(Some(blueprint.manager))
2673 }
2674 }
2675
2676 pub async fn requires_aggregation(&self, service_id: u64, job_index: u8) -> Result<bool> {
2681 let manager = match self.get_blueprint_manager(service_id).await? {
2682 Some(m) => m,
2683 None => return Ok(false), };
2685
2686 let bsm = IBlueprintServiceManager::new(manager, Arc::clone(&self.provider));
2687 match bsm.requiresAggregation(service_id, job_index).call().await {
2688 Ok(required) => Ok(required),
2689 Err(_) => Ok(false), }
2691 }
2692
2693 pub async fn get_aggregation_threshold(
2699 &self,
2700 service_id: u64,
2701 job_index: u8,
2702 ) -> Result<(u16, u8)> {
2703 let manager = match self.get_blueprint_manager(service_id).await? {
2704 Some(m) => m,
2705 None => return Ok((6700, 0)), };
2707
2708 let bsm = IBlueprintServiceManager::new(manager, Arc::clone(&self.provider));
2709 match bsm
2710 .getAggregationThreshold(service_id, job_index)
2711 .call()
2712 .await
2713 {
2714 Ok(result) => Ok((result.thresholdBps, result.thresholdType)),
2715 Err(_) => Ok((6700, 0)), }
2717 }
2718
2719 pub async fn get_aggregation_config(
2723 &self,
2724 service_id: u64,
2725 job_index: u8,
2726 ) -> Result<AggregationConfig> {
2727 let requires_aggregation = self.requires_aggregation(service_id, job_index).await?;
2728 let (threshold_bps, threshold_type) = self
2729 .get_aggregation_threshold(service_id, job_index)
2730 .await?;
2731
2732 Ok(AggregationConfig {
2733 required: requires_aggregation,
2734 threshold_bps,
2735 threshold_type: if threshold_type == 0 {
2736 ThresholdType::CountBased
2737 } else {
2738 ThresholdType::StakeWeighted
2739 },
2740 })
2741 }
2742
2743 pub async fn submit_job(
2749 &self,
2750 service_id: u64,
2751 job_index: u8,
2752 inputs: Bytes,
2753 ) -> Result<JobSubmissionResult> {
2754 self.submit_job_with_value(service_id, job_index, inputs, U256::ZERO)
2755 .await
2756 }
2757
2758 pub async fn submit_job_with_value(
2763 &self,
2764 service_id: u64,
2765 job_index: u8,
2766 inputs: Bytes,
2767 value: U256,
2768 ) -> Result<JobSubmissionResult> {
2769 use crate::contracts::ITangle::submitJobCall;
2770 use alloy_sol_types::SolCall;
2771
2772 let wallet = self.wallet()?;
2773 let provider = ProviderBuilder::new()
2774 .wallet(wallet)
2775 .connect(self.config.http_rpc_endpoint.as_str())
2776 .await
2777 .map_err(Error::Transport)?;
2778
2779 let call = submitJobCall {
2780 serviceId: service_id,
2781 jobIndex: job_index,
2782 inputs,
2783 };
2784 let calldata = call.abi_encode();
2785
2786 let mut tx_request = TransactionRequest::default()
2787 .to(self.tangle_address)
2788 .input(calldata.into());
2789 if value > U256::ZERO {
2790 tx_request = tx_request.value(value);
2791 }
2792
2793 let pending_tx = provider
2794 .send_transaction(tx_request)
2795 .await
2796 .map_err(Error::Transport)?;
2797
2798 let receipt = pending_tx
2799 .get_receipt()
2800 .await
2801 .map_err(Error::PendingTransaction)?;
2802
2803 self.parse_job_submitted(&receipt)
2804 }
2805
2806 pub async fn submit_job_from_quote(
2811 &self,
2812 service_id: u64,
2813 job_index: u8,
2814 inputs: Bytes,
2815 quotes: Vec<ITangleTypes::SignedJobQuote>,
2816 ) -> Result<JobSubmissionResult> {
2817 use crate::contracts::ITangle::submitJobFromQuoteCall;
2818 use alloy_sol_types::SolCall;
2819
2820 let total_value: U256 = quotes.iter().map(|q| q.details.price).sum();
2822
2823 let wallet = self.wallet()?;
2824 let provider = ProviderBuilder::new()
2825 .wallet(wallet)
2826 .connect(self.config.http_rpc_endpoint.as_str())
2827 .await
2828 .map_err(Error::Transport)?;
2829
2830 let call = submitJobFromQuoteCall {
2831 serviceId: service_id,
2832 jobIndex: job_index,
2833 inputs,
2834 quotes,
2835 };
2836 let calldata = call.abi_encode();
2837
2838 let mut tx_request = TransactionRequest::default()
2839 .to(self.tangle_address)
2840 .input(calldata.into());
2841 if total_value > U256::ZERO {
2842 tx_request = tx_request.value(total_value);
2843 }
2844
2845 let pending_tx = provider
2846 .send_transaction(tx_request)
2847 .await
2848 .map_err(Error::Transport)?;
2849
2850 let receipt = pending_tx
2851 .get_receipt()
2852 .await
2853 .map_err(Error::PendingTransaction)?;
2854
2855 self.parse_job_submitted(&receipt)
2856 }
2857
2858 fn parse_job_submitted(&self, receipt: &TransactionReceipt) -> Result<JobSubmissionResult> {
2860 let tx = TransactionResult {
2861 tx_hash: receipt.transaction_hash,
2862 block_number: receipt.block_number,
2863 gas_used: receipt.gas_used,
2864 success: receipt.status(),
2865 };
2866
2867 let job_submitted_sig = keccak256("JobSubmitted(uint64,uint64,uint8,address,bytes)");
2868 let call_id = receipt
2869 .logs()
2870 .iter()
2871 .find_map(|log| {
2872 let topics = log.topics();
2873 if log.address() != self.tangle_address || topics.len() < 3 {
2874 return None;
2875 }
2876 if topics[0].0 != job_submitted_sig {
2877 return None;
2878 }
2879 let mut buf = [0u8; 32];
2880 buf.copy_from_slice(topics[2].as_slice());
2881 Some(U256::from_be_bytes(buf).to::<u64>())
2882 })
2883 .ok_or_else(|| {
2884 let status = receipt.status();
2885 let log_count = receipt.logs().len();
2886 let topics: Vec<String> = receipt
2887 .logs()
2888 .iter()
2889 .map(|log| {
2890 log.topics()
2891 .iter()
2892 .map(|topic| format!("{topic:#x}"))
2893 .collect::<Vec<_>>()
2894 .join(",")
2895 })
2896 .collect();
2897 Error::Contract(format!(
2898 "submitJob receipt missing JobSubmitted event (status={status:?}, logs={log_count}, topics={topics:?})"
2899 ))
2900 })?;
2901
2902 Ok(JobSubmissionResult { tx, call_id })
2903 }
2904
2905 pub async fn submit_result(
2917 &self,
2918 service_id: u64,
2919 call_id: u64,
2920 output: Bytes,
2921 ) -> Result<TransactionResult> {
2922 use crate::contracts::ITangle::submitResultCall;
2923
2924 let wallet = self.wallet()?;
2925 let from_address = wallet.default_signer().address();
2926 let provider = ProviderBuilder::new()
2927 .wallet(wallet)
2928 .connect(self.config.http_rpc_endpoint.as_str())
2929 .await
2930 .map_err(Error::Transport)?;
2931
2932 let tx_request = TransactionRequest::default().to(self.tangle_address).input(
2933 submitResultCall {
2934 serviceId: service_id,
2935 callId: call_id,
2936 result: output,
2937 }
2938 .abi_encode()
2939 .into(),
2940 );
2941
2942 let receipt = send_transaction_with_fallback_gas(
2943 &provider,
2944 from_address,
2945 tx_request,
2946 SUBMIT_RESULT_MIN_GAS_LIMIT,
2947 )
2948 .await?;
2949
2950 Ok(TransactionResult {
2951 tx_hash: receipt.transaction_hash,
2952 block_number: receipt.block_number,
2953 gas_used: receipt.gas_used,
2954 success: receipt.status(),
2955 })
2956 }
2957
2958 pub async fn submit_aggregated_result(
2973 &self,
2974 service_id: u64,
2975 call_id: u64,
2976 output: Bytes,
2977 signer_bitmap: U256,
2978 aggregated_signature: [U256; 2],
2979 aggregated_pubkey: [U256; 4],
2980 ) -> Result<TransactionResult> {
2981 use crate::contracts::ITangle::submitAggregatedResultCall;
2982 use alloy_sol_types::SolCall;
2983
2984 let wallet = self.wallet()?;
2985 let provider = ProviderBuilder::new()
2986 .wallet(wallet)
2987 .connect(self.config.http_rpc_endpoint.as_str())
2988 .await
2989 .map_err(Error::Transport)?;
2990
2991 let call = submitAggregatedResultCall {
2992 serviceId: service_id,
2993 callId: call_id,
2994 output,
2995 signerBitmap: signer_bitmap,
2996 aggregatedSignature: aggregated_signature,
2997 aggregatedPubkey: aggregated_pubkey,
2998 };
2999 let calldata = call.abi_encode();
3000
3001 let tx_request = TransactionRequest::default()
3002 .to(self.tangle_address)
3003 .input(calldata.into());
3004
3005 let pending_tx = provider
3006 .send_transaction(tx_request)
3007 .await
3008 .map_err(Error::Transport)?;
3009
3010 let receipt = pending_tx
3011 .get_receipt()
3012 .await
3013 .map_err(Error::PendingTransaction)?;
3014
3015 Ok(TransactionResult {
3016 tx_hash: receipt.transaction_hash,
3017 block_number: receipt.block_number,
3018 gas_used: receipt.gas_used,
3019 success: receipt.status(),
3020 })
3021 }
3022
3023 async fn extract_request_id(
3024 &self,
3025 receipt: &TransactionReceipt,
3026 blueprint_id: u64,
3027 ) -> Result<u64> {
3028 if let Some(event) = receipt.decoded_log::<ITangle::ServiceRequested>() {
3029 return Ok(event.data.requestId);
3030 }
3031 if let Some(event) = receipt.decoded_log::<ITangle::ServiceRequestedWithSecurity>() {
3032 return Ok(event.data.requestId);
3033 }
3034
3035 let requested_sig = keccak256("ServiceRequested(uint64,uint64,address)".as_bytes());
3036 let requested_with_security_sig = keccak256(
3037 "ServiceRequestedWithSecurity(uint64,uint64,address,address[],((uint8,address),uint16,uint16)[])"
3038 .as_bytes(),
3039 );
3040
3041 for log in receipt.logs() {
3042 let topics = log.topics();
3043 if topics.is_empty() {
3044 continue;
3045 }
3046 let sig = topics[0].0;
3047 if sig != requested_sig && sig != requested_with_security_sig {
3048 continue;
3049 }
3050 if topics.len() < 2 {
3051 continue;
3052 }
3053
3054 let mut buf = [0u8; 32];
3055 buf.copy_from_slice(topics[1].as_slice());
3056 let id = U256::from_be_bytes(buf).to::<u64>();
3057 return Ok(id);
3058 }
3059
3060 if let Some(block_number) = receipt.block_number {
3061 let filter = Filter::new()
3062 .select(block_number)
3063 .address(self.tangle_address)
3064 .event_signature(vec![requested_sig, requested_with_security_sig]);
3065 if let Ok(logs) = self.get_logs(&filter).await {
3066 for log in logs {
3067 let topics = log.topics();
3068 if topics.len() < 2 {
3069 continue;
3070 }
3071 let mut buf = [0u8; 32];
3072 buf.copy_from_slice(topics[1].as_slice());
3073 let id = U256::from_be_bytes(buf).to::<u64>();
3074 return Ok(id);
3075 }
3076 }
3077 }
3078
3079 let count = self.service_request_count().await?;
3080 if count == 0 {
3081 return Err(Error::Contract(
3082 "requestService receipt missing ServiceRequested event".into(),
3083 ));
3084 }
3085
3086 let account = self.account();
3087 let start = count.saturating_sub(5);
3088 for candidate in (start..count).rev() {
3089 if let Ok(request) = self.get_service_request(candidate).await
3090 && request.blueprintId == blueprint_id
3091 && request.requester == account
3092 {
3093 return Ok(candidate);
3094 }
3095 }
3096
3097 Ok(count - 1)
3098 }
3099
3100 fn extract_blueprint_id(&self, receipt: &TransactionReceipt) -> Result<u64> {
3101 for log in receipt.logs() {
3102 if let Ok(event) = log.log_decode::<ITangle::BlueprintCreated>() {
3103 return Ok(event.inner.blueprintId);
3104 }
3105 }
3106
3107 Err(Error::Contract(
3108 "createBlueprint receipt missing BlueprintCreated event".into(),
3109 ))
3110 }
3111}
3112
3113#[derive(Debug, Clone)]
3115pub struct TransactionResult {
3116 pub tx_hash: B256,
3118 pub block_number: Option<u64>,
3120 pub gas_used: u64,
3122 pub success: bool,
3124}
3125
3126#[derive(Debug, Clone)]
3128pub struct JobSubmissionResult {
3129 pub tx: TransactionResult,
3131 pub call_id: u64,
3133}
3134
3135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3137pub enum ThresholdType {
3138 CountBased,
3140 StakeWeighted,
3142}
3143
3144#[derive(Debug, Clone)]
3146pub struct AggregationConfig {
3147 pub required: bool,
3149 pub threshold_bps: u16,
3151 pub threshold_type: ThresholdType,
3153}
3154
3155fn ecdsa_public_key_to_address(pubkey: &[u8]) -> Result<Address> {
3157 use alloy_primitives::keccak256;
3158
3159 let uncompressed = if pubkey.len() == 33 {
3161 use k256::EncodedPoint;
3163 use k256::elliptic_curve::sec1::FromEncodedPoint;
3164
3165 let point = EncodedPoint::from_bytes(pubkey)
3166 .map_err(|e| Error::InvalidAddress(format!("Invalid compressed key: {e}")))?;
3167
3168 let pubkey: k256::PublicKey = Option::from(k256::PublicKey::from_encoded_point(&point))
3169 .ok_or_else(|| Error::InvalidAddress("Failed to decompress public key".into()))?;
3170
3171 pubkey.to_encoded_point(false).as_bytes().to_vec()
3172 } else if pubkey.len() == 65 {
3173 pubkey.to_vec()
3174 } else if pubkey.len() == 64 {
3175 let mut full = vec![0x04];
3177 full.extend_from_slice(pubkey);
3178 full
3179 } else {
3180 return Err(Error::InvalidAddress(format!(
3181 "Invalid public key length: {}",
3182 pubkey.len()
3183 )));
3184 };
3185
3186 let hash = keccak256(&uncompressed[1..]);
3188
3189 Ok(Address::from_slice(&hash[12..]))
3191}
3192
3193fn normalize_public_key(raw: &[u8]) -> Result<EcdsaPublicKey> {
3194 match raw.len() {
3195 65 => {
3196 let mut key = [0u8; 65];
3197 key.copy_from_slice(raw);
3198 Ok(key)
3199 }
3200 64 => {
3201 let mut key = [0u8; 65];
3202 key[0] = 0x04;
3203 key[1..].copy_from_slice(raw);
3204 Ok(key)
3205 }
3206 33 => {
3207 use k256::EncodedPoint;
3208 use k256::elliptic_curve::sec1::FromEncodedPoint;
3209
3210 let point = EncodedPoint::from_bytes(raw)
3211 .map_err(|e| Error::InvalidAddress(format!("Invalid compressed key: {e}")))?;
3212 let public_key: k256::PublicKey =
3213 Option::from(k256::PublicKey::from_encoded_point(&point)).ok_or_else(|| {
3214 Error::InvalidAddress("Failed to decompress public key".into())
3215 })?;
3216 let encoded = public_key.to_encoded_point(false);
3217 let bytes = encoded.as_bytes();
3218 let mut key = [0u8; 65];
3219 key.copy_from_slice(bytes);
3220 Ok(key)
3221 }
3222 0 => Err(Error::Other(
3223 "Operator has not published an ECDSA public key".into(),
3224 )),
3225 len => Err(Error::InvalidAddress(format!(
3226 "Unexpected operator key length: {len}"
3227 ))),
3228 }
3229}
3230
3231fn asset_info_from_types(asset: IMultiAssetDelegationTypes::Asset) -> AssetInfo {
3232 AssetInfo {
3233 kind: AssetKind::from(asset.kind),
3234 token: asset.token,
3235 }
3236}
3237
3238fn selection_mode_to_u8(mode: BlueprintSelectionMode) -> u8 {
3239 match mode {
3240 BlueprintSelectionMode::All => 0,
3241 BlueprintSelectionMode::Fixed => 1,
3242 BlueprintSelectionMode::Unknown(value) => value,
3243 }
3244}
3245
3246fn transaction_result_from_receipt(receipt: &TransactionReceipt) -> TransactionResult {
3247 TransactionResult {
3248 tx_hash: receipt.transaction_hash,
3249 block_number: receipt.block_number,
3250 gas_used: receipt.gas_used,
3251 success: receipt.status(),
3252 }
3253}
3254
3255impl BlueprintServicesClient for TangleClient {
3260 type PublicApplicationIdentity = EcdsaPublicKey;
3261 type PublicAccountIdentity = Address;
3262 type Id = u64;
3263 type Error = Error;
3264
3265 async fn get_operators(
3267 &self,
3268 ) -> core::result::Result<
3269 OperatorSet<Self::PublicAccountIdentity, Self::PublicApplicationIdentity>,
3270 Self::Error,
3271 > {
3272 let service_id = self
3273 .config
3274 .settings
3275 .service_id
3276 .ok_or_else(|| Error::Other("No service ID configured".into()))?;
3277
3278 let operators = self.get_service_operators(service_id).await?;
3280
3281 let mut map = BTreeMap::new();
3282
3283 for operator in operators {
3284 let metadata = self
3285 .get_operator_metadata(self.config.settings.blueprint_id, operator)
3286 .await?;
3287 map.insert(operator, metadata.public_key);
3288 }
3289
3290 Ok(map)
3291 }
3292
3293 async fn operator_id(
3295 &self,
3296 ) -> core::result::Result<Self::PublicApplicationIdentity, Self::Error> {
3297 let key = self
3298 .keystore
3299 .first_local::<K256Ecdsa>()
3300 .map_err(Error::Keystore)?;
3301
3302 let encoded = key.0.to_encoded_point(false);
3304 let bytes = encoded.as_bytes();
3305
3306 let mut uncompressed = [0u8; 65];
3307 uncompressed.copy_from_slice(bytes);
3308
3309 Ok(uncompressed)
3310 }
3311
3312 async fn blueprint_id(&self) -> core::result::Result<Self::Id, Self::Error> {
3314 Ok(self.config.settings.blueprint_id)
3315 }
3316}
3317
3318#[cfg(test)]
3319mod gas_fallback_tests {
3320 use super::{
3321 APPROVE_SERVICE_MIN_GAS_LIMIT, CREATE_BLUEPRINT_MIN_GAS_LIMIT, ERC20_APPROVE_MIN_GAS_LIMIT,
3322 REGISTER_BLUEPRINT_OPERATOR_MIN_GAS_LIMIT, REGISTER_OPERATOR_RESTAKING_MIN_GAS_LIMIT,
3323 REQUEST_SERVICE_MIN_GAS_LIMIT, SUBMIT_RESULT_GAS_BUFFER_DENOMINATOR,
3324 SUBMIT_RESULT_GAS_BUFFER_NUMERATOR, SUBMIT_RESULT_MIN_GAS_LIMIT, buffered_gas_limit,
3325 };
3326
3327 #[test]
3328 fn fallback_to_min_when_estimation_unavailable() {
3329 assert_eq!(
3330 buffered_gas_limit(None, SUBMIT_RESULT_MIN_GAS_LIMIT),
3331 SUBMIT_RESULT_MIN_GAS_LIMIT
3332 );
3333 }
3334
3335 #[test]
3336 fn buffered_estimate_applied_when_above_min() {
3337 let estimate = SUBMIT_RESULT_MIN_GAS_LIMIT * 2;
3338 let expected = estimate.saturating_mul(SUBMIT_RESULT_GAS_BUFFER_NUMERATOR)
3339 / SUBMIT_RESULT_GAS_BUFFER_DENOMINATOR;
3340 assert_eq!(
3341 buffered_gas_limit(Some(estimate), SUBMIT_RESULT_MIN_GAS_LIMIT),
3342 expected
3343 );
3344 assert!(expected > estimate, "buffer must increase estimate");
3345 }
3346
3347 #[test]
3348 fn min_floor_applied_when_buffered_estimate_is_small() {
3349 assert_eq!(
3351 buffered_gas_limit(Some(1), SUBMIT_RESULT_MIN_GAS_LIMIT),
3352 SUBMIT_RESULT_MIN_GAS_LIMIT
3353 );
3354 assert_eq!(
3355 buffered_gas_limit(Some(21_000), ERC20_APPROVE_MIN_GAS_LIMIT),
3356 ERC20_APPROVE_MIN_GAS_LIMIT
3357 );
3358 }
3359
3360 #[test]
3361 fn saturating_math_never_overflows() {
3362 let out = buffered_gas_limit(Some(u64::MAX), SUBMIT_RESULT_MIN_GAS_LIMIT);
3364 assert!(out >= SUBMIT_RESULT_MIN_GAS_LIMIT);
3365 }
3366
3367 #[test]
3368 fn min_limits_are_nonzero_sanity() {
3369 for min in [
3371 SUBMIT_RESULT_MIN_GAS_LIMIT,
3372 CREATE_BLUEPRINT_MIN_GAS_LIMIT,
3373 REGISTER_BLUEPRINT_OPERATOR_MIN_GAS_LIMIT,
3374 REQUEST_SERVICE_MIN_GAS_LIMIT,
3375 APPROVE_SERVICE_MIN_GAS_LIMIT,
3376 ERC20_APPROVE_MIN_GAS_LIMIT,
3377 REGISTER_OPERATOR_RESTAKING_MIN_GAS_LIMIT,
3378 ] {
3379 assert!(min > 21_000, "gas floor {min} below 21k base tx cost");
3380 }
3381 }
3382}
3383
3384#[cfg(test)]
3385mod definition_event_tests {
3386 use super::BlueprintDefinitionRecorded;
3387 use alloy_primitives::{Address, Bytes, LogData, U256, b256, keccak256};
3388 use alloy_sol_types::{SolEvent, SolValue};
3389
3390 #[test]
3394 fn event_signature_hash_matches_deployed_topic() {
3395 assert_eq!(
3396 BlueprintDefinitionRecorded::SIGNATURE_HASH,
3397 b256!("e0bc1e42405ffea94dcef03a915081f9674bc9eea38d46bad147b5cce7438e3e")
3398 );
3399 }
3400
3401 fn sample_log(blueprint_id: u64, encoded: &[u8]) -> LogData {
3402 let mut id_topic = [0u8; 32];
3403 id_topic[24..].copy_from_slice(&blueprint_id.to_be_bytes());
3404 let owner = Address::repeat_byte(0x11);
3405 let mut owner_topic = [0u8; 32];
3406 owner_topic[12..].copy_from_slice(owner.as_slice());
3407 let data = Bytes::from(encoded.to_vec()).abi_encode();
3409 LogData::new_unchecked(
3410 vec![
3411 BlueprintDefinitionRecorded::SIGNATURE_HASH,
3412 id_topic.into(),
3413 owner_topic.into(),
3414 ],
3415 data.into(),
3416 )
3417 }
3418
3419 #[test]
3423 fn decodes_payload_and_matches_integrity_hash() {
3424 let encoded = U256::from(0xABCDu64).to_be_bytes::<32>().to_vec();
3425 let log = sample_log(7, &encoded);
3426
3427 let decoded = BlueprintDefinitionRecorded::decode_log_data(&log).expect("decode event");
3428 assert_eq!(decoded.encodedDefinition.as_ref(), encoded.as_slice());
3429 assert_eq!(decoded.blueprintId, 7);
3430
3431 let expected = keccak256(&encoded);
3432 assert_eq!(keccak256(decoded.encodedDefinition.as_ref()), expected);
3433 }
3434
3435 #[test]
3437 fn tampered_payload_fails_integrity_hash() {
3438 let encoded = vec![1u8, 2, 3, 4];
3439 let log = sample_log(7, &encoded);
3440 let decoded = BlueprintDefinitionRecorded::decode_log_data(&log).expect("decode event");
3441
3442 let honest_hash = keccak256(&encoded);
3443 let mut tampered = decoded.encodedDefinition.to_vec();
3444 tampered[0] ^= 0xff;
3445 assert_ne!(keccak256(&tampered), honest_hash);
3446 }
3447}