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