From beb1a80c6aef1fd5c17186fc2feee1702fae130d Mon Sep 17 00:00:00 2001 From: ardocrat Date: Tue, 3 Mar 2026 19:54:46 +0000 Subject: [PATCH] Payment proofs (#52) - Ability to export and verify payment proofs Some fixes: - Migrated tx heights store from lmdb (also changed heights key from local id to slate_id to avoid conflict between wallets) - Close address panel on wallet change Reviewed-on: https://code.gri.mw/GUI/grim/pulls/52 --- Cargo.lock | 33 +- Cargo.toml | 3 +- locales/de.yml | 4 + locales/en.yml | 4 + locales/fr.yml | 4 + locales/ru.yml | 4 + locales/tr.yml | 4 + locales/zh-CN.yml | 6 +- src/gui/views/qr.rs | 2 +- src/gui/views/wallets/wallet/content.rs | 122 ++++-- src/gui/views/wallets/wallet/message.rs | 44 +- src/gui/views/wallets/wallet/mod.rs | 3 +- src/gui/views/wallets/wallet/proof.rs | 214 +++++++++ src/gui/views/wallets/wallet/request/send.rs | 9 +- .../views/wallets/wallet/transport/content.rs | 11 +- src/gui/views/wallets/wallet/txs/content.rs | 15 +- src/gui/views/wallets/wallet/txs/tx.rs | 59 ++- src/main.rs | 7 +- src/node/config.rs | 1 - src/wallet/config.rs | 26 +- src/wallet/store.rs | 47 +- src/wallet/types.rs | 58 ++- src/wallet/wallet.rs | 406 +++++++++++++----- 23 files changed, 830 insertions(+), 256 deletions(-) create mode 100644 src/gui/views/wallets/wallet/proof.rs diff --git a/Cargo.lock b/Cargo.lock index 389357ef..c0dc8811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4077,6 +4077,7 @@ dependencies = [ "ur", "url", "usvg", + "uuid 0.8.2", "wgpu", "winit", ] @@ -5765,29 +5766,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" -[[package]] -name = "lmdb-rkv" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447a296f7aca299cfbb50f4e4f3d49451549af655fb7215d7f8c0c3d64bad42b" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "libc", - "lmdb-rkv-sys", -] - -[[package]] -name = "lmdb-rkv-sys" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b9ce6b3be08acefa3003c57b7565377432a89ec24476bbe72e11d101f852fe" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "lmdb-zero" version = "0.4.4" @@ -8433,23 +8411,20 @@ dependencies = [ [[package]] name = "rkv" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6d906922d99c677624d2042a93f89b2b7df0f6411032237d5d99a602c2487c" +checksum = "0f67a9dbc634fcd36a2d1d800ca818065dcf71a1d907dc35130c2d1552c6e1dc" dependencies = [ "arrayref", "bincode 1.3.3", "bitflags 2.10.0", - "byteorder", "id-arena", "lazy_static", - "lmdb-rkv", "log", "ordered-float 3.9.2", - "paste", "serde", "serde_derive", - "thiserror 1.0.69", + "thiserror 2.0.18", "url", "uuid 1.20.0", ] diff --git a/Cargo.toml b/Cargo.toml index 5218d03f..7491720d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ qrcodegen = "1.8.0" qrcode = "0.14.1" ur = "0.4.1" gif = "0.14.1" -rkv = { version = "0.19.0", features = ["lmdb"] } +rkv = "0.20.0" usvg = "0.45.1" ring = "0.16.20" hyper = { version = "1.6.0", features = ["full"], package = "hyper" } @@ -87,6 +87,7 @@ hyper-socks2 = "0.9.1" hyper-proxy2 = "0.1.0" hyper-tls = "0.6.0" async-std = "1.13.2" +uuid = { version = "0.8.2", features = ["v4"] } ## tor arti-client = { version = "0.38.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] } diff --git a/locales/de.yml b/locales/de.yml index dd0beb95..f8936b6d 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -135,6 +135,10 @@ wallets: rec_phrase_not_found: Wiederhestellungsphrase nicht gefunden. restore_wallet_desc: Stellen Sie das Wallet wieder her, indem Sie alle Dateien löschen. Wenn die normale Reparatur nicht geholfen hat, müssen Sie Ihr Wallet erneut öffnen. fee_base_desc: 'Gebühr (basiswert%{value}):' + payment_proof: Zahlungsnachweis + payment_proof_desc: 'Geben Sie den erhaltenen Zahlungsnachweis ein, um die Transaktion zu verifizieren:' + payment_proof_valid: 'Der eingegebene Zahlungsnachweis ist gültig:' + payment_proof_error: 'Der eingetragene Zahlungsnachweis ist nicht gültig:' transport: desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:' tor_network: Tor Netzwek diff --git a/locales/en.yml b/locales/en.yml index 3adf73fd..55ac5d1d 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -135,6 +135,10 @@ wallets: rec_phrase_not_found: Recovery phrase not found. restore_wallet_desc: Restore wallet by deleting all files if usual repair not helped, you will need to re-open your wallet. fee_base_desc: 'Fee (base value%{value}):' + payment_proof: Payment proof + payment_proof_desc: 'Enter received payment proof to verify transaction:' + payment_proof_valid: 'Entered payment proof is valid:' + payment_proof_error: 'Entered payment proof is not valid:' transport: desc: 'Use transport to receive or send messages synchronously:' tor_network: Tor network diff --git a/locales/fr.yml b/locales/fr.yml index d72ba765..185be00a 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -135,6 +135,10 @@ wallets: rec_phrase_not_found: Phrase de récupération non trouvée. restore_wallet_desc: "Restaurer le portefeuille en supprimant tous les fichiers si la réparation habituelle n'a pas aidé. Vous devrez rouvrir votre portefeuille." fee_base_desc: 'Frais (valeur de base%{value}):' + payment_proof: Preuve de paiement + payment_proof_desc: 'Saisissez la preuve de paiement reçue pour vérifier la transaction:' + payment_proof_valid: 'La preuve de paiement saisie est valide:' + payment_proof_error: "La preuve de paiement saisie n'est pas valide:" transport: desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:' tor_network: Réseau Tor diff --git a/locales/ru.yml b/locales/ru.yml index 65fc323b..b137aa77 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -135,6 +135,10 @@ wallets: rec_phrase_not_found: Фраза восстановления не найдена. restore_wallet_desc: Восстановить кошелёк, удалив все файлы, если обычное исправление не помогло. Необходимо переоткрыть кошелёк. fee_base_desc: 'Комиссия (базовое значение%{value}):' + payment_proof: Подтверждение оплаты + payment_proof_desc: 'Введите полученное подтверждение оплаты для проверки транзакции:' + payment_proof_valid: 'Введённое подтверждение оплаты действительно:' + payment_proof_error: 'Введённое подтверждение оплаты недействительно:' transport: desc: 'Используйте транспорт для синхронных получения или отправки сообщений:' tor_network: Сеть Tor diff --git a/locales/tr.yml b/locales/tr.yml index 55122a46..f769f7b0 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -135,6 +135,10 @@ wallets: rec_phrase_not_found: Sifre kelime bulunmuyor restore_wallet_desc: Cuzdani restore et fee_base_desc: 'Ücret (taban değeri%{value}):' + payment_proof: Ödeme kaniti + payment_proof_desc: 'Islemi doğrulamak için alinan ödeme kanitini girin:' + payment_proof_valid: 'Girilen ödeme kaniti geçerlidir:' + payment_proof_error: 'Girilen ödeme kaniti geçerli değildir:' transport: desc: 'Adresten senkronize GONDER veya AL:' tor_network: Tor network diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index be01c913..e80e8231 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -85,7 +85,6 @@ wallets: tx_sent: 已发送 tx_received: 已接收 tx_sending: 发送中 - tx_sending_tor: 通过 Tor 发送 tx_receiving: 接收中 tx_confirming: 等待确认 tx_canceled: 已取消 @@ -136,6 +135,10 @@ wallets: rec_phrase_not_found: 找不到恢复助记词. restore_wallet_desc: 如果常规修复没有帮助,通过删除所有文件来恢复钱包.您将需要重新打开您的钱包. fee_base_desc: '费用 (基值%{value}):' + payment_proof: 付款證明 + payment_proof_desc: '輸入已收款證明以驗證交易:' + payment_proof_valid: '輸入的付款證明有效:' + payment_proof_error: '輸入的付款證明無效:' transport: desc: '使用传输同步接收或发送消息:' tor_network: Tor 网络 @@ -148,6 +151,7 @@ transport: incorrect_addr_err: '输入的地址不正确:' tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消. tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易. + tor_sending: 通过 Tor 发送 tor_settings: Tor 设置 bridges: 桥梁 bridges_desc: 如果常规连接不正常,设置网桥,可以绕过 Tor 网络审查. diff --git a/src/gui/views/qr.rs b/src/gui/views/qr.rs index 2687903a..92a7c14f 100644 --- a/src/gui/views/qr.rs +++ b/src/gui/views/qr.rs @@ -31,7 +31,7 @@ use crate::gui::Colors; /// QR code image from text. pub struct QrCodeContent { /// QR code text. - text: String, + pub text: String, /// Maximum QR code size. max_size: f32, diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 62ba6a81..259f7dad 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -31,6 +31,7 @@ use crate::node::Node; use crate::wallet::types::{ConnectionMethod, WalletTask}; use crate::wallet::{ExternalConnection, Wallet}; use crate::AppConfig; +use crate::gui::views::wallets::wallet::proof::PaymentProofContent; /// Wallet content. pub struct WalletContent { @@ -267,43 +268,7 @@ impl WalletContentContainer for WalletContent { } // Handle wallet task result. - if let Some((id, t)) = wallet.consume_task_result() { - match Modal::opened() { - None => { - // Show transaction modal on wallet task result. - if let Some(id) = id { - let tx = wallet.get_data().unwrap().tx_by_slate_id(id); - if tx.is_some() { - self.txs_content = Some(WalletTransactionsContent::new(tx)); - self.settings_content = None; - } - } - } - Some(modal_id) => { - match modal_id { - SEND_MODAL_ID => { - match t { - WalletTask::CalculateFee(_, f) => { - // Setup calculated tx fee at modal. - if let Some(m) = self.send_content.as_mut() { - if m.max_calculating { - let data = wallet.get_data().unwrap(); - let a = data.info.amount_currently_spendable; - let max = a - f; - m.on_max_amount_calculated(max, f); - } else { - m.on_fee_calculated(f); - } - } - } - _ => {} - } - } - _ => {} - } - } - } - } + self.handle_task_result(wallet); } let rect = { let mut r = rect.clone(); @@ -466,6 +431,89 @@ impl WalletContent { }); }); } + + /// Handle wallet task result. + fn handle_task_result(&mut self, wallet: &Wallet) { + let res = wallet.consume_task_result(); + if res.is_none() { + return; + } + let (id, t) = res.unwrap(); + match Modal::opened() { + None => { + // Show transaction modal on wallet task result. + if let Some(id) = id { + let tx = wallet.get_data().unwrap().tx_by_slate_id(id); + if tx.is_some() { + self.txs_content = Some(WalletTransactionsContent::new(tx)); + self.settings_content = None; + } + } + } + Some(modal_id) => { + match modal_id { + SEND_MODAL_ID => { + match t { + WalletTask::CalculateFee(_, f) => { + // Setup calculated tx fee at modal. + if let Some(m) = self.send_content.as_mut() { + if m.max_calculating { + let data = wallet.get_data().unwrap(); + let a = data.info.amount_currently_spendable; + let max = if f > a { + 0 + } else { + a - f + }; + m.on_max_amount_calculated(max, f); + } else { + m.on_fee_calculated(f); + } + } + } + _ => {} + } + } + MESSAGE_MODAL_ID => { + match t { + WalletTask::VerifyProof(proof, res) => { + if let Some(res) = res { + // Update message content on validation error + // or when tx not belongs to current wallet. + if res.is_err() || (!res.as_ref().unwrap().1 && + !res.as_ref().unwrap().2) { + if let Some(m) = self.message_content.as_mut() { + if let Ok(p) = serde_json::to_string_pretty(&proof) { + let mut c = PaymentProofContent::new(Some(p)); + c.validation_result = Some(res); + m.proof_content = Some(c); + } + } + } else if let Ok((id, _, _)) = res { + let tx = wallet.get_data().unwrap().tx_by_id(id); + if let Some(tx) = tx { + let mut tx_c = WalletTransactionsContent::new(Some(tx)); + if let Ok(p) = serde_json::to_string_pretty(&proof) { + let proof_c = PaymentProofContent::new(Some(p)); + tx_c.tx_info_content + .as_mut() + .unwrap() + .proof_content = Some(proof_c); + self.txs_content = Some(tx_c); + self.settings_content = None; + } + } + } + } + } + _ => {} + } + } + _ => {} + } + } + } + } } /// Draw content when wallet is syncing and not ready to use, returns `true` at this case. diff --git a/src/gui/views/wallets/wallet/message.rs b/src/gui/views/wallets/wallet/message.rs index eb0911a3..c378fca9 100644 --- a/src/gui/views/wallets/wallet/message.rs +++ b/src/gui/views/wallets/wallet/message.rs @@ -15,10 +15,11 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, RichText, ScrollArea}; -use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, SCAN}; +use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, SCAN, SEAL_CHECK}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, View}; use crate::gui::Colors; +use crate::gui::views::wallets::wallet::proof::PaymentProofContent; use crate::wallet::types::WalletTask; use crate::wallet::Wallet; @@ -27,12 +28,13 @@ pub struct MessageInputContent { message_edit: String, /// Flag to check if error happened at Slatepack message parsing. parse_error: bool, - /// QR code scanner content. scan_qr_content: Option, - /// Button to parse picked file content. file_pick_button: FilePickContent, + + /// Payment proof input content. + pub proof_content: Option, } /// Hint for Slatepack message input. @@ -45,6 +47,7 @@ impl Default for MessageInputContent { parse_error: false, scan_qr_content: None, file_pick_button: FilePickContent::new(FilePickContentType::Button), + proof_content: None, } } } @@ -88,6 +91,19 @@ impl MessageInputContent { }); }); }); + } else if let Some(proof_content) = self.proof_content.as_mut() { + proof_content.input_ui(ui, wallet, cb); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.message_edit = "".to_string(); + Modal::close(); + }); + }); } else { ui.add_space(6.0); ui.vertical_centered(|ui| { @@ -114,14 +130,18 @@ impl MessageInputContent { .show(ui, |ui| { ui.add_space(7.0); let input_id = scroll_id.with("_input"); - egui::TextEdit::multiline(&mut self.message_edit) + let resp = egui::TextEdit::multiline(&mut self.message_edit) .id(input_id) .font(egui::TextStyle::Small) .desired_rows(5) .interactive(true) .hint_text(SLATEPACK_MESSAGE_HINT) .desired_width(f32::INFINITY) - .show(ui).response; + .show(ui) + .response; + if View::is_desktop() { + resp.request_focus(); + } ui.add_space(6.0); }); }); @@ -181,6 +201,20 @@ impl MessageInputContent { View::horizontal_line(ui, Colors::item_stroke()); ui.add_space(8.0); + ui.vertical_centered(|ui| { + let proof_label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof")); + View::colored_text_button(ui, + proof_label, + Colors::gold_dark(), + Colors::white_or_black(false), || { + Modal::set_title(t!("wallets.payment_proof")); + self.proof_content = Some(PaymentProofContent::new(None)); + }); + }); + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + // Show button to close modal. ui.vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index 599a4516..6832c31e 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -26,4 +26,5 @@ pub use content::WalletContent; mod account; mod transport; mod request; -mod message; \ No newline at end of file +mod message; +mod proof; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/proof.rs b/src/gui/views/wallets/wallet/proof.rs new file mode 100644 index 00000000..acb2bccb --- /dev/null +++ b/src/gui/views/wallets/wallet/proof.rs @@ -0,0 +1,214 @@ +use egui::scroll_area::ScrollBarVisibility; +use egui::{Id, RichText, ScrollArea}; +use grin_wallet_libwallet::{Error, PaymentProof}; + +use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, FILE_TEXT, SEAL_CHECK}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{FilePickContent, FilePickContentType, Modal, View}; +use crate::gui::Colors; +use crate::wallet::types::WalletTask; +use crate::wallet::Wallet; + +pub struct PaymentProofContent { + /// Payment proof text. + input_edit: String, + /// Button to pick payment proof file. + pick_button: FilePickContent, + /// Flag to check if an error occurred during proof parsing. + parse_error: bool, + /// Proof validation result. + pub validation_result: Option>, +} + +impl PaymentProofContent { + /// Create new content to share or validate payment proof. + pub fn new(proof_text: Option) -> Self { + Self { + input_edit: proof_text.unwrap_or("".to_string()), + pick_button: FilePickContent::new(FilePickContentType::Button), + parse_error: false, + validation_result: None, + } + } + + /// Draw transaction payment proof input. + pub fn input_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + if self.parse_error { + let label_text = t!("wallets.payment_proof_error"); + ui.label(RichText::new(label_text).size(16.0).color(Colors::red())); + } else if let Some(proof) = self.validation_result.as_ref() { + match proof { + Ok(_) => { + let label_text = t!("wallets.payment_proof_valid"); + ui.label(RichText::new(label_text).size(16.0).color(Colors::green())); + } + Err(e) => { + let error_text = t!("wallets.payment_proof_error"); + let label_text = format!("{} ({:?})", error_text, e); + ui.label(RichText::new(label_text).size(16.0).color(Colors::red())); + } + } + } else { + let desc_label = t!("wallets.payment_proof_desc"); + ui.label(RichText::new(desc_label).size(16.0).color(Colors::inactive_text())); + } + }); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let scroll_id = Id::from("tx_info_payment_proof_input"); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("edit"); + let proof_input_before = self.input_edit.clone(); + let resp = egui::TextEdit::multiline(&mut self.input_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(!wallet.payment_proof_verifying()) + .desired_width(f32::INFINITY) + .show(ui) + .response; + if View::is_desktop() { + resp.request_focus(); + } + // Parse payment proof on input change. + if self.input_edit != proof_input_before { + self.on_proof_edit_change(wallet); + } + ui.add_space(6.0); + }); + }); + + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + if wallet.payment_proof_verifying() { + ui.vertical_centered(|ui| { + View::small_loading_spinner(ui); + }); + } else { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + if self.parse_error || (self.validation_result.is_some() && + self.validation_result.as_ref().unwrap().is_err()) { + // Draw button to clear message input. + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::white_or_black(false), || { + self.input_edit = "".to_string(); + self.parse_error = false; + self.validation_result = None; + }); + } else { + // Draw button to paste proof text. + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::white_or_black(false), || { + self.input_edit = cb.get_string_from_buffer(); + self.on_proof_edit_change(wallet); + }); + } + }); + columns[1].vertical_centered_justified(|ui| { + let mut changed = false; + self.pick_button.ui(ui, cb, |data| { + self.input_edit = data.clone(); + changed = true; + }); + if changed { + self.on_proof_edit_change(wallet); + } + }); + }); + } + } + + /// Callback on payment proof input change. + fn on_proof_edit_change(&mut self, wallet: &Wallet) { + if wallet.payment_proof_verifying() { + return; + } + if self.input_edit.is_empty() { + self.parse_error = false; + return; + } + if let Ok(p) = serde_json::from_str::(self.input_edit.as_str()) { + wallet.task(WalletTask::VerifyProof(p, None)); + } else { + self.parse_error = true; + } + } + + /// Draw transaction payment proof content to share. + pub fn share_ui(&mut self, ui: &mut egui::Ui, file_name: String, cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let desc = format!("{} {}:", SEAL_CHECK, t!("wallets.payment_proof")); + ui.label(RichText::new(desc).size(16.0).color(Colors::inactive_text())); + }); + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let scroll_id = Id::from("tx_info_payment_proof_share"); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("edit"); + egui::TextEdit::multiline(&mut self.input_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui); + ui.add_space(6.0); + }); + }); + + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw copy button. + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::white_or_black(false), || { + cb.copy_string_to_buffer(self.input_edit.clone()); + Modal::close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + let share_text = format!("{} {}", FILE_TEXT, t!("share")); + View::colored_text_button(ui, + share_text, + Colors::blue(), + Colors::white_or_black(false), || { + let file_name = format!("{}.txt", file_name); + let data = self.input_edit.as_bytes().to_vec(); + cb.share_data(file_name, data).unwrap_or_default(); + Modal::close(); + }); + }); + }); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/request/send.rs b/src/gui/views/wallets/wallet/request/send.rs index 5c0102bd..05b4f24e 100644 --- a/src/gui/views/wallets/wallet/request/send.rs +++ b/src/gui/views/wallets/wallet/request/send.rs @@ -63,8 +63,13 @@ impl SendRequestContent { /// Setup maximum amount to send and fee. pub fn on_max_amount_calculated(&mut self, amount: u64, fee: u64) { self.max_calculating = false; - self.amount_edit = amount_to_hr_string(amount, true); - self.fee_edit = amount_to_hr_string(fee, true); + if amount == 0 { + self.amount_edit = "".to_string(); + self.fee_edit = "".to_string(); + } else { + self.amount_edit = amount_to_hr_string(amount, true); + self.fee_edit = amount_to_hr_string(fee, true); + } } /// Draw [`Modal`] content. diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs index a686339d..40d19ba5 100644 --- a/src/gui/views/wallets/wallet/transport/content.rs +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -40,11 +40,16 @@ impl WalletContentContainer for WalletTransportContent { fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { if let Some(content) = self.qr_address_content.as_mut() { - ui.add_space(6.0); - + // Close panel on wallet change. + if let Some(address) = wallet.slatepack_address() { + if address != content.text { + self.qr_address_content = None; + return; + } + } // Draw QR code content. + ui.add_space(6.0); content.ui(ui, cb); - ui.vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { self.qr_address_content = None; diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index 8971b09c..9a693a2f 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -34,7 +34,7 @@ use crate::wallet::Wallet; /// Wallet transactions tab content. pub struct WalletTransactionsContent { /// Transaction information [`Modal`] content. - tx_info_content: Option, + pub tx_info_content: Option, /// Transaction identifier to use at confirmation [`Modal`]. confirm_cancel_tx_id: Option, @@ -69,6 +69,7 @@ impl WalletContentContainer for WalletTransactionsContent { /// Identifier for transaction information [`Modal`]. const TX_INFO_MODAL: &'static str = "tx_info_modal"; + /// Identifier for transaction cancellation confirmation [`Modal`]. const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal"; @@ -171,7 +172,7 @@ impl WalletTransactionsContent { let tx = txs.get(index).unwrap(); Self::tx_item_ui(ui, tx, rect, &data, |ui| { // Draw button to show transaction info. - if tx.data.tx_slate_id.is_some() { + if tx.data.tx_slate_id.is_some() || tx.data.payment_proof.is_some() { r.nw = 0.0 as u8; r.sw = 0.0 as u8; View::item_button(ui, r, FILE_TEXT, None, || { @@ -459,17 +460,17 @@ impl WalletTransactionsContent { let (icon, color) = (ARROWS_CLOCKWISE, Some(Colors::green())); View::item_button(ui, rounding, icon, color, || { if repost { - wallet.task(WalletTask::Post(None, tx.data.id)); + wallet.task(WalletTask::Post(tx.data.id)); } else { match tx.action.as_ref().unwrap() { WalletTransactionAction::Cancelling => { - wallet.task(WalletTask::Cancel(tx.clone())); + wallet.task(WalletTask::Cancel(tx.data.clone())); } WalletTransactionAction::Finalizing => { - wallet.task(WalletTask::Finalize(None, tx.data.id)); + wallet.task(WalletTask::Finalize(tx.data.id)); } WalletTransactionAction::Posting => { - wallet.task(WalletTask::Post(None, tx.data.id)); + wallet.task(WalletTask::Post(tx.data.id)); } WalletTransactionAction::SendingTor => { if let Some(a) = &tx.receiver { @@ -536,7 +537,7 @@ impl WalletTransactionsContent { }); columns[1].vertical_centered_justified(|ui| { View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { - wallet.task(WalletTask::Cancel(tx.clone())); + wallet.task(WalletTask::Cancel(tx.data.clone())); self.confirm_cancel_tx_id = None; Modal::close(); }); diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index b241e7fe..db076f66 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -19,8 +19,9 @@ use grin_util::ToHex; use grin_wallet_libwallet::TxLogEntryType; use std::fs; -use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE}; +use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SEAL_CHECK}; use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::wallets::wallet::proof::PaymentProofContent; use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent; use crate::gui::views::{Modal, QrCodeContent, View}; use crate::gui::Colors; @@ -36,15 +37,19 @@ pub struct WalletTransactionContent { /// QR code Slatepack message image content. qr_code_content: Option, + + /// Payment proof sharing content. + pub proof_content: Option, } impl WalletTransactionContent { /// Create new content instance with [`Wallet`] from provided [`WalletTransaction`]. - pub fn new(id: u32) -> Self { + pub fn new(tx_id: u32) -> Self { Self { - tx_id: id, + tx_id, message: None, qr_code_content: None, + proof_content: None, } } @@ -92,9 +97,35 @@ impl WalletTransactionContent { // Show transaction information. self.info_ui(ui, tx, wallet, cb); - // Show transaction sharing content when can cancel or finalized. - if tx.can_cancel() && !tx.finalized() { + // Show transaction sharing content or payment proof. + if self.proof_content.is_none() && tx.can_cancel() && !tx.finalized() { self.share_ui(ui, wallet, tx, cb); + } else { + if let Some(proof_content) = self.proof_content.as_mut() { + // Payment proof file name setup. + let file_name = if let Some(slate_id) = tx.data.tx_slate_id { + slate_id.to_string() + } else { + tx.data.id.to_string() + }; + // Draw payment proof sharing content. + proof_content.share_ui(ui, file_name, cb); + } else if tx.proof.is_some() && !tx.sending_tor() && + tx.action_error.is_none() { + ui.vertical_centered(|ui| { + ui.add_space(8.0); + let label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof")); + let text_color = Colors::gold_dark(); + let btn_color = Colors::white_or_black(false); + // Draw button to show payment proof sharing content. + View::colored_text_button(ui, label, text_color, btn_color, || { + if let Ok(p) = serde_json::to_string_pretty(&tx.proof) { + let c = PaymentProofContent::new(Some(p)); + self.proof_content = Some(c); + } + }); + }); + } } ui.add_space(8.0); @@ -125,6 +156,7 @@ impl WalletTransactionContent { if m.is_empty() { return; } + let amount = amount_to_hr_string(tx.amount, true); let desc_text = if tx.can_finalize() { if tx.data.tx_type == TxLogEntryType::TxSent { @@ -146,7 +178,7 @@ impl WalletTransactionContent { ui.add_space(6.0); let mut message = m.clone(); - // Draw slatepack message content. + // Draw Slatepack message content. ui.vertical_centered(|ui| { let scroll_id = Id::from("tx_info_message_request").with(tx.data.id); View::horizontal_line(ui, Colors::item_stroke()); @@ -165,7 +197,7 @@ impl WalletTransactionContent { .desired_rows(5) .interactive(false) .desired_width(f32::INFINITY) - .show(ui).response; + .show(ui); ui.add_space(6.0); }); }); @@ -255,7 +287,7 @@ impl WalletTransactionContent { if tx.can_cancel() || rebroadcast { let r = View::item_rounding(0, 2, true); View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || { - wallet.task(WalletTask::Cancel(tx.clone())); + wallet.task(WalletTask::Cancel(tx.data.clone())); Modal::close(); }); } @@ -282,10 +314,15 @@ impl WalletTransactionContent { let label = format!("{} {}", FILE_ARCHIVE, t!("kernel")); info_item_ui(ui, kernel.0.to_hex(), label, true, cb); } - // Show receiver address. - if let Some(rec) = &tx.receiver { + // Show receiver or sender address. + let addr = if tx.data.tx_type == TxLogEntryType::TxSent { + &tx.receiver + } else { + &tx.sender + }; + if let Some(addr) = addr { let label = format!("{} {}", CIRCLE_HALF, t!("network_mining.address")); - info_item_ui(ui, rec.to_string(), label, true, cb); + info_item_ui(ui, addr.to_string(), label, true, cb); } } } diff --git a/src/main.rs b/src/main.rs index 1afff588..fe3d91aa 100755 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,8 @@ #![windows_subsystem = "windows"] +use log::error; + pub fn main() { #[allow(dead_code)] #[cfg(not(target_os = "android"))] @@ -23,7 +25,6 @@ pub fn main() { #[allow(dead_code)] #[cfg(not(target_os = "android"))] fn real_main() { - #[cfg(debug_assertions)] env_logger::builder() .filter_level(log::LevelFilter::Info) .parse_default_env() @@ -249,7 +250,7 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) { let opts = ListenerOptions::new().name(name); let listener = match opts.create_tokio() { Err(e) if e.kind() == io::ErrorKind::AddrInUse => { - eprintln!("Socket file is occupied."); + error!("Socket file is occupied."); return Err::(e); } x => x?, @@ -259,7 +260,7 @@ fn start_app_socket(platform: grim::gui::platform::Desktop) { let conn = match listener.accept().await { Ok(c) => c, Err(e) => { - println!("{:?}", e); + error!("{:?}", e); continue } }; diff --git a/src/node/config.rs b/src/node/config.rs index 1b6b85e2..57404da8 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -398,7 +398,6 @@ impl NodeConfig { .unwrap() .wallet_listener_url = id.to_string(); w_config.save(); - println!() } /// Get the amount of time in seconds to attempt to mine on a particular header. diff --git a/src/wallet/config.rs b/src/wallet/config.rs index 1d7f4371..6ee8375d 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -203,26 +203,33 @@ impl WalletConfig { } } - /// Get wallet seed path. - pub fn seed_path(&self) -> String { + /// Get base wallet data path. + fn get_base_data_path(&self) -> String { let mut path = PathBuf::from(self.get_data_path()); path.push(Self::DATA_DIR_NAME); + if !path.exists() { + let _ = fs::create_dir_all(path.clone()); + } + path.to_str().unwrap().to_string() + } + + /// Get wallet seed path. + pub fn seed_path(&self) -> String { + let mut path = PathBuf::from(self.get_base_data_path()); path.push(SEED_FILE); path.to_str().unwrap().to_string() } /// Get wallet database data path. pub fn get_db_path(&self) -> String { - let mut path = PathBuf::from(self.get_data_path()); - path.push(Self::DATA_DIR_NAME); + let mut path = PathBuf::from(self.get_base_data_path()); path.push(DB_DIR_NAME); path.to_str().unwrap().to_string() } /// Get Slatepack file path for transaction. pub fn get_tx_slate_path(&self, tx: &WalletTransaction) -> PathBuf { - let mut path = PathBuf::from(self.get_data_path()); - path.push(Self::DATA_DIR_NAME); + let mut path = PathBuf::from(self.get_base_data_path()); path.push(SLATEPACKS_DIR_NAME); if !path.exists() { let _ = fs::create_dir_all(path.clone()); @@ -234,8 +241,7 @@ impl WalletConfig { /// Get Slatepack file path for Slate. pub fn get_slate_path(&self, slate: &Slate) -> PathBuf { - let mut path = PathBuf::from(self.get_data_path()); - path.push(Self::DATA_DIR_NAME); + let mut path = PathBuf::from(self.get_base_data_path()); path.push(SLATEPACKS_DIR_NAME); if !path.exists() { let _ = fs::create_dir_all(path.clone()); @@ -246,13 +252,13 @@ impl WalletConfig { } /// Get path to extra db storage. - pub fn get_extra_db_path(&self) -> String { + pub fn get_extra_db_path(&self) -> PathBuf { let mut path = PathBuf::from(self.get_db_path()); path.push("extra"); if !path.exists() { let _ = fs::create_dir_all(path.clone()); } - path.to_str().unwrap().to_string() + path } /// Check config version to migrate if needed. diff --git a/src/wallet/store.rs b/src/wallet/store.rs index feccb5be..d0c57201 100644 --- a/src/wallet/store.rs +++ b/src/wallet/store.rs @@ -12,41 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::path::PathBuf; use std::sync::{Arc, RwLock}; -use rkv::backend::{Lmdb, LmdbDatabase, LmdbEnvironment}; -use rkv::{IntegerStore, Manager, Rkv, StoreOptions, Value}; +use rkv::backend::{SafeMode, SafeModeDatabase, SafeModeEnvironment}; +use rkv::{Manager, Rkv, SingleStore, StoreOptions, Value}; /// Transaction height storage. pub struct TxHeightStore { - env: Arc>>, + env: Arc>>, /// Confirmed heights. - confirmed: IntegerStore, + confirmed: SingleStore, /// Broadcasting heights. - broadcasting: IntegerStore + broadcasting: SingleStore } impl TxHeightStore { /// Create new transaction height storage from provided directory. - pub fn new(dir: String) -> Self { - let mut manager = Manager::::singleton().write().unwrap(); - let env_arc = manager.get_or_create(std::path::Path::new(&dir), Rkv::new::).unwrap(); + pub fn new(dir: PathBuf) -> Self { + let mut manager = Manager::::singleton().write().unwrap(); + let created_arc = manager.get_or_create(dir.as_path(), Rkv::new::).unwrap(); + let env = created_arc.clone(); + let k = created_arc.read().unwrap(); - let env_arc_store = env_arc.clone(); - let env = env_arc_store.read().unwrap(); - let confirmed = env.open_integer("tx_height", StoreOptions::create()).unwrap(); - let broadcasting = env.open_integer("broadcast_tx_height", StoreOptions::create()).unwrap(); + let confirmed = k.open_single("tx_height", StoreOptions::create()).unwrap(); + let broadcasting = k.open_single("broadcast_tx_height", StoreOptions::create()).unwrap(); Self { - env: env_arc, + env, confirmed, broadcasting } } /// Read transaction height from database. - pub fn read_tx_height(&self, id: u32) -> Option { + pub fn read_tx_height(&self, slate_id: &String) -> Option { let env = self.env.read().unwrap(); let reader = env.read().unwrap(); - if let Ok(value) = self.confirmed.get(&reader, id) { + if let Ok(value) = self.confirmed.get(&reader, slate_id) { if let Some(height) = value { return match height { Value::U64(v) => Some(v), @@ -59,18 +60,18 @@ impl TxHeightStore { } /// Write transaction height to database. - pub fn write_tx_height(&self, id: u32, height: u64) { + pub fn write_tx_height(&self, slate_id: &String, height: u64) { let env = self.env.read().unwrap(); let mut writer = env.write().unwrap(); - self.confirmed.put(&mut writer, id, &Value::U64(height)).unwrap(); + self.confirmed.put(&mut writer, slate_id, &Value::U64(height)).unwrap(); writer.commit().unwrap(); } /// Read broadcasting height from database. - pub fn read_broadcasting_height(&self, id: u32) -> Option { + pub fn read_broadcasting_height(&self, slate_id: &String) -> Option { let env = self.env.read().unwrap(); let reader = env.read().unwrap(); - if let Ok(value) = self.broadcasting.get(&reader, id) { + if let Ok(value) = self.broadcasting.get(&reader, slate_id) { if let Some(height) = value { return match height { Value::U64(v) => Some(v), @@ -83,18 +84,18 @@ impl TxHeightStore { } /// Write broadcasting height to database. - pub fn write_broadcasting_height(&self, id: u32, height: u64) { + pub fn write_broadcasting_height(&self, slate_id: &String, height: u64) { let env = self.env.read().unwrap(); let mut writer = env.write().unwrap(); - self.broadcasting.put(&mut writer, id, &Value::U64(height)).unwrap(); + self.broadcasting.put(&mut writer, slate_id, &Value::U64(height)).unwrap(); writer.commit().unwrap(); } /// Delete broadcasting height from database. - pub fn delete_broadcasting_height(&self, id: u32) { + pub fn delete_broadcasting_height(&self, slate_id: &String) { let env = self.env.read().unwrap(); let mut writer = env.write().unwrap(); - self.broadcasting.delete(&mut writer, id).unwrap_or_default(); + self.broadcasting.delete(&mut writer, slate_id).unwrap_or_default(); writer.commit().unwrap(); } } diff --git a/src/wallet/types.rs b/src/wallet/types.rs index c220dcd3..62a57185 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -15,7 +15,7 @@ use grin_keychain::ExtKeychain; use grin_util::Mutex; use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient}; -use grin_wallet_libwallet::{Error, Slate, SlateState, SlatepackAddress, TxLogEntry, TxLogEntryType, WalletInfo, WalletInst}; +use grin_wallet_libwallet::{Error, PaymentProof, Slate, SlateState, SlatepackAddress, TxLogEntry, TxLogEntryType, WalletInfo, WalletInst}; use grin_wallet_util::OnionV3Address; use serde_derive::{Deserialize, Serialize}; use std::sync::Arc; @@ -198,6 +198,19 @@ impl WalletData { } None } + + /// Get transaction by identifier. + pub fn tx_by_id(&self, id: u32) -> Option { + if self.txs.is_none() { + return None; + } + for tx in self.txs.as_ref().unwrap() { + if tx.data.id == id { + return Some(tx.clone()); + } + } + None + } } /// Wallet transaction action. @@ -213,11 +226,15 @@ pub struct WalletTransaction { pub data: TxLogEntry, /// State of transaction Slate. pub state: SlateState, + /// Payment proof. + pub(crate) proof: Option, /// Transaction amount without fees. pub amount: u64, /// Possible receiver of transaction. pub receiver: Option, + /// Possible sender of transaction. + pub sender: Option, /// Block height where tx was included. pub height: Option, /// Block height where tx started broadcasting. @@ -232,6 +249,7 @@ pub struct WalletTransaction { impl WalletTransaction { /// Create new wallet transaction. pub fn new(tx: TxLogEntry, + proof: Option, wallet: &Wallet, height: Option, broadcasting_height: Option, @@ -243,17 +261,24 @@ impl WalletTransaction { tx.amount_credited - tx.amount_debited }; let mut receiver: Option = None; + let mut sender: Option = None; if let Some(proof) = &tx.payment_proof { - let onion_addr = OnionV3Address::from_bytes(proof.receiver_address.to_bytes()); - if let Ok(addr) = SlatepackAddress::try_from(onion_addr) { + let rec_onion_addr = OnionV3Address::from_bytes(proof.receiver_address.to_bytes()); + if let Ok(addr) = SlatepackAddress::try_from(rec_onion_addr) { receiver = Some(addr); } + let send_onion_addr = OnionV3Address::from_bytes(proof.sender_address.to_bytes()); + if let Ok(addr) = SlatepackAddress::try_from(send_onion_addr) { + sender = Some(addr); + } } let mut t = Self { data: tx, state: SlateState::Unknown, + proof, amount, receiver, + sender, height, broadcasting_height, action, @@ -289,10 +314,15 @@ impl WalletTransaction { self.state = slate.state; } else { // Transaction just was created. - self.state = match tx.tx_type { + slate.state = match tx.tx_type { TxLogEntryType::TxReceived => SlateState::Invoice1, _ => SlateState::Standard1 }; + if wallet.slatepack_exists(&slate) { + self.state = slate.state; + } else { + self.state = SlateState::Unknown; + } } } } @@ -361,9 +391,8 @@ impl WalletTransaction { /// Check if possible to repeat transaction action. pub fn can_repeat_action(&self) -> bool { - if let Some(a) = &self.action { - return self.action_error.is_some() && a != &WalletTransactionAction::SendingTor && - a != &WalletTransactionAction::Cancelling + if let Some(a) = &self.action { + return self.action_error.is_some() && a != &WalletTransactionAction::Cancelling } false } @@ -379,6 +408,9 @@ impl WalletTransaction { if self.broadcasting() { let last_height = data.info.last_confirmed_height; let broadcasting_height = self.broadcasting_height.unwrap_or(0); + if broadcasting_height == 0 { + return false; + } let delay = wallet.broadcasting_delay(); return last_height - broadcasting_height > delay; } @@ -396,6 +428,10 @@ pub enum WalletTask { /// * amount /// * fee (to read at result) CalculateFee(u64, u64), + /// Verify payment proof. + /// * payment proof + /// * result (tx id, sender mine, receiver mine) + VerifyProof(PaymentProof, Option>), /// Create request to send. /// * amount /// * receiver @@ -408,14 +444,12 @@ pub enum WalletTask { /// * amount Receive(u64), /// Transaction finalization. - /// * tx /// * local tx id - Finalize(Option, u32), + Finalize(u32), /// Post transaction to blockchain. - /// * tx /// * local tx id - Post(Option, u32), + Post(u32), /// Cancel transaction. /// * tx - Cancel(WalletTransaction), + Cancel(TxLogEntry), } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 5976b30b..551301c3 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -32,8 +32,8 @@ use grin_wallet_controller::command::parse_slatepack; use grin_wallet_controller::controller; use grin_wallet_controller::controller::ForeignAPIHandlerV2; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient, LMDBBackend}; -use grin_wallet_libwallet::api_impl::owner::{cancel_tx, init_send_tx, retrieve_summary_info, retrieve_txs}; -use grin_wallet_libwallet::{address, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, Slate, SlateState, SlateVersion, SlatepackAddress, StatusMessage, TxLogEntry, TxLogEntryType, VersionedSlate, WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider}; +use grin_wallet_libwallet::api_impl::owner::{cancel_tx, init_send_tx, retrieve_summary_info, retrieve_txs, verify_payment_proof}; +use grin_wallet_libwallet::{address, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, Slate, SlateState, SlateVersion, SlatepackAddress, StatusMessage, StoredProofInfo, TxLogEntry, TxLogEntryType, VersionedSlate, WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider}; use grin_wallet_util::OnionV3Address; use parking_lot::RwLock; use rand::Rng; @@ -49,6 +49,8 @@ use std::sync::{mpsc, Arc}; use std::thread::Thread; use std::time::Duration; use std::{fs, thread}; +use log::error; +use uuid::Uuid; /// Contains wallet instance, configuration and state, handles wallet commands. #[derive(Clone)] @@ -59,6 +61,8 @@ pub struct Wallet { instance: Arc>>, /// Connection of current wallet instance. connection: Arc>, + /// Wallet secret key for transport service. + keychain_mask: Arc>>, /// Wallet Slatepack address to receive txs at transport. slatepack_address: Arc>>, @@ -109,11 +113,15 @@ pub struct Wallet { /// Amount requests to calculate fee. fee_calculating: Arc, + /// Flag to check if sending request is creating. send_creating: Arc, /// Flag to check if invoice is creating. invoice_creating: Arc, + /// Amount requests to calculate fee. + proof_verifying: Arc, + /// Tasks sender. tasks_sender: Arc>>>, /// Task result with optional transaction identifier. @@ -128,6 +136,7 @@ impl Wallet { config: Arc::new(RwLock::new(config)), instance: Arc::new(RwLock::new(None)), connection: Arc::new(RwLock::new(connection)), + keychain_mask: Arc::new(RwLock::new(None)), slatepack_address: Arc::new(RwLock::new(None)), sync_thread: Arc::from(RwLock::new(None)), foreign_api_server: Arc::new(RwLock::new(None)), @@ -150,6 +159,7 @@ impl Wallet { send_creating: Arc::new(AtomicBool::new(false)), fee_calculating: Arc::new(AtomicU8::new(0)), invoice_creating: Arc::new(AtomicBool::new(false)), + proof_verifying: Arc::new(AtomicBool::new(false)), tasks_sender: Arc::new(RwLock::new(None)), task_result: Arc::new(RwLock::new(None)), } @@ -325,8 +335,12 @@ impl Wallet { }; let mut wallet_lock = instance.lock(); let lc = wallet_lock.lc_provider()?; - match lc.open_wallet(None, password, false, false) { - Ok(_) => { + match lc.open_wallet(None, password, true, false) { + Ok(m) => { + { + let mut w_mask = self.keychain_mask.write(); + *w_mask = m; + } // Reset an error on opening. self.set_sync_error(false); self.reset_sync_attempts(); @@ -360,7 +374,7 @@ impl Wallet { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut api = Owner::new(instance, None); - controller::owner_single_use(None, None, Some(&mut api), |api, m| { + controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { let mut w_address = self.slatepack_address.write(); *w_address = Some(api.get_slatepack_address(m, 0)?.to_string()); Ok(()) @@ -379,6 +393,12 @@ impl Wallet { Ok(w_inst.parent_key_id()) } + /// Get keychain mask [`SecretKey`]. + pub fn keychain_mask(&self) -> Option { + let r_key = self.keychain_mask.read(); + r_key.clone() + } + /// Get wallet [`SecretKey`] for transport. pub fn secret_key(&self) -> Option { let r_key = self.secret_key.read(); @@ -392,7 +412,7 @@ impl Wallet { let mut w_lock = instance.lock(); let lc = w_lock.lc_provider()?; let w_inst = lc.wallet_inst()?; - let k = w_inst.keychain((&None).as_ref())?; + let k = w_inst.keychain(self.keychain_mask().as_ref())?; let parent_key_id = w_inst.parent_key_id(); let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; @@ -608,6 +628,20 @@ impl Wallet { self.sync_attempts.store(0, Ordering::Relaxed); } + /// Select transaction by slate id. + fn tx_by_id(&self, id: Uuid) -> Option { + let r_inst = self.instance.as_ref().read(); + let inst = r_inst.clone().unwrap(); + let mask = self.keychain_mask(); + let tx_id = Some(id); + if let Ok((_, txs)) = retrieve_txs(inst, mask.as_ref(), &None, false, None, tx_id, None) { + if !txs.is_empty() { + return Some(txs.get(0).unwrap().clone()) + } + } + None + } + /// Send a task to the wallet. pub fn task(&self, task: WalletTask) { let r_tasks = self.tasks_sender.read(); @@ -628,7 +662,7 @@ impl Wallet { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut api = Owner::new(instance, None); - controller::owner_single_use(None, None, Some(&mut api), |api, m| { + controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { let id = api.create_account_path(m, label)?; if self.get_data().is_none() { return Err(Error::GenericError("No wallet data".to_string())); @@ -652,7 +686,7 @@ impl Wallet { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut api = Owner::new(instance, None); - controller::owner_single_use(None, None, Some(&mut api), |api, m| { + controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { api.set_active_account(m, label)?; // Set Slatepack address. let mut w_address = self.slatepack_address.write(); @@ -749,24 +783,30 @@ impl Wallet { } /// Parse Slatepack message into [`Slate`]. - pub fn parse_slatepack(&self, text: &String) -> Result { + pub fn parse_slatepack(&self, text: &String) + -> Result<(Slate, Option), grin_wallet_controller::Error> { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut api = Owner::new(instance, None); - match parse_slatepack(&mut api, None, None, Some(text.clone())) { - Ok(s) => Ok(s.0), + match parse_slatepack(&mut api, self.keychain_mask().as_ref(), None, Some(text.clone())) { + Ok(s) => Ok(s), Err(e) => Err(e) } } /// Create Slatepack message from provided slate. - fn create_slatepack_message(&self, slate: &Slate) -> Result { + fn create_slatepack_message(&self, slate: &Slate, dest: Option) + -> Result { let mut message = "".to_string(); let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut api = Owner::new(instance, None); - controller::owner_single_use(None, None, Some(&mut api), |api, m| { - message = api.create_slatepack_message(m, &slate, Some(0), vec![])?; + controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { + let recipients = match dest { + Some(a) => vec![a], + None => vec![], + }; + message = api.create_slatepack_message(m, &slate, Some(0), recipients)?; Ok(()) })?; @@ -800,7 +840,7 @@ impl Wallet { estimate_only: Some(true), ..Default::default() }; - let res = init_send_tx(&mut **w, None, args, false); + let res = init_send_tx(&mut **w, self.keychain_mask().as_ref(), args, false); match res { Ok(slate) => { Ok(slate.fee_fields.fee()) @@ -824,10 +864,10 @@ impl Wallet { } /// Initialize a transaction to send amount. - fn send(&self, a: u64, r: Option) -> Result { + fn send(&self, a: u64, dest: Option) -> Result { let config = self.get_config(); let args = InitTxArgs { - payment_proof_recipient_address: r, + payment_proof_recipient_address: dest.clone(), src_acct_name: Some(config.account), amount: a, minimum_confirmations: config.min_confirmations, @@ -837,16 +877,23 @@ impl Wallet { }; let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - let slate = api.init_send_tx(None, args)?; - - // Lock outputs to for this transaction. - api.tx_lock_outputs(None, &slate)?; - - // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate)?; - - Ok(slate) + let mut api = Owner::new(instance, None); + let mut slate = None; + let keychain_mask = self.keychain_mask(); + controller::owner_single_use(None, keychain_mask.as_ref(), Some(&mut api), |api, m| { + let s = api.init_send_tx(m, args)?; + // Create Slatepack message response. + let _ = self.create_slatepack_message(&s, dest)?; + // Lock outputs to for this transaction. + api.tx_lock_outputs(m, &s)?; + slate = Some(s); + Ok(()) + })?; + if let Some(slate) = slate { + Ok(slate) + } else { + Err(Error::GenericError("slate was not created".to_string())) + } } /// Send slate to Tor address. @@ -874,17 +921,17 @@ impl Wallet { } // Parse response. - let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap(); - if res["error"] != json!(null) { - return Err(Error::GenericError("Response error".to_string())); + if let Ok(res) = serde_json::from_str::(&req_res.unwrap()) { + if res["error"] != json!(null) { + return Err(Error::GenericError("Response error".to_string())); + } + let slate_value = res["result"]["Ok"].clone(); + if let Ok(res) = &serde_json::to_string::(&slate_value) { + let res = Slate::deserialize_upgrade(res); + return res; + } } - let slate_value = res["result"]["Ok"].clone(); - let res = Slate::deserialize_upgrade(&serde_json::to_string(&slate_value).unwrap()); - - // Clear tx action. - self.on_tx_action(slate.id.to_string(), None); - - res + Err(Error::GenericError("Parse error".to_string())) } /// Check if request to send funds is creating. @@ -902,19 +949,14 @@ impl Wallet { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let api = Owner::new(instance, None); - let slate = api.issue_invoice_tx(None, args)?; + let slate = api.issue_invoice_tx(self.keychain_mask().as_ref(), args)?; // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate, None)?; Ok(slate) } - /// Check if request to receive funds is creating. - pub fn invoice_creating(&self) -> bool { - self.invoice_creating.load(Ordering::Relaxed) - } - /// Handle message from the invoice issuer to send founds, return response for funds receiver. fn pay(&self, slate: &Slate) -> Result { let config = self.get_config(); @@ -928,28 +970,33 @@ impl Wallet { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let api = Owner::new(instance, None); - let slate = api.process_invoice_tx(None, &slate, args)?; - api.tx_lock_outputs(None, &slate)?; + let slate = api.process_invoice_tx(self.keychain_mask().as_ref(), &slate, args)?; + api.tx_lock_outputs(self.keychain_mask().as_ref(), &slate)?; // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate, None)?; Ok(slate) } + /// Check if request to receive funds is creating. + pub fn invoice_creating(&self) -> bool { + self.invoice_creating.load(Ordering::Relaxed) + } + /// Create response to sender to receive funds. - fn receive(&self, slate: &Slate) -> Result { + fn receive(&self, slate: &Slate, dest: Option) -> Result { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let api = Owner::new(instance, None); let mut slate = slate.clone(); - controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { + controller::foreign_single_use(api.wallet_inst.clone(), self.keychain_mask(), |api| { slate = api.receive_tx(&slate, Some(self.get_config().account.as_str()), None)?; Ok(()) })?; // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate, dest)?; Ok(slate) } @@ -961,10 +1008,14 @@ impl Wallet { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let api = Owner::new(instance, None); - let slate = api.finalize_tx(None, slate)?; + let mut slate = slate.clone(); + controller::foreign_single_use(api.wallet_inst.clone(), self.keychain_mask(), |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; // Save Slatepack message to file. - let _ = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate, None)?; // Clear tx action. self.on_tx_action(slate.id.to_string(), None); @@ -978,8 +1029,11 @@ impl Wallet { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - api.post_tx(None, slate, self.can_use_dandelion())?; + let mut api = Owner::new(instance, None); + controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { + api.post_tx(m, &slate, self.can_use_dandelion())?; + Ok(()) + })?; // Clear tx action. self.on_tx_action(slate.id.to_string(), None); @@ -988,13 +1042,13 @@ impl Wallet { } /// Cancel transaction. - fn cancel(&self, tx: &WalletTransaction) -> Result<(), Error> { - let id = tx.data.tx_slate_id.unwrap().to_string(); + fn cancel(&self, tx: &TxLogEntry) -> Result<(), Error> { + let id = tx.tx_slate_id.unwrap().to_string(); self.on_tx_action(id.clone(), Some(WalletTransactionAction::Cancelling)); let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); - cancel_tx(instance, None, &None, Some(tx.data.id), None)?; + cancel_tx(instance, self.keychain_mask().as_ref(), &None, Some(tx.id), None)?; // Clear tx action. self.on_tx_action(id, None); @@ -1026,7 +1080,7 @@ impl Wallet { let r_res = self.task_result.read(); r_res.clone() }; - // Clear result for transaction task. + // Clear result for task. let mut w_res = self.task_result.write(); *w_res = None; res @@ -1059,12 +1113,12 @@ impl Wallet { Ok(tx_height) } - /// Get transaction from database. + /// Get transaction Slate from database. fn get_tx(&self, tx_id: u32) -> Option { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let api = Owner::new(instance, None); - if let Ok(s) = api.get_stored_tx(None, Some(tx_id), None) { + if let Ok(s) = api.get_stored_tx(self.keychain_mask().as_ref(), Some(tx_id), None) { return s; } None @@ -1162,12 +1216,107 @@ impl Wallet { self.files_moving.load(Ordering::Relaxed) } + /// Retrieve payment proof. + pub fn get_payment_proof(&self, tx_id: Option, slate_id: Option) + -> Result, Error> { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let key_mask = self.keychain_mask(); + let mut api = Owner::new(instance, None); + let mut proof = None; + controller::owner_single_use(None, key_mask.as_ref(), Some(&mut api), |api, m| { + let result = api.retrieve_payment_proof(m, false, tx_id, slate_id); + proof = match result { + Ok(p) => Some(p), + Err(e) => { + error!("retrieve_payment_proof error: {}", e); + None + } + }; + Ok(()) + })?; + Ok(proof) + } + + /// Verify payment proof. + fn verify_payment_proof(&self, proof: &PaymentProof) -> Result<(u32, bool, bool), Error> { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let keychain_mask = self.keychain_mask(); + let verify_res = verify_payment_proof(instance.clone(), keychain_mask.as_ref(), proof); + let res = match verify_res { + Ok((send, rec)) => { + // Update proof at local database for valid proof. + if send || rec { + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider()?; + let w = lc.wallet_inst()?; + // Find wallet transaction to update or create. + let txs = w.tx_log_iter().filter(|entry| { + if let Some(excess) = entry.kernel_excess { + return excess == proof.excess; + } + false + }).collect::>(); + if let Some(tx) = txs.get(0) { + let mut tx = tx.clone(); + let mut batch = w.batch(keychain_mask.as_ref())?; + let parent_key = &tx.parent_key_id; + tx.payment_proof = Some(StoredProofInfo { + receiver_address: proof.recipient_address.pub_key, + receiver_signature: Some(proof.recipient_sig), + sender_address_path: 0, + sender_address: proof.sender_address.pub_key, + sender_signature: Some(proof.sender_sig), + }); + batch.save_tx_log_entry(tx.clone(), &parent_key)?; + batch.commit()?; + Ok((tx.id, send, rec)) + } else { + let parent_key = w.parent_key_id(); + let mut batch = w.batch(keychain_mask.as_ref())?; + let log_id = batch.next_tx_log_id(&parent_key)?; + let log_type = TxLogEntryType::TxSent; + let mut tx = TxLogEntry::new(parent_key.clone(), log_type, log_id); + tx.amount_debited = proof.amount; + tx.kernel_excess = Some(proof.excess); + tx.tx_type = TxLogEntryType::TxSent; + tx.confirmed = true; + tx.payment_proof = Some(StoredProofInfo { + receiver_address: proof.recipient_address.pub_key, + receiver_signature: Some(proof.recipient_sig), + sender_address_path: 0, + sender_address: proof.sender_address.pub_key, + sender_signature: Some(proof.sender_sig), + }); + batch.save_tx_log_entry(tx.clone(), &parent_key)?; + batch.commit()?; + Ok((tx.id, send, rec)) + } + } else { + Ok((0, send, rec)) + } + } + Err(e) => Err(e) + }; + // Sync wallet data on success. + if res.is_ok() { + sync_wallet_data(self, false); + } + res + } + + /// Check if payment proof is verifying. + pub fn payment_proof_verifying(&self) -> bool { + self.proof_verifying.load(Ordering::Relaxed) + } + /// Get recovery phrase. pub fn get_recovery(&self, password: String) -> Result { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider().unwrap(); + let lc = wallet_lock.lc_provider()?; lc.get_mnemonic(None, ZeroingString::from(password)) } @@ -1376,16 +1525,22 @@ async fn handle_task(w: &Wallet, t: WalletTask) { w.on_task_result(Some(id), &t); } Err(e) => { + error!("send tor post error: {:?}", e); w.on_tx_error(id, Some(e)); } } } Err(e) => { + if let Some(tx) = w.tx_by_id(s.id) { + let _ = w.cancel(&tx); + } + error!("send tor finalize error: {:?}", e); w.on_tx_error(id, Some(e)); } } } Err(e) => { + error!("send tor error: {:?}", e); w.on_tx_error(id.clone(), Some(e)); w.on_task_result(Some(id), &t); } @@ -1401,7 +1556,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) { let msg = m.clone(); thread::spawn(move || { load.store(true, Ordering::Relaxed); - if let Ok(s) = w.parse_slatepack(&msg) { + if let Ok((s, dest)) = w.parse_slatepack(&msg) { let id = s.id.to_string(); // Check if message already exists. let exists = { @@ -1432,7 +1587,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) { w.on_task_result(Some(id), &t); } } else { - if let Ok(_) = w.receive(&s) { + if let Ok(_) = w.receive(&s, dest) { sync_wallet_data(&w, false); w.on_task_result(Some(id), &t); } @@ -1446,11 +1601,16 @@ async fn handle_task(w: &Wallet, t: WalletTask) { sync_wallet_data(&w, false); } Err(e) => { + error!("message tx post error: {:?}", e); w.on_tx_error(id, Some(e)); } } } Err(e) => { + if let Some(tx) = w.tx_by_id(s.id) { + let _ = w.cancel(&tx); + } + error!("message tx finalize error: {:?}", e); w.on_tx_error(id, Some(e)); } } @@ -1486,9 +1646,10 @@ async fn handle_task(w: &Wallet, t: WalletTask) { w.send_creating.store(true, Ordering::Relaxed); if let Ok(s) = w.send(*a, r.clone()) { sync_wallet_data(&w, false); - if let Some(r) = r { + if r.is_some() && Tor::is_service_running(&w.identifier()) { w.send_creating.store(false, Ordering::Relaxed); - send_tor(&s, r).await; + let addr = r.as_ref().unwrap(); + send_tor(&s, addr).await; return; } else { w.on_task_result(Some(s.id.to_string()), &t); @@ -1509,11 +1670,8 @@ async fn handle_task(w: &Wallet, t: WalletTask) { } w.invoice_creating.store(false, Ordering::Relaxed); }, - WalletTask::Finalize(s, id) => { - let slate = match s { - None => &w.get_tx(*id).unwrap(), - Some(s) => s - }; + WalletTask::Finalize(id) => { + let slate = &w.get_tx(*id).unwrap(); w.on_tx_error(slate.id.to_string(), None); match w.finalize(slate) { Ok(s) => { @@ -1522,27 +1680,27 @@ async fn handle_task(w: &Wallet, t: WalletTask) { sync_wallet_data(&w, false); } Err(e) => { + error!("tx finalize post error: {:?}", e); w.on_tx_error(slate.id.to_string(), Some(e)); } } } Err(e) => { + if let Some(tx) = w.tx_by_id(slate.id) { + let _ = w.cancel(&tx); + } + error!("tx finalize error: {:?}", e); w.on_tx_error(slate.id.to_string(), Some(e)); } } } - WalletTask::Post(s, id) => { - w.on_tx_action(id.to_string(), Some(WalletTransactionAction::Posting)); - - let slate = match s { - None => &w.get_tx(*id).unwrap(), - Some(s) => s - }; + WalletTask::Post(id) => { + let slate = &w.get_tx(*id).unwrap(); w.on_tx_error(slate.id.to_string(), None); // Cleanup broadcasting tx height. let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path()); - tx_height_store.delete_broadcasting_height(*id); + tx_height_store.delete_broadcasting_height(&slate.id.to_string()); let has_data = { let r_data = w.data.read(); @@ -1563,6 +1721,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) { sync_wallet_data(&w, false); } Err(e) => { + error!("tx post error: {:?}", e); w.on_tx_error(slate.id.to_string(), Some(e)); } } @@ -1574,11 +1733,18 @@ async fn handle_task(w: &Wallet, t: WalletTask) { sync_wallet_data(&w, false); } Err(e) => { - let id = tx.data.tx_slate_id.unwrap().to_string(); + error!("tx cancel error: {:?}", e); + let id = tx.tx_slate_id.unwrap().to_string(); w.on_tx_error(id, Some(e)); } } } + WalletTask::VerifyProof(p, _) => { + w.proof_verifying.store(true, Ordering::Relaxed); + let res = w.verify_payment_proof(p); + w.proof_verifying.store(false, Ordering::Relaxed); + w.on_task_result(None, &WalletTask::VerifyProof(p.clone(), Some(res))); + } }; } @@ -1612,7 +1778,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { let instance = r_inst.clone().unwrap(); if let Ok((_, info)) = retrieve_summary_info( instance.clone(), - None, + wallet.keychain_mask().as_ref(), &Some(info_tx), from_node, config.min_confirmations @@ -1679,16 +1845,18 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { } /// Update wallet transactions. -fn update_txs(wallet: &Wallet, instance: WalletInstance, info: WalletInfo) - -> Result<(), Error> { +fn update_txs(wallet: &Wallet, inst: WalletInstance, info: WalletInfo) + -> Result<(), Error> { // Retrieve txs from local database. let txs_args = RetrieveTxQueryArgs { exclude_cancelled: Some(false), sort_field: Some(RetrieveTxQuerySortField::CreationTimestamp), sort_order: Some(RetrieveTxQuerySortOrder::Desc), + min_amount: Some(1), ..Default::default() }; - let txs = retrieve_txs(instance, None, &None, false, None, None, Some(txs_args.clone()))?; + let mask = wallet.keychain_mask(); + let txs = retrieve_txs(inst, mask.as_ref(), &None, false, None, None, Some(txs_args.clone()))?; // Exit if wallet was closed. if !wallet.is_open() || wallet.is_closing() { @@ -1699,7 +1867,8 @@ fn update_txs(wallet: &Wallet, instance: WalletInstance, info: WalletInfo) let account_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| { match wallet.get_parent_key_id() { Ok(key) => { - tx.parent_key_id == key && tx.tx_slate_id.is_some() + tx.parent_key_id == key && (tx.tx_slate_id.is_some() || + (tx.tx_slate_id.is_none() && tx.payment_proof.is_some())) } Err(_) => { true @@ -1716,6 +1885,7 @@ fn update_txs(wallet: &Wallet, instance: WalletInstance, info: WalletInfo) let mut broadcasting_height: Option = None; let mut action: Option = None; let mut action_error: Option = None; + let mut proof: Option = None; for t in &data_txs { if t.data.id == tx.id { action = t.action.clone(); @@ -1726,6 +1896,7 @@ fn update_txs(wallet: &Wallet, instance: WalletInstance, info: WalletInfo) } } let mut new = WalletTransaction::new(tx.clone(), + proof.clone(), wallet, height, broadcasting_height, @@ -1738,28 +1909,43 @@ fn update_txs(wallet: &Wallet, instance: WalletInstance, info: WalletInfo) new.update_slate_state(wallet); } - // Setup initial tx heights. - if height.is_none() && tx.confirmed { - height = if let Some(height) = tx_height_store.read_tx_height(tx.id) { - Some(height) - } else { - tx_height_store.delete_broadcasting_height(tx.id); - let h = wallet.tx_height(&new)?; - if let Some(h) = h { - tx_height_store.write_tx_height(tx.id, h); - } - h - }; - new.height = height; - } else if broadcasting_height.is_none() && new.broadcasting() { - broadcasting_height = if let Some(h) = tx_height_store.read_broadcasting_height(tx.id) { - Some(h) - } else { - let h = data.info.last_confirmed_height; - tx_height_store.write_broadcasting_height(tx.id, h); - Some(h) - }; - new.broadcasting_height = Some(broadcasting_height.unwrap_or(0)); + // Payment proof setup. + if proof.is_none() && tx.payment_proof.is_some() && + tx.payment_proof.as_ref().unwrap().receiver_signature.is_some() && + tx.payment_proof.as_ref().unwrap().sender_signature.is_some() && + tx.kernel_excess.is_some() { + if let Ok(p) = wallet.get_payment_proof(Some(tx.id), tx.tx_slate_id) { + proof = p.clone(); + new.proof = proof; + } + } + + // Initial tx heights setup. + if let Some(slate_id) = tx.tx_slate_id { + let id = slate_id.to_string(); + if height.is_none() && tx.confirmed { + height = if let Some(height) = tx_height_store.read_tx_height(&id) { + Some(height) + } else { + tx_height_store.delete_broadcasting_height(&id); + let h = wallet.tx_height(&new)?; + if let Some(h) = h { + tx_height_store.write_tx_height(&id, h); + } + h + }; + new.height = height; + } else if broadcasting_height.is_none() && new.broadcasting() { + let br_height = tx_height_store.read_broadcasting_height(&id); + broadcasting_height = if br_height.is_none() || br_height.unwrap() == 0 { + let h = data.info.last_confirmed_height; + tx_height_store.write_broadcasting_height(&id, h); + Some(h) + } else { + Some(br_height.unwrap()) + }; + new.broadcasting_height = broadcasting_height; + } } new_txs.push(new); @@ -1797,8 +1983,9 @@ fn start_api_server(wallet: &Wallet) -> Result<(ApiServer, u16), Error> { // Start Foreign API server thread. let r_inst = wallet.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); + let keychain_mask = wallet.keychain_mask(); let api_handler_v2 = ForeignAPIHandlerV2::new(instance, - Arc::new(Mutex::new(None)), + Arc::new(Mutex::new(keychain_mask)), false, Mutex::new(None)); let mut router = Router::new(); @@ -1833,7 +2020,8 @@ fn update_accounts(wallet: &Wallet, height: u64, spendable: Option) { let r_inst = wallet.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut api = Owner::new(instance, None); - let _ = controller::owner_single_use(None, None, Some(&mut api), |api, m| { + let key_mask = wallet.keychain_mask(); + let _ = controller::owner_single_use(None, key_mask.as_ref(), Some(&mut api), |api, m| { let mut accounts = vec![]; for a in api.accounts(m)? { api.set_active_account(m, a.label.as_str())?; @@ -1886,7 +2074,7 @@ fn repair_wallet(wallet: &Wallet) { let r_inst = wallet.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let api = Owner::new(instance, Some(info_tx)); - match api.scan(None, Some(1), false) { + match api.scan(wallet.keychain_mask().as_ref(), Some(1), false) { Ok(()) => { // Set sync error if scanning was not complete and wallet is open. if wallet.is_open() && wallet.repair_progress.load(Ordering::Relaxed) != 100 {