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::{
6 chain::{ChainReceipt, ChainValues, DeployedSafe, SafeSelector},
7 types::{chain::prelude::PayloadGenerator, crypto::prelude::Keypair, primitive::prelude::*},
8};
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
63const DEPLOY_SAFE_CUSTOM_TX_TIMEOUT_MULTIPLIER: u32 = 8;
64
65#[async_trait::async_trait]
66impl<B, C, P> hopr_api::chain::ChainWriteSafeOperations for HoprBlockchainConnector<C, B, P, P::TxRequest>
67where
68 B: Send + Sync + 'static,
69 C: BlokliQueryClient + BlokliTransactionClient + Send + Sync + 'static,
70 P: PayloadGenerator + Send + Sync + 'static,
71 P::TxRequest: Send + Sync + 'static,
72{
73 type Error = ConnectorError;
74
75 async fn deploy_safe<'a>(
76 &'a self,
77 balance: HoprBalance,
78 ) -> Result<BoxFuture<'a, Result<ChainReceipt, Self::Error>>, Self::Error> {
79 let admin = self.chain_key.public().to_address();
80 if self
81 .client
82 .query_safe(blokli_client::api::SafeSelector::ChainKey(admin.into()))
83 .await?
84 .is_some()
85 {
86 return Err(ConnectorError::InvalidState("safe already deployed for this signer"));
87 }
88
89 if self.balance(admin).await? < balance {
90 return Err(ConnectorError::InvalidState("insufficient token balance at the signer"));
91 }
92
93 let tx_req = self.payload_generator.deploy_safe(
94 balance,
95 &[admin],
96 true,
97 hopr_api::types::crypto_random::random_bytes(),
98 )?;
99 tracing::debug!(%balance, %admin, "deploying safe");
100
101 Ok(self
102 .send_tx(tx_req, DEPLOY_SAFE_CUSTOM_TX_TIMEOUT_MULTIPLIER.into())
103 .await?
104 .boxed())
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use hopr_api::{
111 chain::ChainReadSafeOperations,
112 types::{crypto::prelude::*, internal::prelude::*},
113 };
114
115 use super::*;
116 use crate::{
117 connector::tests::{MODULE_ADDR, PRIVATE_KEY_1, create_connector},
118 testing::BlokliTestStateBuilder,
119 };
120
121 #[tokio::test]
122 async fn connector_should_safe_allowance() -> anyhow::Result<()> {
123 let account = AccountEntry {
124 public_key: *OffchainKeypair::random().public(),
125 chain_addr: [1u8; Address::SIZE].into(),
126 entry_type: AccountType::NotAnnounced,
127 safe_address: Some([2u8; Address::SIZE].into()),
128 key_id: 1.into(),
129 };
130
131 let blokli_client = BlokliTestStateBuilder::default()
132 .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
133 .with_safe_allowances([(account.safe_address.unwrap(), HoprBalance::new_base(10000))])
134 .build_static_client();
135
136 let connector = create_connector(blokli_client)?;
137
138 assert_eq!(
139 connector.safe_allowance(account.safe_address.unwrap()).await?,
140 HoprBalance::new_base(10000)
141 );
142
143 Ok(())
144 }
145
146 #[tokio::test]
147 async fn connector_should_query_existing_safe() -> anyhow::Result<()> {
148 let me = ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address();
149 let safe_addr = [1u8; Address::SIZE].into();
150 let safe = DeployedSafe {
151 address: safe_addr,
152 owner: me,
153 module: MODULE_ADDR.into(),
154 registered_nodes: vec![],
155 };
156 let blokli_client = BlokliTestStateBuilder::default()
157 .with_balances([(me, XDaiBalance::new_base(10))])
158 .with_deployed_safes([safe.clone()])
159 .with_hopr_network_chain_info("rotsee")
160 .build_dynamic_client(MODULE_ADDR.into());
161
162 let connector = create_connector(blokli_client)?;
163
164 assert_eq!(Some(safe.clone()), connector.safe_info(SafeSelector::Owner(me)).await?);
165 assert_eq!(Some(safe), connector.safe_info(SafeSelector::Address(safe_addr)).await?);
166
167 insta::assert_yaml_snapshot!(*connector.client.snapshot());
168
169 Ok(())
170 }
171
172 #[tokio::test]
173 async fn connector_should_predict_module_address() -> anyhow::Result<()> {
174 let me = ChainKeypair::from_secret(&PRIVATE_KEY_1)?.public().to_address();
175 let safe_addr = [1u8; Address::SIZE].into();
176 let blokli_client = BlokliTestStateBuilder::default()
177 .with_hopr_network_chain_info("rotsee")
178 .build_dynamic_client(MODULE_ADDR.into());
179
180 let connector = create_connector(blokli_client)?;
181
182 assert_eq!(
183 "0xff3dae517c13a59014c79c397de258c9557c04b8",
184 connector.predict_module_address(0, &me, &safe_addr).await?.to_string()
185 );
186
187 Ok(())
188 }
189}