Skip to main content

hopr_chain_connector/connector/
values.rs

1use std::time::Duration;
2
3use blokli_client::api::{BlokliQueryClient, RedeemedStatsSelector};
4use futures::TryFutureExt;
5use hopr_api::{
6    chain::{ChainInfo, DomainSeparators, RedemptionStats},
7    types::{internal::prelude::WinningProbability, primitive::prelude::*},
8};
9
10use crate::{
11    HoprBlockchainReader,
12    connector::HoprBlockchainConnector,
13    errors::ConnectorError,
14    utils::{ParsedChainInfo, model_to_chain_info, model_to_redeemed_stats},
15};
16
17pub(crate) const CHAIN_INFO_CACHE_KEY: u32 = 0;
18
19impl<B, C, P, R> HoprBlockchainConnector<C, R, B, P>
20where
21    C: BlokliQueryClient + Send + Sync + 'static,
22{
23    /// Queries chain info from cache, fetching from Blokli on cold start.
24    ///
25    /// The cache has no TTL - it's kept fresh by the Blokli subscription handler
26    /// which updates it whenever ticket parameters change. This prevents the
27    /// cascading timeout issue where cache expiration during packet processing
28    /// would trigger blocking Blokli queries.
29    pub(crate) async fn query_cached_chain_info(&self) -> Result<ParsedChainInfo, ConnectorError> {
30        Ok(self
31            .values
32            .try_get_with(
33                CHAIN_INFO_CACHE_KEY,
34                self.client
35                    .query_chain_info()
36                    .map_err(ConnectorError::from)
37                    .and_then(|model| futures::future::ready(model_to_chain_info(model))),
38            )
39            .await?)
40    }
41}
42
43#[async_trait::async_trait]
44impl<B, R, C, P> hopr_api::chain::ChainValues for HoprBlockchainConnector<C, B, P, R>
45where
46    B: Send + Sync,
47    C: BlokliQueryClient + Send + Sync + 'static,
48    P: Send + Sync,
49    R: Send + Sync,
50{
51    type Error = ConnectorError;
52
53    // NOTE: these APIs can be called without calling `connect` first
54
55    #[inline]
56    async fn balance<Cy: Currency, A: Into<Address> + Send>(&self, address: A) -> Result<Balance<Cy>, Self::Error> {
57        HoprBlockchainReader(self.client.clone()).balance(address).await
58    }
59
60    async fn domain_separators(&self) -> Result<DomainSeparators, Self::Error> {
61        Ok(self.query_cached_chain_info().await?.domain_separators)
62    }
63
64    async fn minimum_incoming_ticket_win_prob(&self) -> Result<WinningProbability, Self::Error> {
65        Ok(self.query_cached_chain_info().await?.ticket_win_prob)
66    }
67
68    async fn minimum_ticket_price(&self) -> Result<HoprBalance, Self::Error> {
69        Ok(self.query_cached_chain_info().await?.ticket_price)
70    }
71
72    async fn key_binding_fee(&self) -> Result<HoprBalance, Self::Error> {
73        Ok(self.query_cached_chain_info().await?.key_binding_fee)
74    }
75
76    async fn channel_closure_notice_period(&self) -> Result<Duration, Self::Error> {
77        Ok(self.query_cached_chain_info().await?.channel_closure_grace_period)
78    }
79
80    async fn chain_info(&self) -> Result<ChainInfo, Self::Error> {
81        Ok(self.query_cached_chain_info().await?.info)
82    }
83
84    async fn redemption_stats<A: Into<Address> + Send>(&self, safe_addr: A) -> Result<RedemptionStats, Self::Error> {
85        let safe_addr = safe_addr.into();
86        model_to_redeemed_stats(
87            self.client
88                .query_redeemed_stats(RedeemedStatsSelector::SafeAddress(safe_addr.into()))
89                .await?,
90        )
91    }
92
93    async fn typical_resolution_time(&self) -> Result<Duration, Self::Error> {
94        let v = self.query_cached_chain_info().await?;
95        Ok(v.expected_block_time * v.finality)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use std::str::FromStr;
102
103    use hopr_api::{
104        chain::{ChainValues, DeployedSafe},
105        types::{
106            crypto::prelude::*,
107            internal::account::{AccountEntry, AccountType},
108        },
109    };
110
111    use super::*;
112    use crate::{connector::tests::create_connector, testing::BlokliTestStateBuilder};
113
114    #[tokio::test]
115    async fn connector_should_get_balance() -> anyhow::Result<()> {
116        let account = AccountEntry {
117            public_key: *OffchainKeypair::random().public(),
118            chain_addr: [1u8; Address::SIZE].into(),
119            entry_type: AccountType::NotAnnounced,
120            safe_address: Some([2u8; Address::SIZE].into()),
121            key_id: 1.into(),
122        };
123
124        let blokli_client = BlokliTestStateBuilder::default()
125            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
126            .with_safe_allowances([(account.safe_address.unwrap(), HoprBalance::new_base(10000))])
127            .build_static_client();
128
129        let mut connector = create_connector(blokli_client)?;
130        connector.connect().await?;
131
132        assert_eq!(
133            connector.balance(account.safe_address.unwrap()).await?,
134            HoprBalance::new_base(100)
135        );
136        assert_eq!(connector.balance(account.chain_addr).await?, XDaiBalance::new_base(1));
137
138        Ok(())
139    }
140
141    #[tokio::test]
142    async fn connector_should_query_chain_info() -> anyhow::Result<()> {
143        let blokli_client = BlokliTestStateBuilder::default()
144            .with_hopr_network_chain_info("rotsee")
145            .build_static_client();
146
147        let mut connector = create_connector(blokli_client)?;
148        connector.connect().await?;
149
150        let chain_info = connector.chain_info().await?;
151
152        assert_eq!(100, chain_info.chain_id);
153        assert_eq!("rotsee", &chain_info.hopr_network_name);
154
155        assert_eq!(Duration::from_mins(5), connector.channel_closure_notice_period().await?);
156        assert_eq!(Hash::default(), connector.domain_separators().await?.channel);
157        assert_eq!(
158            HoprBalance::from_str("0.01 wxHOPR")?,
159            connector.key_binding_fee().await?
160        );
161
162        Ok(())
163    }
164
165    #[tokio::test]
166    async fn connector_should_query_chain_info_without_calling_connect_first() -> anyhow::Result<()> {
167        let blokli_client = BlokliTestStateBuilder::default()
168            .with_hopr_network_chain_info("rotsee")
169            .build_static_client();
170
171        let connector = create_connector(blokli_client)?;
172
173        let chain_info = connector.chain_info().await?;
174
175        assert_eq!(100, chain_info.chain_id);
176        assert_eq!("rotsee", &chain_info.hopr_network_name);
177
178        assert_eq!(Duration::from_mins(5), connector.channel_closure_notice_period().await?);
179        assert_eq!(Hash::default(), connector.domain_separators().await?.channel);
180        assert_eq!(
181            HoprBalance::from_str("0.01 wxHOPR")?,
182            connector.key_binding_fee().await?
183        );
184
185        Ok(())
186    }
187
188    #[tokio::test]
189    async fn connector_should_retrieve_redeemed_stats() -> anyhow::Result<()> {
190        let blokli_client = BlokliTestStateBuilder::default()
191            .with_deployed_safes([DeployedSafe {
192                address: [1u8; Address::SIZE].into(),
193                owners: vec![[2u8; Address::SIZE].into()],
194                module: [3u8; Address::SIZE].into(),
195                registered_nodes: vec![],
196                deployer: [2u8; Address::SIZE].into(),
197            }])
198            .with_hopr_network_chain_info("rotsee")
199            .build_static_client();
200
201        let connector = create_connector(blokli_client)?;
202
203        let stats = connector.redemption_stats([1u8; Address::SIZE]).await?;
204
205        assert_eq!(0, stats.redeemed_count);
206        assert_eq!(HoprBalance::zero(), stats.redeemed_value);
207
208        Ok(())
209    }
210}