hopr_strategy/
auto_funding.rs

1//! ## Auto Funding Strategy
2//! This strategy listens for channel state change events to check whether a channel has dropped below `min_stake_threshold` HOPR.
3//! If this happens, the strategy issues a **fund channel** transaction to re-stake the channel with `funding_amount` HOPR.
4//!
5//! For details on default parameters see [AutoFundingStrategyConfig].
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_with::{serde_as, DisplayFromStr};
9use std::fmt::{Debug, Display, Formatter};
10use tracing::info;
11use validator::Validate;
12
13use hopr_chain_actions::channels::ChannelActions;
14use hopr_internal_types::prelude::*;
15use hopr_primitive_types::prelude::*;
16
17use crate::errors::StrategyError::CriteriaNotSatisfied;
18use crate::strategy::SingularStrategy;
19use crate::Strategy;
20
21#[cfg(all(feature = "prometheus", not(test)))]
22use hopr_metrics::metrics::SimpleCounter;
23
24#[cfg(all(feature = "prometheus", not(test)))]
25lazy_static::lazy_static! {
26    static ref METRIC_COUNT_AUTO_FUNDINGS: SimpleCounter =
27        SimpleCounter::new("hopr_strategy_auto_funding_funding_count", "Count of initiated automatic fundings").unwrap();
28}
29
30/// Configuration for `AutoFundingStrategy`
31#[serde_as]
32#[derive(Debug, Clone, Copy, PartialEq, Eq, smart_default::SmartDefault, Validate, Serialize, Deserialize)]
33pub struct AutoFundingStrategyConfig {
34    /// Minimum stake that a channel's balance must not go below.
35    ///
36    /// Default is 1 HOPR
37    #[serde_as(as = "DisplayFromStr")]
38    #[default(Balance::new_from_str("1000000000000000000", BalanceType::HOPR))]
39    pub min_stake_threshold: Balance,
40
41    /// Funding amount.
42    ///
43    /// Defaults to 10 HOPR.
44    #[serde_as(as = "DisplayFromStr")]
45    #[default(Balance::new_from_str("10000000000000000000", BalanceType::HOPR))]
46    pub funding_amount: Balance,
47}
48
49/// The `AutoFundingStrategy` automatically funds channel that
50/// dropped it's staked balance below the configured threshold.
51pub struct AutoFundingStrategy<A: ChannelActions> {
52    hopr_chain_actions: A,
53    cfg: AutoFundingStrategyConfig,
54}
55
56impl<A: ChannelActions> AutoFundingStrategy<A> {
57    pub fn new(cfg: AutoFundingStrategyConfig, hopr_chain_actions: A) -> Self {
58        Self {
59            cfg,
60            hopr_chain_actions,
61        }
62    }
63}
64
65impl<A: ChannelActions> Debug for AutoFundingStrategy<A> {
66    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
67        write!(f, "{:?}", Strategy::AutoFunding(self.cfg))
68    }
69}
70
71impl<A: ChannelActions> Display for AutoFundingStrategy<A> {
72    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73        write!(f, "{}", Strategy::AutoFunding(self.cfg))
74    }
75}
76
77#[async_trait]
78impl<A: ChannelActions + Send + Sync> SingularStrategy for AutoFundingStrategy<A> {
79    async fn on_own_channel_changed(
80        &self,
81        channel: &ChannelEntry,
82        direction: ChannelDirection,
83        change: ChannelChange,
84    ) -> crate::errors::Result<()> {
85        // Can only auto-fund outgoing channels
86        if direction != ChannelDirection::Outgoing {
87            return Ok(());
88        }
89
90        if let ChannelChange::CurrentBalance { right: new, .. } = change {
91            if new.lt(&self.cfg.min_stake_threshold) && channel.status == ChannelStatus::Open {
92                info!(%channel, balance = %channel.balance, threshold = %self.cfg.min_stake_threshold,
93                    "stake on channel is below threshold",
94                );
95
96                #[cfg(all(feature = "prometheus", not(test)))]
97                METRIC_COUNT_AUTO_FUNDINGS.increment();
98
99                let rx = self
100                    .hopr_chain_actions
101                    .fund_channel(channel.get_id(), self.cfg.funding_amount)
102                    .await?;
103                std::mem::drop(rx); // The Receiver is not intentionally awaited here and the oneshot Sender can fail safely
104                info!(%channel, amount = %self.cfg.funding_amount, "issued re-staking of channel", );
105            }
106            Ok(())
107        } else {
108            Err(CriteriaNotSatisfied)
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use crate::auto_funding::{AutoFundingStrategy, AutoFundingStrategyConfig};
116    use crate::strategy::SingularStrategy;
117    use async_trait::async_trait;
118    use futures::{future::ok, FutureExt};
119    use hex_literal::hex;
120    use hopr_chain_actions::action_queue::{ActionConfirmation, PendingAction};
121    use hopr_chain_actions::channels::ChannelActions;
122    use hopr_chain_types::actions::Action;
123    use hopr_chain_types::chain_events::ChainEventType;
124    use hopr_crypto_random::random_bytes;
125    use hopr_crypto_types::types::Hash;
126    use hopr_internal_types::prelude::*;
127    use hopr_primitive_types::prelude::*;
128    use mockall::mock;
129
130    lazy_static::lazy_static! {
131        static ref ALICE: Address = hex!("18f8ae833c85c51fbeba29cef9fbfb53b3bad950").into();
132        static ref BOB: Address = hex!("44f23fa14130ca540b37251309700b6c281d972e").into();
133        static ref CHRIS: Address = hex!("b6021e0860dd9d96c9ff0a73e2e5ba3a466ba234").into();
134        static ref DAVE: Address = hex!("68499f50ff68d523385dc60686069935d17d762a").into();
135    }
136
137    mock! {
138        ChannelAct { }
139        #[async_trait]
140        impl ChannelActions for ChannelAct {
141            async fn open_channel(&self, destination: Address, amount: Balance) -> hopr_chain_actions::errors::Result<PendingAction>;
142            async fn fund_channel(&self, channel_id: Hash, amount: Balance) -> hopr_chain_actions::errors::Result<PendingAction>;
143            async fn close_channel(
144                &self,
145                counterparty: Address,
146                direction: ChannelDirection,
147                redeem_before_close: bool,
148            ) -> hopr_chain_actions::errors::Result<PendingAction>;
149        }
150    }
151
152    fn mock_action_confirmation(channel: ChannelEntry, balance: Balance) -> ActionConfirmation {
153        let random_hash = Hash::from(random_bytes::<{ Hash::SIZE }>());
154        ActionConfirmation {
155            tx_hash: random_hash,
156            event: Some(ChainEventType::ChannelBalanceIncreased(channel, balance)),
157            action: Action::FundChannel(channel, balance),
158        }
159    }
160
161    #[async_std::test]
162    async fn test_auto_funding_strategy() -> anyhow::Result<()> {
163        let stake_limit = Balance::new(7_u32, BalanceType::HOPR);
164        let fund_amount = Balance::new(5_u32, BalanceType::HOPR);
165
166        let c1 = ChannelEntry::new(
167            *ALICE,
168            *BOB,
169            Balance::new(10_u32, BalanceType::HOPR),
170            0_u32.into(),
171            ChannelStatus::Open,
172            0_u32.into(),
173        );
174
175        let c2 = ChannelEntry::new(
176            *BOB,
177            *CHRIS,
178            Balance::new(5_u32, BalanceType::HOPR),
179            0_u32.into(),
180            ChannelStatus::Open,
181            0_u32.into(),
182        );
183
184        let c3 = ChannelEntry::new(
185            *CHRIS,
186            *DAVE,
187            Balance::new(5_u32, BalanceType::HOPR),
188            0_u32.into(),
189            ChannelStatus::PendingToClose(std::time::SystemTime::now()),
190            0_u32.into(),
191        );
192
193        let mut actions = MockChannelAct::new();
194        let fund_amount_c = fund_amount.clone();
195        actions
196            .expect_fund_channel()
197            .times(1)
198            .withf(move |h, balance| c2.get_id().eq(h) && fund_amount_c.eq(&balance))
199            .return_once(move |_, _| Ok(ok(mock_action_confirmation(c2, fund_amount)).boxed()));
200
201        let cfg = AutoFundingStrategyConfig {
202            min_stake_threshold: stake_limit,
203            funding_amount: fund_amount,
204        };
205
206        let afs = AutoFundingStrategy::new(cfg, actions);
207        afs.on_own_channel_changed(
208            &c1,
209            ChannelDirection::Outgoing,
210            ChannelChange::CurrentBalance {
211                left: Balance::zero(BalanceType::HOPR),
212                right: c1.balance,
213            },
214        )
215        .await?;
216
217        afs.on_own_channel_changed(
218            &c2,
219            ChannelDirection::Outgoing,
220            ChannelChange::CurrentBalance {
221                left: Balance::zero(BalanceType::HOPR),
222                right: c2.balance,
223            },
224        )
225        .await?;
226
227        afs.on_own_channel_changed(
228            &c3,
229            ChannelDirection::Outgoing,
230            ChannelChange::CurrentBalance {
231                left: Balance::zero(BalanceType::HOPR),
232                right: c3.balance,
233            },
234        )
235        .await?;
236
237        Ok(())
238    }
239}