Skip to content
Open
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
37 changes: 37 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ fedimint-api-client = { version = "0.12.0", git = "https://github.com/fedimint/f
fedimint-connectors = { version = "0.12.0", git = "https://github.com/fedimint/fedimint", tag = "v0.12.0" }
fedimint-core = { version = "0.12.0", git = "https://github.com/fedimint/fedimint", tag = "v0.12.0" }
fedimint-ln-common = { version = "0.12.0", git = "https://github.com/fedimint/fedimint", tag = "v0.12.0" }
fedimint-lnv2-common = { version = "0.12.0", git = "https://github.com/fedimint/fedimint", tag = "v0.12.0" }
fedimint-meta-client = { version = "0.12.0", git = "https://github.com/fedimint/fedimint", tag = "v0.12.0" }
fedimint-mint-common = { version = "0.12.0", git = "https://github.com/fedimint/fedimint", tag = "v0.12.0" }
fedimint-wallet-common = { version = "0.12.0", git = "https://github.com/fedimint/fedimint", tag = "v0.12.0" }
59 changes: 59 additions & 0 deletions fmo_api_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,65 @@ pub struct GatewayInfo {
/// `7d`
#[serde(skip_serializing_if = "Option::is_none")]
pub metrics_window: Option<String>,
/// Lightning module protocols this gateway is registered under. A gateway
/// appears under both only if its identity was verified to match.
#[serde(skip_serializing_if = "Option::is_none")]
pub protocols: Option<Vec<GatewayProtocol>>,
/// LNv2 registration details, present if the gateway is registered with
/// the federation's LNv2 module
#[serde(skip_serializing_if = "Option::is_none")]
pub lnv2: Option<Lnv2GatewayInfo>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GatewayProtocol {
Lnv1,
Lnv2,
}

/// Result of asking an LNv2 gateway for its routing info. `Ok` only means the
/// gateway advertises routing info for this federation, not that payments
/// through it succeed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Lnv2RoutingStatus {
/// The gateway returned routing info for this federation
Ok,
/// The gateway answered but does not serve this federation
NotServing,
/// The gateway could not be reached or returned an invalid response
Unreachable,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Lnv2GatewayInfo {
/// Gateway API URL as listed in the federation's LNv2 registry
pub api_endpoint: String,
pub routing_status: Lnv2RoutingStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub routing_checked_at: Option<DateTime<Utc>>,
/// LN node public key (hex-encoded), from the gateway's routing info
#[serde(skip_serializing_if = "Option::is_none")]
pub lightning_public_key: Option<String>,
/// Key the gateway uses to claim and refund LNv2 contracts (hex-encoded)
#[serde(skip_serializing_if = "Option::is_none")]
pub module_public_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_fee_minimum: Option<GatewayFee>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_fee_default: Option<GatewayFee>,
#[serde(skip_serializing_if = "Option::is_none")]
pub receive_fee: Option<GatewayFee>,
/// Registry presence of the LNv2 registration over the requested window
#[serde(skip_serializing_if = "Option::is_none")]
pub uptime_window: Option<GatewayUptimeMetrics>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayFee {
pub base_msat: u64,
pub parts_per_million: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
25 changes: 25 additions & 0 deletions fmo_frontend_react/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ export interface GatewayInfo {
activity_window?: GatewayActivityMetrics;
uptime_window?: GatewayUptimeMetrics;
metrics_window?: GatewayWindow;
protocols?: GatewayProtocol[];
lnv2?: Lnv2GatewayInfo;
}

export type GatewayProtocol = 'lnv1' | 'lnv2';

// 'ok' only means the gateway advertises routing info for this federation,
// not that payments through it succeed.
export type Lnv2RoutingStatus = 'ok' | 'not_serving' | 'unreachable';

export interface Lnv2GatewayInfo {
api_endpoint: string;
routing_status: Lnv2RoutingStatus;
routing_checked_at?: string;
lightning_public_key?: string;
module_public_key?: string;
send_fee_minimum?: GatewayFee;
send_fee_default?: GatewayFee;
receive_fee?: GatewayFee;
uptime_window?: GatewayUptimeMetrics;
}

export interface GatewayFee {
base_msat: number;
parts_per_million: number;
}

export interface GatewayActivityMetrics {
Expand Down
1 change: 1 addition & 0 deletions fmo_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ fedimint-api-client = { workspace = true }
fedimint-connectors = { workspace = true }
fedimint-core = { workspace = true }
fedimint-ln-common = { workspace = true }
fedimint-lnv2-common = { workspace = true }
fedimint-meta-client = { workspace = true }
fedimint-mint-common = { workspace = true }
fedimint-wallet-common = { workspace = true }
Expand Down
53 changes: 53 additions & 0 deletions fmo_server/schema/v11.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- LNv2 gateway discovery: track which Lightning module protocol a gateway is registered under
BEGIN;

INSERT INTO schema_version (version)
VALUES (11);

-- Existing rows all came from the LNv1 (`ln`) module registry. LNv2 rows use the normalized
-- gateway API URL as `gateway_id`, since the LNv2 registry only lists URLs.
ALTER TABLE gateways
ADD COLUMN IF NOT EXISTS protocol TEXT NOT NULL DEFAULT 'lnv1';

ALTER TABLE gateways DROP CONSTRAINT gateways_pkey;
ALTER TABLE gateways ADD PRIMARY KEY (federation_id, protocol, gateway_id);

-- LNv2 identity comes from probing the gateway, which may fail
ALTER TABLE gateways ALTER COLUMN node_pub_key DROP NOT NULL;
ALTER TABLE gateways ALTER COLUMN lightning_alias DROP NOT NULL;

-- LNv2 only: result of the most recent `routing_info` probe
-- 'ok' - the gateway returned routing info for this federation
-- 'not_serving' - the gateway answered but does not serve this federation
-- 'unreachable' - the probe failed or timed out
ALTER TABLE gateways
ADD COLUMN IF NOT EXISTS module_public_key TEXT,
ADD COLUMN IF NOT EXISTS routing_status TEXT,
ADD COLUMN IF NOT EXISTS routing_checked_at TIMESTAMPTZ;

ALTER TABLE gateway_poll_snapshots
ADD COLUMN IF NOT EXISTS protocol TEXT NOT NULL DEFAULT 'lnv1';

ALTER TABLE gateway_poll_snapshots DROP CONSTRAINT gateway_poll_snapshots_pkey;
ALTER TABLE gateway_poll_snapshots ADD PRIMARY KEY (federation_id, protocol, gateway_id, poll_time);

-- `is_seen` remains registry presence; `routing_ok` is LNv2 routing readiness (NULL for LNv1)
ALTER TABLE gateway_poll_snapshots
ADD COLUMN IF NOT EXISTS routing_ok BOOLEAN;

-- Every LNv2 module key ever observed for a gateway, so that contracts can still be
-- attributed after a gateway rotates its key or leaves the registry.
CREATE TABLE IF NOT EXISTS lnv2_gateway_keys (
federation_id BYTEA NOT NULL REFERENCES federations (federation_id),
gateway_id TEXT NOT NULL,
module_public_key TEXT NOT NULL,
lightning_public_key TEXT NOT NULL,
first_seen TIMESTAMPTZ NOT NULL,
last_seen TIMESTAMPTZ NOT NULL,
PRIMARY KEY (federation_id, gateway_id, module_public_key)
);

CREATE INDEX IF NOT EXISTS lnv2_gateway_keys_module_public_key
ON lnv2_gateway_keys (federation_id, module_public_key);

COMMIT;
23 changes: 21 additions & 2 deletions fmo_server/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use std::collections::HashMap;
use std::sync::Arc;

use axum::extract::{Path, State};
use axum::extract::{Path, Query, State};
use axum::routing::get;
use axum::{Json, Router};
use fedimint_core::config::{FederationId, JsonClientConfig};
use fedimint_core::invite_code::InviteCode;
use fmo_api_types::GatewayInfo;
use reqwest::Method;
use serde::Deserialize;
use tower_http::cors::{Any, CorsLayer};
use tracing::warn;

Expand Down Expand Up @@ -59,15 +60,33 @@ pub async fn fetch_federation_config(
.into())
}

#[derive(Debug, Deserialize)]
pub struct FetchFederationGatewaysParams {
/// `all` to include LNv2 gateways. Omitted, only LNv1 gateways are
/// returned, exactly as before LNv2 support existed.
protocols: Option<String>,
}

pub async fn fetch_federation_gateways(
Path(invite): Path<InviteCode>,
Query(params): Query<FetchFederationGatewaysParams>,
) -> Result<Json<Vec<GatewayInfo>>> {
let include_lnv2 = match params.protocols.as_deref() {
None => false,
Some("all") => true,
Some(invalid) => {
return Err(
anyhow::anyhow!("Invalid protocols '{invalid}'. Supported values: all").into(),
)
}
};

let connectors = fedimint_connectors::ConnectorRegistry::build_from_client_env()?
.bind()
.await?;
let (config, _api) =
fedimint_api_client::download_from_invite_code(&connectors, &invite).await?;
let gateways = fetch_gateways_for_config(&config).await?;
let gateways = fetch_gateways_for_config(&config, include_lnv2).await?;
Ok(gateways.into())
}

Expand Down
Loading
Loading