Skip to main content

hopr_chain_connector/connector/
values.rs

1use std::time::Duration;
2
3use blokli_client::api::BlokliQueryClient;
4use futures::TryFutureExt;
5use hopr_api::{
6    chain::{ChainInfo, DomainSeparators},
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},
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
85#[cfg(test)]
86mod tests {
87    use std::str::FromStr;
88
89    use hopr_api::{
90        chain::ChainValues,
91        types::{
92            crypto::prelude::*,
93            internal::account::{AccountEntry, AccountType},
94        },
95    };
96
97    use super::*;
98    use crate::{connector::tests::create_connector, testing::BlokliTestStateBuilder};
99
100    #[tokio::test]
101    async fn connector_should_get_balance() -> anyhow::Result<()> {
102        let account = AccountEntry {
103            public_key: *OffchainKeypair::random().public(),
104            chain_addr: [1u8; Address::SIZE].into(),
105            entry_type: AccountType::NotAnnounced,
106            safe_address: Some([2u8; Address::SIZE].into()),
107            key_id: 1.into(),
108        };
109
110        let blokli_client = BlokliTestStateBuilder::default()
111            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
112            .with_safe_allowances([(account.safe_address.unwrap(), HoprBalance::new_base(10000))])
113            .build_static_client();
114
115        let mut connector = create_connector(blokli_client)?;
116        connector.connect().await?;
117
118        assert_eq!(
119            connector.balance(account.safe_address.unwrap()).await?,
120            HoprBalance::new_base(100)
121        );
122        assert_eq!(connector.balance(account.chain_addr).await?, XDaiBalance::new_base(1));
123
124        Ok(())
125    }
126
127    #[tokio::test]
128    async fn connector_should_query_chain_info() -> anyhow::Result<()> {
129        let blokli_client = BlokliTestStateBuilder::default()
130            .with_hopr_network_chain_info("rotsee")
131            .build_static_client();
132
133        let mut connector = create_connector(blokli_client)?;
134        connector.connect().await?;
135
136        let chain_info = connector.chain_info().await?;
137
138        assert_eq!(100, chain_info.chain_id);
139        assert_eq!("rotsee", &chain_info.hopr_network_name);
140
141        assert_eq!(Duration::from_mins(5), connector.channel_closure_notice_period().await?);
142        assert_eq!(HoprBalance::new_base(1), connector.minimum_ticket_price().await?);
143        assert!(WinningProbability::ALWAYS.approx_eq(&connector.minimum_incoming_ticket_win_prob().await?));
144        assert_eq!(Hash::default(), connector.domain_separators().await?.channel);
145        assert_eq!(
146            HoprBalance::from_str("0.01 wxHOPR")?,
147            connector.key_binding_fee().await?
148        );
149
150        Ok(())
151    }
152
153    #[tokio::test]
154    async fn connector_should_query_chain_info_without_calling_connect_first() -> anyhow::Result<()> {
155        let blokli_client = BlokliTestStateBuilder::default()
156            .with_hopr_network_chain_info("rotsee")
157            .build_static_client();
158
159        let connector = create_connector(blokli_client)?;
160
161        let chain_info = connector.chain_info().await?;
162
163        assert_eq!(100, chain_info.chain_id);
164        assert_eq!("rotsee", &chain_info.hopr_network_name);
165
166        assert_eq!(Duration::from_mins(5), connector.channel_closure_notice_period().await?);
167        assert_eq!(HoprBalance::new_base(1), connector.minimum_ticket_price().await?);
168        assert!(WinningProbability::ALWAYS.approx_eq(&connector.minimum_incoming_ticket_win_prob().await?));
169        assert_eq!(Hash::default(), connector.domain_separators().await?.channel);
170        assert_eq!(
171            HoprBalance::from_str("0.01 wxHOPR")?,
172            connector.key_binding_fee().await?
173        );
174
175        Ok(())
176    }
177}