Skip to main content

fedimint_meta_client/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::module_name_repetitions)]
4
5#[cfg(feature = "uniffi")]
6::uniffi::setup_scaffolding!();
7
8pub mod api;
9#[cfg(feature = "cli")]
10pub mod cli;
11pub mod db;
12pub mod states;
13
14use std::collections::BTreeMap;
15use std::time::Duration;
16
17use anyhow::Context as _;
18use api::MetaFederationApi;
19use common::{KIND, MetaConsensusValue, MetaKey, MetaValue};
20use db::DbKeyPrefix;
21use fedimint_api_client::api::{DynGlobalApi, DynModuleApi};
22use fedimint_client_module::db::ClientModuleMigrationFn;
23use fedimint_client_module::meta::{FetchKind, LegacyMetaSource, MetaSource, MetaValues};
24use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
25use fedimint_client_module::module::recovery::NoModuleBackup;
26use fedimint_client_module::module::{ClientModule, IClientModule};
27use fedimint_client_module::sm::Context;
28use fedimint_core::config::ClientConfig;
29use fedimint_core::core::{Decoder, ModuleKind};
30use fedimint_core::db::{DatabaseTransaction, DatabaseVersion};
31use fedimint_core::module::{
32    Amounts, ApiAuth, ApiVersion, ModuleCommon, ModuleInit, MultiApiVersion,
33};
34use fedimint_core::util::backoff_util::FibonacciBackoff;
35#[cfg(feature = "uniffi")]
36use fedimint_core::util::ffi::UniffiError;
37use fedimint_core::util::{BoxStream, backoff_util, retry};
38use fedimint_core::{PeerId, apply, async_trait_maybe_send};
39use fedimint_logging::LOG_CLIENT_MODULE_META;
40pub use fedimint_meta_common as common;
41use fedimint_meta_common::{DEFAULT_META_KEY, MetaCommonInit, MetaModuleTypes};
42use futures::stream;
43use serde::Deserialize;
44use serde_json::json;
45use states::MetaStateMachine;
46use strum::IntoEnumIterator;
47use tracing::{debug, warn};
48
49#[derive(Debug)]
50#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
51pub struct MetaClientModule {
52    module_api: DynModuleApi,
53    admin_auth: Option<ApiAuth>,
54}
55
56impl MetaClientModule {
57    fn admin_auth(&self) -> anyhow::Result<ApiAuth> {
58        self.admin_auth
59            .clone()
60            .ok_or_else(|| anyhow::format_err!("Admin auth not set"))
61    }
62
63    /// Submit a meta consensus value
64    ///
65    /// When *threshold* amount of peers submits the exact same value it
66    /// becomes a new consensus value.
67    ///
68    /// To "cancel" previous vote, peer can submit a value equal to the current
69    /// consensus value.
70    pub async fn submit(&self, key: MetaKey, value: MetaValue) -> anyhow::Result<()> {
71        self.module_api
72            .submit(key, value, self.admin_auth()?)
73            .await?;
74
75        Ok(())
76    }
77
78    /// Get the current meta consensus value along with it's revision
79    ///
80    /// See [`Self::get_consensus_value_rev`] to use when checking for updates.
81    pub async fn get_consensus_value(
82        &self,
83        key: MetaKey,
84    ) -> anyhow::Result<Option<MetaConsensusValue>> {
85        Ok(self.module_api.get_consensus(key).await?)
86    }
87
88    /// Get the current meta consensus value revision
89    ///
90    /// Each time a meta consensus value changes, the revision increases,
91    /// so checking just the revision can save a lot of bandwidth in periodic
92    /// checks.
93    pub async fn get_consensus_value_rev(&self, key: MetaKey) -> anyhow::Result<Option<u64>> {
94        Ok(self.module_api.get_consensus_rev(key).await?)
95    }
96
97    /// Get current submissions to change the meta consensus value.
98    ///
99    /// Upon changing the consensus
100    pub async fn get_submissions(
101        &self,
102        key: MetaKey,
103    ) -> anyhow::Result<BTreeMap<PeerId, MetaValue>> {
104        Ok(self
105            .module_api
106            .get_submissions(key, self.admin_auth()?)
107            .await?)
108    }
109}
110
111#[cfg(feature = "uniffi")]
112#[uniffi::export(async_runtime = "tokio")]
113impl MetaClientModule {
114    #[uniffi::method(name = "get_consensus_value")]
115    pub async fn get_consensus_value_uniffi(
116        &self,
117        key: MetaKey,
118    ) -> Result<Option<MetaConsensusValue>, UniffiError> {
119        self.module_api
120            .get_consensus(key)
121            .await
122            .map_err(|e| UniffiError::from(anyhow::anyhow!(e.to_string())))
123    }
124}
125
126#[derive(Debug, Deserialize)]
127struct GetConsensusValueRequest {
128    key: MetaKey,
129}
130
131fn format_rpc_consensus_value_response(
132    maybe_consensus_value: Option<MetaConsensusValue>,
133) -> anyhow::Result<serde_json::Value> {
134    Ok(match maybe_consensus_value {
135        Some(MetaConsensusValue { revision, value }) => {
136            let value = value
137                .to_json_lossy()
138                .context("deserializing consensus value as json")?;
139
140            json!({
141                "revision": revision,
142                "value": value,
143            })
144        }
145        None => serde_json::Value::Null,
146    })
147}
148
149/// Data needed by the state machine
150#[derive(Debug, Clone)]
151pub struct MetaClientContext {
152    pub meta_decoder: Decoder,
153}
154
155// TODO: Boiler-plate
156impl Context for MetaClientContext {
157    const KIND: Option<ModuleKind> = Some(KIND);
158}
159
160#[apply(async_trait_maybe_send!)]
161impl ClientModule for MetaClientModule {
162    type Init = MetaClientInit;
163    type Common = MetaModuleTypes;
164    type Backup = NoModuleBackup;
165    type ModuleStateMachineContext = MetaClientContext;
166    type States = MetaStateMachine;
167
168    fn context(&self) -> Self::ModuleStateMachineContext {
169        MetaClientContext {
170            meta_decoder: self.decoder(),
171        }
172    }
173
174    fn input_fee(
175        &self,
176        _amount: &Amounts,
177        _input: &<Self::Common as ModuleCommon>::Input,
178    ) -> Option<Amounts> {
179        unreachable!()
180    }
181
182    fn output_fee(
183        &self,
184        _amount: &Amounts,
185        _output: &<Self::Common as ModuleCommon>::Output,
186    ) -> Option<Amounts> {
187        unreachable!()
188    }
189
190    async fn handle_rpc(
191        &self,
192        method: String,
193        request: serde_json::Value,
194    ) -> BoxStream<'_, anyhow::Result<serde_json::Value>> {
195        Box::pin(stream::once(async move {
196            match method.as_str() {
197                "get_consensus_value" => {
198                    let req: GetConsensusValueRequest = serde_json::from_value(request)?;
199                    let maybe_consensus_value = self.get_consensus_value(req.key).await?;
200                    format_rpc_consensus_value_response(maybe_consensus_value)
201                }
202                _ => Err(anyhow::format_err!("Unknown method: {method}")),
203            }
204        }))
205    }
206
207    #[cfg(feature = "cli")]
208    async fn handle_cli_command(
209        &self,
210        args: &[std::ffi::OsString],
211    ) -> anyhow::Result<serde_json::Value> {
212        cli::handle_cli_command(self, args).await
213    }
214}
215
216#[derive(Debug, Clone)]
217pub struct MetaClientInit;
218
219// TODO: Boilerplate-code
220impl ModuleInit for MetaClientInit {
221    type Common = MetaCommonInit;
222
223    async fn dump_database(
224        &self,
225        _dbtx: &mut DatabaseTransaction<'_>,
226        prefix_names: Vec<String>,
227    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
228        let items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
229        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
230            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
231        });
232
233        #[allow(clippy::never_loop)]
234        for table in filtered_prefixes {
235            match table {}
236        }
237
238        Box::new(items.into_iter())
239    }
240}
241
242/// Generates the client module
243#[apply(async_trait_maybe_send!)]
244impl ClientModuleInit for MetaClientInit {
245    type Module = MetaClientModule;
246
247    fn supported_api_versions(&self) -> MultiApiVersion {
248        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
249            .expect("no version conflicts")
250    }
251
252    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
253        Ok(MetaClientModule {
254            module_api: args.module_api().clone(),
255            admin_auth: args.admin_auth().cloned(),
256        })
257    }
258
259    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
260        BTreeMap::new()
261    }
262}
263
264/// Meta source fetching meta values from the meta module if available or the
265/// legacy meta source otherwise.
266#[derive(Clone, Debug, Default)]
267pub struct MetaModuleMetaSourceWithFallback<S = LegacyMetaSource> {
268    legacy: S,
269}
270
271impl<S> MetaModuleMetaSourceWithFallback<S> {
272    pub fn new(legacy: S) -> Self {
273        Self { legacy }
274    }
275}
276
277#[apply(async_trait_maybe_send!)]
278impl<S: MetaSource> MetaSource for MetaModuleMetaSourceWithFallback<S> {
279    async fn wait_for_update(&self) {
280        fedimint_core::runtime::sleep(Duration::from_mins(10)).await;
281    }
282
283    async fn fetch(
284        &self,
285        client_config: &ClientConfig,
286        api: &DynGlobalApi,
287        fetch_kind: fedimint_client_module::meta::FetchKind,
288        last_revision: Option<u64>,
289    ) -> anyhow::Result<fedimint_client_module::meta::MetaValues> {
290        let backoff = match fetch_kind {
291            // need to be fast the first time.
292            FetchKind::Initial => backoff_util::aggressive_backoff(),
293            FetchKind::Background => backoff_util::background_backoff(),
294        };
295
296        let maybe_meta_module_meta = get_meta_module_value(client_config, api, backoff)
297            .await
298            .map(|meta| {
299                Result::<_, anyhow::Error>::Ok(MetaValues {
300                    values: serde_json::from_slice(meta.value.as_slice())?,
301                    revision: meta.revision,
302                })
303            })
304            .transpose()?;
305
306        // If we couldn't fetch valid meta values from the meta module for any reason,
307        // fall back to the legacy meta source
308        if let Some(maybe_meta_module_meta) = maybe_meta_module_meta {
309            Ok(maybe_meta_module_meta)
310        } else {
311            self.legacy
312                .fetch(client_config, api, fetch_kind, last_revision)
313                .await
314        }
315    }
316}
317
318async fn get_meta_module_value(
319    client_config: &ClientConfig,
320    api: &DynGlobalApi,
321    backoff: FibonacciBackoff,
322) -> Option<MetaConsensusValue> {
323    match client_config.get_first_module_by_kind_cfg(KIND) {
324        Ok((instance_id, _)) => {
325            let meta_api = api.with_module(instance_id);
326
327            let overrides_res = retry("fetch_meta_values", backoff, || async {
328                Ok(meta_api.get_consensus(DEFAULT_META_KEY).await?)
329            })
330            .await;
331
332            match overrides_res {
333                Ok(Some(consensus)) => Some(consensus),
334                Ok(None) => {
335                    debug!(target: LOG_CLIENT_MODULE_META, "Meta module returned no consensus value");
336                    None
337                }
338                Err(e) => {
339                    warn!(target: LOG_CLIENT_MODULE_META, "Failed to fetch meta module consensus value: {}", e);
340                    None
341                }
342            }
343        }
344        _ => None,
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use fedimint_meta_common::MetaValue;
351    use serde_json::json;
352
353    use super::{MetaConsensusValue, format_rpc_consensus_value_response};
354
355    #[test]
356    fn formats_consensus_value_as_json() {
357        let response = format_rpc_consensus_value_response(Some(MetaConsensusValue {
358            revision: 7,
359            value: MetaValue::from(br#"{"welcome_message":"hello"}"#.as_slice()),
360        }))
361        .expect("valid json meta value should format");
362
363        assert_eq!(
364            response,
365            json!({
366                "revision": 7,
367                "value": {
368                    "welcome_message": "hello",
369                },
370            })
371        );
372    }
373
374    #[test]
375    fn formats_missing_consensus_value_as_null() {
376        let response =
377            format_rpc_consensus_value_response(None).expect("null response should format");
378
379        assert_eq!(response, serde_json::Value::Null);
380    }
381}