Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
let
pkgs = import nixpkgs {
inherit system;
overlays = [
(final: prev: {
nodePackages = {
bash-language-server = final.bash-language-server;
};
})
];
};
flakeboxLib = flakebox.lib.mkLib pkgs { };
lib = pkgs.lib;
Expand All @@ -33,7 +40,7 @@
wasm-pack
trunk
nodejs
nodePackages.tailwindcss
tailwindcss
];
};
targets = (pkgs.lib.getAttrs
Expand Down Expand Up @@ -130,7 +137,7 @@
wasm-pack
nodejs
binaryen
nodePackages.tailwindcss
tailwindcss
];

FMO_API_SERVER = api;
Expand All @@ -157,7 +164,7 @@
# Get the npm dependencies hash
# To update: nix build .#fmo_frontend_react_default --impure
# and use the hash from the error message
npmDepsHash = "sha256-d04Zjrg1mOhnO7FgG6rvDSh0ovt+z7PqIE8FCSG2Czk=";
npmDepsHash = "sha256-j+iWBBnZQLHcFx65dkyET3nKjT3W7WQQDI722xVyJNg=";
in
rec {
fmo_frontend_react = api: pkgs.buildNpmPackage {
Expand Down
1 change: 1 addition & 0 deletions fmo_api_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ bitcoin = { version = "0.32.5", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
fedimint-core = { workspace = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
115 changes: 115 additions & 0 deletions fmo_api_types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bitcoin::address::NetworkUnchecked;
use chrono::{DateTime, Utc};
use fedimint_core::config::FederationId;
use fedimint_core::Amount;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -40,10 +41,70 @@ pub struct FederationUtxo {
pub amount: Amount,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FederationUtxosResponse {
pub observed: Vec<FederationUtxo>,
pub guardian_claims: Vec<GuardianUtxoClaim>,
pub disagreements: Vec<GuardianUtxoDisagreement>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardianUtxoClaim {
pub guardian_id: u16,
pub status: GuardianUtxoClaimStatus,
pub utxos: Vec<GuardianClaimedUtxo>,
pub error: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardianUtxoClaimStatus {
Unavailable,
Ok,
Error,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardianClaimedUtxo {
pub out_point: bitcoin::OutPoint,
pub amount: Amount,
pub state: GuardianClaimedUtxoState,
#[serde(skip_serializing_if = "Option::is_none")]
pub onchain: Option<GuardianClaimedUtxoOnchain>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution_error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardianClaimedUtxoOnchain {
pub script_pubkey: String,
pub address: Option<String>,
pub amount: Amount,
pub confirmed: bool,
pub block_height: Option<u32>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardianClaimedUtxoState {
Spendable,
UnsignedPegOut,
UnsignedChange,
UnconfirmedPegOut,
UnconfirmedChange,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardianUtxoDisagreement {
pub out_point: bitcoin::OutPoint,
pub description: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardianHealth {
pub avg_uptime: f32,
pub avg_latency: f32,
pub software_version: Option<String>,
pub latest: Option<GuardianHealthLatest>,
}

Expand All @@ -63,6 +124,60 @@ pub enum FederationHealth {
Offline,
}

/// Subset of a gateway's registration info suitable for public API responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayInfo {
/// Gateway's public key (hex-encoded)
pub gateway_id: String,
/// LN node public key (hex-encoded)
pub node_pub_key: String,
pub lightning_alias: String,
/// URL of the gateway's public API
pub api_endpoint: String,
/// Whether the federation has vetted this gateway
pub vetted: bool,
/// Full raw announcement, useful for forwards-compatible client usage
#[serde(skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
/// First time this gateway was seen by the observer
#[serde(skip_serializing_if = "Option::is_none")]
pub first_seen: Option<DateTime<Utc>>,
/// Most recent time this gateway was seen by the observer
#[serde(skip_serializing_if = "Option::is_none")]
pub last_seen: Option<DateTime<Utc>>,
/// Real LN activity metrics over the last 7 days
#[serde(skip_serializing_if = "Option::is_none")]
pub activity_7d: Option<GatewayActivityMetrics>,
/// Real LN activity metrics over the requested API window
#[serde(skip_serializing_if = "Option::is_none")]
pub activity_window: Option<GatewayActivityMetrics>,
/// Uptime metrics computed from periodic gateway snapshots over the
/// requested window
#[serde(skip_serializing_if = "Option::is_none")]
pub uptime_window: Option<GatewayUptimeMetrics>,
/// The window label used for `activity_window` and `uptime_window`, e.g.
/// `7d`
#[serde(skip_serializing_if = "Option::is_none")]
pub metrics_window: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayActivityMetrics {
pub fund_count: u64,
pub settle_count: u64,
pub cancel_count: u64,
pub total_volume_msat: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayUptimeMetrics {
pub sample_count: u64,
pub seen_samples: u64,
pub online_minutes: u64,
pub offline_minutes: u64,
pub uptime_pct: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoncesRequest {
pub nonces: Vec<String>,
Expand Down
8 changes: 8 additions & 0 deletions fmo_frontend/src/components/federation/guardians.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub fn Guardians(federation_id: FederationId, guardians: Vec<Guardian>) -> impl
let health = health.get(&PeerId::from(guardian_idx as u16)).expect("Guardian exists").clone();

let mut badges = vec![];
let software_version = health
.software_version
.unwrap_or_else(|| "Version unknown".to_owned());
if let Some(latest) = health.latest {
badges.push(view! {
<Badge level=BadgeLevel::Success>
Expand Down Expand Up @@ -77,6 +80,11 @@ pub fn Guardians(federation_id: FederationId, guardians: Vec<Guardian>) -> impl
</Badge>
}.into_view());
}
badges.push(view! {
<Badge level=BadgeLevel::Info>
{software_version}
</Badge>
}.into_view());

view! { {badges} }.into_any()
}
Expand Down
50 changes: 50 additions & 0 deletions fmo_frontend/src/components/federation/stars_selector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use leptos::prelude::*;

#[component]
pub fn StarsSelector(default_value: u8, selected_stars: WriteSignal<u8>) -> impl IntoView {
selected_stars.set(default_value);

let (selected, set_selected) = signal(default_value);
let (hover, set_hover) = signal(None);

let star = move |star_idx: u8, fill: bool, border: bool| {
view! {
<svg
class={
if fill {
"w-6 h-6 ms-2 text-yellow-300"
} else {
"w-6 h-6 ms-2 text-gray-300 dark:text-gray-500"
}
}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
stroke=border.then_some("orange")
stroke-width=border.then_some("2px")
viewBox="0 0 22 20"
on:mouseenter=move |_| {set_hover.set(Some(star_idx));}
on:click=move |_| {
set_selected.set(star_idx);
selected_stars.set(star_idx);
}
on:mouseleave=move |_| {set_hover.set(None);}
>
<path d="M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z"/>
</svg>
}
};

view! {
<div
class="inline-flex px-1 py-2"
on:mouseleave=move |_| {set_hover.set(None);}
>
{ move || {
let selected = selected.get();
let hover = hover.get();
(1..=5).map(|idx| star(idx, selected >= idx, hover.is_some_and(|h| h >= idx))).collect::<Vec<_>>()
}}
</div>
}
}
17 changes: 10 additions & 7 deletions fmo_frontend_react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions fmo_frontend_react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NavBar } from './components/NavBar';
import { Home } from './pages/Home';
import { Nostr } from './pages/Nostr';
import { FederationDetail } from './pages/FederationDetail';
import { FederationGateways } from './pages/FederationGateways';
import { useTheme } from './hooks/useTheme';

function App() {
Expand All @@ -16,6 +17,7 @@ function App() {
<Route path="/" element={<Home />} />
<Route path="/nostr" element={<Nostr />} />
<Route path="/federations/:id" element={<FederationDetail />} />
<Route path="/federations/:id/gateways" element={<FederationGateways />} />
<Route path="*" element={<div className="p-4 text-gray-900 dark:text-white">Page not found</div>} />
</Routes>
</main>
Expand Down
42 changes: 42 additions & 0 deletions fmo_frontend_react/src/components/GatewayWarningPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export type GatewayWarningLevel = 'info' | 'warning' | 'error';

export interface GatewayWarningState {
level: GatewayWarningLevel;
title: string;
message: string;
detail?: string;
}

interface GatewayWarningPageProps {
warning: GatewayWarningState;
className?: string;
}

function levelClasses(level: GatewayWarningLevel): string {
switch (level) {
case 'info':
return 'border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-700 dark:bg-blue-900/30 dark:text-blue-200';
case 'error':
return 'border-red-200 bg-red-50 text-red-900 dark:border-red-700 dark:bg-red-900/30 dark:text-red-200';
default:
return 'border-yellow-200 bg-yellow-50 text-yellow-900 dark:border-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-200';
}
}

export function GatewayWarningPage({ warning, className = '' }: GatewayWarningPageProps) {
return (
<section
className={`rounded-lg border p-4 sm:p-5 ${levelClasses(warning.level)} ${className}`}
role="status"
aria-live="polite"
>
<h2 className="text-sm sm:text-base font-semibold">{warning.title}</h2>
<p className="mt-1 text-sm leading-relaxed">{warning.message}</p>
{warning.detail && (
<p className="mt-2 text-xs sm:text-sm opacity-90 break-words">
{warning.detail}
</p>
)}
</section>
);
}
Loading
Loading