Skip to main content

hopr_test_stubs/
lib.rs

1use std::sync::Arc;
2
3use bimap::BiMap;
4use futures::stream::{self, BoxStream};
5use hopr_api::{
6    chain::*,
7    types::{
8        crypto::prelude::{Hash, OffchainPublicKey},
9        internal::prelude::*,
10        primitive::{
11            balance::{Balance, Currency, HoprBalance},
12            prelude::{Address, KeyIdMapping},
13        },
14    },
15};
16
17/// Error type for test stubs — never constructed, exists only to satisfy trait bounds.
18#[derive(Debug, thiserror::Error)]
19#[error("stub error")]
20pub struct StubError;
21
22// ---------------------------------------------------------------------------
23// StubKeyIdMapper
24// ---------------------------------------------------------------------------
25
26/// Bidirectional mapping between [`HoprKeyIdent`] and [`OffchainPublicKey`].
27///
28/// Extracted from the test helper in `transport/path/src/planner.rs`.
29#[derive(Clone)]
30pub struct StubKeyIdMapper {
31    map: Arc<BiMap<OffchainPublicKey, HoprKeyIdent>>,
32}
33
34impl KeyIdMapping<HoprKeyIdent, OffchainPublicKey> for StubKeyIdMapper {
35    fn map_key_to_id(&self, key: &OffchainPublicKey) -> Option<HoprKeyIdent> {
36        self.map.get_by_left(key).copied()
37    }
38
39    fn map_id_to_public(&self, id: &HoprKeyIdent) -> Option<OffchainPublicKey> {
40        self.map.get_by_right(id).copied()
41    }
42}
43
44// ---------------------------------------------------------------------------
45// StubChainApi
46// ---------------------------------------------------------------------------
47
48/// Lightweight in-memory stub implementing the chain-API traits needed by
49/// `HoprEncoder` / `HoprDecoder` / `HoprTicketProcessor`.
50///
51/// All lookups are simple BiMap or Vec scans — no async I/O, no database.
52#[derive(Clone)]
53pub struct StubChainApi {
54    me: Address,
55    key_addr_map: BiMap<OffchainPublicKey, Address>,
56    channels: Vec<ChannelEntry>,
57    id_mapper: StubKeyIdMapper,
58    ticket_price: HoprBalance,
59    win_prob: WinningProbability,
60}
61
62/// Builder for [`StubChainApi`].
63pub struct StubChainApiBuilder {
64    me: Option<Address>,
65    key_addr_map: BiMap<OffchainPublicKey, Address>,
66    key_id_map: BiMap<OffchainPublicKey, HoprKeyIdent>,
67    channels: Vec<ChannelEntry>,
68    ticket_price: HoprBalance,
69    win_prob: WinningProbability,
70    next_key_id: u32,
71}
72
73impl Default for StubChainApiBuilder {
74    fn default() -> Self {
75        Self {
76            me: None,
77            key_addr_map: BiMap::new(),
78            key_id_map: BiMap::new(),
79            channels: Vec::new(),
80            ticket_price: HoprBalance::zero(),
81            win_prob: WinningProbability::ALWAYS,
82            next_key_id: 0,
83        }
84    }
85}
86
87impl StubChainApiBuilder {
88    /// Sets the "self" address of this stub node.
89    pub fn me(mut self, addr: Address) -> Self {
90        self.me = Some(addr);
91        self
92    }
93
94    /// Registers a single peer (offchain key ↔ chain address + key-id mapping).
95    pub fn peer(mut self, offchain: &OffchainPublicKey, chain_addr: Address) -> Self {
96        self.key_addr_map.insert(*offchain, chain_addr);
97        self.key_id_map.insert(*offchain, self.next_key_id.into());
98        self.next_key_id += 1;
99        self
100    }
101
102    /// Adds a channel entry.
103    pub fn channel(mut self, entry: ChannelEntry) -> Self {
104        self.channels.push(entry);
105        self
106    }
107
108    /// Sets the default outgoing ticket price.
109    pub fn ticket_price(mut self, price: HoprBalance) -> Self {
110        self.ticket_price = price;
111        self
112    }
113
114    /// Sets the default winning probability.
115    pub fn win_prob(mut self, prob: WinningProbability) -> Self {
116        self.win_prob = prob;
117        self
118    }
119
120    /// Finalizes the builder into a [`StubChainApi`].
121    ///
122    /// # Panics
123    /// Panics if `me` was not set.
124    pub fn build(self) -> StubChainApi {
125        StubChainApi {
126            me: self.me.expect("me address must be set"),
127            key_addr_map: self.key_addr_map,
128            channels: self.channels,
129            id_mapper: StubKeyIdMapper {
130                map: Arc::new(self.key_id_map),
131            },
132            ticket_price: self.ticket_price,
133            win_prob: self.win_prob,
134        }
135    }
136}
137
138impl StubChainApi {
139    /// Returns a new [`StubChainApiBuilder`].
140    pub fn builder() -> StubChainApiBuilder {
141        StubChainApiBuilder::default()
142    }
143
144    /// Returns the key-address mapping.
145    pub fn key_addr_map(&self) -> &BiMap<OffchainPublicKey, Address> {
146        &self.key_addr_map
147    }
148
149    /// Returns the registered channels.
150    pub fn channels(&self) -> &[ChannelEntry] {
151        &self.channels
152    }
153}
154
155// -- ChainKeyOperations -----------------------------------------------------
156
157impl ChainKeyOperations for StubChainApi {
158    type Error = StubError;
159    type Mapper = StubKeyIdMapper;
160
161    fn chain_key_to_packet_key(&self, chain: &Address) -> Result<Option<OffchainPublicKey>, Self::Error> {
162        Ok(self.key_addr_map.get_by_right(chain).copied())
163    }
164
165    fn packet_key_to_chain_key(&self, packet: &OffchainPublicKey) -> Result<Option<Address>, Self::Error> {
166        Ok(self.key_addr_map.get_by_left(packet).copied())
167    }
168
169    fn key_id_mapper_ref(&self) -> &Self::Mapper {
170        &self.id_mapper
171    }
172}
173
174// -- ChainReadChannelOperations ---------------------------------------------
175
176impl ChainReadChannelOperations for StubChainApi {
177    type Error = StubError;
178
179    fn me(&self) -> &Address {
180        &self.me
181    }
182
183    fn channel_by_id(&self, channel_id: &ChannelId) -> Result<Option<ChannelEntry>, Self::Error> {
184        Ok(self.channels.iter().find(|c| c.get_id() == channel_id).cloned())
185    }
186
187    fn stream_channels<'a>(&'a self, _selector: ChannelSelector) -> Result<BoxStream<'a, ChannelEntry>, Self::Error> {
188        Ok(Box::pin(stream::iter(self.channels.clone())))
189    }
190}
191
192impl ChainReadTicketOperations for StubChainApi {
193    type Error = StubError;
194
195    fn outgoing_ticket_values(
196        &self,
197        configured_wp: Option<WinningProbability>,
198        configured_price: Option<HoprBalance>,
199    ) -> Result<(WinningProbability, HoprBalance), Self::Error> {
200        Ok((
201            configured_wp.unwrap_or(self.win_prob),
202            configured_price.unwrap_or(self.ticket_price),
203        ))
204    }
205
206    fn incoming_ticket_values(&self) -> Result<(WinningProbability, HoprBalance), Self::Error> {
207        Ok((self.win_prob, self.ticket_price))
208    }
209}
210
211// -- ChainValues ------------------------------------------------------------
212
213#[async_trait::async_trait]
214impl ChainValues for StubChainApi {
215    type Error = StubError;
216
217    async fn balance<C: Currency, A: Into<Address> + Send>(&self, _address: A) -> Result<Balance<C>, Self::Error> {
218        Ok(Balance::new_base(1_000_000))
219    }
220
221    async fn domain_separators(&self) -> Result<DomainSeparators, Self::Error> {
222        Ok(DomainSeparators {
223            ledger: Hash::default(),
224            safe_registry: Hash::default(),
225            channel: Hash::default(),
226        })
227    }
228
229    async fn minimum_incoming_ticket_win_prob(&self) -> Result<WinningProbability, Self::Error> {
230        Ok(self.win_prob)
231    }
232
233    async fn minimum_ticket_price(&self) -> Result<HoprBalance, Self::Error> {
234        Ok(self.ticket_price)
235    }
236
237    async fn key_binding_fee(&self) -> Result<HoprBalance, Self::Error> {
238        Ok(HoprBalance::zero())
239    }
240
241    async fn channel_closure_notice_period(&self) -> Result<std::time::Duration, Self::Error> {
242        Ok(std::time::Duration::from_secs(300))
243    }
244
245    async fn chain_info(&self) -> Result<ChainInfo, Self::Error> {
246        Ok(ChainInfo {
247            chain_id: 100,
248            hopr_network_name: "stub".to_string(),
249            contract_addresses: ContractAddresses::default(),
250        })
251    }
252
253    async fn redemption_stats<A: Into<Address> + Send>(&self, _: A) -> Result<RedemptionStats, Self::Error> {
254        Ok(RedemptionStats {
255            redeemed_count: 0,
256            redeemed_value: HoprBalance::zero(),
257        })
258    }
259
260    async fn typical_resolution_time(&self) -> Result<std::time::Duration, Self::Error> {
261        Ok(std::time::Duration::from_secs(15))
262    }
263}
264
265// ---------------------------------------------------------------------------
266// StubPathResolver
267// ---------------------------------------------------------------------------
268
269/// Minimal stub for [`PathAddressResolver`], using the same BiMap lookups
270/// as the `MockPathResolver` in `transport/protocol/tests/common/mod.rs`.
271pub struct StubPathResolver {
272    key_addr_map: BiMap<OffchainPublicKey, Address>,
273    channels: Vec<ChannelEntry>,
274}
275
276impl StubPathResolver {
277    /// Creates a new resolver sharing key/channel data with the given [`StubChainApi`].
278    pub fn from_chain_api(api: &StubChainApi) -> Self {
279        Self {
280            key_addr_map: api.key_addr_map().clone(),
281            channels: api.channels().to_vec(),
282        }
283    }
284}
285
286#[async_trait::async_trait]
287impl PathAddressResolver for StubPathResolver {
288    async fn resolve_transport_address(
289        &self,
290        address: &Address,
291    ) -> Result<Option<OffchainPublicKey>, hopr_api::types::internal::errors::PathError> {
292        Ok(self.key_addr_map.get_by_right(address).copied())
293    }
294
295    async fn resolve_chain_address(
296        &self,
297        key: &OffchainPublicKey,
298    ) -> Result<Option<Address>, hopr_api::types::internal::errors::PathError> {
299        Ok(self.key_addr_map.get_by_left(key).copied())
300    }
301
302    async fn get_channel(
303        &self,
304        src: &Address,
305        dst: &Address,
306    ) -> Result<Option<ChannelEntry>, hopr_api::types::internal::errors::PathError> {
307        Ok(self
308            .channels
309            .iter()
310            .find(|c| &c.source == src && &c.destination == dst)
311            .cloned())
312    }
313}