mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-26 10:46:23 -05:00
feat: Recover from / write down seed (#439)
* vendor: seed crate from monero-serai * refactor: make approval type generic over respone from frontend * progress * Raphaels Progress * feat(gui): seed import flow skeleton * fix(gui): Seed import in the development version * fix(gui): specify the imported seed type * remove initializeHandle, make tauri handle non optional in state, dont allow closing seed dialog * feat(gui): check if seed is valid dynamically * fix(gui): refine the dynamic seed validation * push * progress * progress * Fix pending trimeout --------- Co-authored-by: Maksim Kirillov <artist@eduroam-141-23-183-184.wlan.tu-berlin.de> Co-authored-by: Maksim Kirillov <artist@eduroam-141-23-189-144.wlan.tu-berlin.de> Co-authored-by: Maksim Kirillov <maksim.kirillov@staticlabs.de>
This commit is contained in:
parent
b8982b5ac2
commit
7606982de3
39 changed files with 22465 additions and 171 deletions
79
Cargo.lock
generated
79
Cargo.lock
generated
|
|
@ -2130,6 +2130,8 @@ dependencies = [
|
|||
"curve25519-dalek-derive",
|
||||
"digest 0.10.7",
|
||||
"fiat-crypto",
|
||||
"group",
|
||||
"rand_core 0.6.4",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
|
|
@ -2219,6 +2221,22 @@ dependencies = [
|
|||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dalek-ff-group"
|
||||
version = "0.4.1"
|
||||
source = "git+https://github.com/serai-dex/serai#dc1b8dfccd68b7c2eb4359a1e37b55ce5e4453b5"
|
||||
dependencies = [
|
||||
"crypto-bigint",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"digest 0.10.7",
|
||||
"ff",
|
||||
"group",
|
||||
"rand_core 0.6.4",
|
||||
"rustversion",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.13.4"
|
||||
|
|
@ -3147,6 +3165,7 @@ version = "0.13.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
|
@ -5929,6 +5948,20 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monero-generators"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/serai-dex/serai#dc1b8dfccd68b7c2eb4359a1e37b55ce5e4453b5"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.3",
|
||||
"dalek-ff-group",
|
||||
"group",
|
||||
"monero-io",
|
||||
"sha3",
|
||||
"std-shims",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monero-harness"
|
||||
version = "0.1.0"
|
||||
|
|
@ -5946,6 +5979,28 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monero-io"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/serai-dex/serai#dc1b8dfccd68b7c2eb4359a1e37b55ce5e4453b5"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.3",
|
||||
"std-shims",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monero-primitives"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/serai-dex/serai#dc1b8dfccd68b7c2eb4359a1e37b55ce5e4453b5"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.3",
|
||||
"monero-generators",
|
||||
"monero-io",
|
||||
"sha3",
|
||||
"std-shims",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monero-rpc"
|
||||
version = "0.1.0"
|
||||
|
|
@ -5994,6 +6049,19 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monero-seed"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.3",
|
||||
"hex",
|
||||
"monero-primitives",
|
||||
"rand_core 0.6.4",
|
||||
"std-shims",
|
||||
"thiserror 1.0.69",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monero-sys"
|
||||
version = "0.1.0"
|
||||
|
|
@ -9565,6 +9633,15 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "std-shims"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/serai-dex/serai#dc1b8dfccd68b7c2eb4359a1e37b55ce5e4453b5"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
|
|
@ -9741,6 +9818,7 @@ dependencies = [
|
|||
"monero-harness",
|
||||
"monero-rpc",
|
||||
"monero-rpc-pool",
|
||||
"monero-seed",
|
||||
"monero-sys",
|
||||
"once_cell",
|
||||
"pem",
|
||||
|
|
@ -9786,6 +9864,7 @@ dependencies = [
|
|||
"uuid",
|
||||
"vergen",
|
||||
"void",
|
||||
"zeroize",
|
||||
"zip 0.5.13",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["electrum-pool", "monero-rpc", "monero-rpc-pool", "monero-sys", "src-tauri", "swap"]
|
||||
members = ["electrum-pool", "monero-rpc", "monero-rpc-pool", "monero-sys", "seed", "src-tauri", "swap"]
|
||||
|
||||
[patch.crates-io]
|
||||
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
||||
|
|
@ -11,3 +11,6 @@ monero = { git = "https://github.com/comit-network/monero-rs", rev = "818f38b" }
|
|||
bdk_wallet = { git = "https://github.com/Einliterflasche/bdk", branch = "bump/rusqlite-0.32", package = "bdk_wallet" }
|
||||
bdk_electrum = { git = "https://github.com/Einliterflasche/bdk", branch = "bump/rusqlite-0.32", package = "bdk_electrum" }
|
||||
bdk_chain = { git = "https://github.com/Einliterflasche/bdk", branch = "bump/rusqlite-0.32", package = "bdk_chain" }
|
||||
|
||||
[workspace.lints]
|
||||
rust.unused_crate_dependencies = "warn"
|
||||
|
|
|
|||
41
seed/Cargo.toml
Normal file
41
seed/Cargo.toml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[package]
|
||||
name = "monero-seed"
|
||||
version = "0.1.0"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/kayabaNerve/monero-wallet-util/tree/develop/seed"
|
||||
rust-version = "1.80"
|
||||
description = "Rust implementation of Monero's seed algorithm"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
std-shims = { git = "https://github.com/serai-dex/serai", version = "^0.1.1", default-features = false }
|
||||
|
||||
thiserror = { version = "1", default-features = false, optional = true }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||
monero-primitives = { git = "https://github.com/serai-dex/serai", default-features = false, features = ["std"] }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"std-shims/std",
|
||||
|
||||
"thiserror",
|
||||
|
||||
"zeroize/std",
|
||||
"rand_core/std",
|
||||
]
|
||||
default = ["std"]
|
||||
21
seed/LICENSE
Normal file
21
seed/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022-2024 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
11
seed/README.md
Normal file
11
seed/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Monero Seeds
|
||||
|
||||
Rust implementation of Monero's seed algorithm.
|
||||
|
||||
This library is usable under no-std when the `std` feature (on by default) is
|
||||
disabled.
|
||||
|
||||
### Cargo Features
|
||||
|
||||
- `std` (on by default): Enables `std` (and with it, more efficient internal
|
||||
implementations).
|
||||
400
seed/src/lib.rs
Normal file
400
seed/src/lib.rs
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use core::{fmt, ops::Deref};
|
||||
use std_shims::{
|
||||
collections::HashMap,
|
||||
string::{String, ToString},
|
||||
sync::LazyLock,
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use rand_core::{CryptoRng, RngCore};
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
// The amount of words in a seed without a checksum.
|
||||
const SEED_LENGTH: usize = 24;
|
||||
// The amount of words in a seed with a checksum.
|
||||
const SEED_LENGTH_WITH_CHECKSUM: usize = 25;
|
||||
|
||||
/// An error when working with a seed.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||
pub enum SeedError {
|
||||
#[cfg_attr(feature = "std", error("invalid seed"))]
|
||||
/// The seed was invalid.
|
||||
InvalidSeed,
|
||||
/// The checksum did not match the data.
|
||||
#[cfg_attr(feature = "std", error("invalid checksum"))]
|
||||
InvalidChecksum,
|
||||
/// The deprecated English language option was used with a checksum.
|
||||
///
|
||||
/// The deprecated English language option did not include a checksum.
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
error("deprecated English language option included a checksum")
|
||||
)]
|
||||
DeprecatedEnglishWithChecksum,
|
||||
}
|
||||
|
||||
/// Language options.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Zeroize)]
|
||||
pub enum Language {
|
||||
/// Chinese language option.
|
||||
Chinese,
|
||||
/// English language option.
|
||||
English,
|
||||
/// Dutch language option.
|
||||
Dutch,
|
||||
/// French language option.
|
||||
French,
|
||||
/// Spanish language option.
|
||||
Spanish,
|
||||
/// German language option.
|
||||
German,
|
||||
/// Italian language option.
|
||||
Italian,
|
||||
/// Portuguese language option.
|
||||
Portuguese,
|
||||
/// Japanese language option.
|
||||
Japanese,
|
||||
/// Russian language option.
|
||||
Russian,
|
||||
/// Esperanto language option.
|
||||
Esperanto,
|
||||
/// Lojban language option.
|
||||
Lojban,
|
||||
/// The original, and deprecated, English language.
|
||||
DeprecatedEnglish,
|
||||
}
|
||||
|
||||
fn trim(word: &str, len: usize) -> Zeroizing<String> {
|
||||
Zeroizing::new(word.chars().take(len).collect())
|
||||
}
|
||||
|
||||
struct WordList {
|
||||
word_list: &'static [&'static str],
|
||||
word_map: HashMap<&'static str, usize>,
|
||||
trimmed_word_map: HashMap<String, usize>,
|
||||
unique_prefix_length: usize,
|
||||
}
|
||||
|
||||
impl WordList {
|
||||
fn new(word_list: &'static [&'static str], prefix_length: usize) -> WordList {
|
||||
let mut lang = WordList {
|
||||
word_list,
|
||||
word_map: HashMap::new(),
|
||||
trimmed_word_map: HashMap::new(),
|
||||
unique_prefix_length: prefix_length,
|
||||
};
|
||||
|
||||
for (i, word) in lang.word_list.iter().enumerate() {
|
||||
lang.word_map.insert(word, i);
|
||||
lang.trimmed_word_map
|
||||
.insert(trim(word, lang.unique_prefix_length).deref().clone(), i);
|
||||
}
|
||||
|
||||
lang
|
||||
}
|
||||
}
|
||||
|
||||
static LANGUAGES: LazyLock<HashMap<Language, WordList>> = LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
Language::Chinese,
|
||||
WordList::new(include!("./words/zh.rs"), 1),
|
||||
),
|
||||
(
|
||||
Language::English,
|
||||
WordList::new(include!("./words/en.rs"), 3),
|
||||
),
|
||||
(Language::Dutch, WordList::new(include!("./words/nl.rs"), 4)),
|
||||
(
|
||||
Language::French,
|
||||
WordList::new(include!("./words/fr.rs"), 4),
|
||||
),
|
||||
(
|
||||
Language::Spanish,
|
||||
WordList::new(include!("./words/es.rs"), 4),
|
||||
),
|
||||
(
|
||||
Language::German,
|
||||
WordList::new(include!("./words/de.rs"), 4),
|
||||
),
|
||||
(
|
||||
Language::Italian,
|
||||
WordList::new(include!("./words/it.rs"), 4),
|
||||
),
|
||||
(
|
||||
Language::Portuguese,
|
||||
WordList::new(include!("./words/pt.rs"), 4),
|
||||
),
|
||||
(
|
||||
Language::Japanese,
|
||||
WordList::new(include!("./words/ja.rs"), 3),
|
||||
),
|
||||
(
|
||||
Language::Russian,
|
||||
WordList::new(include!("./words/ru.rs"), 4),
|
||||
),
|
||||
(
|
||||
Language::Esperanto,
|
||||
WordList::new(include!("./words/eo.rs"), 4),
|
||||
),
|
||||
(
|
||||
Language::Lojban,
|
||||
WordList::new(include!("./words/jbo.rs"), 4),
|
||||
),
|
||||
(
|
||||
Language::DeprecatedEnglish,
|
||||
WordList::new(include!("./words/ang.rs"), 4),
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
fn checksum_index(words: &[Zeroizing<String>], lang: &WordList) -> usize {
|
||||
let mut trimmed_words = Zeroizing::new(String::new());
|
||||
for w in words {
|
||||
*trimmed_words += &trim(w, lang.unique_prefix_length);
|
||||
}
|
||||
|
||||
const fn crc32_table() -> [u32; 256] {
|
||||
let poly = 0xedb88320u32;
|
||||
|
||||
let mut res = [0; 256];
|
||||
let mut i = 0;
|
||||
while i < 256 {
|
||||
let mut entry = i;
|
||||
let mut b = 0;
|
||||
while b < 8 {
|
||||
let trigger = entry & 1;
|
||||
entry >>= 1;
|
||||
if trigger == 1 {
|
||||
entry ^= poly;
|
||||
}
|
||||
b += 1;
|
||||
}
|
||||
res[i as usize] = entry;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
const CRC32_TABLE: [u32; 256] = crc32_table();
|
||||
|
||||
let trimmed_words = trimmed_words.as_bytes();
|
||||
let mut checksum = u32::MAX;
|
||||
for i in 0..trimmed_words.len() {
|
||||
checksum = CRC32_TABLE
|
||||
[usize::from(u8::try_from(checksum % 256).unwrap() ^ trimmed_words[i])]
|
||||
^ (checksum >> 8);
|
||||
}
|
||||
|
||||
usize::try_from(!checksum).unwrap() % words.len()
|
||||
}
|
||||
|
||||
// Convert a private key to a seed
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> Seed {
|
||||
let bytes = Zeroizing::new(key.to_bytes());
|
||||
|
||||
// get the language words
|
||||
let words = &LANGUAGES[&lang].word_list;
|
||||
let list_len = u64::try_from(words.len()).unwrap();
|
||||
|
||||
// To store the found words & add the checksum word later.
|
||||
let mut seed = Vec::with_capacity(25);
|
||||
|
||||
// convert to words
|
||||
// 4 bytes -> 3 words. 8 digits base 16 -> 3 digits base 1626
|
||||
let mut segment = [0; 4];
|
||||
let mut indices = [0; 4];
|
||||
for i in 0..8 {
|
||||
// convert first 4 byte to u32 & get the word indices
|
||||
let start = i * 4;
|
||||
// convert 4 byte to u32
|
||||
segment.copy_from_slice(&bytes[start..(start + 4)]);
|
||||
// Actually convert to a u64 so we can add without overflowing
|
||||
indices[0] = u64::from(u32::from_le_bytes(segment));
|
||||
indices[1] = indices[0];
|
||||
indices[0] /= list_len;
|
||||
indices[2] = indices[0] + indices[1];
|
||||
indices[0] /= list_len;
|
||||
indices[3] = indices[0] + indices[2];
|
||||
|
||||
// append words to seed
|
||||
for i in indices.iter().skip(1) {
|
||||
let word = usize::try_from(i % list_len).unwrap();
|
||||
seed.push(Zeroizing::new(words[word].to_string()));
|
||||
}
|
||||
}
|
||||
segment.zeroize();
|
||||
indices.zeroize();
|
||||
|
||||
// create a checksum word for all languages except old english
|
||||
if lang != Language::DeprecatedEnglish {
|
||||
let checksum = seed[checksum_index(&seed, &LANGUAGES[&lang])].clone();
|
||||
seed.push(checksum);
|
||||
}
|
||||
|
||||
let mut res = Zeroizing::new(String::new());
|
||||
for (i, word) in seed.iter().enumerate() {
|
||||
if i != 0 {
|
||||
*res += " ";
|
||||
}
|
||||
*res += word;
|
||||
}
|
||||
Seed(lang, res)
|
||||
}
|
||||
|
||||
// Convert a seed to bytes
|
||||
fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, SeedError> {
|
||||
// get seed words
|
||||
let words = words
|
||||
.split_whitespace()
|
||||
.map(|w| Zeroizing::new(w.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
if (words.len() != SEED_LENGTH) && (words.len() != SEED_LENGTH_WITH_CHECKSUM) {
|
||||
panic!("invalid seed passed to seed_to_bytes");
|
||||
}
|
||||
|
||||
let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM;
|
||||
if has_checksum && lang == Language::DeprecatedEnglish {
|
||||
Err(SeedError::DeprecatedEnglishWithChecksum)?;
|
||||
}
|
||||
|
||||
// Validate words are in the language word list
|
||||
let lang_word_list: &WordList = &LANGUAGES[&lang];
|
||||
let matched_indices = (|| {
|
||||
let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM;
|
||||
let mut matched_indices = Zeroizing::new(vec![]);
|
||||
|
||||
// Iterate through all the words and see if they're all present
|
||||
for word in &words {
|
||||
let trimmed = trim(word, lang_word_list.unique_prefix_length);
|
||||
let word = if has_checksum { &trimmed } else { word };
|
||||
|
||||
if let Some(index) = if has_checksum {
|
||||
lang_word_list.trimmed_word_map.get(word.deref())
|
||||
} else {
|
||||
lang_word_list.word_map.get(&word.as_str())
|
||||
} {
|
||||
matched_indices.push(*index);
|
||||
} else {
|
||||
Err(SeedError::InvalidSeed)?;
|
||||
}
|
||||
}
|
||||
|
||||
if has_checksum {
|
||||
// exclude the last word when calculating a checksum.
|
||||
let last_word = words.last().unwrap().clone();
|
||||
let checksum = words[checksum_index(&words[..words.len() - 1], lang_word_list)].clone();
|
||||
|
||||
// check the trimmed checksum and trimmed last word line up
|
||||
if trim(&checksum, lang_word_list.unique_prefix_length)
|
||||
!= trim(&last_word, lang_word_list.unique_prefix_length)
|
||||
{
|
||||
Err(SeedError::InvalidChecksum)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(matched_indices)
|
||||
})()?;
|
||||
|
||||
// convert to bytes
|
||||
let mut res = Zeroizing::new([0; 32]);
|
||||
let mut indices = Zeroizing::new([0; 4]);
|
||||
for i in 0..8 {
|
||||
// read 3 indices at a time
|
||||
let i3 = i * 3;
|
||||
indices[1] = matched_indices[i3];
|
||||
indices[2] = matched_indices[i3 + 1];
|
||||
indices[3] = matched_indices[i3 + 2];
|
||||
|
||||
let inner = |i| {
|
||||
let mut base = (lang_word_list.word_list.len() - indices[i] + indices[i + 1])
|
||||
% lang_word_list.word_list.len();
|
||||
// Shift the index over
|
||||
for _ in 0..i {
|
||||
base *= lang_word_list.word_list.len();
|
||||
}
|
||||
base
|
||||
};
|
||||
// set the last index
|
||||
indices[0] = indices[1] + inner(1) + inner(2);
|
||||
if (indices[0] % lang_word_list.word_list.len()) != indices[1] {
|
||||
Err(SeedError::InvalidSeed)?;
|
||||
}
|
||||
|
||||
let pos = i * 4;
|
||||
let mut bytes = u32::try_from(indices[0]).unwrap().to_le_bytes();
|
||||
res[pos..(pos + 4)].copy_from_slice(&bytes);
|
||||
bytes.zeroize();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// A Monero seed.
|
||||
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
||||
pub struct Seed(Language, Zeroizing<String>);
|
||||
|
||||
impl fmt::Debug for Seed {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("Seed").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Seed {
|
||||
/// Create a new seed.
|
||||
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, lang: Language) -> Seed {
|
||||
let mut scalar_bytes = Zeroizing::new([0; 64]);
|
||||
rng.fill_bytes(scalar_bytes.as_mut());
|
||||
key_to_seed(
|
||||
lang,
|
||||
Zeroizing::new(Scalar::from_bytes_mod_order_wide(scalar_bytes.deref())),
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse a seed from a string.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn from_string(lang: Language, words: Zeroizing<String>) -> Result<Seed, SeedError> {
|
||||
let entropy = seed_to_bytes(lang, &words)?;
|
||||
|
||||
// Make sure this is a valid scalar
|
||||
let scalar = Scalar::from_canonical_bytes(*entropy);
|
||||
if scalar.is_none().into() {
|
||||
Err(SeedError::InvalidSeed)?;
|
||||
}
|
||||
let mut scalar = scalar.unwrap();
|
||||
scalar.zeroize();
|
||||
|
||||
// Call from_entropy so a trimmed seed becomes a full seed
|
||||
Ok(Self::from_entropy(lang, entropy).unwrap())
|
||||
}
|
||||
|
||||
/// Create a seed from entropy.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn from_entropy(lang: Language, entropy: Zeroizing<[u8; 32]>) -> Option<Seed> {
|
||||
Option::from(Scalar::from_canonical_bytes(*entropy))
|
||||
.map(|scalar| key_to_seed(lang, Zeroizing::new(scalar)))
|
||||
}
|
||||
|
||||
/// Convert a seed to a string.
|
||||
pub fn to_string(&self) -> Zeroizing<String> {
|
||||
self.1.clone()
|
||||
}
|
||||
|
||||
/// Return the entropy underlying this seed.
|
||||
pub fn entropy(&self) -> Zeroizing<[u8; 32]> {
|
||||
seed_to_bytes(self.0, &self.1).unwrap()
|
||||
}
|
||||
}
|
||||
257
seed/src/tests.rs
Normal file
257
seed/src/tests.rs
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
use rand_core::OsRng;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
|
||||
use monero_primitives::keccak256;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
fn test_original_seed() {
|
||||
struct Vector {
|
||||
language: Language,
|
||||
seed: String,
|
||||
spend: String,
|
||||
view: String,
|
||||
}
|
||||
|
||||
let vectors = [
|
||||
Vector {
|
||||
language: Language::Chinese,
|
||||
seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武"
|
||||
.into(),
|
||||
spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(),
|
||||
view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::English,
|
||||
seed: "washing thirsty occur lectures tuesday fainted toxic adapt \
|
||||
abnormal memoir nylon mostly building shrugged online ember northern \
|
||||
ruby woes dauntless boil family illness inroads northern"
|
||||
.into(),
|
||||
spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(),
|
||||
view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Dutch,
|
||||
seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \
|
||||
ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \
|
||||
wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst"
|
||||
.into(),
|
||||
spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(),
|
||||
view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::French,
|
||||
seed: "poids vaseux tarte bazar poivre effet entier nuance \
|
||||
sensuel ennui pacte osselet poudre battre alibi mouton \
|
||||
stade paquet pliage gibier type question position projet pliage"
|
||||
.into(),
|
||||
spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(),
|
||||
view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Spanish,
|
||||
seed: "minero ocupar mirar evadir octubre cal logro miope \
|
||||
opaco disco ancla litio clase cuello nasal clase \
|
||||
fiar avance deseo mente grumo negro cordón croqueta clase"
|
||||
.into(),
|
||||
spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(),
|
||||
view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::German,
|
||||
seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \
|
||||
Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \
|
||||
Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide"
|
||||
.into(),
|
||||
spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(),
|
||||
view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Italian,
|
||||
seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \
|
||||
forzare meritare litigare lezione segreto evasione votare buio \
|
||||
licenza cliente dorso natale crescere vento tutelare vetta evasione"
|
||||
.into(),
|
||||
spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(),
|
||||
view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Portuguese,
|
||||
seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \
|
||||
iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \
|
||||
cibernetico hoquei gleba driver buffer azoto megera nogueira agito"
|
||||
.into(),
|
||||
spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(),
|
||||
view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Japanese,
|
||||
seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \
|
||||
かほう つかれる えらぶ にちじょう くのう にちようび ぬまえび さんきゃく \
|
||||
おおや ちぬき うすめる いがく せつでん さうな すいえい せつだん おおや"
|
||||
.into(),
|
||||
spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(),
|
||||
view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Russian,
|
||||
seed: "шатер икра нация ехать получать инерция доза реальный \
|
||||
рыжий таможня лопата душа веселый клетка атлас лекция \
|
||||
обгонять паек наивный лыжный дурак стать ежик задача паек"
|
||||
.into(),
|
||||
spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(),
|
||||
view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Esperanto,
|
||||
seed: "ukazo klini peco etikedo fabriko imitado onklino urino \
|
||||
pudro incidento kumuluso ikono smirgi hirundo uretro krii \
|
||||
sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko"
|
||||
.into(),
|
||||
spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(),
|
||||
view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Lojban,
|
||||
seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \
|
||||
mlatu xedja muvgau palpi xindo sfubu ciste cinri \
|
||||
blabi darno dembi janli blabi fenki bukpu burcu blabi"
|
||||
.into(),
|
||||
spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(),
|
||||
view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::DeprecatedEnglish,
|
||||
seed: "glorious especially puff son moment add youth nowhere \
|
||||
throw glide grip wrong rhythm consume very swear \
|
||||
bitter heavy eventually begin reason flirt type unable"
|
||||
.into(),
|
||||
spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(),
|
||||
view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(),
|
||||
},
|
||||
// The following seeds require the language specification in order to calculate
|
||||
// a single valid checksum
|
||||
Vector {
|
||||
language: Language::Spanish,
|
||||
seed: "pluma laico atraer pintor peor cerca balde buscar \
|
||||
lancha batir nulo reloj resto gemelo nevera poder columna gol \
|
||||
oveja latir amplio bolero feliz fuerza nevera"
|
||||
.into(),
|
||||
spend: "30303983fc8d215dd020cc6b8223793318d55c466a86e4390954f373fdc7200a".into(),
|
||||
view: "97c649143f3c147ba59aa5506cc09c7992c5c219bb26964442142bf97980800e".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Spanish,
|
||||
seed: "pluma pluma pluma pluma pluma pluma pluma pluma \
|
||||
pluma pluma pluma pluma pluma pluma pluma pluma \
|
||||
pluma pluma pluma pluma pluma pluma pluma pluma pluma"
|
||||
.into(),
|
||||
spend: "b4050000b4050000b4050000b4050000b4050000b4050000b4050000b4050000".into(),
|
||||
view: "d73534f7912b395eb70ef911791a2814eb6df7ce56528eaaa83ff2b72d9f5e0f".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::English,
|
||||
seed: "plus plus plus plus plus plus plus plus \
|
||||
plus plus plus plus plus plus plus plus \
|
||||
plus plus plus plus plus plus plus plus plus"
|
||||
.into(),
|
||||
spend: "3b0400003b0400003b0400003b0400003b0400003b0400003b0400003b040000".into(),
|
||||
view: "43a8a7715eed11eff145a2024ddcc39740255156da7bbd736ee66a0838053a02".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::Spanish,
|
||||
seed: "audio audio audio audio audio audio audio audio \
|
||||
audio audio audio audio audio audio audio audio \
|
||||
audio audio audio audio audio audio audio audio audio"
|
||||
.into(),
|
||||
spend: "ba000000ba000000ba000000ba000000ba000000ba000000ba000000ba000000".into(),
|
||||
view: "1437256da2c85d029b293d8c6b1d625d9374969301869b12f37186e3f906c708".into(),
|
||||
},
|
||||
Vector {
|
||||
language: Language::English,
|
||||
seed: "audio audio audio audio audio audio audio audio \
|
||||
audio audio audio audio audio audio audio audio \
|
||||
audio audio audio audio audio audio audio audio audio"
|
||||
.into(),
|
||||
spend: "7900000079000000790000007900000079000000790000007900000079000000".into(),
|
||||
view: "20bec797ab96780ae6a045dd816676ca7ed1d7c6773f7022d03ad234b581d600".into(),
|
||||
},
|
||||
];
|
||||
|
||||
for vector in vectors {
|
||||
fn trim_by_lang(word: &str, lang: Language) -> String {
|
||||
if lang != Language::DeprecatedEnglish {
|
||||
word.chars()
|
||||
.take(LANGUAGES[&lang].unique_prefix_length)
|
||||
.collect()
|
||||
} else {
|
||||
word.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
let trim_seed = |seed: &str| {
|
||||
seed.split_whitespace()
|
||||
.map(|word| trim_by_lang(word, vector.language))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
};
|
||||
|
||||
// Test against Monero
|
||||
{
|
||||
println!(
|
||||
"{}. language: {:?}, seed: {}",
|
||||
line!(),
|
||||
vector.language,
|
||||
vector.seed.clone()
|
||||
);
|
||||
let seed =
|
||||
Seed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap();
|
||||
let trim = trim_seed(&vector.seed);
|
||||
assert_eq!(
|
||||
seed,
|
||||
Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap()
|
||||
);
|
||||
|
||||
let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap();
|
||||
// For originalal seeds, Monero directly uses the entropy as a spend key
|
||||
assert_eq!(
|
||||
Option::<Scalar>::from(Scalar::from_canonical_bytes(*seed.entropy())),
|
||||
Option::<Scalar>::from(Scalar::from_canonical_bytes(spend)),
|
||||
);
|
||||
|
||||
let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap();
|
||||
// Monero then derives the view key as H(spend)
|
||||
assert_eq!(
|
||||
Scalar::from_bytes_mod_order(keccak256(spend)),
|
||||
Scalar::from_canonical_bytes(view).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Seed::from_entropy(vector.language, Zeroizing::new(spend)).unwrap(),
|
||||
seed
|
||||
);
|
||||
}
|
||||
|
||||
// Test against ourselves
|
||||
{
|
||||
let seed = Seed::new(&mut OsRng, vector.language);
|
||||
println!("{}. seed: {}", line!(), *seed.to_string());
|
||||
let trim = trim_seed(&seed.to_string());
|
||||
assert_eq!(
|
||||
seed,
|
||||
Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
seed,
|
||||
Seed::from_entropy(vector.language, seed.entropy()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
seed,
|
||||
Seed::from_string(vector.language, seed.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
1628
seed/src/words/ang.rs
Normal file
1628
seed/src/words/ang.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/de.rs
Normal file
1628
seed/src/words/de.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/en.rs
Normal file
1628
seed/src/words/en.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/eo.rs
Normal file
1628
seed/src/words/eo.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/es.rs
Normal file
1628
seed/src/words/es.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/fr.rs
Normal file
1628
seed/src/words/fr.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/it.rs
Normal file
1628
seed/src/words/it.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/ja.rs
Normal file
1628
seed/src/words/ja.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/jbo.rs
Normal file
1628
seed/src/words/jbo.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/nl.rs
Normal file
1628
seed/src/words/nl.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/pt.rs
Normal file
1628
seed/src/words/pt.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/ru.rs
Normal file
1628
seed/src/words/ru.rs
Normal file
File diff suppressed because it is too large
Load diff
1628
seed/src/words/zh.rs
Normal file
1628
seed/src/words/zh.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -253,27 +253,38 @@ export function isGetSwapInfoResponseWithTimelock(
|
|||
return response.timelock !== null;
|
||||
}
|
||||
|
||||
export type PendingApprovalRequest = Extract<
|
||||
ApprovalRequest,
|
||||
{ state: "Pending" }
|
||||
>;
|
||||
|
||||
export type PendingLockBitcoinApprovalRequest = PendingApprovalRequest & {
|
||||
content: {
|
||||
details: { type: "LockBitcoin" };
|
||||
export type PendingApprovalRequest = ApprovalRequest & {
|
||||
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
|
||||
};
|
||||
|
||||
export type PendingLockBitcoinApprovalRequest = ApprovalRequest & {
|
||||
request: Extract<ApprovalRequest["request"], { type: "LockBitcoin" }>;
|
||||
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
|
||||
};
|
||||
|
||||
export type PendingSeedSelectionApprovalRequest = ApprovalRequest & {
|
||||
type: "SeedSelection";
|
||||
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
|
||||
};
|
||||
|
||||
export function isPendingLockBitcoinApprovalEvent(
|
||||
event: ApprovalRequest,
|
||||
): event is PendingLockBitcoinApprovalRequest {
|
||||
// Check if the request is pending
|
||||
if (event.state !== "Pending") {
|
||||
return false;
|
||||
// Check if the request is a LockBitcoin request and is pending
|
||||
return (
|
||||
event.request.type === "LockBitcoin" &&
|
||||
event.request_status.state === "Pending"
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the request is a LockBitcoin request
|
||||
return event.content.details.type === "LockBitcoin";
|
||||
export function isPendingSeedSelectionApprovalEvent(
|
||||
event: ApprovalRequest,
|
||||
): event is PendingSeedSelectionApprovalRequest {
|
||||
// Check if the request is a SeedSelection request and is pending
|
||||
return (
|
||||
event.request.type === "SeedSelection" &&
|
||||
event.request_status.state === "Pending"
|
||||
);
|
||||
}
|
||||
|
||||
export function isPendingBackgroundProcess(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { setupBackgroundTasks } from "renderer/background";
|
|||
import "@fontsource/roboto";
|
||||
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
||||
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
||||
import SeedSelectionDialog from "./modal/seed-selection/SeedSelectionDialog";
|
||||
|
||||
declare module "@mui/material/styles" {
|
||||
interface Theme {
|
||||
|
|
@ -46,6 +47,7 @@ export default function App() {
|
|||
<CssBaseline />
|
||||
<GlobalSnackbarProvider>
|
||||
<IntroductionModal />
|
||||
<SeedSelectionDialog />
|
||||
<Router>
|
||||
<Navigation />
|
||||
<InnerContent />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePendingSeedSelectionApproval } from "store/hooks";
|
||||
import { resolveApproval, checkSeed } from "renderer/rpc";
|
||||
|
||||
export default function SeedSelectionDialog() {
|
||||
const pendingApprovals = usePendingSeedSelectionApproval();
|
||||
const [selectedOption, setSelectedOption] = useState<string>("RandomSeed");
|
||||
const [customSeed, setCustomSeed] = useState<string>("");
|
||||
const [isSeedValid, setIsSeedValid] = useState<boolean>(false);
|
||||
const approval = pendingApprovals[0]; // Handle the first pending approval
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOption === "FromSeed" && customSeed.trim()) {
|
||||
checkSeed(customSeed.trim())
|
||||
.then((valid) => {
|
||||
setIsSeedValid(valid);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSeedValid(false);
|
||||
});
|
||||
} else {
|
||||
setIsSeedValid(false);
|
||||
}
|
||||
}, [customSeed, selectedOption]);
|
||||
|
||||
const handleClose = async (accept: boolean) => {
|
||||
if (!approval) return;
|
||||
|
||||
if (accept) {
|
||||
const seedChoice =
|
||||
selectedOption === "RandomSeed"
|
||||
? { type: "RandomSeed" }
|
||||
: { type: "FromSeed", content: { seed: customSeed } };
|
||||
|
||||
await resolveApproval(approval.request_id, seedChoice);
|
||||
} else {
|
||||
// On reject, just close without approval
|
||||
await resolveApproval(approval.request_id, { type: "RandomSeed" });
|
||||
}
|
||||
};
|
||||
|
||||
if (!approval) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={true} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Monero Wallet</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
Choose what seed to use for the wallet.
|
||||
</Typography>
|
||||
|
||||
<FormControl component="fieldset">
|
||||
<RadioGroup
|
||||
value={selectedOption}
|
||||
onChange={(e) => setSelectedOption(e.target.value)}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="RandomSeed"
|
||||
control={<Radio />}
|
||||
label="Create a new wallet"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="FromSeed"
|
||||
control={<Radio />}
|
||||
label="Restore wallet from seed"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
{selectedOption === "FromSeed" && (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="Enter your seed phrase"
|
||||
value={customSeed}
|
||||
onChange={(e) => setCustomSeed(e.target.value)}
|
||||
sx={{ mt: 2 }}
|
||||
placeholder="Enter your Monero 25 words seed phrase..."
|
||||
error={!isSeedValid && customSeed.length > 0}
|
||||
helperText={
|
||||
isSeedValid
|
||||
? "Seed is valid"
|
||||
: customSeed.length > 0
|
||||
? "Seed is invalid"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => handleClose(true)}
|
||||
variant="contained"
|
||||
disabled={
|
||||
selectedOption === "FromSeed"
|
||||
? !customSeed.trim() || !isSeedValid
|
||||
: false
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,9 +20,7 @@ function useActiveLockBitcoinApprovalRequest(): PendingLockBitcoinApprovalReques
|
|||
const activeSwapId = useActiveSwapId();
|
||||
|
||||
return (
|
||||
approvals?.find(
|
||||
(r) => r.content.details.content.swap_id === activeSwapId,
|
||||
) || null
|
||||
approvals?.find((r) => r.request.content.swap_id === activeSwapId) || null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -32,10 +30,18 @@ export default function SwapSetupInflightPage({
|
|||
const request = useActiveLockBitcoinApprovalRequest();
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||
|
||||
const expiresAtMs = request?.content.expiration_ts * 1000 || 0;
|
||||
const expirationTs =
|
||||
request?.request_status.state === "Pending"
|
||||
? request.request_status.content.expiration_ts
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (expirationTs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresAtMs = expirationTs * 1000 || 0;
|
||||
|
||||
const tick = () => {
|
||||
const remainingMs = Math.max(expiresAtMs - Date.now(), 0);
|
||||
setTimeLeft(Math.ceil(remainingMs / 1000));
|
||||
|
|
@ -44,11 +50,11 @@ export default function SwapSetupInflightPage({
|
|||
tick();
|
||||
const id = setInterval(tick, 250);
|
||||
return () => clearInterval(id);
|
||||
}, [expiresAtMs]);
|
||||
}, [request, expirationTs]);
|
||||
|
||||
// If we do not have an approval request yet for the Bitcoin lock transaction, we haven't received the offer from Alice yet
|
||||
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
||||
if (!request) {
|
||||
if (request == null) {
|
||||
return (
|
||||
<CircularProgressWithSubtitle
|
||||
description={
|
||||
|
|
@ -61,7 +67,7 @@ export default function SwapSetupInflightPage({
|
|||
}
|
||||
|
||||
const { btc_network_fee, monero_receive_pool, xmr_receive_amount } =
|
||||
request.content.details.content;
|
||||
request.request.content;
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
|
@ -124,7 +130,9 @@ export default function SwapSetupInflightPage({
|
|||
variant="text"
|
||||
size="large"
|
||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||
onInvoke={() => resolveApproval(request.content.request_id, false)}
|
||||
onInvoke={() =>
|
||||
resolveApproval(request.request_id, false as unknown as object)
|
||||
}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
>
|
||||
|
|
@ -135,7 +143,9 @@ export default function SwapSetupInflightPage({
|
|||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
onInvoke={() => resolveApproval(request.content.request_id, true)}
|
||||
onInvoke={() =>
|
||||
resolveApproval(request.request_id, true as unknown as object)
|
||||
}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
endIcon={<CheckIcon />}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import {
|
|||
GetSwapInfoArgs,
|
||||
ExportBitcoinWalletResponse,
|
||||
CheckMoneroNodeArgs,
|
||||
CheckSeedArgs,
|
||||
CheckSeedResponse,
|
||||
CheckMoneroNodeResponse,
|
||||
TauriSettings,
|
||||
CheckElectrumNodeArgs,
|
||||
|
|
@ -304,10 +306,16 @@ export async function initializeContext() {
|
|||
|
||||
logger.info("Initializing context with settings", tauriSettings);
|
||||
|
||||
try {
|
||||
await invokeUnsafe<void>("initialize_context", {
|
||||
settings: tauriSettings,
|
||||
testnet,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error("Couldn't initialize context: " + error);
|
||||
}
|
||||
|
||||
logger.info("Initialized context");
|
||||
}
|
||||
|
||||
export async function getWalletDescriptor() {
|
||||
|
|
@ -395,7 +403,7 @@ export async function getDataDir(): Promise<string> {
|
|||
|
||||
export async function resolveApproval(
|
||||
requestId: string,
|
||||
accept: boolean,
|
||||
accept: object,
|
||||
): Promise<void> {
|
||||
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
|
||||
"resolve_approval_request",
|
||||
|
|
@ -403,6 +411,16 @@ export async function resolveApproval(
|
|||
);
|
||||
}
|
||||
|
||||
export async function checkSeed(seed: string): Promise<boolean> {
|
||||
const response = await invoke<CheckSeedArgs, CheckSeedResponse>(
|
||||
"check_seed",
|
||||
{
|
||||
seed,
|
||||
},
|
||||
);
|
||||
return response.available;
|
||||
}
|
||||
|
||||
export async function saveLogFiles(
|
||||
zipFileName: string,
|
||||
content: Record<string, string>,
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export const rpcSlice = createSlice({
|
|||
},
|
||||
approvalEventReceived(slice, action: PayloadAction<ApprovalRequest>) {
|
||||
const event = action.payload;
|
||||
const requestId = event.content.request_id;
|
||||
const requestId = event.request_id;
|
||||
slice.state.approvalRequests[requestId] = event;
|
||||
},
|
||||
backgroundProgressEventReceived(
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import {
|
|||
isBitcoinSyncProgress,
|
||||
isPendingBackgroundProcess,
|
||||
isPendingLockBitcoinApprovalEvent,
|
||||
isPendingSeedSelectionApprovalEvent,
|
||||
PendingApprovalRequest,
|
||||
PendingLockBitcoinApprovalRequest,
|
||||
PendingSeedSelectionApprovalRequest,
|
||||
} from "models/tauriModelExt";
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
||||
|
|
@ -155,7 +157,9 @@ export function useNodes<T>(selector: (nodes: NodesSlice) => T): T {
|
|||
|
||||
export function usePendingApprovals(): PendingApprovalRequest[] {
|
||||
const approvals = useAppSelector((state) => state.rpc.state.approvalRequests);
|
||||
return Object.values(approvals).filter((c) => c.state === "Pending");
|
||||
return Object.values(approvals).filter(
|
||||
(c) => c.request_status.state === "Pending",
|
||||
) as PendingApprovalRequest[];
|
||||
}
|
||||
|
||||
export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] {
|
||||
|
|
@ -163,6 +167,11 @@ export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalReque
|
|||
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
|
||||
}
|
||||
|
||||
export function usePendingSeedSelectionApproval(): PendingSeedSelectionApprovalRequest[] {
|
||||
const approvals = usePendingApprovals();
|
||||
return approvals.filter((c) => isPendingSeedSelectionApprovalEvent(c));
|
||||
}
|
||||
|
||||
/// Returns all the pending background processes
|
||||
/// In the format [id, {componentName, {type: "Pending", content: {consumed, total}}}]
|
||||
export function usePendingBackgroundProcesses(): [
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ use swap::cli::{
|
|||
data,
|
||||
request::{
|
||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
||||
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse,
|
||||
ExportBitcoinWalletArgs, GetDataDirArgs, GetHistoryArgs, GetLogsArgs,
|
||||
GetMoneroAddressesArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs,
|
||||
MoneroRecoveryArgs, RedactArgs, ResolveApprovalArgs, ResumeSwapArgs,
|
||||
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
||||
CheckSeedResponse, ExportBitcoinWalletArgs, GetDataDirArgs, GetHistoryArgs,
|
||||
GetLogsArgs, GetMoneroAddressesArgs, GetSwapInfoArgs, GetSwapInfosAllArgs,
|
||||
ListSellersArgs, MoneroRecoveryArgs, RedactArgs, ResolveApprovalArgs, ResumeSwapArgs,
|
||||
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||
},
|
||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||
|
|
@ -93,12 +93,16 @@ macro_rules! tauri_command {
|
|||
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
||||
struct State {
|
||||
pub context: Option<Arc<Context>>,
|
||||
pub handle: TauriHandle,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new State instance with no Context
|
||||
fn new() -> Self {
|
||||
Self { context: None }
|
||||
fn new(handle: TauriHandle) -> Self {
|
||||
Self {
|
||||
context: None,
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the context for the application state
|
||||
|
|
@ -141,7 +145,8 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
// We need to set a value for the Tauri state right at the start
|
||||
// If we don't do this, Tauri commands will panic at runtime if no value is present
|
||||
let state = RwLock::new(State::new());
|
||||
let handle = TauriHandle::new(app_handle.clone());
|
||||
let state = RwLock::new(State::new(handle));
|
||||
app_handle.manage::<RwLock<State>>(state);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -194,6 +199,7 @@ pub fn run() {
|
|||
resolve_approval_request,
|
||||
redact,
|
||||
save_txt_files,
|
||||
check_seed,
|
||||
])
|
||||
.setup(setup)
|
||||
.build(tauri::generate_context!())
|
||||
|
|
@ -234,7 +240,6 @@ tauri_command!(monero_recovery, MoneroRecoveryArgs);
|
|||
tauri_command!(get_logs, GetLogsArgs);
|
||||
tauri_command!(list_sellers, ListSellersArgs);
|
||||
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
||||
tauri_command!(resolve_approval_request, ResolveApprovalArgs);
|
||||
tauri_command!(redact, RedactArgs);
|
||||
|
||||
// These commands require no arguments
|
||||
|
|
@ -268,6 +273,14 @@ async fn check_electrum_node(
|
|||
args.request().await.to_string_result()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn check_seed(
|
||||
args: CheckSeedArgs,
|
||||
_: tauri::State<'_, RwLock<State>>,
|
||||
) -> Result<CheckSeedResponse, String> {
|
||||
args.request().await.to_string_result()
|
||||
}
|
||||
|
||||
// Returns the data directory
|
||||
// This is independent of the context to ensure the user can open the directory even if the context cannot
|
||||
// be initialized (for troubleshooting purposes)
|
||||
|
|
@ -327,6 +340,23 @@ async fn save_txt_files(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn resolve_approval_request(
|
||||
args: ResolveApprovalArgs,
|
||||
state: tauri::State<'_, RwLock<State>>,
|
||||
) -> Result<(), String> {
|
||||
println!("Resolving approval request");
|
||||
let lock = state.read().await;
|
||||
|
||||
lock.handle
|
||||
.resolve_approval(args.request_id.parse().unwrap(), args.accept)
|
||||
.await
|
||||
.to_string_result()?;
|
||||
println!("Resolved approval request");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tauri command to initialize the Context
|
||||
#[tauri::command]
|
||||
async fn initialize_context(
|
||||
|
|
@ -361,14 +391,13 @@ async fn initialize_context(
|
|||
}
|
||||
}
|
||||
|
||||
// Acquire a write lock on the state
|
||||
let mut state_write_lock = state
|
||||
.try_write()
|
||||
.context("Context is already being initialized")
|
||||
.to_string_result()?;
|
||||
|
||||
// Get app handle and create a Tauri handle
|
||||
let tauri_handle = TauriHandle::new(app_handle.clone());
|
||||
let tauri_handle = state
|
||||
.try_read()
|
||||
.context("Context is already being initialized")
|
||||
.to_string_result()?
|
||||
.handle
|
||||
.clone();
|
||||
|
||||
// Notify frontend that the context is being initialized
|
||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Initializing);
|
||||
|
|
@ -388,7 +417,11 @@ async fn initialize_context(
|
|||
|
||||
match context_result {
|
||||
Ok(context_instance) => {
|
||||
state_write_lock.set_context(Arc::new(context_instance));
|
||||
state
|
||||
.try_write()
|
||||
.context("Context is already being initialized")
|
||||
.to_string_result()?
|
||||
.set_context(Arc::new(context_instance));
|
||||
|
||||
tracing::info!("Context initialized");
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ moka = { version = "0.12", features = ["sync", "future"] }
|
|||
monero = { version = "0.12", features = ["serde_support"] }
|
||||
monero-rpc = { path = "../monero-rpc" }
|
||||
monero-rpc-pool = { path = "../monero-rpc-pool" }
|
||||
monero-seed = { version = "0.1.0", path = "../seed" }
|
||||
monero-sys = { path = "../monero-sys" }
|
||||
once_cell = "1.19"
|
||||
pem = "3.0"
|
||||
|
|
@ -86,6 +87,7 @@ unsigned-varint = { version = "0.8.0", features = ["codec", "asynchronous_codec"
|
|||
url = { version = "2", features = ["serde"] }
|
||||
uuid = { version = "1.9", features = ["serde", "v4"] }
|
||||
void = "1"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
tokio-tar = "0.3"
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ mod addr_list {
|
|||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = Value::deserialize(deserializer)?;
|
||||
return match s {
|
||||
match s {
|
||||
Value::String(s) => {
|
||||
let list: Result<Vec<_>, _> = s
|
||||
.split(',')
|
||||
|
|
@ -173,7 +173,7 @@ mod addr_list {
|
|||
Unexpected::Other(&value.to_string()),
|
||||
&"a string or array",
|
||||
)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ mod electrum_urls {
|
|||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = Value::deserialize(deserializer)?;
|
||||
return match s {
|
||||
match s {
|
||||
Value::String(s) => {
|
||||
let list: Result<Vec<_>, _> = s
|
||||
.split(',')
|
||||
|
|
@ -214,7 +214,7 @@ mod electrum_urls {
|
|||
Unexpected::Other(&value.to_string()),
|
||||
&"a string or array",
|
||||
)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -378,15 +378,19 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
|||
let prompt = format!(
|
||||
"Enter additional Electrum RPC URL ({electrum_number}). Or just hit Enter to continue."
|
||||
);
|
||||
let electrum_url = Input::<Url>::with_theme(&ColorfulTheme::default())
|
||||
let electrum_url = Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
if electrum_url.as_str().is_empty() {
|
||||
electrum_done = true;
|
||||
} else if electrum_rpc_urls.contains(&electrum_url) {
|
||||
} else if electrum_rpc_urls
|
||||
.iter()
|
||||
.any(|url| url.to_string() == electrum_url)
|
||||
{
|
||||
println!("That Electrum URL is already in the list.");
|
||||
} else {
|
||||
let electrum_url = Url::parse(&electrum_url).context("Invalid Electrum URL")?;
|
||||
electrum_rpc_urls.push(electrum_url);
|
||||
electrum_number += 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,8 +132,9 @@ pub async fn main() -> Result<()> {
|
|||
));
|
||||
}
|
||||
|
||||
let seed =
|
||||
Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed");
|
||||
let seed = Seed::from_file_or_generate(&config.data.dir, None)
|
||||
.await
|
||||
.expect("Could not retrieve/initialize seed");
|
||||
|
||||
let db_file = config.data.dir.join("sqlite");
|
||||
|
||||
|
|
|
|||
|
|
@ -952,7 +952,7 @@ impl Wallet {
|
|||
//
|
||||
// At least one chunk is always required. At most total_spks / batch_size or the provided num_chunks (whichever is smaller)
|
||||
let num_chunks = max_num_chunks.min(total_spks / batch_size).max(1);
|
||||
let chunk_size = (total_spks + num_chunks - 1) / num_chunks;
|
||||
let chunk_size = total_spks.div_ceil(num_chunks);
|
||||
|
||||
let mut chunks = Vec::new();
|
||||
|
||||
|
|
@ -2208,10 +2208,7 @@ mod sync_ext {
|
|||
///
|
||||
/// Ensures the callback is always invoked when progress reaches 100%.
|
||||
fn throttle_callback(self, min_percentage_increase: f32) -> InnerSyncCallback {
|
||||
let mut callback = match self {
|
||||
None => return None,
|
||||
Some(cb) => cb,
|
||||
};
|
||||
let mut callback = self?;
|
||||
|
||||
let mut last_reported_percentage: f64 = 0.0;
|
||||
let threshold = min_percentage_increase as f64 / 100.0;
|
||||
|
|
|
|||
|
|
@ -281,11 +281,7 @@ impl ContextBuilder {
|
|||
|
||||
/// Takes the builder, initializes the context by initializing the wallets and other components and returns the Context.
|
||||
pub async fn build(self) -> Result<Context> {
|
||||
// These are needed for everything else, and are blocking calls
|
||||
let data_dir = &data::data_dir_from(self.data, self.is_testnet)?;
|
||||
let env_config = env_config_from(self.is_testnet);
|
||||
let seed = &Seed::from_file_or_generate(data_dir.as_path())
|
||||
.context("Failed to read seed in file")?;
|
||||
|
||||
// Initialize logging
|
||||
let format = if self.json { Format::Json } else { Format::Raw };
|
||||
|
|
@ -312,6 +308,12 @@ impl ContextBuilder {
|
|||
);
|
||||
});
|
||||
|
||||
// These are needed for everything else, and are blocking calls
|
||||
let env_config = env_config_from(self.is_testnet);
|
||||
let seed = &Seed::from_file_or_generate(data_dir.as_path(), self.tauri_handle.clone())
|
||||
.await
|
||||
.context("Failed to read seed in file")?;
|
||||
|
||||
// Create the data structure we use to manage the swap lock
|
||||
let swap_lock = Arc::new(SwapLock::new());
|
||||
let tasks = PendingTaskList::default().into();
|
||||
|
|
@ -698,14 +700,16 @@ pub mod api_test {
|
|||
pub const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b";
|
||||
|
||||
impl Config {
|
||||
pub fn default(
|
||||
pub async fn default(
|
||||
is_testnet: bool,
|
||||
data_dir: Option<PathBuf>,
|
||||
debug: bool,
|
||||
json: bool,
|
||||
) -> Self {
|
||||
let data_dir = data::data_dir_from(data_dir, is_testnet).unwrap();
|
||||
let seed = Seed::from_file_or_generate(data_dir.as_path()).unwrap();
|
||||
let seed = Seed::from_file_or_generate(data_dir.as_path(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let env_config = env_config_from(is_testnet);
|
||||
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use ::monero::Network;
|
|||
use anyhow::{bail, Context as AnyContext, Result};
|
||||
use libp2p::core::Multiaddr;
|
||||
use libp2p::PeerId;
|
||||
use monero_seed::{Language, Seed as MoneroSeed};
|
||||
use once_cell::sync::Lazy;
|
||||
use qrcode::render::unicode;
|
||||
use qrcode::QrCode;
|
||||
|
|
@ -37,6 +38,7 @@ use tracing::Span;
|
|||
use typeshare::typeshare;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// This trait is implemented by all types of request args that
|
||||
/// the CLI can handle.
|
||||
|
|
@ -1446,10 +1448,11 @@ impl CheckElectrumNodeArgs {
|
|||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResolveApprovalArgs {
|
||||
pub request_id: String,
|
||||
pub accept: bool,
|
||||
#[typeshare(serialized_as = "object")]
|
||||
pub accept: serde_json::Value,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
|
@ -1458,10 +1461,23 @@ pub struct ResolveApprovalResponse {
|
|||
pub success: bool,
|
||||
}
|
||||
|
||||
impl Request for ResolveApprovalArgs {
|
||||
type Response = ResolveApprovalResponse;
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CheckSeedArgs {
|
||||
pub seed: String,
|
||||
}
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
resolve_approval_request(self, ctx).await
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CheckSeedResponse {
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
impl CheckSeedArgs {
|
||||
pub async fn request(self) -> Result<CheckSeedResponse> {
|
||||
let seed = MoneroSeed::from_string(Language::English, Zeroizing::new(self.seed));
|
||||
Ok(CheckSeedResponse {
|
||||
available: seed.is_ok(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ use super::request::BalanceResponse;
|
|||
use crate::bitcoin;
|
||||
use crate::monero::MoneroAddressPool;
|
||||
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use bitcoin::Txid;
|
||||
use monero_rpc_pool::pool::PoolStatus;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -52,35 +54,49 @@ pub struct LockBitcoinDetails {
|
|||
#[typeshare]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum ApprovalRequestDetails {
|
||||
pub enum SeedChoice {
|
||||
RandomSeed,
|
||||
FromSeed { seed: String },
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ApprovalRequest {
|
||||
request: ApprovalRequestType,
|
||||
request_status: RequestStatus,
|
||||
#[typeshare(serialized_as = "string")]
|
||||
request_id: Uuid,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum ApprovalRequestType {
|
||||
/// Request approval before locking Bitcoin.
|
||||
/// Contains specific details for review.
|
||||
LockBitcoin(LockBitcoinDetails),
|
||||
/// Request seed selection from user.
|
||||
/// User can choose between random seed or provide their own.
|
||||
SeedSelection,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "state", content = "content")]
|
||||
pub enum ApprovalRequest {
|
||||
pub enum RequestStatus {
|
||||
Pending {
|
||||
request_id: String,
|
||||
#[typeshare(serialized_as = "number")]
|
||||
expiration_ts: u64,
|
||||
details: ApprovalRequestDetails,
|
||||
},
|
||||
Resolved {
|
||||
request_id: String,
|
||||
details: ApprovalRequestDetails,
|
||||
},
|
||||
Rejected {
|
||||
request_id: String,
|
||||
details: ApprovalRequestDetails,
|
||||
#[typeshare(serialized_as = "object")]
|
||||
approve_input: serde_json::Value,
|
||||
},
|
||||
Rejected,
|
||||
}
|
||||
|
||||
struct PendingApproval {
|
||||
responder: Option<oneshot::Sender<bool>>,
|
||||
details: ApprovalRequestDetails,
|
||||
responder: Option<oneshot::Sender<serde_json::Value>>,
|
||||
#[allow(dead_code)]
|
||||
expiration_ts: u64,
|
||||
}
|
||||
|
|
@ -136,88 +152,105 @@ impl TauriHandle {
|
|||
self.emit_unified_event(TauriEvent::Approval(event))
|
||||
}
|
||||
|
||||
pub async fn request_approval(
|
||||
pub async fn request_approval<Response>(
|
||||
&self,
|
||||
request_type: ApprovalRequestDetails,
|
||||
timeout_secs: u64,
|
||||
) -> Result<bool> {
|
||||
request_type: ApprovalRequestType,
|
||||
timeout_secs: Option<u64>,
|
||||
) -> Result<Response>
|
||||
where
|
||||
Response: serde::de::DeserializeOwned + Clone,
|
||||
{
|
||||
#[cfg(not(feature = "tauri"))]
|
||||
{
|
||||
return Ok(true);
|
||||
bail!("Tauri feature not enabled");
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
// Compute absolute expiration timestamp, and UUID for the request
|
||||
let request_id = Uuid::new_v4();
|
||||
let now_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system time to be after unix epoch (1970-01-01)")
|
||||
.as_secs();
|
||||
let expiration_ts = now_secs + timeout_secs;
|
||||
// Emit the creation of the approval request to the frontend
|
||||
// TODO: We need to send a UUID with it here
|
||||
|
||||
// Build the approval event
|
||||
let details = request_type.clone();
|
||||
let pending_event = ApprovalRequest::Pending {
|
||||
request_id: request_id.to_string(),
|
||||
expiration_ts,
|
||||
details: details.clone(),
|
||||
let request_id = Uuid::new_v4();
|
||||
// No timeout = one week
|
||||
let timeout_secs = timeout_secs.unwrap_or(60 * 60 * 24 * 7);
|
||||
let expiration_ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
+ timeout_secs;
|
||||
let request = ApprovalRequest {
|
||||
request: request_type,
|
||||
request_status: RequestStatus::Pending { expiration_ts },
|
||||
request_id,
|
||||
};
|
||||
|
||||
// Emit the creation of the approval request to the frontend
|
||||
self.emit_approval(pending_event.clone());
|
||||
use anyhow::bail;
|
||||
self.emit_approval(request.clone());
|
||||
|
||||
tracing::debug!(%request_id, request=?pending_event, "Emitted approval request event");
|
||||
tracing::debug!(%request, "Emitted approval request event");
|
||||
|
||||
// Construct the data structure we use to internally track the approval request
|
||||
let (responder, receiver) = oneshot::channel();
|
||||
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
let pending = PendingApproval {
|
||||
responder: Some(responder),
|
||||
details: request_type.clone(),
|
||||
expiration_ts,
|
||||
expiration_ts: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
+ timeout_secs,
|
||||
};
|
||||
|
||||
// Lock map and insert the pending approval
|
||||
{
|
||||
let mut pending_map = self.0.pending_approvals.lock().await;
|
||||
pending_map.insert(request_id, pending);
|
||||
pending_map.insert(request.request_id, pending);
|
||||
}
|
||||
|
||||
// Determine if the request will be accepted or rejected
|
||||
// Either by being resolved by the user, or by timing out
|
||||
let accepted = tokio::select! {
|
||||
let unparsed_response = tokio::select! {
|
||||
res = receiver => res.map_err(|_| anyhow!("Approval responder dropped"))?,
|
||||
_ = tokio::time::sleep(timeout_duration) => {
|
||||
tracing::debug!(%request_id, "Approval request timed out and was therefore rejected");
|
||||
false
|
||||
bail!("Approval request timed out and was therefore rejected");
|
||||
},
|
||||
};
|
||||
|
||||
tracing::debug!(%unparsed_response, "Received approval response");
|
||||
|
||||
let response: Result<Response> = serde_json::from_value(unparsed_response.clone())
|
||||
.context("Failed to parse approval response to expected type");
|
||||
|
||||
let mut map = self.0.pending_approvals.lock().await;
|
||||
if let Some(pending) = map.remove(&request_id) {
|
||||
let event = if accepted {
|
||||
ApprovalRequest::Resolved {
|
||||
request_id: request_id.to_string(),
|
||||
details: pending.details,
|
||||
if let Some(_pending) = map.remove(&request.request_id) {
|
||||
let status = if response.is_ok() {
|
||||
RequestStatus::Resolved {
|
||||
approve_input: unparsed_response,
|
||||
}
|
||||
} else {
|
||||
ApprovalRequest::Rejected {
|
||||
request_id: request_id.to_string(),
|
||||
details: pending.details,
|
||||
}
|
||||
RequestStatus::Rejected {}
|
||||
};
|
||||
|
||||
self.emit_approval(event);
|
||||
tracing::debug!(%request_id, %accepted, "Resolved approval request");
|
||||
let mut approval = request.clone();
|
||||
approval.request_status = status.clone();
|
||||
|
||||
tracing::debug!(%approval, "Resolved approval request");
|
||||
self.emit_approval(approval);
|
||||
}
|
||||
|
||||
Ok(accepted)
|
||||
tracing::debug!("Returning approval response");
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_approval(&self, request_id: Uuid, accepted: bool) -> Result<()> {
|
||||
pub async fn resolve_approval(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
response: serde_json::Value,
|
||||
) -> Result<()> {
|
||||
#[cfg(not(feature = "tauri"))]
|
||||
{
|
||||
return Err(anyhow!(
|
||||
|
|
@ -233,7 +266,7 @@ impl TauriHandle {
|
|||
.responder
|
||||
.take()
|
||||
.context("Approval responder was already consumed")?
|
||||
.send(accepted);
|
||||
.send(response);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -243,15 +276,24 @@ impl TauriHandle {
|
|||
}
|
||||
}
|
||||
|
||||
impl Display for ApprovalRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.request {
|
||||
ApprovalRequestType::LockBitcoin(..) => write!(f, "LockBitcoin()"),
|
||||
ApprovalRequestType::SeedSelection => write!(f, "SeedSelection()"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait TauriEmitter {
|
||||
fn request_approval<'life0, 'async_trait>(
|
||||
&'life0 self,
|
||||
request_type: ApprovalRequestDetails,
|
||||
async fn request_bitcoin_approval(
|
||||
&self,
|
||||
details: LockBitcoinDetails,
|
||||
timeout_secs: u64,
|
||||
) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
Self: 'async_trait;
|
||||
) -> Result<bool>;
|
||||
|
||||
async fn request_seed_selection(&self) -> Result<SeedChoice>;
|
||||
|
||||
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>;
|
||||
|
||||
|
|
@ -317,17 +359,25 @@ pub trait TauriEmitter {
|
|||
) -> TauriBackgroundProgressHandle<T>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TauriEmitter for TauriHandle {
|
||||
fn request_approval<'life0, 'async_trait>(
|
||||
&'life0 self,
|
||||
request_type: ApprovalRequestDetails,
|
||||
async fn request_bitcoin_approval(
|
||||
&self,
|
||||
details: LockBitcoinDetails,
|
||||
timeout_secs: u64,
|
||||
) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(self.request_approval(request_type, timeout_secs))
|
||||
) -> Result<bool> {
|
||||
Ok(self
|
||||
.request_approval(
|
||||
ApprovalRequestType::LockBitcoin(details),
|
||||
Some(timeout_secs),
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false))
|
||||
}
|
||||
|
||||
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
||||
self.request_approval(ApprovalRequestType::SeedSelection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
|
||||
|
|
@ -359,6 +409,7 @@ impl TauriEmitter for TauriHandle {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TauriEmitter for Option<TauriHandle> {
|
||||
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
|
||||
match self {
|
||||
|
|
@ -369,21 +420,22 @@ impl TauriEmitter for Option<TauriHandle> {
|
|||
}
|
||||
}
|
||||
|
||||
fn request_approval<'life0, 'async_trait>(
|
||||
&'life0 self,
|
||||
request_type: ApprovalRequestDetails,
|
||||
async fn request_bitcoin_approval(
|
||||
&self,
|
||||
details: LockBitcoinDetails,
|
||||
timeout_secs: u64,
|
||||
) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
) -> Result<bool> {
|
||||
match self {
|
||||
Some(tauri) => tauri.request_approval(request_type, timeout_secs).await,
|
||||
None => Ok(true),
|
||||
Some(tauri) => tauri.request_bitcoin_approval(details, timeout_secs).await,
|
||||
None => bail!("No Tauri handle available"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
||||
match self {
|
||||
Some(tauri) => tauri.request_seed_selection().await,
|
||||
None => bail!("No Tauri handle available"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn new_background_process<T: Clone>(
|
||||
|
|
|
|||
|
|
@ -488,7 +488,7 @@ pub mod monero_private_key {
|
|||
|
||||
struct BytesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for BytesVisitor {
|
||||
impl Visitor<'_> for BytesVisitor {
|
||||
type Value = PrivateKey;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
use crate::bitcoin::wallet::ScriptStatus;
|
||||
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
||||
use crate::cli::api::tauri_bindings::ApprovalRequestDetails;
|
||||
use crate::cli::api::tauri_bindings::{
|
||||
LockBitcoinDetails, TauriEmitter, TauriHandle, TauriSwapProgressEvent,
|
||||
};
|
||||
use crate::cli::api::tauri_bindings::LockBitcoinDetails;
|
||||
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent};
|
||||
use crate::cli::EventLoopHandle;
|
||||
use crate::common::retry;
|
||||
use crate::monero::MoneroAddressPool;
|
||||
|
|
@ -166,19 +164,19 @@ async fn next_state(
|
|||
.context("Failed to get lock amount")?
|
||||
.value;
|
||||
|
||||
let request = ApprovalRequestDetails::LockBitcoin(LockBitcoinDetails {
|
||||
let details = LockBitcoinDetails {
|
||||
btc_lock_amount,
|
||||
btc_network_fee,
|
||||
xmr_receive_amount,
|
||||
monero_receive_pool,
|
||||
swap_id,
|
||||
});
|
||||
};
|
||||
|
||||
// We request approval before publishing the Bitcoin lock transaction,
|
||||
// as the exchange rate determined at this step might be different
|
||||
// from the one we previously displayed to the user.
|
||||
let approval_result = event_emitter
|
||||
.request_approval(request, PRE_BTC_LOCK_APPROVAL_TIMEOUT_SECS)
|
||||
.request_bitcoin_approval(details, PRE_BTC_LOCK_APPROVAL_TIMEOUT_SECS)
|
||||
.await;
|
||||
|
||||
match approval_result {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::cli::api::tauri_bindings::{SeedChoice, TauriEmitter, TauriHandle};
|
||||
use crate::fs::ensure_directory_exists;
|
||||
use ::bitcoin::bip32::Xpriv as ExtendedPrivKey;
|
||||
use anyhow::{Context, Result};
|
||||
|
|
@ -5,6 +6,7 @@ use bitcoin::hashes::{sha256, Hash, HashEngine};
|
|||
use bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
||||
use bitcoin::secp256k1::{self, SecretKey};
|
||||
use libp2p::identity;
|
||||
use monero_seed::{Language, Seed as MoneroSeed};
|
||||
use pem::{encode, Pem};
|
||||
use rand::prelude::*;
|
||||
use std::ffi::OsStr;
|
||||
|
|
@ -12,6 +14,7 @@ use std::fmt;
|
|||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub const SEED_LENGTH: usize = 32;
|
||||
|
||||
|
|
@ -60,20 +63,57 @@ impl Seed {
|
|||
identity::Keypair::ed25519_from_bytes(bytes).expect("we always pass 32 bytes")
|
||||
}
|
||||
|
||||
pub fn from_file_or_generate(data_dir: &Path) -> Result<Self, Error> {
|
||||
pub async fn from_file_or_generate(
|
||||
data_dir: &Path,
|
||||
tauri_handle: Option<TauriHandle>,
|
||||
) -> Result<Self> {
|
||||
let file_path_buf = data_dir.join("seed.pem");
|
||||
let file_path = Path::new(&file_path_buf);
|
||||
|
||||
if file_path.exists() {
|
||||
return Self::from_file(file_path);
|
||||
return Self::from_file(file_path).context("Couldn't get seed from file");
|
||||
}
|
||||
|
||||
tracing::debug!("No seed file found, creating at {}", file_path.display());
|
||||
|
||||
let random_seed = Seed::random()?;
|
||||
random_seed.write_to(file_path.to_path_buf())?;
|
||||
// In debug mode, we allow the user to enter a seed manually.
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let new_seed = match tauri_handle {
|
||||
Some(tauri_handle) => {
|
||||
let seed_choice = tauri_handle.request_seed_selection().await?;
|
||||
let seed_entered = match seed_choice {
|
||||
SeedChoice::RandomSeed => Seed::random()?,
|
||||
SeedChoice::FromSeed { seed } => {
|
||||
println!("seed: {}", seed);
|
||||
let monero_seed =
|
||||
MoneroSeed::from_string(Language::English, Zeroizing::new(seed))
|
||||
.unwrap();
|
||||
Seed(*monero_seed.entropy())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(random_seed)
|
||||
//TODO: Send error type to front end
|
||||
|
||||
seed_entered
|
||||
}
|
||||
None => {
|
||||
let random_seed = Seed::random()?;
|
||||
random_seed
|
||||
}
|
||||
};
|
||||
|
||||
new_seed.write_to(file_path.to_path_buf())?;
|
||||
Ok(new_seed)
|
||||
}
|
||||
|
||||
// In release mode, we generate a random seed.
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let new_seed = Seed::random()?;
|
||||
new_seed.write_to(file_path.to_path_buf())?;
|
||||
Ok(new_seed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a new seed using the given scope.
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ impl MakeCapturingWriter {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> MakeWriter<'a> for MakeCapturingWriter {
|
||||
impl MakeWriter<'_> for MakeCapturingWriter {
|
||||
type Writer = CapturingWriter;
|
||||
|
||||
fn make_writer(&self) -> Self::Writer {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue