1use std::fmt::{Debug, Display, Formatter};
8
9use async_trait::async_trait;
10use hopr_api::chain::ChainWriteChannelOperations;
11use hopr_internal_types::prelude::*;
12use hopr_primitive_types::prelude::*;
13use serde::{Deserialize, Serialize};
14use serde_with::{DisplayFromStr, serde_as};
15use tracing::info;
16use validator::Validate;
17
18use crate::{
19 Strategy,
20 errors::{StrategyError, StrategyError::CriteriaNotSatisfied},
21 strategy::SingularStrategy,
22};
23
24#[cfg(all(feature = "prometheus", not(test)))]
25lazy_static::lazy_static! {
26 static ref METRIC_COUNT_AUTO_FUNDINGS: hopr_metrics::SimpleCounter =
27 hopr_metrics::SimpleCounter::new("hopr_strategy_auto_funding_funding_count", "Count of initiated automatic fundings").unwrap();
28}
29
30#[serde_as]
32#[derive(Debug, Clone, Copy, PartialEq, Eq, smart_default::SmartDefault, Validate, Serialize, Deserialize)]
33pub struct AutoFundingStrategyConfig {
34 #[serde_as(as = "DisplayFromStr")]
38 #[default(HoprBalance::new_base(1))]
39 pub min_stake_threshold: HoprBalance,
40
41 #[serde_as(as = "DisplayFromStr")]
45 #[default(HoprBalance::new_base(10))]
46 pub funding_amount: HoprBalance,
47}
48
49pub struct AutoFundingStrategy<A> {
52 hopr_chain_actions: A,
53 cfg: AutoFundingStrategyConfig,
54}
55
56impl<A: ChainWriteChannelOperations> 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> 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> 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: ChainWriteChannelOperations + 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 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 channel_id = channel.get_id();
100 let rx = self
101 .hopr_chain_actions
102 .fund_channel(&channel_id, self.cfg.funding_amount)
103 .await
104 .map_err(|e| StrategyError::Other(e.into()))?;
105
106 std::mem::drop(rx); info!(%channel, amount = %self.cfg.funding_amount, "issued re-staking of channel", );
108 }
109 Ok(())
110 } else {
111 Err(CriteriaNotSatisfied)
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use hex_literal::hex;
119 use hopr_api::chain::ChainReceipt;
120
121 use super::*;
122 use crate::{
123 auto_funding::{AutoFundingStrategy, AutoFundingStrategyConfig},
124 strategy::SingularStrategy,
125 tests::{MockChainActions, MockTestActions},
126 };
127
128 lazy_static::lazy_static! {
129 static ref ALICE: Address = hex!("18f8ae833c85c51fbeba29cef9fbfb53b3bad950").into();
130 static ref BOB: Address = hex!("44f23fa14130ca540b37251309700b6c281d972e").into();
131 static ref CHRIS: Address = hex!("b6021e0860dd9d96c9ff0a73e2e5ba3a466ba234").into();
132 static ref DAVE: Address = hex!("68499f50ff68d523385dc60686069935d17d762a").into();
133 }
134
135 #[tokio::test]
136 async fn test_auto_funding_strategy() -> anyhow::Result<()> {
137 let stake_limit = HoprBalance::from(7_u32);
138 let fund_amount = HoprBalance::from(5_u32);
139
140 let c1 = ChannelEntry::new(
141 *ALICE,
142 *BOB,
143 10_u32.into(),
144 0_u32.into(),
145 ChannelStatus::Open,
146 0_u32.into(),
147 );
148
149 let c2 = ChannelEntry::new(
150 *BOB,
151 *CHRIS,
152 5_u32.into(),
153 0_u32.into(),
154 ChannelStatus::Open,
155 0_u32.into(),
156 );
157
158 let c3 = ChannelEntry::new(
159 *CHRIS,
160 *DAVE,
161 5_u32.into(),
162 0_u32.into(),
163 ChannelStatus::PendingToClose(std::time::SystemTime::now()),
164 0_u32.into(),
165 );
166
167 let cfg = AutoFundingStrategyConfig {
168 min_stake_threshold: stake_limit,
169 funding_amount: fund_amount,
170 };
171
172 let mut mock = MockTestActions::new();
173 mock.expect_fund_channel()
174 .once()
175 .with(mockall::predicate::eq(c2.get_id()), mockall::predicate::eq(fund_amount))
176 .return_once(|_, _| Ok(ChainReceipt::default()));
177
178 let afs = AutoFundingStrategy::new(cfg, MockChainActions(mock.into()));
179 afs.on_own_channel_changed(
180 &c1,
181 ChannelDirection::Outgoing,
182 ChannelChange::CurrentBalance {
183 left: HoprBalance::zero(),
184 right: c1.balance,
185 },
186 )
187 .await?;
188
189 afs.on_own_channel_changed(
190 &c2,
191 ChannelDirection::Outgoing,
192 ChannelChange::CurrentBalance {
193 left: HoprBalance::zero(),
194 right: c2.balance,
195 },
196 )
197 .await?;
198
199 afs.on_own_channel_changed(
200 &c3,
201 ChannelDirection::Outgoing,
202 ChannelChange::CurrentBalance {
203 left: HoprBalance::zero(),
204 right: c3.balance,
205 },
206 )
207 .await?;
208
209 Ok(())
210 }
211}