Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wire up transfer #133

Merged
merged 1 commit into from
Jan 29, 2025
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
27 changes: 23 additions & 4 deletions harbor-client/src/fedimint_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ pub(crate) async fn spawn_invoice_receive_subscription(
storage: Arc<dyn DBConnection + Send + Sync>,
operation_id: OperationId,
msg_id: Uuid,
is_transfer: bool,
subscription: UpdateStreamOrOutcome<LnReceiveState>,
) {
spawn(async move {
Expand All @@ -283,10 +284,15 @@ pub(crate) async fn spawn_invoice_receive_subscription(
}
LnReceiveState::Claimed => {
info!("Payment claimed");
let params = if is_transfer {
ReceiveSuccessMsg::Transfer
} else {
ReceiveSuccessMsg::Lightning
};
sender
.send(CoreUIMsgPacket {
id: Some(msg_id),
msg: CoreUIMsg::ReceiveSuccess(ReceiveSuccessMsg::Lightning),
msg: CoreUIMsg::ReceiveSuccess(params),
})
.await
.unwrap();
Expand Down Expand Up @@ -323,6 +329,7 @@ pub(crate) async fn spawn_invoice_payment_subscription(
storage: Arc<dyn DBConnection + Send + Sync>,
operation_id: OperationId,
msg_id: Uuid,
is_transfer: bool,
subscription: UpdateStreamOrOutcome<LnPayState>,
) {
spawn(async move {
Expand All @@ -334,7 +341,11 @@ pub(crate) async fn spawn_invoice_payment_subscription(
sender
.send(CoreUIMsgPacket {
id: Some(msg_id),
msg: CoreUIMsg::SendFailure("Canceled".to_string()),
msg: if is_transfer {
CoreUIMsg::TransferFailure("Canceled".to_string())
} else {
CoreUIMsg::SendFailure("Canceled".to_string())
},
})
.await
.unwrap();
Expand All @@ -349,7 +360,11 @@ pub(crate) async fn spawn_invoice_payment_subscription(
sender
.send(CoreUIMsgPacket {
id: Some(msg_id),
msg: CoreUIMsg::SendFailure(error_message),
msg: if is_transfer {
CoreUIMsg::TransferFailure(error_message)
} else {
CoreUIMsg::SendFailure(error_message)
},
})
.await
.unwrap();
Expand All @@ -363,7 +378,11 @@ pub(crate) async fn spawn_invoice_payment_subscription(
info!("Payment success");
let preimage: [u8; 32] =
FromHex::from_hex(&preimage).expect("Invalid preimage");
let params = SendSuccessMsg::Lightning { preimage };
let params = if is_transfer {
SendSuccessMsg::Transfer
} else {
SendSuccessMsg::Lightning { preimage }
};
sender
.send(CoreUIMsgPacket {
id: Some(msg_id),
Expand Down
29 changes: 27 additions & 2 deletions harbor-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ pub enum UICoreMsg {
ReceiveOnChain {
federation_id: FederationId,
},
Transfer {
to: FederationId,
from: FederationId,
amount: Amount,
},
GetFederationInfo(InviteCode),
AddFederation(InviteCode),
RemoveFederation(FederationId),
Expand All @@ -84,16 +89,18 @@ pub enum UICoreMsg {
SetOnchainReceiveEnabled(bool),
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum SendSuccessMsg {
Lightning { preimage: [u8; 32] },
Onchain { txid: Txid },
Transfer,
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ReceiveSuccessMsg {
Lightning,
Onchain { txid: Txid },
Transfer,
}

#[derive(Debug, Clone)]
Expand All @@ -112,6 +119,7 @@ pub enum CoreUIMsg {
ReceiveAddressGenerated(Address),
ReceiveSuccess(ReceiveSuccessMsg),
ReceiveFailed(String),
TransferFailure(String),
// todo probably want a way to incrementally add items to the history
TransactionHistoryUpdated(Vec<TransactionItem>),
FederationBalanceUpdated { id: FederationId, balance: Amount },
Expand Down Expand Up @@ -207,6 +215,7 @@ impl HarborCore {
msg_id: Uuid,
federation_id: FederationId,
invoice: Bolt11Invoice,
is_transfer: bool,
) -> anyhow::Result<()> {
if invoice.amount_milli_satoshis().is_none() {
return Err(anyhow!("Invoice must have an amount"));
Expand Down Expand Up @@ -259,6 +268,7 @@ impl HarborCore {
self.storage.clone(),
op_id,
msg_id,
is_transfer,
sub,
)
.await;
Expand All @@ -275,6 +285,7 @@ impl HarborCore {
msg_id: Uuid,
federation_id: FederationId,
amount: Amount,
is_transfer: bool,
) -> anyhow::Result<Bolt11Invoice> {
let client = self.get_client(federation_id).await.fedimint_client;
let lightning_module = client
Expand Down Expand Up @@ -315,6 +326,7 @@ impl HarborCore {
self.storage.clone(),
op_id,
msg_id,
is_transfer,
subscription,
)
.await;
Expand All @@ -325,6 +337,19 @@ impl HarborCore {
Ok(invoice)
}

pub async fn transfer(
&self,
msg_id: Uuid,
to: FederationId,
from: FederationId,
amount: Amount,
) -> anyhow::Result<()> {
log::info!("Transferring {amount} from {from} to {to}");
let invoice = self.receive_lightning(msg_id, to, amount, true).await?;
self.send_lightning(msg_id, from, invoice, true).await?;
Ok(())
}

/// Sends a given amount of sats to a given address, if the amount is None, send all funds
pub async fn send_onchain(
&self,
Expand Down
17 changes: 15 additions & 2 deletions harbor-ui/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,10 @@ async fn process_core(core_handle: &mut CoreHandle, core: &HarborCore) {
} => {
log::info!("Got UICoreMsg::Send");
core.msg(msg.id, CoreUIMsg::Sending).await;
if let Err(e) = core.send_lightning(msg.id, federation_id, invoice).await {
if let Err(e) = core
.send_lightning(msg.id, federation_id, invoice, false)
.await
{
error!("Error sending: {e}");
core.msg(msg.id, CoreUIMsg::SendFailure(e.to_string()))
.await;
Expand All @@ -298,7 +301,10 @@ async fn process_core(core_handle: &mut CoreHandle, core: &HarborCore) {
amount,
} => {
core.msg(msg.id, CoreUIMsg::ReceiveGenerating).await;
match core.receive_lightning(msg.id, federation_id, amount).await {
match core
.receive_lightning(msg.id, federation_id, amount, false)
.await
{
Err(e) => {
core.msg(msg.id, CoreUIMsg::ReceiveFailed(e.to_string()))
.await;
Expand Down Expand Up @@ -338,6 +344,13 @@ async fn process_core(core_handle: &mut CoreHandle, core: &HarborCore) {
}
}
}
UICoreMsg::Transfer { to, from, amount } => {
if let Err(e) = core.transfer(msg.id, to, from, amount).await {
error!("Error transferring: {e}");
core.msg(msg.id, CoreUIMsg::TransferFailure(e.to_string()))
.await;
}
}
UICoreMsg::GetFederationInfo(invite_code) => {
match core.get_federation_info(invite_code).await {
Err(e) => {
Expand Down
2 changes: 1 addition & 1 deletion harbor-ui/src/components/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{HarborWallet, Message, Route};
use super::{harbor_logo, indicator, lighten, map_icon, sidebar_button};

pub fn sidebar(harbor: &HarborWallet) -> Element<Message> {
let transfer_disabled = harbor.total_balance_sats() == 0 || harbor.federation_list.is_empty();
let transfer_disabled = harbor.federation_list.is_empty();
let transfer_button = sidebar_button(
"Transfer",
SvgIcon::LeftRight,
Expand Down
111 changes: 94 additions & 17 deletions harbor-ui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub enum Message {
// Async commands we fire from the UI to core
Noop,
Send(String),
Transfer,
GenerateInvoice,
GenerateAddress,
Unlock(String),
Expand Down Expand Up @@ -189,13 +190,15 @@ pub struct HarborWallet {
transfer_from_federation_selection: Option<String>,
transfer_to_federation_selection: Option<String>,
transfer_amount_input_str: String,
transfer_status: SendStatus,
// Donate
donate_amount_str: String,
// Settings
settings_show_seed_words: bool,
seed_words: Option<String>,
current_send_id: Option<Uuid>,
current_receive_id: Option<Uuid>,
current_transfer_id: Option<Uuid>,
peek_status: PeekStatus,
add_federation_status: AddFederationStatus,
// Onboarding
Expand All @@ -211,10 +214,6 @@ impl HarborWallet {
.and_then(|id| self.federation_list.iter().find(|f| f.id == *id))
}

fn total_balance_sats(&self) -> u64 {
self.federation_list.iter().map(|f| f.balance).sum()
}

fn subscription(&self) -> Subscription<Message> {
Subscription::run(run_core)
}
Expand Down Expand Up @@ -261,6 +260,13 @@ impl HarborWallet {
// transaction
}

fn clear_transfer_state(&mut self) {
self.transfer_amount_input_str = String::new();
self.transfer_to_federation_selection = None;
self.transfer_from_federation_selection = None;
self.transfer_status = SendStatus::Idle;
}

fn send_from_ui(&self, msg: UICoreMsg) -> (Uuid, Task<Message>) {
let id = Uuid::new_v4();
let task = Task::perform(
Expand Down Expand Up @@ -436,6 +442,50 @@ impl HarborWallet {
}
}
},
Message::Transfer => {
let from = if let Some(name) = &self.transfer_from_federation_selection {
self.federation_list
.iter()
.find(|f| &f.name == name)
.unwrap()
.id
} else {
error!("No source federation selected");
return Task::none();
};
let to = if let Some(name) = &self.transfer_to_federation_selection {
self.federation_list
.iter()
.find(|f| &f.name == name)
.unwrap()
.id
} else {
error!("No destination federation selected");
return Task::none();
};

if from == to {
error!("Cannot transfer to same federation");
return Task::none();
}

let amount = match self.transfer_amount_input_str.parse::<u64>() {
Ok(a) => a,
Err(_) => {
error!("Invalid amount");
return Task::none();
}
};

let (id, task) = self.send_from_ui(UICoreMsg::Transfer {
from,
to,
amount: Amount::from_sats(amount),
});
self.current_transfer_id = Some(id);
self.transfer_status = SendStatus::Sending;
task
}
Message::GenerateInvoice => match self.receive_status {
ReceiveStatus::Generating => Task::none(),
_ => {
Expand Down Expand Up @@ -625,13 +675,17 @@ impl HarborWallet {
self.clear_send_state();
}
// Toast success
Task::perform(async {}, move |_| {
Message::AddToast(Toast {
title: "Payment sent".to_string(),
body: None,
status: ToastStatus::Good,
if params != SendSuccessMsg::Transfer {
Task::perform(async {}, move |_| {
Message::AddToast(Toast {
title: "Payment sent".to_string(),
body: None,
status: ToastStatus::Good,
})
})
})
} else {
Task::none()
}
}
CoreUIMsg::SendFailure(reason) => {
if self.current_send_id == msg.id {
Expand All @@ -656,15 +710,31 @@ impl HarborWallet {
// Navigate to the history screen
self.active_route = Route::History;
self.clear_receive_state();
} else if self.current_transfer_id == msg.id && msg.id.is_some() {
self.current_transfer_id = None;

// Navigate to the history screen
self.active_route = Route::History;
self.clear_transfer_state();
}
// Toast success
Task::perform(async {}, move |_| {
Message::AddToast(Toast {
title: "Payment received".to_string(),
body: None,
status: ToastStatus::Good,
if params != ReceiveSuccessMsg::Transfer {
// Toast success
Task::perform(async {}, move |_| {
Message::AddToast(Toast {
title: "Payment received".to_string(),
body: None,
status: ToastStatus::Good,
})
})
})
} else {
Task::perform(async {}, move |_| {
Message::AddToast(Toast {
title: "Transfer complete".to_string(),
body: None,
status: ToastStatus::Good,
})
})
}
}
CoreUIMsg::ReceiveFailed(reason) => {
if self.current_receive_id == msg.id {
Expand All @@ -681,6 +751,13 @@ impl HarborWallet {
})
})
}
CoreUIMsg::TransferFailure(reason) => {
if self.current_transfer_id == msg.id {
self.transfer_status = SendStatus::Idle;
}
error!("Transfer failed: {reason}");
Task::none()
}
CoreUIMsg::TransactionHistoryUpdated(history) => {
self.transaction_history = history;
Task::none()
Expand Down
Loading