diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index c244184..a07090a 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -10,6 +10,8 @@ pub enum GrinWalletInterfaceError { WalletLibWallet(#[from] grin_wallet_libwallet::Error), #[error("Owner API not Instantiated")] OwnerAPINotInstantiated, + #[error("Foreign API not Instantiated")] + ForeignAPINotInstantiated, } #[derive(thiserror::Error, Debug)] diff --git a/crates/core/src/wallet/mod.rs b/crates/core/src/wallet/mod.rs index 82842c0..44827cf 100644 --- a/crates/core/src/wallet/mod.rs +++ b/crates/core/src/wallet/mod.rs @@ -1,7 +1,7 @@ /// Placeholder for all wallet calls /// Should eventually feature async calls that work via local wallet or remote owner API use grin_wallet::cmd::wallet_args::inst_wallet; -use grin_wallet_api::Owner; +use grin_wallet_api::{Owner, Foreign}; use grin_wallet_config::{self, GlobalWalletConfig}; use grin_wallet_controller::command::InitArgs; use grin_wallet_impls::DefaultLCProvider; @@ -21,8 +21,8 @@ use dirs; pub use global::ChainTypes; pub use grin_wallet_impls::HTTPNodeClient; pub use grin_wallet_libwallet::{ - InitTxArgs, RetrieveTxQueryArgs, RetrieveTxQuerySortOrder, Slate, SlatepackAddress, - StatusMessage, TxLogEntry, TxLogEntryType, WalletInfo, + InitTxArgs, RetrieveTxQueryArgs, RetrieveTxQuerySortOrder, Slate, SlatepackAddress, Slatepack, + StatusMessage, SlateState, TxLogEntry, TxLogEntryType, WalletInfo, }; use crate::error::GrinWalletInterfaceError; @@ -92,6 +92,8 @@ where pub config: Option, // owner api will hold instantiated/opened wallets pub owner_api: Option>, + // Also need reference to foreign API + pub foreign_api: Option>, // Simple flag to check whether wallet has been opened wallet_is_open: bool, // Hold on to check node foreign API secret for now @@ -112,6 +114,7 @@ where chain_type: None, config: None, owner_api: None, + foreign_api: None, wallet_is_open: false, check_node_foreign_api_secret_path: None, node_client, @@ -208,7 +211,7 @@ where Ok(wallet_inst) } - fn inst_owner_api( + fn inst_apis( wallet_interface: Arc>>, chain_type: global::ChainTypes, top_level_directory: PathBuf, @@ -220,6 +223,7 @@ where )?; let mut w = wallet_interface.write().unwrap(); w.owner_api = Some(Owner::new(wallet_inst.clone(), None)); + w.foreign_api = Some(Foreign::new(wallet_inst.clone(), None, None, false)); global::set_local_chain_type(chain_type); Ok(()) @@ -233,7 +237,7 @@ where chain_type: global::ChainTypes, recovery_phrase: Option, ) -> Result<(String, String, String, global::ChainTypes), GrinWalletInterfaceError> { - WalletInterface::inst_owner_api( + WalletInterface::inst_apis( wallet_interface.clone(), chain_type, top_level_directory.clone(), @@ -305,7 +309,7 @@ where top_level_directory: PathBuf, chain_type: global::ChainTypes, ) -> Result<(), GrinWalletInterfaceError> { - WalletInterface::inst_owner_api( + WalletInterface::inst_apis( wallet_interface.clone(), chain_type, top_level_directory.clone(), @@ -369,6 +373,21 @@ where Ok(api.create_slatepack_message(None, &unenc_slate, Some(0), recipients)?) } + /// Attempt to decode and decrypt a given slatepack + pub fn decrypt_slatepack( + wallet_interface: Arc>>, + slatepack: String, + ) -> Result<(Slatepack, Slate), GrinWalletInterfaceError> { + let w = wallet_interface.read().unwrap(); + if let Some(o) = &w.owner_api { + let sp= o.decode_slatepack_message(None, slatepack.clone(), vec![0])?; + let slate = o.slate_from_slatepack_message(None, slatepack, vec![0])?; + return Ok((sp, slate)) + } else { + return Err(GrinWalletInterfaceError::OwnerAPINotInstantiated); + } + } + pub async fn get_wallet_info( wallet_interface: Arc>>, ) -> Result<(bool, WalletInfo), GrinWalletInterfaceError> { @@ -433,6 +452,41 @@ where } } + pub async fn receive_tx_from_s1( + wallet_interface: Arc>>, + slate: Slate, + dest_slatepack_address: String, + ) -> Result, GrinWalletInterfaceError> { + let w = wallet_interface.write().unwrap(); + let ret_slate; + if let Some(f) = &w.foreign_api { + ret_slate = f.receive_tx(&slate, None, None)?; + } else { + return Err(GrinWalletInterfaceError::ForeignAPINotInstantiated); + } + if let Some(o) = &w.owner_api { + let encrypted = WalletInterface::encrypt_slatepack(o, &dest_slatepack_address, &ret_slate)?; + return Ok(Some(encrypted)) + } else { + return Err(GrinWalletInterfaceError::OwnerAPINotInstantiated); + } + } + + pub async fn finalize_from_s2( + wallet_interface: Arc>>, + slate: Slate, + send_to_chain: bool, + ) -> Result, GrinWalletInterfaceError> { + let w = wallet_interface.write().unwrap(); + if let Some(o) = &w.owner_api { + let ret_slate = o.finalize_tx(None, &slate)?; + o.post_tx(None, &ret_slate, true)?; + return Ok(None) + } else { + return Err(GrinWalletInterfaceError::ForeignAPINotInstantiated); + } + } + pub async fn cancel_tx( wallet_interface: Arc>>, id: u32, diff --git a/locale/de.json b/locale/de.json index 6585c95..41c6ab2 100644 --- a/locale/de.json +++ b/locale/de.json @@ -219,7 +219,7 @@ "wallet-create-tx": "Send", "wallet-apply-tx": "Receive", "wallet-home": "Wallet - Set Name Here TBD", - "apply-tx": "Progress Transaction", + "apply-tx": "Apply Transaction", "create-tx": "Start Transaction", "slatepack-address-name": "This Wallet's Slatepack Address", "address-instruction": "Provide this address to others to allow them to send you funds", @@ -235,6 +235,13 @@ "tx-list": "Transactions", "tx-outstanding": "Outstanding", "tx-recent": "Most Recent", - "tx-details": "Details" - + "tx-details": "Details", + "tx-slatepack-paste-transaction-here": "Paste Transaction from Clipboard", + "tx-slatepack-read-result-default": "Ensure the clipboard contains the encrypted slatepack contents and press 'Continue'", + "tx-slatepack-read-failure": "Clipboard does not contain a slatepack that can be decrypted by this wallet", + "tx-continue": "Continue", + "apply-tx-confirm": "Confirm Transaction Details", + "tx-sender-name": "Sender", + "apply-tx-amount": "Incoming amount", + "tx-state": "Transaction Stage (this will be presented better)" } \ No newline at end of file diff --git a/locale/en.json b/locale/en.json index 4f7e556..cd6d10c 100644 --- a/locale/en.json +++ b/locale/en.json @@ -229,7 +229,7 @@ "wallet-create-tx": "Send", "wallet-apply-tx": "Receive", "wallet-home": "Wallet - Set Name Here TBD", - "apply-tx": "Progress Transaction", + "apply-tx": "Apply Transaction", "create-tx": "Start Transaction", "slatepack-address-name": "This Wallet's Slatepack Address", "address-instruction": "Provide this address to others to allow them to send you funds", @@ -244,5 +244,13 @@ "tx-recent": "Most Recent", "tx-details": "Details", "tx-create-success-title": "Encrypted Transaction", - "tx-create-success-desc": "Copy/Paste this encrypted transaction to the recipient via a channel of your choosing" + "tx-create-success-desc": "Copy/Paste this encrypted transaction to the recipient via a channel of your choosing", + "tx-slatepack-paste-transaction-here": "Paste Transaction from Clipboard", + "tx-slatepack-read-result-default": "Ensure the clipboard contains the encrypted slatepack contents and press 'Continue'", + "tx-slatepack-read-failure": "Clipboard does not contain a slatepack that can be decrypted by this wallet", + "tx-continue": "Continue", + "apply-tx-confirm": "Confirm Transaction Details", + "tx-sender-name": "Sender", + "apply-tx-amount": "Incoming amount", + "tx-state": "Transaction Stage (this will be presented better)" } \ No newline at end of file diff --git a/src/gui/element/wallet/operation/apply_tx.rs b/src/gui/element/wallet/operation/apply_tx.rs index 4ec6098..d3d4bdf 100644 --- a/src/gui/element/wallet/operation/apply_tx.rs +++ b/src/gui/element/wallet/operation/apply_tx.rs @@ -13,18 +13,22 @@ use std::path::PathBuf; use super::tx_list::{HeaderState, TxList}; use { - super::super::super::{DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, SMALLER_FONT_SIZE, DEFAULT_PADDING}, + super::super::super::{ + BUTTON_HEIGHT, BUTTON_WIDTH, DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING, + SMALLER_FONT_SIZE, + }, crate::gui::{GrinGui, Interaction, Message}, crate::localization::localized_string, crate::Result, anyhow::Context, + grin_gui_core::theme::{ + Button, Column, Container, Element, Header, PickList, Row, Scrollable, TableRow, Text, + TextInput, + }, grin_gui_core::wallet::{StatusMessage, WalletInfo, WalletInterface}, grin_gui_core::{node::amount_to_hr_string, theme::ColorPalette}, - grin_gui_core::theme::{Container, Button, Element, Column, PickList, Row, Scrollable, Text, TextInput, Header, TableRow}, + iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space}, iced::{alignment, Alignment, Command, Length}, - iced::widget::{ - button, pick_list, scrollable, text_input,Checkbox, Space, - }, serde::{Deserialize, Serialize}, std::sync::{Arc, RwLock}, }; @@ -34,6 +38,8 @@ pub struct StateContainer { // pub copy_address_button_state: button::State, // pub address_state: text_input::State, pub address_value: String, + // Slatepack read result + pub slatepack_read_result: String, } impl Default for StateContainer { @@ -43,6 +49,7 @@ impl Default for StateContainer { // copy_address_button_state: Default::default(), // address_state: Default::default(), address_value: Default::default(), + slatepack_read_result: localized_string("tx-slatepack-read-result-default"), } } } @@ -54,63 +61,73 @@ pub enum Action {} pub enum LocalViewInteraction { Back, Address(String), + ApplyTransaction(String), + ReadFromClipboardSuccess(String), + ReadFromClipboardFailure, } pub fn handle_message<'a>( grin_gui: &mut GrinGui, message: LocalViewInteraction, ) -> Result> { - /*let state = &mut grin_gui - .wallet_state - .operation_state - .home_state - .action_menu_state;*/ + let state = &mut grin_gui.wallet_state.operation_state.apply_tx_state; match message { LocalViewInteraction::Back => { log::debug!("Interaction::WalletOperationApplyTxViewInteraction(Back)"); grin_gui.wallet_state.operation_state.mode = crate::gui::element::wallet::operation::Mode::Home; } - LocalViewInteraction::Address(_) => { - + LocalViewInteraction::ReadFromClipboardSuccess(value) => { + debug!("Read from clipboard: {}", value); + let w = grin_gui.wallet_interface.clone(); + let decode_res = WalletInterface::decrypt_slatepack(w, value); + match decode_res { + Err(e) => { + state.slatepack_read_result = localized_string("tx-slatepack-read-failure") + } + Ok(s) => { + debug!("{}", s.0); + grin_gui.wallet_state.operation_state.apply_tx_confirm_state.slatepack_parsed = Some(s); + grin_gui.wallet_state.operation_state.mode = + crate::gui::element::wallet::operation::Mode::ApplyTxConfirm; + } + } + } + LocalViewInteraction::ReadFromClipboardFailure => { + error!("Failed to read from clipboard"); } + LocalViewInteraction::Address(_) => {} + LocalViewInteraction::ApplyTransaction(_) => {} } Ok(Command::none()) } -pub fn data_container<'a>( - config: &'a Config, - state: &'a StateContainer, -) -> Container<'a, Message> { +pub fn data_container<'a>(config: &'a Config, state: &'a StateContainer) -> Container<'a, Message> { + let unit_spacing = 15; + // Title row let title = Text::new(localized_string("apply-tx")) .size(DEFAULT_HEADER_FONT_SIZE) .horizontal_alignment(alignment::Horizontal::Center); - let title_container = - Container::new(title).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + let title_container = Container::new(title) + .style(grin_gui_core::theme::ContainerStyle::BrightBackground) + .padding(iced::Padding::from([ + 2, // top + 0, // right + 2, // bottom + 5, // left + ])); - let back_button_label_container = - Container::new(Text::new(localized_string("back")).size(DEFAULT_FONT_SIZE)) - .height(Length::Units(20)) - .align_y(alignment::Vertical::Bottom) - .align_x(alignment::Horizontal::Center); + // push more items on to header here: e.g. other buttons, things that belong on the header + let header_row = Row::new().push(title_container); - let back_button: Element = - Button::new( back_button_label_container) - .style(grin_gui_core::theme::ButtonStyle::NormalText) - .on_press(Interaction::WalletOperationApplyTxViewInteraction( - LocalViewInteraction::Back, - )) - .into(); - - let title_row = Row::new() - .push(title_container) - .push(back_button.map(Message::Interaction)) - //.push(Space::new(Length::Fill, Length::Units(0))) - .align_items(Alignment::Center) - .padding(6) - .spacing(20); + let header_container = Container::new(header_row).padding(iced::Padding::from([ + 0, // top + 0, // right + DEFAULT_PADDING, // bottom + 0, // left + ])); let address_name = Text::new(localized_string("slatepack-address-name")) .size(DEFAULT_FONT_SIZE) @@ -119,7 +136,7 @@ pub fn data_container<'a>( let address_name_container = Container::new(address_name).style(grin_gui_core::theme::ContainerStyle::NormalBackground); - let address_input = TextInput::new( "", &state.address_value, |s| { + let address_input = TextInput::new("", &state.address_value, |s| { Interaction::WalletOperationApplyTxViewInteraction(LocalViewInteraction::Address(s)) }) .size(DEFAULT_FONT_SIZE) @@ -136,16 +153,14 @@ pub fn data_container<'a>( .horizontal_alignment(alignment::Horizontal::Center), ) .style(grin_gui_core::theme::ButtonStyle::NormalText) - .on_press(Interaction::WriteToClipboard( - state.address_value.clone(), - )); + .on_press(Interaction::WriteToClipboard(state.address_value.clone())); let copy_address_button: Element = copy_address_button.into(); let address_row = Row::new() - .push(address_input) - .push(copy_address_button) - .spacing(DEFAULT_PADDING); + .push(address_input) + .push(copy_address_button) + .spacing(DEFAULT_PADDING); let address_row: Element = address_row.into(); @@ -153,20 +168,138 @@ pub fn data_container<'a>( .size(SMALLER_FONT_SIZE) .horizontal_alignment(alignment::Horizontal::Left); - let address_instruction_container = - Container::new(address_instruction_container).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + let address_instruction_container = Container::new(address_instruction_container) + .style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let slatepack_paste_name = Text::new(localized_string("tx-slatepack-paste-transaction-here")) + .size(DEFAULT_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Left); + + let slatepack_paste_name_container = Container::new(slatepack_paste_name) + .style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let slatepack_text_area = Text::new(state.slatepack_read_result.clone()) + .size(DEFAULT_FONT_SIZE) + .width(Length::Units(400)); + + /*let paste_slatepack_button = Button::new( + // &mut state.copy_address_button_state, + Text::new(localized_string("tx-slatepack-paste-from-clipboard")) + .size(SMALLER_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Center), + ) + .style(grin_gui_core::theme::ButtonStyle::NormalText) + .on_press(Interaction::ReadSlatepackFromClipboard); + + let paste_slatepack_button: Element = paste_slatepack_button.into();*/ + + let paste_slatepack_row = Row::new() + .push(slatepack_text_area) + //.push(paste_slatepack_button.map(Message::Interaction)) + .spacing(DEFAULT_PADDING); + + let slatepack_area = Column::new() + .push(slatepack_paste_name_container) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) + .push(paste_slatepack_row); + + let slatepack_area_container = Container::new(slatepack_area); + + let button_height = Length::Units(BUTTON_HEIGHT); + let button_width = Length::Units(BUTTON_WIDTH); + let submit_button_label_container = + Container::new(Text::new(localized_string("tx-continue")).size(DEFAULT_FONT_SIZE)) + .width(button_width) + .height(button_height) + .center_x() + .center_y() + .align_x(alignment::Horizontal::Center); + + let mut submit_button = Button::new(submit_button_label_container) + .style(grin_gui_core::theme::ButtonStyle::Primary) + .on_press(Interaction::ReadSlatepackFromClipboard); + /*let submit_button = submit_button.on_press(Interaction::WalletOperationApplyTxViewInteraction( + LocalViewInteraction::ApplyTransaction("_".into()), + ));*/ + + let submit_button: Element = submit_button.into(); + + let cancel_button_label_container = + Container::new(Text::new(localized_string("cancel")).size(DEFAULT_FONT_SIZE)) + .width(button_width) + .height(button_height) + .center_x() + .center_y() + .align_x(alignment::Horizontal::Center); + + let cancel_button: Element = Button::new(cancel_button_label_container) + .style(grin_gui_core::theme::ButtonStyle::Primary) + .on_press(Interaction::WalletOperationApplyTxViewInteraction( + LocalViewInteraction::Back, + )) + .into(); + + let submit_container = Container::new(submit_button.map(Message::Interaction)).padding(1); + let submit_container = Container::new(submit_container) + .style(grin_gui_core::theme::ContainerStyle::Segmented) + .padding(1); + + let cancel_container = Container::new(cancel_button.map(Message::Interaction)).padding(1); + let cancel_container = Container::new(cancel_container) + .style(grin_gui_core::theme::ContainerStyle::Segmented) + .padding(1); + + let button_row = Row::new() + .push(submit_container) + .push(Space::new(Length::Units(unit_spacing), Length::Units(0))) + .push(cancel_container); let column = Column::new() - .push(title_row) .push(address_name_container) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) .push(address_instruction_container) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) .push(address_row.map(Message::Interaction)) - .spacing(DEFAULT_PADDING) - .align_items(Alignment::Start); + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) + .push(slatepack_area_container) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) + .push(button_row) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) + .push(Space::new( + Length::Units(0), + Length::Units(unit_spacing + 10), + )); - Container::new(column) - .center_y() - .center_x() + let form_container = Container::new(column) .width(Length::Fill) + .padding(iced::Padding::from([ + 0, // top + 0, // right + 0, // bottom + 5, // left + ])); + + // form container should be scrollable in tiny windows + let scrollable = Scrollable::new(form_container) + .height(Length::Fill) + .style(grin_gui_core::theme::ScrollableStyle::Primary); + + let content = Container::new(scrollable) + .width(Length::Fill) + .height(Length::Shrink) + .style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let wrapper_column = Column::new() + .height(Length::Fill) + .push(header_container) + .push(content); + + // Returns the final container. + Container::new(wrapper_column).padding(iced::Padding::from([ + DEFAULT_PADDING, // top + DEFAULT_PADDING, // right + DEFAULT_PADDING, // bottom + DEFAULT_PADDING, // left + ])) } diff --git a/src/gui/element/wallet/operation/apply_tx_confirm.rs b/src/gui/element/wallet/operation/apply_tx_confirm.rs new file mode 100644 index 0000000..dfccf5d --- /dev/null +++ b/src/gui/element/wallet/operation/apply_tx_confirm.rs @@ -0,0 +1,383 @@ +use super::tx_list::{self, ExpandType}; +use crate::log_error; +use async_std::prelude::FutureExt; +use grin_gui_core::{ + config::Config, + wallet::{Slate, SlateState, Slatepack, TxLogEntry, TxLogEntryType}, +}; +use grin_gui_widgets::widget::header; +use iced_aw::Card; +use iced_native::Widget; +use std::path::PathBuf; + +use super::tx_list::{HeaderState, TxList}; + +use { + super::super::super::{ + BUTTON_HEIGHT, BUTTON_WIDTH, DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING, + SMALLER_FONT_SIZE, + }, + crate::gui::{GrinGui, Interaction, Message}, + crate::localization::localized_string, + crate::Result, + anyhow::Context, + grin_gui_core::theme::{ + Button, Column, Container, Element, Header, PickList, Row, Scrollable, TableRow, Text, + TextInput, + }, + grin_gui_core::wallet::{StatusMessage, WalletInfo, WalletInterface}, + grin_gui_core::{node::amount_to_hr_string, theme::ColorPalette}, + iced::widget::{button, pick_list, scrollable, text_input, Checkbox, Space}, + iced::{alignment, Alignment, Command, Length}, + serde::{Deserialize, Serialize}, + std::sync::{Arc, RwLock}, +}; + +pub struct StateContainer { + // pub back_button_state: button::State, + // pub copy_address_button_state: button::State, + // pub address_state: text_input::State, + pub address_value: String, + // Slatepack read result + pub slatepack_read_result: String, + // Actual read slatepack + pub slatepack_parsed: Option<(Slatepack, Slate)>, +} + +impl Default for StateContainer { + fn default() -> Self { + Self { + // back_button_state: Default::default(), + // copy_address_button_state: Default::default(), + // address_state: Default::default(), + address_value: Default::default(), + slatepack_read_result: localized_string("tx-slatepack-read-result-default"), + slatepack_parsed: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Action {} + +#[derive(Debug, Clone)] +pub enum LocalViewInteraction { + Back, + Accept, + TxAcceptSuccess(Option), + TxAcceptFailure(Arc>>), +} + +pub fn handle_message<'a>( + grin_gui: &mut GrinGui, + message: LocalViewInteraction, +) -> Result> { + let state = &mut grin_gui.wallet_state.operation_state.apply_tx_confirm_state; + match message { + LocalViewInteraction::Back => { + log::debug!("Interaction::WalletOperationApplyTxConfirmViewInteraction(Cancel)"); + grin_gui.wallet_state.operation_state.mode = + crate::gui::element::wallet::operation::Mode::Home; + } + LocalViewInteraction::Accept => { + grin_gui.error.take(); + + log::debug!("Interaction::WalletOperationApplyTxConfirmViewInteraction(Accept)"); + if state.slatepack_parsed.is_none() { + log::debug!("you should never see this - dev make sure slatepack is not None"); + return Ok(Command::none()); + } + + let (slatepack, slate) = state.slatepack_parsed.as_ref().unwrap(); + + let sp_sending_address = match &slatepack.sender { + None => "None".to_string(), + Some(s) => s.to_string(), + }; + + let w = grin_gui.wallet_interface.clone(); + let out_slate = slate.clone(); + match slate.state { + SlateState::Standard1 => { + let fut = move || { + WalletInterface::receive_tx_from_s1(w, out_slate, sp_sending_address) + }; + + return Ok(Command::perform(fut(), |r| { + match r.context("Failed to Progress Transaction") { + Ok(ret) => Message::Interaction( + Interaction::WalletOperationApplyTxConfirmViewInteraction( + LocalViewInteraction::TxAcceptSuccess(ret), + ), + ), + Err(e) => Message::Interaction( + Interaction::WalletOperationApplyTxConfirmViewInteraction( + LocalViewInteraction::TxAcceptFailure(Arc::new(RwLock::new( + Some(e), + ))), + ), + ), + } + })); + } + SlateState::Standard2 => { + let fut = move || WalletInterface::finalize_from_s2(w, out_slate, true); + + return Ok(Command::perform(fut(), |r| { + match r.context("Failed to Progress Transaction") { + Ok(ret) => Message::Interaction( + Interaction::WalletOperationApplyTxConfirmViewInteraction( + LocalViewInteraction::TxAcceptSuccess(ret), + ), + ), + Err(e) => Message::Interaction( + Interaction::WalletOperationApplyTxConfirmViewInteraction( + LocalViewInteraction::TxAcceptFailure(Arc::new(RwLock::new( + Some(e), + ))), + ), + ), + } + })); + } + _ => { + log::error!("Slate state not yet supported"); + return Ok(Command::none()); + } + } + } + LocalViewInteraction::TxAcceptSuccess(slate) => { + log::debug!("{:?}", slate); + grin_gui + .wallet_state + .operation_state + .apply_tx_success_state + .encrypted_slate = slate; + grin_gui.wallet_state.operation_state.mode = + crate::gui::element::wallet::operation::Mode::ApplyTxSuccess; + } + LocalViewInteraction::TxAcceptFailure(err) => { + grin_gui.error = err.write().unwrap().take(); + if let Some(e) = grin_gui.error.as_ref() { + log_error(e); + } + } + } + Ok(Command::none()) +} + +pub fn data_container<'a>(config: &'a Config, state: &'a StateContainer) -> Container<'a, Message> { + let unit_spacing = 15; + + if state.slatepack_parsed.is_none() { + return Container::new(Text::new( + "you should never see this - dev make sure slatepack is not None", + )); + } + + // Decode/parse/etc fields for display here + let (slatepack, slate) = state.slatepack_parsed.as_ref().unwrap(); + + let sp_sending_address = match &slatepack.sender { + None => "None".to_string(), + Some(s) => s.to_string(), + }; + + let amount = amount_to_hr_string(slate.amount, false); + + let mut state_text = slate.state.to_string(); + + // TODO: What's displayed here should change based on the slate state + let state_text_append = match slate.state { + SlateState::Standard1 => "You are the recipient - Standard workflow", + SlateState::Standard2 => { + "You are the payee, and are finalizing the transaction and sending it to the chain for validation - Standard workflow" + } + SlateState::Standard3 => "This transaction is finalised - Standard workflow", + _ => "Support still in development", + }; + + state_text = format!("{} - {}", state_text, state_text_append); + + let hide_continue = + slate.state != SlateState::Standard1 && slate.state != SlateState::Standard2; + + // Title row + let title = Text::new(localized_string("apply-tx-confirm")) + .size(DEFAULT_HEADER_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Center); + + let title_container = Container::new(title) + .style(grin_gui_core::theme::ContainerStyle::BrightBackground) + .padding(iced::Padding::from([ + 2, // top + 0, // right + 2, // bottom + 5, // left + ])); + + // push more items on to header here: e.g. other buttons, things that belong on the header + let header_row = Row::new().push(title_container); + + let header_container = Container::new(header_row).padding(iced::Padding::from([ + 0, // top + 0, // right + DEFAULT_PADDING, // bottom + 0, // left + ])); + + /// TX State (i.e. Stage) + let state_label = Text::new(format!("{}: ", localized_string("tx-state"))) + .size(DEFAULT_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Left); + + let state_label_container = + Container::new(state_label).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let state = Text::new(state_text).size(DEFAULT_FONT_SIZE); + //.width(Length::Units(400)) + //.style(grin_gui_core::theme::TextInputStyle::AddonsQuery); + + let state_container = + Container::new(state).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let state_row = Row::new().push(state_label_container).push(state_container); + + /// Sender address + let sender_address_label = Text::new(format!("{}: ", localized_string("tx-sender-name"))) + .size(DEFAULT_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Left); + + let sender_address_label_container = Container::new(sender_address_label) + .style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let sender_address = Text::new(sp_sending_address).size(DEFAULT_FONT_SIZE); + //.width(Length::Units(400)) + //.style(grin_gui_core::theme::TextInputStyle::AddonsQuery); + + let sender_address_container = Container::new(sender_address) + .style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let sender_address_row = Row::new() + .push(sender_address_label_container) + .push(sender_address_container); + + let amount_label = Text::new(format!("{}: ", localized_string("apply-tx-amount"))) + .size(DEFAULT_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Left); + + let amount_label_container = + Container::new(amount_label).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let amount = Text::new(amount).size(DEFAULT_FONT_SIZE); + //.width(Length::Units(400)) + //.style(grin_gui_core::theme::TextInputStyle::AddonsQuery); + + let amount_container = + Container::new(amount).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let amount_row = Row::new() + .push(amount_label_container) + .push(amount_container); + + let button_height = Length::Units(BUTTON_HEIGHT); + let button_width = Length::Units(BUTTON_WIDTH); + + let submit_button_label_container = + Container::new(Text::new(localized_string("tx-continue")).size(DEFAULT_FONT_SIZE)) + .width(button_width) + .height(button_height) + .center_x() + .center_y() + .align_x(alignment::Horizontal::Center); + + let mut submit_button = Button::new(submit_button_label_container); + + if hide_continue { + submit_button = submit_button.style(grin_gui_core::theme::ButtonStyle::NormalText); + } else { + submit_button = submit_button + .style(grin_gui_core::theme::ButtonStyle::Primary) + .on_press(Interaction::WalletOperationApplyTxConfirmViewInteraction( + LocalViewInteraction::Accept, + )); + } + + let submit_button: Element = submit_button.into(); + + let cancel_button_label_container = + Container::new(Text::new(localized_string("cancel")).size(DEFAULT_FONT_SIZE)) + .width(button_width) + .height(button_height) + .center_x() + .center_y() + .align_x(alignment::Horizontal::Center); + + let cancel_button: Element = Button::new(cancel_button_label_container) + .style(grin_gui_core::theme::ButtonStyle::Primary) + .on_press(Interaction::WalletOperationApplyTxConfirmViewInteraction( + LocalViewInteraction::Back, + )) + .into(); + + let submit_container = Container::new(submit_button.map(Message::Interaction)).padding(1); + let submit_container = Container::new(submit_container) + .style(grin_gui_core::theme::ContainerStyle::Segmented) + .padding(1); + + let cancel_container = Container::new(cancel_button.map(Message::Interaction)).padding(1); + let cancel_container = Container::new(cancel_container) + .style(grin_gui_core::theme::ContainerStyle::Segmented) + .padding(1); + + let button_row = Row::new() + .push(submit_container) + .push(Space::new(Length::Units(unit_spacing), Length::Units(0))) + .push(cancel_container); + + let column = Column::new() + .push(state_row) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) + .push(sender_address_row) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) + .push(amount_row) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) + .push(button_row) + .push(Space::new(Length::Units(0), Length::Units(unit_spacing))) + .push(Space::new( + Length::Units(0), + Length::Units(unit_spacing + 10), + )); + + let form_container = Container::new(column) + .width(Length::Fill) + .padding(iced::Padding::from([ + 0, // top + 0, // right + 0, // bottom + 5, // left + ])); + + // form container should be scrollable in tiny windows + let scrollable = Scrollable::new(form_container) + .height(Length::Fill) + .style(grin_gui_core::theme::ScrollableStyle::Primary); + + let content = Container::new(scrollable) + .width(Length::Fill) + .height(Length::Shrink) + .style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let wrapper_column = Column::new() + .height(Length::Fill) + .push(header_container) + .push(content); + + // Returns the final container. + Container::new(wrapper_column).padding(iced::Padding::from([ + DEFAULT_PADDING, // top + DEFAULT_PADDING, // right + DEFAULT_PADDING, // bottom + DEFAULT_PADDING, // left + ])) +} diff --git a/src/gui/element/wallet/operation/apply_tx_success.rs b/src/gui/element/wallet/operation/apply_tx_success.rs new file mode 100644 index 0000000..40a41ed --- /dev/null +++ b/src/gui/element/wallet/operation/apply_tx_success.rs @@ -0,0 +1,191 @@ +use { + super::super::super::{ + BUTTON_HEIGHT, BUTTON_WIDTH, DEFAULT_FONT_SIZE, DEFAULT_HEADER_FONT_SIZE, DEFAULT_PADDING, + SMALLER_FONT_SIZE, + }, + crate::gui::{GrinGui, Interaction, Message}, + crate::localization::localized_string, + crate::Result, + grin_gui_core::config::Config, + grin_gui_core::theme::ColorPalette, + grin_gui_core::theme::{ + Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, + }, + iced::widget::{button, pick_list, scrollable, text_input, Button, Checkbox, Space}, + iced::{alignment, Alignment, Command, Length}, + iced_aw::Card, +}; + +pub struct StateContainer { + // Encrypted slate to send to recipient + pub encrypted_slate: Option, +} + +impl Default for StateContainer { + fn default() -> Self { + Self { + encrypted_slate: Default::default(), + } + } +} + +#[derive(Debug, Clone)] +pub enum LocalViewInteraction { + Submit, +} + +pub fn handle_message( + grin_gui: &mut GrinGui, + message: LocalViewInteraction, +) -> Result> { + let state = &mut grin_gui.wallet_state.setup_state.setup_wallet_state; + match message { + LocalViewInteraction::Submit => { + grin_gui.wallet_state.operation_state.mode = + crate::gui::element::wallet::operation::Mode::Home; + } + } + Ok(Command::none()) +} + +pub fn data_container<'a>( + _config: &'a Config, + state: &'a StateContainer, +) -> Container<'a, Message> { + // Title row + let title = Text::new(localized_string("tx-create-success")) + .size(DEFAULT_HEADER_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Center); + + let title_container = Container::new(title) + .style(grin_gui_core::theme::ContainerStyle::BrightBackground) + .padding(iced::Padding::from([ + 2, // top + 0, // right + 2, // bottom + 5, // left + ])); + + // push more items on to header here: e.g. other buttons, things that belong on the header + let header_row = Row::new().push(title_container); + + let header_container = Container::new(header_row).padding(iced::Padding::from([ + 0, // top + 0, // right + DEFAULT_PADDING, // bottom + 0, // left + ])); + + let description = Text::new(localized_string("tx-create-success-desc")) + .size(DEFAULT_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Center); + let description_container = + Container::new(description).style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let slate_card_content = match state.encrypted_slate.as_ref() { + Some(s) => s.clone(), + None => "Transaction was posted to chain".to_owned(), + }; + + let encrypted_slate_card = Card::new( + Text::new(localized_string("tx-create-success-title")).size(DEFAULT_HEADER_FONT_SIZE), + Text::new(slate_card_content).size(DEFAULT_FONT_SIZE), + ) + .foot( + Column::new() + .spacing(10) + .padding(5) + .width(Length::Fill) + .align_items(Alignment::Center) + .push( + Button::new( + Text::new(localized_string("copy-to-clipboard")) + .size(SMALLER_FONT_SIZE) + .horizontal_alignment(alignment::Horizontal::Center), + ) + .style(grin_gui_core::theme::ButtonStyle::NormalText) + .on_press(Message::Interaction(Interaction::WriteToClipboard( + state.encrypted_slate.clone().unwrap_or("None".to_owned()), + ))), + ), + ) + .max_width(400) + .style(grin_gui_core::theme::CardStyle::Normal); + + let unit_spacing = 15; + + let button_height = Length::Units(BUTTON_HEIGHT); + let button_width = Length::Units(BUTTON_WIDTH); + + let cancel_button_label_container = + Container::new(Text::new(localized_string("ok-caps")).size(DEFAULT_FONT_SIZE)) + .width(button_width) + .height(button_height) + .center_x() + .center_y() + .align_x(alignment::Horizontal::Center); + + let cancel_button: Element = Button::new(cancel_button_label_container) + .style(grin_gui_core::theme::ButtonStyle::Primary) + .on_press(Interaction::WalletOperationApplyTxSuccessViewInteraction( + LocalViewInteraction::Submit, + )) + .into(); + + let cancel_container = Container::new(cancel_button.map(Message::Interaction)).padding(1); + let cancel_container = Container::new(cancel_container) + .style(grin_gui_core::theme::ContainerStyle::Segmented) + .padding(1); + + let unit_spacing = 15; + let button_row = Row::new().push(cancel_container); + + let column = Column::new() + .push(description_container) + .push(Space::new( + Length::Units(0), + Length::Units(unit_spacing + 5), + )) + .push(encrypted_slate_card) + .push(Space::new( + Length::Units(0), + Length::Units(unit_spacing + 10), + )) + .push(button_row) + .push(Space::new( + Length::Units(0), + Length::Units(unit_spacing + 10), + )); + + let form_container = Container::new(column) + .width(Length::Fill) + .padding(iced::Padding::from([ + 0, // top + 0, // right + 0, // bottom + 5, // left + ])); + + // form container should be scrollable in tiny windows + let scrollable = Scrollable::new(form_container) + .height(Length::Fill) + .style(grin_gui_core::theme::ScrollableStyle::Primary); + + let content = Container::new(scrollable) + .width(Length::Fill) + .height(Length::Shrink) + .style(grin_gui_core::theme::ContainerStyle::NormalBackground); + + let wrapper_column = Column::new() + .height(Length::Fill) + .push(header_container) + .push(content); + + // Returns the final container. + Container::new(wrapper_column).padding(iced::Padding::from([ + DEFAULT_PADDING, // top + DEFAULT_PADDING, // right + DEFAULT_PADDING, // bottom + DEFAULT_PADDING, // left + ])) +} diff --git a/src/gui/element/wallet/operation/mod.rs b/src/gui/element/wallet/operation/mod.rs index d60344c..e034395 100644 --- a/src/gui/element/wallet/operation/mod.rs +++ b/src/gui/element/wallet/operation/mod.rs @@ -1,22 +1,24 @@ -pub mod open; pub mod action_menu; -pub mod home; -pub mod tx_list; -pub mod create_tx; -pub mod create_tx_success; pub mod apply_tx; +pub mod apply_tx_confirm; +pub mod apply_tx_success; pub mod chart; +pub mod create_tx; +pub mod create_tx_success; +pub mod home; +pub mod open; +pub mod tx_list; pub mod tx_list_display; use { crate::gui::{GrinGui, Message}, crate::Result, - grin_gui_core::theme::ColorPalette, grin_gui_core::config::Config, - iced::{Command, Length}, + grin_gui_core::theme::ColorPalette, grin_gui_core::theme::{ Button, Column, Container, Element, PickList, Row, Scrollable, Text, TextInput, }, + iced::{Command, Length}, }; pub struct StateContainer { @@ -26,6 +28,8 @@ pub struct StateContainer { pub create_tx_state: create_tx::StateContainer, pub create_tx_success_state: create_tx_success::StateContainer, pub apply_tx_state: apply_tx::StateContainer, + pub apply_tx_confirm_state: apply_tx_confirm::StateContainer, + pub apply_tx_success_state: apply_tx_success::StateContainer, // When changed to true, this should stay false until a wallet is opened with a password has_wallet_open_check_failed_one_time: bool, } @@ -36,7 +40,9 @@ pub enum Mode { Home, CreateTx, CreateTxSuccess, - ApplyTx + ApplyTx, + ApplyTxConfirm, + ApplyTxSuccess } impl Default for StateContainer { @@ -48,6 +54,8 @@ impl Default for StateContainer { create_tx_state: Default::default(), create_tx_success_state: Default::default(), apply_tx_state: Default::default(), + apply_tx_confirm_state: Default::default(), + apply_tx_success_state: Default::default(), has_wallet_open_check_failed_one_time: false, } } @@ -68,7 +76,6 @@ impl StateContainer { } } - #[derive(Debug, Clone)] pub enum LocalViewInteraction {} @@ -76,32 +83,27 @@ pub fn handle_message( grin_gui: &mut GrinGui, message: LocalViewInteraction, ) -> Result> { - Ok(Command::none()) } -pub fn data_container<'a>( - state: &'a StateContainer, - config:&'a Config -) -> Container<'a, Message> { +pub fn data_container<'a>(state: &'a StateContainer, config: &'a Config) -> Container<'a, Message> { let content = match state.mode { Mode::Open => open::data_container(&state.open_state, config), - Mode::Home => { - home::data_container(config, &state.home_state) - } - Mode::CreateTx => { - create_tx::data_container(config, &state.create_tx_state) - } + Mode::Home => home::data_container(config, &state.home_state), + Mode::CreateTx => create_tx::data_container(config, &state.create_tx_state), Mode::CreateTxSuccess => { create_tx_success::data_container(config, &state.create_tx_success_state) } - Mode::ApplyTx => { - apply_tx::data_container(config, &state.apply_tx_state) + Mode::ApplyTx => apply_tx::data_container(config, &state.apply_tx_state), + Mode::ApplyTxConfirm => { + apply_tx_confirm::data_container(config, &state.apply_tx_confirm_state) + } + Mode::ApplyTxSuccess => { + apply_tx_success::data_container(config, &state.apply_tx_success_state) } }; - let column = Column::new() - .push(content); + let column = Column::new().push(content); Container::new(column) .center_y() diff --git a/src/gui/element/wallet/operation/tx_list_display.rs b/src/gui/element/wallet/operation/tx_list_display.rs index ce665f1..6a29a6f 100644 --- a/src/gui/element/wallet/operation/tx_list_display.rs +++ b/src/gui/element/wallet/operation/tx_list_display.rs @@ -179,7 +179,7 @@ pub fn handle_message<'a>( let credits = tx.amount_credited; let debits = tx.amount_debited; - datetime_sums.push((datetime, credits - debits)); + datetime_sums.push((datetime, credits as i64 - debits as i64)); } let mut sum = 0; @@ -195,7 +195,7 @@ pub fn handle_message<'a>( let txns = datetime_sums.iter().filter(|(date, _)| *date == dt); // sum up balance amount - sum = sum + txns.map(|x| x.1).collect::>().iter().sum::(); + sum = sum + txns.map(|x| x.1).collect::>().iter().sum::(); // convert to grin units let grin_sum = (sum as f64 / grin_gui_core::GRIN_BASE as f64) as f64; diff --git a/src/gui/mod.rs b/src/gui/mod.rs index a32ea46..cc776b6 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -412,7 +412,7 @@ pub enum Interaction { CloseErrorModal, /// Clipboard copy WriteToClipboard(String), - ReadFromClipboard(String), + ReadSlatepackFromClipboard, /// View interactions MenuViewInteraction(element::menu::LocalViewInteraction), SettingsViewInteraction(element::settings::LocalViewInteraction), @@ -433,6 +433,8 @@ pub enum Interaction { WalletOperationCreateTxViewInteraction(element::wallet::operation::create_tx::LocalViewInteraction), WalletOperationCreateTxSuccessViewInteraction(element::wallet::operation::create_tx_success::LocalViewInteraction), WalletOperationApplyTxViewInteraction(element::wallet::operation::apply_tx::LocalViewInteraction), + WalletOperationApplyTxConfirmViewInteraction(element::wallet::operation::apply_tx_confirm::LocalViewInteraction), + WalletOperationApplyTxSuccessViewInteraction(element::wallet::operation::apply_tx_success::LocalViewInteraction), ViewInteraction(String, String), ModeSelected(Mode), ModeSelectedSettings(element::settings::Mode), diff --git a/src/gui/update.rs b/src/gui/update.rs index f5cef8c..3be024b 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -1,7 +1,10 @@ use { super::{GrinGui, Interaction, Message, Mode}, crate::{gui::element, log_error, Result}, - grin_gui_core::{fs::PersistentData, node::subscriber::UIMessage, node::ChainTypes::Testnet, node::ChainTypes::Mainnet}, + grin_gui_core::{ + fs::PersistentData, node::subscriber::UIMessage, node::ChainTypes::Mainnet, + node::ChainTypes::Testnet, + }, iced::{clipboard, Command}, //grin_gui_widgets::header::ResizeEvent, std::path::PathBuf, @@ -53,12 +56,12 @@ pub fn handle_message(grin_gui: &mut GrinGui, message: Message) -> Result Result {} Message::Interaction(Interaction::CloseErrorModal) => {} Message::Interaction(Interaction::WriteToClipboard(_)) => {} + Message::Interaction(Interaction::ReadSlatepackFromClipboard) => {} Message::Interaction(_) => { grin_gui.error.take(); } @@ -108,14 +112,19 @@ pub fn handle_message(grin_gui: &mut GrinGui, message: Message) -> Result grin_gui.modal_state.show(true), - Message::Interaction(Interaction::CloseErrorModal) => { - grin_gui.modal_state.show(false) - } + Message::Interaction(Interaction::CloseErrorModal) => grin_gui.modal_state.show(false), // Clipboard messages Message::Interaction(Interaction::WriteToClipboard(contents)) => { return Ok(clipboard::write::(contents)); } - // Top level menu + Message::Interaction(Interaction::ReadSlatepackFromClipboard) => { + return Ok(clipboard::read::(|value| { + match value { + Some(v) => return Message::Interaction(Interaction::WalletOperationApplyTxViewInteraction(element::wallet::operation::apply_tx::LocalViewInteraction::ReadFromClipboardSuccess(v))), + None => return Message::Interaction(Interaction::WalletOperationApplyTxViewInteraction(element::wallet::operation::apply_tx::LocalViewInteraction::ReadFromClipboardFailure)) + } + })) + } // Top level menu Message::Interaction(Interaction::MenuViewInteraction(l)) => { let _ = element::menu::handle_message(grin_gui, l); } @@ -168,7 +177,7 @@ pub fn handle_message(grin_gui: &mut GrinGui, message: Message) -> Result { return element::wallet::operation::tx_list_display::handle_message(grin_gui, l); } - // Wallet -> Operation -> TxList + // Wallet -> Operation -> TxList Message::Interaction(Interaction::WalletOperationTxListInteraction(l)) => { return element::wallet::operation::tx_list::handle_message(grin_gui, l); } @@ -180,15 +189,23 @@ pub fn handle_message(grin_gui: &mut GrinGui, message: Message) -> Result { return element::wallet::operation::create_tx_success::handle_message(grin_gui, l); } - // Wallet -> Operation -> Home -> Action + // Wallet -> Operation -> Home -> Action Message::Interaction(Interaction::WalletOperationApplyTxViewInteraction(l)) => { return element::wallet::operation::apply_tx::handle_message(grin_gui, l); } // Wallet -> Operation -> Home -> Action + Message::Interaction(Interaction::WalletOperationApplyTxConfirmViewInteraction(l)) => { + return element::wallet::operation::apply_tx_confirm::handle_message(grin_gui, l); + } + // Wallet -> Operation -> Home -> Action + Message::Interaction(Interaction::WalletOperationApplyTxSuccessViewInteraction(l)) => { + return element::wallet::operation::apply_tx_success::handle_message(grin_gui, l); + } + // Wallet -> Operation -> Home -> Action Message::Interaction(Interaction::WalletOperationHomeActionMenuViewInteraction(l)) => { return element::wallet::operation::action_menu::handle_message(grin_gui, l); } - Message::Interaction(Interaction::ModeSelected(mode)) => { + Message::Interaction(Interaction::ModeSelected(mode)) => { log::debug!("Interaction::ModeSelected({:?})", mode); // Set Mode grin_gui.mode = mode;