wallet: import recovery phrase from qr code

This commit is contained in:
ardocrat 2024-05-05 17:09:33 +03:00
parent f29527a891
commit f01d6cc863
8 changed files with 134 additions and 35 deletions

View file

@ -117,6 +117,7 @@ wallets:
change_server_confirmation: To apply change of connection settings, you need to re-open your wallet. Reopen it now?
tx_send_cancel_conf: 'Are you sure you want to cancel sending of %{amount} ツ?'
tx_receive_cancel_conf: 'Are you sure you want to cancel receiving of %{amount} ツ?'
rec_phrase_not_found: Recovery phrase not found.
transport:
desc: 'Use transport to receive or send messages synchronously:'
tor_network: Tor network

View file

@ -117,6 +117,7 @@ wallets:
change_server_confirmation: Для применения изменения настроек соединения необходимо переоткрыть кошелёк. Переоткрыть его сейчас?
tx_send_cancel_conf: 'Вы действительно хотите отменить отправку %{amount} ツ?'
tx_receive_cancel_conf: 'Вы действительно хотите отменить получение %{amount} ツ?'
rec_phrase_not_found: Фраза восстановления не найдена.
transport:
desc: 'Используйте транспорт для синхронных получения или отправки сообщений:'
tor_network: Сеть Tor

View file

@ -17,6 +17,7 @@ use std::thread;
use eframe::emath::Align;
use egui::load::SizedTexture;
use egui::{Layout, Pos2, Rect, TextureOptions, Widget};
use grin_util::ZeroingString;
use grin_wallet_libwallet::SlatepackAddress;
use image::{DynamicImage, EncodableLayout, ImageFormat};
use crate::gui::Colors;
@ -181,16 +182,16 @@ impl CameraContent {
// Check if string starts with Grin address prefix.
if text.starts_with("tgrin") || text.starts_with("grin") {
if SlatepackAddress::try_from(text).is_ok() {
return QrScanResult::Address(text.to_string())
return QrScanResult::Address(ZeroingString::from(text))
}
}
// Check if string contains Slatepack message prefix and postfix.
if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") {
return QrScanResult::Slatepack(text.to_string())
return QrScanResult::Slatepack(ZeroingString::from(text))
}
QrScanResult::Text(text.to_string())
QrScanResult::Text(ZeroingString::from(text))
}
/// Get QR code scan result.

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use grin_util::ZeroingString;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::Modal;
@ -144,11 +145,11 @@ impl TextEditOptions {
#[derive(Clone)]
pub enum QrScanResult {
/// Slatepack message.
Slatepack(String),
Slatepack(ZeroingString),
/// Slatepack address.
Address(String),
Address(ZeroingString),
/// Parsed text.
Text(String)
Text(ZeroingString)
}
/// QR code scan state.

View file

@ -14,10 +14,11 @@
use egui::{Id, Margin, RichText, ScrollArea, TextStyle, vec2, Widget};
use egui_extras::{RetainedImage, Size, StripBuilder};
use grin_util::ZeroingString;
use crate::built_info;
use crate::gui::Colors;
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, FOLDER_PLUS, SHARE_FAT};
use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, FOLDER_PLUS, SCAN, SHARE_FAT};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Root, View};
use crate::gui::views::types::{ModalPosition, TextEditOptions};
@ -178,10 +179,7 @@ impl WalletCreation {
}
if step == Step::EnterMnemonic {
ui.add_space(4.0);
if !step_available {
self.copy_or_paste_button_ui(ui, cb);
ui.add_space(4.0);
} else {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
@ -191,15 +189,20 @@ impl WalletCreation {
self.copy_or_paste_button_ui(ui, cb);
});
// Show next step button if there are no empty words.
if step_available {
columns[1].vertical_centered_justified(|ui| {
if step_available {
// Show next step button if there are no empty words.
self.next_step_button_ui(ui, step, on_create);
} else {
// Show QR code scan button.
let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase());
View::button(ui, scan_text, Colors::WHITE, || {
self.mnemonic_setup.show_qr_scan_modal(cb);
});
}
});
});
ui.add_space(4.0);
}
} else {
if step_available {
ui.add_space(4.0);
@ -225,7 +228,8 @@ impl WalletCreation {
// Show paste button.
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
View::button(ui, p_t, Colors::WHITE, || {
self.mnemonic_setup.mnemonic.import_text(cb.get_string_from_buffer());
let data = ZeroingString::from(cb.get_string_from_buffer());
self.mnemonic_setup.mnemonic.import_text(&data);
});
}
}

View file

@ -17,8 +17,8 @@ use egui::{Id, RichText};
use crate::gui::Colors;
use crate::gui::icons::PENCIL;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, Root, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions};
use crate::gui::views::{CameraContent, Modal, Root, View};
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult, TextEditOptions};
use crate::wallet::Mnemonic;
use crate::wallet::types::{PhraseMode, PhraseSize};
@ -37,6 +37,11 @@ pub struct MnemonicSetup {
/// Flag to check if entered word is valid.
valid_word_edit: bool,
/// Camera content for QR scan [`Modal`].
camera_content: CameraContent,
/// Flag to check if recovery phrase was found at QR code scanning [`Modal`].
scan_phrase_not_found: Option<bool>,
/// [`Modal`] identifiers allowed at this ui container.
modal_ids: Vec<&'static str>
}
@ -44,6 +49,9 @@ pub struct MnemonicSetup {
/// Identifier for word input [`Modal`].
pub const WORD_INPUT_MODAL: &'static str = "word_input_modal";
/// Identifier for QR code recovery phrase scan [`Modal`].
const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal";
impl Default for MnemonicSetup {
fn default() -> Self {
Self {
@ -52,8 +60,11 @@ impl Default for MnemonicSetup {
word_num_edit: 0,
word_edit: String::from(""),
valid_word_edit: true,
camera_content: CameraContent::default(),
scan_phrase_not_found: None,
modal_ids: vec![
WORD_INPUT_MODAL
WORD_INPUT_MODAL,
QR_CODE_PHRASE_SCAN_MODAL
]
}
}
@ -71,6 +82,7 @@ impl ModalContainer for MnemonicSetup {
cb: &dyn PlatformCallbacks) {
match modal.id {
WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb),
QR_CODE_PHRASE_SCAN_MODAL => self.scan_qr_modal_ui(ui, modal, cb),
_ => {}
}
}
@ -338,6 +350,85 @@ impl MnemonicSetup {
ui.add_space(6.0);
});
}
/// Show QR code recovery phrase scanner [`Modal`].
pub fn show_qr_scan_modal(&mut self, cb: &dyn PlatformCallbacks) {
self.scan_phrase_not_found = None;
// Show QR code scan modal.
Modal::new(QR_CODE_PHRASE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
.closeable(false)
.show();
cb.start_camera();
}
/// Draw QR code scan [`Modal`] content.
fn scan_qr_modal_ui(&mut self,
ui: &mut egui::Ui,
modal: &Modal,
cb: &dyn PlatformCallbacks) {
// Show scan result if exists or show camera content while scanning.
if let Some(_) = &self.scan_phrase_not_found {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(RichText::new(t!("wallets.rec_phrase_not_found"))
.size(17.0)
.color(Colors::RED));
});
ui.add_space(6.0);
} else if let Some(result) = self.camera_content.qr_scan_result() {
cb.stop_camera();
self.camera_content.clear_state();
match &result {
QrScanResult::Text(text) => {
self.mnemonic.import_text(text);
if self.mnemonic.is_valid_phrase() {
modal.close();
return;
}
}
_ => {}
}
// Set an error when found phrase was not valid.
self.scan_phrase_not_found = Some(true);
Modal::set_title(t!("scan_result"));
} else {
ui.add_space(6.0);
self.camera_content.ui(ui, cb);
ui.add_space(6.0);
}
if self.scan_phrase_not_found.is_some() {
// Setup spacing between buttons.
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
ui.columns(2, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::button(ui, t!("close"), Colors::WHITE, || {
self.scan_phrase_not_found = None;
modal.close();
});
});
columns[1].vertical_centered_justified(|ui| {
View::button(ui, t!("repeat"), Colors::WHITE, || {
Modal::set_title(t!("scan_qr"));
self.scan_phrase_not_found = None;
cb.start_camera();
});
});
});
} else {
ui.vertical_centered_justified(|ui| {
View::button(ui, t!("modal.cancel"), Colors::WHITE, || {
cb.stop_camera();
modal.close();
});
});
}
ui.add_space(6.0);
}
}
/// Calculate word list columns count based on available ui width.

