1use std::{
2 pin::Pin,
3 task::{Context, Poll},
4 time::Duration,
5};
6
7use hopr_api::{
8 chain::ChainValues,
9 types::primitive::{
10 balance::{Balance, Currency},
11 prelude::Address,
12 },
13};
14
15pub(crate) async fn wait_for_balance<C: Currency, R: ChainValues>(
19 min_balance: Balance<C>,
20 max_delay: Duration,
21 address: Address,
22 resolver: &R,
23) -> Result<(), ()> {
24 let multiplier = 1.05;
25 let mut current_delay = Duration::from_secs(2).min(max_delay);
26
27 while current_delay <= max_delay {
28 match resolver.balance::<C, _>(address).await {
29 Ok(current_balance) => {
30 tracing::info!(%address, balance = %current_balance, "balance status");
31 if current_balance.ge(&min_balance) {
32 return Ok(());
33 } else {
34 tracing::warn!(%address, "still underfunded, trying again soon");
35 }
36 }
37 Err(error) => tracing::error!(%address, %error, "failed to fetch balance from the chain"),
38 }
39
40 hopr_utils::runtime::prelude::sleep(current_delay).await;
41 current_delay = current_delay.mul_f64(multiplier);
42 }
43
44 Err(())
45}
46
47#[cfg(test)]
48mod tests {
49 use std::{
50 sync::atomic::{AtomicUsize, Ordering},
51 time::Duration,
52 };
53
54 use hopr_api::{
55 chain::{ChainInfo, ChainValues, DomainSeparators, RedemptionStats, WinningProbability},
56 types::primitive::{
57 balance::{Balance, Currency},
58 prelude::{Address, HoprBalance},
59 },
60 };
61
62 use super::wait_for_balance;
63
64 #[derive(Debug, thiserror::Error)]
65 #[error("mock chain error")]
66 struct MockError;
67
68 #[derive(Default)]
74 struct MockChain {
75 calls: AtomicUsize,
76 fail_first: usize,
77 bump_after: usize,
78 initial_base: u64,
79 funded_base: u64,
80 }
81
82 impl MockChain {
83 fn call_count(&self) -> usize {
85 self.calls.load(Ordering::SeqCst)
86 }
87 }
88
89 #[async_trait::async_trait]
90 impl ChainValues for MockChain {
91 type Error = MockError;
92
93 async fn balance<C: Currency, A: Into<Address> + Send>(&self, _address: A) -> Result<Balance<C>, Self::Error> {
94 let n = self.calls.fetch_add(1, Ordering::SeqCst);
95 if n < self.fail_first {
96 return Err(MockError);
97 }
98 let base = if n >= self.bump_after {
99 self.funded_base
100 } else {
101 self.initial_base
102 };
103 Ok(Balance::<C>::new_base(base))
104 }
105
106 async fn domain_separators(&self) -> Result<DomainSeparators, Self::Error> {
107 unimplemented!("not used by wait_for_balance")
108 }
109
110 async fn minimum_incoming_ticket_win_prob(&self) -> Result<WinningProbability, Self::Error> {
111 unimplemented!("not used by wait_for_balance")
112 }
113
114 async fn minimum_ticket_price(&self) -> Result<HoprBalance, Self::Error> {
115 unimplemented!("not used by wait_for_balance")
116 }
117
118 async fn key_binding_fee(&self) -> Result<HoprBalance, Self::Error> {
119 unimplemented!("not used by wait_for_balance")
120 }
121
122 async fn channel_closure_notice_period(&self) -> Result<Duration, Self::Error> {
123 unimplemented!("not used by wait_for_balance")
124 }
125
126 async fn chain_info(&self) -> Result<ChainInfo, Self::Error> {
127 unimplemented!("not used by wait_for_balance")
128 }
129
130 async fn redemption_stats<A: Into<Address> + Send>(
131 &self,
132 _safe_addr: A,
133 ) -> Result<RedemptionStats, Self::Error> {
134 unimplemented!("not used by wait_for_balance")
135 }
136
137 async fn typical_resolution_time(&self) -> Result<Duration, Self::Error> {
138 unimplemented!("not used by wait_for_balance")
139 }
140 }
141
142 #[tokio::test(start_paused = true)]
143 async fn returns_immediately_when_safe_is_already_funded() {
144 let chain = MockChain {
145 initial_base: 100,
146 funded_base: 100,
147 ..Default::default()
148 };
149
150 let result = wait_for_balance(
151 HoprBalance::new_base(50),
152 Duration::from_secs(200),
153 Address::default(),
154 &chain,
155 )
156 .await;
157
158 assert!(result.is_ok());
159 assert_eq!(chain.call_count(), 1);
161 }
162
163 #[tokio::test(start_paused = true)]
164 async fn errors_when_safe_stays_underfunded_until_deadline() {
165 let chain = MockChain {
166 initial_base: 10,
167 funded_base: 10,
168 ..Default::default()
169 };
170
171 let result = wait_for_balance(
172 HoprBalance::new_base(50),
173 Duration::from_secs(200),
174 Address::default(),
175 &chain,
176 )
177 .await;
178
179 assert_eq!(result, Err(()), "expected the wait to time out, got {result:?}");
180 }
181
182 #[tokio::test(start_paused = true)]
183 async fn succeeds_after_the_safe_gets_topped_up() {
184 let chain = MockChain {
186 initial_base: 10,
187 funded_base: 100,
188 bump_after: 2,
189 ..Default::default()
190 };
191
192 let result = wait_for_balance(
193 HoprBalance::new_base(50),
194 Duration::from_secs(200),
195 Address::default(),
196 &chain,
197 )
198 .await;
199
200 assert!(result.is_ok());
201 assert_eq!(chain.call_count(), chain.bump_after + 1);
203 }
204
205 #[tokio::test(start_paused = true)]
206 async fn tolerates_transient_fetch_errors_then_succeeds() {
207 let chain = MockChain {
209 initial_base: 100,
210 funded_base: 100,
211 fail_first: 1,
212 ..Default::default()
213 };
214
215 let result = wait_for_balance(
216 HoprBalance::new_base(50),
217 Duration::from_secs(200),
218 Address::default(),
219 &chain,
220 )
221 .await;
222
223 assert!(result.is_ok());
224 assert_eq!(chain.call_count(), chain.fail_first + 1);
226 }
227}
228
229#[derive(Clone)]
230pub struct BroadcastSenderSink<T>(pub async_broadcast::Sender<T>);
231
232impl<T: Clone> futures::Sink<T> for BroadcastSenderSink<T> {
233 type Error = async_broadcast::TrySendError<T>;
234
235 fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
236 Poll::Ready(Ok(()))
237 }
238
239 fn start_send(self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
240 self.0.try_broadcast(item).map(|_| ())
241 }
242
243 fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
244 Poll::Ready(Ok(()))
245 }
246
247 fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
248 self.0.close();
249 Poll::Ready(Ok(()))
250 }
251}