1use std::{
16 ops::Div,
17 sync::Arc,
18 time::{Duration, SystemTime},
19};
20
21use async_broadcast::TrySendError;
22use futures::{Stream, StreamExt};
23use futures_time::{stream::StreamExt as TimeStreamExt, time::Duration as Duration2};
24use hopr_types::{crypto::types::Hash, primitive::prelude::HoprBalance as PrimitiveHoprBalance};
25use indexmap::IndexMap;
26
27use crate::{
28 api::{types::*, v1::graphql::services::service_type_name, *},
29 errors::{BlokliClientError, ErrorKind, InternalTxError, TrackingErrorKind},
30};
31
32fn serialize_as_empty_map<K, V, S>(_: &IndexMap<K, V>, serializer: S) -> std::result::Result<S::Ok, S::Error>
33where
34 K: serde::Serialize,
35 V: serde::Serialize,
36 S: serde::Serializer,
37{
38 serde::Serialize::serialize(&IndexMap::<K, V>::new(), serializer)
39}
40
41fn default_service_registry_config() -> ServiceRegistryConfig {
42 ServiceRegistryConfig {
43 type_registration_fee: "0 wxHOPR".into(),
44 node_safe_registry: "0x0000000000000000000000000000000000000000".into(),
45 }
46}
47
48#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
54pub struct BlokliTestState {
55 pub accounts: IndexMap<u32, Account>,
57 pub native_balances: IndexMap<String, NativeBalance>,
59 pub token_balances: IndexMap<String, HoprBalance>,
61 pub safe_allowances: IndexMap<String, SafeHoprAllowance>,
63 pub deployed_safes: IndexMap<String, Safe>,
65 pub safe_redeem_stats: IndexMap<String, RedeemedStats>,
67 pub tx_counts: IndexMap<String, u64>,
69 pub channels: IndexMap<String, Channel>,
71 pub services: IndexMap<String, ServiceEntry>,
73 pub service_types: IndexMap<String, ServiceTypeInfo>,
75 #[serde(default = "default_service_registry_config")]
77 pub service_registry_config: ServiceRegistryConfig,
78 pub chain_info: ChainInfo,
80 pub version: String,
82 pub health: String,
84 #[serde(serialize_with = "serialize_as_empty_map")]
89 pub active_txs: IndexMap<TxId, Transaction>,
90}
91
92impl PartialEq for BlokliTestState {
93 fn eq(&self, other: &Self) -> bool {
94 self.accounts == other.accounts
96 && self.deployed_safes == other.deployed_safes
97 && self.native_balances == other.native_balances
98 && self.token_balances == other.token_balances
99 && self.safe_allowances == other.safe_allowances
100 && self.safe_redeem_stats == other.safe_redeem_stats
101 && self.tx_counts == other.tx_counts
102 && self.channels == other.channels
103 && self.services == other.services
104 && self.service_types == other.service_types
105 && self.service_registry_config == other.service_registry_config
106 && self.chain_info == other.chain_info
107 && self.version == other.version
108 && self.health == other.health
109 }
110}
111
112impl Default for BlokliTestState {
113 fn default() -> Self {
114 Self {
115 accounts: Default::default(),
116 native_balances: Default::default(),
117 token_balances: Default::default(),
118 safe_allowances: Default::default(),
119 deployed_safes: Default::default(),
120 safe_redeem_stats: Default::default(),
121 tx_counts: Default::default(),
122 channels: Default::default(),
123 services: Default::default(),
124 service_types: Default::default(),
125 service_registry_config: default_service_registry_config(),
126 chain_info: ChainInfo {
127 channel_closure_grace_period: Uint64("300".into()),
128 channel_dst: Some("0000000000000000000000000000000000000000000000000000000000000000".into()),
129 block_number: 1,
130 chain_id: 100,
131 gas_price: Some("1000000000".into()),
132 ledger_dst: Some("0000000000000000000000000000000000000000000000000000000000000000".into()),
133 max_fee_per_gas: Some("3000000000".into()),
134 max_priority_fee_per_gas: Some("100000000".into()),
135 min_ticket_winning_probability: 1.0,
136 key_binding_fee: TokenValueString("0.01 wxHOPR".into()),
137 safe_registry_dst: Some("0000000000000000000000000000000000000000000000000000000000000000".into()),
138 ticket_price: TokenValueString("1 wxHOPR".into()),
139 network: "jura".into(),
140 contract_addresses: ContractAddressMap(
141 r#"
142 {
143 "announcements": "0xf1c143B1bA20C7606d56aA2FA94502D25744b982",
144 "channels": "0x77C9414043d27fdC98A6A2d73fc77b9b383092a7",
145 "module_implementation": "0x32863c4974fBb6253E338a0cb70C382DCeD2eFCb",
146 "node_safe_registry": "0x4F7C7dE3BA2B29ED8B2448dF2213cA43f94E45c0",
147 "node_stake_factory": "0x791d190b2c95397F4BcE7bD8032FD67dCEA7a5F2",
148 "node_safe_migration": "0x0000000000000000000000000000000000000000",
149 "service_registry": "0x9A676e781A523b5d0C0e43731313A708CB607508",
150 "token": "0xD4fdec44DB9D44B8f2b6d529620f9C0C7066A2c1",
151 "ticket_price_oracle": "0x442df1d946303fB088C9377eefdaeA84146DA0A6",
152 "winning_probability_oracle": "0xC15675d4CCa538D91a91a8D3EcFBB8499C3B0471",
153 "xhopr_token": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0"
154 }"#
155 .into(),
156 ),
157 expected_block_time: Uint64("5".into()),
158 finality: Uint64("3".into()),
159 },
160
161 version: "1".to_string(),
162 health: "OK".to_string(),
163 active_txs: Default::default(),
164 }
165 }
166}
167
168impl BlokliTestState {
169 fn safe_matches_owner(safe: &Safe, owner_hex: &str) -> bool {
170 safe.chain_key == owner_hex || safe.owners.iter().any(|owner| owner == owner_hex)
171 }
172
173 pub fn service_entry_key(service_type: &str, node: &ChainAddress) -> String {
178 format!("{service_type}/{}", hex::encode(node))
179 }
180
181 pub fn get_service_entry(&self, service_type: &ServiceTypeId, node: &ChainAddress) -> Option<&ServiceEntry> {
183 self.services
184 .values()
185 .find(|entry| service_type_matches(&entry.service_type, service_type) && hex_matches(&entry.node, node))
186 }
187
188 pub fn get_service_entry_mut(
190 &mut self,
191 service_type: &ServiceTypeId,
192 node: &ChainAddress,
193 ) -> Option<&mut ServiceEntry> {
194 self.services
195 .values_mut()
196 .find(|entry| service_type_matches(&entry.service_type, service_type) && hex_matches(&entry.node, node))
197 }
198
199 pub fn get_service_type(&self, service_type: &ServiceTypeId) -> Option<&ServiceTypeInfo> {
201 self.service_types
202 .values()
203 .find(|info| service_type_matches(&info.service_type, service_type))
204 }
205
206 pub fn get_service_type_mut(&mut self, service_type: &ServiceTypeId) -> Option<&mut ServiceTypeInfo> {
208 self.service_types
209 .values_mut()
210 .find(|info| service_type_matches(&info.service_type, service_type))
211 }
212
213 pub fn get_account(&self, chain_key: &ChainAddress) -> Option<&Account> {
215 self.accounts
216 .values()
217 .find(|acc| acc.chain_key == hex::encode(chain_key))
218 }
219
220 pub fn get_account_mut(&mut self, chain_key: &ChainAddress) -> Option<&mut Account> {
222 self.accounts
223 .values_mut()
224 .find(|acc| acc.chain_key == hex::encode(chain_key))
225 }
226
227 pub fn get_channel_by_id(&self, channel_id: &ChannelId) -> Option<&Channel> {
229 self.channels.get(&hex::encode(channel_id))
230 }
231
232 pub fn get_channel_by_id_mut(&mut self, channel_id: &ChannelId) -> Option<&mut Channel> {
234 self.channels.get_mut(&hex::encode(channel_id))
235 }
236
237 pub fn get_account_safe_token_balance(&self, chain_key: &ChainAddress) -> Option<&HoprBalance> {
240 let account = self.get_account(chain_key)?;
241 self.token_balances.get(account.safe_address.as_ref()?)
242 }
243
244 pub fn get_account_safe_token_balance_mut(&mut self, chain_key: &ChainAddress) -> Option<&mut HoprBalance> {
247 let account = self.get_account(chain_key).and_then(|a| a.safe_address.clone())?;
248 self.token_balances.get_mut(&account)
249 }
250
251 pub fn get_account_safe_native_balance(&self, chain_key: &ChainAddress) -> Option<&NativeBalance> {
254 let account = self.get_account(chain_key)?;
255 self.native_balances.get(account.safe_address.as_ref()?)
256 }
257
258 pub fn get_account_safe_native_balance_mut(&mut self, chain_key: &ChainAddress) -> Option<&mut NativeBalance> {
261 let account = self.get_account(chain_key).and_then(|a| a.safe_address.clone())?;
262 self.native_balances.get_mut(&account)
263 }
264
265 pub fn get_account_safe_allowance(&self, chain_key: &ChainAddress) -> Option<&SafeHoprAllowance> {
268 let account = self.get_account(chain_key)?;
269 self.safe_allowances.get(account.safe_address.as_ref()?)
270 }
271
272 pub fn get_account_safe_allowance_mut(&mut self, chain_key: &ChainAddress) -> Option<&mut SafeHoprAllowance> {
275 let account = self.get_account(chain_key).and_then(|a| a.safe_address.clone())?;
276 self.safe_allowances.get_mut(&account)
277 }
278
279 pub fn get_safe_redeem_stats(&self, chain_address: &ChainAddress) -> Option<&RedeemedStats> {
281 self.safe_redeem_stats.get(&hex::encode(chain_address))
282 }
283
284 pub fn get_safe_redeem_stats_mut(&mut self, chain_address: &ChainAddress) -> Option<&mut RedeemedStats> {
286 self.safe_redeem_stats.get_mut(&hex::encode(chain_address))
287 }
288
289 pub fn get_safe_by_owner(&self, owner: &ChainAddress) -> Vec<&Safe> {
291 let owner_hex = hex::encode(owner);
292 self.deployed_safes
293 .values()
294 .filter(|safe| Self::safe_matches_owner(safe, &owner_hex))
295 .collect()
296 }
297
298 pub fn get_safe_by_owner_mut(&mut self, owner: &ChainAddress) -> Vec<&mut Safe> {
300 let owner_hex = hex::encode(owner);
301 self.deployed_safes
302 .values_mut()
303 .filter(|safe| Self::safe_matches_owner(safe, &owner_hex))
304 .collect()
305 }
306}
307
308pub trait BlokliTestStateMutator {
314 fn update_state(&self, signed_tx: &[u8], state: &mut BlokliTestState) -> Result<()>;
321}
322
323#[derive(Clone, Debug, Default)]
328pub struct NopStateMutator;
329
330impl BlokliTestStateMutator for NopStateMutator {
331 fn update_state(&self, _: &[u8], _: &mut BlokliTestState) -> Result<()> {
332 Ok(())
333 }
334}
335
336impl<F: Fn(&[u8], &mut BlokliTestState) -> Result<()>> BlokliTestStateMutator for F {
337 fn update_state(&self, signed_tx: &[u8], state: &mut BlokliTestState) -> Result<()> {
338 self(signed_tx, state)
339 }
340}
341
342type AccountEvents = (
343 async_broadcast::Sender<Account>,
344 async_broadcast::InactiveReceiver<Account>,
345);
346
347type GraphEvents = (
348 async_broadcast::Sender<(Account, Channel, Account)>,
349 async_broadcast::InactiveReceiver<(Account, Channel, Account)>,
350);
351
352type TicketParamEvents = (
353 async_broadcast::Sender<TicketParameters>,
354 async_broadcast::InactiveReceiver<TicketParameters>,
355);
356
357type SafeDeployEvents = (async_broadcast::Sender<Safe>, async_broadcast::InactiveReceiver<Safe>);
358
359type ServiceEvents = (
360 async_broadcast::Sender<ServiceUpdate>,
361 async_broadcast::InactiveReceiver<ServiceUpdate>,
362);
363
364type ServiceTypeEvents = (
365 async_broadcast::Sender<ServiceTypeUpdate>,
366 async_broadcast::InactiveReceiver<ServiceTypeUpdate>,
367);
368
369type ServiceRegistryConfigEvents = (
370 async_broadcast::Sender<ServiceRegistryConfig>,
371 async_broadcast::InactiveReceiver<ServiceRegistryConfig>,
372);
373
374#[derive(Clone)]
379pub struct BlokliTestStateSnapshot {
380 state: Arc<parking_lot::RwLock<BlokliTestState>>,
381 snapshot: BlokliTestState,
382}
383
384impl BlokliTestStateSnapshot {
385 pub fn refresh(mut self) -> Self {
387 {
388 let state = self.state.read();
389 self.snapshot = state.clone();
390 }
391 self
392 }
393}
394
395impl AsRef<BlokliTestState> for BlokliTestStateSnapshot {
396 fn as_ref(&self) -> &BlokliTestState {
397 &self.snapshot
398 }
399}
400
401impl std::ops::Deref for BlokliTestStateSnapshot {
402 type Target = BlokliTestState;
403
404 fn deref(&self) -> &Self::Target {
405 &self.snapshot
406 }
407}
408
409#[derive(Clone)]
423pub struct BlokliTestClient<M> {
424 state: Arc<parking_lot::RwLock<BlokliTestState>>,
425 mutator: M,
426 accounts_channel: AccountEvents,
427 channels_channel: GraphEvents,
428 ticket_channel: TicketParamEvents,
429 safe_deployed_channel: SafeDeployEvents,
430 services_channel: ServiceEvents,
431 service_types_channel: ServiceTypeEvents,
432 service_registry_config_channel: ServiceRegistryConfigEvents,
433 tx_simulation_delay: Duration,
434 use_internal_txs: bool,
435}
436
437fn channel_matches(channel: &Channel, selector: &ChannelSelector, accounts: &IndexMap<u32, Account>) -> bool {
438 let filter = match selector.filter {
439 Some(ChannelFilter::ChannelId(id)) => channel.concrete_channel_id == hex::encode(id),
440 Some(ChannelFilter::DestinationKeyId(dst_id)) => channel.destination as u32 == dst_id,
441 Some(ChannelFilter::SourceKeyId(src_id)) => channel.source as u32 == src_id,
442 Some(ChannelFilter::SourceAndDestinationKeyIds(src_id, dst_id)) => {
443 channel.source as u32 == src_id && channel.destination as u32 == dst_id
444 }
445 None => true,
446 };
447 let safe_ok = selector.safe_address.is_none_or(|safe| {
448 accounts
449 .get(&(channel.source as u32))
450 .and_then(|acc| acc.safe_address.as_ref())
451 .is_some_and(|acc_safe| *acc_safe == hex::encode(safe))
452 });
453 filter && safe_ok && selector.status.is_none_or(|status| channel.status == status)
454}
455
456fn account_matches(account: &Account, selector: &AccountSelector) -> bool {
457 match selector {
458 AccountSelector::Address(address) => account.chain_key == hex::encode(address),
459 AccountSelector::KeyId(id) => account.keyid as u32 == *id,
460 AccountSelector::PacketKey(packet_key) => account.packet_key == hex::encode(packet_key),
461 AccountSelector::Any => true,
462 }
463}
464
465fn hex_matches(value: &str, expected: &[u8]) -> bool {
467 value
468 .trim_start_matches("0x")
469 .eq_ignore_ascii_case(&hex::encode(expected))
470}
471
472fn service_type_matches(rendered: &str, wanted: &ServiceTypeId) -> bool {
476 hex_matches(rendered, wanted) || service_type_name(wanted).is_some_and(|name| rendered == name)
477}
478
479fn service_matches(service_type: &str, node: &str, selector: &ServiceSelector) -> bool {
480 match selector {
481 ServiceSelector::ServiceType(wanted) => service_type_matches(service_type, wanted),
482 ServiceSelector::Node(wanted) => hex_matches(node, wanted),
483 ServiceSelector::ServiceTypeAndNode {
484 service_type: wanted_type,
485 node: wanted_node,
486 } => service_type_matches(service_type, wanted_type) && hex_matches(node, wanted_node),
487 ServiceSelector::Any => true,
488 }
489}
490
491fn broadcast_or_log<T: Clone>(sender: &async_broadcast::Sender<T>, value: T, description: &str) {
492 match sender.try_broadcast(value) {
493 Err(TrySendError::Full(_)) => {
494 tracing::error!("failed to broadcast {description} - channel is full");
495 }
496 Err(TrySendError::Closed(_)) => {
497 tracing::error!("failed to broadcast {description} - channel is closed");
498 }
499 _ => {}
500 }
501}
502
503impl<M: BlokliTestStateMutator> BlokliTestClient<M> {
504 pub fn new(initial_state: BlokliTestState, mutator: M) -> Self {
509 let (mut accounts_tx, accounts_rx) = async_broadcast::broadcast(1024);
510 accounts_tx.set_await_active(false);
511 accounts_tx.set_overflow(false);
512
513 let (mut channels_tx, channels_rx) = async_broadcast::broadcast(1024);
514 channels_tx.set_await_active(false);
515 channels_tx.set_overflow(false);
516
517 let (mut tickets_tx, tickets_rx) = async_broadcast::broadcast(1024);
518 tickets_tx.set_await_active(false);
519 tickets_tx.set_overflow(false);
520
521 let (mut safes_tx, safes_rx) = async_broadcast::broadcast(1024);
522 safes_tx.set_await_active(false);
523 safes_tx.set_overflow(false);
524
525 let (mut services_tx, services_rx) = async_broadcast::broadcast(1024);
526 services_tx.set_await_active(false);
527 services_tx.set_overflow(false);
528
529 let (mut service_types_tx, service_types_rx) = async_broadcast::broadcast(1024);
530 service_types_tx.set_await_active(false);
531 service_types_tx.set_overflow(false);
532
533 let (mut service_registry_config_tx, service_registry_config_rx) = async_broadcast::broadcast(1024);
534 service_registry_config_tx.set_await_active(false);
535 service_registry_config_tx.set_overflow(false);
536
537 Self {
538 state: Arc::new(parking_lot::RwLock::new(initial_state)),
539 mutator,
540 accounts_channel: (accounts_tx, accounts_rx.deactivate()),
541 channels_channel: (channels_tx, channels_rx.deactivate()),
542 ticket_channel: (tickets_tx, tickets_rx.deactivate()),
543 safe_deployed_channel: (safes_tx, safes_rx.deactivate()),
544 services_channel: (services_tx, services_rx.deactivate()),
545 service_types_channel: (service_types_tx, service_types_rx.deactivate()),
546 service_registry_config_channel: (service_registry_config_tx, service_registry_config_rx.deactivate()),
547 tx_simulation_delay: Duration::from_secs(1),
548 use_internal_txs: false,
549 }
550 }
551
552 #[must_use]
556 pub fn with_mutator(mut self, mutator: M) -> Self {
557 self.mutator = mutator;
558 self
559 }
560
561 #[must_use]
566 pub fn with_use_internal_txs(mut self, use_internal_txs: bool) -> Self {
567 self.use_internal_txs = use_internal_txs;
568 self
569 }
570
571 #[must_use]
575 pub fn with_tx_simulation_delay(mut self, tx_simulation_delay: Duration) -> Self {
576 self.tx_simulation_delay = tx_simulation_delay;
577 self
578 }
579
580 pub fn snapshot(&self) -> BlokliTestStateSnapshot {
584 let state = self.state.read();
585 BlokliTestStateSnapshot {
586 state: self.state.clone(),
587 snapshot: state.clone(),
588 }
589 }
590
591 pub fn hidden_state_update(&self, update: impl FnOnce(&mut BlokliTestState)) {
597 let mut state = self.state.write();
598 update(&mut state);
599 }
600
601 pub fn update_price_and_win_prob(&self, new_price: Option<TokenValueString>, new_win_prob: Option<f64>) {
606 let mut updated = false;
607 let (new_price_param, new_win_prob_param) = {
608 let mut state = self.state.write();
609
610 let mut new_price_param = state.chain_info.ticket_price.clone();
611 if let Some(new_price) = new_price {
612 state.chain_info.ticket_price = new_price.clone();
613
614 new_price_param = new_price;
615 updated = true;
616 }
617
618 let mut new_win_prob_param = state.chain_info.min_ticket_winning_probability;
619 if let Some(new_win_prob) = new_win_prob {
620 state.chain_info.min_ticket_winning_probability = new_win_prob;
621
622 new_win_prob_param = new_win_prob;
623 updated = true;
624 }
625 (new_price_param, new_win_prob_param)
626 };
627
628 if updated
629 && let Err(error) = self.ticket_channel.0.try_broadcast(TicketParameters {
630 min_ticket_winning_probability: new_win_prob_param,
631 ticket_price: new_price_param,
632 })
633 {
634 tracing::error!(%error, "failed to broadcast ticket parameters update");
635 }
636 }
637
638 fn do_query_channels(&self, selector: ChannelSelector) -> Result<Vec<Channel>> {
639 let state = self.state.read();
640 Ok(state
641 .channels
642 .values()
643 .filter(|c| channel_matches(c, &selector, &state.accounts))
644 .cloned()
645 .collect())
646 }
647
648 fn do_query_accounts(&self, selector: AccountSelector) -> Result<Vec<Account>> {
649 Ok(self
650 .state
651 .read()
652 .accounts
653 .values()
654 .filter(|a| account_matches(a, &selector))
655 .cloned()
656 .collect())
657 }
658
659 fn do_query_services(&self, selector: ServiceSelector) -> Result<Vec<ServiceEntry>> {
660 Ok(self
661 .state
662 .read()
663 .services
664 .values()
665 .filter(|entry| service_matches(&entry.service_type, &entry.node, &selector))
666 .cloned()
667 .collect())
668 }
669}
670
671#[async_trait::async_trait]
672impl<M: BlokliTestStateMutator + Send + Sync> BlokliQueryClient for BlokliTestClient<M> {
673 #[cfg(feature = "curvy")]
674 async fn query_curvy_pending_notes(
675 &self,
676 _from_block: Option<u64>,
677 _after: Option<CurvyEventCursor>,
678 _first: u32,
679 ) -> Result<CurvyPendingNotes> {
680 Ok(CurvyPendingNotes { notes: Vec::new() })
681 }
682
683 #[cfg(feature = "curvy")]
684 async fn query_curvy_committed_notes(
685 &self,
686 _from_block: Option<u64>,
687 _after: Option<CurvyEventCursor>,
688 _first: u32,
689 ) -> Result<CurvyCommittedNotes> {
690 Ok(CurvyCommittedNotes { notes: Vec::new() })
691 }
692
693 #[cfg(feature = "curvy")]
694 async fn query_curvy_committed_nullifiers(
695 &self,
696 _from_block: Option<u64>,
697 _after: Option<CurvyEventCursor>,
698 _first: u32,
699 ) -> Result<CurvyCommittedNullifiers> {
700 Ok(CurvyCommittedNullifiers { nullifiers: Vec::new() })
701 }
702
703 #[cfg(feature = "curvy")]
704 async fn query_curvy_sync_checkpoint(&self, _block_hash: Option<String>) -> Result<CurvySyncCheckpoint> {
705 Err(ErrorKind::NoData.into())
706 }
707
708 #[cfg(feature = "curvy")]
709 async fn query_curvy_sync_notes(
710 &self,
711 _checkpoint: String,
712 _from_index: Option<u64>,
713 _first: u32,
714 ) -> Result<CurvySyncNotePage> {
715 Err(ErrorKind::NoData.into())
716 }
717
718 #[cfg(feature = "curvy")]
719 async fn query_curvy_sync_nullifiers(
720 &self,
721 _checkpoint: String,
722 _from_index: Option<u64>,
723 _first: u32,
724 ) -> Result<CurvySyncNullifierPage> {
725 Err(ErrorKind::NoData.into())
726 }
727
728 #[cfg(feature = "curvy")]
729 async fn query_curvy_shard_roots(
730 &self,
731 _checkpoint: String,
732 _from_index: Option<u64>,
733 _first: u32,
734 ) -> Result<CurvyShardRootPage> {
735 Err(ErrorKind::NoData.into())
736 }
737
738 #[cfg(feature = "curvy")]
739 async fn query_curvy_aggregator_state(&self) -> Result<CurvyAggregatorState> {
740 Err(ErrorKind::NoData.into())
741 }
742
743 #[cfg(feature = "curvy")]
744 async fn query_curvy_note_status(&self, _note_id: String) -> Result<CurvyNoteStatus> {
745 Err(ErrorKind::NoData.into())
746 }
747
748 #[cfg(feature = "curvy")]
749 async fn query_curvy_valid_notes_root(&self, _root: String) -> Result<bool> {
750 Err(ErrorKind::NoData.into())
751 }
752
753 #[cfg(feature = "curvy")]
754 async fn query_curvy_nullifier_spent(&self, _nullifier: String) -> Result<bool> {
755 Err(ErrorKind::NoData.into())
756 }
757
758 #[cfg(feature = "curvy")]
759 async fn query_curvy_vault_fees(&self) -> Result<CurvyVaultFees> {
760 Err(ErrorKind::NoData.into())
761 }
762
763 #[cfg(feature = "curvy")]
764 async fn query_curvy_aggregator_fees(&self) -> Result<CurvyAggregatorFees> {
765 Err(ErrorKind::NoData.into())
766 }
767
768 #[cfg(feature = "curvy")]
769 async fn query_curvy_vault_token_count(&self) -> Result<CurvyVaultTokenCount> {
770 Err(ErrorKind::NoData.into())
771 }
772
773 #[cfg(feature = "curvy")]
774 async fn query_curvy_vault_token(&self, _token_id: String) -> Result<CurvyVaultToken> {
775 Err(ErrorKind::NoData.into())
776 }
777
778 #[cfg(feature = "curvy")]
779 async fn query_curvy_entry_portal_address(&self, _owner_hash: String, _recovery: String) -> Result<String> {
780 Err(ErrorKind::NoData.into())
781 }
782
783 #[cfg(feature = "curvy")]
784 async fn query_curvy_exit_portal_address(
785 &self,
786 _exit_address: String,
787 _exit_chain_id: String,
788 _recovery: String,
789 ) -> Result<String> {
790 Err(ErrorKind::NoData.into())
791 }
792
793 #[cfg(feature = "curvy")]
794 async fn query_curvy_portal_registered(&self, _portal_address: String) -> Result<bool> {
795 Err(ErrorKind::NoData.into())
796 }
797
798 async fn count_accounts(&self, selector: AccountSelector) -> Result<u32> {
799 Ok(match selector {
800 AccountSelector::Any => self.state.read().accounts.len() as u32,
801 selector => self.query_accounts(selector).await?.len() as u32,
802 })
803 }
804
805 async fn query_accounts(&self, selector: AccountSelector) -> Result<Vec<Account>> {
806 self.do_query_accounts(selector)
807 }
808
809 async fn query_native_balance(&self, address: &ChainAddress) -> Result<NativeBalance> {
810 let address = hex::encode(address);
811 self.state
812 .read()
813 .native_balances
814 .get(&address)
815 .cloned()
816 .ok_or_else(|| ErrorKind::NoData.into())
817 }
818
819 async fn query_token_balance(&self, address: &ChainAddress, _token: Token) -> Result<HoprBalance> {
820 let address = hex::encode(address);
821 self.state
822 .read()
823 .token_balances
824 .get(&address)
825 .cloned()
826 .ok_or_else(|| ErrorKind::NoData.into())
827 }
828
829 async fn query_transaction_count(&self, address: &ChainAddress) -> Result<u64> {
830 let address = hex::encode(address);
831 let state = self.state.upgradable_read();
832 if let Some(value) = state.tx_counts.get(&address) {
833 return Ok(*value);
834 }
835
836 let mut state = parking_lot::RwLockUpgradableReadGuard::upgrade(state);
837 Ok(*state.tx_counts.entry(address).or_default())
838 }
839
840 async fn query_safe_allowance(&self, address: &ChainAddress) -> Result<SafeHoprAllowance> {
841 let address = hex::encode(address);
842 self.state
843 .read()
844 .safe_allowances
845 .get(&address)
846 .cloned()
847 .ok_or_else(|| ErrorKind::NoData.into())
848 }
849
850 async fn query_redeemed_stats(&self, selector: RedeemedStatsSelector) -> Result<RedeemedStats> {
851 let state = self.state.upgradable_read();
852
853 let maybe_safe = match selector {
854 RedeemedStatsSelector::SafeAddress(addr) => Some(addr),
855 RedeemedStatsSelector::SafeAndNodeAddress { safe_address, .. } => Some(safe_address),
856 RedeemedStatsSelector::NodeAddress(_) => None,
857 };
858
859 if let Some(safe_address) = maybe_safe {
860 let safe_address_hex = hex::encode(safe_address);
861 if !state.deployed_safes.contains_key(&safe_address_hex) {
862 return Err(ErrorKind::NoData.into());
863 }
864
865 if let Some(v) = state.safe_redeem_stats.get(&safe_address_hex) {
866 Ok(v.clone())
867 } else {
868 let mut state = parking_lot::RwLockUpgradableReadGuard::upgrade(state);
869 let stats = RedeemedStats {
870 __typename: "RedeemedStats".to_string(),
871 redeemed_amount: TokenValueString("0 wxHOPR".into()),
872 redemption_count: Uint64("0".into()),
873 rejected_amount: TokenValueString("0 wxHOPR".into()),
874 rejection_count: Uint64("0".into()),
875 };
876 state.safe_redeem_stats.insert(safe_address_hex, stats.clone());
877 Ok(stats)
878 }
879 } else {
880 Err(ErrorKind::NoData.into())
881 }
882 }
883
884 async fn query_safe(&self, selector: SafeSelector) -> Result<Vec<Safe>> {
885 let state = self.state.read();
886 match selector {
887 SafeSelector::SafeAddress(addr) => Ok(state
888 .deployed_safes
889 .get(&hex::encode(addr))
890 .cloned()
891 .into_iter()
892 .collect()),
893 SafeSelector::Owner(owner_address) | SafeSelector::ChainKey(owner_address) => Ok(state
894 .deployed_safes
895 .values()
896 .filter(|s| BlokliTestState::safe_matches_owner(s, &hex::encode(owner_address)))
897 .cloned()
898 .collect()),
899 SafeSelector::RegisteredNode(node_address) => Ok(state
900 .deployed_safes
901 .values()
902 .filter(|s| s.registered_nodes.contains(&hex::encode(node_address)))
903 .cloned()
904 .collect()),
905 }
906 }
907
908 async fn query_module_address_prediction(&self, input: ModulePredictionInput) -> Result<ChainAddress> {
909 let hash = Hash::create(&[
910 input.nonce.to_be_bytes().as_ref(),
911 input.owner.as_ref(),
912 input.safe_address.as_ref(),
913 ]);
914
915 hash.as_ref()[0..20].try_into().map_err(|_| ErrorKind::NoData.into())
916 }
917
918 async fn count_channels(&self, selector: ChannelSelector) -> Result<u32> {
919 Ok(if selector.matches_all() {
920 self.state.read().channels.len() as u32
921 } else {
922 self.query_channels(selector).await?.channels.len() as u32
923 })
924 }
925
926 async fn query_channel_stats(&self, selector: ChannelSelector) -> Result<ChannelStats> {
927 let channels = self.do_query_channels(selector)?;
928 let count = i32::try_from(channels.len()).map_err(|_| ErrorKind::ParseError)?;
929 let mut total = PrimitiveHoprBalance::zero();
930 for ch in &channels {
931 let bal: PrimitiveHoprBalance = ch.balance.0.parse().map_err(|_| ErrorKind::ParseError)?;
932 total += bal;
933 }
934 Ok(ChannelStats {
935 count,
936 balance: TokenValueString(total.to_string()),
937 })
938 }
939
940 async fn query_channels(&self, selector: ChannelSelector) -> Result<ChannelsList> {
941 let channels = self.do_query_channels(selector)?;
942 Ok(ChannelsList {
943 __typename: "ChannelsList".to_string(),
944 channels,
945 })
946 }
947
948 async fn query_safes_balance(&self, owner_address: Option<ChainAddress>) -> Result<SafesBalance> {
949 let state = self.state.read();
950 let matching_safes: Vec<&Safe> = if let Some(owner) = owner_address {
951 let owner_hex = hex::encode(owner);
952 state
953 .deployed_safes
954 .values()
955 .filter(|s| BlokliTestState::safe_matches_owner(s, &owner_hex))
956 .collect()
957 } else {
958 state.deployed_safes.values().collect()
959 };
960
961 let count = i32::try_from(matching_safes.len()).map_err(|_| ErrorKind::ParseError)?;
962 let mut total = PrimitiveHoprBalance::zero();
963 for safe in &matching_safes {
964 if let Some(hopr_balance) = state.token_balances.get(&safe.address) {
965 let bal: PrimitiveHoprBalance = hopr_balance.balance.0.parse().map_err(|_| ErrorKind::ParseError)?;
966 total += bal;
967 }
968 }
969
970 Ok(SafesBalance {
971 count,
972 balance: TokenValueString(total.to_string()),
973 })
974 }
975
976 async fn count_services(&self, selector: ServiceSelector) -> Result<u32> {
977 Ok(match selector {
978 ServiceSelector::Any => self.state.read().services.len() as u32,
979 selector => self.do_query_services(selector)?.len() as u32,
980 })
981 }
982
983 async fn query_services(&self, selector: ServiceSelector) -> Result<Vec<ServiceEntry>> {
984 self.do_query_services(selector)
985 }
986
987 async fn query_live_services(&self, selector: ServiceSelector) -> Result<Vec<ServiceEntry>> {
988 let entries = self.query_services(selector).await?;
989 let state = self.state.read();
990 Ok(entries
991 .into_iter()
992 .filter(|entry| {
993 state
994 .deployed_safes
995 .values()
996 .any(|safe| safe.registered_nodes.iter().any(|node| node == &entry.node))
997 })
998 .collect())
999 }
1000
1001 async fn query_service_types(&self, service_type: Option<ServiceTypeId>) -> Result<Vec<ServiceTypeInfo>> {
1002 Ok(self
1003 .state
1004 .read()
1005 .service_types
1006 .values()
1007 .filter(|info| service_type.is_none_or(|wanted| service_type_matches(&info.service_type, &wanted)))
1008 .cloned()
1009 .collect())
1010 }
1011
1012 async fn query_service_registry_config(&self) -> Result<ServiceRegistryConfig> {
1013 Ok(self.state.read().service_registry_config.clone())
1014 }
1015
1016 async fn query_transaction_status(&self, tx_id: TxId) -> Result<Transaction> {
1017 self.state
1018 .read()
1019 .active_txs
1020 .get(&tx_id)
1021 .cloned()
1022 .ok_or_else(|| ErrorKind::NoData.into())
1023 }
1024
1025 async fn query_chain_info(&self) -> Result<ChainInfo> {
1026 Ok(self.state.read().chain_info.clone())
1027 }
1028
1029 async fn query_version(&self) -> Result<String> {
1030 Ok(self.state.read().version.clone())
1031 }
1032
1033 async fn query_health(&self) -> Result<String> {
1034 Ok(self.state.read().health.clone())
1035 }
1036
1037 async fn query_compatibility(&self) -> Result<Compatibility> {
1038 Ok(Compatibility {
1039 api_version: env!("CARGO_PKG_VERSION").to_string(),
1040 supported_client_versions: "*".to_string(),
1041 features: vec![],
1042 })
1043 }
1044}
1045
1046impl<M: BlokliTestStateMutator + Send + Sync> BlokliSubscriptionClient for BlokliTestClient<M> {
1047 fn subscribe_channels(&self, selector: ChannelSelector) -> Result<impl Stream<Item = Result<Channel>> + Send> {
1048 Ok(if selector.matches_all() {
1049 let channels = self.state.read().channels.clone();
1050 futures::stream::iter(channels.into_values())
1051 .map(Ok)
1052 .chain(
1053 self.channels_channel
1054 .1
1055 .activate_cloned()
1056 .map(|(_, channel, _)| Ok(channel)),
1057 )
1058 .boxed()
1059 } else {
1060 let accounts = self.state.read().accounts.clone();
1061 futures::stream::iter(self.do_query_channels(selector.clone())?)
1062 .map(Ok)
1063 .chain(
1064 self.channels_channel
1065 .1
1066 .activate_cloned()
1067 .filter(move |(_, c, _)| futures::future::ready(channel_matches(c, &selector, &accounts)))
1068 .map(|(_, channel, _)| Ok(channel)),
1069 )
1070 .boxed()
1071 })
1072 }
1073
1074 fn subscribe_accounts(&self, selector: AccountSelector) -> Result<impl Stream<Item = Result<Account>> + Send> {
1075 Ok(match selector {
1076 AccountSelector::Any => {
1077 let accounts = self.state.read().accounts.clone();
1078 futures::stream::iter(accounts.into_values())
1079 .map(Ok)
1080 .chain(self.accounts_channel.1.activate_cloned().map(Ok))
1081 .boxed()
1082 }
1083 selector => futures::stream::iter(self.do_query_accounts(selector.clone())?)
1084 .map(Ok)
1085 .chain(
1086 self.accounts_channel
1087 .1
1088 .activate_cloned()
1089 .filter(move |a| futures::future::ready(account_matches(a, &selector)))
1090 .map(Ok),
1091 )
1092 .boxed(),
1093 })
1094 }
1095
1096 fn subscribe_graph(&self) -> Result<impl Stream<Item = Result<OpenedChannelsGraphEntry>> + Send> {
1097 let (accounts, channels) = {
1098 let state = self.state.read();
1099 (state.accounts.clone(), state.channels.clone())
1100 };
1101
1102 Ok(futures::stream::iter(channels.into_values().map(move |channel| {
1103 let source = accounts
1104 .get(&(channel.source as u32))
1105 .cloned()
1106 .ok_or_else(|| BlokliClientError::from(ErrorKind::NoData))?;
1107 let destination = accounts
1108 .get(&(channel.destination as u32))
1109 .cloned()
1110 .ok_or_else(|| BlokliClientError::from(ErrorKind::NoData))?;
1111
1112 Ok::<_, BlokliClientError>(OpenedChannelsGraphEntry {
1113 channel,
1114 destination,
1115 source,
1116 })
1117 }))
1118 .chain(
1119 self.channels_channel
1120 .1
1121 .activate_cloned()
1122 .map(|(source, channel, destination)| {
1123 Ok(OpenedChannelsGraphEntry {
1124 channel,
1125 destination,
1126 source,
1127 })
1128 }),
1129 ))
1130 }
1131
1132 fn subscribe_ticket_params(&self) -> Result<impl Stream<Item = Result<TicketParameters>> + Send> {
1133 let info = self.state.read().chain_info.clone();
1134 Ok(futures::stream::once(futures::future::ready(TicketParameters {
1135 min_ticket_winning_probability: info.min_ticket_winning_probability,
1136 ticket_price: info.ticket_price,
1137 }))
1138 .chain(self.ticket_channel.1.activate_cloned())
1139 .map(Ok))
1140 }
1141
1142 fn subscribe_health(&self) -> Result<impl Stream<Item = Result<ReadinessState>> + Send> {
1143 Ok(futures::stream::once(futures::future::ready(Ok(ReadinessState::Ready))).boxed())
1144 }
1145
1146 fn subscribe_safe_deployments(&self) -> Result<impl Stream<Item = Result<Safe>> + Send> {
1147 let safes = self.state.read().deployed_safes.clone();
1148 Ok(futures::stream::iter(safes.into_values())
1149 .chain(self.safe_deployed_channel.1.activate_cloned())
1150 .map(Ok))
1151 }
1152
1153 fn subscribe_services(
1155 &self,
1156 selector: ServiceSelector,
1157 ) -> Result<impl Stream<Item = Result<ServiceUpdate>> + Send> {
1158 let (initial, updates) = {
1159 let state = self.state.read();
1160 let initial = state
1161 .services
1162 .values()
1163 .filter(|entry| service_matches(&entry.service_type, &entry.node, &selector))
1164 .cloned()
1165 .map(|entry| ServiceUpdate {
1166 kind: ServiceUpdateKind::Registered,
1167 service_type: entry.service_type.clone(),
1168 node: entry.node.clone(),
1169 entry: Some(entry),
1170 })
1171 .collect::<Vec<_>>();
1172 (initial, self.services_channel.1.activate_cloned())
1173 };
1174 Ok(futures::stream::iter(initial)
1175 .chain(updates)
1176 .filter(move |update| {
1177 futures::future::ready(service_matches(&update.service_type, &update.node, &selector))
1178 })
1179 .map(Ok))
1180 }
1181
1182 fn subscribe_service_types(
1188 &self,
1189 service_type: Option<ServiceTypeId>,
1190 ) -> Result<impl Stream<Item = Result<ServiceTypeUpdate>> + Send> {
1191 let (initial, updates) = {
1192 let state = self.state.read();
1193 let initial = state
1194 .service_types
1195 .values()
1196 .filter(|config| service_type.is_none_or(|wanted| service_type_matches(&config.service_type, &wanted)))
1197 .cloned()
1198 .map(|config| ServiceTypeUpdate {
1199 kind: ServiceTypeUpdateKind::Registered,
1200 service_type: Some(config.service_type.clone()),
1201 config: Some(config),
1202 registry_config: None,
1203 })
1204 .collect::<Vec<_>>();
1205 (initial, self.service_types_channel.1.activate_cloned())
1206 };
1207 Ok(futures::stream::iter(initial)
1208 .chain(updates)
1209 .filter(move |update| {
1210 futures::future::ready(service_type.is_none_or(|wanted| {
1211 update
1212 .service_type
1213 .as_deref()
1214 .is_some_and(|rendered| service_type_matches(rendered, &wanted))
1215 }))
1216 })
1217 .map(Ok))
1218 }
1219
1220 fn subscribe_service_registry_config(
1221 &self,
1222 ) -> Result<impl Stream<Item = Result<ServiceRegistryConfig>> + Send + 'static> {
1223 let (initial, updates) = {
1227 let state = self.state.read();
1228 (
1229 state.service_registry_config.clone(),
1230 self.service_registry_config_channel.1.activate_cloned(),
1231 )
1232 };
1233 Ok(futures::stream::once(futures::future::ready(initial))
1234 .chain(updates)
1235 .map(Ok))
1236 }
1237
1238 fn subscribe_track_transaction(
1239 &self,
1240 tx_id: TxId,
1241 ) -> Result<impl futures::Stream<Item = Result<types::Transaction>> + Send> {
1242 let tx = self
1243 .state
1244 .write()
1245 .active_txs
1246 .shift_remove(&tx_id)
1247 .ok_or_else(|| BlokliClientError::from(ErrorKind::NoData))?;
1248
1249 Ok(futures::stream::once(futures::future::ok(tx)).delay(Duration2::from(self.tx_simulation_delay)))
1250 }
1251
1252 fn subscribe_ticket_redeemed(
1253 &self,
1254 _selector: TicketSelector,
1255 ) -> Result<impl futures::Stream<Item = Result<RedeemTicketDetails>> + Send> {
1256 Ok(futures::stream::empty())
1257 }
1258
1259 #[cfg(feature = "curvy")]
1260 fn subscribe_curvy_pending_notes(
1261 &self,
1262 _from_block: Option<u64>,
1263 ) -> Result<impl futures::Stream<Item = Result<CurvyPendingNote>> + Send> {
1264 Ok(futures::stream::empty())
1265 }
1266
1267 #[cfg(feature = "curvy")]
1268 fn subscribe_curvy_committed_notes(
1269 &self,
1270 _from_block: Option<u64>,
1271 ) -> Result<impl futures::Stream<Item = Result<CurvyCommittedNote>> + Send> {
1272 Ok(futures::stream::empty())
1273 }
1274
1275 #[cfg(feature = "curvy")]
1276 fn subscribe_curvy_committed_nullifiers(
1277 &self,
1278 _from_block: Option<u64>,
1279 ) -> Result<impl futures::Stream<Item = Result<CurvyCommittedNullifier>> + Send> {
1280 Ok(futures::stream::empty())
1281 }
1282}
1283
1284struct SubscriptionSenders<'a> {
1286 accounts: &'a async_broadcast::Sender<Account>,
1287 channels: &'a async_broadcast::Sender<(Account, Channel, Account)>,
1288 tickets: &'a async_broadcast::Sender<TicketParameters>,
1289 safes: &'a async_broadcast::Sender<Safe>,
1290 services: &'a async_broadcast::Sender<ServiceUpdate>,
1291 service_types: &'a async_broadcast::Sender<ServiceTypeUpdate>,
1292 service_registry_config: &'a async_broadcast::Sender<ServiceRegistryConfig>,
1293}
1294
1295impl<M: BlokliTestStateMutator> BlokliTestClient<M> {
1296 fn subscription_senders(&self) -> SubscriptionSenders<'_> {
1297 SubscriptionSenders {
1298 accounts: &self.accounts_channel.0,
1299 channels: &self.channels_channel.0,
1300 tickets: &self.ticket_channel.0,
1301 safes: &self.safe_deployed_channel.0,
1302 services: &self.services_channel.0,
1303 service_types: &self.service_types_channel.0,
1304 service_registry_config: &self.service_registry_config_channel.0,
1305 }
1306 }
1307}
1308
1309fn broadcast_service_changes(
1314 old_state: &BlokliTestState,
1315 state: &BlokliTestState,
1316 services_channel: &async_broadcast::Sender<ServiceUpdate>,
1317) {
1318 for (key, old_entry) in &old_state.services {
1319 if !state.services.contains_key(key) {
1320 broadcast_or_log(
1321 services_channel,
1322 ServiceUpdate {
1323 kind: ServiceUpdateKind::Deregistered,
1324 service_type: old_entry.service_type.clone(),
1325 node: old_entry.node.clone(),
1326 entry: None,
1327 },
1328 "service deregistration",
1329 );
1330 }
1331 }
1332
1333 for (key, new_entry) in &state.services {
1334 let kind = match old_state.services.get(key) {
1335 None => ServiceUpdateKind::Registered,
1336 Some(old_entry) if old_entry != new_entry => ServiceUpdateKind::Updated,
1337 Some(_) => continue,
1338 };
1339
1340 broadcast_or_log(
1341 services_channel,
1342 ServiceUpdate {
1343 kind,
1344 service_type: new_entry.service_type.clone(),
1345 node: new_entry.node.clone(),
1346 entry: Some(new_entry.clone()),
1347 },
1348 "service entry change",
1349 );
1350 }
1351}
1352
1353fn broadcast_service_type_changes(
1358 old_state: &BlokliTestState,
1359 state: &BlokliTestState,
1360 service_types_channel: &async_broadcast::Sender<ServiceTypeUpdate>,
1361) {
1362 for (key, new_type) in &state.service_types {
1363 let kinds: Vec<ServiceTypeUpdateKind> = match old_state.service_types.get(key) {
1364 None => vec![ServiceTypeUpdateKind::Registered],
1365 Some(old_type) => [
1366 (old_type.owner != new_type.owner).then_some(ServiceTypeUpdateKind::OwnerChanged),
1367 (old_type.requirement != new_type.requirement).then_some(ServiceTypeUpdateKind::RequirementChanged),
1368 (old_type.registration_burn != new_type.registration_burn)
1369 .then_some(ServiceTypeUpdateKind::RegistrationBurnChanged),
1370 (old_type.update_burn != new_type.update_burn).then_some(ServiceTypeUpdateKind::UpdateBurnChanged),
1371 ]
1372 .into_iter()
1373 .flatten()
1374 .collect(),
1375 };
1376
1377 for kind in kinds {
1378 broadcast_or_log(
1379 service_types_channel,
1380 ServiceTypeUpdate {
1381 kind,
1382 service_type: Some(new_type.service_type.clone()),
1383 config: Some(new_type.clone()),
1384 registry_config: None,
1385 },
1386 "service type change",
1387 );
1388 }
1389 }
1390}
1391
1392fn simulate_tx_execution(
1393 signed_tx: &[u8],
1394 state: &mut BlokliTestState,
1395 mutator: &dyn BlokliTestStateMutator,
1396 senders: SubscriptionSenders<'_>,
1397) -> Result<()> {
1398 let old_state = state.clone();
1399 if let Err(error) = mutator.update_state(signed_tx, state) {
1400 *state = old_state;
1401 return Err(error);
1402 }
1403
1404 if old_state.accounts.len() > state.accounts.len() {
1405 *state = old_state;
1406 return Err(ErrorKind::MockClientError(anyhow::anyhow!("mutation cannot remove accounts")).into());
1407 }
1408
1409 if old_state.channels.len() > state.channels.len() {
1410 *state = old_state;
1411 return Err(ErrorKind::MockClientError(anyhow::anyhow!("mutation cannot remove channels")).into());
1412 }
1413
1414 if old_state.native_balances.len() > state.native_balances.len() {
1415 *state = old_state;
1416 return Err(ErrorKind::MockClientError(anyhow::anyhow!("mutation cannot remove native balances")).into());
1417 }
1418
1419 if old_state.token_balances.len() > state.token_balances.len() {
1420 *state = old_state;
1421 return Err(ErrorKind::MockClientError(anyhow::anyhow!("mutation cannot remove token balances")).into());
1422 }
1423
1424 if old_state.safe_allowances.len() > state.safe_allowances.len() {
1425 *state = old_state;
1426 return Err(ErrorKind::MockClientError(anyhow::anyhow!("mutation cannot remove safe allowances")).into());
1427 }
1428
1429 if old_state.active_txs.len() > state.active_txs.len() {
1430 *state = old_state;
1431 return Err(ErrorKind::MockClientError(anyhow::anyhow!("mutation cannot remove active txs")).into());
1432 }
1433
1434 if old_state.service_types.len() > state.service_types.len() {
1437 *state = old_state;
1438 return Err(ErrorKind::MockClientError(anyhow::anyhow!("mutation cannot remove service types")).into());
1439 }
1440
1441 state
1443 .accounts
1444 .iter()
1445 .filter(|&(new_id, new_account)| {
1446 old_state.accounts.get(new_id).is_none_or(|old_account| {
1447 old_account.safe_address != new_account.safe_address
1449 || old_account.multi_addresses != new_account.multi_addresses
1450 })
1451 })
1452 .for_each(
1453 |(_, changed_account)| match senders.accounts.try_broadcast(changed_account.clone()) {
1454 Err(TrySendError::Full(_)) => {
1455 tracing::error!("failed to broadcast account change - channel is full");
1456 }
1457 Err(TrySendError::Closed(_)) => {
1458 tracing::error!("failed to broadcast account change - channel is closed");
1459 }
1460 _ => {}
1461 },
1462 );
1463
1464 state
1466 .channels
1467 .iter()
1468 .filter(|&(new_id, new_channel)| {
1469 old_state
1470 .channels
1471 .get(new_id)
1472 .is_none_or(|old_channel| old_channel != new_channel)
1473 })
1474 .filter_map(|(_, changed_channel)| {
1475 let source = state.accounts.get(&(changed_channel.source as u32)).cloned();
1476 let destination = state.accounts.get(&(changed_channel.destination as u32)).cloned();
1477 source
1478 .zip(destination)
1479 .map(|(source, destination)| (source, changed_channel.clone(), destination))
1480 })
1481 .for_each(|(source, changed_channel, destination)| {
1482 match senders.channels.try_broadcast((source, changed_channel, destination)) {
1483 Err(TrySendError::Full(_)) => {
1484 tracing::error!("failed to broadcast channel change - channel is full");
1485 }
1486 Err(TrySendError::Closed(_)) => {
1487 tracing::error!("failed to broadcast channel change - channel is closed");
1488 }
1489 _ => {}
1490 }
1491 });
1492
1493 if state.chain_info.min_ticket_winning_probability != old_state.chain_info.min_ticket_winning_probability
1494 || state.chain_info.ticket_price != old_state.chain_info.ticket_price
1495 {
1496 match senders.tickets.try_broadcast(TicketParameters {
1497 min_ticket_winning_probability: state.chain_info.min_ticket_winning_probability,
1498 ticket_price: state.chain_info.ticket_price.clone(),
1499 }) {
1500 Err(TrySendError::Full(_)) => {
1501 tracing::error!("failed to broadcast ticket params change - channel is full");
1502 }
1503 Err(TrySendError::Closed(_)) => {
1504 tracing::error!("failed to broadcast ticket params change - channel is closed");
1505 }
1506 _ => {}
1507 }
1508 }
1509
1510 state
1512 .deployed_safes
1513 .iter()
1514 .filter(|&(new_id, new_safe)| {
1515 old_state
1516 .deployed_safes
1517 .get(new_id)
1518 .is_none_or(|old_safe| old_safe != new_safe)
1519 })
1520 .for_each(
1521 |(_, changed_safe)| match senders.safes.try_broadcast(changed_safe.clone()) {
1522 Err(TrySendError::Full(_)) => {
1523 tracing::error!("failed to broadcast safe change - channel is full");
1524 }
1525 Err(TrySendError::Closed(_)) => {
1526 tracing::error!("failed to broadcast safe change - channel is closed");
1527 }
1528 _ => {}
1529 },
1530 );
1531
1532 broadcast_service_changes(&old_state, state, senders.services);
1533 broadcast_service_type_changes(&old_state, state, senders.service_types);
1534 if old_state.service_registry_config != state.service_registry_config {
1535 broadcast_or_log(
1536 senders.service_registry_config,
1537 state.service_registry_config.clone(),
1538 "service registry configuration change",
1539 );
1540 }
1541
1542 Ok(())
1543}
1544
1545#[async_trait::async_trait]
1546impl<M: BlokliTestStateMutator + Send + Sync> BlokliTransactionClient for BlokliTestClient<M> {
1547 async fn submit_transaction(&self, signed_tx: &[u8]) -> Result<TxReceipt> {
1548 let mut tx_receipt = [0u8; 32];
1549 rand::fill(&mut tx_receipt);
1550
1551 let mut state = self.state.write();
1552 if let Err(error) = simulate_tx_execution(signed_tx, &mut state, &self.mutator, self.subscription_senders()) {
1553 tracing::error!(%error, signed_tx_data = hex::encode(signed_tx), "failed to execute transaction, state reverted");
1554 } else {
1555 tracing::debug!("transaction execution succeeded");
1556 }
1557
1558 Ok(tx_receipt)
1559 }
1560
1561 async fn submit_and_track_transaction(&self, signed_tx: &[u8]) -> Result<TxId> {
1562 let tx_id = hex::encode(rand::random_iter::<u8>().take(16).collect::<Vec<_>>());
1563 let tx_hash = hex::encode(rand::random_iter::<u8>().take(32).collect::<Vec<_>>());
1564
1565 let mut state = self.state.write();
1566
1567 let mut internal_tx_failure_reason: Option<String> = None;
1568
1569 let status = simulate_tx_execution(
1570 signed_tx,
1571 &mut state,
1572 &self.mutator,
1573 self.subscription_senders(),
1574 )
1575 .map(|_| {
1576 tracing::debug!("transaction execution succeeded");
1577 TransactionStatus::Confirmed
1578 })
1579 .inspect_err(|e| if let ErrorKind::MockClientError(int_err) = e.kind() {
1580 internal_tx_failure_reason = int_err.downcast_ref::<InternalTxError>().map(|err| err.0.to_string());
1581 })
1582 .unwrap_or_else(|error| {
1583 tracing::error!(%error, signed_tx_data = hex::encode(signed_tx), "failed to execute transaction, state reverted");
1584 if self.use_internal_txs && internal_tx_failure_reason.is_some() {
1586 TransactionStatus::Confirmed
1587 } else {
1588 TransactionStatus::Reverted
1589 }
1590 });
1591
1592 state.active_txs.insert(
1593 tx_id.clone(),
1594 Transaction {
1595 id: tx_id.clone().into(),
1596 status,
1597 submitted_at: DateTime(chrono::DateTime::<chrono::Utc>::from(SystemTime::now()).to_rfc3339()),
1598 transaction_hash: Hex32(tx_hash.clone()),
1599 safe_execution: (self.use_internal_txs && status == TransactionStatus::Confirmed).then(|| {
1600 SafeExecution {
1601 success: internal_tx_failure_reason.is_none(),
1602 safe_tx_hash: Some(Hex32(tx_hash)),
1603 revert_reason: internal_tx_failure_reason,
1604 }
1605 }),
1606 },
1607 );
1608
1609 Ok(tx_id)
1610 }
1611
1612 async fn submit_and_confirm_transaction(&self, signed_tx: &[u8], num_confirmations: usize) -> Result<TxReceipt> {
1613 futures_time::task::sleep((self.tx_simulation_delay * num_confirmations as u32).into()).await;
1614
1615 let mut tx_receipt = [0u8; 32];
1616 rand::fill(&mut tx_receipt);
1617
1618 let mut state = self.state.write();
1619 simulate_tx_execution(
1620 signed_tx,
1621 &mut state,
1622 &self.mutator,
1623 self.subscription_senders(),
1624 )
1625 .inspect_err(|error| {
1626 tracing::error!(%error, signed_tx_data = hex::encode(signed_tx), "failed to execute transaction, state reverted");
1627 })?;
1628
1629 tracing::debug!("transaction execution succeeded");
1630 Ok(tx_receipt)
1631 }
1632
1633 async fn track_transaction(&self, tx_id: TxId, client_timeout: Duration) -> Result<Transaction> {
1634 futures_time::task::sleep(self.tx_simulation_delay.min(client_timeout.div(2)).into()).await;
1635 let tx = self
1636 .state
1637 .write()
1638 .active_txs
1639 .shift_remove(&tx_id)
1640 .ok_or_else(|| BlokliClientError::from(ErrorKind::NoData))?;
1641
1642 match tx.status {
1643 TransactionStatus::Confirmed => Ok(tx),
1644 TransactionStatus::Timeout => Err(ErrorKind::TrackingError(TrackingErrorKind::Timeout).into()),
1645 TransactionStatus::SubmissionFailed => {
1646 Err(ErrorKind::TrackingError(TrackingErrorKind::SubmissionFailed).into())
1647 }
1648 TransactionStatus::ValidationFailed => {
1649 Err(ErrorKind::TrackingError(TrackingErrorKind::ValidationFailed).into())
1650 }
1651 TransactionStatus::Reverted => Err(ErrorKind::TrackingError(TrackingErrorKind::Reverted).into()),
1652 _ => Err(ErrorKind::MockClientError(anyhow::anyhow!("unexpected transaction status")).into()),
1653 }
1654 }
1655}
1656
1657#[cfg(test)]
1658mod tests {
1659 use std::collections::BTreeMap;
1660
1661 use futures::StreamExt;
1662
1663 use super::{
1664 BlokliQueryClient, BlokliSubscriptionClient, BlokliTestClient, BlokliTestState, BlokliTransactionClient,
1665 ChainAddress, NopStateMutator, Result, ServiceEntry, ServiceRegistryConfig, ServiceSelector, ServiceTypeInfo,
1666 ServiceTypeUpdateKind, ServiceUpdateKind, Uint64,
1667 };
1668
1669 const GVPN_EXIT: [u8; 32] = [
1671 0x67, 0x76, 0x70, 0x6e, 0x3a, 0x65, 0x78, 0x69, 0x74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1672 0, 0, 0, 0,
1673 ];
1674 const NODE: ChainAddress = [0x11; 20];
1675 const OTHER_NODE: ChainAddress = [0x22; 20];
1676
1677 fn entry(service_type: &str, node: &ChainAddress) -> ServiceEntry {
1678 ServiceEntry {
1679 service_type: service_type.to_string(),
1680 node: hex::encode(node),
1681 safe: hex::encode([0x33; 20]),
1682 metadata: "0xdeadbeef".to_string(),
1683 registered_at: Uint64("1700000000".into()),
1684 updated_at: Uint64("1700000000".into()),
1685 }
1686 }
1687
1688 fn service_type_info(service_type: &str, owner: Option<&str>) -> ServiceTypeInfo {
1689 ServiceTypeInfo {
1690 service_type: service_type.to_string(),
1691 owner: owner.map(str::to_string),
1692 requirement: None,
1693 registration_burn: "1 wxHOPR".to_string(),
1694 update_burn: "0 wxHOPR".to_string(),
1695 }
1696 }
1697
1698 fn state_with_entries() -> BlokliTestState {
1699 let mut state = BlokliTestState::default();
1700 state.services.insert(
1701 BlokliTestState::service_entry_key("gvpn:exit", &NODE),
1702 entry("gvpn:exit", &NODE),
1703 );
1704 state.services.insert(
1705 BlokliTestState::service_entry_key("gvpn:relay", &OTHER_NODE),
1706 entry("gvpn:relay", &OTHER_NODE),
1707 );
1708 state
1709 }
1710
1711 #[test]
1714 fn default_contract_addresses_include_the_service_registry() -> anyhow::Result<()> {
1715 let blob = BlokliTestState::default().chain_info.contract_addresses.0;
1716 let addresses: BTreeMap<String, String> = serde_json::from_str(&blob)?;
1717
1718 let required = [
1726 "announcements",
1727 "channels",
1728 "module_implementation",
1729 "node_safe_migration",
1730 "node_safe_registry",
1731 "node_stake_factory",
1732 "service_registry",
1733 "ticket_price_oracle",
1734 "token",
1735 "winning_probability_oracle",
1736 "xhopr_token",
1737 ];
1738
1739 let missing: Vec<&str> = required
1740 .into_iter()
1741 .filter(|key| !addresses.contains_key(*key))
1742 .collect();
1743
1744 assert_eq!(Vec::<&str>::new(), missing);
1745 insta::assert_yaml_snapshot!(addresses);
1746
1747 Ok(())
1748 }
1749
1750 #[tokio::test]
1751 async fn query_services_accepts_the_any_selector() -> anyhow::Result<()> {
1752 let client = BlokliTestClient::new(state_with_entries(), NopStateMutator);
1753
1754 assert_eq!(client.query_services(ServiceSelector::Any).await?.len(), 2);
1755 Ok(())
1756 }
1757
1758 #[tokio::test]
1759 async fn count_services_accepts_the_any_selector() -> anyhow::Result<()> {
1760 let client = BlokliTestClient::new(state_with_entries(), NopStateMutator);
1761
1762 assert_eq!(client.count_services(ServiceSelector::Any).await?, 2);
1763 Ok(())
1764 }
1765
1766 #[tokio::test]
1767 async fn query_services_matches_a_service_type_written_as_its_ascii_name() -> anyhow::Result<()> {
1768 let client = BlokliTestClient::new(state_with_entries(), NopStateMutator);
1769
1770 let entries = client.query_services(ServiceSelector::ServiceType(GVPN_EXIT)).await?;
1771
1772 insta::assert_yaml_snapshot!(entries);
1773 Ok(())
1774 }
1775
1776 #[tokio::test]
1777 async fn query_services_matches_a_service_type_written_as_hex() -> anyhow::Result<()> {
1778 let mut state = BlokliTestState::default();
1779 let hex_id = format!("0x{}", hex::encode(GVPN_EXIT));
1780 state.services.insert(
1781 BlokliTestState::service_entry_key(&hex_id, &NODE),
1782 entry(&hex_id, &NODE),
1783 );
1784 let client = BlokliTestClient::new(state, NopStateMutator);
1785
1786 let entries = client.query_services(ServiceSelector::ServiceType(GVPN_EXIT)).await?;
1787
1788 assert_eq!(entries.len(), 1);
1789 Ok(())
1790 }
1791
1792 #[tokio::test]
1793 async fn query_services_narrows_to_one_node() -> anyhow::Result<()> {
1794 let client = BlokliTestClient::new(state_with_entries(), NopStateMutator);
1795
1796 let entries = client.query_services(ServiceSelector::Node(OTHER_NODE)).await?;
1797
1798 insta::assert_yaml_snapshot!(entries);
1799 Ok(())
1800 }
1801
1802 #[tokio::test]
1803 async fn query_service_types_returns_every_type_when_unfiltered() -> anyhow::Result<()> {
1804 let mut state = BlokliTestState::default();
1805 state
1806 .service_types
1807 .insert("gvpn:exit".to_string(), service_type_info("gvpn:exit", Some("0x4444")));
1808 state
1809 .service_types
1810 .insert("gvpn:relay".to_string(), service_type_info("gvpn:relay", None));
1811 let client = BlokliTestClient::new(state, NopStateMutator);
1812
1813 let types = client.query_service_types(None).await?;
1814
1815 insta::assert_yaml_snapshot!(types);
1816 Ok(())
1817 }
1818
1819 #[tokio::test]
1820 async fn query_service_registry_config_returns_current_configuration() -> anyhow::Result<()> {
1821 let mut state = BlokliTestState::default();
1822 state.service_registry_config = ServiceRegistryConfig {
1823 type_registration_fee: "1000 wxHOPR".into(),
1824 node_safe_registry: "0x4444444444444444444444444444444444444444".into(),
1825 };
1826 let client = BlokliTestClient::new(state, NopStateMutator);
1827
1828 let config = client.query_service_registry_config().await?;
1829
1830 insta::assert_yaml_snapshot!(config);
1831 Ok(())
1832 }
1833
1834 #[tokio::test]
1835 async fn subscribe_services_reports_registration_update_and_deregistration() -> anyhow::Result<()> {
1836 let client = BlokliTestClient::new(
1837 BlokliTestState::default(),
1838 |signed_tx: &[u8], state: &mut BlokliTestState| {
1839 let key = BlokliTestState::service_entry_key("gvpn:exit", &NODE);
1840 match signed_tx {
1841 [0] => {
1842 state.services.insert(key, entry("gvpn:exit", &NODE));
1843 }
1844 [1] => {
1845 let mut updated = entry("gvpn:exit", &NODE);
1846 updated.metadata = "0xc0ffee".to_string();
1847 updated.updated_at = Uint64("1700000100".into());
1848 state.services.insert(key, updated);
1849 }
1850 _ => {
1851 state.services.shift_remove(&key);
1852 }
1853 }
1854 Result::Ok(())
1855 },
1856 );
1857
1858 let mut stream = client.subscribe_services(ServiceSelector::ServiceType(GVPN_EXIT))?;
1859 for step in [0u8, 1, 2] {
1860 client.submit_transaction(&[step]).await?;
1861 }
1862
1863 let updates = stream.by_ref().take(3).collect::<Vec<_>>().await;
1864 let updates = updates.into_iter().collect::<Result<Vec<_>>>()?;
1865
1866 insta::assert_yaml_snapshot!(updates);
1867 assert_eq!(
1868 updates.iter().map(|update| update.kind).collect::<Vec<_>>(),
1869 vec![
1870 ServiceUpdateKind::Registered,
1871 ServiceUpdateKind::Updated,
1872 ServiceUpdateKind::Deregistered
1873 ]
1874 );
1875 Ok(())
1876 }
1877
1878 #[tokio::test]
1879 async fn subscribe_service_registry_config_reports_snapshot_then_update() -> anyhow::Result<()> {
1880 let mut initial = BlokliTestState::default();
1881 initial.service_registry_config = ServiceRegistryConfig {
1882 type_registration_fee: "1 wxHOPR".into(),
1883 node_safe_registry: "0x1111111111111111111111111111111111111111".into(),
1884 };
1885 let client = BlokliTestClient::new(initial, |_: &[u8], state: &mut BlokliTestState| {
1886 state.service_registry_config = ServiceRegistryConfig {
1887 type_registration_fee: "2 wxHOPR".into(),
1888 node_safe_registry: "0x2222222222222222222222222222222222222222".into(),
1889 };
1890 Result::Ok(())
1891 });
1892
1893 let mut stream = client.subscribe_service_registry_config()?;
1894 let initial = stream
1895 .next()
1896 .await
1897 .ok_or_else(|| anyhow::anyhow!("missing snapshot"))??;
1898 client.submit_transaction(&[0]).await?;
1899 let updated = stream.next().await.ok_or_else(|| anyhow::anyhow!("missing update"))??;
1900
1901 insta::assert_yaml_snapshot!(vec![initial, updated]);
1902 Ok(())
1903 }
1904
1905 #[tokio::test]
1906 async fn subscribe_services_filters_out_other_nodes() -> anyhow::Result<()> {
1907 let client = BlokliTestClient::new(BlokliTestState::default(), |_: &[u8], state: &mut BlokliTestState| {
1908 state.services.insert(
1909 BlokliTestState::service_entry_key("gvpn:exit", &OTHER_NODE),
1910 entry("gvpn:exit", &OTHER_NODE),
1911 );
1912 state.services.insert(
1913 BlokliTestState::service_entry_key("gvpn:exit", &NODE),
1914 entry("gvpn:exit", &NODE),
1915 );
1916 Result::Ok(())
1917 });
1918
1919 let mut stream = client.subscribe_services(ServiceSelector::Node(NODE))?;
1920 client.submit_transaction(&[0]).await?;
1921
1922 let update = stream
1923 .next()
1924 .await
1925 .ok_or_else(|| anyhow::anyhow!("service subscription ended early"))??;
1926
1927 assert_eq!(update.node, hex::encode(NODE));
1928 Ok(())
1929 }
1930
1931 #[tokio::test]
1932 async fn subscribe_service_types_reports_one_event_per_changed_field() -> anyhow::Result<()> {
1933 let mut initial = BlokliTestState::default();
1934 initial
1935 .service_types
1936 .insert("gvpn:exit".to_string(), service_type_info("gvpn:exit", Some("0x4444")));
1937
1938 let client = BlokliTestClient::new(initial, |_: &[u8], state: &mut BlokliTestState| {
1939 let info = state
1940 .service_types
1941 .get_mut("gvpn:exit")
1942 .ok_or_else(|| anyhow::anyhow!("missing service type"))
1943 .map_err(|e| crate::errors::ErrorKind::MockClientError(e))?;
1944 info.owner = None;
1945 info.update_burn = "5 wei wxHOPR".to_string();
1946 Result::Ok(())
1947 });
1948
1949 let mut stream = client.subscribe_service_types(Some(GVPN_EXIT))?;
1950 client.submit_transaction(&[0]).await?;
1951
1952 let updates = stream.by_ref().take(2).collect::<Vec<_>>().await;
1953 let updates = updates.into_iter().collect::<Result<Vec<_>>>()?;
1954
1955 assert_eq!(
1956 updates.iter().map(|update| update.kind).collect::<Vec<_>>(),
1957 vec![
1958 ServiceTypeUpdateKind::OwnerChanged,
1959 ServiceTypeUpdateKind::UpdateBurnChanged
1960 ]
1961 );
1962 insta::assert_yaml_snapshot!(updates);
1963 Ok(())
1964 }
1965
1966 #[cfg(feature = "curvy")]
1967 #[tokio::test]
1968 async fn curvy_test_client_methods_have_deterministic_defaults() -> anyhow::Result<()> {
1969 let client = BlokliTestClient::new(BlokliTestState::default(), NopStateMutator);
1970
1971 assert!(client.query_curvy_pending_notes(None, None, 1).await?.notes.is_empty());
1972 assert!(
1973 client
1974 .query_curvy_committed_notes(None, None, 1)
1975 .await?
1976 .notes
1977 .is_empty()
1978 );
1979 assert!(
1980 client
1981 .query_curvy_committed_nullifiers(None, None, 1)
1982 .await?
1983 .nullifiers
1984 .is_empty()
1985 );
1986
1987 assert!(client.query_curvy_sync_checkpoint(None).await.is_err());
1988 assert!(
1989 client
1990 .query_curvy_sync_notes("checkpoint".to_owned(), None, 1)
1991 .await
1992 .is_err()
1993 );
1994 assert!(
1995 client
1996 .query_curvy_sync_nullifiers("checkpoint".to_owned(), None, 1)
1997 .await
1998 .is_err()
1999 );
2000 assert!(
2001 client
2002 .query_curvy_shard_roots("checkpoint".to_owned(), None, 1)
2003 .await
2004 .is_err()
2005 );
2006 assert!(client.query_curvy_aggregator_state().await.is_err());
2007 assert!(client.query_curvy_note_status("note".to_owned()).await.is_err());
2008 assert!(client.query_curvy_valid_notes_root("root".to_owned()).await.is_err());
2009 assert!(
2010 client
2011 .query_curvy_nullifier_spent("nullifier".to_owned())
2012 .await
2013 .is_err()
2014 );
2015 assert!(client.query_curvy_vault_fees().await.is_err());
2016 assert!(client.query_curvy_aggregator_fees().await.is_err());
2017 assert!(client.query_curvy_vault_token_count().await.is_err());
2018 assert!(client.query_curvy_vault_token("1".to_owned()).await.is_err());
2019 assert!(
2020 client
2021 .query_curvy_entry_portal_address("owner".to_owned(), "recovery".to_owned())
2022 .await
2023 .is_err()
2024 );
2025 assert!(
2026 client
2027 .query_curvy_exit_portal_address("exit".to_owned(), "1".to_owned(), "recovery".to_owned(),)
2028 .await
2029 .is_err()
2030 );
2031 assert!(client.query_curvy_portal_registered("portal".to_owned()).await.is_err());
2032
2033 assert!(client.subscribe_curvy_pending_notes(None)?.next().await.is_none());
2034 assert!(client.subscribe_curvy_committed_notes(None)?.next().await.is_none());
2035 assert!(
2036 client
2037 .subscribe_curvy_committed_nullifiers(None)?
2038 .next()
2039 .await
2040 .is_none()
2041 );
2042
2043 Ok(())
2044 }
2045}