fedimint_client_module/
meta.rs

1use std::collections::BTreeMap;
2use std::time::{Duration, SystemTime};
3
4use anyhow::{Context as _, bail};
5use fedimint_api_client::api::DynGlobalApi;
6use fedimint_core::config::ClientConfig;
7use fedimint_core::encoding::{Decodable, Encodable};
8use fedimint_core::task::{MaybeSend, MaybeSync};
9use fedimint_core::util::{backoff_util, retry};
10use fedimint_core::{apply, async_trait_maybe_send};
11use fedimint_logging::LOG_CLIENT;
12use serde::{Deserialize, Serialize};
13use tracing::{debug, warn};
14
15#[apply(async_trait_maybe_send!)]
16pub trait MetaSource: MaybeSend + MaybeSync + 'static {
17    /// Wait for next change in this source.
18    async fn wait_for_update(&self);
19    async fn fetch(
20        &self,
21        client_config: &ClientConfig,
22        api: &DynGlobalApi,
23        fetch_kind: FetchKind,
24        last_revision: Option<u64>,
25    ) -> anyhow::Result<MetaValues>;
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum FetchKind {
30    /// Meta source should return fast, retry less.
31    /// This blocks getting any meta values.
32    Initial,
33    /// Meta source can retry infinitely.
34    Background,
35}
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38pub struct MetaValues {
39    pub values: BTreeMap<MetaFieldKey, MetaFieldValue>,
40    pub revision: u64,
41}
42
43#[derive(Debug, Clone, Copy)]
44pub struct MetaValue<T> {
45    pub fetch_time: SystemTime,
46    pub value: Option<T>,
47}
48
49/// Legacy non-meta module config source uses client config meta and
50/// meta_override_url meta field.
51#[derive(Clone, Debug, Default)]
52#[non_exhaustive]
53pub struct LegacyMetaSource {
54    reqwest: reqwest::Client,
55}
56
57#[apply(async_trait_maybe_send!)]
58impl MetaSource for LegacyMetaSource {
59    async fn wait_for_update(&self) {
60        fedimint_core::runtime::sleep(Duration::from_secs(10 * 60)).await;
61    }
62
63    async fn fetch(
64        &self,
65        client_config: &ClientConfig,
66        _api: &DynGlobalApi,
67        fetch_kind: FetchKind,
68        last_revision: Option<u64>,
69    ) -> anyhow::Result<MetaValues> {
70        let config_iter = client_config
71            .global
72            .meta
73            .iter()
74            .map(|(key, value)| (MetaFieldKey(key.clone()), MetaFieldValue(value.clone())));
75        let backoff = match fetch_kind {
76            // need to be fast the first time.
77            FetchKind::Initial => backoff_util::aggressive_backoff(),
78            FetchKind::Background => backoff_util::background_backoff(),
79        };
80        let overrides = retry("fetch_meta_overrides", backoff, || {
81            fetch_meta_overrides(&self.reqwest, client_config, "meta_override_url")
82        })
83        .await?;
84        Ok(MetaValues {
85            values: config_iter.chain(overrides).collect(),
86            revision: last_revision.map_or(0, |r| r + 1),
87        })
88    }
89}
90
91#[derive(
92    Encodable, Decodable, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize,
93)]
94pub struct MetaFieldKey(pub String);
95
96#[derive(Encodable, Decodable, Debug, Clone, Serialize, Deserialize)]
97pub struct MetaFieldValue(pub String);
98
99pub async fn fetch_meta_overrides(
100    reqwest: &reqwest::Client,
101    client_config: &ClientConfig,
102    field_name: &str,
103) -> anyhow::Result<BTreeMap<MetaFieldKey, MetaFieldValue>> {
104    let Some(url) = client_config.meta::<String>(field_name)? else {
105        return Ok(BTreeMap::new());
106    };
107    let response = reqwest
108        .get(&url)
109        .send()
110        .await
111        .context("Meta override source could not be fetched")?;
112
113    debug!("Meta override source returned status: {response:?}");
114
115    if response.status() != reqwest::StatusCode::OK {
116        bail!(
117            "Meta override request returned non-OK status code: {}",
118            response.status()
119        );
120    }
121
122    let mut federation_map = response
123        .json::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
124        .await
125        .context("Meta override could not be parsed as JSON")?;
126
127    let federation_id = client_config.calculate_federation_id().to_string();
128    let meta_fields = federation_map
129        .remove(&federation_id)
130        .with_context(|| anyhow::format_err!("No entry for federation {federation_id} in {url}"))?
131        .into_iter()
132        .filter_map(|(key, value)| {
133            if let serde_json::Value::String(value_str) = value {
134                Some((MetaFieldKey(key), MetaFieldValue(value_str)))
135            } else {
136                warn!(target: LOG_CLIENT, "Meta override map contained non-string key: {key}, ignoring");
137                None
138            }
139        })
140        .collect::<BTreeMap<_, _>>();
141
142    Ok(meta_fields)
143}