Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f79cc0708 | |||
| b0b4f9068a | |||
| cb9e86750c | |||
| 86fbf2e14f | |||
| e0351cea84 |
@@ -41,15 +41,6 @@ jobs:
|
||||
run: |
|
||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --profile release-apk
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
- name: Build APK ARMv8
|
||||
working-directory: android
|
||||
run: |
|
||||
./gradlew assembleRelease
|
||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv8.apk
|
||||
- name: Checksum APK ARMv8
|
||||
working-directory: android
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv8.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
|
||||
- name: Build lib ARMv7 1/2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
@@ -59,17 +50,17 @@ jobs:
|
||||
run: |
|
||||
unset CPPFLAGS && unset CFLAGS && cargo ndk -t armeabi-v7a -o android/app/src/main/jniLibs build --profile release-apk
|
||||
sed -i -e 's/"cdylib","rlib"/"rlib"/g' Cargo.toml
|
||||
- name: Build APK ARMv7
|
||||
- name: Build APK ARM
|
||||
working-directory: android
|
||||
run: |
|
||||
rm -rf app/build
|
||||
rm -rf app/src/main/jniLibs/*
|
||||
./gradlew assembleRelease
|
||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-armv7.apk
|
||||
- name: Checksum APK ARMv7
|
||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android.apk
|
||||
rm -rf app/src/main/jniLibs/*
|
||||
- name: Checksum APK ARM
|
||||
working-directory: android
|
||||
shell: pwsh
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android-armv7.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
|
||||
run: get-filehash -algorithm sha256 grim-${{ github.ref_name }}-android.apk | Format-List | Out-String | ForEach-Object { $_.Trim() } > grim-${{ github.ref_name }}-android-sha256sum.txt
|
||||
- name: Build lib x86 1/2
|
||||
continue-on-error: true
|
||||
run: |
|
||||
@@ -83,7 +74,6 @@ jobs:
|
||||
working-directory: android
|
||||
run: |
|
||||
rm -rf app/build
|
||||
rm -rf app/src/main/jniLibs/*
|
||||
./gradlew assembleRelease
|
||||
mv app/build/outputs/apk/release/app-release.apk grim-${{ github.ref_name }}-android-x86_64.apk
|
||||
- name: Checksum APK x86
|
||||
@@ -94,10 +84,8 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
android/grim-${{ github.ref_name }}-android-armv8.apk
|
||||
android/grim-${{ github.ref_name }}-android-armv8-sha256sum.txt
|
||||
android/grim-${{ github.ref_name }}-android-armv7.apk
|
||||
android/grim-${{ github.ref_name }}-android-armv7-sha256sum.txt
|
||||
android/grim-${{ github.ref_name }}-android.apk
|
||||
android/grim-${{ github.ref_name }}-android-sha256sum.txt
|
||||
android/grim-${{ github.ref_name }}-android-x86_64.apk
|
||||
android/grim-${{ github.ref_name }}-android-x86_64-sha256sum.txt
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -3796,7 +3796,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "grim"
|
||||
version = "0.1.0"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"android-activity 0.6.0",
|
||||
"android_logger",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "grim"
|
||||
version = "0.1.0"
|
||||
version = "0.1.3"
|
||||
authors = ["Ardocrat <ardocrat@proton.me>"]
|
||||
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId "mw.gri.android"
|
||||
minSdk 24
|
||||
targetSdk 33
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
versionCode 3
|
||||
versionName "0.1.3"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage to bump version
|
||||
# ./version.sh patch
|
||||
# ./version.sh minor
|
||||
# ./version.sh major
|
||||
|
||||
# Setup base directory
|
||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
||||
cd ${BASEDIR}
|
||||
cd ..
|
||||
|
||||
# Exit script if command fails or uninitialized variables used
|
||||
set -euo pipefail
|
||||
|
||||
# ==================================
|
||||
# Verify repo is clean
|
||||
# ==================================
|
||||
|
||||
# List uncommitted changes and
|
||||
# check if the output is not empty
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
# Print error message
|
||||
printf "\nError: repo has uncommitted changes\n\n"
|
||||
# Exit with error code
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ==================================
|
||||
# Get latest version from git tags
|
||||
# ==================================
|
||||
|
||||
# List git tags sorted lexicographically
|
||||
# so version numbers sorted correctly
|
||||
GIT_TAGS=$(git tag --sort=version:refname)
|
||||
|
||||
# Get last line of output which returns the
|
||||
# last tag (most recent version)
|
||||
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
|
||||
|
||||
# If no tag found, default to v0.1.0
|
||||
if [ -z "$GIT_TAG_LATEST" ]; then
|
||||
GIT_TAG_LATEST="v0.1.0"
|
||||
fi
|
||||
|
||||
# Strip prefix 'v' from the tag to easily increment
|
||||
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
|
||||
|
||||
# ==================================
|
||||
# Increment version number
|
||||
# ==================================
|
||||
|
||||
# Get version type from first argument passed to script
|
||||
VERSION_TYPE="${1-}"
|
||||
VERSION_NEXT=""
|
||||
|
||||
if [ "$VERSION_TYPE" = "patch" ]; then
|
||||
# Increment patch version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
|
||||
elif [ "$VERSION_TYPE" = "minor" ]; then
|
||||
# Increment minor version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
|
||||
elif [ "$VERSION_TYPE" = "major" ]; then
|
||||
# Increment major version
|
||||
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
|
||||
else
|
||||
# Print error for unknown versioning type
|
||||
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
|
||||
# Exit with error code
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ==================================
|
||||
# Update Android build.gradle file
|
||||
# and package version at Cargo.toml
|
||||
# ==================================
|
||||
|
||||
# Update version in build.gradle
|
||||
|
||||
sed -i 's/versionName [0-9a-zA-Z -_]*/versionName "'"$VERSION_NEXT"'"/' android/app/build.gradle
|
||||
|
||||
# Update version in Cargo.toml
|
||||
sed -i "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
|
||||
|
||||
# Update Cargo.lock as this changes when
|
||||
# updating the version in your manifest
|
||||
cargo update -p grim
|
||||
|
||||
# Commit the changes
|
||||
git add .
|
||||
git commit -m "release: v$VERSION_NEXT"
|
||||
|
||||
# ==================================
|
||||
# Create git tag for new version
|
||||
# ==================================
|
||||
|
||||
# Create a tag at master branch
|
||||
git tag "v$VERSION_NEXT" master
|
||||
@@ -589,7 +589,7 @@ impl WalletsContent {
|
||||
if self.pass_edit.is_empty() {
|
||||
return;
|
||||
}
|
||||
match self.wallets.open_selected(self.pass_edit.clone()) {
|
||||
match self.wallets.open_selected(&self.pass_edit) {
|
||||
Ok(_) => {
|
||||
// Clear values.
|
||||
self.pass_edit = "".to_string();
|
||||
|
||||
@@ -43,7 +43,10 @@ pub struct WalletCreation {
|
||||
/// Mnemonic phrase setup content.
|
||||
pub(crate) mnemonic_setup: MnemonicSetup,
|
||||
/// Network setup content.
|
||||
pub(crate) network_setup: ConnectionSettings
|
||||
pub(crate) network_setup: ConnectionSettings,
|
||||
|
||||
/// Flag to check if an error occurred during wallet creation.
|
||||
creation_error: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for WalletCreation {
|
||||
@@ -54,7 +57,8 @@ impl Default for WalletCreation {
|
||||
name_edit: String::from(""),
|
||||
pass_edit: String::from(""),
|
||||
mnemonic_setup: MnemonicSetup::default(),
|
||||
network_setup: ConnectionSettings::default()
|
||||
network_setup: ConnectionSettings::default(),
|
||||
creation_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,41 +143,50 @@ impl WalletCreation {
|
||||
// Setup step description text and availability.
|
||||
let (step_text, mut step_available) = match step {
|
||||
Step::EnterMnemonic => {
|
||||
let mode = &self.mnemonic_setup.mnemonic.mode;
|
||||
let text = if mode == &PhraseMode::Generate {
|
||||
t!("wallets.create_phrase_desc")
|
||||
} else {
|
||||
t!("wallets.restore_phrase_desc")
|
||||
let mode = &self.mnemonic_setup.mnemonic.mode();
|
||||
let (text, available) = match mode {
|
||||
PhraseMode::Generate => (t!("wallets.create_phrase_desc"), true),
|
||||
PhraseMode::Import => {
|
||||
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
|
||||
(t!("wallets.restore_phrase_desc"), available)
|
||||
}
|
||||
};
|
||||
let available = !self
|
||||
.mnemonic_setup
|
||||
.mnemonic
|
||||
.words
|
||||
.contains(&String::from(""));
|
||||
(text, available)
|
||||
}
|
||||
Step::ConfirmMnemonic => {
|
||||
let text = t!("wallets.restore_phrase_desc");
|
||||
let available = !self
|
||||
.mnemonic_setup
|
||||
.mnemonic
|
||||
.confirm_words
|
||||
.contains(&String::from(""));
|
||||
let available = !self.mnemonic_setup.mnemonic.has_empty_or_invalid();
|
||||
(text, available)
|
||||
},
|
||||
Step::SetupConnection => (t!("wallets.setup_conn_desc"), true)
|
||||
}
|
||||
Step::SetupConnection => {
|
||||
(t!("wallets.setup_conn_desc"), self.creation_error.is_none())
|
||||
}
|
||||
};
|
||||
// Show step description.
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
|
||||
ui.add_space(2.0);
|
||||
// Show error if entered phrase is not valid.
|
||||
if !self.mnemonic_setup.valid_phrase {
|
||||
step_available = false;
|
||||
ui.label(RichText::new(t!("wallets.not_valid_phrase"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()));
|
||||
|
||||
// Show step description or error.
|
||||
let generate_step = step == Step::EnterMnemonic &&
|
||||
self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate;
|
||||
if (self.mnemonic_setup.mnemonic.valid() && self.creation_error.is_none()) ||
|
||||
generate_step {
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(step_text).size(16.0).color(Colors::gray()));
|
||||
ui.add_space(2.0);
|
||||
} else {
|
||||
step_available = false;
|
||||
// Show error text.
|
||||
if let Some(err) = &self.creation_error {
|
||||
ui.add_space(10.0);
|
||||
ui.label(RichText::new(err)
|
||||
.size(16.0)
|
||||
.color(Colors::red()));
|
||||
ui.add_space(10.0);
|
||||
} else {
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(&t!("wallets.not_valid_phrase"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()));
|
||||
ui.add_space(2.0);
|
||||
};
|
||||
}
|
||||
if step == Step::EnterMnemonic {
|
||||
ui.add_space(4.0);
|
||||
@@ -209,8 +222,8 @@ impl WalletCreation {
|
||||
} else {
|
||||
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
|
||||
View::button(ui, paste_text, Colors::white_or_black(false), || {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
|
||||
self.mnemonic_setup.mnemonic.import_text(&data, true);
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
});
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
@@ -225,7 +238,7 @@ impl WalletCreation {
|
||||
|
||||
/// Draw copy or paste button at [`Step::EnterMnemonic`].
|
||||
fn copy_or_paste_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
match self.mnemonic_setup.mnemonic.mode {
|
||||
match self.mnemonic_setup.mnemonic.mode() {
|
||||
PhraseMode::Generate => {
|
||||
// Show copy button.
|
||||
let c_t = format!("{} {}", COPY, t!("copy").to_uppercase());
|
||||
@@ -237,8 +250,8 @@ impl WalletCreation {
|
||||
// Show paste button.
|
||||
let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase());
|
||||
View::button(ui, p_t, Colors::white_or_black(false), || {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer().trim());
|
||||
self.mnemonic_setup.mnemonic.import_text(&data, false);
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -262,15 +275,10 @@ impl WalletCreation {
|
||||
self.step = if let Some(step) = &self.step {
|
||||
match step {
|
||||
Step::EnterMnemonic => {
|
||||
if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate {
|
||||
if self.mnemonic_setup.mnemonic.mode() == PhraseMode::Generate {
|
||||
Some(Step::ConfirmMnemonic)
|
||||
} else {
|
||||
// Check if entered phrase was valid.
|
||||
if self.mnemonic_setup.valid_phrase {
|
||||
Some(Step::SetupConnection)
|
||||
} else {
|
||||
Some(Step::EnterMnemonic)
|
||||
}
|
||||
Some(Step::SetupConnection)
|
||||
}
|
||||
}
|
||||
Step::ConfirmMnemonic => {
|
||||
@@ -278,24 +286,29 @@ impl WalletCreation {
|
||||
},
|
||||
Step::SetupConnection => {
|
||||
// Create wallet at last step.
|
||||
let name = self.name_edit.clone();
|
||||
let pass = self.pass_edit.clone();
|
||||
let phrase = self.mnemonic_setup.mnemonic.get_phrase();
|
||||
let conn_method = &self.network_setup.method;
|
||||
let mut wallet = Wallet::create(name,
|
||||
pass.clone(),
|
||||
phrase,
|
||||
conn_method).unwrap();
|
||||
// Open created wallet.
|
||||
wallet.open(pass).unwrap();
|
||||
// Pass created wallet to callback.
|
||||
(on_create)(wallet);
|
||||
// Reset input data.
|
||||
self.step = None;
|
||||
self.name_edit = String::from("");
|
||||
self.pass_edit = String::from("");
|
||||
self.mnemonic_setup.reset();
|
||||
None
|
||||
match Wallet::create(&self.name_edit,
|
||||
&self.pass_edit,
|
||||
&self.mnemonic_setup.mnemonic,
|
||||
conn_method) {
|
||||
Ok(mut w) => {
|
||||
// Open created wallet.
|
||||
w.open(&self.pass_edit).unwrap();
|
||||
// Pass created wallet to callback.
|
||||
(on_create)(w);
|
||||
// Reset input data.
|
||||
self.step = None;
|
||||
self.name_edit = String::from("");
|
||||
self.pass_edit = String::from("");
|
||||
self.mnemonic_setup.reset();
|
||||
None
|
||||
|
||||
}
|
||||
Err(e) => {
|
||||
self.creation_error = Some(format!("{:?}", e));
|
||||
Some(Step::SetupConnection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -364,9 +377,13 @@ impl WalletCreation {
|
||||
self.name_edit = String::from("");
|
||||
self.pass_edit = String::from("");
|
||||
self.mnemonic_setup.reset();
|
||||
self.creation_error = None;
|
||||
},
|
||||
Step::ConfirmMnemonic => self.step = Some(Step::EnterMnemonic),
|
||||
Step::SetupConnection => self.step = Some(Step::EnterMnemonic)
|
||||
Step::SetupConnection => {
|
||||
self.creation_error = None;
|
||||
self.step = Some(Step::EnterMnemonic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,21 +20,18 @@ use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{CameraContent, Modal, Content, View};
|
||||
use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult, TextEditOptions};
|
||||
use crate::wallet::Mnemonic;
|
||||
use crate::wallet::types::{PhraseMode, PhraseSize};
|
||||
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
|
||||
|
||||
/// Mnemonic phrase setup content.
|
||||
pub struct MnemonicSetup {
|
||||
/// Current mnemonic phrase.
|
||||
pub(crate) mnemonic: Mnemonic,
|
||||
|
||||
/// Flag to check if entered phrase was valid.
|
||||
pub(crate) valid_phrase: bool,
|
||||
pub mnemonic: Mnemonic,
|
||||
|
||||
/// Current word number to edit at [`Modal`].
|
||||
word_num_edit: usize,
|
||||
word_index_edit: usize,
|
||||
/// Entered word value for [`Modal`].
|
||||
word_edit: String,
|
||||
/// Flag to check if entered word is valid.
|
||||
/// Flag to check if entered word is valid at [`Modal`].
|
||||
valid_word_edit: bool,
|
||||
|
||||
/// Camera content for QR scan [`Modal`].
|
||||
@@ -56,8 +53,7 @@ impl Default for MnemonicSetup {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mnemonic: Mnemonic::default(),
|
||||
valid_phrase: true,
|
||||
word_num_edit: 0,
|
||||
word_index_edit: 0,
|
||||
word_edit: String::from(""),
|
||||
valid_word_edit: true,
|
||||
camera_content: CameraContent::default(),
|
||||
@@ -103,7 +99,7 @@ impl MnemonicSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show words setup.
|
||||
self.word_list_ui(ui, self.mnemonic.mode == PhraseMode::Import, cb);
|
||||
self.word_list_ui(ui, self.mnemonic.mode() == PhraseMode::Import, cb);
|
||||
}
|
||||
|
||||
/// Draw content for phrase confirmation step.
|
||||
@@ -123,7 +119,7 @@ impl MnemonicSetup {
|
||||
/// Draw mode and size setup.
|
||||
fn mode_type_ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Show mode setup.
|
||||
let mut mode = self.mnemonic.mode.clone();
|
||||
let mut mode = self.mnemonic.mode();
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
let create_mode = PhraseMode::Generate;
|
||||
@@ -136,8 +132,8 @@ impl MnemonicSetup {
|
||||
View::radio_value(ui, &mut mode, import_mode, import_text);
|
||||
});
|
||||
});
|
||||
if mode != self.mnemonic.mode {
|
||||
self.mnemonic.set_mode(mode)
|
||||
if mode != self.mnemonic.mode() {
|
||||
self.mnemonic.set_mode(mode);
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
@@ -150,7 +146,7 @@ impl MnemonicSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show mnemonic phrase size setup.
|
||||
let mut size = self.mnemonic.size.clone();
|
||||
let mut size = self.mnemonic.size();
|
||||
ui.columns(5, |columns| {
|
||||
for (index, word) in PhraseSize::VALUES.iter().enumerate() {
|
||||
columns[index].vertical_centered(|ui| {
|
||||
@@ -159,29 +155,20 @@ impl MnemonicSetup {
|
||||
});
|
||||
}
|
||||
});
|
||||
if size != self.mnemonic.size {
|
||||
if size != self.mnemonic.size() {
|
||||
self.mnemonic.set_size(size);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw list of words for mnemonic phrase.
|
||||
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit_words: bool, cb: &dyn PlatformCallbacks) {
|
||||
/// Draw grid of words for mnemonic phrase.
|
||||
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool, cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(6.0);
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between columns.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 6.0);
|
||||
|
||||
// Select list of words based on current mode and edit flag.
|
||||
let words = match self.mnemonic.mode {
|
||||
PhraseMode::Generate => {
|
||||
if edit_words {
|
||||
&self.mnemonic.confirm_words
|
||||
} else {
|
||||
&self.mnemonic.words
|
||||
}
|
||||
}
|
||||
PhraseMode::Import => &self.mnemonic.words
|
||||
}.clone();
|
||||
let words = self.mnemonic.words(edit);
|
||||
|
||||
let mut word_number = 0;
|
||||
let cols = list_columns_count(ui);
|
||||
@@ -192,25 +179,25 @@ impl MnemonicSetup {
|
||||
ui.columns(cols, |columns| {
|
||||
columns[0].horizontal(|ui| {
|
||||
let word = chunk.get(0).unwrap();
|
||||
self.word_item_ui(ui, word_number, word, edit_words, cb);
|
||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
||||
});
|
||||
columns[1].horizontal(|ui| {
|
||||
word_number += 1;
|
||||
let word = chunk.get(1).unwrap();
|
||||
self.word_item_ui(ui, word_number, word, edit_words, cb);
|
||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
||||
});
|
||||
if size > 2 {
|
||||
columns[2].horizontal(|ui| {
|
||||
word_number += 1;
|
||||
let word = chunk.get(2).unwrap();
|
||||
self.word_item_ui(ui, word_number, word, edit_words, cb);
|
||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
||||
});
|
||||
}
|
||||
if size > 3 {
|
||||
columns[3].horizontal(|ui| {
|
||||
word_number += 1;
|
||||
let word = chunk.get(3).unwrap();
|
||||
self.word_item_ui(ui, word_number, word, edit_words, cb);
|
||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -218,7 +205,7 @@ impl MnemonicSetup {
|
||||
ui.columns(cols, |columns| {
|
||||
columns[0].horizontal(|ui| {
|
||||
let word = chunk.get(0).unwrap();
|
||||
self.word_item_ui(ui, word_number, word, edit_words, cb);
|
||||
self.word_item_ui(ui, word_number, word, edit, cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -227,20 +214,24 @@ impl MnemonicSetup {
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw word list item for current mode.
|
||||
/// Draw word grid item.
|
||||
fn word_item_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
num: usize,
|
||||
word: &String,
|
||||
word: &PhraseWord,
|
||||
edit: bool,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
let color = if !word.valid || (word.text.is_empty() && !self.mnemonic.valid()) {
|
||||
Colors::red()
|
||||
} else {
|
||||
Colors::white_or_black(true)
|
||||
};
|
||||
if edit {
|
||||
ui.add_space(6.0);
|
||||
View::button(ui, PENCIL.to_string(), Colors::button(), || {
|
||||
// Setup modal values.
|
||||
self.word_num_edit = num;
|
||||
self.word_edit = word.clone();
|
||||
self.valid_word_edit = true;
|
||||
self.word_index_edit = num - 1;
|
||||
self.word_edit = word.text.clone();
|
||||
self.valid_word_edit = word.valid;
|
||||
// Show word edit modal.
|
||||
Modal::new(WORD_INPUT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
@@ -248,17 +239,17 @@ impl MnemonicSetup {
|
||||
.show();
|
||||
cb.show_keyboard();
|
||||
});
|
||||
ui.label(RichText::new(format!("#{} {}", num, word))
|
||||
ui.label(RichText::new(format!("#{} {}", num, word.text))
|
||||
.size(17.0)
|
||||
.color(Colors::white_or_black(true)));
|
||||
.color(color));
|
||||
} else {
|
||||
ui.add_space(12.0);
|
||||
let text = format!("#{} {}", num, word);
|
||||
ui.label(RichText::new(text).size(17.0).color(Colors::white_or_black(true)));
|
||||
let text = format!("#{} {}", num, word.text);
|
||||
ui.label(RichText::new(text).size(17.0).color(color));
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset mnemonic phrase to default values.
|
||||
/// Reset mnemonic phrase state to default values.
|
||||
pub fn reset(&mut self) {
|
||||
self.mnemonic = Mnemonic::default();
|
||||
}
|
||||
@@ -267,14 +258,14 @@ impl MnemonicSetup {
|
||||
fn word_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_num_edit))
|
||||
ui.label(RichText::new(t!("wallets.enter_word", "number" => self.word_index_edit + 1))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw word value text edit.
|
||||
let mut text_edit_opts = TextEditOptions::new(
|
||||
Id::from(modal.id).with(self.word_num_edit)
|
||||
Id::from(modal.id).with(self.word_index_edit)
|
||||
);
|
||||
View::text_edit(ui, cb, &mut self.word_edit, &mut text_edit_opts);
|
||||
|
||||
@@ -304,38 +295,22 @@ impl MnemonicSetup {
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
// Callback to save the word.
|
||||
let mut save = || {
|
||||
self.word_edit = self.word_edit.trim().to_string();
|
||||
|
||||
// Check if word is valid.
|
||||
let word_index = self.word_num_edit - 1;
|
||||
if !self.mnemonic.is_valid_word(&self.word_edit, word_index) {
|
||||
self.valid_word_edit = false;
|
||||
// Insert word checking validity.
|
||||
let word = &self.word_edit.trim().to_string();
|
||||
self.valid_word_edit = self.mnemonic.insert(self.word_index_edit, word);
|
||||
if !self.valid_word_edit {
|
||||
return;
|
||||
}
|
||||
self.valid_word_edit = true;
|
||||
|
||||
// Select list where to save word.
|
||||
let words = match self.mnemonic.mode {
|
||||
PhraseMode::Generate => &mut self.mnemonic.confirm_words,
|
||||
PhraseMode::Import => &mut self.mnemonic.words
|
||||
};
|
||||
|
||||
// Save word at list.
|
||||
words.remove(word_index);
|
||||
words.insert(word_index, self.word_edit.clone());
|
||||
|
||||
// Close modal or go to next word to edit.
|
||||
let close_modal = words.len() == self.word_num_edit
|
||||
|| !words.get(self.word_num_edit).unwrap().is_empty();
|
||||
let next_word = self.mnemonic.get(self.word_index_edit + 1);
|
||||
let close_modal = next_word.is_none() ||
|
||||
(!next_word.as_ref().unwrap().text.is_empty() &&
|
||||
next_word.unwrap().valid);
|
||||
if close_modal {
|
||||
// Check if entered phrase was valid when all words were entered.
|
||||
if !self.mnemonic.words.contains(&String::from("")) {
|
||||
self.valid_phrase = self.mnemonic.is_valid_phrase();
|
||||
}
|
||||
cb.hide_keyboard();
|
||||
modal.close();
|
||||
} else {
|
||||
self.word_num_edit += 1;
|
||||
self.word_index_edit += 1;
|
||||
self.word_edit = String::from("");
|
||||
}
|
||||
};
|
||||
@@ -382,8 +357,8 @@ impl MnemonicSetup {
|
||||
self.camera_content.clear_state();
|
||||
match &result {
|
||||
QrScanResult::Text(text) => {
|
||||
self.mnemonic.import_text(text, false);
|
||||
if self.mnemonic.is_valid_phrase() {
|
||||
self.mnemonic.import(text);
|
||||
if self.mnemonic.valid() {
|
||||
modal.close();
|
||||
return;
|
||||
}
|
||||
|
||||
+2
-2
@@ -133,11 +133,11 @@ impl WalletList {
|
||||
}
|
||||
|
||||
/// Open selected [`Wallet`].
|
||||
pub fn open_selected(&mut self, password: String) -> Result<(), Error> {
|
||||
pub fn open_selected(&mut self, password: &String) -> Result<(), Error> {
|
||||
let selected_id = self.selected_id.clone();
|
||||
for w in self.mut_list() {
|
||||
if Some(w.get_config().id) == selected_id {
|
||||
return w.open(password.clone());
|
||||
return w.open(password);
|
||||
}
|
||||
}
|
||||
Err(Error::GenericError("Wallet is not selected".to_string()))
|
||||
|
||||
+147
-43
@@ -16,18 +16,20 @@ use grin_keychain::mnemonic::{from_entropy, search, to_entropy};
|
||||
use grin_util::ZeroingString;
|
||||
use rand::{Rng, thread_rng};
|
||||
|
||||
use crate::wallet::types::{PhraseMode, PhraseSize};
|
||||
use crate::wallet::types::{PhraseMode, PhraseSize, PhraseWord};
|
||||
|
||||
/// Mnemonic phrase container.
|
||||
pub struct Mnemonic {
|
||||
/// Phrase setup mode.
|
||||
pub(crate) mode: PhraseMode,
|
||||
mode: PhraseMode,
|
||||
/// Size of phrase based on words count.
|
||||
pub(crate) size: PhraseSize,
|
||||
size: PhraseSize,
|
||||
/// Generated words.
|
||||
pub(crate) words: Vec<String>,
|
||||
words: Vec<PhraseWord>,
|
||||
/// Words to confirm the phrase.
|
||||
pub(crate) confirm_words: Vec<String>
|
||||
confirmation: Vec<PhraseWord>,
|
||||
/// Flag to check if entered phrase if valid.
|
||||
valid: bool,
|
||||
}
|
||||
|
||||
impl Default for Mnemonic {
|
||||
@@ -35,49 +37,67 @@ impl Default for Mnemonic {
|
||||
let size = PhraseSize::Words24;
|
||||
let mode = PhraseMode::Generate;
|
||||
let words = Self::generate_words(&mode, &size);
|
||||
let confirm_words = Self::empty_words(&size);
|
||||
Self { mode, size, words, confirm_words }
|
||||
let confirmation = Self::empty_words(&size);
|
||||
Self { mode, size, words, confirmation, valid: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl Mnemonic {
|
||||
/// Change mnemonic phrase setup [`PhraseMode`].
|
||||
/// Generate words based on provided [`PhraseMode`].
|
||||
pub fn set_mode(&mut self, mode: PhraseMode) {
|
||||
self.mode = mode;
|
||||
self.words = Self::generate_words(&self.mode, &self.size);
|
||||
self.confirm_words = Self::empty_words(&self.size);
|
||||
self.confirmation = Self::empty_words(&self.size);
|
||||
self.valid = true;
|
||||
}
|
||||
|
||||
/// Change mnemonic phrase words [`PhraseSize`].
|
||||
/// Get current phrase mode.
|
||||
pub fn mode(&self) -> PhraseMode {
|
||||
self.mode.clone()
|
||||
}
|
||||
|
||||
/// Generate words based on provided [`PhraseSize`].
|
||||
pub fn set_size(&mut self, size: PhraseSize) {
|
||||
self.size = size;
|
||||
self.words = Self::generate_words(&self.mode, &self.size);
|
||||
self.confirm_words = Self::empty_words(&self.size);
|
||||
self.confirmation = Self::empty_words(&self.size);
|
||||
self.valid = true;
|
||||
}
|
||||
|
||||
/// Check if provided word is in BIP39 format and equal to non-empty generated word at index.
|
||||
pub fn is_valid_word(&self, word: &String, index: usize) -> bool {
|
||||
let valid = search(word).is_ok();
|
||||
let equal = if let Some(gen_word) = self.words.get(index) {
|
||||
gen_word.is_empty() || gen_word == word
|
||||
} else {
|
||||
false
|
||||
};
|
||||
valid && equal
|
||||
/// Get current phrase size.
|
||||
pub fn size(&self) -> PhraseSize {
|
||||
self.size.clone()
|
||||
}
|
||||
|
||||
/// Get words based on current [`PhraseMode`].
|
||||
pub fn words(&self, edit: bool) -> Vec<PhraseWord> {
|
||||
match self.mode {
|
||||
PhraseMode::Generate => {
|
||||
if edit {
|
||||
&self.confirmation
|
||||
} else {
|
||||
&self.words
|
||||
}
|
||||
}
|
||||
PhraseMode::Import => &self.words
|
||||
}.clone()
|
||||
}
|
||||
|
||||
/// Check if current phrase is valid.
|
||||
pub fn is_valid_phrase(&self) -> bool {
|
||||
to_entropy(self.get_phrase().as_str()).is_ok()
|
||||
pub fn valid(&self) -> bool {
|
||||
self.valid
|
||||
}
|
||||
|
||||
/// Get phrase from words.
|
||||
pub fn get_phrase(&self) -> String {
|
||||
self.words.iter().map(|x| x.to_string() + " ").collect::<String>()
|
||||
self.words.iter()
|
||||
.enumerate()
|
||||
.map(|(i, x)| if i == 0 { "" } else { " " }.to_owned() + &x.text)
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
/// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`].
|
||||
fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec<String> {
|
||||
/// Generate [`PhraseWord`] list based on provided [`PhraseMode`] and [`PhraseSize`].
|
||||
fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec<PhraseWord> {
|
||||
match mode {
|
||||
PhraseMode::Generate => {
|
||||
let mut rng = thread_rng();
|
||||
@@ -87,8 +107,14 @@ impl Mnemonic {
|
||||
}
|
||||
from_entropy(&entropy).unwrap()
|
||||
.split(" ")
|
||||
.map(|s| String::from(s))
|
||||
.collect::<Vec<String>>()
|
||||
.map(|s| {
|
||||
let text = s.to_string();
|
||||
PhraseWord {
|
||||
text,
|
||||
valid: true,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<PhraseWord>>()
|
||||
},
|
||||
PhraseMode::Import => {
|
||||
Self::empty_words(size)
|
||||
@@ -97,39 +123,117 @@ impl Mnemonic {
|
||||
}
|
||||
|
||||
/// Generate empty list of words based on provided [`PhraseSize`].
|
||||
fn empty_words(size: &PhraseSize) -> Vec<String> {
|
||||
fn empty_words(size: &PhraseSize) -> Vec<PhraseWord> {
|
||||
let mut words = Vec::with_capacity(size.value());
|
||||
for _ in 0..size.value() {
|
||||
words.push(String::from(""))
|
||||
words.push(PhraseWord {
|
||||
text: "".to_string(),
|
||||
valid: true,
|
||||
});
|
||||
}
|
||||
words
|
||||
}
|
||||
|
||||
/// Set words from provided text if possible.
|
||||
pub fn import_text(&mut self, text: &ZeroingString, confirmation: bool) {
|
||||
/// Insert word into provided index and return validation result.
|
||||
pub fn insert(&mut self, index: usize, word: &String) -> bool {
|
||||
// Check if word is valid.
|
||||
let found = search(word).is_ok();
|
||||
if !found {
|
||||
return false;
|
||||
}
|
||||
let is_confirmation = self.mode == PhraseMode::Generate;
|
||||
if is_confirmation {
|
||||
let w = self.words.get(index).unwrap();
|
||||
if word != &w.text {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Save valid word at list.
|
||||
let words = if is_confirmation {
|
||||
&mut self.confirmation
|
||||
} else {
|
||||
&mut self.words
|
||||
};
|
||||
words.remove(index);
|
||||
words.insert(index, PhraseWord { text: word.to_owned(), valid: true });
|
||||
|
||||
// Validate phrase when all words are entered.
|
||||
let mut has_empty = false;
|
||||
let _: Vec<_> = words.iter().map(|w| {
|
||||
if w.text.is_empty() {
|
||||
has_empty = true;
|
||||
}
|
||||
}).collect();
|
||||
if !has_empty {
|
||||
self.valid = to_entropy(self.get_phrase().as_str()).is_ok();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Get word from provided index.
|
||||
pub fn get(&self, index: usize) -> Option<PhraseWord> {
|
||||
let words = match self.mode {
|
||||
PhraseMode::Generate => &self.confirmation,
|
||||
PhraseMode::Import => &self.words
|
||||
};
|
||||
let word = words.get(index);
|
||||
if let Some(w) = word {
|
||||
return Some(PhraseWord {
|
||||
text: w.text.clone(),
|
||||
valid: w.valid
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Setup phrase from provided text if possible.
|
||||
pub fn import(&mut self, text: &ZeroingString) {
|
||||
let words_split = text.trim().split(" ");
|
||||
let count = words_split.clone().count();
|
||||
if let Some(size) = PhraseSize::type_for_value(count) {
|
||||
if !confirmation {
|
||||
// Setup phrase size.
|
||||
let confirm = self.mode == PhraseMode::Generate;
|
||||
if !confirm {
|
||||
self.size = size;
|
||||
} else if self.size != size {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup word list.
|
||||
let mut words = vec![];
|
||||
words_split.enumerate().for_each(|(i, word)| {
|
||||
if confirmation && !self.is_valid_word(&word.to_string(), i) {
|
||||
words = vec![];
|
||||
return;
|
||||
}
|
||||
words.insert(i, word.to_string())
|
||||
words_split.for_each(|w| {
|
||||
let mut text = w.to_string();
|
||||
text.retain(|c| c.is_alphabetic());
|
||||
let valid = search(&text).is_ok();
|
||||
words.push(PhraseWord { text, valid });
|
||||
});
|
||||
if confirmation {
|
||||
if !words.is_empty() {
|
||||
self.confirm_words = words;
|
||||
let mut has_invalid = false;
|
||||
for (i, w) in words.iter().enumerate() {
|
||||
if !self.insert(i, &w.text) {
|
||||
has_invalid = true;
|
||||
}
|
||||
} else {
|
||||
self.words = words;
|
||||
}
|
||||
self.valid = !has_invalid;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if phrase has invalid or empty words.
|
||||
pub fn has_empty_or_invalid(&self) -> bool {
|
||||
let words = match self.mode {
|
||||
PhraseMode::Generate => &self.confirmation,
|
||||
PhraseMode::Import => &self.words
|
||||
};
|
||||
let mut has_empty = false;
|
||||
let mut has_invalid = false;
|
||||
let _: Vec<_> = words.iter().map(|w| {
|
||||
if w.text.is_empty() {
|
||||
has_empty = true;
|
||||
}
|
||||
if !w.valid {
|
||||
has_invalid = true;
|
||||
}
|
||||
}).collect();
|
||||
has_empty || has_invalid
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,15 @@ use grin_util::Mutex;
|
||||
use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient};
|
||||
use grin_wallet_libwallet::{TxLogEntry, TxLogEntryType, WalletInfo, WalletInst};
|
||||
|
||||
/// Mnemonic phrase word.
|
||||
#[derive(Clone)]
|
||||
pub struct PhraseWord {
|
||||
/// Word text.
|
||||
pub text: String,
|
||||
/// Flag to check if word is valid.
|
||||
pub valid: bool,
|
||||
}
|
||||
|
||||
/// Mnemonic phrase setup mode.
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub enum PhraseMode {
|
||||
|
||||
+10
-10
@@ -46,7 +46,7 @@ use rand::Rng;
|
||||
use crate::AppConfig;
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::tor::Tor;
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection, WalletConfig};
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection, Mnemonic, WalletConfig};
|
||||
use crate::wallet::store::TxHeightStore;
|
||||
use crate::wallet::types::{ConnectionMethod, WalletAccount, WalletData, WalletInstance, WalletTransaction};
|
||||
|
||||
@@ -127,21 +127,21 @@ impl Wallet {
|
||||
|
||||
/// Create new wallet.
|
||||
pub fn create(
|
||||
name: String,
|
||||
password: String,
|
||||
mnemonic: String,
|
||||
name: &String,
|
||||
password: &String,
|
||||
mnemonic: &Mnemonic,
|
||||
conn_method: &ConnectionMethod
|
||||
) -> Result<Wallet, Error> {
|
||||
let mut config = WalletConfig::create(name, conn_method);
|
||||
let mut config = WalletConfig::create(name.clone(), conn_method);
|
||||
let w = Wallet::new(config.clone());
|
||||
{
|
||||
let instance = Self::create_wallet_instance(&mut config)?;
|
||||
let mut w_lock = instance.lock();
|
||||
let p = w_lock.lc_provider()?;
|
||||
p.create_wallet(None,
|
||||
Some(ZeroingString::from(mnemonic.clone())),
|
||||
mnemonic.len(),
|
||||
ZeroingString::from(password),
|
||||
Some(ZeroingString::from(mnemonic.get_phrase())),
|
||||
mnemonic.size().entropy_size(),
|
||||
ZeroingString::from(password.clone()),
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
@@ -312,7 +312,7 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Open the wallet and start [`WalletData`] sync at separate thread.
|
||||
pub fn open(&mut self, password: String) -> Result<(), Error> {
|
||||
pub fn open(&mut self, password: &String) -> Result<(), Error> {
|
||||
if self.is_open() {
|
||||
return Err(Error::GenericError("Already opened".to_string()));
|
||||
}
|
||||
@@ -334,7 +334,7 @@ impl Wallet {
|
||||
let instance = self.instance.clone().unwrap();
|
||||
let mut wallet_lock = instance.lock();
|
||||
let lc = wallet_lock.lc_provider()?;
|
||||
match lc.open_wallet(None, ZeroingString::from(password), false, false) {
|
||||
match lc.open_wallet(None, ZeroingString::from(password.clone()), false, false) {
|
||||
Ok(_) => {
|
||||
// Reset an error on opening.
|
||||
self.set_sync_error(false);
|
||||
|
||||
Reference in New Issue
Block a user