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