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 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 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 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 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#[derive(Debug, Clone)]
151pub struct MetaClientContext {
152 pub meta_decoder: Decoder,
153}
154
155impl 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
219impl 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#[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#[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 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 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}