From bdcf1113454d88e4e095c9b55809b746260e026c Mon Sep 17 00:00:00 2001 From: elsirion Date: Tue, 3 Sep 2024 11:50:28 +0200 Subject: [PATCH 1/4] feat: add totals endpoint --- fmo_api_types/src/lib.rs | 6 ++++++ fmo_server/src/federation/mod.rs | 9 ++++++++- fmo_server/src/federation/observer.rs | 28 +++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/fmo_api_types/src/lib.rs b/fmo_api_types/src/lib.rs index 36ce160..27d73e9 100644 --- a/fmo_api_types/src/lib.rs +++ b/fmo_api_types/src/lib.rs @@ -3,6 +3,12 @@ use fedimint_core::config::FederationId; use fedimint_core::Amount; use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FedimintTotals { + pub tx_volume: Amount, + pub tx_count: u64, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FederationSummary { pub id: FederationId, diff --git a/fmo_server/src/federation/mod.rs b/fmo_server/src/federation/mod.rs index ec0ef8b..6c784aa 100644 --- a/fmo_server/src/federation/mod.rs +++ b/fmo_server/src/federation/mod.rs @@ -13,7 +13,7 @@ use fedimint_core::api::InviteCode; use fedimint_core::config::{ClientConfig, FederationId, JsonClientConfig}; use fedimint_core::core::ModuleInstanceId; use fedimint_core::module::registry::ModuleDecoderRegistry; -use fmo_api_types::FederationSummary; +use fmo_api_types::{FederationSummary, FedimintTotals}; use serde_json::json; use crate::federation::meta::get_federation_meta; @@ -28,6 +28,7 @@ pub fn get_federations_routes() -> Router { Router::new() .route("/", get(list_observed_federations)) .route("/", put(add_observed_federation)) + .route("/totals", get(get_federation_totals)) .route("/:federation_id", get(get_federation_overview)) .route( "/:federation_id/config", @@ -128,6 +129,12 @@ async fn get_federation_utxos( Ok(utxos.into()) } +async fn get_federation_totals( + State(state): State, +) -> crate::error::Result> { + Ok(state.federation_observer.totals().await?.into()) +} + fn decoders_from_config(config: &ClientConfig) -> ModuleDecoderRegistry { get_decoders( config diff --git a/fmo_server/src/federation/observer.rs b/fmo_server/src/federation/observer.rs index 0a21f1d..c7664f3 100644 --- a/fmo_server/src/federation/observer.rs +++ b/fmo_server/src/federation/observer.rs @@ -20,7 +20,7 @@ use fedimint_ln_common::contracts::{Contract, IdentifiableContract}; use fedimint_ln_common::{LightningInput, LightningOutput, LightningOutputV0}; use fedimint_mint_common::{MintInput, MintOutput}; use fedimint_wallet_common::{WalletConsensusItem, WalletInput, WalletOutput, WalletOutputV0}; -use fmo_api_types::{FederationActivity, FederationSummary, FederationUtxo}; +use fmo_api_types::{FederationActivity, FederationSummary, FederationUtxo, FedimintTotals}; use futures::future::join_all; use futures::StreamExt; use postgres_from_row::FromRow; @@ -31,7 +31,7 @@ use tracing::{debug, error, warn}; use crate::federation::db::Federation; use crate::federation::{db, decoders_from_config, instance_to_kind}; -use crate::util::{execute, query, query_opt, query_value}; +use crate::util::{execute, query, query_one, query_opt, query_value}; #[derive(Debug, Clone)] pub struct FederationObserver { @@ -1076,6 +1076,30 @@ impl FederationObserver { }) }).collect() } + + pub async fn totals(&self) -> anyhow::Result { + #[derive(Debug, FromRow)] + struct FedimintTotalsResult { + tx_count: i64, + tx_volume: i64, + } + + let totals = query_one::( + &self.connection().await?, + // language=postgresql + " + SELECT (SELECT count(*) from transactions)::bigint as tx_count, + (SELECT sum(amount_msat) from transaction_inputs)::bigint as tx_volume + ", + &[], + ) + .await?; + + Ok(FedimintTotals { + tx_count: totals.tx_count as u64, + tx_volume: Amount::from_msats(totals.tx_volume as u64), + }) + } } fn last_n_day_iter(now: NaiveDate, days: u32) -> impl Iterator { From 3e96940746570c8f975f422c5d0d5ef825f62ce4 Mon Sep 17 00:00:00 2001 From: elsirion Date: Tue, 3 Sep 2024 13:09:51 +0200 Subject: [PATCH 2/4] feat: show totals on home page --- Cargo.lock | 13 ++- fmo_frontend/Cargo.toml | 1 + fmo_frontend/src/components/alert.rs | 3 +- fmo_frontend/src/components/federation/mod.rs | 10 +- .../src/components/federation/utxos.rs | 13 +-- .../src/components/federations/mod.rs | 5 + .../src/components/federations/totals.rs | 94 +++++++++++++++++++ fmo_frontend/src/components/tabs.rs | 15 ++- 8 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 fmo_frontend/src/components/federations/totals.rs diff --git a/Cargo.lock b/Cargo.lock index fb301c6..4eab590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,7 @@ dependencies = [ "leptos", "leptos-chartistry", "leptos_router", + "num-format", "reqwest 0.12.5", "serde_json", "tokio", @@ -2510,6 +2511,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -4525,7 +4536,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/fmo_frontend/Cargo.toml b/fmo_frontend/Cargo.toml index 857fad5..4c77704 100644 --- a/fmo_frontend/Cargo.toml +++ b/fmo_frontend/Cargo.toml @@ -13,6 +13,7 @@ futures = "0.3.30" leptos = { version = "0.6", features = ["csr"] } leptos-chartistry = "0.1.6" leptos_router = { version = "0.6.13", features = ["csr"] } +num-format = "0.4.4" reqwest = { version = "0.12.5", default-features = false, features = [ "json" ] } serde_json = "1.0.122" tokio = {version = "1.39.2"} diff --git a/fmo_frontend/src/components/alert.rs b/fmo_frontend/src/components/alert.rs index 42a7946..ae3061b 100644 --- a/fmo_frontend/src/components/alert.rs +++ b/fmo_frontend/src/components/alert.rs @@ -25,7 +25,8 @@ pub fn Alert( view! { } } diff --git a/fmo_frontend/src/components/federation/mod.rs b/fmo_frontend/src/components/federation/mod.rs index f9fcbb8..9810556 100644 --- a/fmo_frontend/src/components/federation/mod.rs +++ b/fmo_frontend/src/components/federation/mod.rs @@ -76,7 +76,7 @@ pub fn Federation() -> impl IntoView { url: guardian.url.to_string(), }) .collect()/> - + @@ -87,17 +87,21 @@ pub fn Federation() -> impl IntoView {
-
{serde_json::to_string_pretty(&config).expect("can be encoded")}
+
+                                                {serde_json::to_string_pretty(&config)
+                                                    .expect("can be encoded")}
+                                            
} .into_view() } - Some(Err(e)) => view! { { format!("Error: {}", e) } }.into_view(), + Some(Err(e)) => view! { {format!("Error: {}", e)} }.into_view(), None => view! { "Loading..." }.into_view(), } }} + } diff --git a/fmo_frontend/src/components/federation/utxos.rs b/fmo_frontend/src/components/federation/utxos.rs index b4e7e27..88e567e 100644 --- a/fmo_frontend/src/components/federation/utxos.rs +++ b/fmo_frontend/src/components/federation/utxos.rs @@ -19,10 +19,12 @@ pub fn Utxos(federation_id: FederationId) -> impl IntoView { view! { -
@@ -42,7 +44,6 @@ pub fn Utxos(federation_id: FederationId) -> impl IntoView {
                             }
                         })
                         .collect::>();
