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",
|
"curve25519-dalek-derive",
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"fiat-crypto",
|
"fiat-crypto",
|
||||||
|
"group",
|
||||||
|
"rand_core 0.6.4",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
|
|
@ -2219,6 +2221,22 @@ dependencies = [
|
||||||
"syn 2.0.104",
|
"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]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
|
|
@ -3147,6 +3165,7 @@ version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitvec",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
@ -5929,6 +5948,20 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "monero-harness"
|
name = "monero-harness"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -5946,6 +5979,28 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"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]]
|
[[package]]
|
||||||
name = "monero-rpc"
|
name = "monero-rpc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -5994,6 +6049,19 @@ dependencies = [
|
||||||
"uuid",
|
"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]]
|
[[package]]
|
||||||
name = "monero-sys"
|
name = "monero-sys"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -9565,6 +9633,15 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
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]]
|
[[package]]
|
||||||
name = "string_cache"
|
name = "string_cache"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -9741,6 +9818,7 @@ dependencies = [
|
||||||
"monero-harness",
|
"monero-harness",
|
||||||
"monero-rpc",
|
"monero-rpc",
|
||||||
"monero-rpc-pool",
|
"monero-rpc-pool",
|
||||||
|
"monero-seed",
|
||||||
"monero-sys",
|
"monero-sys",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pem",
|
"pem",
|
||||||
|
|
@ -9786,6 +9864,7 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
"vergen",
|
"vergen",
|
||||||
"void",
|
"void",
|
||||||
|
"zeroize",
|
||||||
"zip 0.5.13",
|
"zip 0.5.13",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
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.crates-io]
|
||||||
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
# 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_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_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" }
|
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;
|
return response.timelock !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PendingApprovalRequest = Extract<
|
export type PendingApprovalRequest = ApprovalRequest & {
|
||||||
ApprovalRequest,
|
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
|
||||||
{ state: "Pending" }
|
};
|
||||||
>;
|
|
||||||
|
|
||||||
export type PendingLockBitcoinApprovalRequest = PendingApprovalRequest & {
|
export type PendingLockBitcoinApprovalRequest = ApprovalRequest & {
|
||||||
content: {
|
request: Extract<ApprovalRequest["request"], { type: "LockBitcoin" }>;
|
||||||
details: { 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(
|
export function isPendingLockBitcoinApprovalEvent(
|
||||||
event: ApprovalRequest,
|
event: ApprovalRequest,
|
||||||
): event is PendingLockBitcoinApprovalRequest {
|
): event is PendingLockBitcoinApprovalRequest {
|
||||||
// Check if the request is pending
|
// Check if the request is a LockBitcoin request and is pending
|
||||||
if (event.state !== "Pending") {
|
return (
|
||||||
return false;
|
event.request.type === "LockBitcoin" &&
|
||||||
}
|
event.request_status.state === "Pending"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the request is a LockBitcoin request
|
export function isPendingSeedSelectionApprovalEvent(
|
||||||
return event.content.details.type === "LockBitcoin";
|
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(
|
export function isPendingBackgroundProcess(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { setupBackgroundTasks } from "renderer/background";
|
||||||
import "@fontsource/roboto";
|
import "@fontsource/roboto";
|
||||||
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
||||||
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
||||||
|
import SeedSelectionDialog from "./modal/seed-selection/SeedSelectionDialog";
|
||||||
|
|
||||||
declare module "@mui/material/styles" {
|
declare module "@mui/material/styles" {
|
||||||
interface Theme {
|
interface Theme {
|
||||||
|
|
@ -46,6 +47,7 @@ export default function App() {
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<GlobalSnackbarProvider>
|
<GlobalSnackbarProvider>
|
||||||
<IntroductionModal />
|
<IntroductionModal />
|
||||||
|
<SeedSelectionDialog />
|
||||||
<Router>
|
<Router>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<InnerContent />
|
<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();
|
const activeSwapId = useActiveSwapId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
approvals?.find(
|
approvals?.find((r) => r.request.content.swap_id === activeSwapId) || null
|
||||||
(r) => r.content.details.content.swap_id === activeSwapId,
|
|
||||||
) || null
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,10 +30,18 @@ export default function SwapSetupInflightPage({
|
||||||
const request = useActiveLockBitcoinApprovalRequest();
|
const request = useActiveLockBitcoinApprovalRequest();
|
||||||
|
|
||||||
const [timeLeft, setTimeLeft] = useState<number>(0);
|
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||||
|
const expirationTs =
|
||||||
const expiresAtMs = request?.content.expiration_ts * 1000 || 0;
|
request?.request_status.state === "Pending"
|
||||||
|
? request.request_status.content.expiration_ts
|
||||||
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (expirationTs == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresAtMs = expirationTs * 1000 || 0;
|
||||||
|
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
const remainingMs = Math.max(expiresAtMs - Date.now(), 0);
|
const remainingMs = Math.max(expiresAtMs - Date.now(), 0);
|
||||||
setTimeLeft(Math.ceil(remainingMs / 1000));
|
setTimeLeft(Math.ceil(remainingMs / 1000));
|
||||||
|
|
@ -44,11 +50,11 @@ export default function SwapSetupInflightPage({
|
||||||
tick();
|
tick();
|
||||||
const id = setInterval(tick, 250);
|
const id = setInterval(tick, 250);
|
||||||
return () => clearInterval(id);
|
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
|
// 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
|
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
||||||
if (!request) {
|
if (request == null) {
|
||||||
return (
|
return (
|
||||||
<CircularProgressWithSubtitle
|
<CircularProgressWithSubtitle
|
||||||
description={
|
description={
|
||||||
|
|
@ -61,7 +67,7 @@ export default function SwapSetupInflightPage({
|
||||||
}
|
}
|
||||||
|
|
||||||
const { btc_network_fee, monero_receive_pool, xmr_receive_amount } =
|
const { btc_network_fee, monero_receive_pool, xmr_receive_amount } =
|
||||||
request.content.details.content;
|
request.request.content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -124,7 +130,9 @@ export default function SwapSetupInflightPage({
|
||||||
variant="text"
|
variant="text"
|
||||||
size="large"
|
size="large"
|
||||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
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
|
displayErrorSnackbar
|
||||||
requiresContext
|
requiresContext
|
||||||
>
|
>
|
||||||
|
|
@ -135,7 +143,9 @@ export default function SwapSetupInflightPage({
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
onInvoke={() => resolveApproval(request.content.request_id, true)}
|
onInvoke={() =>
|
||||||
|
resolveApproval(request.request_id, true as unknown as object)
|
||||||
|
}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
requiresContext
|
requiresContext
|
||||||
endIcon={<CheckIcon />}
|
endIcon={<CheckIcon />}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ import {
|
||||||
GetSwapInfoArgs,
|
GetSwapInfoArgs,
|
||||||
ExportBitcoinWalletResponse,
|
ExportBitcoinWalletResponse,
|
||||||
CheckMoneroNodeArgs,
|
CheckMoneroNodeArgs,
|
||||||
|
CheckSeedArgs,
|
||||||
|
CheckSeedResponse,
|
||||||
CheckMoneroNodeResponse,
|
CheckMoneroNodeResponse,
|
||||||
TauriSettings,
|
TauriSettings,
|
||||||
CheckElectrumNodeArgs,
|
CheckElectrumNodeArgs,
|
||||||
|
|
@ -304,10 +306,16 @@ export async function initializeContext() {
|
||||||
|
|
||||||
logger.info("Initializing context with settings", tauriSettings);
|
logger.info("Initializing context with settings", tauriSettings);
|
||||||
|
|
||||||
await invokeUnsafe<void>("initialize_context", {
|
try {
|
||||||
settings: tauriSettings,
|
await invokeUnsafe<void>("initialize_context", {
|
||||||
testnet,
|
settings: tauriSettings,
|
||||||
});
|
testnet,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("Couldn't initialize context: " + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Initialized context");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWalletDescriptor() {
|
export async function getWalletDescriptor() {
|
||||||
|
|
@ -395,7 +403,7 @@ export async function getDataDir(): Promise<string> {
|
||||||
|
|
||||||
export async function resolveApproval(
|
export async function resolveApproval(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
accept: boolean,
|
accept: object,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
|
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
|
||||||
"resolve_approval_request",
|
"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(
|
export async function saveLogFiles(
|
||||||
zipFileName: string,
|
zipFileName: string,
|
||||||
content: Record<string, string>,
|
content: Record<string, string>,
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ export const rpcSlice = createSlice({
|
||||||
},
|
},
|
||||||
approvalEventReceived(slice, action: PayloadAction<ApprovalRequest>) {
|
approvalEventReceived(slice, action: PayloadAction<ApprovalRequest>) {
|
||||||
const event = action.payload;
|
const event = action.payload;
|
||||||
const requestId = event.content.request_id;
|
const requestId = event.request_id;
|
||||||
slice.state.approvalRequests[requestId] = event;
|
slice.state.approvalRequests[requestId] = event;
|
||||||
},
|
},
|
||||||
backgroundProgressEventReceived(
|
backgroundProgressEventReceived(
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ import {
|
||||||
isBitcoinSyncProgress,
|
isBitcoinSyncProgress,
|
||||||
isPendingBackgroundProcess,
|
isPendingBackgroundProcess,
|
||||||
isPendingLockBitcoinApprovalEvent,
|
isPendingLockBitcoinApprovalEvent,
|
||||||
|
isPendingSeedSelectionApprovalEvent,
|
||||||
PendingApprovalRequest,
|
PendingApprovalRequest,
|
||||||
PendingLockBitcoinApprovalRequest,
|
PendingLockBitcoinApprovalRequest,
|
||||||
|
PendingSeedSelectionApprovalRequest,
|
||||||
} from "models/tauriModelExt";
|
} from "models/tauriModelExt";
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
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[] {
|
export function usePendingApprovals(): PendingApprovalRequest[] {
|
||||||
const approvals = useAppSelector((state) => state.rpc.state.approvalRequests);
|
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[] {
|
export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] {
|
||||||
|
|
@ -163,6 +167,11 @@ export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalReque
|
||||||
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
|
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
|
/// Returns all the pending background processes
|
||||||
/// In the format [id, {componentName, {type: "Pending", content: {consumed, total}}}]
|
/// In the format [id, {componentName, {type: "Pending", content: {consumed, total}}}]
|
||||||
export function usePendingBackgroundProcesses(): [
|
export function usePendingBackgroundProcesses(): [
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ use swap::cli::{
|
||||||
data,
|
data,
|
||||||
request::{
|
request::{
|
||||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
||||||
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse,
|
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
||||||
ExportBitcoinWalletArgs, GetDataDirArgs, GetHistoryArgs, GetLogsArgs,
|
CheckSeedResponse, ExportBitcoinWalletArgs, GetDataDirArgs, GetHistoryArgs,
|
||||||
GetMoneroAddressesArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs,
|
GetLogsArgs, GetMoneroAddressesArgs, GetSwapInfoArgs, GetSwapInfosAllArgs,
|
||||||
MoneroRecoveryArgs, RedactArgs, ResolveApprovalArgs, ResumeSwapArgs,
|
ListSellersArgs, MoneroRecoveryArgs, RedactArgs, ResolveApprovalArgs, ResumeSwapArgs,
|
||||||
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||||
},
|
},
|
||||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
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
|
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
||||||
struct State {
|
struct State {
|
||||||
pub context: Option<Arc<Context>>,
|
pub context: Option<Arc<Context>>,
|
||||||
|
pub handle: TauriHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
/// Creates a new State instance with no Context
|
/// Creates a new State instance with no Context
|
||||||
fn new() -> Self {
|
fn new(handle: TauriHandle) -> Self {
|
||||||
Self { context: None }
|
Self {
|
||||||
|
context: None,
|
||||||
|
handle,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the context for the application state
|
/// 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
|
// 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
|
// 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);
|
app_handle.manage::<RwLock<State>>(state);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -194,6 +199,7 @@ pub fn run() {
|
||||||
resolve_approval_request,
|
resolve_approval_request,
|
||||||
redact,
|
redact,
|
||||||
save_txt_files,
|
save_txt_files,
|
||||||
|
check_seed,
|
||||||
])
|
])
|
||||||
.setup(setup)
|
.setup(setup)
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
|
|
@ -234,7 +240,6 @@ tauri_command!(monero_recovery, MoneroRecoveryArgs);
|
||||||
tauri_command!(get_logs, GetLogsArgs);
|
tauri_command!(get_logs, GetLogsArgs);
|
||||||
tauri_command!(list_sellers, ListSellersArgs);
|
tauri_command!(list_sellers, ListSellersArgs);
|
||||||
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
||||||
tauri_command!(resolve_approval_request, ResolveApprovalArgs);
|
|
||||||
tauri_command!(redact, RedactArgs);
|
tauri_command!(redact, RedactArgs);
|
||||||
|
|
||||||
// These commands require no arguments
|
// These commands require no arguments
|
||||||
|
|
@ -268,6 +273,14 @@ async fn check_electrum_node(
|
||||||
args.request().await.to_string_result()
|
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
|
// Returns the data directory
|
||||||
// This is independent of the context to ensure the user can open the directory even if the context cannot
|
// This is independent of the context to ensure the user can open the directory even if the context cannot
|
||||||
// be initialized (for troubleshooting purposes)
|
// be initialized (for troubleshooting purposes)
|
||||||
|
|
@ -327,6 +340,23 @@ async fn save_txt_files(
|
||||||
Ok(())
|
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 to initialize the Context
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn initialize_context(
|
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
|
// 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
|
// Notify frontend that the context is being initialized
|
||||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Initializing);
|
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Initializing);
|
||||||
|
|
@ -388,7 +417,11 @@ async fn initialize_context(
|
||||||
|
|
||||||
match context_result {
|
match context_result {
|
||||||
Ok(context_instance) => {
|
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");
|
tracing::info!("Context initialized");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ moka = { version = "0.12", features = ["sync", "future"] }
|
||||||
monero = { version = "0.12", features = ["serde_support"] }
|
monero = { version = "0.12", features = ["serde_support"] }
|
||||||
monero-rpc = { path = "../monero-rpc" }
|
monero-rpc = { path = "../monero-rpc" }
|
||||||
monero-rpc-pool = { path = "../monero-rpc-pool" }
|
monero-rpc-pool = { path = "../monero-rpc-pool" }
|
||||||
|
monero-seed = { version = "0.1.0", path = "../seed" }
|
||||||
monero-sys = { path = "../monero-sys" }
|
monero-sys = { path = "../monero-sys" }
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
pem = "3.0"
|
pem = "3.0"
|
||||||
|
|
@ -86,6 +87,7 @@ unsigned-varint = { version = "0.8.0", features = ["codec", "asynchronous_codec"
|
||||||
url = { version = "2", features = ["serde"] }
|
url = { version = "2", features = ["serde"] }
|
||||||
uuid = { version = "1.9", features = ["serde", "v4"] }
|
uuid = { version = "1.9", features = ["serde", "v4"] }
|
||||||
void = "1"
|
void = "1"
|
||||||
|
zeroize = "1.8.1"
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
tokio-tar = "0.3"
|
tokio-tar = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ mod addr_list {
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let s = Value::deserialize(deserializer)?;
|
let s = Value::deserialize(deserializer)?;
|
||||||
return match s {
|
match s {
|
||||||
Value::String(s) => {
|
Value::String(s) => {
|
||||||
let list: Result<Vec<_>, _> = s
|
let list: Result<Vec<_>, _> = s
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|
@ -173,7 +173,7 @@ mod addr_list {
|
||||||
Unexpected::Other(&value.to_string()),
|
Unexpected::Other(&value.to_string()),
|
||||||
&"a string or array",
|
&"a string or array",
|
||||||
)),
|
)),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +188,7 @@ mod electrum_urls {
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let s = Value::deserialize(deserializer)?;
|
let s = Value::deserialize(deserializer)?;
|
||||||
return match s {
|
match s {
|
||||||
Value::String(s) => {
|
Value::String(s) => {
|
||||||
let list: Result<Vec<_>, _> = s
|
let list: Result<Vec<_>, _> = s
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|
@ -214,7 +214,7 @@ mod electrum_urls {
|
||||||
Unexpected::Other(&value.to_string()),
|
Unexpected::Other(&value.to_string()),
|
||||||
&"a string or array",
|
&"a string or array",
|
||||||
)),
|
)),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,15 +378,19 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
||||||
let prompt = format!(
|
let prompt = format!(
|
||||||
"Enter additional Electrum RPC URL ({electrum_number}). Or just hit Enter to continue."
|
"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)
|
.with_prompt(prompt)
|
||||||
.allow_empty(true)
|
.allow_empty(true)
|
||||||
.interact_text()?;
|
.interact_text()?;
|
||||||
if electrum_url.as_str().is_empty() {
|
if electrum_url.as_str().is_empty() {
|
||||||
electrum_done = true;
|
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.");
|
println!("That Electrum URL is already in the list.");
|
||||||
} else {
|
} else {
|
||||||
|
let electrum_url = Url::parse(&electrum_url).context("Invalid Electrum URL")?;
|
||||||
electrum_rpc_urls.push(electrum_url);
|
electrum_rpc_urls.push(electrum_url);
|
||||||
electrum_number += 1;
|
electrum_number += 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,9 @@ pub async fn main() -> Result<()> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let seed =
|
let seed = Seed::from_file_or_generate(&config.data.dir, None)
|
||||||
Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed");
|
.await
|
||||||
|
.expect("Could not retrieve/initialize seed");
|
||||||
|
|
||||||
let db_file = config.data.dir.join("sqlite");
|
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)
|
// 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 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();
|
let mut chunks = Vec::new();
|
||||||
|
|
||||||
|
|
@ -2208,10 +2208,7 @@ mod sync_ext {
|
||||||
///
|
///
|
||||||
/// Ensures the callback is always invoked when progress reaches 100%.
|
/// Ensures the callback is always invoked when progress reaches 100%.
|
||||||
fn throttle_callback(self, min_percentage_increase: f32) -> InnerSyncCallback {
|
fn throttle_callback(self, min_percentage_increase: f32) -> InnerSyncCallback {
|
||||||
let mut callback = match self {
|
let mut callback = self?;
|
||||||
None => return None,
|
|
||||||
Some(cb) => cb,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut last_reported_percentage: f64 = 0.0;
|
let mut last_reported_percentage: f64 = 0.0;
|
||||||
let threshold = min_percentage_increase as f64 / 100.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.
|
/// Takes the builder, initializes the context by initializing the wallets and other components and returns the Context.
|
||||||
pub async fn build(self) -> Result<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 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
|
// Initialize logging
|
||||||
let format = if self.json { Format::Json } else { Format::Raw };
|
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
|
// Create the data structure we use to manage the swap lock
|
||||||
let swap_lock = Arc::new(SwapLock::new());
|
let swap_lock = Arc::new(SwapLock::new());
|
||||||
let tasks = PendingTaskList::default().into();
|
let tasks = PendingTaskList::default().into();
|
||||||
|
|
@ -698,14 +700,16 @@ pub mod api_test {
|
||||||
pub const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b";
|
pub const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b";
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn default(
|
pub async fn default(
|
||||||
is_testnet: bool,
|
is_testnet: bool,
|
||||||
data_dir: Option<PathBuf>,
|
data_dir: Option<PathBuf>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
json: bool,
|
json: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let data_dir = data::data_dir_from(data_dir, is_testnet).unwrap();
|
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);
|
let env_config = env_config_from(is_testnet);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use ::monero::Network;
|
||||||
use anyhow::{bail, Context as AnyContext, Result};
|
use anyhow::{bail, Context as AnyContext, Result};
|
||||||
use libp2p::core::Multiaddr;
|
use libp2p::core::Multiaddr;
|
||||||
use libp2p::PeerId;
|
use libp2p::PeerId;
|
||||||
|
use monero_seed::{Language, Seed as MoneroSeed};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use qrcode::render::unicode;
|
use qrcode::render::unicode;
|
||||||
use qrcode::QrCode;
|
use qrcode::QrCode;
|
||||||
|
|
@ -37,6 +38,7 @@ use tracing::Span;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
/// This trait is implemented by all types of request args that
|
/// This trait is implemented by all types of request args that
|
||||||
/// the CLI can handle.
|
/// the CLI can handle.
|
||||||
|
|
@ -1446,10 +1448,11 @@ impl CheckElectrumNodeArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ResolveApprovalArgs {
|
pub struct ResolveApprovalArgs {
|
||||||
pub request_id: String,
|
pub request_id: String,
|
||||||
pub accept: bool,
|
#[typeshare(serialized_as = "object")]
|
||||||
|
pub accept: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
|
|
@ -1458,10 +1461,23 @@ pub struct ResolveApprovalResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request for ResolveApprovalArgs {
|
#[typeshare]
|
||||||
type Response = ResolveApprovalResponse;
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CheckSeedArgs {
|
||||||
|
pub seed: String,
|
||||||
|
}
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
#[typeshare]
|
||||||
resolve_approval_request(self, ctx).await
|
#[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::bitcoin;
|
||||||
use crate::monero::MoneroAddressPool;
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
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 bitcoin::Txid;
|
||||||
use monero_rpc_pool::pool::PoolStatus;
|
use monero_rpc_pool::pool::PoolStatus;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -52,35 +54,49 @@ pub struct LockBitcoinDetails {
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[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.
|
/// Request approval before locking Bitcoin.
|
||||||
/// Contains specific details for review.
|
/// Contains specific details for review.
|
||||||
LockBitcoin(LockBitcoinDetails),
|
LockBitcoin(LockBitcoinDetails),
|
||||||
|
/// Request seed selection from user.
|
||||||
|
/// User can choose between random seed or provide their own.
|
||||||
|
SeedSelection,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "state", content = "content")]
|
#[serde(tag = "state", content = "content")]
|
||||||
pub enum ApprovalRequest {
|
pub enum RequestStatus {
|
||||||
Pending {
|
Pending {
|
||||||
request_id: String,
|
|
||||||
#[typeshare(serialized_as = "number")]
|
#[typeshare(serialized_as = "number")]
|
||||||
expiration_ts: u64,
|
expiration_ts: u64,
|
||||||
details: ApprovalRequestDetails,
|
|
||||||
},
|
},
|
||||||
Resolved {
|
Resolved {
|
||||||
request_id: String,
|
#[typeshare(serialized_as = "object")]
|
||||||
details: ApprovalRequestDetails,
|
approve_input: serde_json::Value,
|
||||||
},
|
|
||||||
Rejected {
|
|
||||||
request_id: String,
|
|
||||||
details: ApprovalRequestDetails,
|
|
||||||
},
|
},
|
||||||
|
Rejected,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PendingApproval {
|
struct PendingApproval {
|
||||||
responder: Option<oneshot::Sender<bool>>,
|
responder: Option<oneshot::Sender<serde_json::Value>>,
|
||||||
details: ApprovalRequestDetails,
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
expiration_ts: u64,
|
expiration_ts: u64,
|
||||||
}
|
}
|
||||||
|
|
@ -136,88 +152,105 @@ impl TauriHandle {
|
||||||
self.emit_unified_event(TauriEvent::Approval(event))
|
self.emit_unified_event(TauriEvent::Approval(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request_approval(
|
pub async fn request_approval<Response>(
|
||||||
&self,
|
&self,
|
||||||
request_type: ApprovalRequestDetails,
|
request_type: ApprovalRequestType,
|
||||||
timeout_secs: u64,
|
timeout_secs: Option<u64>,
|
||||||
) -> Result<bool> {
|
) -> Result<Response>
|
||||||
|
where
|
||||||
|
Response: serde::de::DeserializeOwned + Clone,
|
||||||
|
{
|
||||||
#[cfg(not(feature = "tauri"))]
|
#[cfg(not(feature = "tauri"))]
|
||||||
{
|
{
|
||||||
return Ok(true);
|
bail!("Tauri feature not enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
{
|
{
|
||||||
// Compute absolute expiration timestamp, and UUID for the request
|
// Emit the creation of the approval request to the frontend
|
||||||
let request_id = Uuid::new_v4();
|
// TODO: We need to send a UUID with it here
|
||||||
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;
|
|
||||||
|
|
||||||
// Build the approval event
|
let request_id = Uuid::new_v4();
|
||||||
let details = request_type.clone();
|
// No timeout = one week
|
||||||
let pending_event = ApprovalRequest::Pending {
|
let timeout_secs = timeout_secs.unwrap_or(60 * 60 * 24 * 7);
|
||||||
request_id: request_id.to_string(),
|
let expiration_ts = SystemTime::now()
|
||||||
expiration_ts,
|
.duration_since(UNIX_EPOCH)
|
||||||
details: details.clone(),
|
.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
|
use anyhow::bail;
|
||||||
self.emit_approval(pending_event.clone());
|
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
|
// Construct the data structure we use to internally track the approval request
|
||||||
let (responder, receiver) = oneshot::channel();
|
let (responder, receiver) = oneshot::channel();
|
||||||
|
|
||||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||||
|
|
||||||
let pending = PendingApproval {
|
let pending = PendingApproval {
|
||||||
responder: Some(responder),
|
responder: Some(responder),
|
||||||
details: request_type.clone(),
|
expiration_ts: SystemTime::now()
|
||||||
expiration_ts,
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs()
|
||||||
|
+ timeout_secs,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lock map and insert the pending approval
|
// Lock map and insert the pending approval
|
||||||
{
|
{
|
||||||
let mut pending_map = self.0.pending_approvals.lock().await;
|
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
|
// Determine if the request will be accepted or rejected
|
||||||
// Either by being resolved by the user, or by timing out
|
// 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"))?,
|
res = receiver => res.map_err(|_| anyhow!("Approval responder dropped"))?,
|
||||||
_ = tokio::time::sleep(timeout_duration) => {
|
_ = tokio::time::sleep(timeout_duration) => {
|
||||||
tracing::debug!(%request_id, "Approval request timed out and was therefore rejected");
|
bail!("Approval request timed out and was therefore rejected");
|
||||||
false
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
let mut map = self.0.pending_approvals.lock().await;
|
||||||
if let Some(pending) = map.remove(&request_id) {
|
if let Some(_pending) = map.remove(&request.request_id) {
|
||||||
let event = if accepted {
|
let status = if response.is_ok() {
|
||||||
ApprovalRequest::Resolved {
|
RequestStatus::Resolved {
|
||||||
request_id: request_id.to_string(),
|
approve_input: unparsed_response,
|
||||||
details: pending.details,
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ApprovalRequest::Rejected {
|
RequestStatus::Rejected {}
|
||||||
request_id: request_id.to_string(),
|
|
||||||
details: pending.details,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.emit_approval(event);
|
let mut approval = request.clone();
|
||||||
tracing::debug!(%request_id, %accepted, "Resolved approval request");
|
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"))]
|
#[cfg(not(feature = "tauri"))]
|
||||||
{
|
{
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
|
|
@ -233,7 +266,7 @@ impl TauriHandle {
|
||||||
.responder
|
.responder
|
||||||
.take()
|
.take()
|
||||||
.context("Approval responder was already consumed")?
|
.context("Approval responder was already consumed")?
|
||||||
.send(accepted);
|
.send(response);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} 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 {
|
pub trait TauriEmitter {
|
||||||
fn request_approval<'life0, 'async_trait>(
|
async fn request_bitcoin_approval(
|
||||||
&'life0 self,
|
&self,
|
||||||
request_type: ApprovalRequestDetails,
|
details: LockBitcoinDetails,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'async_trait>>
|
) -> Result<bool>;
|
||||||
where
|
|
||||||
'life0: 'async_trait,
|
async fn request_seed_selection(&self) -> Result<SeedChoice>;
|
||||||
Self: 'async_trait;
|
|
||||||
|
|
||||||
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>;
|
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>;
|
||||||
|
|
||||||
|
|
@ -317,17 +359,25 @@ pub trait TauriEmitter {
|
||||||
) -> TauriBackgroundProgressHandle<T>;
|
) -> TauriBackgroundProgressHandle<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl TauriEmitter for TauriHandle {
|
impl TauriEmitter for TauriHandle {
|
||||||
fn request_approval<'life0, 'async_trait>(
|
async fn request_bitcoin_approval(
|
||||||
&'life0 self,
|
&self,
|
||||||
request_type: ApprovalRequestDetails,
|
details: LockBitcoinDetails,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'async_trait>>
|
) -> Result<bool> {
|
||||||
where
|
Ok(self
|
||||||
'life0: 'async_trait,
|
.request_approval(
|
||||||
Self: 'async_trait,
|
ApprovalRequestType::LockBitcoin(details),
|
||||||
{
|
Some(timeout_secs),
|
||||||
Box::pin(self.request_approval(request_type, 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<()> {
|
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> {
|
impl TauriEmitter for Option<TauriHandle> {
|
||||||
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
|
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -369,21 +420,22 @@ impl TauriEmitter for Option<TauriHandle> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_approval<'life0, 'async_trait>(
|
async fn request_bitcoin_approval(
|
||||||
&'life0 self,
|
&self,
|
||||||
request_type: ApprovalRequestDetails,
|
details: LockBitcoinDetails,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'async_trait>>
|
) -> Result<bool> {
|
||||||
where
|
match self {
|
||||||
'life0: 'async_trait,
|
Some(tauri) => tauri.request_bitcoin_approval(details, timeout_secs).await,
|
||||||
Self: 'async_trait,
|
None => bail!("No Tauri handle available"),
|
||||||
{
|
}
|
||||||
Box::pin(async move {
|
}
|
||||||
match self {
|
|
||||||
Some(tauri) => tauri.request_approval(request_type, timeout_secs).await,
|
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
||||||
None => Ok(true),
|
match self {
|
||||||
}
|
Some(tauri) => tauri.request_seed_selection().await,
|
||||||
})
|
None => bail!("No Tauri handle available"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_background_process<T: Clone>(
|
fn new_background_process<T: Clone>(
|
||||||
|
|
|
||||||
|
|
@ -488,7 +488,7 @@ pub mod monero_private_key {
|
||||||
|
|
||||||
struct BytesVisitor;
|
struct BytesVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for BytesVisitor {
|
impl Visitor<'_> for BytesVisitor {
|
||||||
type Value = PrivateKey;
|
type Value = PrivateKey;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
use crate::bitcoin::wallet::ScriptStatus;
|
use crate::bitcoin::wallet::ScriptStatus;
|
||||||
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
||||||
use crate::cli::api::tauri_bindings::ApprovalRequestDetails;
|
use crate::cli::api::tauri_bindings::LockBitcoinDetails;
|
||||||
use crate::cli::api::tauri_bindings::{
|
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent};
|
||||||
LockBitcoinDetails, TauriEmitter, TauriHandle, TauriSwapProgressEvent,
|
|
||||||
};
|
|
||||||
use crate::cli::EventLoopHandle;
|
use crate::cli::EventLoopHandle;
|
||||||
use crate::common::retry;
|
use crate::common::retry;
|
||||||
use crate::monero::MoneroAddressPool;
|
use crate::monero::MoneroAddressPool;
|
||||||
|
|
@ -166,19 +164,19 @@ async fn next_state(
|
||||||
.context("Failed to get lock amount")?
|
.context("Failed to get lock amount")?
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
let request = ApprovalRequestDetails::LockBitcoin(LockBitcoinDetails {
|
let details = LockBitcoinDetails {
|
||||||
btc_lock_amount,
|
btc_lock_amount,
|
||||||
btc_network_fee,
|
btc_network_fee,
|
||||||
xmr_receive_amount,
|
xmr_receive_amount,
|
||||||
monero_receive_pool,
|
monero_receive_pool,
|
||||||
swap_id,
|
swap_id,
|
||||||
});
|
};
|
||||||
|
|
||||||
// We request approval before publishing the Bitcoin lock transaction,
|
// We request approval before publishing the Bitcoin lock transaction,
|
||||||
// as the exchange rate determined at this step might be different
|
// as the exchange rate determined at this step might be different
|
||||||
// from the one we previously displayed to the user.
|
// from the one we previously displayed to the user.
|
||||||
let approval_result = event_emitter
|
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;
|
.await;
|
||||||
|
|
||||||
match approval_result {
|
match approval_result {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::cli::api::tauri_bindings::{SeedChoice, TauriEmitter, TauriHandle};
|
||||||
use crate::fs::ensure_directory_exists;
|
use crate::fs::ensure_directory_exists;
|
||||||
use ::bitcoin::bip32::Xpriv as ExtendedPrivKey;
|
use ::bitcoin::bip32::Xpriv as ExtendedPrivKey;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
@ -5,6 +6,7 @@ use bitcoin::hashes::{sha256, Hash, HashEngine};
|
||||||
use bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
use bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
||||||
use bitcoin::secp256k1::{self, SecretKey};
|
use bitcoin::secp256k1::{self, SecretKey};
|
||||||
use libp2p::identity;
|
use libp2p::identity;
|
||||||
|
use monero_seed::{Language, Seed as MoneroSeed};
|
||||||
use pem::{encode, Pem};
|
use pem::{encode, Pem};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
|
@ -12,6 +14,7 @@ use std::fmt;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
pub const SEED_LENGTH: usize = 32;
|
pub const SEED_LENGTH: usize = 32;
|
||||||
|
|
||||||
|
|
@ -60,20 +63,57 @@ impl Seed {
|
||||||
identity::Keypair::ed25519_from_bytes(bytes).expect("we always pass 32 bytes")
|
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_buf = data_dir.join("seed.pem");
|
||||||
let file_path = Path::new(&file_path_buf);
|
let file_path = Path::new(&file_path_buf);
|
||||||
|
|
||||||
if file_path.exists() {
|
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());
|
tracing::debug!("No seed file found, creating at {}", file_path.display());
|
||||||
|
|
||||||
let random_seed = Seed::random()?;
|
// In debug mode, we allow the user to enter a seed manually.
|
||||||
random_seed.write_to(file_path.to_path_buf())?;
|
#[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.
|
/// 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;
|
type Writer = CapturingWriter;
|
||||||
|
|
||||||
fn make_writer(&self) -> Self::Writer {
|
fn make_writer(&self) -> Self::Writer {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue