Skip to content

Commit

Permalink
Merge pull request #68 from toyota-corolla0/2024-02-ecash-send
Browse files Browse the repository at this point in the history
feat: send ecash component
  • Loading branch information
elsirion authored Feb 26, 2024
2 parents d5625dc + 08e8651 commit 39791f2
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 18 deletions.
44 changes: 36 additions & 8 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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
Expand All @@ -51,7 +52,8 @@ enum RpcResponse {
Join,
GetName(String),
SubscribeBalance(BoxStream<'static, Amount>),
Receive(Amount),
EcashSend(OOBNotes),
EcashReceive(Amount),
LnSend,
LnReceive {
invoice: String,
Expand Down Expand Up @@ -256,7 +258,20 @@ async fn run_client(mut rpc: mpsc::Receiver<RpcCall>) {
.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::<MintClientModule>()
.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,
Expand All @@ -269,7 +284,7 @@ async fn run_client(mut rpc: mpsc::Receiver<RpcCall>) {
.get_first_module::<MintClientModule>()
.reissue_external_notes(notes, ())
.await?;
Ok(RpcResponse::Receive(amount))
Ok(RpcResponse::EcashReceive(amount))
}
let _ = response_sender
.send(receive_inner(client, &notes).await)
Expand Down Expand Up @@ -467,15 +482,28 @@ impl ClientRpc {
}
}

pub async fn receive(&self, invoice: String) -> anyhow::Result<Amount, RpcError> {
pub async fn ecash_send(&self, amount: Amount) -> anyhow::Result<OOBNotes, RpcError> {
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<Amount, RpcError> {
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),
}
}
Expand Down
10 changes: 7 additions & 3 deletions src/components/joined.rs
Original file line number Diff line number Diff line change
@@ -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;

//
Expand Down Expand Up @@ -35,13 +35,17 @@ pub fn Joined() -> impl IntoView {
title: "Transactions".into(),
view: view! { <TxList update_signal=move || tab_change_signal.get() /> },
},
MenuItem {
title: "Spend".into(),
view: view! { <SendEcash /> },
},
MenuItem {
title: "Redeem".into(),
view: view! { <Receive /> },
view: view! { <ReceiveEcash /> },
},
MenuItem {
title: "LN Send".into(),
view: view! { <Send /> },
view: view! { <SendLn /> },
},
MenuItem {
title: "LN Receive".into(),
Expand Down
11 changes: 7 additions & 4 deletions src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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::*;
Expand Down
4 changes: 2 additions & 2 deletions src/components/receive.rs → src/components/receive_ecash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ClientContext>();

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! {
Expand Down
102 changes: 102 additions & 0 deletions src/components/send_ecash.rs
Original file line number Diff line number Diff line change
@@ -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::<ClientContext>();

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::<Amount>() {
Ok(a) => a,
Err(e) => {
set_error.set(Some(format!("Invalid amount: {e}")));
return;
}
};

set_error.set(None);

submit_action.dispatch(amount);
};

view! {
<div class="flex flex-col gap-4">
<input
type="number"
placeholder="Amount msat"
class="w-full text-xl font-body text-gray-600 border-gray-400 placeholder:text-gray-400 ring-0 focus:border-blue-400 focus:ring-0"
on:input=move |ev| {
set_amount.set(event_target_value(&ev));
}
prop:value=move || amount.get()
/>

<SubmitButton
loading=submit_action.pending()
disabled=submit_action.pending().into()
on_click=move |_| parse_and_submit()
class="w-full"
>
Spend
</SubmitButton>

{move || {
error.get().map(|e| {
view! {
<ErrorBlock>
{e}
</ErrorBlock>
}
})
}}

{move || {
submit_action.value().get().map(|r| {
r.err().map(|err| {
view! {
<ErrorBlock>
{format!("{:?}", err)}
</ErrorBlock>
}
})
})
}}

{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! {
<SuccessBlock>
{format!("Notes representing {} shown below.", total)}
</SuccessBlock>
<CopyableText
text=notes_string_signal
rows=10
/>
<QrCode
data=notes_string_signal
/>
}
})
})
}}
</div>
}
}
2 changes: 1 addition & 1 deletion src/components/send.rs → src/components/send_ln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ClientContext>();

let client = client.clone();
Expand Down

0 comments on commit 39791f2

Please sign in to comment.