Skip to main content

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::{
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    // 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
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}