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