hopr_chain_connector/connector/
safe.rs1use std::time::Duration;
2
3use blokli_client::api::{BlokliQueryClient, BlokliSubscriptionClient, BlokliTransactionClient};
4use futures::{FutureExt, future::BoxFuture};
5use hopr_api::chain::{ChainReceipt, ChainValues, DeployedSafe, SafeSelector};
6use hopr_chain_types::prelude::PayloadGenerator;
7use hopr_crypto_types::prelude::Keypair;
8use hopr_primitive_types::prelude::*;
9
10use crate::{Backend, HoprBlockchainConnector, HoprBlockchainReader, errors::ConnectorError};
11
12#[async_trait::async_trait]
13impl<B, C, P, R> hopr_api::chain::ChainReadSafeOperations for HoprBlockchainConnector<C, B, P, R>
14where
15 B: Backend + Send + Sync + 'static,
16 C: BlokliQueryClient + BlokliSubscriptionClient + Send + Sync + 'static,
17 P: Send + Sync + 'static,
18 R: Send + Sync,
19{
20 type Error = ConnectorError;
21
22 #[inline]
25 async fn safe_allowance<Cy: Currency, A: Into<Address> + Send>(
26 &self,
27 safe_address: A,
28 ) -> Result<Balance<Cy>, Self::Error> {
29 HoprBlockchainReader(self.client.clone())
30 .safe_allowance(safe_address)
31 .await
32 }
33
34 #[inline]
35 async fn safe_info(&self, selector: SafeSelector) -> Result<Option<DeployedSafe>, Self::Error> {
36 HoprBlockchainReader(self.client.clone()).safe_info(selector).await
37 }
38
39 #[inline]
40 async fn await_safe_deployment(
41 &self,
42 selector: SafeSelector,
43 timeout: Duration,
44 ) -> Result<DeployedSafe, Self::Error> {
45 HoprBlockchainReader(self.client.clone())
46 .await_safe_deployment(selector, timeout)
47 .await
48 }
49
50 #[inline]
51 async fn predict_module_address(
52 &self,
53 nonce: u64,
54 owner: &Address,
55 safe_address: &Address,
56 ) -> Result<Address, Self::Error> {
57 HoprBlockchainReader(self.client.clone())
58 .predict_module_address(nonce, owner, safe_address)
59 .await
60 }
61}
62
63#[async_trait::async_trait]
64impl<B, C, P> hopr_api::chain::ChainWriteSafeOperations for HoprBlockchainConnector<C, B, P, P::TxRequest>
65where
66 B: Send + Sync + 'static,
67 C: BlokliQueryClient + BlokliTransactionClient + Send + Sync + 'static,
68 P: PayloadGenerator + Send + Sync + 'static,
69 P::TxRequest: Send + Sync + 'static,
70{
71 type Error = ConnectorError;
72
73 async fn deploy_safe<'a>(
74 &'a self,
75 balance: HoprBalance,
76 ) -> Result<BoxFuture<'a, Result<ChainReceipt, Self::Error>>, Self::Error> {
77 let admin = self.chain_key.public().to_address();
78 if self
79 .client
80 .query_safe(blokli_client::api::SafeSelector::ChainKey(admin.into()))
81 .await?
82 .is_some()
83 {
84 return Err(ConnectorError::InvalidState("safe already deployed for this signer"));
85 }
86
87 if self.balance(admin).await? < balance {
88 return Err(ConnectorError::InvalidState("insufficient token balance at the signer"));
89 }
90
91 let tx_req = self
92 .payload_generator
93 .deploy_safe(balance, &[admin], true, hopr_crypto_random::random_bytes())?;
94 tracing::debug!(%balance, %admin, "deploying safe");
95
96 Ok(self.send_tx(tx_req).await?.boxed())
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use hopr_api::chain::ChainReadSafeOperations;
103 use hopr_crypto_types::prelude::*;
104 use hopr_internal_types::prelude::*;
105
106 use super::*;
107 use crate::{
108 connector::tests::{MODULE_ADDR, PRIVATE_KEY_1, create_connector},
109 testing::BlokliTestStateBuilder,
110 };
111
112 #[tokio::test]
113 async fn connector_should_safe_allowance() -> anyhow::Result<()> {
114 let account = AccountEntry {
115 public_key: *OffchainKeypair::random().public(),
116 chain_addr: [1u8; Address::SIZE].into(),
117 entry_type: AccountType::NotAnnounced,
118 safe_address: Some([2u8; Address::SIZE].into()),
119 key_id: 1.into(),
120 };
121
122 let blokli_client = BlokliTestStateBuilder::default()
123 .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
124 .with_safe_allowances([(account.safe_address.unwrap(), HoprBalance::new_base(10000))])
125 .build_static_client();
126
127 let connector = create_connector(blokli_client)?;
128
129 assert_eq!(
130 connector.safe_allowance(account.safe_address.unwrap()).await?,
131 HoprBalance::new_base(10000)
132 );
133
134 Ok(())
135 }
136
137 #[tokio::test]
138 async fn connector_should_query_existing_safe() -> anyhow::Result<()> {
139 let me = ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address();
140 let safe_addr = [1u8; Address::SIZE].into();
141 let safe = DeployedSafe {
142 address: safe_addr,
143 owner: me,
144 module: MODULE_ADDR.into(),
145 registered_nodes: vec![],
146 };
147 let blokli_client = BlokliTestStateBuilder::default()
148 .with_balances([(me, XDaiBalance::new_base(10))])
149 .with_deployed_safes([safe.clone()])
150 .with_hopr_network_chain_info("rotsee")
151 .build_dynamic_client(MODULE_ADDR.into());
152
153 let connector = create_connector(blokli_client)?;
154
155 assert_eq!(Some(safe.clone()), connector.safe_info(SafeSelector::Owner(me)).await?);
156 assert_eq!(Some(safe), connector.safe_info(SafeSelector::Address(safe_addr)).await?);
157
158 insta::assert_yaml_snapshot!(*connector.client.snapshot());
159
160 Ok(())
161 }
162
163 #[tokio::test]
164 async fn connector_should_predict_module_address() -> anyhow::Result<()> {
165 let me = ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address();
166 let safe_addr = [1u8; Address::SIZE].into();
167 let blokli_client = BlokliTestStateBuilder::default()
168 .with_hopr_network_chain_info("rotsee")
169 .build_dynamic_client(MODULE_ADDR.into());
170
171 let connector = create_connector(blokli_client)?;
172
173 assert_eq!(
174 "0xff3dae517c13a59014c79c397de258c9557c04b8",
175 connector.predict_module_address(0, &me, &safe_addr).await?.to_string()
176 );
177
178 Ok(())
179 }
180}