1use 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#[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(Balance::new_from_str("1000000000000000000", BalanceType::HOPR))]
39 pub min_stake_threshold: Balance,
40
41 #[serde_as(as = "DisplayFromStr")]
45 #[default(Balance::new_from_str("10000000000000000000", BalanceType::HOPR))]
46 pub funding_amount: Balance,
47}
48
49pub 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 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); 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}