hopr_chain_connector/connector/
safe.rs

1use 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    // NOTE: these APIs can be called without calling `connect` first
23
24    #[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}