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