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 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 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 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 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 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 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}