-
                     view! {
                         
impl IntoView { - "UTXOs (" { utxos.len() } " total)" + "UTXOs (" + {utxos.len()} + " total)" Amount - - { rows } - + {rows}
} diff --git a/fmo_frontend/src/components/federations/mod.rs b/fmo_frontend/src/components/federations/mod.rs index e08d48f..f1cbf39 100644 --- a/fmo_frontend/src/components/federations/mod.rs +++ b/fmo_frontend/src/components/federations/mod.rs @@ -1,10 +1,12 @@ mod federation_row; +mod totals; use fedimint_core::Amount; use fmo_api_types::FederationSummary; use leptos::{component, create_resource, view, IntoView, SignalGet}; use crate::components::federations::federation_row::FederationRow; +use crate::components::federations::totals::Totals; use crate::BASE_URL; #[component] @@ -37,6 +39,9 @@ pub fn Federations() -> impl IntoView { }; view! { +
+ +
diff --git a/fmo_frontend/src/components/federations/totals.rs b/fmo_frontend/src/components/federations/totals.rs new file mode 100644 index 0000000..ba09fc1 --- /dev/null +++ b/fmo_frontend/src/components/federations/totals.rs @@ -0,0 +1,94 @@ +use std::time::Duration; + +use fedimint_core::runtime::sleep; +use fedimint_core::util::{retry, FibonacciBackoff}; +use fedimint_core::Amount; +use fmo_api_types::FedimintTotals; +use leptos::{component, create_resource, view, IntoView, SignalGet}; +use num_format::{Locale, ToFormattedString}; +use tracing::error; + +use crate::util::{AsBitcoin, FmtBitcoin}; + +#[component] +pub fn Totals() -> impl IntoView { + let totals_res = create_resource( + || (), + |_| async { + retry( + "fetching federation totals", + FibonacciBackoff::default().with_max_times(usize::MAX), + || fetch_federation_totals(), + ) + .await + .expect("Will never return Err") + }, + ); + + view! { +
+
+ + {move || { + match totals_res.get() { + Some(totals) => { + view! { +
+ {totals.tx_count.to_formatted_string(&Locale::en)} +
+ } + } + None => { + view! { +
+ } + } + } + }} +
Total Transactions
+
+
+
+ + {move || { + match totals_res.get() { + Some(totals) => { + view! { +
+ + + + {format!( + "{:.*}", + 5, + totals.tx_volume.msats as f64 / 100_000_000_000f64, + )} +
+ } + } + None => { + view! { +
+ } + } + } + }} +
Total Volume
+
+
+ } +} + +async fn fetch_federation_totals() -> anyhow::Result { + let url = format!("{}/federations/totals", crate::BASE_URL); + let res = reqwest::get(&url).await?; + Ok(res.json().await?) +} diff --git a/fmo_frontend/src/components/tabs.rs b/fmo_frontend/src/components/tabs.rs index 77aa297..2e13a65 100644 --- a/fmo_frontend/src/components/tabs.rs +++ b/fmo_frontend/src/components/tabs.rs @@ -38,10 +38,17 @@ pub fn Tabs(#[prop(into)] default: String, children: Children) -> impl IntoView
  • - { tab_name } + {tab_name}
  • } @@ -62,9 +69,7 @@ pub fn Tabs(#[prop(into)] default: String, children: Children) -> impl IntoView view! {
    -
      - { tabs } -
    +
      {tabs}
    {move || (get_tab_content(active_tab.get()))()} } From da992d03788a2356d55c0c50c829f08dc887c8e8 Mon Sep 17 00:00:00 2001 From: elsirion Date: Sun, 18 Aug 2024 16:08:03 +0200 Subject: [PATCH 3/4] feat: basic degraded federation UI Still need to impl automatic detection by the backend --- .../components/federations/federation_row.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/fmo_frontend/src/components/federations/federation_row.rs b/fmo_frontend/src/components/federations/federation_row.rs index 1eed150..a4baf12 100644 --- a/fmo_frontend/src/components/federations/federation_row.rs +++ b/fmo_frontend/src/components/federations/federation_row.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use fedimint_core::config::FederationId; use fedimint_core::Amount; use leptos::{component, view, IntoView}; @@ -14,6 +16,11 @@ pub fn FederationRow( avg_txs: f64, avg_volume: Amount, ) -> impl IntoView { + let degraded_federations = vec![FederationId::from_str( + "4b13a146ee4ba732b2b8914a72a0a2e5873e3e942da2d4eeefd85a5fe41f27ba", + ) + .expect("can be parsed")]; + view! {
    - + + {if degraded_federations.contains(&id) { + view! { + + "Degraded" + + } + .into_view() + } else { + view! { }.into_view() + }} + {total_assets.as_bitcoin(6).to_string()} From 437386c586e5bd77ffaa61823291f3971070cab3 Mon Sep 17 00:00:00 2001 From: elsirion Date: Wed, 4 Sep 2024 10:19:22 +0200 Subject: [PATCH 4/4] feat: add federation counter --- fmo_api_types/src/lib.rs | 1 + .../src/components/federations/totals.rs | 23 +++++++++++++++++-- fmo_server/src/federation/observer.rs | 5 +++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/fmo_api_types/src/lib.rs b/fmo_api_types/src/lib.rs index 27d73e9..269b3db 100644 --- a/fmo_api_types/src/lib.rs +++ b/fmo_api_types/src/lib.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FedimintTotals { + pub federations: u64, pub tx_volume: Amount, pub tx_count: u64, } diff --git a/fmo_frontend/src/components/federations/totals.rs b/fmo_frontend/src/components/federations/totals.rs index ba09fc1..9db2838 100644 --- a/fmo_frontend/src/components/federations/totals.rs +++ b/fmo_frontend/src/components/federations/totals.rs @@ -28,7 +28,26 @@ pub fn Totals() -> impl IntoView { view! {
    - + {move || { + match totals_res.get() { + Some(totals) => { + view! { +
    + {totals.federations.to_formatted_string(&Locale::en)} +
    + } + } + None => { + view! { +
    + } + } + } + }} +
    Public Federations
    +
    +
    +
    {move || { match totals_res.get() { Some(totals) => { @@ -49,7 +68,6 @@ pub fn Totals() -> impl IntoView {
    - {move || { match totals_res.get() { Some(totals) => { @@ -71,6 +89,7 @@ pub fn Totals() -> impl IntoView { 5, totals.tx_volume.msats as f64 / 100_000_000_000f64, )} +
    } } diff --git a/fmo_server/src/federation/observer.rs b/fmo_server/src/federation/observer.rs index c7664f3..a8c6745 100644 --- a/fmo_server/src/federation/observer.rs +++ b/fmo_server/src/federation/observer.rs @@ -1080,6 +1080,7 @@ impl FederationObserver { pub async fn totals(&self) -> anyhow::Result { #[derive(Debug, FromRow)] struct FedimintTotalsResult { + federations: i64, tx_count: i64, tx_volume: i64, } @@ -1088,7 +1089,8 @@ impl FederationObserver { &self.connection().await?, // language=postgresql " - SELECT (SELECT count(*) from transactions)::bigint as tx_count, + SELECT (SELECT count(*) from federations)::bigint as federations, + (SELECT count(*) from transactions)::bigint as tx_count, (SELECT sum(amount_msat) from transaction_inputs)::bigint as tx_volume ", &[], @@ -1096,6 +1098,7 @@ impl FederationObserver { .await?; Ok(FedimintTotals { + federations: totals.federations as u64, tx_count: totals.tx_count as u64, tx_volume: Amount::from_msats(totals.tx_volume as u64), })