diff --git a/Cargo.lock b/Cargo.lock index be04809..8064d36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1272,7 +1272,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types 0.1.3", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -1285,7 +1285,7 @@ dependencies = [ "bitflags 2.6.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -2831,6 +2831,15 @@ dependencies = [ "ttf-parser 0.21.1", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -2838,7 +2847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -2852,6 +2861,12 @@ dependencies = [ "syn 2.0.95", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -3315,6 +3330,7 @@ dependencies = [ "diesel", "diesel_migrations", "fedimint-api-client", + "fedimint-arti-client", "fedimint-bip39", "fedimint-client", "fedimint-core", @@ -3325,11 +3341,18 @@ dependencies = [ "futures", "hex", "home", + "http-body-util", + "hyper 1.5.0", + "hyper-util", "log", + "once_cell", + "reqwest 0.12.12", "rusqlite", "serde", + "serde_json", "tempdir", "tokio", + "tokio-native-tls", "uuid", ] @@ -3666,6 +3689,22 @@ dependencies = [ "webpki-roots 0.26.6", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -4711,7 +4750,7 @@ dependencies = [ "bitflags 2.6.0", "block", "core-graphics-types 0.1.3", - "foreign-types", + "foreign-types 0.5.0", "log", "objc", "paste", @@ -4808,6 +4847,23 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -5201,6 +5257,38 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-src" version = "300.3.2+3.3.2" @@ -6107,7 +6195,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-rustls 0.24.1", "tokio-socks", @@ -6137,11 +6225,13 @@ dependencies = [ "http-body-util", "hyper 1.5.0", "hyper-rustls 0.27.3", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -6153,7 +6243,9 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", + "system-configuration 0.6.1", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.0", "tokio-socks", "tokio-util", @@ -6464,6 +6556,15 @@ dependencies = [ "regex", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scheduled-thread-pool" version = "0.2.7" @@ -6543,6 +6644,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.0.4" @@ -6611,9 +6735,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -6908,7 +7032,7 @@ dependencies = [ "core-graphics 0.24.0", "drm", "fastrand", - "foreign-types", + "foreign-types 0.5.0", "js-sys", "log", "memmap2", @@ -7190,7 +7314,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -7203,6 +7338,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -7405,6 +7550,16 @@ dependencies = [ "syn 2.0.95", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" diff --git a/harbor-client/Cargo.toml b/harbor-client/Cargo.toml index aad9e7e..cde2e80 100644 --- a/harbor-client/Cargo.toml +++ b/harbor-client/Cargo.toml @@ -6,13 +6,14 @@ edition = "2021" [features] default = [] vendored = ["rusqlite/bundled-sqlcipher-vendored-openssl"] -disable-tor = [] +disable-tor = ["reqwest"] [dependencies] anyhow = "1.0.89" log = "0.4" tokio = { version = "1", features = ["full"] } serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.138" chrono = "0.4.38" rusqlite = { version = "0.28.0", features = ["sqlcipher"] } diesel = { version = "2.1.6", features = ["sqlite", "chrono", "r2d2"] } @@ -23,9 +24,11 @@ async-trait = "0.1.77" bincode = "1.3.3" hex = "0.4.3" home = "0.5.9" +once_cell = "1.20.2" bitcoin = { version = "0.32.4", features = ["base64"] } bip39 = "2.0.0" + fedimint-api-client = "0.5.0" fedimint-client = "0.5.0" fedimint-core = "0.5.0" @@ -34,6 +37,15 @@ fedimint-mint-client = "0.5.0" fedimint-ln-client = "0.5.0" fedimint-bip39 = "0.5.0" fedimint-ln-common = "0.5.0" +# update this when updating fedimint if we need too +arti-client = { version = "0.20.0", default-features = false, package = "fedimint-arti-client" } + +http-body-util = "0.1.0" +hyper = { version = "1", features = ["http1", "client"] } +hyper-util = { version = "0.1.1", features = ["tokio"] } +tokio-native-tls = "0.3.1" + +reqwest = { version = "0.12.12", features = ["json"], optional = true} [dev-dependencies] tempdir = "0.3.7" diff --git a/harbor-client/src/db_models/mod.rs b/harbor-client/src/db_models/mod.rs index 4098995..8c1317d 100644 --- a/harbor-client/src/db_models/mod.rs +++ b/harbor-client/src/db_models/mod.rs @@ -20,6 +20,7 @@ pub(crate) mod schema; pub mod transaction_item; +use crate::metadata::FederationMeta; use fedimint_core::config::FederationId; use fedimint_core::core::ModuleKind; @@ -30,6 +31,7 @@ pub struct FederationItem { pub balance: u64, pub guardians: Option>, pub module_kinds: Option>, + pub metadata: FederationMeta, } impl FederationItem { @@ -40,6 +42,7 @@ impl FederationItem { balance: 0, guardians: None, module_kinds: None, + metadata: FederationMeta::default(), } } } diff --git a/harbor-client/src/http.rs b/harbor-client/src/http.rs new file mode 100644 index 0000000..bca1a1d --- /dev/null +++ b/harbor-client/src/http.rs @@ -0,0 +1,134 @@ +#[cfg(not(feature = "disable-tor"))] +use arti_client::{TorAddr, TorClient, TorClientConfig}; +#[cfg(not(feature = "disable-tor"))] +use fedimint_core::util::SafeUrl; +#[cfg(not(feature = "disable-tor"))] +use http_body_util::{BodyExt, Empty}; +#[cfg(not(feature = "disable-tor"))] +use hyper::body::Bytes; +#[cfg(not(feature = "disable-tor"))] +use hyper::Request; +#[cfg(not(feature = "disable-tor"))] +use hyper_util::rt::TokioIo; +use serde::de::DeserializeOwned; +#[cfg(not(feature = "disable-tor"))] +use tokio::io::{AsyncRead, AsyncWrite}; +#[cfg(not(feature = "disable-tor"))] +use tokio_native_tls::native_tls::TlsConnector; + +#[cfg(not(feature = "disable-tor"))] +pub(crate) async fn make_get_request(url: &str) -> anyhow::Result { + let tor_config = TorClientConfig::default(); + let tor_client = TorClient::create_bootstrapped(tor_config) + .await? + .isolated_client(); + + let safe_url = SafeUrl::parse(url)?; + let https = safe_url.scheme() == "https"; + + log::debug!("Successfully created and bootstrapped the `TorClient`, for given `TorConfig`."); + + let host = safe_url + .host_str() + .ok_or_else(|| anyhow::anyhow!("Expected host str"))?; + let port = safe_url + .port_or_known_default() + .ok_or_else(|| anyhow::anyhow!("Expected port number"))?; + let tor_addr = TorAddr::from((host, port)) + .map_err(|e| anyhow::anyhow!("Invalid endpoint addr: {:?}: {e:#}", (host, port)))?; + + log::debug!("Successfully created `TorAddr` for given address (i.e. host and port)"); + + let stream = if safe_url.is_onion_address() { + let mut stream_prefs = arti_client::StreamPrefs::default(); + stream_prefs.connect_to_onion_services(arti_client::config::BoolOrAuto::Explicit(true)); + + let anonymized_stream = tor_client + .connect_with_prefs(tor_addr, &stream_prefs) + .await?; + + log::debug!("Successfully connected to onion address `TorAddr`, and established an anonymized `DataStream`"); + anonymized_stream + } else { + let anonymized_stream = tor_client.connect(tor_addr).await?; + + log::debug!("Successfully connected to `Hostname`or `Ip` `TorAddr`, and established an anonymized `DataStream`"); + anonymized_stream + }; + + let res = if https { + let cx = TlsConnector::builder().build()?; + let cx = tokio_native_tls::TlsConnector::from(cx); + let stream = cx.connect(host, stream).await?; + make_request(&safe_url, stream).await? + } else { + make_request(&safe_url, stream).await? + }; + + Ok(res) +} + +#[cfg(not(feature = "disable-tor"))] +async fn make_request( + url: &SafeUrl, + stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static, +) -> anyhow::Result { + let (mut request_sender, connection) = + hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?; + + // spawn a task to poll the connection and drive the HTTP state + tokio::spawn(async move { + connection.await.unwrap(); + }); + + let req = Request::get(url.as_str()) + .header("Host", url.host_str().expect("already checked for host")) + .body(Empty::::new())?; + let mut resp = request_sender.send_request(req).await?; + + let len: usize = resp + .headers() + .get("content-length") + .and_then(|h| h.to_str().ok().and_then(|s| s.parse().ok())) + .unwrap_or(10_000); + + // if over 20MB, something is going wrong + if len > 20000000 { + return Err(anyhow::anyhow!( + "Received too large of response, size: {len}" + )); + } + + let mut buf: Vec = Vec::with_capacity(len); + while let Some(frame) = resp.body_mut().frame().await { + let bytes = frame?.into_data().unwrap(); + buf.extend_from_slice(&bytes); + } + + Ok(serde_json::from_slice::(&buf)?) +} + +#[cfg(feature = "disable-tor")] +pub(crate) async fn make_get_request(url: &str) -> anyhow::Result { + reqwest::get(url) + .await? + .json() + .await + .map_err(anyhow::Error::from) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metadata::FederationMetaConfig; + + #[tokio::test] + async fn test_fetch_metadata() { + let res = + make_get_request::("https://meta.dev.fedibtc.com/meta.json") + .await + .unwrap(); + + assert!(!res.federations.is_empty()); + } +} diff --git a/harbor-client/src/lib.rs b/harbor-client/src/lib.rs index 8b49128..d312644 100644 --- a/harbor-client/src/lib.rs +++ b/harbor-client/src/lib.rs @@ -6,6 +6,7 @@ use crate::fedimint_client::{ spawn_invoice_receive_subscription, spawn_onchain_payment_subscription, spawn_onchain_receive_subscription, FederationInviteOrId, FedimintClient, }; +use crate::metadata::{get_federation_metadata, FederationData, FederationMeta, CACHE}; use anyhow::anyhow; use bip39::Mnemonic; use bitcoin::address::NetworkUnchecked; @@ -47,6 +48,8 @@ pub fn data_dir(network: Network) -> PathBuf { pub mod db; pub mod db_models; pub mod fedimint_client; +mod http; +pub mod metadata; #[derive(Debug, Clone)] pub struct UICoreMsgPacket { @@ -80,6 +83,7 @@ pub enum UICoreMsg { GetFederationInfo(InviteCode), AddFederation(InviteCode), RemoveFederation(FederationId), + FederationListNeedsUpdate, Unlock(String), Init { password: String, @@ -122,12 +126,19 @@ pub enum CoreUIMsg { TransferFailure(String), // todo probably want a way to incrementally add items to the history TransactionHistoryUpdated(Vec), - FederationBalanceUpdated { id: FederationId, balance: Amount }, + FederationBalanceUpdated { + id: FederationId, + balance: Amount, + }, AddFederationFailed(String), RemoveFederationFailed(String), - FederationInfo(ClientConfig), + FederationInfo { + config: ClientConfig, + metadata: FederationMeta, + }, AddFederationSuccess, RemoveFederationSuccess, + FederationListNeedsUpdate, FederationListUpdated(Vec), NeedsInit, Initing, @@ -467,7 +478,7 @@ impl HarborCore { pub async fn get_federation_info( &self, invite_code: InviteCode, - ) -> anyhow::Result { + ) -> anyhow::Result<(ClientConfig, FederationMeta)> { log::info!("Getting federation info for invite code: {invite_code}"); let download = Instant::now(); let config = { @@ -494,7 +505,17 @@ impl HarborCore { download.elapsed().as_millis() ); - Ok(config) + let mut cache = CACHE.write().await; + let metadata = match cache.get(&invite_code.federation_id()).cloned() { + None => { + let m = get_federation_metadata(FederationData::Config(&config)).await; + cache.insert(invite_code.federation_id(), m.clone()); + m + } + Some(metadata) => metadata, + }; + + Ok((config, metadata)) } pub async fn add_federation(&self, invite_code: InviteCode) -> anyhow::Result<()> { @@ -535,8 +556,10 @@ impl HarborCore { pub async fn get_federation_items(&self) -> Vec { let clients = self.clients.read().await; + let metadata_cache = CACHE.read().await; + // Tell the UI about any clients we have - join_all(clients.values().map(|c| async { + let res = join_all(clients.values().map(|c| async { let balance = c.fedimint_client.get_balance().await; let config = c.fedimint_client.config().await; @@ -553,6 +576,11 @@ impl HarborCore { .map(|module_config| module_config.kind().to_owned()) .collect::>(); + // get metadata from in memory cache + let metadata = metadata_cache + .get(&c.fedimint_client.federation_id()) + .cloned(); + FederationItem { id: c.fedimint_client.federation_id(), name: c @@ -562,9 +590,43 @@ impl HarborCore { balance: balance.sats_round_down(), guardians: Some(guardians), module_kinds: Some(module_kinds), + metadata: metadata.unwrap_or_default(), } })) - .await + .await; + + drop(metadata_cache); + + // go through federations metadata and start background task to fetch + let needs_metadata = res + .iter() + .filter(|f| f.metadata == FederationMeta::default()) + .flat_map(|f| clients.get(&f.id).map(|c| c.fedimint_client.clone())) + .collect::>(); + + // if we're missing metadata for federations, start background task to populate it + if !needs_metadata.is_empty() { + let mut tx = self.tx.clone(); + tokio::task::spawn(async move { + let mut w = CACHE.write().await; + for client in needs_metadata { + let id = client.federation_id(); + let metadata = get_federation_metadata(FederationData::Client(&client)).await; + w.insert(id, metadata); + } + drop(w); + + // update list in front end + tx.send(CoreUIMsgPacket { + id: None, + msg: CoreUIMsg::FederationListNeedsUpdate, + }) + .await + .expect("federation list needs update"); + }); + } + + res } pub async fn get_seed_words(&self) -> String { diff --git a/harbor-client/src/metadata.rs b/harbor-client/src/metadata.rs new file mode 100644 index 0000000..39bce56 --- /dev/null +++ b/harbor-client/src/metadata.rs @@ -0,0 +1,137 @@ +use crate::http::make_get_request; +use bitcoin::secp256k1::PublicKey; +use fedimint_client::ClientHandleArc; +use fedimint_core::config::ClientConfig; +use fedimint_core::config::FederationId; +use fedimint_core::module::serde_json; +use log::error; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tokio::sync::RwLock; + +/// Global cache of federation metadata +pub(crate) static CACHE: Lazy>> = + Lazy::new(|| RwLock::new(HashMap::new())); + +pub(crate) enum FederationData<'a> { + Client(&'a ClientHandleArc), + Config(&'a ClientConfig), +} + +impl FederationData<'_> { + pub(crate) fn get_meta(&self, str: &str) -> Option { + match self { + FederationData::Client(c) => c.get_meta(str), + FederationData::Config(c) => c.meta(str).ok().flatten(), + } + } + + pub(crate) fn federation_id(&self) -> FederationId { + match self { + FederationData::Client(c) => c.federation_id(), + FederationData::Config(c) => c.global.calculate_federation_id(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct FederationMetaConfig { + #[serde(flatten)] + pub federations: HashMap, +} + +/// Metadata we might get from the federation +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug, Default)] +pub struct FederationMeta { + // https://github.com/fedimint/fedimint/tree/master/docs/meta_fields + pub federation_name: Option, + federation_expiry_timestamp: Option, + pub welcome_message: Option, + vetted_gateways: Option, + // undocumented parameters that fedi uses: https://meta.dev.fedibtc.com/meta.json + pub federation_icon_url: Option, + pub meta_external_url: Option, + pub preview_message: Option, + pub popup_end_timestamp: Option, + pub popup_countdown_message: Option, +} + +impl FederationMeta { + pub fn federation_expiry_timestamp(&self) -> Option { + self.federation_expiry_timestamp + .as_ref() + .and_then(|s| s.parse().ok()) + } + + pub fn vetted_gateways(&self) -> Vec { + match self.vetted_gateways.as_deref() { + None => vec![], + Some(str) => serde_json::from_str(str).unwrap_or_default(), + } + } +} + +pub(crate) async fn get_federation_metadata(data: FederationData<'_>) -> FederationMeta { + let meta_external_url = data.get_meta("meta_external_url"); + let config: Option = match meta_external_url.as_ref() { + None => None, + Some(url) => match make_get_request::(url).await { + Ok(m) => m + .federations + .get(&data.federation_id().to_string()) + .cloned(), + Err(e) => { + error!("Error fetching external metadata: {}", e); + None + } + }, + }; + + FederationMeta { + meta_external_url, // Already set... + federation_name: merge_values( + data.get_meta("federation_name").clone(), + config.as_ref().and_then(|c| c.federation_name.clone()), + ), + federation_expiry_timestamp: merge_values( + data.get_meta("federation_expiry_timestamp"), + config + .as_ref() + .and_then(|c| c.federation_expiry_timestamp.clone()), + ), + welcome_message: merge_values( + data.get_meta("welcome_message"), + config.as_ref().and_then(|c| c.welcome_message.clone()), + ), + vetted_gateways: config.as_ref().and_then(|c| c.vetted_gateways.clone()), + federation_icon_url: merge_values( + data.get_meta("federation_icon_url"), + config.as_ref().and_then(|c| c.federation_icon_url.clone()), + ), + preview_message: merge_values( + data.get_meta("preview_message"), + config.as_ref().and_then(|c| c.preview_message.clone()), + ), + popup_end_timestamp: merge_values( + data.get_meta("popup_end_timestamp"), + config.as_ref().and_then(|c| c.popup_end_timestamp.clone()), + ), + popup_countdown_message: merge_values( + data.get_meta("popup_countdown_message") + .map(|v| v.to_string()), + config + .as_ref() + .and_then(|c| c.popup_countdown_message.clone()), + ), + } +} + +fn merge_values(a: Option, b: Option) -> Option { + match (a, b) { + // If a has value return that; otherwise, use the one from b if available. + (Some(val), _) => Some(val), + (None, Some(val)) => Some(val), + (None, None) => None, + } +} diff --git a/harbor-ui/src/bridge.rs b/harbor-ui/src/bridge.rs index 64eb26e..6fe4d10 100644 --- a/harbor-ui/src/bridge.rs +++ b/harbor-ui/src/bridge.rs @@ -398,8 +398,9 @@ async fn process_core(core_handle: &mut CoreHandle, core: &HarborCore) { core.msg(msg.id, CoreUIMsg::AddFederationFailed(e.to_string())) .await; } - Ok(config) => { - core.msg(msg.id, CoreUIMsg::FederationInfo(config)).await; + Ok((config, metadata)) => { + core.msg(msg.id, CoreUIMsg::FederationInfo { config, metadata }) + .await; } } } @@ -433,6 +434,14 @@ async fn process_core(core_handle: &mut CoreHandle, core: &HarborCore) { core.msg(msg.id, CoreUIMsg::RemoveFederationSuccess).await; } } + UICoreMsg::FederationListNeedsUpdate => { + let new_federation_list = core.get_federation_items().await; + core.msg( + msg.id, + CoreUIMsg::FederationListUpdated(new_federation_list), + ) + .await; + } UICoreMsg::GetSeedWords => { let seed_words = core.get_seed_words().await; core.msg(msg.id, CoreUIMsg::SeedWords(seed_words)).await; diff --git a/harbor-ui/src/components/federation_item.rs b/harbor-ui/src/components/federation_item.rs index 8a0df45..8f9fce2 100644 --- a/harbor-ui/src/components/federation_item.rs +++ b/harbor-ui/src/components/federation_item.rs @@ -14,6 +14,7 @@ pub fn h_federation_item(item: &FederationItem, invite_code: Option) -> balance, guardians, module_kinds: _, // We don't care about module kinds + metadata, } = item; let name_row = row![map_icon(SvgIcon::People, 24., 24.), text(name).size(24)] @@ -32,6 +33,10 @@ pub fn h_federation_item(item: &FederationItem, invite_code: Option) -> column = column.push(text(guardian_text).size(18).style(subtitle)); } + if let Some(welcome) = metadata.welcome_message.as_ref() { + column = column.push(text(welcome).size(18).style(subtitle)); + } + match invite_code { // Preview mode with Add button Some(code) => { diff --git a/harbor-ui/src/main.rs b/harbor-ui/src/main.rs index 3320b0d..510e645 100644 --- a/harbor-ui/src/main.rs +++ b/harbor-ui/src/main.rs @@ -887,7 +887,11 @@ impl HarborWallet { }) }) } - CoreUIMsg::FederationInfo(config) => { + CoreUIMsg::FederationListNeedsUpdate => { + let (_, task) = self.send_from_ui(UICoreMsg::FederationListNeedsUpdate); + task + } + CoreUIMsg::FederationInfo { config, metadata } => { let id = config.calculate_federation_id(); let name = config.meta::("federation_name"); let guardians: Vec = config @@ -914,6 +918,7 @@ impl HarborWallet { balance: 0, guardians: Some(guardians), module_kinds: Some(module_kinds), + metadata, }; self.peek_federation_item = Some(item);