1use std::collections::HashMap;
2use std::fmt::{Display, Formatter};
3
4use fedimint_core::core::ModuleInstanceId;
5use futures::StreamExt;
6use itertools::Itertools;
7use serde::{Deserialize, Serialize};
8
9use crate::db::{
10 DatabaseKey, DatabaseLookup, DatabaseRecord, DatabaseTransaction,
11 IDatabaseTransactionOpsCoreTyped,
12};
13use crate::task::{MaybeSend, MaybeSync};
14
15#[derive(Default)]
16pub struct Audit {
17 items: Vec<AuditItem>,
18}
19
20impl Audit {
21 pub fn net_assets(&self) -> Option<AuditItem> {
22 Some(AuditItem {
23 name: "Net assets (sats)".to_string(),
24 milli_sat: calculate_net_assets(self.items.iter())?,
25 module_instance_id: None,
26 })
27 }
28
29 pub async fn add_items<KP, F>(
30 &mut self,
31 dbtx: &mut DatabaseTransaction<'_>,
32 module_instance_id: ModuleInstanceId,
33 key_prefix: &KP,
34 to_milli_sat: F,
35 ) where
36 KP: DatabaseLookup + 'static + MaybeSend + MaybeSync,
37 KP::Record: DatabaseKey,
38 F: Fn(KP::Record, <<KP as DatabaseLookup>::Record as DatabaseRecord>::Value) -> i64,
39 {
40 let mut new_items = dbtx
41 .find_by_prefix(key_prefix)
42 .await
43 .map(|(key, value)| {
44 let name = format!("{key:?}");
45 let milli_sat = to_milli_sat(key, value);
46 AuditItem {
47 name,
48 milli_sat,
49 module_instance_id: Some(module_instance_id),
50 }
51 })
52 .collect::<Vec<AuditItem>>()
53 .await;
54 self.items.append(&mut new_items);
55 }
56}
57
58impl Display for Audit {
59 fn fmt(&self, formatter: &mut Formatter) -> std::fmt::Result {
60 formatter.write_str("- Balance Sheet -")?;
61 for item in &self.items {
62 formatter.write_fmt(format_args!("\n{item}"))?;
63 }
64 formatter.write_fmt(format_args!(
65 "\n{}",
66 self.net_assets()
67 .expect("We'd have crashed already if there was an overflow")
68 ))
69 }
70}
71
72pub struct AuditItem {
73 pub name: String,
74 pub milli_sat: i64,
75 pub module_instance_id: Option<ModuleInstanceId>,
76}
77
78impl Display for AuditItem {
79 fn fmt(&self, formatter: &mut Formatter) -> std::fmt::Result {
80 let sats = (self.milli_sat as f64) / 1000.0;
81 formatter.write_fmt(format_args!("{:>+15.3}|{}", sats, self.name))
82 }
83}
84
85#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
86pub struct AuditSummary {
87 pub net_assets: i64,
88 pub module_summaries: HashMap<ModuleInstanceId, ModuleSummary>,
89}
90
91#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
92pub struct ModuleSummary {
93 pub net_assets: i64,
94 pub kind: String,
95}
96
97impl AuditSummary {
98 pub fn from_audit(
99 audit: &Audit,
100 module_instance_id_to_kind: &HashMap<ModuleInstanceId, String>,
101 ) -> Self {
102 let empty_module_placeholders = module_instance_id_to_kind
103 .iter()
104 .map(|(id, _)| create_empty_module_placeholder(*id))
105 .collect::<Vec<_>>();
106 Self {
107 net_assets: calculate_net_assets(audit.items.iter())
108 .expect("We'd have crashed already if there was an overflow"),
109 module_summaries: generate_module_summaries(
110 audit.items.iter().chain(&empty_module_placeholders),
111 module_instance_id_to_kind,
112 ),
113 }
114 }
115}
116
117fn generate_module_summaries<'a>(
118 audit_items: impl Iterator<Item = &'a AuditItem>,
119 module_instance_id_to_kind: &HashMap<ModuleInstanceId, String>,
120) -> HashMap<ModuleInstanceId, ModuleSummary> {
121 audit_items
122 .filter_map(|item| {
123 item.module_instance_id
124 .as_ref()
125 .map(|module_instance_id| (module_instance_id, item))
126 })
127 .into_group_map()
128 .into_iter()
129 .map(|(module_instance_id, module_audit_items)| {
130 let kind = module_instance_id_to_kind
131 .get(module_instance_id)
132 .expect("module instance id should have a kind")
133 .to_string();
134 (
135 *module_instance_id,
136 ModuleSummary {
137 net_assets: calculate_net_assets(module_audit_items.into_iter())
138 .expect("We'd have crashed already if there was an overflow"),
139 kind,
140 },
141 )
142 })
143 .collect()
144}
145
146fn calculate_net_assets<'a>(items: impl Iterator<Item = &'a AuditItem>) -> Option<i64> {
147 items
148 .map(|item| item.milli_sat)
149 .try_fold(0i64, i64::checked_add)
150}
151
152fn create_empty_module_placeholder(module_instance_id: ModuleInstanceId) -> AuditItem {
156 AuditItem {
157 name: "Module placeholder".to_string(),
158 milli_sat: 0,
159 module_instance_id: Some(module_instance_id),
160 }
161}
162
163#[test]
164fn creates_audit_summary_from_audit() {
165 let audit = Audit {
166 items: vec![
167 AuditItem {
168 name: "ContractKey(...)".to_string(),
169 milli_sat: -101_000,
170 module_instance_id: Some(0),
171 },
172 AuditItem {
173 name: "IssuanceTotal".to_string(),
174 milli_sat: -50_100_000,
175 module_instance_id: Some(1),
176 },
177 AuditItem {
178 name: "Redemption(...)".to_string(),
179 milli_sat: 101_000,
180 module_instance_id: Some(1),
181 },
182 AuditItem {
183 name: "RedemptionTotal".to_string(),
184 milli_sat: 100_000,
185 module_instance_id: Some(1),
186 },
187 AuditItem {
188 name: "UTXOKey(...)".to_string(),
189 milli_sat: 20_000_000,
190 module_instance_id: Some(2),
191 },
192 AuditItem {
193 name: "UTXOKey(...)".to_string(),
194 milli_sat: 10_000_000,
195 module_instance_id: Some(2),
196 },
197 AuditItem {
198 name: "UTXOKey(...)".to_string(),
199 milli_sat: 20_000_000,
200 module_instance_id: Some(2),
201 },
202 ],
203 };
204
205 let audit_summary = AuditSummary::from_audit(
206 &audit,
207 &HashMap::from([
208 (0, "ln".to_string()),
209 (1, "mint".to_string()),
210 (2, "wallet".to_string()),
211 ]),
212 );
213 let expected_audit_summary = AuditSummary {
214 net_assets: 0,
215 module_summaries: HashMap::from([
216 (
217 0,
218 ModuleSummary {
219 net_assets: -101_000,
220 kind: "ln".to_string(),
221 },
222 ),
223 (
224 1,
225 ModuleSummary {
226 net_assets: -49_899_000,
227 kind: "mint".to_string(),
228 },
229 ),
230 (
231 2,
232 ModuleSummary {
233 net_assets: 50_000_000,
234 kind: "wallet".to_string(),
235 },
236 ),
237 ]),
238 };
239
240 assert_eq!(audit_summary, expected_audit_summary);
241}
242
243#[test]
244fn audit_summary_includes_placeholders() {
245 let audit_summary = AuditSummary::from_audit(
246 &Audit::default(),
247 &HashMap::from([
248 (0, "ln".to_string()),
249 (1, "mint".to_string()),
250 (2, "wallet".to_string()),
251 ]),
252 );
253 let expected_audit_summary = AuditSummary {
254 net_assets: 0,
255 module_summaries: HashMap::from([
256 (
257 0,
258 ModuleSummary {
259 net_assets: 0,
260 kind: "ln".to_string(),
261 },
262 ),
263 (
264 1,
265 ModuleSummary {
266 net_assets: 0,
267 kind: "mint".to_string(),
268 },
269 ),
270 (
271 2,
272 ModuleSummary {
273 net_assets: 0,
274 kind: "wallet".to_string(),
275 },
276 ),
277 ]),
278 };
279
280 assert_eq!(audit_summary, expected_audit_summary);
281}