1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::module_name_repetitions)]
4
5pub mod api;
6#[cfg(feature = "cli")]
7pub mod cli;
8pub mod db;
9pub mod states;
10
11use std::collections::BTreeMap;
12use std::time::Duration;
13
14use api::MetaFederationApi;
15use common::{KIND, MetaConsensusValue, MetaKey, MetaValue};
16use db::DbKeyPrefix;
17use fedimint_api_client::api::{DynGlobalApi, DynModuleApi};
18use fedimint_client_module::db::ClientModuleMigrationFn;
19use fedimint_client_module::meta::{FetchKind, LegacyMetaSource, MetaSource, MetaValues};
20use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
21use fedimint_client_module::module::recovery::NoModuleBackup;
22use fedimint_client_module::module::{ClientModule, IClientModule};
23use fedimint_client_module::sm::Context;
24use fedimint_core::config::ClientConfig;
25use fedimint_core::core::{Decoder, ModuleKind};
26use fedimint_core::db::{DatabaseTransaction, DatabaseVersion};
27use fedimint_core::module::{
28 Amounts, ApiAuth, ApiVersion, ModuleCommon, ModuleInit, MultiApiVersion,
29};
30use fedimint_core::util::backoff_util::FibonacciBackoff;
31use fedimint_core::util::{backoff_util, retry};
32use fedimint_core::{PeerId, apply, async_trait_maybe_send};
33use fedimint_logging::LOG_CLIENT_MODULE_META;
34pub use fedimint_meta_common as common;
35use fedimint_meta_common::{DEFAULT_META_KEY, MetaCommonInit, MetaModuleTypes};
36use states::MetaStateMachine;
37use strum::IntoEnumIterator;
38use tracing::{debug, warn};
39
40#[derive(Debug)]
41pub struct MetaClientModule {
42 module_api: DynModuleApi,
43 admin_auth: Option<ApiAuth>,
44}
45
46impl MetaClientModule {
47 fn admin_auth(&self) -> anyhow::Result<ApiAuth> {
48 self.admin_auth
49 .clone()
50 .ok_or_else(|| anyhow::format_err!("Admin auth not set"))
51 }
52
53 pub async fn submit(&self, key: MetaKey, value: MetaValue) -> anyhow::Result<()> {
61 self.module_api
62 .submit(key, value, self.admin_auth()?)
63 .await?;
64
65 Ok(())
66 }
67
68 pub async fn get_consensus_value(
72 &self,
73 key: MetaKey,
74 ) -> anyhow::Result<Option<MetaConsensusValue>> {
75 Ok(self.module_api.get_consensus(key).await?)
76 }
77
78 pub async fn get_consensus_value_rev(&self, key: MetaKey) -> anyhow::Result<Option<u64>> {
84 Ok(self.module_api.get_consensus_rev(key).await?)
85 }
86
87 pub async fn get_submissions(
91 &self,
92 key: MetaKey,
93 ) -> anyhow::Result<BTreeMap<PeerId, MetaValue>> {
94 Ok(self
95 .module_api
96 .get_submissions(key, self.admin_auth()?)
97 .await?)
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct MetaClientContext {
104 pub meta_decoder: Decoder,
105}
106
107impl Context for MetaClientContext {
109 const KIND: Option<ModuleKind> = Some(KIND);
110}
111
112#[apply(async_trait_maybe_send!)]
113impl ClientModule for MetaClientModule {
114 type Init = MetaClientInit;
115 type Common = MetaModuleTypes;
116 type Backup = NoModuleBackup;
117 type ModuleStateMachineContext = MetaClientContext;
118 type States = MetaStateMachine;
119
120 fn context(&self) -> Self::ModuleStateMachineContext {
121 MetaClientContext {
122 meta_decoder: self.decoder(),
123 }
124 }
125
126 fn input_fee(
127 &self,
128 _amount: &Amounts,
129 _input: &<Self::Common as ModuleCommon>::Input,
130 ) -> Option<Amounts> {
131 unreachable!()
132 }
133
134 fn output_fee(
135 &self,
136 _amount: &Amounts,
137 _output: &<Self::Common as ModuleCommon>::Output,
138 ) -> Option<Amounts> {
139 unreachable!()
140 }
141
142 #[cfg(feature = "cli")]
143 async fn handle_cli_command(
144 &self,
145 args: &[std::ffi::OsString],
146 ) -> anyhow::Result<serde_json::Value> {
147 cli::handle_cli_command(self, args).await
148 }
149}
150
151#[derive(Debug, Clone)]
152pub struct MetaClientInit;
153
154impl ModuleInit for MetaClientInit {
156 type Common = MetaCommonInit;
157
158 async fn dump_database(
159 &self,
160 _dbtx: &mut DatabaseTransaction<'_>,
161 prefix_names: Vec<String>,
162 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
163 let items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
164 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
165 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
166 });
167
168 #[allow(clippy::never_loop)]
169 for table in filtered_prefixes {
170 match table {}
171 }
172
173 Box::new(items.into_iter())
174 }
175}
176
177#[apply(async_trait_maybe_send!)]
179impl ClientModuleInit for MetaClientInit {
180 type Module = MetaClientModule;
181
182 fn supported_api_versions(&self) -> MultiApiVersion {
183 MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
184 .expect("no version conflicts")
185 }
186
187 async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
188 Ok(MetaClientModule {
189 module_api: args.module_api().clone(),
190 admin_auth: args.admin_auth().cloned(),
191 })
192 }
193
194 fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
195 BTreeMap::new()
196 }
197}
198
199#[derive(Clone, Debug, Default)]
202pub struct MetaModuleMetaSourceWithFallback<S = LegacyMetaSource> {
203 legacy: S,
204}
205
206impl<S> MetaModuleMetaSourceWithFallback<S> {
207 pub fn new(legacy: S) -> Self {
208 Self { legacy }
209 }
210}
211
212#[apply(async_trait_maybe_send!)]
213impl<S: MetaSource> MetaSource for MetaModuleMetaSourceWithFallback<S> {
214 async fn wait_for_update(&self) {
215 fedimint_core::runtime::sleep(Duration::from_secs(10 * 60)).await;
216 }
217
218 async fn fetch(
219 &self,
220 client_config: &ClientConfig,
221 api: &DynGlobalApi,
222 fetch_kind: fedimint_client_module::meta::FetchKind,
223 last_revision: Option<u64>,
224 ) -> anyhow::Result<fedimint_client_module::meta::MetaValues> {
225 let backoff = match fetch_kind {
226 FetchKind::Initial => backoff_util::aggressive_backoff(),
228 FetchKind::Background => backoff_util::background_backoff(),
229 };
230
231 let maybe_meta_module_meta = get_meta_module_value(client_config, api, backoff)
232 .await
233 .map(|meta| {
234 Result::<_, anyhow::Error>::Ok(MetaValues {
235 values: serde_json::from_slice(meta.value.as_slice())?,
236 revision: meta.revision,
237 })
238 })
239 .transpose()?;
240
241 if let Some(maybe_meta_module_meta) = maybe_meta_module_meta {
244 Ok(maybe_meta_module_meta)
245 } else {
246 self.legacy
247 .fetch(client_config, api, fetch_kind, last_revision)
248 .await
249 }
250 }
251}
252
253async fn get_meta_module_value(
254 client_config: &ClientConfig,
255 api: &DynGlobalApi,
256 backoff: FibonacciBackoff,
257) -> Option<MetaConsensusValue> {
258 match client_config.get_first_module_by_kind_cfg(KIND) {
259 Ok((instance_id, _)) => {
260 let meta_api = api.with_module(instance_id);
261
262 let overrides_res = retry("fetch_meta_values", backoff, || async {
263 Ok(meta_api.get_consensus(DEFAULT_META_KEY).await?)
264 })
265 .await;
266
267 match overrides_res {
268 Ok(Some(consensus)) => Some(consensus),
269 Ok(None) => {
270 debug!(target: LOG_CLIENT_MODULE_META, "Meta module returned no consensus value");
271 None
272 }
273 Err(e) => {
274 warn!(target: LOG_CLIENT_MODULE_META, "Failed to fetch meta module consensus value: {}", e);
275 None
276 }
277 }
278 }
279 _ => None,
280 }
281}