1pub mod backoff_util;
2pub mod broadcaststream;
4#[cfg(not(target_family = "wasm"))]
5mod secret_file;
6pub mod update_merge;
7use std::convert::Infallible;
8use std::fmt::{Debug, Display, Formatter};
9use std::future::Future;
10use std::hash::Hash;
11use std::io::Write;
12use std::path::Path;
13use std::pin::Pin;
14use std::str::FromStr;
15use std::sync::LazyLock;
16use std::{fs, io};
17
18use fedimint_logging::LOG_CORE;
19pub use fedimint_util_error::*;
20use futures::StreamExt;
21#[cfg(not(target_family = "wasm"))]
22pub use secret_file::{SecretInputError, ensure_single_stdin, read_secret_file};
23use serde::{Deserialize, Serialize};
24use tokio::io::AsyncWriteExt;
25use tracing::{Instrument, Span, debug, warn};
26use url::{Host, ParseError, Url};
27
28use crate::envs::{FM_DEBUG_SHOW_SECRETS_ENV, is_env_var_set};
29use crate::task::MaybeSend;
30use crate::{apply, async_trait_maybe_send, maybe_add_send, runtime};
31
32pub type BoxFuture<'a, T> = Pin<Box<maybe_add_send!(dyn Future<Output = T> + 'a)>>;
34
35pub type BoxStream<'a, T> = Pin<Box<maybe_add_send!(dyn futures::Stream<Item = T> + 'a)>>;
37
38#[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)]
40#[error("Stream was unexpectedly closed")]
41pub struct StreamEndedError;
42
43#[apply(async_trait_maybe_send!)]
44pub trait NextOrPending {
45 type Output;
46
47 async fn next_or_pending(&mut self) -> Self::Output;
48
49 async fn ok(&mut self) -> Result<Self::Output, StreamEndedError>;
50}
51
52#[apply(async_trait_maybe_send!)]
53impl<S> NextOrPending for S
54where
55 S: futures::Stream + Unpin + MaybeSend,
56 S::Item: MaybeSend,
57{
58 type Output = S::Item;
59
60 async fn ok(&mut self) -> Result<Self::Output, StreamEndedError> {
63 self.next().await.ok_or(StreamEndedError)
64 }
65
66 async fn next_or_pending(&mut self) -> Self::Output {
71 if let Some(item) = self.next().await {
72 item
73 } else {
74 debug!(target: LOG_CORE, "Stream ended in next_or_pending, pending forever to avoid throwing an error on shutdown");
75 std::future::pending().await
76 }
77 }
78}
79
80#[derive(Hash, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
90pub struct SafeUrl(Url);
92
93impl SafeUrl {
94 pub fn parse(url_str: &str) -> Result<Self, ParseError> {
95 Url::parse(url_str).map(SafeUrl)
96 }
97
98 pub fn to_unsafe(self) -> Url {
101 self.0
102 }
103
104 #[allow(clippy::result_unit_err)] pub fn set_username(&mut self, username: &str) -> Result<(), ()> {
106 self.0.set_username(username)
107 }
108
109 #[allow(clippy::result_unit_err)] pub fn set_password(&mut self, password: Option<&str>) -> Result<(), ()> {
111 self.0.set_password(password)
112 }
113
114 #[allow(clippy::result_unit_err)] pub fn without_auth(&self) -> Result<Self, ()> {
116 let mut url = self.clone();
117
118 url.set_username("").and_then(|()| url.set_password(None))?;
119
120 Ok(url)
121 }
122
123 pub fn host(&self) -> Option<Host<&str>> {
124 self.0.host()
125 }
126 pub fn host_str(&self) -> Option<&str> {
127 self.0.host_str()
128 }
129 pub fn scheme(&self) -> &str {
130 self.0.scheme()
131 }
132 pub fn port(&self) -> Option<u16> {
133 self.0.port()
134 }
135 pub fn port_or_known_default(&self) -> Option<u16> {
136 self.0.port_or_known_default()
137 }
138 pub fn path(&self) -> &str {
139 self.0.path()
140 }
141 pub fn as_str(&self) -> &str {
143 self.0.as_str()
144 }
145 pub fn username(&self) -> &str {
146 self.0.username()
147 }
148 pub fn password(&self) -> Option<&str> {
149 self.0.password()
150 }
151 pub fn join(&self, input: &str) -> Result<Self, ParseError> {
152 self.0.join(input).map(SafeUrl)
153 }
154
155 pub fn join_path(&self, path: &str) -> Self {
161 let base = self.to_string();
162 let base = base.trim_end_matches('/');
163 let path = path.trim_start_matches('/');
164 Self::parse(&format!("{base}/{path}"))
165 .expect("appending a relative path to a valid URL should produce a valid URL")
166 }
167
168 #[allow(clippy::case_sensitive_file_extension_comparisons)]
171 pub fn is_onion_address(&self) -> bool {
172 let host = self.host_str().unwrap_or_default();
173
174 host.ends_with(".onion")
175 }
176
177 pub fn fragment(&self) -> Option<&str> {
178 self.0.fragment()
179 }
180
181 pub fn set_fragment(&mut self, arg: Option<&str>) {
182 self.0.set_fragment(arg);
183 }
184}
185
186static SHOW_SECRETS: LazyLock<bool> = LazyLock::new(|| {
187 let enable = is_env_var_set(FM_DEBUG_SHOW_SECRETS_ENV);
188
189 if enable {
190 warn!(target: LOG_CORE, "{} enabled. Please don't use in production.", FM_DEBUG_SHOW_SECRETS_ENV);
191 }
192
193 enable
194});
195
196impl Display for SafeUrl {
197 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
198 write!(f, "{}://", self.0.scheme())?;
199
200 if !self.0.username().is_empty() {
201 let show_secrets = *SHOW_SECRETS;
202 if show_secrets {
203 write!(f, "{}", self.0.username())?;
204 } else {
205 write!(f, "REDACTEDUSER")?;
206 }
207
208 if self.0.password().is_some() {
209 if show_secrets {
210 write!(
211 f,
212 ":{}",
213 self.0.password().expect("Just checked it's checked")
214 )?;
215 } else {
216 write!(f, ":REDACTEDPASS")?;
217 }
218 }
219
220 write!(f, "@")?;
221 }
222
223 if let Some(host) = self.0.host_str() {
224 write!(f, "{host}")?;
225 }
226
227 if let Some(port) = self.0.port() {
228 write!(f, ":{port}")?;
229 }
230
231 write!(f, "{}", self.0.path())?;
232
233 Ok(())
234 }
235}
236
237impl Debug for SafeUrl {
238 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
239 write!(f, "SafeUrl(")?;
240 Display::fmt(self, f)?;
241 write!(f, ")")?;
242 Ok(())
243 }
244}
245
246impl From<Url> for SafeUrl {
247 fn from(u: Url) -> Self {
248 Self(u)
249 }
250}
251
252impl FromStr for SafeUrl {
253 type Err = ParseError;
254
255 #[inline]
256 fn from_str(input: &str) -> Result<Self, ParseError> {
257 Self::parse(input)
258 }
259}
260
261#[cfg(not(target_family = "wasm"))]
264pub fn write_new<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
265 let mut file = fs::File::options()
266 .write(true)
267 .create_new(true)
268 .open(path)?;
269 file.write_all(contents.as_ref())?;
270 file.sync_all()?;
271 Ok(())
272}
273
274#[cfg(not(target_family = "wasm"))]
275pub fn write_overwrite<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
276 fs::File::options()
277 .write(true)
278 .create(true)
279 .truncate(true)
280 .open(path)?
281 .write_all(contents.as_ref())
282}
283
284#[cfg(not(target_family = "wasm"))]
285pub async fn write_overwrite_async<P: AsRef<Path>, C: AsRef<[u8]>>(
286 path: P,
287 contents: C,
288) -> io::Result<()> {
289 tokio::fs::OpenOptions::new()
290 .write(true)
291 .create(true)
292 .truncate(true)
293 .open(path)
294 .await?
295 .write_all(contents.as_ref())
296 .await
297}
298
299#[cfg(not(target_family = "wasm"))]
300pub async fn write_new_async<P: AsRef<Path>, C: AsRef<[u8]>>(
301 path: P,
302 contents: C,
303) -> io::Result<()> {
304 tokio::fs::OpenOptions::new()
305 .write(true)
306 .create_new(true)
307 .open(path)
308 .await?
309 .write_all(contents.as_ref())
310 .await
311}
312
313#[derive(Debug, Clone)]
314pub struct Spanned<T> {
315 value: T,
316 span: Span,
317}
318
319impl<T> Spanned<T> {
320 pub async fn new<F: Future<Output = T>>(span: Span, make: F) -> Self {
321 Self::try_new::<Infallible, _>(span, async { Ok(make.await) })
322 .await
323 .unwrap()
324 }
325
326 pub async fn try_new<E, F: Future<Output = Result<T, E>>>(
327 span: Span,
328 make: F,
329 ) -> Result<Self, E> {
330 let span2 = span.clone();
331 async {
332 Ok(Self {
333 value: make.await?,
334 span: span2,
335 })
336 }
337 .instrument(span)
338 .await
339 }
340
341 pub fn borrow(&self) -> Spanned<&T> {
342 Spanned {
343 value: &self.value,
344 span: self.span.clone(),
345 }
346 }
347
348 pub fn map<U>(self, map: impl Fn(T) -> U) -> Spanned<U> {
349 Spanned {
350 value: map(self.value),
351 span: self.span,
352 }
353 }
354
355 pub fn borrow_mut(&mut self) -> Spanned<&mut T> {
356 Spanned {
357 value: &mut self.value,
358 span: self.span.clone(),
359 }
360 }
361
362 pub fn with_sync<O, F: FnOnce(T) -> O>(self, f: F) -> O {
363 let _g = self.span.enter();
364 f(self.value)
365 }
366
367 pub async fn with<Fut: Future, F: FnOnce(T) -> Fut>(self, f: F) -> Fut::Output {
368 async { f(self.value).await }.instrument(self.span).await
369 }
370
371 pub fn span(&self) -> Span {
372 self.span.clone()
373 }
374
375 pub fn value(&self) -> &T {
376 &self.value
377 }
378
379 pub fn value_mut(&mut self) -> &mut T {
380 &mut self.value
381 }
382
383 pub fn into_value(self) -> T {
384 self.value
385 }
386}
387
388pub fn handle_version_hash_command(version_hash: &str) {
391 let mut args = std::env::args();
392 if let Some(ref arg) = args.nth(1)
393 && arg.as_str() == "version-hash"
394 {
395 println!("{version_hash}");
396 std::process::exit(0);
397 }
398}
399
400pub async fn retry<F, Fut, T, E>(
427 op_name: impl Into<String>,
428 strategy: impl backoff_util::Backoff,
429 op_fn: F,
430) -> Result<T, E>
431where
432 F: Fn() -> Fut,
433 Fut: Future<Output = Result<T, E>>,
434 E: Display,
435{
436 let mut strategy = strategy;
437 let op_name = op_name.into();
438 let mut attempts: u64 = 0;
439 loop {
440 attempts += 1;
441 match op_fn().await {
442 Ok(result) => return Ok(result),
443 Err(err) => {
444 if let Some(interval) = strategy.next() {
445 debug!(
447 target: LOG_CORE,
448 err = %format_args!("{err:#}"),
449 %attempts,
450 interval = interval.as_secs(),
451 "{} failed, retrying",
452 op_name,
453 );
454 runtime::sleep(interval).await;
455 } else {
456 warn!(
457 target: LOG_CORE,
458 err = %format_args!("{err:#}"),
459 %attempts,
460 "{} failed",
461 op_name,
462 );
463 return Err(err);
464 }
465 }
466 }
467 }
468}
469
470pub fn get_median(vals: &[u64]) -> Option<u64> {
472 if vals.is_empty() {
473 return None;
474 }
475 let len = vals.len();
476 let mid = len / 2;
477
478 if len.is_multiple_of(2) {
479 Some(u64::midpoint(vals[mid - 1], vals[mid]))
480 } else {
481 Some(vals[mid])
482 }
483}
484
485pub fn get_average(vals: &[u64]) -> Option<u64> {
487 if vals.is_empty() {
488 return None;
489 }
490
491 let sum: u64 = vals.iter().sum();
492 Some(sum / vals.len() as u64)
493}
494
495#[cfg(test)]
496mod tests;