feat: add support for cashu wallet (nip60 & nip61) (#71)

Reviewed-on: #71
This commit was merged in pull request #71.
This commit is contained in:
2026-11-09 01:12:27 +00:00
parent c1757bbcae
commit 98a12e438a
20 changed files with 4732 additions and 15 deletions
+6 -2
View File
@@ -9,11 +9,11 @@ ui = { path = "../ui" }
gpui-kit.workspace = true
common = { path = "../common" }
state = { path = "../state" }
nip29 = { path = "../nip29" }
nip29_ui = { path = "../nip29_ui" }
nip4e = { path = "../nip4e" }
chat = { path = "../chat" }
chat_ui = { path = "../chat_ui" }
nip29 = { path = "../nip29" }
nip29_ui = { path = "../nip29_ui" }
settings = { path = "../settings" }
speech = { path = "../speech" }
person = { path = "../person" }
@@ -30,3 +30,7 @@ serde.workspace = true
log.workspace = true
rand.workspace = true
smallvec.workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
nip60 = { path = "../nip60" }
nip60_ui = { path = "../nip60_ui" }
+3 -3
View File
@@ -1,11 +1,11 @@
use std::rc::Rc;
use gpui::{App, AppContext, ClickEvent, ParentElement, Styled, Window, px};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::{Input, InputState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::{WindowExt, h_flex, v_flex};
use std::rc::Rc;
use gpui::{App, AppContext, ClickEvent, ParentElement, Styled, Window, px};
use nip29::{GroupReference, GroupsRegistry};
type SubmitFn = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool>;
+59
View File
@@ -41,6 +41,8 @@ pub struct Preferences {
radius_input: Entity<InputState>,
radius_lg_input: Entity<InputState>,
#[cfg(not(target_arch = "wasm32"))]
mint_input: Entity<InputState>,
#[cfg(not(target_arch = "wasm32"))]
_speech_poll: Task<()>,
/// Keeps the control subscriptions alive while the dialog is open
_subscriptions: Vec<Subscription>,
@@ -90,6 +92,9 @@ impl Preferences {
.max(RADIUS_RANGE.1 as f64)
});
#[cfg(not(target_arch = "wasm32"))]
let mint_input = cx.new(|cx| InputState::new(window, cx).placeholder("https://mint…"));
let subscriptions = vec![
Self::wire_number_input(&font_size_input, FONT_SIZE_RANGE, cx, |value, cx| {
if let Some(font_size) = value {
@@ -140,6 +145,8 @@ impl Preferences {
radius_input,
radius_lg_input,
#[cfg(not(target_arch = "wasm32"))]
mint_input,
#[cfg(not(target_arch = "wasm32"))]
_speech_poll,
_subscriptions: subscriptions,
}
@@ -185,6 +192,56 @@ impl Preferences {
}
}
/// Create the NIP-60 wallet or add a mint to it
#[cfg(not(target_arch = "wasm32"))]
fn setup_mint(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let mint = self.mint_input.read(cx).value().trim().to_string();
if mint.is_empty() {
window.push_notification(Notification::error("Enter a mint URL"), cx);
return;
}
nip60::WalletRegistry::global(cx).update(cx, |registry, cx| {
registry.setup(mint, cx);
});
}
/// The NIP-60 wallet block; absent from the web build
fn render_wallet(
&mut self,
#[cfg_attr(target_arch = "wasm32", allow(unused_variables))] cx: &mut Context<Self>,
) -> AnyElement {
#[cfg(target_arch = "wasm32")]
{
return div().into_any_element();
}
#[cfg(not(target_arch = "wasm32"))]
{
const WALLET: &str = "Cashu mint for your NIP-60 wallet. Set up creates the wallet, Add extends it with another mint.";
let has_wallet = nip60::WalletRegistry::global(cx).read(cx).has_wallet();
settings_block(
"Wallet",
WALLET,
h_flex()
.gap_1()
.child(div().flex_1().child(Input::new(&self.mint_input)))
.child(
Button::new("setup-mint")
.label(if has_wallet { "Add" } else { "Set up" })
.ghost()
.size_8()
.on_click(cx.listener(move |this, _ev, window, cx| {
this.setup_mint(window, cx);
})),
),
cx,
)
.into_any_element()
}
}
/// Set the appearance mode (system, light or dark)
fn set_appearance_mode(mode: AppearanceMode, cx: &mut App) {
AppSettings::update_appearance_mode(mode, cx);
@@ -546,5 +603,7 @@ impl Render for Preferences {
),
cx,
))
.child(Separator::horizontal())
.child(self.render_wallet(cx))
}
}
+32
View File
@@ -61,6 +61,7 @@ enum Command {
ShowContactList,
ShowInbox,
ShowBrowse,
ShowWallet,
NewChat,
NewGroup,
JoinGroup,
@@ -196,6 +197,30 @@ impl Workspace {
}),
);
#[cfg(not(target_arch = "wasm32"))]
{
let wallet = ::nip60::WalletRegistry::global(cx);
subscriptions.push(cx.subscribe_in(
&wallet,
window,
|_this, _wallet, event, window, cx| match event {
::nip60::WalletEvent::Error(error) => {
window.push_notification(Notification::error(error.clone()), cx);
}
::nip60::WalletEvent::TokenCreated(_) => {
window.push_notification(
Notification::new()
.message("Cashu token created, copy it from the wallet panel")
.with_type(NotificationType::Success),
cx,
);
}
::nip60::WalletEvent::Updated => {}
},
));
}
App::on_action(cx, {
let workspace = workspace.clone();
move |action, cx| {
@@ -307,6 +332,13 @@ impl Workspace {
let panel = browse::init(self.dock.downgrade(), window, cx);
self.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
}
Command::ShowWallet => {
#[cfg(not(target_arch = "wasm32"))]
{
let panel = nip60_ui::wallet::init(window, cx);
self.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
}
}
Command::NewChat => {
new_chat::open(self.dock.downgrade(), window, cx);
}
+13 -1
View File
@@ -39,7 +39,6 @@ impl Sidebar {
let requests_open = self.requests_open;
let contacts_open = self.contacts_open && !requests_open;
let rows = Rc::new(self.rows_for(cx));
// Whether to show the create button for the contact picker
let create_button = contacts_open && !self.selected_contacts.is_empty();
@@ -60,12 +59,25 @@ impl Sidebar {
.flex_1()
.min_h_0()
.relative()
.border_l_1()
.border_color(cx.theme().sidebar_border)
.child(
v_flex()
.px_2()
.gap_1()
.items_start()
.justify_start()
.when(cfg!(not(target_arch = "wasm32")), |this| {
this.child(
Nav::new("wallet")
.prefix(Icon::new(gpui_kit::assets::IconName::Wallet).small())
.label("Wallet")
.text_sm()
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(crate::Command::ShowWallet), cx)
}),
)
})
.child(
Nav::new("browse")
.prefix(Icon::new(IconName::Compass).small())