Skip to content

Commit

Permalink
request panel
Browse files Browse the repository at this point in the history
  • Loading branch information
aspect committed Dec 27, 2023
1 parent 5f20b38 commit 0c68871
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 36 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ toml = "0.8.8"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.64", features = ['Window'] }
xxhash-rust = { version = "0.8.7", features = ["xxh3"] }
zeroize = { version = "1", default-features = false, features = ["alloc"] }

[profile.release]
Expand Down
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ slug.workspace = true
thiserror.workspace = true
toml.workspace = true
wasm-bindgen.workspace = true
xxhash-rust.workspace = true
zeroize.workspace = true

egui.workspace = true
Expand Down
1 change: 0 additions & 1 deletion core/src/modules/account_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ impl ManagerContext {

impl Zeroize for ManagerContext {
fn zeroize(&mut self) {
println!("*** resetting send state...");

self.transfer_to_account = None;
self.destination_address_string = String::default();
Expand Down
7 changes: 6 additions & 1 deletion core/src/modules/account_manager/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ impl<'context> NetworkState<'context> {
Self { context }
}

pub fn render(&mut self, core: &mut Core, ui: &mut Ui, _rc: &RenderContext<'_>) {
pub fn render(&mut self, core: &mut Core, ui: &mut Ui, rc: &RenderContext<'_>) {

use egui_phosphor::light::{CLOUD_SLASH,CLOUD_ARROW_DOWN};

Expand Down Expand Up @@ -47,6 +47,11 @@ impl<'context> NetworkState<'context> {
if ui.large_button(i18n("Go to Settings")).clicked() {
core.select::<modules::Settings>();
}
ui.label("");
if ui.large_button(i18n("Payment Request")).clicked() {
core.get_mut::<modules::Request>().select(rc.account);
core.select::<modules::Request>();
}
});

}
Expand Down
3 changes: 2 additions & 1 deletion core/src/modules/account_manager/overview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ impl<'manager> Overview<'manager> {
});
}
layout = layout.add(Button::new(format!("{} Request", QR_CODE)).min_size(theme_style().medium_button_size()), |(_,core)| {
core.get_mut::<modules::Request>().select(rc.account);
core.select::<modules::Request>();

});

layout.build(ui,&mut (self,core));
Expand Down
216 changes: 199 additions & 17 deletions core/src/modules/request.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,146 @@
use crate::imports::*;
use egui_phosphor::light::CLIPBOARD_TEXT;
// use kaspa_rpc_core::hash;
use std::{borrow::Cow, collections::hash_map::Entry};
pub use xxhash_rust::xxh3::xxh3_64;

pub struct RequestUri {
pub address : String,
pub amount_sompi : Option<u64>,
pub label : Option<String>,
}

impl std::fmt::Display for RequestUri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut uri = self.address.clone();
if let Some(amount_sompi) = self.amount_sompi {
uri = format!("{}?amount={}", uri, sompi_to_kaspa(amount_sompi));
}
if let Some(label) = self.label.as_ref() {
uri = format!("{}?label={}", uri, label);
}
write!(f, "{}", uri)
}
}

pub struct Request {
#[allow(dead_code)]
runtime: Runtime,
account : Option<Account>,
qr : HashMap<String, (String,load::Bytes)>,
amount : String,
amount_sompi : Option<u64>,
label : String,
error : Option<String>,
}

impl Request {
pub fn new(runtime: Runtime) -> Self {
Self { runtime }
Self {
runtime,
account : None,
qr : Default::default(),
amount : String::default(),
amount_sompi : None,
label : String::default(),
error : None,
}
}

fn create_request_uri(&self, address : impl Into<String>, amount_sompi : Option<u64>, label : Option<impl Into<String>>) -> RequestUri {

RequestUri {
address : address.into(),
amount_sompi,
label : label.map(|l|l.into()),
}
}

fn qr(&mut self, request_uri : &str) -> (String,load::Bytes) {

let hash = format!("{:x}",xxh3_64(format!("{request_uri}{}", theme_color().name).as_bytes()));
let (qr_uri,qr_bytes) = match self.qr.entry(hash.clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let uri = format!("bytes://{hash}.svg");
// let bits = qrcode::types::Mode::Alphanumeric.data_bits_count(request_uri.len());
let qr = render_qrcode_with_version(request_uri.as_str(), 192, 192, qrcode::Version::Normal(10));
entry.insert((uri, qr.as_bytes().to_vec().into()))
},
};

(qr_uri.clone(),qr_bytes.clone())
}

