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)
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).await?.boxed())
184    }
185
186    async fn register_safe(
187        &self,
188        safe_address: &Address,
189    ) -> Result<BoxFuture<'_, Result<ChainReceipt, Self::Error>>, SafeRegistrationError<Self::Error>> {
190        self.check_connection_state()
191            .map_err(SafeRegistrationError::processing)?;
192
193        // Check if the node isn't already registered with some Safe
194        let my_node_addr = self.chain_key.public().to_address();
195        if let Some(safe_with_node) = self
196            .client
197            .query_safe(blokli_client::api::v1::SafeSelector::RegisteredNode(
198                my_node_addr.into(),
199            ))
200            .await
201            .map_err(SafeRegistrationError::processing)?
202            .first()
203        {
204            // If already registered, return which Safe it is registered with
205            let registered_safe_addr =
206                Address::from_hex(&safe_with_node.address).map_err(SafeRegistrationError::processing)?;
207            return Err(SafeRegistrationError::AlreadyRegistered(registered_safe_addr));
208        }
209
210        // Check if Safe with this address even exists (has been deployed)
211        if self
212            .client
213            .query_safe(blokli_client::api::v1::SafeSelector::SafeAddress(
214                (*safe_address).into(),
215            ))
216            .await
217            .map_err(SafeRegistrationError::processing)?
218            .is_empty()
219        {
220            return Err(SafeRegistrationError::ProcessingError(
221                ConnectorError::SafeDoesNotExist(*safe_address),
222            ));
223        }
224
225        tracing::debug!(%safe_address, %my_node_addr, "safe exists, proceeding with registration");
226
227        let tx_req = self
228            .payload_generator
229            .register_safe_by_node(*safe_address)
230            .map_err(SafeRegistrationError::processing)?;
231
232        Ok(self
233            .send_tx(tx_req, None)
234            .map_err(SafeRegistrationError::processing)
235            .await?
236            .boxed())
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use hex_literal::hex;
243    use hopr_api::{
244        chain::{ChainReadAccountOperations, ChainWriteAccountOperations, DeployedSafe},
245        types::internal::account::AccountType,
246    };
247
248    use super::*;
249    use crate::{
250        connector::tests::{MODULE_ADDR, PRIVATE_KEY_1, PRIVATE_KEY_2, create_connector},
251        testing::BlokliTestStateBuilder,
252    };
253
254    #[tokio::test]
255    async fn connector_should_stream_and_count_accounts() -> anyhow::Result<()> {
256        let account = AccountEntry {
257            public_key: *OffchainKeypair::random().public(),
258            chain_addr: [1u8; Address::SIZE].into(),
259            entry_type: AccountType::NotAnnounced,
260            safe_address: Some([2u8; Address::SIZE].into()),
261            key_id: 1.into(),
262        };
263
264        let blokli_client = BlokliTestStateBuilder::default()
265            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
266            .build_static_client();
267
268        let mut connector = create_connector(blokli_client)?;
269        connector.connect().await?;
270
271        let accounts = connector
272            .stream_accounts(AccountSelector::default())?
273            .collect::<Vec<_>>()
274            .await;
275
276        let count = connector.count_accounts(AccountSelector::default()).await?;
277
278        assert_eq!(accounts.len(), 1);
279        assert_eq!(count, 1);
280        assert_eq!(&accounts[0], &account);
281
282        Ok(())
283    }
284
285    #[tokio::test]
286    async fn connector_should_stream_and_count_accounts_with_selector() -> anyhow::Result<()> {
287        let account_1 = AccountEntry {
288            public_key: *OffchainKeypair::random().public(),
289            chain_addr: [1u8; Address::SIZE].into(),
290            entry_type: AccountType::NotAnnounced,
291            safe_address: Some([2u8; Address::SIZE].into()),
292            key_id: 1.into(),
293        };
294
295        let account_2 = AccountEntry {
296            public_key: *OffchainKeypair::random().public(),
297            chain_addr: [2u8; Address::SIZE].into(),
298            entry_type: AccountType::Announced(vec!["/ip4/1.2.3.4/tcp/1234".parse()?]),
299            safe_address: Some([3u8; Address::SIZE].into()),
300            key_id: 2.into(),
301        };
302
303        let blokli_client = BlokliTestStateBuilder::default()
304            .with_accounts([
305                (account_1.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1)),
306                (account_2.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1)),
307            ])
308            .build_static_client();
309
310        let mut connector = create_connector(blokli_client)?;
311        connector.connect().await?;
312
313        let selector = AccountSelector::default().with_chain_key(account_1.chain_addr);
314        let accounts = connector.stream_accounts(selector)?.collect::<Vec<_>>().await;
315        let count = connector.count_accounts(selector).await?;
316
317        assert_eq!(accounts.len(), count);
318        assert_eq!(accounts, vec![account_1.clone()]);
319
320        let selector = AccountSelector::default().with_offchain_key(account_1.public_key);
321        let accounts = connector.stream_accounts(selector)?.collect::<Vec<_>>().await;
322        let count = connector.count_accounts(selector).await?;
323
324        assert_eq!(accounts.len(), count);
325        assert_eq!(accounts, vec![account_1.clone()]);
326
327        let selector = AccountSelector::default().with_public_only(true);
328        let accounts = connector.stream_accounts(selector)?.collect::<Vec<_>>().await;
329        let count = connector.count_accounts(selector).await?;
330
331        assert_eq!(accounts.len(), count);
332        assert_eq!(accounts, vec![account_2.clone()]);
333
334        let selector = AccountSelector::default()
335            .with_chain_key(account_1.chain_addr)
336            .with_public_only(true);
337        let accounts = connector.stream_accounts(selector)?.collect::<Vec<_>>().await;
338        let count = connector.count_accounts(selector).await?;
339
340        assert_eq!(count, 0);
341        assert!(accounts.is_empty());
342
343        Ok(())
344    }
345
346    #[test_log::test(tokio::test)]
347    async fn connector_should_announce_new_account_with_multiaddresses() -> anyhow::Result<()> {
348        let blokli_client = BlokliTestStateBuilder::default()
349            .with_balances([(
350                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
351                XDaiBalance::new_base(1),
352            )])
353            .with_hopr_network_chain_info("rotsee")
354            .build_dynamic_client(MODULE_ADDR.into());
355
356        let mut connector = create_connector(blokli_client)?;
357        connector.connect().await?;
358
359        let offchain_key = OffchainKeypair::from_secret(&hex!(
360            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
361        ))?;
362        let multiaddress = Multiaddr::from_str("/ip4/127.0.0.1/tcp/1234")?;
363
364        connector.announce(&[multiaddress], &offchain_key).await?.await?;
365
366        insta::assert_yaml_snapshot!(*connector.client.snapshot());
367
368        let accounts = connector
369            .stream_accounts(AccountSelector::default().with_public_only(true))?
370            .collect::<Vec<_>>()
371            .await;
372
373        assert_eq!(accounts.len(), 1);
374        assert_eq!(
375            accounts[0].get_multiaddrs(),
376            &[Multiaddr::from_str("/ip4/127.0.0.1/tcp/1234")?]
377        );
378
379        Ok(())
380    }
381
382    #[test_log::test(tokio::test)]
383    async fn connector_should_announce_new_account_without_multiaddresses() -> anyhow::Result<()> {
384        let blokli_client = BlokliTestStateBuilder::default()
385            .with_hopr_network_chain_info("rotsee")
386            .with_balances([(
387                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
388                XDaiBalance::new_base(1),
389            )])
390            .build_dynamic_client(MODULE_ADDR.into());
391
392        let mut connector = create_connector(blokli_client)?;
393        connector.connect().await?;
394
395        let offchain_key = OffchainKeypair::from_secret(&hex!(
396            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
397        ))?;
398
399        connector.announce(&[], &offchain_key).await?.await?;
400
401        insta::assert_yaml_snapshot!(*connector.client.snapshot());
402
403        let accounts = connector
404            .stream_accounts(AccountSelector::default())?
405            .collect::<Vec<_>>()
406            .await;
407
408        assert_eq!(accounts.len(), 1);
409        assert!(accounts[0].get_multiaddrs().is_empty());
410
411        Ok(())
412    }
413
414    #[test_log::test(tokio::test)]
415    async fn connector_should_not_reannounce_when_existing_account_has_same_multiaddresses() -> anyhow::Result<()> {
416        let offchain_key = OffchainKeypair::from_secret(&hex!(
417            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
418        ))?;
419        let multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/1234".parse()?;
420        let account = AccountEntry {
421            public_key: *offchain_key.public(),
422            chain_addr: ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
423            entry_type: AccountType::Announced(vec![multiaddr.clone()]),
424            safe_address: Some([2u8; Address::SIZE].into()),
425            key_id: 1.into(),
426        };
427
428        let blokli_client = BlokliTestStateBuilder::default()
429            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
430            .with_hopr_network_chain_info("rotsee")
431            .build_dynamic_client(MODULE_ADDR.into());
432
433        let mut connector = create_connector(blokli_client)?;
434        connector.connect().await?;
435
436        assert!(matches!(
437            connector.announce(&[], &offchain_key).await,
438            Err(AnnouncementError::AlreadyAnnounced)
439        ));
440
441        assert!(matches!(
442            connector.announce(&[multiaddr], &offchain_key).await,
443            Err(AnnouncementError::AlreadyAnnounced)
444        ));
445
446        insta::assert_yaml_snapshot!(*connector.client.snapshot());
447
448        Ok(())
449    }
450
451    #[tokio::test]
452    async fn connector_should_reannounce_when_existing_account_has_no_multiaddresses() -> anyhow::Result<()> {
453        let offchain_key = OffchainKeypair::from_secret(&hex!(
454            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
455        ))?;
456        let multiaddr: Multiaddr = "/ip4/127.0.0.1/tcp/1234".parse()?;
457        let account = AccountEntry {
458            public_key: *offchain_key.public(),
459            chain_addr: ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
460            entry_type: AccountType::NotAnnounced,
461            safe_address: Some([2u8; Address::SIZE].into()),
462            key_id: 1.into(),
463        };
464
465        let blokli_client = BlokliTestStateBuilder::default()
466            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
467            .with_hopr_network_chain_info("rotsee")
468            .build_dynamic_client(MODULE_ADDR.into());
469
470        let mut connector = create_connector(blokli_client)?;
471        connector.connect().await?;
472
473        assert!(matches!(
474            connector.announce(&[], &offchain_key).await,
475            Err(AnnouncementError::AlreadyAnnounced)
476        ));
477
478        connector
479            .announce(std::slice::from_ref(&multiaddr), &offchain_key)
480            .await?
481            .await?;
482
483        insta::assert_yaml_snapshot!(*connector.client.snapshot());
484
485        let accounts = connector
486            .stream_accounts(AccountSelector::default().with_public_only(true))?
487            .collect::<Vec<_>>()
488            .await;
489
490        assert_eq!(accounts.len(), 1);
491        assert_eq!(accounts[0].get_multiaddrs(), &[multiaddr]);
492
493        Ok(())
494    }
495
496    #[tokio::test]
497    async fn connector_should_withdraw() -> anyhow::Result<()> {
498        let blokli_client = BlokliTestStateBuilder::default()
499            .with_balances([([1u8; Address::SIZE].into(), HoprBalance::zero())])
500            .with_balances([([1u8; Address::SIZE].into(), XDaiBalance::zero())])
501            .with_balances([(
502                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
503                XDaiBalance::new_base(10),
504            )])
505            .with_balances([(
506                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
507                HoprBalance::new_base(1000),
508            )])
509            .with_hopr_network_chain_info("rotsee")
510            .build_dynamic_client(MODULE_ADDR.into());
511
512        let mut connector = create_connector(blokli_client)?;
513        connector.connect().await?;
514
515        connector
516            .withdraw(HoprBalance::new_base(10), &[1u8; Address::SIZE].into())
517            .await?
518            .await?;
519        connector
520            .withdraw(XDaiBalance::new_base(1), &[1u8; Address::SIZE].into())
521            .await?
522            .await?;
523
524        insta::assert_yaml_snapshot!(*connector.client.snapshot());
525
526        Ok(())
527    }
528
529    #[tokio::test]
530    async fn connector_should_register_safe() -> anyhow::Result<()> {
531        let deployer_addr = ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address();
532        let blokli_client = BlokliTestStateBuilder::default()
533            .with_balances([(
534                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
535                XDaiBalance::new_base(10),
536            )])
537            .with_deployed_safes([DeployedSafe {
538                address: [1u8; Address::SIZE].into(),
539                owners: vec![deployer_addr],
540                module: MODULE_ADDR.into(),
541                registered_nodes: vec![],
542                deployer: deployer_addr,
543            }])
544            .with_hopr_network_chain_info("rotsee")
545            .build_dynamic_client(MODULE_ADDR.into());
546
547        let mut connector = create_connector(blokli_client)?;
548        connector.connect().await?;
549
550        connector.register_safe(&[1u8; Address::SIZE].into()).await?.await?;
551
552        insta::assert_yaml_snapshot!(*connector.client.snapshot());
553
554        Ok(())
555    }
556
557    #[tokio::test]
558    async fn connector_should_register_safe_that_has_nodes_registered_already() -> anyhow::Result<()> {
559        let safe_addr: Address = [2u8; Address::SIZE].into();
560        let deployer_addr = ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address();
561        let other_registered_node = ChainKeypair::from_secret(&PRIVATE_KEY_2)?.public().to_address();
562
563        let blokli_client = BlokliTestStateBuilder::default()
564            .with_balances([(
565                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
566                XDaiBalance::new_base(10),
567            )])
568            .with_deployed_safes([DeployedSafe {
569                address: safe_addr,
570                owners: vec![deployer_addr],
571                module: MODULE_ADDR.into(),
572                registered_nodes: vec![other_registered_node],
573                deployer: deployer_addr,
574            }])
575            .with_hopr_network_chain_info("rotsee")
576            .build_dynamic_client(MODULE_ADDR.into());
577
578        let mut connector = create_connector(blokli_client)?;
579        connector.connect().await?;
580
581        connector.register_safe(&safe_addr).await?.await?;
582
583        insta::assert_yaml_snapshot!(*connector.client.snapshot());
584
585        Ok(())
586    }
587
588    #[tokio::test]
589    async fn connector_should_not_register_safe_that_does_not_exist() -> anyhow::Result<()> {
590        let safe_addr: Address = [2u8; Address::SIZE].into();
591
592        let blokli_client = BlokliTestStateBuilder::default()
593            .with_balances([(
594                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
595                XDaiBalance::new_base(10),
596            )])
597            .with_hopr_network_chain_info("rotsee")
598            .build_dynamic_client(MODULE_ADDR.into());
599
600        let mut connector = create_connector(blokli_client)?;
601        connector.connect().await?;
602
603        assert!(connector.register_safe(&safe_addr).await.is_err());
604
605        insta::assert_yaml_snapshot!(*connector.client.snapshot());
606
607        Ok(())
608    }
609
610    #[tokio::test]
611    async fn connector_should_not_register_any_safe_when_node_already_registered() -> anyhow::Result<()> {
612        let deployer_addr = ChainKeypair::from_secret(&PRIVATE_KEY_2)?.public().to_address();
613        let blokli_client = BlokliTestStateBuilder::default()
614            .with_balances([(
615                ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address(),
616                XDaiBalance::new_base(10),
617            )])
618            .with_deployed_safes([
619                DeployedSafe {
620                    address: [2u8; Address::SIZE].into(),
621                    owners: vec![deployer_addr],
622                    module: MODULE_ADDR.into(),
623                    registered_nodes: vec![ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address()],
624                    deployer: deployer_addr,
625                },
626                DeployedSafe {
627                    address: [1u8; Address::SIZE].into(),
628                    owners: vec![deployer_addr],
629                    module: MODULE_ADDR.into(),
630                    registered_nodes: vec![],
631                    deployer: deployer_addr,
632                },
633            ])
634            .with_hopr_network_chain_info("rotsee")
635            .build_dynamic_client(MODULE_ADDR.into());
636
637        let mut connector = create_connector(blokli_client)?;
638        connector.connect().await?;
639
640        assert!(
641            matches!(connector.register_safe(&[1u8; Address::SIZE].into()).await, Err(SafeRegistrationError::AlreadyRegistered(a)) if a == [2u8; Address::SIZE].into())
642        );
643
644        insta::assert_yaml_snapshot!(*connector.client.snapshot());
645
646        Ok(())
647    }
648}