diff --git a/src/client.rs b/src/client.rs index 5c53975..584cb9c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,6 @@ use std::fmt::{Debug, Formatter}; use std::str::FromStr; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy}; use fedimint_client::{Client, FederationInfo}; @@ -36,7 +36,8 @@ enum RpcRequest { Join(String), GetName, SubscribeBalance, - Receive(String), + EcashSend(Amount), + EcashReceive(String), LnSend(String), LnReceive { amount: Amount, description: String }, // TODO: pagination @@ -51,7 +52,8 @@ enum RpcResponse { Join, GetName(String), SubscribeBalance(BoxStream<'static, Amount>), - Receive(Amount), + EcashSend(OOBNotes), + EcashReceive(Amount), LnSend, LnReceive { invoice: String, @@ -256,7 +258,20 @@ async fn run_client(mut rpc: mpsc::Receiver) { .send(Ok(RpcResponse::SubscribeBalance(stream))) .map_err(|_| warn!("RPC receiver dropped before response was sent")); } - RpcRequest::Receive(notes) => { + RpcRequest::EcashSend(amount) => { + const TRY_CANCEL_AFTER: Duration = Duration::from_secs(60 * 60 * 24 * 3); // 3 days + + let response = client + .get_first_module::() + .spend_notes(amount, TRY_CANCEL_AFTER, ()) + .await + .map(|(_, notes)| RpcResponse::EcashSend(notes)); + + let _ = response_sender + .send(response) + .map_err(|_| warn!("RPC receiver dropped before response was sent")); + } + RpcRequest::EcashReceive(notes) => { async fn receive_inner( client: &Client, notes: &str, @@ -269,7 +284,7 @@ async fn run_client(mut rpc: mpsc::Receiver) { .get_first_module::() .reissue_external_notes(notes, ()) .await?; - Ok(RpcResponse::Receive(amount)) + Ok(RpcResponse::EcashReceive(amount)) } let _ = response_sender .send(receive_inner(client, ¬es).await) @@ -467,15 +482,28 @@ impl ClientRpc { } } - pub async fn receive(&self, invoice: String) -> anyhow::Result { + pub async fn ecash_send(&self, amount: Amount) -> anyhow::Result { + let (response_sender, response_receiver) = oneshot::channel(); + self.sender + .send((RpcRequest::EcashSend(amount), response_sender)) + .await + .expect("Client has stopped"); + let response = response_receiver.await.expect("Client has stopped")?; + match response { + RpcResponse::EcashSend(notes) => Ok(notes), + _ => Err(RpcError::InvalidResponse), + } + } + + pub async fn ecash_receive(&self, invoice: String) -> anyhow::Result { let (response_sender, response_receiver) = oneshot::channel(); self.sender - .send((RpcRequest::Receive(invoice), response_sender)) + .send((RpcRequest::EcashReceive(invoice), response_sender)) .await .expect("Client has stopped"); let response = response_receiver.await.expect("Client has stopped")?; match response { - RpcResponse::Receive(amount) => Ok(amount), + RpcResponse::EcashReceive(amount) => Ok(amount), _ => Err(RpcError::InvalidResponse), } } diff --git a/src/components/joined.rs b/src/components/joined.rs index aead27e..44e7a97 100644 --- a/src/components/joined.rs +++ b/src/components/joined.rs @@ -1,6 +1,6 @@ use leptos::*; -use crate::components::{Balance, Receive, ReceiveLn, Send, TxList}; +use crate::components::{Balance, ReceiveEcash, ReceiveLn, SendEcash, SendLn, TxList}; use crate::context::ClientContext; // @@ -35,13 +35,17 @@ pub fn Joined() -> impl IntoView { title: "Transactions".into(), view: view! { }, }, + MenuItem { + title: "Spend".into(), + view: view! { }, + }, MenuItem { title: "Redeem".into(), - view: view! { }, + view: view! { }, }, MenuItem { title: "LN Send".into(), - view: view! { }, + view: view! { }, }, MenuItem { title: "LN Receive".into(), diff --git a/src/components/mod.rs b/src/components/mod.rs index 383e938..5ddf786 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -9,9 +9,10 @@ pub mod loader_icon; pub mod logo; pub mod logo_fedimint; pub mod qrcode; -pub mod receive; +pub mod receive_ecash; pub mod receive_ln; -pub mod send; +pub mod send_ecash; +pub mod send_ln; pub mod service_worker; pub mod submit_button; pub mod submit_form; @@ -27,9 +28,11 @@ pub use joined::*; pub use loader_icon::*; pub use logo::*; pub use logo_fedimint::*; -pub use receive::*; +pub use qrcode::*; +pub use receive_ecash::*; pub use receive_ln::*; -pub use send::*; +pub use send_ecash::*; +pub use send_ln::*; pub use submit_button::*; pub use submit_form::*; pub use tx_list::*; diff --git a/src/components/receive.rs b/src/components/receive_ecash.rs similarity index 91% rename from src/components/receive.rs rename to src/components/receive_ecash.rs index e30471f..33e7cb4 100644 --- a/src/components/receive.rs +++ b/src/components/receive_ecash.rs @@ -7,13 +7,13 @@ use crate::context::ClientContext; // Receive e-cash component // #[component] -pub fn Receive() -> impl IntoView { +pub fn ReceiveEcash() -> impl IntoView { let ClientContext { client, .. } = expect_context::(); let client = client.clone(); let submit_action = create_action(move |invoice: &String| { let invoice = invoice.clone(); - async move { client.get_value().receive(invoice).await } + async move { client.get_value().ecash_receive(invoice).await } }); view! { diff --git a/src/components/send_ecash.rs b/src/components/send_ecash.rs new file mode 100644 index 0000000..7c4133f --- /dev/null +++ b/src/components/send_ecash.rs @@ -0,0 +1,102 @@ +use fedimint_core::Amount; +use leptos::*; + +use super::{CopyableText, ErrorBlock, QrCode, SubmitButton, SuccessBlock}; +use crate::context::ClientContext; + +// +// Send Ecash component +// +#[component] +pub fn SendEcash() -> impl IntoView { + let ClientContext { client, .. } = expect_context::(); + + let (amount, set_amount) = create_signal("".to_owned()); + let (error, set_error) = create_signal(None); + + let client = client.clone(); + let submit_action = create_action(move |amount: &Amount| { + let amount = amount.clone(); + async move { client.get_value().ecash_send(amount).await } + }); + + let parse_and_submit = move || { + let amount = match amount.get().parse::() { + Ok(a) => a, + Err(e) => { + set_error.set(Some(format!("Invalid amount: {e}"))); + return; + } + }; + + set_error.set(None); + + submit_action.dispatch(amount); + }; + + view! { +
+ + + + Spend + + + {move || { + error.get().map(|e| { + view! { + + {e} + + } + }) + }} + + {move || { + submit_action.value().get().map(|r| { + r.err().map(|err| { + view! { + + {format!("{:?}", err)} + + } + }) + }) + }} + + {move || { + submit_action.value().get().map(|r| { + r.ok().map(|notes| { + let total = notes.total_amount(); + let notes_string_signal = Signal::derive(move || notes.to_string()); + view! { + + {format!("Notes representing {} shown below.", total)} + + + + } + }) + }) + }} +
+ } +} diff --git a/src/components/send.rs b/src/components/send_ln.rs similarity index 97% rename from src/components/send.rs rename to src/components/send_ln.rs index a909711..b3df59d 100644 --- a/src/components/send.rs +++ b/src/components/send_ln.rs @@ -7,7 +7,7 @@ use crate::context::ClientContext; // Send LN component // #[component] -pub fn Send() -> impl IntoView { +pub fn SendLn() -> impl IntoView { let ClientContext { client, .. } = expect_context::(); let client = client.clone();