fn render_destination(&mut self, ui: &mut Ui, address : &str, request_uri : &RequestUri ) {

let request_uri = request_uri.to_string();

let (qr_uri, qr_bytes) = self.qr(request_uri.as_str());


let response = ui.add(Label::new(format!("Address: {} {CLIPBOARD_TEXT}", format_address_string(address, Some(12)))).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui|{
ui.vertical(|ui|{
ui.label(i18n("Click to copy address to clipboard"));
});
});

if response.clicked() {
ui.output_mut(|o| o.copied_text = address.to_owned());
runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Address copied to clipboard"))).short());
}

ui.label(" ");

// --

let response = ui.add(Label::new(format!("URI: {} {CLIPBOARD_TEXT}", format_partial_string(request_uri.as_str(), Some(24)))).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui|{
ui.vertical(|ui|{
ui.label(i18n("Click to copy URI to clipboard"));
});
});

if response.clicked() {
ui.output_mut(|o| o.copied_text = request_uri.to_owned());
runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("URI copied to clipboard"))).short());
}

ui.label(" ");

// --

ui.add(
Image::new(ImageSource::Bytes { uri : Cow::Owned(qr_uri), bytes: qr_bytes })
.fit_to_original_size(1.0)
.texture_options(TextureOptions::NEAREST)
);

// ui.label(" ");

// --

}

pub fn select(&mut self, account : &Account) {
self.account = Some(account.clone());
}

}

impl ModuleT for Request {

fn style(&self) -> ModuleStyle {
ModuleStyle::Mobile
}

fn deactivate(&mut self, _core: &mut Core) {
self.account = None;
self.error = None;
self.qr.clear();
}

fn render(
&mut self,
core: &mut Core,
Expand All @@ -20,23 +149,76 @@ impl ModuleT for Request {
ui: &mut egui::Ui,
) {

let close = Rc::new(RefCell::new(false));

Panel::new(self)
.with_caption("Payment Request")
.with_back_enabled(core.has_stack(), |_|{
.with_caption("Payment Request")
.with_back_enabled(core.has_stack(), |_|{
*close.borrow_mut() = true;
})
.with_header(|this, ui| {
if let Some(account) = this.account.as_ref() {
ui.label("");
ui.label(format!("Payment request to account: {}",account.name_or_id()));
}
// ui.label(text);
})
.with_body(|this, ui| {


if let Some(account) = this.account.as_ref() {
let address = account.receive_address().to_string();
let label = this.label.is_not_empty().then_some(this.label.clone());
let request_uri = this.create_request_uri(address.clone(), this.amount_sompi, label);

this.render_destination(ui, address.as_str(), &request_uri);
}

ui.label("");

ui.label(i18n("Enter the amount"));

let amount = this.amount.clone();
ui.add_sized(
theme_style().panel_editor_size,
TextEdit::singleline(&mut this.amount)
.vertical_align(Align::Center),
);

if amount != this.amount {
match try_kaspa_str_to_sompi(this.amount.as_str()) {
Ok(Some(amount_sompi)) => {
this.amount_sompi = Some(amount_sompi);
this.error = None;
},
Ok(None) => {
this.amount_sompi = None;
this.error = None;
},
Err(_err) => {
this.amount_sompi = None;
this.error = Some(i18n("Please enter a valid about of KAS").to_string());
},
}
}

if let Some(error) = this.error.as_ref() {
ui.label("");
ui.colored_label(error_color(), error);
}

ui.label(" ");

})
.with_footer(|_ctx, ui| {
if ui.large_button("Close").clicked() {
*close.borrow_mut() = true;
}
})
.render(ui);

if *close.borrow() {
core.back();
})
// .with_close_enabled(core.has_stack(), |_| {
// core.back();
// })
.with_header(|_ctx, _ui| {
// ui.label(text);
})
.with_body(|_this, ui| {

ui.label("");
ui.label(i18n("Payment request panel"));
ui.label("");
})
.render(ui);
}
}
}
16 changes: 16 additions & 0 deletions core/src/utils/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ pub fn format_address(address: &Address, range: Option<usize>) -> String {
format_address_string(address, range)
}


pub fn format_partial_string(text: impl Into<String>, range: Option<usize>) -> String {
let text: String = text.into();
let range = range.unwrap_or(6);
if text.len() <= range * 2 {
text
} else {
let start = range;
let finish = text.len() - range;
let left = &text[0..start];
let right = &text[finish..];
format!("{left}....{right}")
}
}


/// SOMPI (u64) to KASPA (string) with suffix layout job generator
pub fn s2kws_layout_job(
enable: bool,
Expand Down
11 changes: 11 additions & 0 deletions core/src/utils/qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,14 @@ pub fn render_qrcode(text: &str, width: usize, height: usize) -> String {
.build()
.to_string()
}

pub fn render_qrcode_with_version(text: &str, width: usize, height: usize, version : Version) -> String {
let code = QrCode::with_version(text, version, EcLevel::L).unwrap();

code.render::<svg::Color<'_>>()
.min_dimensions(width as u32, height as u32)
.light_color(svg::Color(theme_color().qr_background.to_hex().as_str()))
.dark_color(svg::Color(theme_color().qr_foreground.to_hex().as_str()))
.build()
.to_string()
}
Loading

0 comments on commit 0c68871

Please sign in to comment.