View file

@ -192,9 +192,8 @@ impl WalletContent {
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Draw button to scan QR code.
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
// Load accounts.
self.qr_scan_result = None;
// Show account list modal.
// Show QR code scan modal.
Modal::new(QR_CODE_SCAN_MODAL)
.position(ModalPosition::CenterTop)
.title(t!("scan_qr"))
@ -359,7 +358,7 @@ impl WalletContent {
}
}
/// Draw QR scanner [`Modal`] content.
/// Draw QR code scan [`Modal`] content.
fn scan_qr_modal_ui(&mut self,
ui: &mut egui::Ui,
wallet: &mut Wallet,
@ -371,7 +370,7 @@ impl WalletContent {
QrScanResult::Slatepack(t) => t,
QrScanResult::Address(t) => t,
QrScanResult::Text(t) => t
}.clone();
}.to_string();
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(3.0);
ScrollArea::vertical()
@ -396,7 +395,7 @@ impl WalletContent {
ui.vertical_centered(|ui| {
let copy_text = format!("{} {}", COPY, t!("copy"));
View::button(ui, copy_text, Colors::BUTTON, || {
cb.copy_string_to_buffer(result_text);
cb.copy_string_to_buffer(result_text.to_string());
self.qr_scan_result = None;
modal.close();
});
@ -409,7 +408,7 @@ impl WalletContent {
QrScanResult::Slatepack(message) => {
// Redirect to messages to handle parsed message.
let mut messages =
WalletMessages::new(wallet.can_use_dandelion(), Some(message.clone()));
WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string()));
messages.parse_message(wallet);
modal.close();
self.current_tab = Box::new(messages);
@ -420,7 +419,7 @@ impl WalletContent {
// Redirect to send amount with Tor.
let addr = wallet.slatepack_address().unwrap();
let mut transport = WalletTransport::new(addr.clone());
transport.show_send_tor_modal(cb, Some(receiver.clone()));
transport.show_send_tor_modal(cb, Some(receiver.to_string()));
modal.close();
self.current_tab = Box::new(transport);
return;

View file

@ -13,6 +13,7 @@
// limitations under the License.
use grin_keychain::mnemonic::{from_entropy, search, to_entropy};
use grin_util::ZeroingString;
use rand::{Rng, thread_rng};
use crate::wallet::types::{PhraseMode, PhraseSize};
@ -105,7 +106,7 @@ impl Mnemonic {
}
/// Set words from provided text if possible.
pub fn import_text(&mut self, text: String) {
pub fn import_text(&mut self, text: &ZeroingString) {
if self.mode != PhraseMode::Import {
return;
}