Skip to content
Merged
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
20 changes: 20 additions & 0 deletions harbor-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,26 @@ impl MintIdentifier {
}
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum MintConnectionInfo {
Cashu(MintUrl),
Fedimint(InviteCode),
}

impl FromStr for MintConnectionInfo {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(url) = MintUrl::from_str(s) {
Ok(Self::Cashu(url))
} else if let Ok(invite_code) = InviteCode::from_str(s) {
Ok(Self::Fedimint(invite_code))
} else {
Err(anyhow::anyhow!("Invalid mint connection info: {}", s))
Comment on lines +136 to +145
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parsing order in FromStr implementation could be ambiguous if both MintUrl and InviteCode can successfully parse the same string. Consider documenting the precedence order or adding validation to ensure mutual exclusivity.

Suggested change
impl FromStr for MintConnectionInfo {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(url) = MintUrl::from_str(s) {
Ok(Self::Cashu(url))
} else if let Ok(invite_code) = InviteCode::from_str(s) {
Ok(Self::Fedimint(invite_code))
} else {
Err(anyhow::anyhow!("Invalid mint connection info: {}", s))
/// Attempts to parse a string as either a Cashu MintUrl or a Fedimint InviteCode.
/// Precedence: MintUrl is tried first, then InviteCode.
/// If a string can be parsed as both, an error is returned to avoid ambiguity.
impl FromStr for MintConnectionInfo {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mint_url_result = MintUrl::from_str(s);
let invite_code_result = InviteCode::from_str(s);
match (mint_url_result, invite_code_result) {
(Ok(_), Ok(_)) => Err(anyhow::anyhow!(
"Ambiguous input: can be parsed as both MintUrl and InviteCode: {}",
s
)),
(Ok(url), Err(_)) => Ok(Self::Cashu(url)),
(Err(_), Ok(invite_code)) => Ok(Self::Fedimint(invite_code)),
(Err(_), Err(_)) => Err(anyhow::anyhow!("Invalid mint connection info: {}", s)),

Copilot uses AI. Check for mistakes.
}
}
}

#[derive(Debug, Clone)]
pub struct UICoreMsgPacket {
pub id: Uuid,
Expand Down
100 changes: 42 additions & 58 deletions harbor-ui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,14 @@ use components::{MUTINY_GREEN, MUTINY_RED};
use harbor_client::Bolt11Invoice;
use harbor_client::bip39::Mnemonic;
use harbor_client::bitcoin::{Address, Network};
use harbor_client::cdk::mint_url::MintUrl;
use harbor_client::db_models::MintItem;
use harbor_client::db_models::transaction_item::TransactionItem;
use harbor_client::fedimint_core::Amount;
use harbor_client::fedimint_core::core::ModuleKind;
use harbor_client::fedimint_core::invite_code::InviteCode;
use harbor_client::lightning_address::parse_lnurl;
use harbor_client::{
CoreUIMsg, CoreUIMsgPacket, MintIdentifier, ReceiveSuccessMsg, SendSuccessMsg, UICoreMsg,
data_dir,
CoreUIMsg, CoreUIMsgPacket, MintConnectionInfo, MintIdentifier, ReceiveSuccessMsg,
SendSuccessMsg, UICoreMsg, data_dir,
};
use iced::Font;
use iced::Subscription;
Expand Down Expand Up @@ -212,9 +210,9 @@ pub enum Message {
password: String,
seed: Option<String>,
},
AddMint(String),
AddMint(MintConnectionInfo),
RejoinMint(MintIdentifier),
PeekMint(String),
PeekMint(MintConnectionInfo),
RemoveMint(MintIdentifier),
ChangeMint(MintIdentifier),
Donate,
Expand Down Expand Up @@ -876,72 +874,58 @@ impl HarborWallet {
}
}
},
Message::AddMint(string) => match InviteCode::from_str(&string) {
Ok(invite) => {
self.add_federation_status = AddFederationStatus::Adding;
let (id, task) = self.send_from_ui(UICoreMsg::AddFederation(invite));
self.current_add_id = Some(id);
task
}
Err(_) => match MintUrl::from_str(&string) {
Ok(mint_url) => {
self.add_federation_status = AddFederationStatus::Adding;
let (id, task) = self.send_from_ui(UICoreMsg::AddCashuMint(mint_url));
self.current_add_id = Some(id);
task
Message::AddMint(connection_info) => {
self.add_federation_status = AddFederationStatus::Adding;

let (id, task) = match connection_info {
MintConnectionInfo::Fedimint(invite_code) => {
self.send_from_ui(UICoreMsg::AddFederation(invite_code))
}
Err(_) => Task::done(Message::AddToast(Toast {
title: "Can't add mint".to_string(),
body: Some("Invalid invite code".to_string()),
status: ToastStatus::Bad,
})),
},
},
Message::PeekMint(string) => match InviteCode::from_str(&string) {
Ok(invite) => {
if self.mint_list.iter().any(|m| {
m.active
&& m.id
.federation_id()
.is_some_and(|f| f == invite.federation_id())
}) {
return Task::done(Message::AddToast(Toast {
title: "Mint already added".to_string(),
body: None,
status: ToastStatus::Bad,
}));
MintConnectionInfo::Cashu(mint_url) => {
self.send_from_ui(UICoreMsg::AddCashuMint(mint_url))
}
};

self.peek_status = PeekStatus::Peeking;
let (id, task) = self.send_from_ui(UICoreMsg::GetFederationInfo(invite));
self.current_peek_id = Some(id);
task
}
Err(_) => match MintUrl::from_str(&string) {
Ok(mint) => {
self.current_add_id = Some(id);
task
}
Message::PeekMint(connection_info) => {
let (id, task) = match connection_info {
MintConnectionInfo::Fedimint(invite_code) => {
if self.mint_list.iter().any(|m| {
m.active
&& m.id
.federation_id()
.is_some_and(|f| f == invite_code.federation_id())
}) {
return Task::done(Message::AddToast(Toast {
title: "Mint already added".to_string(),
body: None,
status: ToastStatus::Bad,
}));
}
self.send_from_ui(UICoreMsg::GetFederationInfo(invite_code))
}
MintConnectionInfo::Cashu(mint_url) => {
if self
.mint_list
.iter()
.any(|m| m.active && m.id.mint_url().is_some_and(|u| u == mint))
.any(|m| m.active && m.id.mint_url().is_some_and(|u| u == mint_url))
{
return Task::done(Message::AddToast(Toast {
title: "Mint already added".to_string(),
body: None,
status: ToastStatus::Bad,
}));
}
self.peek_status = PeekStatus::Peeking;
let (id, task) = self.send_from_ui(UICoreMsg::GetCashuMintInfo(mint));
self.current_peek_id = Some(id);
task
self.send_from_ui(UICoreMsg::GetCashuMintInfo(mint_url))
}
Err(_) => Task::done(Message::AddToast(Toast {
title: "Can't preview mint".to_string(),
body: Some("Invalid invite code".to_string()),
status: ToastStatus::Bad,
})),
},
},
};

self.peek_status = PeekStatus::Peeking;
self.current_peek_id = Some(id);
task
}
Message::RemoveMint(mint) => {
// Check if the federation still exists before trying to remove it
if !self.mint_list.iter().any(|f| f.id == mint) {
Expand Down
9 changes: 7 additions & 2 deletions harbor-ui/src/routes/mints.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::str::FromStr;

use harbor_client::MintConnectionInfo;
use iced::Element;
use iced::widget::{column, row};

Expand Down Expand Up @@ -56,6 +59,8 @@ fn mints_list(harbor: &HarborWallet) -> Element<Message> {
fn mints_add(harbor: &HarborWallet) -> Element<Message> {
let header = h_header("Add Mint", "Add a new mint to your wallet.");

let mint_connection_info = MintConnectionInfo::from_str(&harbor.mint_invite_code_str).ok();

let column = match &harbor.peek_federation_item {
None => {
let mint_input = h_input(InputArgs {
Expand All @@ -71,7 +76,7 @@ fn mints_add(harbor: &HarborWallet) -> Element<Message> {
SvgIcon::Eye,
harbor.peek_status == PeekStatus::Peeking,
)
.on_press(Message::PeekMint(harbor.mint_invite_code_str.clone()));
.on_press_maybe(mint_connection_info.map(Message::PeekMint));

let mut peek_column = column![mint_input, peek_mint_button].spacing(16);

Expand All @@ -91,7 +96,7 @@ fn mints_add(harbor: &HarborWallet) -> Element<Message> {
let is_joining = harbor.add_federation_status == AddFederationStatus::Adding;

let add_mint_button = h_button("Join Mint", SvgIcon::Plus, is_joining)
.on_press(Message::AddMint(harbor.mint_invite_code_str.clone()));
.on_press_maybe(mint_connection_info.map(Message::AddMint));

let start_over_button = h_button("Start Over", SvgIcon::Restart, false)
.on_press(Message::CancelAddFederation);
Expand Down
Loading