Skip to main content

hopr_chain_connector/connector/
accounts.rs

1use std::str::FromStr;
2
3use blokli_client::api::{BlokliQueryClient, BlokliSubscriptionClient, BlokliTransactionClient};
4use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::BoxFuture, pin_mut, stream::BoxStream};
5use futures_time::future::FutureExt as TimeFutureExt;
6use hopr_api::{
7    chain::{AccountSelector, AnnouncementError, ChainReceipt, Multiaddr, SafeRegistrationError},
8    types::{
9        chain::prelude::*,
10        crypto::prelude::*,
11        internal::{
12            account::AccountEntry,
13            prelude::{AnnouncementData, KeyBinding},
14        },
15        primitive::prelude::*,
16    },
17};
18
19use crate::{
20    backend::Backend, connector::HoprBlockchainConnector, errors::ConnectorError, utils::model_to_account_entry,
21};
22
23impl<B, C, P, R> HoprBlockchainConnector<C, B, P, R>
24where
25    B: Backend + Send + Sync + 'static,
26{
27    pub(crate) fn build_account_stream(
28        &self,
29        selector: AccountSelector,
30    ) -> Result<impl futures::Stream<Item = AccountEntry> + Send + 'static, ConnectorError> {
31        let mut accounts = self.graph.read().nodes().collect::<Vec<_>>();
32
33        // Ensure the returned accounts are always perfectly ordered by their id.
34        accounts.sort_unstable();
35
36        let backend = self.backend.clone();
37        Ok(futures::stream::iter(accounts).filter_map(move |account_id| {
38            let backend = backend.clone();
39            // This avoids the cache on purpose so it does not get spammed
40            async move {
41                match hopr_utils::runtime::prelude::spawn_blocking(move || backend.get_account_by_id(&account_id)).await
42                {
43                    Ok(Ok(value)) => value.filter(|c| selector.satisfies(c)),
44                    Ok(Err(error)) => {
45                        tracing::error!(%error, %account_id, "backend error when looking up account");
46                        None
47                    }
48                    Err(error) => {
49                        tracing::error!(%error, %account_id, "join error when looking up account");
50                        None
51                    }
52                }
53            }
54        }))
55    }
56}
57
58#[async_trait::async_trait]
59impl<B, C, P, R> hopr_api::chain::ChainReadAccountOperations for HoprBlockchainConnector<C, B, P, R>
60where
61    B: Backend + Send + Sync + 'static,
62    C: BlokliQueryClient + BlokliSubscriptionClient + Send + Sync + 'static,
63    P: Send + Sync + 'static,
64    R: Send + Sync,
65{
66    type Error = ConnectorError;
67
68    fn stream_accounts(&self, selector: AccountSelector) -> Result<BoxStream<'_, AccountEntry>, Self::Error> {
69        self.check_connection_state()?;
70
71        Ok(self.build_account_stream(selector)?.boxed())
72    }
73
74    async fn count_accounts(&self, selector: AccountSelector) -> Result<usize, Self::Error> {
75        self.check_connection_state()?;
76
77        Ok(self.stream_accounts(selector)?.count().await)
78    }
79
80    async fn await_key_binding(
81        &self,
82        offchain_key: &OffchainPublicKey,
83        timeout: std::time::Duration,
84    ) -> Result<AccountEntry, Self::Error> {
85        self.check_connection_state()?;
86
87        let selector = blokli_client::api::v1::AccountSelector::PacketKey((*offchain_key).into());
88        if let Some(node) = self.client.query_accounts(selector.clone()).await?.first().cloned() {
89            return model_to_account_entry(node);
90        }
91
92        let stream = self.client.subscribe_accounts(selector)?.map_err(ConnectorError::from);
93        pin_mut!(stream);
94        if let Some(node) = stream
95            .try_next()
96            .timeout(futures_time::time::Duration::from(
97                timeout.max(std::time::Duration::from_secs(1)),
98            ))
99            .await
100            .map_err(|_| ConnectorError::other(anyhow::anyhow!("timeout while waiting for key binding")))??
101        {
102            model_to_account_entry(node)
103        } else {
104            Err(ConnectorError::AccountDoesNotExist(format!(
105                "with packet key {offchain_key}"
106            )))
107        }
108    }
109}
110
111#[async_trait::async_trait]
112impl<B, C, P> hopr_api::chain::ChainWriteAccountOperations for HoprBlockchainConnector<C, B, P, P::TxRequest>
113where
114    B: Send + Sync,
115    C: BlokliTransactionClient + BlokliQueryClient + Send + Sync + 'static,
116    P: PayloadGenerator + Send + Sync + 'static,
117    P::TxRequest: Send + Sync + 'static,
118{
119    type Error = ConnectorError;
120
121    async fn announce(
122        &self,
123        multiaddrs: &[Multiaddr],
124        key: &OffchainKeypair,
125    ) -> Result<BoxFuture<'_, Result<ChainReceipt, Self::Error>>, AnnouncementError<Self::Error>> {
126        self.check_connection_state().map_err(AnnouncementError::processing)?;
127
128        let new_announced_addrs = ahash::HashSet::from_iter(multiaddrs.iter().map(|a| a.to_string()));
129
130        let existing_account = self
131            .client
132            .query_accounts(blokli_client::api::v1::AccountSelector::Address(
133                self.chain_key.public().to_address().into(),
134            ))
135            .await
136            .map_err(AnnouncementError::processing)?
137            .into_iter()
138            .find(|account| OffchainPublicKey::from_str(&account.packet_key).is_ok_and(|k| &k == key.public()));
139
140        if let Some(account) = &existing_account {
141            let old_announced_addrs = ahash::HashSet::from_iter(account.multi_addresses.iter().cloned());
142            if old_announced_addrs == new_announced_addrs || old_announced_addrs.is_superset(&new_announced_addrs) {
143                return Err(AnnouncementError::AlreadyAnnounced);
144            }
145        }
146
147        // No key-binding fee must be set when the account already exists (with multi-addresses or not)
148        let key_binding = KeyBinding::new(self.chain_key.public().to_address(), key);
149        let key_binding_fee = if existing_account.is_none() {
150            self.query_cached_chain_info()
151                .await
152                .map_err(AnnouncementError::processing)?
153                .key_binding_fee
154        } else {
155            HoprBalance::zero()
156        };
157
158        let tx_req = self
159            .payload_generator
160            .announce(
161                AnnouncementData::new(key_binding, multiaddrs.first().cloned())
162                    .map_err(|e| AnnouncementError::ProcessingError(ConnectorError::OtherError(e.into())))?,
163                key_binding_fee,
164            )
165            .map_err(AnnouncementError::processing)?;
166
167        Ok(self
168            .send_tx(tx_req, None, None)
169            .map_err(AnnouncementError::processing)
170            .await?
171            .boxed())
172    }
173
174    async fn withdraw<Cy: Currency + Send>(
175        &self,
176        balance: Balance<Cy>,
177        recipient: &Address,
178    ) -> Result<BoxFuture<'_, Result<ChainReceipt, Self::Error>>, Self::Error> {
179        self.check_connection_state()?;
180
181        let tx_req = self.payload_generator.transfer(*recipient, balance)?;
182
183        Ok(self.send_tx(tx_req, None, None).await?.boxed())
184    }
185
186    async fn withdraw_from_signer<Cy: Currency + Send>(
187        &self,
188        signer: &ChainKeypair,
189        balance: Balance<Cy>,
190        recipient: &Address,
191    ) -> Result<BoxFuture<'_, Result<ChainReceipt, Self::Error>>, Self::Error> {
192        self.check_connection_state()?;
193
194        let tx_req = self.payload_generator.transfer(*recipient, balance)?;
195
196        Ok(self.send_tx(tx_req, None, Some(signer.clone())).await?.boxed())
197    }
198
199    async fn register_safe(
200        &self,
201        safe_address: &Address,
202    ) -> Result<BoxFuture<'_, Result<ChainReceipt, Self::Error>>, SafeRegistrationError<Self::Error>> {
203        self.check_connection_state()
204            .map_err(SafeRegistrationError::processing)?;
205
206        // Check if the node isn't already registered with some Safe
207        let my_node_addr = self.chain_key.public().to_address();
208        if let Some(safe_with_node) = self
209            .client
210            .query_safe(blokli_client::api::v1::SafeSelector::RegisteredNode(
211                my_node_addr.into(),
212            ))
213            .await
214            .map_err(SafeRegistrationError::processing)?
215            .first()
216        {
217            // If already registered, return which Safe it is registered with
218            let registered_safe_addr =
219                Address::from_hex(&safe_with_node.address).map_err(SafeRegistrationError::processing)?;
220            return Err(SafeRegistrationError::AlreadyRegistered(registered_safe_addr));
221        }
222
223        // Check if Safe with this address even exists (has been deployed)
224        if self
225            .client
226            .query_safe(blokli_client::api::v1::SafeSelector::SafeAddress(
227                (*safe_address).into(),
228            ))
229            .await
230            .map_err(SafeRegistrationError::processing)?
231            .is_empty()
232        {
233            return Err(SafeRegistrationError::ProcessingError(
234                ConnectorError::SafeDoesNotExist(*safe_address),
235            ));
236        }
237
238        tracing::debug!(%safe_address, %my_node_addr, "safe exists, proceeding with registration");
239
240        let tx_req = self
241            .payload_generator
242            .register_safe_by_node(*safe_address)
243            .map_err(SafeRegistrationError::processing)?;
244
245        Ok(self
246            .send_tx(tx_req, None, None)
247            .map_err(SafeRegistrationError::processing)
248            .await?
249            .boxed())
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use hex_literal::hex;
256    use hopr_api::{
257        chain::{ChainReadAccountOperations, ChainWriteAccountOperations, DeployedSafe},
258        types::internal::account::AccountType,
259    };
260
261    use super::*;
262    use crate::{
263        connector::tests::{MODULE_ADDR, PRIVATE_KEY_1, PRIVATE_KEY_2, create_connector},
264        testing::BlokliTestStateBuilder,
265    };
266
267    #[tokio::test]
268    async fn connector_should_stream_and_count_accounts() -> anyhow::Result<()> {
269        let account = AccountEntry {
270            public_key: *OffchainKeypair::random().public(),
271            chain_addr: [1u8; Address::SIZE].into(),
272            entry_type: AccountType::NotAnnounced,
273            safe_address: Some([2u8; Address::SIZE].into()),
274            key_id: 1.into(),
275        };
276
277        let blokli_client = BlokliTestStateBuilder::default()
278            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
279            .build_static_client();
280
281        let mut connector = create_connector(blokli_client)?;
282        connector.connect().await?;
283
284        let accounts = connector
285            .stream_accounts(AccountSelector::default())?
286            .collect::<Vec<_>>()
287            .await;
288
289        let count = connector.count_accounts(AccountSelector::default()).await?;
290
291        assert_eq!(accounts.len(), 1);
292        assert_eq!(count, 1);
293        assert_eq!(&accounts[0], &account);
294
295        Ok(())
296    }
297
298    #[tokio::test]
299    async fn connector_should_stream_and_count_accounts_with_selector() -> anyhow::Result<()> {
300        let account_1 = AccountEntry {
301            public_key: *OffchainKeypair::random().public(),
302            chain_addr: [1u8; Address::SIZE].into(),
303            entry_type: AccountType::NotAnnounced,
304            safe_address: Some([2u8; Address::SIZE].into()),
305            key_id: 1.into(),
306        };
307
308        let account_2 = AccountEntry {
309            public_key: *OffchainKeypair::random().public(),
310            chain_addr: [2u8; Address::SIZE].into(),
311            entry_type: AccountType::Announced(vec!["/ip4/1.2.3.4/tcp/1234".parse()?]),
312            safe_address: Some([3u8; Address::SIZE].into()),
313            key_id: 2.into(),
314        };
315
316        let blokli_client = BlokliTestStateBuilder::default()
317            .with_accounts([
318                (account_1.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1)),
319                (account_2.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1)),
320            ])
321            .build_static_client();
322
323        let mut connector = create_connector(blokli_client)?;
324        connector.connect().await?;
325
326        let selector = AccountSelector::default().with_chain_key(account_1.chain_addr);
327        let accounts = connector.stream_accounts(selector)?.collect::<Vec<_>>().await;
328        let count = connector.count_accounts(selector).await?;
329
330        assert_eq!(accounts.len(), count);
331        assert_eq!(accounts, vec![account_1.clone()]);
332
333        let selector = AccountSelector::default().with_offchain_key(account_1.public_key);
334        let accounts = connector.stream_accounts(selector)?.collect::<Vec<_>>().await;
335        let count = connector.count_accounts(selector).await?;
336
337        assert_eq!(accounts.len(), count);
338        assert_eq!(accounts, vec![account_1.clone()]);
339
340        let selector = AccountSelector::default().with_public_only(true);
341        let accounts = connector.stream_accounts(selector)?.collect::<Vec<_>>().await;
342        let count = connector.count_accounts(selector).await?;
343
344        assert_eq!(accounts.len(), count);
345        assert_eq!(accounts, vec![account_2.clone()]);
346
347        let selector = AccountSelector::default()
348            .with_chain_key(account_1.chain_addr)
349            .with_public_only(true);
350        let accounts = connector.stream_accounts(selector)?.collect::<Vec<_>>().await;
351        let count = connector.count_accounts(selector).await?;
352
353        assert_eq!(count, 0);
354        assert!(accounts.is_empty());
355
356        Ok(())
357    }
358
359    #[test_log::test(tokio::test)]
360    async fn connector_should_announce_new_account_with_multiaddresses() -> anyhow::Result<()> {
361        let blokli_client = BlokliTestStateBuilder::default()
362            .with_balances([(
363                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
364                XDaiBalance::new_base(1),
365            )])
366            .with_hopr_network_chain_info("rotsee")
367            .build_dynamic_client(MODULE_ADDR.into());
368
369        let mut connector = create_connector(blokli_client)?;
370        connector.connect().await?;
371
372        let offchain_key = OffchainKeypair::from_secret(&hex!(
373            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
374        ))?;
375        let multiaddress = Multiaddr::from_str("/ip4/127.0.0.1/tcp/1234")?;
376
377        connector.announce(&[multiaddress], &offchain_key).await?.await?;
378
379        insta::assert_yaml_snapshot!(*connector.client.snapshot());
380
381        let accounts = connector
382            .stream_accounts(AccountSelector::default().with_public_only(true))?
383            .collect::<Vec<_>>()
384            .await;
385
386        assert_eq!(accounts.len(), 1);
387        assert_eq!(
388            accounts[0].get_multiaddrs(),
389            &[Multiaddr::from_str("/ip4/127.0.0.1/tcp/1234")?]
390        );
391
392        Ok(())
393    }
394
395    #[test_log::test(tokio::test)]
396    async fn connector_should_announce_new_account_without_multiaddresses() -> anyhow::Result<()> {
397        let blokli_client = BlokliTestStateBuilder::default()
398            .with_hopr_network_chain_info("rotsee")
399            .with_balances([(
400                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
401                XDaiBalance::new_base(1),
402            )])
403            .build_dynamic_client(MODULE_ADDR.into());
404
405        let mut connector = create_connector(blokli_client)?;
406        connector.connect().await?;
407
408        let offchain_key = OffchainKeypair::from_secret(&hex!(
409            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
410        ))?;
411
412        connector.announce(&[], &offchain_key).await?.await?;
413
414        insta::assert_yaml_snapshot!(*connector.client.snapshot());
415
416        let accounts = connector
417            .stream_accounts(AccountSelector::default())?
418            .collect::<Vec<_>>()
419            .await;
420
421        assert_eq!(accounts.len(), 1);
422        assert!(accounts[0].get_multiaddrs().is_empty());
423
424        Ok(())
425    }
426
427    #[test_log::test(tokio::test)]
428    async fn connector_should_not_reannounce_when_existing_account_has_same_multiaddresses() -> anyhow::Result<()> {
429        let offchain_key = OffchainKeypair::from_secret(&hex!(
430            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
431        ))?;
432        let multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/1234".parse()?;
433        let account = AccountEntry {
434            public_key: *offchain_key.public(),
435            chain_addr: ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
436            entry_type: AccountType::Announced(vec![multiaddr.clone()]),
437            safe_address: Some([2u8; Address::SIZE].into()),
438            key_id: 1.into(),
439        };
440
441        let blokli_client = BlokliTestStateBuilder::default()
442            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
443            .with_hopr_network_chain_info("rotsee")
444            .build_dynamic_client(MODULE_ADDR.into());
445
446        let mut connector = create_connector(blokli_client)?;
447        connector.connect().await?;
448
449        assert!(matches!(
450            connector.announce(&[], &offchain_key).await,
451            Err(AnnouncementError::AlreadyAnnounced)
452        ));
453
454        assert!(matches!(
455            connector.announce(&[multiaddr], &offchain_key).await,
456            Err(AnnouncementError::AlreadyAnnounced)
457        ));
458
459        insta::assert_yaml_snapshot!(*connector.client.snapshot());
460
461        Ok(())
462    }
463
464    #[tokio::test]
465    async fn connector_should_reannounce_when_existing_account_has_no_multiaddresses() -> anyhow::Result<()> {
466        let offchain_key = OffchainKeypair::from_secret(&hex!(
467            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
468        ))?;
469        let multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/1234".parse()?;
470        let account = AccountEntry {
471            public_key: *offchain_key.public(),
472            chain_addr: ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
473            entry_type: AccountType::NotAnnounced,
474            safe_address: Some([2u8; Address::SIZE].into()),
475            key_id: 1.into(),
476        };
477
478        let blokli_client = BlokliTestStateBuilder::default()
479            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
480            .with_hopr_network_chain_info("rotsee")
481            .build_dynamic_client(MODULE_ADDR.into());
482
483        let mut connector = create_connector(blokli_client)?;
484        connector.connect().await?;
485
486        assert!(matches!(
487            connector.announce(&[], &offchain_key).await,
488            Err(AnnouncementError::AlreadyAnnounced)
489        ));
490
491        connector
492            .announce(std::slice::from_ref(&multiaddr), &offchain_key)
493            .await?
494            .await?;
495
496        insta::assert_yaml_snapshot!(*connector.client.snapshot());
497
498        let accounts = connector
499            .stream_accounts(AccountSelector::default().with_public_only(true))?
500            .collect::<Vec<_>>()
501            .await;
502
503        assert_eq!(accounts.len(), 1);
504        assert_eq!(accounts[0].get_multiaddrs(), &[multiaddr]);
505
506        Ok(())
507    }
508
509    #[tokio::test]
510    async fn connector_should_withdraw() -> anyhow::Result<()> {
511        let blokli_client = BlokliTestStateBuilder::default()
512            .with_balances([([1u8; Address::SIZE].into(), HoprBalance::zero())])
513            .with_balances([([1u8; Address::SIZE].into(), XDaiBalance::zero())])
514            .with_balances([(
515                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
516                XDaiBalance::new_base(10),
517            )])
518            .with_balances([(
519                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
520                HoprBalance::new_base(1000),
521            )])
522            .with_hopr_network_chain_info("rotsee")
523            .build_dynamic_client(MODULE_ADDR.into());
524
525        let mut connector = create_connector(blokli_client)?;
526        connector.connect().await?;
527
528        connector
529            .withdraw(HoprBalance::new_base(10), &[1u8; Address::SIZE].into())
530            .await?
531            .await?;
532        connector
533            .withdraw(XDaiBalance::new_base(1), &[1u8; Address::SIZE].into())
534            .await?
535            .await?;
536
537        insta::assert_yaml_snapshot!(*connector.client.snapshot());
538
539        Ok(())
540    }
541
542    #[tokio::test]
543    async fn connector_should_withdraw_from_signer() -> anyhow::Result<()> {
544        let blokli_client = BlokliTestStateBuilder::default()
545            .with_balances([([1u8; Address::SIZE].into(), HoprBalance::zero())])
546            .with_balances([([1u8; Address::SIZE].into(), XDaiBalance::zero())])
547            .with_balances([(
548                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
549                XDaiBalance::new_base(10),
550            )])
551            .with_balances([(
552                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
553                HoprBalance::new_base(1000),
554            )])
555            .with_balances([(
556                ChainKeypair::from_secret(&PRIVATE_KEY_2)?.public().to_address(),
557                XDaiBalance::new_base(10),
558            )])
559            .with_balances([(
560                ChainKeypair::from_secret(&PRIVATE_KEY_2)?.public().to_address(),
561                HoprBalance::new_base(1000),
562            )])
563            .with_hopr_network_chain_info("rotsee")
564            .build_dynamic_client(MODULE_ADDR.into());
565
566        let mut connector = create_connector(blokli_client)?;
567        connector.connect().await?;
568
569        let signer = ChainKeypair::from_secret(&PRIVATE_KEY_2)?;
570
571        connector
572            .withdraw_from_signer(&signer, HoprBalance::new_base(10), &[1u8; Address::SIZE].into())
573            .await?
574            .await?;
575        connector
576            .withdraw_from_signer(&signer, XDaiBalance::new_base(1), &[1u8; Address::SIZE].into())
577            .await?
578            .await?;
579
580        insta::assert_yaml_snapshot!(*connector.client.snapshot());
581
582        Ok(())
583    }
584
585    #[tokio::test]
586    async fn connector_should_register_safe() -> anyhow::Result<()> {
587        let deployer_addr = ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address();
588        let blokli_client = BlokliTestStateBuilder::default()
589            .with_balances([(
590                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
591                XDaiBalance::new_base(10),
592            )])
593            .with_deployed_safes([DeployedSafe {
594                address: [1u8; Address::SIZE].into(),
595                owners: vec![deployer_addr],
596                module: MODULE_ADDR.into(),
597                registered_nodes: vec![],
598                deployer: deployer_addr,
599            }])
600            .with_hopr_network_chain_info("rotsee")
601            .build_dynamic_client(MODULE_ADDR.into());
602
603        let mut connector = create_connector(blokli_client)?;
604        connector.connect().await?;
605
606        connector.register_safe(&[1u8; Address::SIZE].into()).await?.await?;
607
608        insta::assert_yaml_snapshot!(*connector.client.snapshot());
609
610        Ok(())
611    }
612
613    #[tokio::test]
614    async fn connector_should_register_safe_that_has_nodes_registered_already() -> anyhow::Result<()> {
615        let safe_addr: Address = [2u8; Address::SIZE].into();
616        let deployer_addr = ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address();
617        let other_registered_node = ChainKeypair::from_secret(&PRIVATE_KEY_2)?.public().to_address();
618
619        let blokli_client = BlokliTestStateBuilder::default()
620            .with_balances([(
621                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
622                XDaiBalance::new_base(10),
623            )])
624            .with_deployed_safes([DeployedSafe {
625                address: safe_addr,
626                owners: vec![deployer_addr],
627                module: MODULE_ADDR.into(),
628                registered_nodes: vec![other_registered_node],
629                deployer: deployer_addr,
630            }])
631            .with_hopr_network_chain_info("rotsee")
632            .build_dynamic_client(MODULE_ADDR.into());
633
634        let mut connector = create_connector(blokli_client)?;
635        connector.connect().await?;
636
637        connector.register_safe(&safe_addr).await?.await?;
638
639        insta::assert_yaml_snapshot!(*connector.client.snapshot());
640
641        Ok(())
642    }
643
644    #[tokio::test]
645    async fn connector_should_not_register_safe_that_does_not_exist() -> anyhow::Result<()> {
646        let safe_addr: Address = [2u8; Address::SIZE].into();
647
648        let blokli_client = BlokliTestStateBuilder::default()
649            .with_balances([(
650                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
651                XDaiBalance::new_base(10),
652            )])
653            .with_hopr_network_chain_info("rotsee")
654            .build_dynamic_client(MODULE_ADDR.into());
655
656        let mut connector = create_connector(blokli_client)?;
657        connector.connect().await?;
658
659        assert!(connector.register_safe(&safe_addr).await.is_err());
660
661        insta::assert_yaml_snapshot!(*connector.client.snapshot());
662
663        Ok(())
664    }
665
666    #[tokio::test]
667    async fn connector_should_not_register_any_safe_when_node_already_registered() -> anyhow::Result<()> {
668        let deployer_addr = ChainKeypair::from_secret(&PRIVATE_KEY_2)?.public().to_address();
669        let blokli_client = BlokliTestStateBuilder::default()
670            .with_balances([(
671                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
672                XDaiBalance::new_base(10),
673            )])
674            .with_deployed_safes([
675                DeployedSafe {
676                    address: [2u8; Address::SIZE].into(),
677                    owners: vec![deployer_addr],
678                    module: MODULE_ADDR.into(),
679                    registered_nodes: vec![ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address()],
680                    deployer: deployer_addr,
681                },
682                DeployedSafe {
683                    address: [1u8; Address::SIZE].into(),
684                    owners: vec![deployer_addr],
685                    module: MODULE_ADDR.into(),
686                    registered_nodes: vec![],
687                    deployer: deployer_addr,
688                },
689            ])
690            .with_hopr_network_chain_info("rotsee")
691            .build_dynamic_client(MODULE_ADDR.into());
692
693        let mut connector = create_connector(blokli_client)?;
694        connector.connect().await?;
695
696        assert!(
697            matches!(connector.register_safe(&[1u8; Address::SIZE].into()).await, Err(SafeRegistrationError::AlreadyRegistered(a)) if a == [2u8; Address::SIZE].into())
698        );
699
700        insta::assert_yaml_snapshot!(*connector.client.snapshot());
701
702        Ok(())
703    }
704}