From a46a154193a6508b15a3daed517cc1e8645c787e Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 28 Sep 2025 23:32:37 +0200 Subject: [PATCH 01/16] progress on dynamic docker compose yml generation --- swap-orchestrator/src/compose2.rs | 433 ++++++++++++++++++++++++++++++ swap-orchestrator/src/lib.rs | 2 + swap-orchestrator/src/main.rs | 60 ++++- swap-orchestrator/src/prompt.rs | 20 +- swap-orchestrator/src/writer.rs | 78 ++++++ 5 files changed, 571 insertions(+), 22 deletions(-) create mode 100644 swap-orchestrator/src/compose2.rs create mode 100644 swap-orchestrator/src/writer.rs diff --git a/swap-orchestrator/src/compose2.rs b/swap-orchestrator/src/compose2.rs new file mode 100644 index 0000000000..083b37b58a --- /dev/null +++ b/swap-orchestrator/src/compose2.rs @@ -0,0 +1,433 @@ +use std::{ + fmt::{Display, Write}, + path::PathBuf, + sync::Arc, +}; + +use url::Url; + +use crate::writer::IndentedWriter; + +/// Trait implemented by every part of a [`ComposeConfig`] +/// which writes that part of the config to an output. +trait WriteConfig { + fn write_to(&self, writer: &mut IndentedWriter); +} + +/// A Docker Compose config that can be written to a `docker-compose.yml` file. +/// +/// Create with [`ComposeConfig::new`] and add volumes and services +/// using [`ComposeConfig::add_volume`] and [`ComposeConfig::add_service`]. +#[derive(Debug, Clone)] +pub struct ComposeConfig { + services: Vec>, + volumes: Vec>, +} + +/// A service which may be added to a [`ComposeConfig`]. +#[derive(Debug, Clone)] +pub struct Service { + name: String, + depends_on: Vec>, + image_source: ImageSource, + exposed_ports: Vec, + volumes: Vec, + restart_type: RestartType, + entrypoint: Option, + command: Option, + stdin_open: Option, + tty: Option, +} + +/// Specify how to mount a specific path or volume of the host system to the container. +#[derive(Debug, Clone)] +pub struct Mount { + host_path: VolumeOrPath, + container_path: PathBuf, +} + +/// Host side of a mount expression. +/// Either a volume or a path to some directory/file. +#[derive(Debug, Clone)] +enum VolumeOrPath { + Volume(Arc), + Path(PathBuf), +} + +/// A volume that's part of a Docker Compose config. +/// Can only be obtained as `Arc` via [`ComposeConfig::add_volume`]. +#[derive(Debug, Clone)] +pub struct Volume { + name: String, +} + +/// Configure how to obtain the container image. +#[derive(Debug, Clone)] +pub enum ImageSource { + BuildFromSource { + /// Url to the git repo (may contain commit hash). + git_url: Url, + /// Relative path to Dockerfile from repo root. + dockerfile_path: String, + }, + PullFromRegistry { + /// Standard docker registry url. + image_url: String, + }, +} + +#[derive(Debug, Clone)] +pub struct Command { + flags: Vec, +} + +#[derive(Debug, Clone)] +pub struct Flag(pub String); + +/// Configure when to restart the service. +#[derive(Debug, Clone, Copy)] +pub enum RestartType { + UnlessStopped, +} + +impl ComposeConfig { + /// Add a volume to the config. + /// Returns a handle which can be used to reference this volume later. + pub fn add_volume(&mut self, name: impl Into) -> Arc { + let volume = Arc::new(Volume::new(name.into())); + self.volumes.push(volume.clone()); + + volume + } + + /// Add a service to the config. + /// Returns a handle which can be used to reference this service later. + /// + /// Create services using [`Service::new`]. + pub fn add_service(&mut self, service: Service) -> Arc { + let service = Arc::new(service); + self.services.push(service.clone()); + + service + } + + /// Finish this config and make it into a docker-compose.yml compatible string. + pub fn build(self) -> String { + let mut writer = IndentedWriter::new(); + self.write_to(&mut writer); + + writer.finish() + } +} + +impl Default for ComposeConfig { + fn default() -> ComposeConfig { + ComposeConfig { + services: Vec::new(), + volumes: Vec::new(), + } + } +} + +impl WriteConfig for ComposeConfig { + fn write_to(&self, writer: &mut IndentedWriter) { + writeln!(writer, "name: eigenwallet-maker").unwrap(); + writeln!(writer, "services:").unwrap(); + + writer.indented(|writer| { + for service in &self.services { + service.write_to(writer); + } + }); + + writeln!(writer, "volumnes:").unwrap(); + + writer.indented(|writer| { + for volume in &self.volumes { + writeln!(writer, "{}:", volume.name()).unwrap(); + } + }); + } +} + +impl Service { + /// Create a new Docker Compose service. Use the `with_*` methods to configure + /// it, before adding it to the config with [`ComposeConfig::add_service`]. + pub fn new(name: impl Into, image_source: ImageSource) -> Service { + let name: String = name.into(); + + Service { + name, + depends_on: Vec::new(), + exposed_ports: Vec::new(), + image_source, + command: None, + restart_type: RestartType::UnlessStopped, + volumes: Vec::new(), + entrypoint: None, + stdin_open: None, + tty: None, + } + } + + /// Expose a specific port of this service. + /// Can be called multiple times. + /// + /// Expands to the following: + /// ```docker ignore + /// expose: + /// - 0.0.0.0:{port}:{port} + /// ``` + /// + /// TODO: support mapping to other port + /// TODO: support exposing only on localhost + pub fn with_exposed_port(mut self, port: u16) -> Service { + self.exposed_ports.push(port); + + self + } + + /// Add a volume or other path to this service's container by + /// mounting it from the host system. + pub fn with_mount(mut self, mount: Mount) -> Service { + self.volumes.push(mount); + + self + } + + /// Add a dependency on another service. The other service will be listed + /// in the `depends_on` section of this service. + pub fn with_dependency(mut self, service: Arc) -> Service { + self.depends_on.push(service); + + self + } + + /// Set this service's `command` field. Further calls will override earlier values. + pub fn with_command(mut self, command: Command) -> Service { + self.command = Some(command); + + self + } + + /// Set `stdin_open` to an explicit value. + /// + /// Todo: find out default value + actual meaning. + pub fn with_stdin_open(mut self, stdin_open: bool) -> Service { + self.stdin_open = Some(stdin_open); + + self + } + + /// Set `tty` to an explicit value. + /// + /// Todo: find out default value + actual meaning. + pub fn with_tty(mut self, tty: bool) -> Service { + self.tty = Some(tty); + + self + } +} + +impl WriteConfig for Service { + fn write_to(&self, writer: &mut IndentedWriter) { + // {service_name}: + writeln!(writer, "{}:", &self.name).unwrap(); + + writer.indented(|writer| { + // container_name + writeln!(writer, "container_name: {}", &self.name).unwrap(); + // image/build + self.image_source.write_to(writer); + // restart + self.restart_type.write_to(writer); + + // depends_on (if specified) + if !self.depends_on.is_empty() { + writeln!(writer, "depends_on").unwrap(); + // write every individual dependency service + writer.indented(|writer| { + for dependency in &self.depends_on { + writeln!(writer, "- {}", &dependency.name).unwrap(); + } + }); + } + + // stdin_open (if specified) + if let Some(stdin_open) = self.stdin_open { + writeln!(writer, "stdin_open: {stdin_open}").unwrap(); + } + // tty (if specified) + if let Some(tty) = self.tty { + writeln!(writer, "tty: {tty}").unwrap(); + } + + // entrypoint (if specified) + if let Some(entrypoint) = &self.entrypoint { + writeln!(writer, "entrypoint: \"{}\"", entrypoint).unwrap(); + } + + // command (if specified) + if let Some(command) = &self.command { + command.write_to(writer); + } + + // volumes (if specified) + if !self.volumes.is_empty() { + writeln!(writer, "volumes:").unwrap(); + // write every individual mount + writer.indented(|writer| { + for mount in &self.volumes { + mount.write_to(writer); + } + }); + } + + // exposed ports (if specified) + if !self.exposed_ports.is_empty() { + writeln!(writer, "expose:").unwrap(); + + writer.indented(|writer| { + for port in &self.exposed_ports { + writeln!(writer, "- {port}").unwrap(); + } + }) + } + }); + } +} + +impl WriteConfig for ImageSource { + fn write_to(&self, writer: &mut IndentedWriter) { + match self { + ImageSource::BuildFromSource { + git_url, + dockerfile_path, + } => { + writeln!( + writer, + "build: {{ context: \"{git_url}\", dockerfile: \"{dockerfile_path}\" }}" + ) + .unwrap(); + } + ImageSource::PullFromRegistry { image_url } => { + writeln!(writer, "image: {image_url}").unwrap() + } + } + } +} + +impl Mount { + /// Mount a specific path from the host system to a specific path on the container system. + /// `container_path` must be an absolute path. + pub fn path(host_path: impl Into, container_path: impl Into) -> Mount { + Mount { + host_path: VolumeOrPath::Path(host_path.into()), + container_path: container_path.into(), + } + } + + /// Mount a volume as a root dir in the container. + /// For example, the volume `foo` will be mounted to `/foo/` in + /// the container. + pub fn volume(volume: Arc) -> Mount { + Mount { + host_path: VolumeOrPath::Volume(volume.clone()), + container_path: volume.as_root_dir(), + } + } + + /// Mount a volume to a specific path in the container system. + /// `container_path` must be an absolute path. + pub fn volume_to(volume: Arc, container_path: impl Into) -> Mount { + Mount { + host_path: VolumeOrPath::Volume(volume), + container_path: container_path.into(), + } + } +} + +impl WriteConfig for Mount { + fn write_to(&self, writer: &mut IndentedWriter) { + let host = match &self.host_path { + VolumeOrPath::Volume(volume) => volume.name.to_string(), + VolumeOrPath::Path(path) => path.to_string_lossy().to_string(), + }; + + let container = self.container_path.to_string_lossy().to_string(); + + writeln!(writer, "- {host}:{container}").expect("writing to a string doesn't fail") + } +} + +impl Volume { + /// Private: callers can only obtain an `Arc`, via + /// [`ComposeConfig::add_volume`]. + fn new(name: String) -> Self { + Self { name } + } + + /// Get the name of the volume. + pub fn name(&self) -> &str { + &self.name + } + + /// Get the path corresponding to a root directory named after the volume. + /// + /// ``` + /// use swap_orchestrator::compose2::*; + /// use std::path::PathBuf; + /// + /// let mut config = ComposeConfig::default(); + /// let my_volume = config.add_volume("foo"); + /// assert_eq!(my_volume.as_root_dir(), PathBuf::from("/foo/")); + /// ``` + pub fn as_root_dir(&self) -> PathBuf { + let mut path = PathBuf::from("/"); + path.push(&self.name); + + path + } +} + +impl WriteConfig for Volume { + fn write_to(&self, writer: &mut IndentedWriter) { + writeln!(writer, "{}:", &self.name).unwrap() + } +} + +impl WriteConfig for RestartType { + fn write_to(&self, writer: &mut IndentedWriter) { + let text = match self { + RestartType::UnlessStopped => "unless-stopped", + }; + writeln!(writer, "restart: {text}").unwrap(); + } +} + +impl Command { + /// Create a new command by combining flags. + pub fn new(flags: impl IntoIterator) -> Command { + Command { + flags: flags.into_iter().collect(), + } + } +} + +impl WriteConfig for Command { + fn write_to(&self, writer: &mut IndentedWriter) { + let flags = self + .flags + .iter() + .map(|flag| format!("{flag}")) + .collect::>() + .join(", "); + writeln!(writer, "command: [ {flags} ]").unwrap(); + } +} + +impl Display for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", self.0) + } +} diff --git a/swap-orchestrator/src/lib.rs b/swap-orchestrator/src/lib.rs index 8b20132996..c9287d1f08 100644 --- a/swap-orchestrator/src/lib.rs +++ b/swap-orchestrator/src/lib.rs @@ -1,3 +1,5 @@ pub mod compose; +pub mod compose2; pub mod containers; pub mod images; +pub mod writer; diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index c981cf9666..a374607b6b 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -4,8 +4,8 @@ mod images; mod prompt; use crate::compose::{ - IntoSpec, OrchestratorDirectories, OrchestratorImage, OrchestratorImages, OrchestratorInput, - OrchestratorNetworks, ASB_DATA_DIR, DOCKER_COMPOSE_FILE, + ASB_DATA_DIR, DOCKER_COMPOSE_FILE, IntoSpec, OrchestratorDirectories, OrchestratorImage, + OrchestratorImages, OrchestratorInput, OrchestratorNetworks, }; use std::path::PathBuf; use swap_env::config::{ @@ -13,10 +13,64 @@ use swap_env::config::{ }; use swap_env::prompt as config_prompt; use swap_env::{defaults::GetDefaults, env::Mainnet, env::Testnet}; +use swap_orchestrator::compose2::{Command, ComposeConfig, Flag, ImageSource, Mount, Service}; use url::Url; fn main() { - let (bitcoin_network, monero_network) = prompt::network(); + { + let mut config = ComposeConfig::default(); + + let monerod_data = config.add_volume("monero-data"); + let monerod = Service::new( + "monerod", + ImageSource::PullFromRegistry { + image_url: "ghcr.io/sethforprivacy/simple-monerod@sha256:f30e5706a335c384e4cf420215cbffd1196f0b3a11d4dd4e819fe3e0bca41ec5".to_string(), + }, + ).with_mount(Mount::volume(monerod_data.clone())) + .with_command(Command::new(vec![Flag("monerod".into())])) + .with_exposed_port(18081); + + let monerod = config.add_service(monerod); + + let asb_data = config.add_volume("asb_data"); + let asb = Service::new("asb", ImageSource::BuildFromSource { git_url: "https://github.com/eigenwallet/core.git#8b817d5efc32f380b4ec0102a5bad86f9c98a499".parse().unwrap(), dockerfile_path: "./swap-asb/Dockerfile".into() }) + .with_dependency(monerod.clone()) + .with_mount(Mount::volume(asb_data.clone())) + .with_mount(Mount::path("./config.toml", asb_data.as_root_dir().join("config.toml"))) + .with_exposed_port(9939); + + let asb = config.add_service(asb); + + // ... configure other services etc + + let yml_config = config.build(); + + // done + } + + return; + + let (mut bitcoin_network, mut monero_network) = + (bitcoin::Network::Bitcoin, monero::Network::Mainnet); + + for arg in std::env::args() { + match arg.as_str() { + "--help" => { + println!( + "Look at our documentation: https://github.com/eigenwallet/core/blob/master/swap-orchestrator/README.md" + ); + return; + } + "--testnet" => { + println!( + "Detected `--testnet` flag, switching to Bitcoin Testnet3 and Monero Stagenet" + ); + bitcoin_network = bitcoin::Network::Testnet; + monero_network = monero::Network::Stagenet; + } + _ => (), + } + } let defaults = match (bitcoin_network, monero_network) { (bitcoin::Network::Bitcoin, monero::Network::Mainnet) => { diff --git a/swap-orchestrator/src/prompt.rs b/swap-orchestrator/src/prompt.rs index 8508572906..b9d183ac62 100644 --- a/swap-orchestrator/src/prompt.rs +++ b/swap-orchestrator/src/prompt.rs @@ -1,4 +1,4 @@ -use dialoguer::{theme::ColorfulTheme, Select}; +use dialoguer::{Select, theme::ColorfulTheme}; use swap_env::prompt as config_prompt; use url::Url; @@ -20,24 +20,6 @@ pub enum ElectrumServerType { Remote(Vec), // Use a specific remote Electrum server } -pub fn network() -> (bitcoin::Network, monero::Network) { - let network = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Which network do you want to run on?") - .items(&[ - "Mainnet Bitcoin & Mainnet Monero", - "Testnet Bitcoin & Stagenet Monero", - ]) - .default(0) - .interact() - .expect("Failed to select network"); - - match network { - 0 => (bitcoin::Network::Bitcoin, monero::Network::Mainnet), - 1 => (bitcoin::Network::Testnet, monero::Network::Stagenet), - _ => unreachable!(), - } -} - #[allow(dead_code)] // will be used in the future pub fn build_type() -> BuildType { let build_type = Select::with_theme(&ColorfulTheme::default()) diff --git a/swap-orchestrator/src/writer.rs b/swap-orchestrator/src/writer.rs new file mode 100644 index 0000000000..bff82c1900 --- /dev/null +++ b/swap-orchestrator/src/writer.rs @@ -0,0 +1,78 @@ +use std::fmt::Write; + +/// Helper struct to make writing to a docker-compose.yml easier. +/// Implements [`std::fmt::Write`], so you can use the `write!` macro on it. +/// Keeps track of the current indentation level and +pub struct IndentedWriter { + /// Inner string buffer we write to under the hood. + buffer: String, + /// Current indentation level - we multiply this by two to get the number of spaces. + current_indentation: usize, +} + +impl IndentedWriter { + const SPACES_PER_INDENTATION: usize = 2; + const WHITESPACE: char = ' '; + + /// Start with a new, empty string and zero indentation. + pub fn new() -> IndentedWriter { + IndentedWriter { + buffer: String::new(), + current_indentation: 0, + } + } + + /// Finish writing and return the final String buffer. + pub fn finish(self) -> String { + self.buffer + } + + /// Get scoped access to the writer but with one more level of indentation. + /// + /// # Example + /// + /// ``` + /// use swap_orchestrator::writer::IndentedWriter; + /// use std::fmt::Write; + /// + /// let mut writer = IndentedWriter::new(); + /// writeln!(&mut writer, "version: 3"); + /// writeln!(&mut writer, "services:"); + /// + /// writer.indented(|writer| { + /// writeln!(writer, "monerod:"); + /// writer.indented(|writer| { + /// writeln!(writer, "container_name: monerod"); + /// }); + /// }); + /// + /// assert_eq!( + /// &writer.finish(), + /// "version: 3 + /// services: + /// monerod: + /// container_name: monerod + /// " + /// ) + /// ``` + pub fn indented(&mut self, closure: impl FnOnce(&mut IndentedWriter) -> T) -> T { + self.current_indentation += 1; + let result = closure(self); + // No underflow possible because we just increased the number and don't change + // it anywhere else + self.current_indentation -= 1; + + result + } +} + +impl Write for IndentedWriter { + fn write_str(&mut self, value: &str) -> std::fmt::Result { + // Todo: only prefix indentation if previous char was a newline? + let indentation = Self::WHITESPACE + .to_string() + .repeat(Self::SPACES_PER_INDENTATION * self.current_indentation); + + write!(&mut self.buffer, "{indentation}{value}") + } +} From 80e4e65bd42dc1e4716e379445ca4491ee1f23f8 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Mon, 29 Sep 2025 09:52:25 +0200 Subject: [PATCH 02/16] fix formatting --- swap-orchestrator/src/compose2.rs | 6 +++--- swap-orchestrator/src/main.rs | 7 +++++++ swap-orchestrator/src/writer.rs | 8 ++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/swap-orchestrator/src/compose2.rs b/swap-orchestrator/src/compose2.rs index 083b37b58a..98a9ef3783 100644 --- a/swap-orchestrator/src/compose2.rs +++ b/swap-orchestrator/src/compose2.rs @@ -140,7 +140,7 @@ impl WriteConfig for ComposeConfig { } }); - writeln!(writer, "volumnes:").unwrap(); + writeln!(writer, "volumes:").unwrap(); writer.indented(|writer| { for volume in &self.volumes { @@ -244,7 +244,7 @@ impl WriteConfig for Service { // depends_on (if specified) if !self.depends_on.is_empty() { - writeln!(writer, "depends_on").unwrap(); + writeln!(writer, "depends_on:").unwrap(); // write every individual dependency service writer.indented(|writer| { for dependency in &self.depends_on { @@ -311,7 +311,7 @@ impl WriteConfig for ImageSource { .unwrap(); } ImageSource::PullFromRegistry { image_url } => { - writeln!(writer, "image: {image_url}").unwrap() + writeln!(writer, "image: \"{image_url}\"").unwrap() } } } diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index a374607b6b..5bd06dab54 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -7,6 +7,8 @@ use crate::compose::{ ASB_DATA_DIR, DOCKER_COMPOSE_FILE, IntoSpec, OrchestratorDirectories, OrchestratorImage, OrchestratorImages, OrchestratorInput, OrchestratorNetworks, }; +use std::fs::File; +use std::io::Write; use std::path::PathBuf; use swap_env::config::{ Bitcoin, Config, ConfigNotInitialized, Data, Maker, Monero, Network, TorConf, @@ -45,6 +47,11 @@ fn main() { let yml_config = config.build(); + File::create("docker-compose.yml") + .unwrap() + .write_all(yml_config.as_bytes()) + .unwrap(); + // done } diff --git a/swap-orchestrator/src/writer.rs b/swap-orchestrator/src/writer.rs index bff82c1900..28880a983e 100644 --- a/swap-orchestrator/src/writer.rs +++ b/swap-orchestrator/src/writer.rs @@ -68,10 +68,14 @@ impl IndentedWriter { impl Write for IndentedWriter { fn write_str(&mut self, value: &str) -> std::fmt::Result { - // Todo: only prefix indentation if previous char was a newline? + let is_new_line = match self.buffer.chars().last() { + Some('\n') => true, + Some(_) => false, + None => true, + }; let indentation = Self::WHITESPACE .to_string() - .repeat(Self::SPACES_PER_INDENTATION * self.current_indentation); + .repeat(Self::SPACES_PER_INDENTATION * self.current_indentation * is_new_line as usize); write!(&mut self.buffer, "{indentation}{value}") } From c1c5ce58cef92a95dbae3cc6bb327048ada0a554 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 1 Oct 2025 14:10:03 +0200 Subject: [PATCH 03/16] progress --- swap-env/src/prompt.rs | 171 +++--- swap-orchestrator/src/compose.rs | 808 ++++++++++++++++------------ swap-orchestrator/src/compose2.rs | 433 --------------- swap-orchestrator/src/containers.rs | 366 +++++++++---- swap-orchestrator/src/images.rs | 15 - swap-orchestrator/src/lib.rs | 1 - swap-orchestrator/src/main.rs | 330 ++++-------- swap-orchestrator/src/prompt.rs | 123 ++++- 8 files changed, 1027 insertions(+), 1220 deletions(-) delete mode 100644 swap-orchestrator/src/compose2.rs diff --git a/swap-env/src/prompt.rs b/swap-env/src/prompt.rs index 6a6c30fc07..70f3ab9cdc 100644 --- a/swap-env/src/prompt.rs +++ b/swap-env/src/prompt.rs @@ -1,15 +1,15 @@ use std::path::{Path, PathBuf}; use crate::defaults::{ - default_rendezvous_points, DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, + DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, default_rendezvous_points, }; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use console::Style; use dialoguer::Confirm; -use dialoguer::{theme::ColorfulTheme, Input, Select}; +use dialoguer::{Input, Select, theme::ColorfulTheme}; use libp2p::Multiaddr; -use rust_decimal::prelude::FromPrimitive; use rust_decimal::Decimal; +use rust_decimal::prelude::FromPrimitive; use url::Url; /// Prompt user for data directory @@ -38,16 +38,27 @@ pub fn bitcoin_confirmation_target(default_target: u16) -> Result { /// Prompt user for listen addresses pub fn listen_addresses(default_listen_address: &Multiaddr) -> Result> { - let listen_addresses = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default") - .default(format!("{}", default_listen_address)) - .interact_text()?; + print_info_box(&[ + "If you also want your maker to be reachable over other addresses (or domains)", + "you can configure them now.", + ]); - listen_addresses - .split(',') - .map(|str| str.parse()) - .collect::, _>>() - .map_err(Into::into) + let mut addresses = vec![default_listen_address.clone()]; + + loop { + let listen_address: Multiaddr = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter additional multiaddress (enter to continue)") + .allow_empty(true) + .interact_text()?; + + if listen_address.is_empty() { + break; + } + + addresses.push(listen_address); + } + + Ok(addresses) } /// Prompt user for electrum RPC URLs @@ -112,7 +123,7 @@ pub fn electrum_rpc_urls(default_electrum_urls: &Vec) -> Result> { pub fn monero_daemon_url() -> Result> { let type_choice = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Do you want to use the Monero RPC pool or a remote node?") - .items(&["Use the Monero RPC pool", "Use a remote node"]) + .items(&["Use the Monero RPC pool", "Use a specific remote node"]) .default(0) .interact()?; @@ -132,16 +143,15 @@ pub fn monero_daemon_url() -> Result> { /// Prompt user for Tor hidden service registration pub fn tor_hidden_service() -> Result { print_info_box([ - "Your ASB needs to be reachable from the outside world to provide quotes to takers.", - "Your ASB can run a hidden service for itself. It'll be reachable at an .onion address.", - "You do not have to run a Tor daemon yourself. You do not have to manage anything.", - "This will hide your IP address and allow you to run from behind a firewall without opening ports.", + "After registering with rendezvous points, your maker needs to be reachable by takers.", + "Running a hidden service means you'll be reachable via a .onion address", + "- without leaking your ip address or requiring an open port.", ]); let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Do you want a Tor hidden service to be created?") + .with_prompt("Do you want a Tor hidden service to be created? It requires no additional setup on your end.") .items(&[ - "Yes, run a hidden service", + "Yes, run a hidden service (recommended)", "No, do not run a hidden service", ]) .default(0) @@ -153,7 +163,9 @@ pub fn tor_hidden_service() -> Result { /// Prompt user for minimum Bitcoin buy amount pub fn min_buy_amount() -> Result { let min_buy = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter minimum Bitcoin amount you are willing to accept per swap or hit enter to use default.") + .with_prompt( + "What's the minimum amount of Bitcoin you are willing to trade? (enter to use default)", + ) .default(DEFAULT_MIN_BUY_AMOUNT) .interact_text()?; bitcoin::Amount::from_btc(min_buy).map_err(Into::into) @@ -162,7 +174,9 @@ pub fn min_buy_amount() -> Result { /// Prompt user for maximum Bitcoin buy amount pub fn max_buy_amount() -> Result { let max_buy = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter maximum Bitcoin amount you are willing to accept per swap or hit enter to use default.") + .with_prompt( + "What's the maximum amount of Bitcoin you are willing to trade? (enter to use default)", + ) .default(DEFAULT_MAX_BUY_AMOUNT) .interact_text()?; @@ -172,7 +186,7 @@ pub fn max_buy_amount() -> Result { /// Prompt user for ask spread pub fn ask_spread() -> Result { let ask_spread = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter spread (in percent; value between 0.x and 1.0) to be used on top of the market rate or hit enter to use default.") + .with_prompt("What markup do you want to charge? 0.02 = 2% markup (enter to use default)") .default(DEFAULT_SPREAD) .interact_text()?; @@ -189,61 +203,82 @@ pub fn ask_spread() -> Result { /// Prompt user for rendezvous points pub fn rendezvous_points() -> Result> { let default_rendezvous_points = default_rendezvous_points(); - let mut info_lines = vec![ - "Your ASB can register with multiple rendezvous nodes for discoverability.".to_string(), - "They act as sort of bootstrap nodes for peer discovery within the peer-to-peer network." - .to_string(), - String::new(), - "The following rendezvous points are ran by community members. We recommend using them." - .to_string(), - String::new(), - ]; - info_lines.extend( - default_rendezvous_points - .iter() - .enumerate() - .map(|(i, point)| format!("{}: {}", i + 1, point)), - ); - print_info_box(info_lines); - // Ask if the user wants to use the default rendezvous points - let use_default_rendezvous_points = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Do you want to use the default rendezvous points? (y/n)") + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Choice { + ContinueWithDefaultNodes, + AddMyOwnNodes, + UseOnlyMyOwnNodes, + SeeDefaultNodes, + } + + impl TryFrom for Choice { + type Error = anyhow::Error; + + fn try_from(value: usize) -> std::result::Result { + Ok(match value { + 0 => Choice::ContinueWithDefaultNodes, + 1 => Choice::AddMyOwnNodes, + 2 => Choice::UseOnlyMyOwnNodes, + 3 => Choice::SeeDefaultNodes, + _ => bail!("unknown choice"), + }) + } + } + + let theme = ColorfulTheme::default(); + print_info_box(&[ + "For takers to trade with your maker, it needs to be discovered first.", + "This happens at 'rendezvous points', which are community run.", + "You can now choose with which of those nodes to connect.", + ]); + let input = Select::with_theme(&theme) + .with_prompt("How do you want to procede?") .items(&[ - "Use default rendezvous points", - "Do not use default rendezvous points", + "Connect to default rendezvous points (recommended)", + "Connect to default rendezvous points and also specify my own", + "Connect only to my own rendezvous point(s) (not recommended)", + "Print a list of the default rendezvous points", ]) - .default(0) - .interact()?; + .default(0); - let mut rendezvous_points = match use_default_rendezvous_points { - 0 => { - print_info_box(["You can now configure additional rendezvous points."]); - default_rendezvous_points - } + let mut choice: Choice = input.clone().interact()?.try_into()?; + + while choice == Choice::SeeDefaultNodes { + print_info_box(default_rendezvous_points.iter().map(|i| format!("{i}"))); + choice = input.clone().interact()?.try_into()?; + } + + let mut rendezvous_points = match choice { + Choice::AddMyOwnNodes | Choice::ContinueWithDefaultNodes => default_rendezvous_points, _ => Vec::new(), }; - let mut number = 1 + rendezvous_points.len(); - let mut done = false; - - while !done { - let prompt = format!( - "Enter the address for rendezvous node ({number}). Or just hit Enter to continue." - ); - let rendezvous_addr = Input::::with_theme(&ColorfulTheme::default()) - .with_prompt(prompt) + while matches!(choice, Choice::AddMyOwnNodes | Choice::UseOnlyMyOwnNodes) { + let address: Multiaddr = Input::with_theme(&theme) + .with_prompt("Enter an address of your rendezvous point (enter to continue)") .allow_empty(true) .interact_text()?; - if rendezvous_addr.is_empty() { - done = true; - } else if rendezvous_points.contains(&rendezvous_addr) { - println!("That rendezvous address is already in the list."); - } else { - rendezvous_points.push(rendezvous_addr); - number += 1; + if address.is_empty() { + if rendezvous_points.is_empty() { + print_info_box(&[ + "You currently have zero rendezvous points configured.", + "Your maker will not be reachable and not make any swaps if you continue.", + ]); + let choice = Confirm::with_theme(&theme) + .with_prompt("Do you wish to continue, even with your maker unreachable?") + .default(false) + .interact()?; + if !choice { + println!("Good choice. Aborting now, so you can restart"); + bail!("No rendezvous points configured"); + } + } + break; } + + rendezvous_points.push(address); } Ok(rendezvous_points) @@ -279,7 +314,7 @@ pub fn developer_tip() -> Result { } let developer_tip = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter developer tip percentage (in percent; value between 0.x and 1.0; 0.01 means 1% of the swap amount is donated)") + .with_prompt("Enter developer tip percentage (value between 0.00 and 1.00; 0.01 means 1% of the swap amount is donated)") .default(Decimal::from_f64(0.01).unwrap()) .interact_text()?; diff --git a/swap-orchestrator/src/compose.rs b/swap-orchestrator/src/compose.rs index aa6eec783d..dec2c39a44 100644 --- a/swap-orchestrator/src/compose.rs +++ b/swap-orchestrator/src/compose.rs @@ -1,411 +1,525 @@ -use crate::containers; -use crate::containers::*; -use crate::images::PINNED_GIT_REPOSITORY; -use compose_spec::Compose; use std::{ - fmt::{self, Display}, + fmt::{Display, Write}, path::PathBuf, + sync::Arc, }; -pub const ASB_DATA_DIR: &str = "/asb-data"; -pub const ASB_CONFIG_FILE: &str = "config.toml"; -pub const DOCKER_COMPOSE_FILE: &str = "./docker-compose.yml"; +use url::Url; -pub struct OrchestratorInput { - pub ports: OrchestratorPorts, - pub networks: OrchestratorNetworks, - pub images: OrchestratorImages, - pub directories: OrchestratorDirectories, +use crate::writer::IndentedWriter; + +/// Trait implemented by every part of a [`ComposeConfig`] +/// which writes that part of the config to an output. +trait WriteConfig { + fn write_to(&self, writer: &mut IndentedWriter); } -pub struct OrchestratorDirectories { - pub asb_data_dir: PathBuf, +/// A Docker Compose config that can be written to a `docker-compose.yml` file. +/// +/// Create with [`ComposeConfig::new`] and add volumes and services +/// using [`ComposeConfig::add_volume`] and [`ComposeConfig::add_service`]. +#[derive(Debug, Clone)] +pub struct ComposeConfig { + services: Vec>, + volumes: Vec>, } -#[derive(Clone)] -pub struct OrchestratorNetworks { - pub monero: MN, - pub bitcoin: BN, +/// A service which may be added to a [`ComposeConfig`]. +#[derive(Debug, Clone)] +pub struct Service { + name: String, + depends_on: Vec>, + image_source: ImageSource, + exposed_ports: Vec, + volumes: Vec, + restart_type: RestartType, + entrypoint: Option, + command: Option, + stdin_open: Option, + tty: Option, + enabled: bool, } -pub struct OrchestratorImages { - pub monerod: T, - pub electrs: T, - pub bitcoind: T, - pub asb: T, - pub asb_controller: T, - pub asb_tracing_logger: T, +/// Specify how to mount a specific path or volume of the host system to the container. +#[derive(Debug, Clone)] +pub struct Mount { + host_path: VolumeOrPath, + container_path: PathBuf, } -pub struct OrchestratorPorts { - pub monerod_rpc: u16, - pub bitcoind_rpc: u16, - pub bitcoind_p2p: u16, - pub electrs: u16, - pub asb_libp2p: u16, - pub asb_rpc_port: u16, +/// Host side of a mount expression. +/// Either a volume or a path to some directory/file. +#[derive(Debug, Clone)] +enum VolumeOrPath { + Volume(Arc), + Path(PathBuf), } -impl From> for OrchestratorPorts { - fn from(val: OrchestratorNetworks) -> Self { - match (val.monero, val.bitcoin) { - (monero::Network::Mainnet, bitcoin::Network::Bitcoin) => OrchestratorPorts { - monerod_rpc: 18081, - bitcoind_rpc: 8332, - bitcoind_p2p: 8333, - electrs: 50001, - asb_libp2p: 9939, - asb_rpc_port: 9944, - }, - (monero::Network::Stagenet, bitcoin::Network::Testnet) => OrchestratorPorts { - monerod_rpc: 38081, - bitcoind_rpc: 18332, - bitcoind_p2p: 18333, - electrs: 50001, - asb_libp2p: 9839, - asb_rpc_port: 9944, - }, - _ => panic!("Unsupported Bitcoin / Monero network combination"), - } +/// A volume that's part of a Docker Compose config. +/// Can only be obtained as `Arc` via [`ComposeConfig::add_volume`]. +#[derive(Debug, Clone)] +pub struct Volume { + name: String, +} + +/// Configure how to obtain the container image. +#[derive(Debug, Clone)] +pub enum ImageSource { + BuildFromSource { + /// Url to the git repo (may contain commit hash). + git_url: Url, + /// Relative path to Dockerfile from repo root. + dockerfile_path: String, + }, + PullFromRegistry { + /// Standard docker registry url. + image_url: String, + }, +} + +#[derive(Debug, Clone)] +pub struct Command { + flags: Vec, +} + +#[derive(Debug, Clone)] +pub struct Flag(String); + +/// Configure when to restart the service. +#[derive(Debug, Clone, Copy)] +pub enum RestartType { + UnlessStopped, +} + +impl ComposeConfig { + /// Add a volume to the config. + /// Returns a handle which can be used to reference this volume later. + pub fn add_volume(&mut self, name: impl Into) -> Arc { + let volume = Arc::new(Volume::new(name.into())); + self.volumes.push(volume.clone()); + + volume + } + + /// Add a service to the config. + /// Returns a handle which can be used to reference this service later. + /// + /// Create services using [`Service::new`]. + pub fn add_service(&mut self, service: Service) -> Arc { + let service = Arc::new(service); + self.services.push(service.clone()); + + service + } + + /// Finish this config and make it into a docker-compose.yml compatible string. + pub fn build(self) -> String { + let mut writer = IndentedWriter::new(); + self.write_to(&mut writer); + + let result = writer.finish(); + + let _: compose_spec::Compose = + serde_yaml::from_str(&result).expect("valid docker-compose.yml syntax"); + + result } } -impl From> for asb::Network { - fn from(val: OrchestratorNetworks) -> Self { - containers::asb::Network::new(val.monero, val.bitcoin) +impl Default for ComposeConfig { + fn default() -> ComposeConfig { + ComposeConfig { + services: Vec::new(), + volumes: Vec::new(), + } } } -impl From> for electrs::Network { - fn from(val: OrchestratorNetworks) -> Self { - containers::electrs::Network::new(val.bitcoin) +impl WriteConfig for ComposeConfig { + fn write_to(&self, writer: &mut IndentedWriter) { + writeln!(writer, "# This file is automatically @generated by the eigenwallet orchestrator.\n# It is not intended for manual editing.").unwrap(); + writeln!(writer, "name: eigenwallet-maker").unwrap(); + writeln!(writer, "services:").unwrap(); + + writer.indented(|writer| { + for service in &self.services { + service.write_to(writer); + } + }); + + writeln!(writer, "volumes:").unwrap(); + + writer.indented(|writer| { + for volume in &self.volumes { + writeln!(writer, "{}:", volume.name()).unwrap(); + } + }); } } -impl OrchestratorDirectories { - pub fn asb_config_path_inside_container(&self) -> PathBuf { - self.asb_data_dir.join(ASB_CONFIG_FILE) +impl Service { + /// Create a new Docker Compose service. Use the `with_*` methods to configure + /// it, before adding it to the config with [`ComposeConfig::add_service`]. + pub fn new(name: impl Into, image_source: ImageSource) -> Service { + let name: String = name.into(); + + Service { + name, + depends_on: Vec::new(), + exposed_ports: Vec::new(), + image_source, + command: None, + restart_type: RestartType::UnlessStopped, + volumes: Vec::new(), + entrypoint: None, + stdin_open: None, + tty: None, + enabled: true, + } + } + + /// Expose a specific port of this service. + /// Can be called multiple times. + /// + /// Expands to the following: + /// ```docker ignore + /// expose: + /// - 0.0.0.0:{port}:{port} + /// ``` + /// + /// TODO: support mapping to other port + /// TODO: support exposing only on localhost + pub fn with_exposed_port(mut self, port: u16) -> Service { + self.exposed_ports.push(port); + + self } - pub fn asb_config_path_on_host(&self) -> &'static str { - // The config file is in the same directory as the docker-compose.yml file - "./config.toml" + /// Add a volume or other path to this service's container by + /// mounting it from the host system. + pub fn with_mount(mut self, mount: Mount) -> Service { + self.volumes.push(mount); + + self + } + + /// Add a dependency on another service. The other service will be listed + /// in the `depends_on` section of this service. + pub fn with_dependency(mut self, service: Arc) -> Service { + self.depends_on.push(service); + + self } - pub fn asb_config_path_on_host_as_path_buf(&self) -> PathBuf { - PathBuf::from(self.asb_config_path_on_host()) + /// Set this service's `command` field. Further calls will override earlier values. + pub fn with_command(mut self, command: Command) -> Service { + self.command = Some(command); + + self + } + + /// Set `stdin_open` to an explicit value. + /// + /// Todo: find out default value + actual meaning. + pub fn with_stdin_open(mut self, stdin_open: bool) -> Service { + self.stdin_open = Some(stdin_open); + + self + } + + /// Set `tty` to an explicit value. + /// + /// Todo: find out default value + actual meaning. + pub fn with_tty(mut self, tty: bool) -> Service { + self.tty = Some(tty); + + self + } + + /// Set whether this service should be enabled. + /// + /// Is represented in the `docker-compose.yml` as + /// `profiles: ['disabled']`, if set to false. + pub fn with_enabled(mut self, enabled: bool) -> Service { + self.enabled = enabled; + + self + } + + /// Get the name of the service. + pub fn name(&self) -> &str { + &self.name + } + + /// Check whether the service is currently enabled. + pub fn is_enabled(&self) -> bool { + self.enabled } } -/// See: https://docs.docker.com/reference/compose-file/build/#illustrative-example -#[derive(Debug, Clone)] -pub struct DockerBuildInput { - // Usually this is the root of the Cargo workspace - pub context: &'static str, - // Usually this is the path to the Dockerfile - pub dockerfile: &'static str, +impl WriteConfig for Service { + fn write_to(&self, writer: &mut IndentedWriter) { + // {service_name}: + writeln!(writer, "{}:", &self.name).unwrap(); + + writer.indented(|writer| { + // container_name + writeln!(writer, "container_name: {}", &self.name).unwrap(); + // image/build + self.image_source.write_to(writer); + // restart + self.restart_type.write_to(writer); + + // depends_on (if specified) + if !self.depends_on.is_empty() { + writeln!(writer, "depends_on:").unwrap(); + // write every individual dependency service + writer.indented(|writer| { + for dependency in &self.depends_on { + writeln!(writer, "- {}", &dependency.name).unwrap(); + } + }); + } + + // stdin_open (if specified) + if let Some(stdin_open) = self.stdin_open { + writeln!(writer, "stdin_open: {stdin_open}").unwrap(); + } + // tty (if specified) + if let Some(tty) = self.tty { + writeln!(writer, "tty: {tty}").unwrap(); + } + + // entrypoint (if specified) + if let Some(entrypoint) = &self.entrypoint { + writeln!(writer, "entrypoint: \"{}\"", entrypoint).unwrap(); + } + + // command (if specified) + if let Some(command) = &self.command { + command.write_to(writer); + } + + // volumes (if specified) + if !self.volumes.is_empty() { + writeln!(writer, "volumes:").unwrap(); + // write every individual mount + writer.indented(|writer| { + for mount in &self.volumes { + mount.write_to(writer); + } + }); + } + + // exposed ports (if specified) + if !self.exposed_ports.is_empty() { + writeln!(writer, "expose:").unwrap(); + + writer.indented(|writer| { + for port in &self.exposed_ports { + writeln!(writer, "- {port}").unwrap(); + } + }) + } + + // Add the "disabled" profile if the service was disabled + // -> service isn't started unless specifically specified + if !self.enabled { + writeln!(writer, "profiles: [\"disabled\"]").unwrap(); + } + }); + } } -/// Specified a docker image to use -/// The image can either be pulled from a registry or built from source -pub enum OrchestratorImage { - Registry(String), - Build(DockerBuildInput), +impl ImageSource { + pub fn from_registry(image_url: impl Into) -> ImageSource { + ImageSource::PullFromRegistry { + image_url: image_url.into(), + } + } + + pub fn from_source(git_url: Url, dockerfile_path: impl Into) -> ImageSource { + ImageSource::BuildFromSource { + git_url, + dockerfile_path: dockerfile_path.into(), + } + } } -#[macro_export] -macro_rules! flag { - ($flag:expr) => { - Flag(Some($flag.to_string())) - }; - ($flag:expr, $($args:expr),*) => { - flag!(format!($flag, $($args),*)) - }; +impl WriteConfig for ImageSource { + fn write_to(&self, writer: &mut IndentedWriter) { + match self { + ImageSource::BuildFromSource { + git_url, + dockerfile_path, + } => { + writeln!( + writer, + "build: {{ context: \"{git_url}\", dockerfile: \"{dockerfile_path}\" }}" + ) + .unwrap(); + } + ImageSource::PullFromRegistry { image_url } => { + writeln!(writer, "image: \"{image_url}\"").unwrap() + } + } + } } -macro_rules! command { - ($command:expr $(, $flag:expr)* $(,)?) => { - Flags(vec![flag!($command) $(, $flag)*]) - }; +impl Mount { + /// Mount a specific path from the host system to a specific path on the container system. + /// `container_path` must be an absolute path. + pub fn path(host_path: impl Into, container_path: impl Into) -> Mount { + Mount { + host_path: VolumeOrPath::Path(host_path.into()), + container_path: container_path.into(), + } + } + + /// Mount a volume as a root dir in the container. + /// For example, the volume `foo` will be mounted to `/foo/` in + /// the container. + pub fn volume(volume: &Arc) -> Mount { + Mount { + host_path: VolumeOrPath::Volume(volume.clone()), + container_path: volume.as_root_dir(), + } + } + + /// Mount a volume to a specific path in the container system. + /// `container_path` must be an absolute path. + pub fn volume_to(volume: &Arc, container_path: impl Into) -> Mount { + Mount { + host_path: VolumeOrPath::Volume(volume.clone()), + container_path: container_path.into(), + } + } } -fn build(input: OrchestratorInput) -> String { - // Every docker compose project has a name - // The name is prefixed to the container names - // See: https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name - let project_name = format!( - "{}_monero_{}_bitcoin", - input.networks.monero.to_display(), - input.networks.bitcoin.to_display() - ); - - let asb_config_path = PathBuf::from(ASB_DATA_DIR).join(ASB_CONFIG_FILE); - let asb_network: asb::Network = input.networks.clone().into(); - - let command_asb = command![ - "asb", - asb_network.to_flag(), - flag!("--config={}", asb_config_path.display()), - flag!("start"), - flag!("--rpc-bind-port={}", input.ports.asb_rpc_port), - flag!("--rpc-bind-host=0.0.0.0"), - ]; - - let command_monerod = command![ - "monerod", - input.networks.monero.to_flag(), - flag!("--rpc-bind-ip=0.0.0.0"), - flag!("--rpc-bind-port={}", input.ports.monerod_rpc), - flag!("--data-dir=/monerod-data/"), - flag!("--confirm-external-bind"), - flag!("--restricted-rpc"), - flag!("--non-interactive"), - flag!("--enable-dns-blocklist"), - ]; - - let command_bitcoind = command![ - "bitcoind", - input.networks.bitcoin.to_flag(), - flag!("-rpcallowip=0.0.0.0/0"), - flag!("-rpcbind=0.0.0.0:{}", input.ports.bitcoind_rpc), - flag!("-bind=0.0.0.0:{}", input.ports.bitcoind_p2p), - flag!("-datadir=/bitcoind-data/"), - flag!("-dbcache=16384"), - // These are required for electrs - // See: See: https://github.com/romanz/electrs/blob/master/doc/config.md#bitcoind-configuration - flag!("-server=1"), - flag!("-prune=0"), - flag!("-txindex=1"), - ]; - - let electrs_network: containers::electrs::Network = input.networks.clone().into(); - - let command_electrs = command![ - "electrs", - electrs_network.to_flag(), - flag!("--daemon-dir=/bitcoind-data/"), - flag!("--db-dir=/electrs-data/db"), - flag!("--daemon-rpc-addr=bitcoind:{}", input.ports.bitcoind_rpc), - flag!("--daemon-p2p-addr=bitcoind:{}", input.ports.bitcoind_p2p), - flag!("--electrum-rpc-addr=0.0.0.0:{}", input.ports.electrs), - flag!("--log-filters=INFO"), - ]; - - let command_asb_controller = command![ - "asb-controller", - flag!("--url=http://asb:{}", input.ports.asb_rpc_port), - ]; - - let command_asb_tracing_logger = command![ - "sh", - flag!("-c"), - flag!("tail -f /asb-data/logs/tracing*.log"), - ]; - - let date = chrono::Utc::now() - .format("%Y-%m-%d %H:%M:%S UTC") - .to_string(); - - let compose_str = format!( - "\ -# This file was auto-generated by `orchestrator` on {date} -# -# It is pinned to build the `asb` and `asb-controller` images from this commit: -# {PINNED_GIT_REPOSITORY} -# -# If the code does not match the hash, the build will fail. This ensures that the code cannot be altered by Github. -# The compiled `orchestrator` has this hash burned into the binary. -# -# To update the `asb` and `asb-controller` images, you need to either: -# - re-compile the `orchestrator` binary from a commit from Github -# - download a newer pre-compiled version of the `orchestrator` binary from Github. -# -# After updating the `orchestrator` binary, re-generate the compose file by running `orchestrator` again. -# -# The used images for `bitcoind`, `monerod`, `electrs` are pinned to specific hashes which prevents them from being altered by the Docker registry. -# -# Please check for new releases regularly. Breaking network changes are rare, but they do happen from time to time. -name: {project_name} -services: - monerod: - container_name: monerod - {image_monerod} - restart: unless-stopped - user: root - volumes: - - 'monerod-data:/monerod-data/' - expose: - - {port_monerod_rpc} - entrypoint: '' - command: {command_monerod} - bitcoind: - container_name: bitcoind - {image_bitcoind} - restart: unless-stopped - volumes: - - 'bitcoind-data:/bitcoind-data/' - expose: - - {port_bitcoind_rpc} - - {port_bitcoind_p2p} - user: root - entrypoint: '' - command: {command_bitcoind} - electrs: - container_name: electrs - {image_electrs} - restart: unless-stopped - user: root - depends_on: - - bitcoind - volumes: - - 'bitcoind-data:/bitcoind-data' - - 'electrs-data:/electrs-data' - expose: - - {electrs_port} - entrypoint: '' - command: {command_electrs} - asb: - container_name: asb - {image_asb} - restart: unless-stopped - depends_on: - - electrs - volumes: - - '{asb_config_path_on_host}:{asb_config_path_inside_container}' - - 'asb-data:{asb_data_dir}' - ports: - - '0.0.0.0:{asb_port}:{asb_port}' - entrypoint: '' - command: {command_asb} - asb-controller: - container_name: asb-controller - {image_asb_controller} - stdin_open: true - tty: true - restart: unless-stopped - depends_on: - - asb - entrypoint: '' - command: {command_asb_controller} - asb-tracing-logger: - container_name: asb-tracing-logger - {image_asb_tracing_logger} - restart: unless-stopped - depends_on: - - asb - volumes: - - 'asb-data:/asb-data:ro' - entrypoint: '' - command: {command_asb_tracing_logger} -volumes: - monerod-data: - bitcoind-data: - electrs-data: - asb-data: -", - port_monerod_rpc = input.ports.monerod_rpc, - port_bitcoind_rpc = input.ports.bitcoind_rpc, - port_bitcoind_p2p = input.ports.bitcoind_p2p, - electrs_port = input.ports.electrs, - asb_port = input.ports.asb_libp2p, - image_monerod = input.images.monerod.to_image_attribute(), - image_electrs = input.images.electrs.to_image_attribute(), - image_bitcoind = input.images.bitcoind.to_image_attribute(), - image_asb = input.images.asb.to_image_attribute(), - image_asb_controller = input.images.asb_controller.to_image_attribute(), - image_asb_tracing_logger = input.images.asb_tracing_logger.to_image_attribute(), - asb_data_dir = input.directories.asb_data_dir.display(), - asb_config_path_on_host = input.directories.asb_config_path_on_host(), - asb_config_path_inside_container = input.directories.asb_config_path_inside_container().display(), - ); - - validate_compose(&compose_str); - - compose_str +impl WriteConfig for Mount { + fn write_to(&self, writer: &mut IndentedWriter) { + let host = match &self.host_path { + VolumeOrPath::Volume(volume) => volume.name.to_string(), + VolumeOrPath::Path(path) => path.to_string_lossy().to_string(), + }; + + let container = self.container_path.to_string_lossy().to_string(); + + writeln!(writer, "- {host}:{container}").expect("writing to a string doesn't fail") + } } -pub struct Flags(Vec); +impl Volume { + /// Private: callers can only obtain an `Arc`, via + /// [`ComposeConfig::add_volume`]. + fn new(name: String) -> Self { + Self { name } + } -/// Displays a list of flags into the "Exec form" supported by Docker -/// This is documented here: -/// https://docs.docker.com/reference/dockerfile/#exec-form -/// -/// E.g ["/bin/bash", "-c", "echo hello"] -impl Display for Flags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Collect all non-none flags - let flags = self - .0 - .iter() - .filter_map(|f| f.0.as_ref()) - .collect::>(); - - // Put the " around each flag, join with a comma, put the whole thing in [] - write!( - f, - "[{}]", - flags - .into_iter() - .map(|f| format!("\"{}\"", f)) - .collect::>() - .join(",") - ) + /// Get the name of the volume. + pub fn name(&self) -> &str { + &self.name + } + + /// Get the path corresponding to a root directory named after the volume. + /// + /// ``` + /// use swap_orchestrator::compose2::*; + /// use std::path::PathBuf; + /// + /// let mut config = ComposeConfig::default(); + /// let my_volume = config.add_volume("foo"); + /// assert_eq!(my_volume.as_root_dir(), PathBuf::from("/foo/")); + /// ``` + pub fn as_root_dir(&self) -> PathBuf { + let mut path = PathBuf::from("/"); + path.push(&self.name); + + path } } -pub struct Flag(pub Option); +impl WriteConfig for Volume { + fn write_to(&self, writer: &mut IndentedWriter) { + writeln!(writer, "{}:", &self.name).unwrap() + } +} -impl Display for Flag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(s) = &self.0 { - return write!(f, "{}", s); +impl WriteConfig for RestartType { + fn write_to(&self, writer: &mut IndentedWriter) { + let text = match self { + RestartType::UnlessStopped => "unless-stopped", + }; + writeln!(writer, "restart: {text}").unwrap(); + } +} + +impl Command { + /// Create a new command by combining flags. + pub fn new(flags: impl IntoIterator) -> Command { + Command { + flags: flags.into_iter().collect(), } + } - Ok(()) + /// Add a flag dynamically. + pub fn add_flag(&mut self, flag: impl Into) { + self.flags.push(flag.into()); } } -pub trait IntoFlag { - /// Converts into a flag that can be used in a docker compose file - fn to_flag(self) -> Flag; - /// Converts into a string that can be used for display purposes - fn to_display(self) -> &'static str; +impl WriteConfig for Command { + fn write_to(&self, writer: &mut IndentedWriter) { + let flags = self + .flags + .iter() + .map(|flag| format!("{flag}")) + .collect::>() + .join(", "); + writeln!(writer, "command: [ {flags} ]").unwrap(); + } +} + +#[macro_export] +macro_rules! flag { + ($flag:expr) => { + crate::compose::Flag::new(format!($flag)) + }; + ($flag:expr, $($args:expr),*) => { + crate::compose::Flag::new(format!($flag, $($args),*)) + }; } -pub trait IntoSpec { - fn to_spec(self) -> String; +#[macro_export] +macro_rules! command { + ($command:expr $(, $flag:expr)* $(,)?) => { + crate::compose::Command::new(vec![flag!($command) $(, $flag)*]) + }; } -impl IntoSpec for OrchestratorInput { - fn to_spec(self) -> String { - build(self) +impl Flag { + pub fn new(value: impl Into) -> Flag { + Flag(value.into()) } } -/// Converts something into either a: -/// - image: -/// - build: -pub trait IntoImageAttribute { - fn to_image_attribute(self) -> String; +impl Display for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", self.0) + } } -impl IntoImageAttribute for OrchestratorImage { - fn to_image_attribute(self) -> String { - match self { - OrchestratorImage::Registry(image) => format!("image: {}", image), - OrchestratorImage::Build(input) => format!( - r#"build: {{ context: "{}", dockerfile: "{}" }}"#, - input.context, input.dockerfile - ), - } +impl From for Flag { + fn from(value: String) -> Self { + Flag::new(value) } } -fn validate_compose(compose_str: &str) { - serde_yaml::from_str::(compose_str).unwrap_or_else(|_| { - panic!( - "Expected generated compose spec to be valid. But it was not. This is the spec: \n\n{}", - compose_str - ) - }); +impl IntoIterator for Flag { + type Item = Flag; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + vec![self].into_iter() + } } diff --git a/swap-orchestrator/src/compose2.rs b/swap-orchestrator/src/compose2.rs deleted file mode 100644 index 98a9ef3783..0000000000 --- a/swap-orchestrator/src/compose2.rs +++ /dev/null @@ -1,433 +0,0 @@ -use std::{ - fmt::{Display, Write}, - path::PathBuf, - sync::Arc, -}; - -use url::Url; - -use crate::writer::IndentedWriter; - -/// Trait implemented by every part of a [`ComposeConfig`] -/// which writes that part of the config to an output. -trait WriteConfig { - fn write_to(&self, writer: &mut IndentedWriter); -} - -/// A Docker Compose config that can be written to a `docker-compose.yml` file. -/// -/// Create with [`ComposeConfig::new`] and add volumes and services -/// using [`ComposeConfig::add_volume`] and [`ComposeConfig::add_service`]. -#[derive(Debug, Clone)] -pub struct ComposeConfig { - services: Vec>, - volumes: Vec>, -} - -/// A service which may be added to a [`ComposeConfig`]. -#[derive(Debug, Clone)] -pub struct Service { - name: String, - depends_on: Vec>, - image_source: ImageSource, - exposed_ports: Vec, - volumes: Vec, - restart_type: RestartType, - entrypoint: Option, - command: Option, - stdin_open: Option, - tty: Option, -} - -/// Specify how to mount a specific path or volume of the host system to the container. -#[derive(Debug, Clone)] -pub struct Mount { - host_path: VolumeOrPath, - container_path: PathBuf, -} - -/// Host side of a mount expression. -/// Either a volume or a path to some directory/file. -#[derive(Debug, Clone)] -enum VolumeOrPath { - Volume(Arc), - Path(PathBuf), -} - -/// A volume that's part of a Docker Compose config. -/// Can only be obtained as `Arc` via [`ComposeConfig::add_volume`]. -#[derive(Debug, Clone)] -pub struct Volume { - name: String, -} - -/// Configure how to obtain the container image. -#[derive(Debug, Clone)] -pub enum ImageSource { - BuildFromSource { - /// Url to the git repo (may contain commit hash). - git_url: Url, - /// Relative path to Dockerfile from repo root. - dockerfile_path: String, - }, - PullFromRegistry { - /// Standard docker registry url. - image_url: String, - }, -} - -#[derive(Debug, Clone)] -pub struct Command { - flags: Vec, -} - -#[derive(Debug, Clone)] -pub struct Flag(pub String); - -/// Configure when to restart the service. -#[derive(Debug, Clone, Copy)] -pub enum RestartType { - UnlessStopped, -} - -impl ComposeConfig { - /// Add a volume to the config. - /// Returns a handle which can be used to reference this volume later. - pub fn add_volume(&mut self, name: impl Into) -> Arc { - let volume = Arc::new(Volume::new(name.into())); - self.volumes.push(volume.clone()); - - volume - } - - /// Add a service to the config. - /// Returns a handle which can be used to reference this service later. - /// - /// Create services using [`Service::new`]. - pub fn add_service(&mut self, service: Service) -> Arc { - let service = Arc::new(service); - self.services.push(service.clone()); - - service - } - - /// Finish this config and make it into a docker-compose.yml compatible string. - pub fn build(self) -> String { - let mut writer = IndentedWriter::new(); - self.write_to(&mut writer); - - writer.finish() - } -} - -impl Default for ComposeConfig { - fn default() -> ComposeConfig { - ComposeConfig { - services: Vec::new(), - volumes: Vec::new(), - } - } -} - -impl WriteConfig for ComposeConfig { - fn write_to(&self, writer: &mut IndentedWriter) { - writeln!(writer, "name: eigenwallet-maker").unwrap(); - writeln!(writer, "services:").unwrap(); - - writer.indented(|writer| { - for service in &self.services { - service.write_to(writer); - } - }); - - writeln!(writer, "volumes:").unwrap(); - - writer.indented(|writer| { - for volume in &self.volumes { - writeln!(writer, "{}:", volume.name()).unwrap(); - } - }); - } -} - -impl Service { - /// Create a new Docker Compose service. Use the `with_*` methods to configure - /// it, before adding it to the config with [`ComposeConfig::add_service`]. - pub fn new(name: impl Into, image_source: ImageSource) -> Service { - let name: String = name.into(); - - Service { - name, - depends_on: Vec::new(), - exposed_ports: Vec::new(), - image_source, - command: None, - restart_type: RestartType::UnlessStopped, - volumes: Vec::new(), - entrypoint: None, - stdin_open: None, - tty: None, - } - } - - /// Expose a specific port of this service. - /// Can be called multiple times. - /// - /// Expands to the following: - /// ```docker ignore - /// expose: - /// - 0.0.0.0:{port}:{port} - /// ``` - /// - /// TODO: support mapping to other port - /// TODO: support exposing only on localhost - pub fn with_exposed_port(mut self, port: u16) -> Service { - self.exposed_ports.push(port); - - self - } - - /// Add a volume or other path to this service's container by - /// mounting it from the host system. - pub fn with_mount(mut self, mount: Mount) -> Service { - self.volumes.push(mount); - - self - } - - /// Add a dependency on another service. The other service will be listed - /// in the `depends_on` section of this service. - pub fn with_dependency(mut self, service: Arc) -> Service { - self.depends_on.push(service); - - self - } - - /// Set this service's `command` field. Further calls will override earlier values. - pub fn with_command(mut self, command: Command) -> Service { - self.command = Some(command); - - self - } - - /// Set `stdin_open` to an explicit value. - /// - /// Todo: find out default value + actual meaning. - pub fn with_stdin_open(mut self, stdin_open: bool) -> Service { - self.stdin_open = Some(stdin_open); - - self - } - - /// Set `tty` to an explicit value. - /// - /// Todo: find out default value + actual meaning. - pub fn with_tty(mut self, tty: bool) -> Service { - self.tty = Some(tty); - - self - } -} - -impl WriteConfig for Service { - fn write_to(&self, writer: &mut IndentedWriter) { - // {service_name}: - writeln!(writer, "{}:", &self.name).unwrap(); - - writer.indented(|writer| { - // container_name - writeln!(writer, "container_name: {}", &self.name).unwrap(); - // image/build - self.image_source.write_to(writer); - // restart - self.restart_type.write_to(writer); - - // depends_on (if specified) - if !self.depends_on.is_empty() { - writeln!(writer, "depends_on:").unwrap(); - // write every individual dependency service - writer.indented(|writer| { - for dependency in &self.depends_on { - writeln!(writer, "- {}", &dependency.name).unwrap(); - } - }); - } - - // stdin_open (if specified) - if let Some(stdin_open) = self.stdin_open { - writeln!(writer, "stdin_open: {stdin_open}").unwrap(); - } - // tty (if specified) - if let Some(tty) = self.tty { - writeln!(writer, "tty: {tty}").unwrap(); - } - - // entrypoint (if specified) - if let Some(entrypoint) = &self.entrypoint { - writeln!(writer, "entrypoint: \"{}\"", entrypoint).unwrap(); - } - - // command (if specified) - if let Some(command) = &self.command { - command.write_to(writer); - } - - // volumes (if specified) - if !self.volumes.is_empty() { - writeln!(writer, "volumes:").unwrap(); - // write every individual mount - writer.indented(|writer| { - for mount in &self.volumes { - mount.write_to(writer); - } - }); - } - - // exposed ports (if specified) - if !self.exposed_ports.is_empty() { - writeln!(writer, "expose:").unwrap(); - - writer.indented(|writer| { - for port in &self.exposed_ports { - writeln!(writer, "- {port}").unwrap(); - } - }) - } - }); - } -} - -impl WriteConfig for ImageSource { - fn write_to(&self, writer: &mut IndentedWriter) { - match self { - ImageSource::BuildFromSource { - git_url, - dockerfile_path, - } => { - writeln!( - writer, - "build: {{ context: \"{git_url}\", dockerfile: \"{dockerfile_path}\" }}" - ) - .unwrap(); - } - ImageSource::PullFromRegistry { image_url } => { - writeln!(writer, "image: \"{image_url}\"").unwrap() - } - } - } -} - -impl Mount { - /// Mount a specific path from the host system to a specific path on the container system. - /// `container_path` must be an absolute path. - pub fn path(host_path: impl Into, container_path: impl Into) -> Mount { - Mount { - host_path: VolumeOrPath::Path(host_path.into()), - container_path: container_path.into(), - } - } - - /// Mount a volume as a root dir in the container. - /// For example, the volume `foo` will be mounted to `/foo/` in - /// the container. - pub fn volume(volume: Arc) -> Mount { - Mount { - host_path: VolumeOrPath::Volume(volume.clone()), - container_path: volume.as_root_dir(), - } - } - - /// Mount a volume to a specific path in the container system. - /// `container_path` must be an absolute path. - pub fn volume_to(volume: Arc, container_path: impl Into) -> Mount { - Mount { - host_path: VolumeOrPath::Volume(volume), - container_path: container_path.into(), - } - } -} - -impl WriteConfig for Mount { - fn write_to(&self, writer: &mut IndentedWriter) { - let host = match &self.host_path { - VolumeOrPath::Volume(volume) => volume.name.to_string(), - VolumeOrPath::Path(path) => path.to_string_lossy().to_string(), - }; - - let container = self.container_path.to_string_lossy().to_string(); - - writeln!(writer, "- {host}:{container}").expect("writing to a string doesn't fail") - } -} - -impl Volume { - /// Private: callers can only obtain an `Arc`, via - /// [`ComposeConfig::add_volume`]. - fn new(name: String) -> Self { - Self { name } - } - - /// Get the name of the volume. - pub fn name(&self) -> &str { - &self.name - } - - /// Get the path corresponding to a root directory named after the volume. - /// - /// ``` - /// use swap_orchestrator::compose2::*; - /// use std::path::PathBuf; - /// - /// let mut config = ComposeConfig::default(); - /// let my_volume = config.add_volume("foo"); - /// assert_eq!(my_volume.as_root_dir(), PathBuf::from("/foo/")); - /// ``` - pub fn as_root_dir(&self) -> PathBuf { - let mut path = PathBuf::from("/"); - path.push(&self.name); - - path - } -} - -impl WriteConfig for Volume { - fn write_to(&self, writer: &mut IndentedWriter) { - writeln!(writer, "{}:", &self.name).unwrap() - } -} - -impl WriteConfig for RestartType { - fn write_to(&self, writer: &mut IndentedWriter) { - let text = match self { - RestartType::UnlessStopped => "unless-stopped", - }; - writeln!(writer, "restart: {text}").unwrap(); - } -} - -impl Command { - /// Create a new command by combining flags. - pub fn new(flags: impl IntoIterator) -> Command { - Command { - flags: flags.into_iter().collect(), - } - } -} - -impl WriteConfig for Command { - fn write_to(&self, writer: &mut IndentedWriter) { - let flags = self - .flags - .iter() - .map(|flag| format!("{flag}")) - .collect::>() - .join(", "); - writeln!(writer, "command: [ {flags} ]").unwrap(); - } -} - -impl Display for Flag { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "\"{}\"", self.0) - } -} diff --git a/swap-orchestrator/src/containers.rs b/swap-orchestrator/src/containers.rs index 0fee149a40..0e45f060a0 100644 --- a/swap-orchestrator/src/containers.rs +++ b/swap-orchestrator/src/containers.rs @@ -1,127 +1,279 @@ +use std::{path::PathBuf, sync::Arc}; + ///! This meta module describes **how to run** containers /// /// Currently this only includes which flags we need to pass to the binaries -use crate::compose::{Flag, IntoFlag}; - -pub mod bitcoind { - use super::*; - - impl IntoFlag for bitcoin::Network { - /// This is documented here: - /// https://www.mankier.com/1/bitcoind - fn to_flag(self) -> Flag { - Flag(Some(match self { - bitcoin::Network::Bitcoin => "-chain=main".to_string(), - bitcoin::Network::Testnet => "-chain=test".to_string(), - _ => panic!("Only Mainnet and Testnet are supported"), - })) - } - - fn to_display(self) -> &'static str { - match self { - bitcoin::Network::Bitcoin => "mainnet", - bitcoin::Network::Testnet => "testnet", - _ => panic!("Only Mainnet and Testnet are supported"), - } - } - } +use crate::{ + command, + compose::{ComposeConfig, Flag, ImageSource, Mount, Service, Volume}, + flag, + images::{self, PINNED_GIT_REPOSITORY}, +}; + +// Important: don't add slashes or anything here +// Todo: find better way to do that +const MONEROD_DATA: &str = "monerod-data"; +const BITCOIN_DATA: &str = "bitcoin-data"; +const ELECTRS_DATA: &str = "electrs-data"; +const ASB_DATA: &str = "asb-data"; + +/// Add all the services/volumes to the compose config +#[allow(unused_variables)] +pub fn add_maker_services( + compose: &mut ComposeConfig, + bitcoin_network: bitcoin::Network, + monero_network: monero::Network, + create_full_bitcoin_node: bool, + create_full_monero_node: bool, +) { + let (monerod_data, monerod, monerod_rpc_port) = + monerod(compose, monero_network, create_full_monero_node); + let (bitcoind_data, bitcoind, bitcoind_rpc_port, bitcoind_p2p_port) = + bitcoind(compose, bitcoin_network, create_full_bitcoin_node); + let (electrs_data, electrs, electrs_port) = electrs( + compose, + bitcoin_network, + create_full_bitcoin_node, + bitcoind_rpc_port, + bitcoind_p2p_port, + bitcoind, + bitcoind_data, + ); + let (asb_data, asb, asb_p2p_port, asb_rpc_port) = asb( + compose, + bitcoin_network, + monero_network, + electrs, + monerod, + true, + PathBuf::from("./config.toml"), + ); + let asb_controller = asb_controller(compose, asb_rpc_port, asb.clone()); + let asb_tracing_logger = asb_tracing_logger(compose, asb, asb_data); } -pub mod monerod { - use super::*; - - impl IntoFlag for monero::Network { - /// This is documented here: - /// https://docs.getmonero.org/interacting/monerod-reference/#pick-monero-network-blockchain - fn to_flag(self) -> Flag { - Flag(match self { - monero::Network::Mainnet => None, - monero::Network::Stagenet => Some("--stagenet".to_string()), - monero::Network::Testnet => Some("--testnet".to_string()), - }) - } - - fn to_display(self) -> &'static str { - match self { - monero::Network::Mainnet => "mainnet", - monero::Network::Stagenet => "stagenet", - monero::Network::Testnet => "testnet", - } - } +/// Add the servie/volume to the compose config + return them + the rpc port +pub fn monerod( + compose: &mut ComposeConfig, + network: monero::Network, + enabled: bool, +) -> (Arc, Arc, u16) { + let (network_flag, monerod_rpc_port): (Option, u16) = match network { + monero::Network::Mainnet => (None, 18081), + monero::Network::Stagenet => (Some(flag!("--stagenet")), 38081), + _ => unimplemented!(), + }; + + let monerod_data = compose.add_volume(MONEROD_DATA); + let mut monerod_command = command![ + "monerod", + flag!("--rpc-bind-ip=0.0.0.0"), + flag!("--rpc-bind-port={monerod_rpc_port}"), + flag!("--data-dir={}", monerod_data.as_root_dir().display()), + flag!("--confirm-external-bind"), + flag!("--restricted-rpc"), + flag!("--non-interactive"), + flag!("--enable-dns-blocklist") + ]; + + if let Some(network_flag) = network_flag { + monerod_command.add_flag(network_flag); } + + let monerod_service = + Service::new("monerod", ImageSource::from_registry(images::MONEROD_IMAGE)) + .with_enabled(enabled) + .with_mount(Mount::volume(&monerod_data)) + .with_exposed_port(monerod_rpc_port) + .with_command(monerod_command); + let monerod_service = compose.add_service(monerod_service); + + (monerod_data, monerod_service, monerod_rpc_port) } -pub mod electrs { - use super::*; - use crate::flag; +/// Adds the volume/service to the compose config and returns them + the rpc bind port and p2p bind port +pub fn bitcoind( + compose: &mut ComposeConfig, + network: bitcoin::Network, + enabled: bool, +) -> (Arc, Arc, u16, u16) { + let (rpc_port, p2p_port, chain): (u16, u16, &str) = match network { + bitcoin::Network::Bitcoin => (8332, 8333, "main"), + bitcoin::Network::Testnet => (18332, 18333, "test"), + _ => panic!("unsupported bitcoin network"), + }; - /// Wrapper around a Bitcoin network for Electrs - /// Electrs needs a different network flag than bitcoind - #[derive(Clone)] - pub struct Network(bitcoin::Network); + let bitcoind_data = compose.add_volume(BITCOIN_DATA); - impl Network { - pub fn new(bitcoin: bitcoin::Network) -> Self { - Self(bitcoin) - } - } + let bitcoind_command = command!( + "bitcoind", + flag!("-chain={chain}"), + flag!("-rpcallowip=0.0.0.0/0"), + flag!("-rpcbind=0.0.0.0:{rpc_port}"), + flag!("-bind=0.0.0.0:{p2p_port}"), + flag!("-datadir={}", bitcoind_data.as_root_dir().display()), + flag!("-dbcache=16384"), + flag!("-server=1"), + flag!("-prune=0"), + flag!("-txindex=1"), + ); - impl IntoFlag for Network { - fn to_flag(self) -> Flag { - match self.0 { - bitcoin::Network::Bitcoin => flag!("--network=bitcoin"), - bitcoin::Network::Testnet => flag!("--network=testnet"), - _ => panic!("Only Mainnet and Testnet are supported"), - } - } + let bitcoind = Service::new( + "bitcoind", + ImageSource::from_registry(images::BITCOIND_IMAGE), + ) + .with_mount(Mount::volume(&bitcoind_data)) + .with_exposed_port(rpc_port) + .with_exposed_port(p2p_port) + .with_command(bitcoind_command) + .with_enabled(enabled); - fn to_display(self) -> &'static str { - match self.0 { - bitcoin::Network::Bitcoin => "mainnet", - bitcoin::Network::Testnet => "testnet", - _ => panic!("Only Mainnet and Testnet are supported"), - } - } - } + let bitcoind = compose.add_service(bitcoind); + + (bitcoind_data, bitcoind, rpc_port, p2p_port) } -pub mod asb { - use super::*; - use crate::{compose::Flag, flag}; +pub fn electrs( + compose: &mut ComposeConfig, + network: bitcoin::Network, + enabled: bool, + bitcoind_rpc_port: u16, + bitcoind_p2p_port: u16, + bitcoind: Arc, + bitcoind_data: Arc, +) -> (Arc, Arc, u16) { + let (port, chain): (u16, &str) = match network { + bitcoin::Network::Bitcoin => (50001, "bitcoin"), + bitcoin::Network::Testnet => (50001, "testnet"), + _ => panic!("unsupported bitcoin network"), + }; - /// Wrapper around the network used for ASB - /// There are only two combinations of networks that are supported: - /// - Mainnet Bitcoin & Mainnet Monero - /// - Testnet Bitcoin & Stagenet Monero - pub struct Network((monero::Network, bitcoin::Network)); + let bitcoind_name = bitcoind.name(); - impl Network { - pub fn new(monero: monero::Network, bitcoin: bitcoin::Network) -> Self { - Self((monero, bitcoin)) - } - } + let electrs_data = compose.add_volume(ELECTRS_DATA); + let command = command!( + "electrs", + flag!("--network={chain}"), + flag!("--daemon-dir={}", bitcoind_data.as_root_dir().display()), + flag!( + "--db-dir={}", + electrs_data.as_root_dir().join("db").display() + ), + flag!("--daemon-rpc-addr={bitcoind_name}:{bitcoind_rpc_port}"), + flag!("--daemon-p2p-addr={bitcoind_name}:{bitcoind_p2p_port}"), + flag!("--electrum-rpc-addr=0.0.0.0:{port}"), + flag!("--log-filters=INFO"), + ); + let service = Service::new("electrs", ImageSource::from_registry(images::ELECTRS_IMAGE)) + .with_dependency(bitcoind.clone()) + .with_exposed_port(port) + .with_command(command) + .with_mount(Mount::volume(&electrs_data)) + .with_enabled(enabled); - impl IntoFlag for Network { - fn to_flag(self) -> Flag { - match self.0 { - // Mainnet is the default for the asb - (monero::Network::Mainnet, bitcoin::Network::Bitcoin) => Flag(None), - // Testnet requires the --testnet flag - (monero::Network::Stagenet, bitcoin::Network::Testnet) => flag!("--testnet"), - _ => panic!( - "Only either Mainnet Bitcoin & Mainnet Monero or Testnet Bitcoin & Stagenet Monero are supported" - ), - } - } - - fn to_display(self) -> &'static str { - match self.0 { - (monero::Network::Mainnet, bitcoin::Network::Bitcoin) => "mainnet", - (monero::Network::Stagenet, bitcoin::Network::Testnet) => "testnet", - _ => panic!( - "Only either Mainnet Bitcoin & Mainnet Monero or Testnet Bitcoin & Stagenet Monero are supported" - ), + let service = compose.add_service(service); + + (electrs_data, service, port) +} + +pub fn asb( + compose: &mut ComposeConfig, + bitcoin_network: bitcoin::Network, + monero_network: monero::Network, + electrs: Arc, + monerod: Arc, + build_from_source: bool, + config_path: PathBuf, +) -> (Arc, Arc, u16, u16) { + let (network_flag, asb_p2p_port, asb_rpc_port): (Option, u16, u16) = + match (bitcoin_network, monero_network) { + (bitcoin::Network::Bitcoin, monero::Network::Mainnet) => (None, 9939, 9944), + (bitcoin::Network::Testnet, monero::Network::Stagenet) => { + (Some(flag!("--testnet")), 9839, 9944) } - } + _ => unreachable!("invalid network combination"), + }; + + let asb_data = compose.add_volume(ASB_DATA); + let container_config_path = asb_data.as_root_dir().join("config.toml"); + let mut command = command![ + "asb", + flag!("--config={}", container_config_path.display()), + flag!("start"), + flag!("--rpc-bind-port={asb_rpc_port}"), + flag!("--rpc-bind-host=0.0.0.0") + ]; + + if let Some(network_flag) = network_flag { + command.add_flag(network_flag); } + + let image_source = match build_from_source { + // Todo: allow prebuilt image + _ => ImageSource::from_source( + PINNED_GIT_REPOSITORY.parse().expect("valid url"), + "./swap-asb/Dockerfile", + ), + }; + let mut asb_service = Service::new("asb", image_source) + .with_exposed_port(asb_p2p_port) + .with_mount(Mount::volume(&asb_data)) + .with_mount(Mount::path(config_path, container_config_path)) + .with_command(command); + + if electrs.is_enabled() { + asb_service = asb_service.with_dependency(electrs); + } + + if monerod.is_enabled() { + asb_service = asb_service.with_dependency(monerod) + } + + let service = compose.add_service(asb_service); + + (asb_data, service, asb_p2p_port, asb_rpc_port) +} + +pub fn asb_controller( + compose: &mut ComposeConfig, + asb_rpc_port: u16, + asb: Arc, +) -> Arc { + let command = command!["asb-controller", flag!("--url=http://asb:{asb_rpc_port}")]; + + let service = Service::new( + "asb-controller", + ImageSource::from_source( + PINNED_GIT_REPOSITORY.parse().expect("valid url"), + "./swap-controller/Dockerfile", + ), + ) + .with_dependency(asb) + .with_stdin_open(true) + .with_tty(true) + .with_command(command); + + compose.add_service(service) +} + +pub fn asb_tracing_logger( + compose: &mut ComposeConfig, + asb: Arc, + asb_data: Arc, +) -> Arc { + let command = command![ + "sh", + flag!("-c"), + flag!("tail -f /asb-data/logs/tracing*.log") + ]; + + let service = Service::new( + "asb-tracing-logger", + ImageSource::from_registry(images::ASB_TRACING_LOGGER_IMAGE), + ) + .with_dependency(asb) + .with_mount(Mount::volume(&asb_data)) + .with_command(command); + + compose.add_service(service) } diff --git a/swap-orchestrator/src/images.rs b/swap-orchestrator/src/images.rs index 96f11cfac9..b592081a5e 100644 --- a/swap-orchestrator/src/images.rs +++ b/swap-orchestrator/src/images.rs @@ -2,7 +2,6 @@ /// This means either: /// 1. Pulling from a registry (pinned to a hash) /// 2. Building from source from a specific git hash (pinned to a hash) -use crate::compose::DockerBuildInput; /// At compile time, we determine the git repository and commit hash /// This is then burned into the binary as a static string @@ -29,17 +28,3 @@ pub static BITCOIND_IMAGE: &str = /// alpine 3.22.1 (https://hub.docker.com/layers/library/alpine/3.22.1/images/sha256-0a88b42ba69d6b900848f9cb9151587bb82827d0aecfa222e51981fad97b5b9a) pub static ASB_TRACING_LOGGER_IMAGE: &str = "alpine@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1"; - -pub static ASB_IMAGE_FROM_SOURCE: DockerBuildInput = DockerBuildInput { - // The context is the root of the Cargo workspace - context: PINNED_GIT_REPOSITORY, - // The Dockerfile of the asb is in the swap-asb crate - dockerfile: "./swap-asb/Dockerfile", -}; - -pub static ASB_CONTROLLER_IMAGE_FROM_SOURCE: DockerBuildInput = DockerBuildInput { - // The context is the root of the Cargo workspace - context: PINNED_GIT_REPOSITORY, - // The Dockerfile of the asb-controller is in the swap-controller crate - dockerfile: "./swap-controller/Dockerfile", -}; diff --git a/swap-orchestrator/src/lib.rs b/swap-orchestrator/src/lib.rs index c9287d1f08..6fc544a8cb 100644 --- a/swap-orchestrator/src/lib.rs +++ b/swap-orchestrator/src/lib.rs @@ -1,5 +1,4 @@ pub mod compose; -pub mod compose2; pub mod containers; pub mod images; pub mod writer; diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index 5bd06dab54..89be09c0fd 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -3,60 +3,27 @@ mod containers; mod images; mod prompt; -use crate::compose::{ - ASB_DATA_DIR, DOCKER_COMPOSE_FILE, IntoSpec, OrchestratorDirectories, OrchestratorImage, - OrchestratorImages, OrchestratorInput, OrchestratorNetworks, -}; +use anyhow::{Result, anyhow, bail}; +use dialoguer::Select; +use dialoguer::theme::ColorfulTheme; use std::fs::File; use std::io::Write; use std::path::PathBuf; use swap_env::config::{ Bitcoin, Config, ConfigNotInitialized, Data, Maker, Monero, Network, TorConf, }; -use swap_env::prompt as config_prompt; +use swap_env::defaults::{ + Defaults, default_electrum_servers_mainnet, default_electrum_servers_testnet, +}; +use swap_env::prompt::{self as config_prompt, print_info_box}; use swap_env::{defaults::GetDefaults, env::Mainnet, env::Testnet}; -use swap_orchestrator::compose2::{Command, ComposeConfig, Flag, ImageSource, Mount, Service}; +use swap_orchestrator::compose::{Command, ComposeConfig, Flag, ImageSource, Mount, Service}; use url::Url; -fn main() { - { - let mut config = ComposeConfig::default(); - - let monerod_data = config.add_volume("monero-data"); - let monerod = Service::new( - "monerod", - ImageSource::PullFromRegistry { - image_url: "ghcr.io/sethforprivacy/simple-monerod@sha256:f30e5706a335c384e4cf420215cbffd1196f0b3a11d4dd4e819fe3e0bca41ec5".to_string(), - }, - ).with_mount(Mount::volume(monerod_data.clone())) - .with_command(Command::new(vec![Flag("monerod".into())])) - .with_exposed_port(18081); - - let monerod = config.add_service(monerod); - - let asb_data = config.add_volume("asb_data"); - let asb = Service::new("asb", ImageSource::BuildFromSource { git_url: "https://github.com/eigenwallet/core.git#8b817d5efc32f380b4ec0102a5bad86f9c98a499".parse().unwrap(), dockerfile_path: "./swap-asb/Dockerfile".into() }) - .with_dependency(monerod.clone()) - .with_mount(Mount::volume(asb_data.clone())) - .with_mount(Mount::path("./config.toml", asb_data.as_root_dir().join("config.toml"))) - .with_exposed_port(9939); - - let asb = config.add_service(asb); - - // ... configure other services etc - - let yml_config = config.build(); - - File::create("docker-compose.yml") - .unwrap() - .write_all(yml_config.as_bytes()) - .unwrap(); - - // done - } - - return; +const CONFIG_PATH: &str = "config.toml"; +fn main() { + // Default to mainnet, switch to testnet when `--testnet` flag is provided let (mut bitcoin_network, mut monero_network) = (bitcoin::Network::Bitcoin, monero::Network::Mainnet); @@ -89,192 +56,115 @@ fn main() { _ => panic!("Unsupported Bitcoin / Monero network combination"), }; - let recipe = OrchestratorInput { - ports: OrchestratorNetworks { - monero: monero_network, - bitcoin: bitcoin_network, - } - .into(), - networks: OrchestratorNetworks { - monero: monero_network, - bitcoin: bitcoin_network, - }, - images: OrchestratorImages { - // TODO: These containers should be conditonally removed / disabled, - // depending on if they are used by the asb - monerod: OrchestratorImage::Registry(images::MONEROD_IMAGE.to_string()), - electrs: OrchestratorImage::Registry(images::ELECTRS_IMAGE.to_string()), - bitcoind: OrchestratorImage::Registry(images::BITCOIND_IMAGE.to_string()), - // TODO: Allow pre-built images here - asb: OrchestratorImage::Build(images::ASB_IMAGE_FROM_SOURCE.clone()), - // TODO: Allow pre-built images here - asb_controller: OrchestratorImage::Build( - images::ASB_CONTROLLER_IMAGE_FROM_SOURCE.clone(), - ), - asb_tracing_logger: OrchestratorImage::Registry( - images::ASB_TRACING_LOGGER_IMAGE.to_string(), - ), - }, - directories: OrchestratorDirectories { - asb_data_dir: PathBuf::from(ASB_DATA_DIR), - }, - }; - - // If the config file already exists and be de-serialized, - // we give the user the ability to skip the setup for the "asb config" (config.toml) - // - // The "asb config" is distinctly different from the [`monero_node_type`] and [`electrum_server_type`] - // since these are also required to decide on the structure of the `docker-compose.yml` file - enum ConfigExistence { - PresentAndValid, - Missing, - PresentButInvalid(anyhow::Error), - } - - let asb_config_state = match swap_env::config::read_config( - recipe.directories.asb_config_path_on_host_as_path_buf(), - ) { - Ok(Ok(_)) => ConfigExistence::PresentAndValid, - Ok(Err(ConfigNotInitialized)) => ConfigExistence::Missing, - Err(err) => ConfigExistence::PresentButInvalid(err), - }; - - // None, means to do nothing because we already have a valid file at the correct location - // Some(None) means to generate a config from scratch - // Some(Some(PathBuf)) means to move the file at the location of the config file to the path - let should_prompt_config_wizard = match asb_config_state { - // Config is present and valid => we do not need to prompt a wizard - ConfigExistence::PresentAndValid => None, - // Config file is missing => force wizard - ConfigExistence::Missing => Some(None), - // Config is present but invalid => Ask user to rename old config file and generate new one - ConfigExistence::PresentButInvalid(err) => { - println!("The asb config is present but it is invalid. We were unable to parse it."); - println!("{:?}", err); - println!("Do you want to re-generate your asb config from scratch?"); - - let unix_epoch = unix_epoch_secs(); - - let renamed_file_name = format!( - "{}.backup_at_{}", - recipe - .directories - .asb_config_path_on_host_as_path_buf() - .file_name() - .expect("asb config file to have filename") - .to_str() - .expect("asb config file to fit into non OsStr"), - unix_epoch - ); - - println!( - "Your previous (invalid) config will be renamed to {}", - renamed_file_name - ); + let existing_config: Option> = + match swap_env::config::read_config(PathBuf::from(CONFIG_PATH)) { + Ok(Ok(config)) => Some(Ok(config)), + Ok(Err(ConfigNotInitialized)) => None, + Err(err) => Some(Err(anyhow!(err))), + }; - let renamed_path = recipe - .directories - .asb_config_path_on_host_as_path_buf() - .with_file_name(renamed_file_name); + let (config, create_full_bitcoin_node, create_full_monero_node) = + setup_wizard(existing_config, defaults).unwrap(); + { + let mut compose = ComposeConfig::default(); - Some(Some(renamed_path)) - } - }; + containers::add_maker_services( + &mut compose, + bitcoin_network, + monero_network, + create_full_bitcoin_node, + create_full_monero_node, + ); - // If the config is invalid or doesn't exist, we prompt the user - if let Some(should_move_old_file) = should_prompt_config_wizard { - let min_buy_btc = - config_prompt::min_buy_amount().expect("Failed to prompt for min buy amount"); - let max_buy_btc = - config_prompt::max_buy_amount().expect("Failed to prompt for max buy amount"); - let ask_spread = config_prompt::ask_spread().expect("Failed to prompt for ask spread"); - let rendezvous_points = - config_prompt::rendezvous_points().expect("Failed to prompt for rendezvous points"); - let tor_hidden_service = - config_prompt::tor_hidden_service().expect("Failed to prompt for tor hidden service"); - let listen_addresses = config_prompt::listen_addresses(&defaults.listen_address_tcp) - .expect("Failed to prompt for listen addresses"); - let monero_node_type = prompt::monero_node_type(); - let electrum_server_type = prompt::electrum_server_type(&defaults.electrum_rpc_urls); - let developer_tip = - config_prompt::developer_tip().expect("Failed to prompt for developer tip"); + let yml_config = compose.build(); - let electrs_url = Url::parse(&format!("tcp://electrs:{}", recipe.ports.electrs)) - .expect("electrs url to be convertible to a valid url"); - let monerod_daemon_url = - Url::parse(&format!("http://monerod:{}", recipe.ports.monerod_rpc)) - .expect("monerod daemon url to be convertible to a valid url"); + File::create("docker-compose.yml") + .unwrap() + .write_all(yml_config.as_bytes()) + .unwrap(); + } +} - let config = Config { - data: Data { - dir: recipe.directories.asb_data_dir.clone(), - }, - network: Network { - listen: listen_addresses, - rendezvous_point: rendezvous_points, - external_addresses: vec![], - }, - bitcoin: Bitcoin { - electrum_rpc_urls: match electrum_server_type { - // If user chose the included option, we will use the electrs url from the container - prompt::ElectrumServerType::Included => vec![electrs_url], - prompt::ElectrumServerType::Remote(electrum_servers) => electrum_servers, - }, - network: bitcoin_network, - target_block: defaults.bitcoin_confirmation_target, - use_mempool_space_fee_estimation: defaults.use_mempool_space_fee_estimation, - // This means that we will use the default set in swap-env/src/env.rs - finality_confirmations: None, - }, - monero: Monero { - daemon_url: match monero_node_type.clone() { - prompt::MoneroNodeType::Included => Some(monerod_daemon_url), - prompt::MoneroNodeType::Pool => None, - prompt::MoneroNodeType::Remote(url) => Some(url), - }, - network: monero_network, - // This means that we will use the default set in swap-env/src/env.rs - finality_confirmations: None, - }, - tor: TorConf { - register_hidden_service: tor_hidden_service, - ..Default::default() - }, - maker: Maker { - min_buy_btc, - max_buy_btc, - ask_spread, - price_ticker_ws_url: defaults.price_ticker_ws_url, - external_bitcoin_redeem_address: None, - developer_tip, - }, - }; +/// Take a possibly already existing config.toml and (if necessary) go through the wizard steps necessary to +/// (if necessary) generate it and the docker-compose.yml +/// +/// # Returns +/// The complete config, whether to create a full bitcoin/electrum node and whether to create a full monero node +fn setup_wizard( + existing_config: Option>, + defaults: Defaults, +) -> Result<(Config, bool, bool)> { + // If we already have a valid config, just use it and deduce the monero/bitcoin settings + if let Some(Ok(config)) = existing_config { + // If the config points to our local electrs node, we must have previously created it + let create_full_bitcoin_node = config + .bitcoin + .electrum_rpc_urls + .iter() + .any(|url| url.as_str().contains("tcp://electrs:")); + // Same for monero + let create_full_monero_node = config + .monero + .daemon_url + .as_ref() + .is_some_and(|url| url.as_str().contains("http://monerod:")); + + return Ok((config, create_full_bitcoin_node, create_full_monero_node)); + } - // If there was an invalid config file previously, we rename it - if let Some(move_invalid_config_to) = should_move_old_file { - std::fs::rename( - recipe.directories.asb_config_path_on_host(), - move_invalid_config_to, - ) - .expect("to be able to move old invalid config file"); + // If we have an invalid config we offer to procede as if there was no config and rename the old one + if let Some(Err(err)) = existing_config { + println!( + "Error: We couldn't parse your existing config.toml file (`{}`)", + err + ); + + let proposed_filename = format!("config.toml.invalid-{}", unix_epoch_secs()); + + let choice = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("How do you want to procede?") + .item(format!( + "Start wizard from scratch and rename my existing `config.toml` to `{proposed_filename}`" + )) + .item("Abort and leave my `config.toml` alone") + .interact()?; + + if choice != 0 { + println!("Stopping wizard. Goodbye!"); + bail!("User doesn't want to procede.") } - // Write the asb config to the host, the config will then be mounted into the `asb` docker container - let asb_config_path = recipe.directories.asb_config_path_on_host(); - - std::fs::write( - asb_config_path, - toml::to_string(&config).expect("Failed to write config.toml"), - ) - .expect("Failed to write config.toml"); + std::fs::rename(CONFIG_PATH, &proposed_filename)?; + println!("Renamed your old config to `{proposed_filename}`.") } - // Write the compose to ./docker-compose.yml - let compose = recipe.to_spec(); - std::fs::write(DOCKER_COMPOSE_FILE, compose).expect("Failed to write docker-compose.yml"); - - println!(); - println!("Run `docker compose up -d` to start the services."); + // At this point we either have no or an invalid config, so we do the whole wizard. + println!("Starting the wizard."); + + // we need + // - monero node type + // - electrum node type + // - min buy + // - max buy + // - markup + // - rendezvous points + // - hidden service + // - listen addresses + // - tip + + let min_buy = config_prompt::min_buy_amount()?; + let max_buy = config_prompt::max_buy_amount()?; + let markup = config_prompt::ask_spread()?; + let rendezvous_points = config_prompt::rendezvous_points()?; + let hidden_service = config_prompt::tor_hidden_service()?; + let listen_addresses = config_prompt::listen_addresses(&defaults.listen_address_tcp)?; + + let monero_node_type = prompt::monero_node_type(); + let electrum_node_type = prompt::electrum_server_type(&defaults.electrum_rpc_urls); + + let tip = config_prompt::developer_tip()?; + + bail!("unimplemented") } fn unix_epoch_secs() -> u64 { diff --git a/swap-orchestrator/src/prompt.rs b/swap-orchestrator/src/prompt.rs index b9d183ac62..1325817c3f 100644 --- a/swap-orchestrator/src/prompt.rs +++ b/swap-orchestrator/src/prompt.rs @@ -1,5 +1,9 @@ -use dialoguer::{Select, theme::ColorfulTheme}; -use swap_env::prompt as config_prompt; +use anyhow::bail; +use dialoguer::{Input, Select, theme::ColorfulTheme}; +use swap_env::{ + config::Monero, + prompt::{self as config_prompt, print_info_box}, +}; use url::Url; #[derive(Debug)] @@ -23,10 +27,10 @@ pub enum ElectrumServerType { #[allow(dead_code)] // will be used in the future pub fn build_type() -> BuildType { let build_type = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("How do you want to build the Docker image for the ASB?") + .with_prompt("How do you want to obtain the maker Docker image?") .items(&[ - "Build Docker image from source (can take >1h)", - "Prebuild Docker image (pinned to a specific commit with SHA256 hash)", + "Build from source (can take >1h depending on your machine)", + "Use a prebuilt Docker image (pinned to a specific version using a SHA256 hash)", ]) .default(0) .interact() @@ -41,50 +45,111 @@ pub fn build_type() -> BuildType { pub fn monero_node_type() -> MoneroNodeType { let node_choice = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Do you want to include a Monero node or use an existing node/remote node?") + .with_prompt("How do you want to connect to the Monero blockchain?") .items(&[ - "Include a full Monero node", - "Use an existing node or remote node", + "Use a mix of default remote nodes (instant)", + "Create a full Monero node (most private - but requires 1-2 days to sync and ~500GB of disk space)", + "I already have a node (instant)", ]) .default(0) .interact() .expect("Failed to select node choice"); match node_choice { - 0 => MoneroNodeType::Included, - 1 => { - match config_prompt::monero_daemon_url() - .expect("Failed to prompt for Monero daemon URL") - { - Some(url) => MoneroNodeType::Remote(url), - None => MoneroNodeType::Pool, - } + 0 => MoneroNodeType::Pool, + 1 => MoneroNodeType::Included, + 2 => { + let node: Url = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter the address of your node") + .interact_text() + .expect("user to enter url"); + + MoneroNodeType::Remote(node) } _ => unreachable!(), } } pub fn electrum_server_type(default_electrum_urls: &Vec) -> ElectrumServerType { - let electrum_server_type = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("How do you want to connect to the Bitcoin network?") + let theme = ColorfulTheme::default(); + let select = Select::with_theme(&theme) + .with_prompt("How do you want to connect to the Bitcoin blockchain?") .items(&[ - "Run a full Bitcoin node & Electrum server", - "List of remote Electrum servers", + "Create a full Bitcoin node and Electrum server (most private - but requires 1-2 days to sync and ~500GB of disk space)", + "Use a mix of default Electrum servers (instant)", + "Specify my own Electrum server (instant)", + "Specify my own Electrum server in addition to the mix of default Electrum servers (instant)", + "Print the list of the default Electrum servers" ]) - .default(0) + .default(0); + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum Choice { + ElectrumPool, + RunFullNode, + CustomElectrumNode, + CustomElectrumNodeAndPool, + PrintPoolUrls, + } + + impl TryFrom for Choice { + type Error = anyhow::Error; + + fn try_from(value: usize) -> Result { + Ok(match value { + 0 => Choice::RunFullNode, + 1 => Choice::ElectrumPool, + 2 => Choice::CustomElectrumNode, + 3 => Choice::CustomElectrumNodeAndPool, + 4 => Choice::PrintPoolUrls, + 5.. => bail!("invalid choice"), + }) + } + } + + let mut electrum_servers: Vec = Vec::new(); + + let mut choice: Choice = select + .clone() .interact() - .expect("Failed to select electrum server type"); + .expect("valid choice") + .try_into() + .expect("valid choice"); - match electrum_server_type { - 0 => ElectrumServerType::Included, - 1 => { - println!("Okay, let's use remote Electrum servers!"); + // Keep printing the list until the user makes and actual choice + while choice == Choice::PrintPoolUrls { + print_info_box(default_electrum_urls.iter().map(Url::to_string)); + choice = select + .clone() + .interact() + .expect("valid choice") + .try_into() + .expect("valid choice"); + } - let electrum_servers = config_prompt::electrum_rpc_urls(default_electrum_urls) - .expect("Failed to prompt for electrum servers"); + if matches!( + choice, + Choice::ElectrumPool | Choice::CustomElectrumNodeAndPool + ) { + electrum_servers.extend_from_slice(&default_electrum_urls); + } + if matches!( + choice, + Choice::CustomElectrumNode | Choice::CustomElectrumNodeAndPool + ) { + let url: Url = Input::with_theme(&theme) + .with_prompt("Please enter the url of your own Electrum server") + .interact_text() + .expect("invalid input"); + electrum_servers.push(url); + } + + match choice { + Choice::RunFullNode => ElectrumServerType::Included, + Choice::CustomElectrumNode | Choice::CustomElectrumNodeAndPool | Choice::ElectrumPool => { ElectrumServerType::Remote(electrum_servers) } - _ => unreachable!(), + Choice::PrintPoolUrls => unimplemented!(), } } From cd250ecba555e7e13a317de3228934d1a476b7b6 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Mon, 6 Oct 2025 15:59:01 +0200 Subject: [PATCH 04/16] fmt --- swap-orchestrator/src/containers.rs | 28 ++++- swap-orchestrator/src/lib.rs | 1 + swap-orchestrator/src/main.rs | 165 ++++++++++++++++++---------- 3 files changed, 133 insertions(+), 61 deletions(-) diff --git a/swap-orchestrator/src/containers.rs b/swap-orchestrator/src/containers.rs index 0e45f060a0..2000f2c215 100644 --- a/swap-orchestrator/src/containers.rs +++ b/swap-orchestrator/src/containers.rs @@ -1,5 +1,7 @@ use std::{path::PathBuf, sync::Arc}; +use url::Url; + ///! This meta module describes **how to run** containers /// /// Currently this only includes which flags we need to pass to the binaries @@ -17,7 +19,8 @@ const BITCOIN_DATA: &str = "bitcoin-data"; const ELECTRS_DATA: &str = "electrs-data"; const ASB_DATA: &str = "asb-data"; -/// Add all the services/volumes to the compose config +/// Add all the services/volumes to the compose config. +/// Returns urls for the electrum rpc and monero rpc endpoints. #[allow(unused_variables)] pub fn add_maker_services( compose: &mut ComposeConfig, @@ -25,7 +28,7 @@ pub fn add_maker_services( monero_network: monero::Network, create_full_bitcoin_node: bool, create_full_monero_node: bool, -) { +) -> (Arc, Url, Url) { let (monerod_data, monerod, monerod_rpc_port) = monerod(compose, monero_network, create_full_monero_node); let (bitcoind_data, bitcoind, bitcoind_rpc_port, bitcoind_p2p_port) = @@ -43,13 +46,28 @@ pub fn add_maker_services( compose, bitcoin_network, monero_network, - electrs, - monerod, + electrs.clone(), + monerod.clone(), true, PathBuf::from("./config.toml"), ); let asb_controller = asb_controller(compose, asb_rpc_port, asb.clone()); - let asb_tracing_logger = asb_tracing_logger(compose, asb, asb_data); + let asb_tracing_logger = asb_tracing_logger(compose, asb, asb_data.clone()); + + let electrum_rpc_url: Url = format!( + "tcp://{electrs_name}:{electrs_port}", + electrs_name = electrs.name() + ) + .parse() + .expect("valid url"); + let monerod_rpc_url: Url = format!( + "http://{monerod_name}:{monerod_rpc_port}", + monerod_name = monerod.name() + ) + .parse() + .expect("valid url"); + + (asb_data, electrum_rpc_url, monerod_rpc_url) } /// Add the servie/volume to the compose config + return them + the rpc port diff --git a/swap-orchestrator/src/lib.rs b/swap-orchestrator/src/lib.rs index 6fc544a8cb..a9073d00e9 100644 --- a/swap-orchestrator/src/lib.rs +++ b/swap-orchestrator/src/lib.rs @@ -1,4 +1,5 @@ pub mod compose; pub mod containers; pub mod images; +pub mod prompt; pub mod writer; diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index 89be09c0fd..f97ae3dd08 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -1,8 +1,3 @@ -mod compose; -mod containers; -mod images; -mod prompt; - use anyhow::{Result, anyhow, bail}; use dialoguer::Select; use dialoguer::theme::ColorfulTheme; @@ -12,15 +7,15 @@ use std::path::PathBuf; use swap_env::config::{ Bitcoin, Config, ConfigNotInitialized, Data, Maker, Monero, Network, TorConf, }; -use swap_env::defaults::{ - Defaults, default_electrum_servers_mainnet, default_electrum_servers_testnet, -}; -use swap_env::prompt::{self as config_prompt, print_info_box}; +use swap_env::prompt::{self as config_prompt}; use swap_env::{defaults::GetDefaults, env::Mainnet, env::Testnet}; -use swap_orchestrator::compose::{Command, ComposeConfig, Flag, ImageSource, Mount, Service}; -use url::Url; +use swap_orchestrator::compose::ComposeConfig; +use swap_orchestrator::containers::add_maker_services; + +use swap_orchestrator::prompt::{self, ElectrumServerType, MoneroNodeType}; const CONFIG_PATH: &str = "config.toml"; +const DOCKER_COMPOSE_PATH: &str = "docker-compose.yml"; fn main() { // Default to mainnet, switch to testnet when `--testnet` flag is provided @@ -46,16 +41,6 @@ fn main() { } } - let defaults = match (bitcoin_network, monero_network) { - (bitcoin::Network::Bitcoin, monero::Network::Mainnet) => { - Mainnet::get_config_file_defaults().expect("defaults to be available") - } - (bitcoin::Network::Testnet, monero::Network::Stagenet) => { - Testnet::get_config_file_defaults().expect("defaults to be available") - } - _ => panic!("Unsupported Bitcoin / Monero network combination"), - }; - let existing_config: Option> = match swap_env::config::read_config(PathBuf::from(CONFIG_PATH)) { Ok(Ok(config)) => Some(Ok(config)), @@ -63,37 +48,34 @@ fn main() { Err(err) => Some(Err(anyhow!(err))), }; - let (config, create_full_bitcoin_node, create_full_monero_node) = - setup_wizard(existing_config, defaults).unwrap(); - { - let mut compose = ComposeConfig::default(); + let (config, compose) = setup_wizard(existing_config, bitcoin_network, monero_network).unwrap(); - containers::add_maker_services( - &mut compose, - bitcoin_network, - monero_network, - create_full_bitcoin_node, - create_full_monero_node, - ); + // Write output to files + let config_stringified = toml::to_string(&config).unwrap(); + File::create(CONFIG_PATH) + .unwrap() + .write_all(config_stringified.as_bytes()) + .unwrap(); - let yml_config = compose.build(); + let compose_stringified = compose.build(); + File::create(DOCKER_COMPOSE_PATH) + .unwrap() + .write_all(compose_stringified.as_bytes()) + .unwrap(); - File::create("docker-compose.yml") - .unwrap() - .write_all(yml_config.as_bytes()) - .unwrap(); - } + println!("Ok. run `docker compose up -d`."); } /// Take a possibly already existing config.toml and (if necessary) go through the wizard steps necessary to /// (if necessary) generate it and the docker-compose.yml /// /// # Returns -/// The complete config, whether to create a full bitcoin/electrum node and whether to create a full monero node +/// The complete maker config.toml and docker compose config. fn setup_wizard( existing_config: Option>, - defaults: Defaults, -) -> Result<(Config, bool, bool)> { + bitcoin_network: bitcoin::Network, + monero_network: monero::Network, +) -> Result<(Config, ComposeConfig)> { // If we already have a valid config, just use it and deduce the monero/bitcoin settings if let Some(Ok(config)) = existing_config { // If the config points to our local electrs node, we must have previously created it @@ -109,7 +91,16 @@ fn setup_wizard( .as_ref() .is_some_and(|url| url.as_str().contains("http://monerod:")); - return Ok((config, create_full_bitcoin_node, create_full_monero_node)); + let mut compose = ComposeConfig::default(); + add_maker_services( + &mut compose, + config.bitcoin.network, + config.monero.network, + create_full_bitcoin_node, + create_full_monero_node, + ); + + return Ok((config, compose)); } // If we have an invalid config we offer to procede as if there was no config and rename the old one @@ -138,33 +129,95 @@ fn setup_wizard( println!("Renamed your old config to `{proposed_filename}`.") } + let defaults = match (bitcoin_network, monero_network) { + (bitcoin::Network::Bitcoin, monero::Network::Mainnet) => { + Mainnet::get_config_file_defaults()? + } + (bitcoin::Network::Testnet, monero::Network::Stagenet) => { + Testnet::get_config_file_defaults()? + } + (a, b) => bail!("unsupported network combo (bitocoin={a}, monero={b:?}"), + }; + // At this point we either have no or an invalid config, so we do the whole wizard. println!("Starting the wizard."); - // we need - // - monero node type - // - electrum node type - // - min buy - // - max buy - // - markup - // - rendezvous points - // - hidden service - // - listen addresses - // - tip - + // Maker questions (spread, max, min etc) let min_buy = config_prompt::min_buy_amount()?; let max_buy = config_prompt::max_buy_amount()?; let markup = config_prompt::ask_spread()?; + // Networking: rendezvous points, hidden service, etc. let rendezvous_points = config_prompt::rendezvous_points()?; let hidden_service = config_prompt::tor_hidden_service()?; let listen_addresses = config_prompt::listen_addresses(&defaults.listen_address_tcp)?; - + // Monero and Electrum node types (local vs remote) let monero_node_type = prompt::monero_node_type(); let electrum_node_type = prompt::electrum_server_type(&defaults.electrum_rpc_urls); - + // Whether to tip the devs let tip = config_prompt::developer_tip()?; - bail!("unimplemented") + // Derive docker compose config from + let create_full_bitcoin_node = matches!(electrum_node_type, ElectrumServerType::Included); + let create_full_monero_node = matches!(monero_node_type, MoneroNodeType::Included); + + let mut compose = ComposeConfig::default(); + let (asb_data, compose_electrs_url, compose_monerd_rpc_url) = add_maker_services( + &mut compose, + bitcoin_network, + monero_network, + create_full_bitcoin_node, + create_full_monero_node, + ); + + let actual_electrum_rpc_urls = match electrum_node_type { + ElectrumServerType::Included => vec![compose_electrs_url], + ElectrumServerType::Remote(remote_nodes) => remote_nodes, + }; + // None means Monero RPC pool + let actual_monerod_url = match monero_node_type { + MoneroNodeType::Included => Some(compose_monerd_rpc_url), + MoneroNodeType::Remote(remote_node) => Some(remote_node), + MoneroNodeType::Pool => None, + }; + + let config = Config { + data: Data { + dir: asb_data.as_root_dir(), + }, + maker: Maker { + max_buy_btc: max_buy, + min_buy_btc: min_buy, + ask_spread: markup, + external_bitcoin_redeem_address: None, + price_ticker_ws_url: defaults.price_ticker_ws_url, + developer_tip: tip, + }, + bitcoin: Bitcoin { + electrum_rpc_urls: actual_electrum_rpc_urls, + target_block: defaults.bitcoin_confirmation_target, + // None means use default from env.rs + finality_confirmations: None, + network: bitcoin_network, + use_mempool_space_fee_estimation: defaults.use_mempool_space_fee_estimation, + }, + monero: Monero { + daemon_url: actual_monerod_url, + // None means use default from env.rs + finality_confirmations: None, + network: monero_network, + }, + network: Network { + listen: listen_addresses, + rendezvous_point: rendezvous_points, + external_addresses: vec![], + }, + tor: TorConf { + register_hidden_service: hidden_service, + ..Default::default() + }, + }; + + Ok((config, compose)) } fn unix_epoch_secs() -> u64 { From 6a135813e9da12fb03d6926f42c9bf67aa311fce Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Mon, 6 Oct 2025 17:08:31 +0200 Subject: [PATCH 05/16] integrate clap --- swap-orchestrator/src/main.rs | 115 +++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index f97ae3dd08..dfee73c968 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -1,4 +1,5 @@ -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow, bail}; +use clap::Parser; use dialoguer::Select; use dialoguer::theme::ColorfulTheme; use std::fs::File; @@ -17,53 +18,77 @@ use swap_orchestrator::prompt::{self, ElectrumServerType, MoneroNodeType}; const CONFIG_PATH: &str = "config.toml"; const DOCKER_COMPOSE_PATH: &str = "docker-compose.yml"; -fn main() { +#[derive(clap::Parser)] +struct Args { + #[arg( + long, + default_value = "false", + long_help = "Specify this flag when you want to run on Bitcoin Testnet and Monero Stagenet. Mainly used for development." + )] + testnet: bool, + #[command(subcommand)] + command: Option, +} + +#[derive(clap::Subcommand)] +enum Commands { + Init, + Start, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + // Default to mainnet, switch to testnet when `--testnet` flag is provided - let (mut bitcoin_network, mut monero_network) = - (bitcoin::Network::Bitcoin, monero::Network::Mainnet); - - for arg in std::env::args() { - match arg.as_str() { - "--help" => { - println!( - "Look at our documentation: https://github.com/eigenwallet/core/blob/master/swap-orchestrator/README.md" - ); - return; - } - "--testnet" => { - println!( - "Detected `--testnet` flag, switching to Bitcoin Testnet3 and Monero Stagenet" - ); - bitcoin_network = bitcoin::Network::Testnet; - monero_network = monero::Network::Stagenet; - } - _ => (), - } + let (bitcoin_network, monero_network) = if args.testnet { + (bitcoin::Network::Bitcoin, monero::Network::Mainnet) + } else { + (bitcoin::Network::Testnet, monero::Network::Stagenet) + }; + + let result = match args.command { + None | Some(Commands::Init) => handle_init_command(bitcoin_network, monero_network).await, + Some(Commands::Start) => handle_start_command().await, + }; + + if let Err(err) = result { + println!( + "The orchestrator command you executed just failed: \n\n{:?}\n\nThis is unexpected, please open a GitHub issue on our official repo: \nhttps://www.github.com/eigenwallet/core/issues/new", + err + ); } +} + +async fn handle_start_command() -> anyhow::Result<()> { + let docker_client = bollard::Docker::connect_with_local_defaults(); - let existing_config: Option> = - match swap_env::config::read_config(PathBuf::from(CONFIG_PATH)) { - Ok(Ok(config)) => Some(Ok(config)), - Ok(Err(ConfigNotInitialized)) => None, - Err(err) => Some(Err(anyhow!(err))), - }; - - let (config, compose) = setup_wizard(existing_config, bitcoin_network, monero_network).unwrap(); - - // Write output to files - let config_stringified = toml::to_string(&config).unwrap(); - File::create(CONFIG_PATH) - .unwrap() - .write_all(config_stringified.as_bytes()) - .unwrap(); - - let compose_stringified = compose.build(); - File::create(DOCKER_COMPOSE_PATH) - .unwrap() - .write_all(compose_stringified.as_bytes()) - .unwrap(); - - println!("Ok. run `docker compose up -d`."); + Ok(()) +} + +async fn handle_init_command( + bitcoin_network: bitcoin::Network, + monero_network: monero::Network, +) -> anyhow::Result<()> { + // Read the already existing config, if it's there + let existing_config: Option> = + swap_env::config::read_config(PathBuf::from(CONFIG_PATH)) + .ok() + .map(|res| res.context("Couldn't parse config.toml")); + + // Run the wizard and generate the configs, if necessary. + // If the maker config already exists, it will be returned as-is and only the docker config re-generated. + let (maker_config, compose_config) = + setup_wizard(existing_config, bitcoin_network, monero_network) + .context("Couldn't execute swap wizard")?; + + // Write the configs to their files. + let maker_config_stringified = toml::to_string(&maker_config)?; + File::create(CONFIG_PATH)?.write_all(maker_config_stringified.as_bytes())?; + let compose_config_stringified = compose_config.build(); + File::create(DOCKER_COMPOSE_PATH)?.write_all(compose_config_stringified.as_bytes())?; + + Ok(()) } /// Take a possibly already existing config.toml and (if necessary) go through the wizard steps necessary to From 748f43e4dccdf4ff93e1dc2e12fe9fad9283d58d Mon Sep 17 00:00:00 2001 From: Binarybaron Date: Tue, 7 Oct 2025 15:16:13 +0200 Subject: [PATCH 06/16] feat(rendezvous-server): Build in CI; fix timeout issue --- .github/workflows/build-release-binaries.yml | 5 +- libp2p-rendezvous-server/Dockerfile | 15 +- libp2p-rendezvous-server/src/main.rs | 195 +++++-------------- libp2p-rendezvous-server/src/swarm.rs | 156 +++++++++++++++ 4 files changed, 221 insertions(+), 150 deletions(-) create mode 100644 libp2p-rendezvous-server/src/swarm.rs diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 9bcada7186..623f1fb9d7 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -1,4 +1,4 @@ -name: "Build swap, asb, asb-controller, and orchestrator release binaries" +name: "Build swap, asb, asb-controller, orchestrator and rendezvous-server release binaries" on: pull_request: @@ -52,6 +52,9 @@ jobs: - name: asb-controller smoke_test_args: "" smoke_test_fake_interactive: false + - name: rendezvous-server + smoke_test_args: "--help" + smoke_test_fake_interactive: false - name: orchestrator smoke_test_args: "" smoke_test_fake_interactive: true diff --git a/libp2p-rendezvous-server/Dockerfile b/libp2p-rendezvous-server/Dockerfile index 59c5ef5c1f..571194f103 100644 --- a/libp2p-rendezvous-server/Dockerfile +++ b/libp2p-rendezvous-server/Dockerfile @@ -1,14 +1,23 @@ -FROM rust:1.87-slim AS builder +# Latest Rust 1.87.0 image as of Tue, 05 Aug 2025 15:34:08 GMT +FROM rust:1.87.0-bookworm@sha256:251cec8da4689d180f124ef00024c2f83f79d9bf984e43c180a598119e326b84 AS builder WORKDIR /build + +# Install some system dependencies +# TODO: These might not be required? RUN apt-get update RUN apt-get install -y git clang cmake libsnappy-dev + +# Build the rendezvous server binary COPY . . RUN cargo build --release --package rendezvous-server --bin rendezvous-server +# Latest Debian Bookworm image as of Tue, 05 Aug 2025 15:34:08 GMT +FROM debian:bookworm@sha256:b6507e340c43553136f5078284c8c68d86ec8262b1724dde73c325e8d3dcdeba AS runner -FROM debian:bullseye-slim -WORKDIR /data +# Copy the compiled binary from the previous stage COPY --from=builder /build/target/release/rendezvous-server /bin/rendezvous-server + EXPOSE 8888 + ENTRYPOINT ["rendezvous-server"] diff --git a/libp2p-rendezvous-server/src/main.rs b/libp2p-rendezvous-server/src/main.rs index 6193e69813..0aa0c27fa6 100644 --- a/libp2p-rendezvous-server/src/main.rs +++ b/libp2p-rendezvous-server/src/main.rs @@ -1,34 +1,26 @@ use anyhow::{Context, Result}; -use futures::{AsyncRead, AsyncWrite, StreamExt}; -use libp2p::core::muxing::StreamMuxerBox; -use libp2p::core::transport::Boxed; -use libp2p::core::upgrade::Version; -use libp2p::identity::ed25519; -use libp2p::noise; -use libp2p::rendezvous::server::Behaviour; +use futures::StreamExt; +use libp2p::identity::{self, ed25519}; +use libp2p::rendezvous; use libp2p::swarm::SwarmEvent; -use libp2p::tcp; -use libp2p::yamux; -use libp2p::{dns, SwarmBuilder}; -use libp2p::{identity, rendezvous, Multiaddr, PeerId, Swarm, Transport}; -use libp2p_tor::{AddressConversion, TorTransport}; -use std::fmt; use std::path::{Path, PathBuf}; -use std::time::Duration; use structopt::StructOpt; use tokio::fs; use tokio::fs::{DirBuilder, OpenOptions}; use tokio::io::AsyncWriteExt; -use tor_hsservice::config::OnionServiceConfigBuilder; use tracing::level_filters::LevelFilter; use tracing_subscriber::FmtSubscriber; +use crate::swarm::{create_swarm, create_swarm_with_onion, Addresses}; + +pub mod swarm; + #[derive(Debug, StructOpt)] struct Cli { /// Path to the file that contains the secret key of the rendezvous server's /// identity keypair /// If the file does not exist, a new secret key will be generated and saved to the file - #[structopt(long, default_value = "rendezvous-server-secret.key")] + #[structopt(long, default_value = "./rendezvous-server-secret.key")] secret_file: PathBuf, /// Port used for listening on TCP (default) @@ -37,7 +29,7 @@ struct Cli { /// Enable listening on Tor onion service #[structopt(long)] - onion: bool, + no_onion: bool, /// Port for the onion service (only used if --onion is enabled) #[structopt(long, default_value = "8888")] @@ -57,16 +49,16 @@ struct Cli { async fn main() -> Result<()> { let cli = Cli::from_args(); - init_tracing(LevelFilter::INFO, cli.json, cli.no_timestamp); + init_tracing(LevelFilter::TRACE, cli.json, cli.no_timestamp); let secret_key = load_secret_key_from_file(&cli.secret_file).await?; let identity = identity::Keypair::from(ed25519::Keypair::from(secret_key)); - let mut swarm = if cli.onion { - create_swarm_with_onion(identity, cli.onion_port).await? - } else { + let mut swarm = if cli.no_onion { create_swarm(identity)? + } else { + create_swarm_with_onion(identity, cli.onion_port).await? }; tracing::info!(peer_id=%swarm.local_peer_id(), "Rendezvous server peer id"); @@ -126,7 +118,42 @@ fn init_tracing(level: LevelFilter, json_format: bool, no_timestamp: bool) { let is_terminal = atty::is(atty::Stream::Stderr); let builder = FmtSubscriber::builder() - .with_env_filter(format!("rendezvous_server={}", level)) + .with_env_filter(format!( + "rendezvous_server={},\ + libp2p={},\ + libp2p_allow_block_list={},\ + libp2p_connection_limits={},\ + libp2p_core={},\ + libp2p_dns={},\ + libp2p_identity={},\ + libp2p_noise={},\ + libp2p_ping={},\ + libp2p_rendezvous={},\ + libp2p_request_response={},\ + libp2p_swarm={},\ + libp2p_tcp={},\ + libp2p_tls={},\ + libp2p_tor={},\ + libp2p_websocket={},\ + libp2p_yamux={}", + level, + level, + level, + level, + level, + level, + level, + level, + level, + level, + level, + level, + level, + level, + level, + level, + level + )) .with_writer(std::io::stderr) .with_ansi(is_terminal) .with_target(false); @@ -193,127 +220,3 @@ async fn write_secret_key_to_file(secret_key: &ed25519::SecretKey, path: PathBuf Ok(()) } - -fn create_swarm(identity: identity::Keypair) -> Result> { - let transport = create_transport(&identity).context("Failed to create transport")?; - let rendezvous = rendezvous::server::Behaviour::new(rendezvous::server::Config::default()); - - let swarm = SwarmBuilder::with_existing_identity(identity) - .with_tokio() - .with_other_transport(|_| transport)? - .with_behaviour(|_| rendezvous)? - .build(); - - Ok(swarm) -} - -async fn create_swarm_with_onion( - identity: identity::Keypair, - onion_port: u16, -) -> Result> { - let (transport, onion_address) = create_transport_with_onion(&identity, onion_port) - .await - .context("Failed to create transport with onion")?; - let rendezvous = rendezvous::server::Behaviour::new(rendezvous::server::Config::default()); - - let mut swarm = SwarmBuilder::with_existing_identity(identity) - .with_tokio() - .with_other_transport(|_| transport)? - .with_behaviour(|_| rendezvous)? - .build(); - - // Listen on the onion address - swarm - .listen_on(onion_address.clone()) - .context("Failed to listen on onion address")?; - - tracing::info!(%onion_address, "Onion service configured"); - - Ok(swarm) -} - -fn create_transport(identity: &identity::Keypair) -> Result> { - let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); - let tcp_with_dns = dns::tokio::Transport::system(tcp)?; - - let transport = authenticate_and_multiplex(tcp_with_dns.boxed(), &identity).unwrap(); - - Ok(transport) -} - -async fn create_transport_with_onion( - identity: &identity::Keypair, - onion_port: u16, -) -> Result<(Boxed<(PeerId, StreamMuxerBox)>, Multiaddr)> { - // Create TCP transport - let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); - let tcp_with_dns = dns::tokio::Transport::system(tcp)?; - - // Create Tor transport - let mut tor_transport = TorTransport::unbootstrapped() - .await? - .with_address_conversion(AddressConversion::IpAndDns); - - // Create onion service configuration - let onion_service_config = OnionServiceConfigBuilder::default() - .nickname( - identity - .public() - .to_peer_id() - .to_base58() - .to_ascii_lowercase() - .parse() - .unwrap(), - ) - .num_intro_points(3) - .build() - .unwrap(); - - // Add onion service and get the address - let onion_address = tor_transport.add_onion_service(onion_service_config, onion_port)?; - - // Combine transports - let combined_transport = tcp_with_dns - .boxed() - .or_transport(tor_transport.boxed()) - .boxed(); - - let transport = authenticate_and_multiplex(combined_transport, &identity).unwrap(); - - Ok((transport, onion_address)) -} - -fn authenticate_and_multiplex( - transport: Boxed, - identity: &identity::Keypair, -) -> Result> -where - T: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - let noise_config = noise::Config::new(identity).unwrap(); - - let transport = transport - .upgrade(Version::V1) - .authenticate(noise_config) - .multiplex(yamux::Config::default()) - .timeout(Duration::from_secs(20)) - .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) - .boxed(); - - Ok(transport) -} - -struct Addresses<'a>(&'a [Multiaddr]); - -// Prints an array of multiaddresses as a comma seperated string -impl fmt::Display for Addresses<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let display = self - .0 - .iter() - .map(|addr| addr.to_string()) - .collect::>() - .join(","); - write!(f, "{}", display) - } -} diff --git a/libp2p-rendezvous-server/src/swarm.rs b/libp2p-rendezvous-server/src/swarm.rs new file mode 100644 index 0000000000..96e8cc764b --- /dev/null +++ b/libp2p-rendezvous-server/src/swarm.rs @@ -0,0 +1,156 @@ +use anyhow::{Context, Result}; +use futures::{AsyncRead, AsyncWrite}; +use libp2p::core::transport::Boxed; +use libp2p::core::upgrade::Version; +use libp2p::identity::{self}; +use libp2p::rendezvous::server::Behaviour; +use libp2p::tcp; +use libp2p::yamux; +use libp2p::{core::muxing::StreamMuxerBox, SwarmBuilder}; +use libp2p::{dns, noise, rendezvous, Multiaddr, PeerId, Swarm, Transport}; +use libp2p_tor::{AddressConversion, TorTransport}; +use std::fmt; +use tor_hsservice::config::OnionServiceConfigBuilder; + +/// Defaults we use for the networking +mod defaults { + use std::time::Duration; + + // We keep connections open for 10 minutes + pub const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60 * 10); + + // Five intro points are a reasonable default + pub const HIDDEN_SERVICE_NUM_INTRO_POINTS: u8 = 5; + + pub const MULTIPLEX_TIMEOUT: Duration = Duration::from_secs(60); +} + +pub fn create_swarm(identity: identity::Keypair) -> Result> { + let transport = create_transport(&identity).context("Failed to create transport")?; + let rendezvous = rendezvous::server::Behaviour::new(rendezvous::server::Config::default()); + + let swarm = SwarmBuilder::with_existing_identity(identity) + .with_tokio() + .with_other_transport(|_| transport)? + .with_behaviour(|_| rendezvous)? + .with_swarm_config(|cfg| { + cfg.with_idle_connection_timeout(defaults::IDLE_CONNECTION_TIMEOUT) + }) + .build(); + + Ok(swarm) +} + +pub async fn create_swarm_with_onion( + identity: identity::Keypair, + onion_port: u16, +) -> Result> { + let (transport, onion_address) = create_transport_with_onion(&identity, onion_port) + .await + .context("Failed to create transport with onion")?; + let rendezvous = rendezvous::server::Behaviour::new(rendezvous::server::Config::default()); + + let mut swarm = SwarmBuilder::with_existing_identity(identity) + .with_tokio() + .with_other_transport(|_| transport)? + .with_behaviour(|_| rendezvous)? + .with_swarm_config(|cfg| { + cfg.with_idle_connection_timeout(defaults::IDLE_CONNECTION_TIMEOUT) + }) + .build(); + + // Listen on the onion address + swarm + .listen_on(onion_address.clone()) + .context("Failed to listen on onion address")?; + + tracing::info!(%onion_address, "Onion service configured"); + + Ok(swarm) +} + +fn create_transport(identity: &identity::Keypair) -> Result> { + let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); + let tcp_with_dns = dns::tokio::Transport::system(tcp)?; + + let transport = authenticate_and_multiplex(tcp_with_dns.boxed(), &identity).unwrap(); + + Ok(transport) +} + +async fn create_transport_with_onion( + identity: &identity::Keypair, + onion_port: u16, +) -> Result<(Boxed<(PeerId, StreamMuxerBox)>, Multiaddr)> { + // Create TCP transport + let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); + let tcp_with_dns = dns::tokio::Transport::system(tcp)?; + + // Create Tor transport + let mut tor_transport = TorTransport::unbootstrapped() + .await? + .with_address_conversion(AddressConversion::IpAndDns); + + // Create onion service configuration + let onion_service_config = OnionServiceConfigBuilder::default() + .nickname( + identity + .public() + .to_peer_id() + .to_base58() + .to_ascii_lowercase() + .parse() + .unwrap(), + ) + .num_intro_points(defaults::HIDDEN_SERVICE_NUM_INTRO_POINTS) + .build() + .unwrap(); + + // Add onion service and get the address + let onion_address = tor_transport.add_onion_service(onion_service_config, onion_port)?; + + // Combine transports + let combined_transport = tcp_with_dns + .boxed() + .or_transport(tor_transport.boxed()) + .boxed(); + + let transport = authenticate_and_multiplex(combined_transport, &identity).unwrap(); + + Ok((transport, onion_address)) +} + +fn authenticate_and_multiplex( + transport: Boxed, + identity: &identity::Keypair, +) -> Result> +where + T: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + let noise_config = noise::Config::new(identity).unwrap(); + + let transport = transport + .upgrade(Version::V1) + .authenticate(noise_config) + .multiplex(yamux::Config::default()) + .timeout(defaults::MULTIPLEX_TIMEOUT) + .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) + .boxed(); + + Ok(transport) +} + +pub struct Addresses<'a>(pub &'a [Multiaddr]); + +// Prints an array of multiaddresses as a comma seperated string +impl fmt::Display for Addresses<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let display = self + .0 + .iter() + .map(|addr| addr.to_string()) + .collect::>() + .join(","); + write!(f, "{}", display) + } +} From 72906ab18563d33de4f71345383b1678fcb16f86 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 8 Oct 2025 17:15:10 +0200 Subject: [PATCH 07/16] actually start docker integration --- Cargo.lock | 44 +++++++++++++++++++++++++ swap-env/src/config.rs | 4 +-- swap-orchestrator/Cargo.toml | 3 ++ swap-orchestrator/src/main.rs | 57 ++++++++++++++++++++++++++++----- swap-orchestrator/src/prompt.rs | 6 ++-- 5 files changed, 101 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0d453e3f9..16e068f920 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,6 +1347,34 @@ dependencies = [ "tokio", ] +[[package]] +name = "bollard" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82e7850583ead5f8bbef247e2a3c37a19bd576e8420cd262a6711921827e1e5" +dependencies = [ + "base64 0.13.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http 0.2.12", + "hyper 0.14.32", + "hyperlocal", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "url", + "winapi", +] + [[package]] name = "bollard-stubs" version = "1.42.0-rc.3" @@ -4907,6 +4935,19 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "hyperlocal" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" +dependencies = [ + "futures-util", + "hex", + "hyper 0.14.32", + "pin-project", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -11234,12 +11275,15 @@ version = "0.1.0" dependencies = [ "anyhow", "bitcoin 0.32.7", + "bollard", "chrono", + "clap 4.5.48", "compose_spec", "dialoguer", "monero", "serde_yaml", "swap-env", + "tokio", "toml 0.9.7", "url", "vergen 8.3.2", diff --git a/swap-env/src/config.rs b/swap-env/src/config.rs index 60538eb54b..67b3370654 100644 --- a/swap-env/src/config.rs +++ b/swap-env/src/config.rs @@ -1,7 +1,7 @@ use crate::defaults::GetDefaults; use crate::env::{Mainnet, Testnet}; use crate::prompt; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use config::ConfigError; use libp2p::core::Multiaddr; use rust_decimal::Decimal; @@ -145,7 +145,7 @@ impl TryFrom for Config { pub struct ConfigNotInitialized; pub fn read_config(config_path: PathBuf) -> Result> { - if config_path.exists() { + if config_path.try_exists().unwrap_or(false) { tracing::info!( path = %config_path.display(), "Reading config file", diff --git a/swap-orchestrator/Cargo.toml b/swap-orchestrator/Cargo.toml index f214518f2d..d80b5126a3 100644 --- a/swap-orchestrator/Cargo.toml +++ b/swap-orchestrator/Cargo.toml @@ -10,12 +10,15 @@ path = "src/main.rs" [dependencies] anyhow = { workspace = true } bitcoin = { workspace = true } +bollard = "0.13.0" # old version because of dependency conflicts with testcontainers chrono = "0.4.41" +clap = { version = "4.5.48", features = ["derive"] } compose_spec = "0.3.0" dialoguer = { workspace = true } monero = { workspace = true } serde_yaml = "0.9.34" swap-env = { path = "../swap-env" } +tokio = { workspace = true, features = ["macros"] } toml = { workspace = true } url = { workspace = true } diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index dfee73c968..b4a7f263f2 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -1,10 +1,13 @@ use anyhow::{Context, Result, anyhow, bail}; +use bollard::container::{self, CreateContainerOptions}; use clap::Parser; use dialoguer::Select; use dialoguer::theme::ColorfulTheme; use std::fs::File; -use std::io::Write; +use std::io::{Read, Write}; +use std::os::unix::process::CommandExt; use std::path::PathBuf; +use std::process::Command; use swap_env::config::{ Bitcoin, Config, ConfigNotInitialized, Data, Maker, Monero, Network, TorConf, }; @@ -61,7 +64,40 @@ async fn main() { } async fn handle_start_command() -> anyhow::Result<()> { - let docker_client = bollard::Docker::connect_with_local_defaults(); + println!("connecting to docker daemon..."); + + let docker = bollard::Docker::connect_with_local_defaults() + .context("Couldn't connect to docker daemon")?; + + let info = match docker.info().await { + Ok(info) => info, + Err(error) => { + if matches!(error, bollard::errors::Error::IOError { .. }) { + println!("Couldn't connect to your docker daemon. Is it running?"); + return Ok(()); + } + + bail!(error); + } + }; + + println!("connected to daemon"); + + let mut unparsed_compose_config = String::new(); + File::open(DOCKER_COMPOSE_PATH)?.read_to_string(&mut unparsed_compose_config)?; + + let compose_config: compose_spec::Compose = serde_yaml::from_str(&unparsed_compose_config) + .context("invalid docker-compose.yml syntax")?; + + let services: Vec = compose_config.services.values().cloned().collect(); + + let probe_result = Command::new("docker") + .args(&["compose", "version"]) + .output(); + + if let Err(err) = probe_result { + bail!(anyhow!(err).context("Couldn't call docker compose cli - is the daemon running?")); + } Ok(()) } @@ -72,9 +108,11 @@ async fn handle_init_command( ) -> anyhow::Result<()> { // Read the already existing config, if it's there let existing_config: Option> = - swap_env::config::read_config(PathBuf::from(CONFIG_PATH)) - .ok() - .map(|res| res.context("Couldn't parse config.toml")); + match swap_env::config::read_config(PathBuf::from(CONFIG_PATH)) { + Ok(Ok(config)) => Some(Ok(config)), + Ok(Err(_)) => None, + Err(err) => Some(Err(err)), + }; // Run the wizard and generate the configs, if necessary. // If the maker config already exists, it will be returned as-is and only the docker config re-generated. @@ -84,9 +122,13 @@ async fn handle_init_command( // Write the configs to their files. let maker_config_stringified = toml::to_string(&maker_config)?; - File::create(CONFIG_PATH)?.write_all(maker_config_stringified.as_bytes())?; + File::create(CONFIG_PATH) + .context("Can't create maker config.toml file")? + .write_all(maker_config_stringified.as_bytes())?; let compose_config_stringified = compose_config.build(); - File::create(DOCKER_COMPOSE_PATH)?.write_all(compose_config_stringified.as_bytes())?; + File::create(DOCKER_COMPOSE_PATH) + .context("Can't create docker-compose.yml file")? + .write_all(compose_config_stringified.as_bytes())?; Ok(()) } @@ -165,7 +207,6 @@ fn setup_wizard( }; // At this point we either have no or an invalid config, so we do the whole wizard. - println!("Starting the wizard."); // Maker questions (spread, max, min etc) let min_buy = config_prompt::min_buy_amount()?; diff --git a/swap-orchestrator/src/prompt.rs b/swap-orchestrator/src/prompt.rs index 1325817c3f..7430555ef4 100644 --- a/swap-orchestrator/src/prompt.rs +++ b/swap-orchestrator/src/prompt.rs @@ -75,8 +75,8 @@ pub fn electrum_server_type(default_electrum_urls: &Vec) -> ElectrumServerT let select = Select::with_theme(&theme) .with_prompt("How do you want to connect to the Bitcoin blockchain?") .items(&[ - "Create a full Bitcoin node and Electrum server (most private - but requires 1-2 days to sync and ~500GB of disk space)", "Use a mix of default Electrum servers (instant)", + "Create a full Bitcoin node and Electrum server (most private - but requires 1-2 days to sync and ~500GB of disk space)", "Specify my own Electrum server (instant)", "Specify my own Electrum server in addition to the mix of default Electrum servers (instant)", "Print the list of the default Electrum servers" @@ -97,8 +97,8 @@ pub fn electrum_server_type(default_electrum_urls: &Vec) -> ElectrumServerT fn try_from(value: usize) -> Result { Ok(match value { - 0 => Choice::RunFullNode, - 1 => Choice::ElectrumPool, + 0 => Choice::ElectrumPool, + 1 => Choice::RunFullNode, 2 => Choice::CustomElectrumNode, 3 => Choice::CustomElectrumNodeAndPool, 4 => Choice::PrintPoolUrls, From d32ad614c06e74f48fb3e547c3397b6b12599ec3 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 15 Oct 2025 15:58:55 +0200 Subject: [PATCH 08/16] progress --- Cargo.lock | 43 --- swap-orchestrator/Cargo.toml | 9 +- swap-orchestrator/src/command.rs | 33 +++ swap-orchestrator/src/command/build.rs | 22 ++ swap-orchestrator/src/command/export.rs | 11 + swap-orchestrator/src/command/init.rs | 218 ++++++++++++++ swap-orchestrator/src/{ => command}/prompt.rs | 5 +- swap-orchestrator/src/command/start.rs | 20 ++ swap-orchestrator/src/docker.rs | 37 +++ swap-orchestrator/src/{ => docker}/compose.rs | 57 +++- .../src/{ => docker}/containers.rs | 4 +- swap-orchestrator/src/{ => docker}/images.rs | 0 swap-orchestrator/src/lib.rs | 10 +- swap-orchestrator/src/main.rs | 276 +----------------- swap-orchestrator/src/util.rs | 63 ++++ swap-orchestrator/tests/spec.rs | 4 +- 16 files changed, 467 insertions(+), 345 deletions(-) create mode 100644 swap-orchestrator/src/command.rs create mode 100644 swap-orchestrator/src/command/build.rs create mode 100644 swap-orchestrator/src/command/export.rs create mode 100644 swap-orchestrator/src/command/init.rs rename swap-orchestrator/src/{ => command}/prompt.rs (98%) create mode 100644 swap-orchestrator/src/command/start.rs create mode 100644 swap-orchestrator/src/docker.rs rename swap-orchestrator/src/{ => docker}/compose.rs (91%) rename swap-orchestrator/src/{ => docker}/containers.rs (98%) rename swap-orchestrator/src/{ => docker}/images.rs (100%) create mode 100644 swap-orchestrator/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 61e92819b6..8bba28e253 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,34 +1369,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "bollard" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82e7850583ead5f8bbef247e2a3c37a19bd576e8420cd262a6711921827e1e5" -dependencies = [ - "base64 0.13.1", - "bollard-stubs", - "bytes", - "futures-core", - "futures-util", - "hex", - "http 0.2.12", - "hyper 0.14.32", - "hyperlocal", - "log", - "pin-project-lite", - "serde", - "serde_derive", - "serde_json", - "serde_urlencoded", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "url", - "winapi", -] - [[package]] name = "bollard-stubs" version = "1.42.0-rc.3" @@ -4965,19 +4937,6 @@ dependencies = [ "windows-registry", ] -[[package]] -name = "hyperlocal" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" -dependencies = [ - "futures-util", - "hex", - "hyper 0.14.32", - "pin-project", - "tokio", -] - [[package]] name = "iana-time-zone" version = "0.1.64" @@ -11400,8 +11359,6 @@ version = "0.1.0" dependencies = [ "anyhow", "bitcoin 0.32.7", - "bollard", - "chrono", "clap 4.5.49", "compose_spec", "dialoguer", diff --git a/swap-orchestrator/Cargo.toml b/swap-orchestrator/Cargo.toml index d80b5126a3..10e0b38005 100644 --- a/swap-orchestrator/Cargo.toml +++ b/swap-orchestrator/Cargo.toml @@ -10,18 +10,17 @@ path = "src/main.rs" [dependencies] anyhow = { workspace = true } bitcoin = { workspace = true } -bollard = "0.13.0" # old version because of dependency conflicts with testcontainers -chrono = "0.4.41" clap = { version = "4.5.48", features = ["derive"] } -compose_spec = "0.3.0" dialoguer = { workspace = true } monero = { workspace = true } -serde_yaml = "0.9.34" +serde_yaml = "0.9.33" swap-env = { path = "../swap-env" } -tokio = { workspace = true, features = ["macros"] } +tokio = { workspace = true, features = ["macros", "process"] } toml = { workspace = true } url = { workspace = true } +compose_spec = "0.3.0" + [build-dependencies] anyhow = { workspace = true } vergen = { workspace = true } diff --git a/swap-orchestrator/src/command.rs b/swap-orchestrator/src/command.rs new file mode 100644 index 0000000000..8a5001234d --- /dev/null +++ b/swap-orchestrator/src/command.rs @@ -0,0 +1,33 @@ +mod build; +mod export; +mod init; +pub mod prompt; +mod start; + +pub use build::build; +pub use export::export; +pub use init::init; +pub use start::start; + +/// Top level args to the orchestrator cli. +/// Fields in here can/must always be specified. +#[derive(clap::Parser)] +pub struct Args { + #[arg( + long, + default_value = "false", + long_help = "Specify this flag when you want to run on Bitcoin Testnet and Monero Stagenet. Mainly used for development." + )] + pub testnet: bool, + /// The actual command to execute. + #[command(subcommand)] + pub command: Option, +} + +#[derive(clap::Subcommand)] +pub enum Command { + Init, + Start, + Build, + Export, +} diff --git a/swap-orchestrator/src/command/build.rs b/swap-orchestrator/src/command/build.rs new file mode 100644 index 0000000000..c85535c49a --- /dev/null +++ b/swap-orchestrator/src/command/build.rs @@ -0,0 +1,22 @@ +use crate::util::CommandExt; +use crate::{command, flag}; + +pub async fn build() -> anyhow::Result<()> { + println!("Pulling the latest Docker images..."); + let mut command = command!("docker", flag!("compose"), flag!("pull")).to_tokio_command()?; + command.exec_piped().await?; + + println!("Building the Docker images... (this might take a while)"); + let mut command = command!( + "docker", + flag!("compose"), + flag!("build"), + flag!("--no-cache") + ) + .to_tokio_command()?; + command.exec_piped().await?; + + println!("Done!"); + + Ok(()) +} diff --git a/swap-orchestrator/src/command/export.rs b/swap-orchestrator/src/command/export.rs new file mode 100644 index 0000000000..2199dd2958 --- /dev/null +++ b/swap-orchestrator/src/command/export.rs @@ -0,0 +1,11 @@ +use anyhow::Context; + +pub async fn export() -> anyhow::Result<()> { + let volume_path = crate::docker::get_volume_path("asb-data") + .await + .context("Couldn't get the path to the ASB data volume")?; + + println!("ASB data volume path: {}", volume_path.display()); + + Ok(()) +} diff --git a/swap-orchestrator/src/command/init.rs b/swap-orchestrator/src/command/init.rs new file mode 100644 index 0000000000..541dd5acd2 --- /dev/null +++ b/swap-orchestrator/src/command/init.rs @@ -0,0 +1,218 @@ +use std::{fs::File, io::Write, path::PathBuf}; + +use crate::command::prompt::{self, ElectrumServerType, MoneroNodeType}; +use crate::docker::containers::add_maker_services; +use crate::{docker::compose::ComposeConfig, util::unix_epoch_secs}; +use anyhow::{Context, Result, bail}; +use dialoguer::{Select, theme::ColorfulTheme}; +use swap_env::config::{Bitcoin, Data, Maker, Monero, Network, TorConf}; +use swap_env::defaults::GetDefaults; +use swap_env::{ + config::Config, + env::{Mainnet, Testnet}, + prompt as config_prompt, +}; + +pub async fn init( + bitcoin_network: bitcoin::Network, + monero_network: monero::Network, +) -> anyhow::Result<()> { + // Read the already existing config, if it's there + let existing_config: Option> = + match swap_env::config::read_config(PathBuf::from(crate::CONFIG_PATH)) { + Ok(Ok(config)) => Some(Ok(config)), + Ok(Err(_)) => None, + Err(err) => Some(Err(err)), + }; + + // Run the wizard and generate the configs, if necessary. + // If the maker config already exists, it will be returned as-is and only the docker config re-generated. + let (maker_config, compose_config) = + setup_wizard(existing_config, bitcoin_network, monero_network) + .context("Couldn't execute swap wizard")?; + + // Write the configs to their files. + let maker_config_stringified = toml::to_string(&maker_config)?; + File::create(crate::CONFIG_PATH) + .context("Can't create maker config.toml file")? + .write_all(maker_config_stringified.as_bytes())?; + let compose_config_stringified = compose_config.build(); + File::create(crate::DOCKER_COMPOSE_PATH) + .context("Can't create docker-compose.yml file")? + .write_all(compose_config_stringified.as_bytes())?; + + Ok(()) +} + +/// Take a possibly already existing config.toml and (if necessary) go through the wizard steps necessary to +/// (if necessary) generate it and the docker-compose.yml +/// +/// # Returns +/// The complete maker config.toml and docker compose config. +fn setup_wizard( + existing_config: Option>, + bitcoin_network: bitcoin::Network, + monero_network: monero::Network, +) -> Result<(Config, ComposeConfig)> { + // If we already have a valid config, just use it and deduce the monero/bitcoin settings + if let Some(Ok(config)) = existing_config { + // If the config points to our local electrs node, we must have previously created it + let create_full_bitcoin_node = config + .bitcoin + .electrum_rpc_urls + .iter() + .any(|url| url.as_str().contains("tcp://electrs:")); + // Same for monero + let create_full_monero_node = config + .monero + .daemon_url + .as_ref() + .is_some_and(|url| url.as_str().contains("http://monerod:")); + + let name = compose_name(config.bitcoin.network, config.monero.network)?; + let mut compose = ComposeConfig::new(name); + crate::docker::containers::add_maker_services( + &mut compose, + config.bitcoin.network, + config.monero.network, + create_full_bitcoin_node, + create_full_monero_node, + ); + + return Ok((config, compose)); + } + + // If we have an invalid config we offer to procede as if there was no config and rename the old one + if let Some(Err(err)) = existing_config { + println!( + "Error: We couldn't parse your existing config.toml file (`{}`)", + err + ); + + let proposed_filename = format!("config.toml.invalid-{}", unix_epoch_secs()); + + let choice = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("How do you want to procede?") + .item(format!( + "Start wizard from scratch and rename my existing `config.toml` to `{proposed_filename}`" + )) + .item("Abort and leave my `config.toml` alone") + .interact()?; + + if choice != 0 { + println!("Stopping wizard. Goodbye!"); + bail!("User doesn't want to procede.") + } + + std::fs::rename(crate::CONFIG_PATH, &proposed_filename)?; + println!("Renamed your old config to `{proposed_filename}`.") + } + + let defaults = match (bitcoin_network, monero_network) { + (bitcoin::Network::Bitcoin, monero::Network::Mainnet) => { + Mainnet::get_config_file_defaults()? + } + (bitcoin::Network::Testnet, monero::Network::Stagenet) => { + Testnet::get_config_file_defaults()? + } + (a, b) => bail!("unsupported network combo (bitocoin={a}, monero={b:?}"), + }; + + // At this point we either have no or an invalid config, so we do the whole wizard. + + // Maker questions (spread, max, min etc) + let min_buy = config_prompt::min_buy_amount()?; + let max_buy = config_prompt::max_buy_amount()?; + let markup = config_prompt::ask_spread()?; + // Networking: rendezvous points, hidden service, etc. + let rendezvous_points = config_prompt::rendezvous_points()?; + let hidden_service = config_prompt::tor_hidden_service()?; + let listen_addresses = config_prompt::listen_addresses(&defaults.listen_address_tcp)?; + // Monero and Electrum node types (local vs remote) + let monero_node_type = prompt::monero_node_type(); + let electrum_node_type = prompt::electrum_server_type(&defaults.electrum_rpc_urls); + // Whether to tip the devs + let tip = config_prompt::developer_tip()?; + + // Derive docker compose config from + let create_full_bitcoin_node = matches!(electrum_node_type, ElectrumServerType::Included); + let create_full_monero_node = matches!(monero_node_type, MoneroNodeType::Included); + + let mut compose = ComposeConfig::new(compose_name(bitcoin_network, monero_network)?); + let (asb_data, compose_electrs_url, compose_monerd_rpc_url) = add_maker_services( + &mut compose, + bitcoin_network, + monero_network, + create_full_bitcoin_node, + create_full_monero_node, + ); + + let actual_electrum_rpc_urls = match electrum_node_type { + ElectrumServerType::Included => vec![compose_electrs_url], + ElectrumServerType::Remote(remote_nodes) => remote_nodes, + }; + // None means Monero RPC pool + let actual_monerod_url = match monero_node_type { + MoneroNodeType::Included => Some(compose_monerd_rpc_url), + MoneroNodeType::Remote(remote_node) => Some(remote_node), + MoneroNodeType::Pool => None, + }; + + let config = Config { + data: Data { + dir: asb_data.as_root_dir(), + }, + maker: Maker { + max_buy_btc: max_buy, + min_buy_btc: min_buy, + ask_spread: markup, + external_bitcoin_redeem_address: None, + price_ticker_ws_url: defaults.price_ticker_ws_url, + developer_tip: tip, + }, + bitcoin: Bitcoin { + electrum_rpc_urls: actual_electrum_rpc_urls, + target_block: defaults.bitcoin_confirmation_target, + // None means use default from env.rs + finality_confirmations: None, + network: bitcoin_network, + use_mempool_space_fee_estimation: defaults.use_mempool_space_fee_estimation, + }, + monero: Monero { + daemon_url: actual_monerod_url, + // None means use default from env.rs + finality_confirmations: None, + network: monero_network, + }, + network: Network { + listen: listen_addresses, + rendezvous_point: rendezvous_points, + external_addresses: vec![], + }, + tor: TorConf { + register_hidden_service: hidden_service, + ..Default::default() + }, + }; + + Ok((config, compose)) +} + +fn compose_name( + bitcoin_network: bitcoin::Network, + monero_network: monero::Network, +) -> Result { + let monero_network_str = match monero_network { + monero::Network::Mainnet => "mainnet", + monero::Network::Stagenet => "stagenet", + _ => bail!("unknown monero network"), + }; + let bitcoin_network_str = match bitcoin_network { + bitcoin::Network::Bitcoin => "bitcoin", + bitcoin::Network::Testnet => "testnet", + _ => bail!("unknown bitcoin network"), + }; + Ok(format!( + "bitcoin_{bitcoin_network_str}_monero_{monero_network_str}" + )) +} diff --git a/swap-orchestrator/src/prompt.rs b/swap-orchestrator/src/command/prompt.rs similarity index 98% rename from swap-orchestrator/src/prompt.rs rename to swap-orchestrator/src/command/prompt.rs index 7430555ef4..78be050b43 100644 --- a/swap-orchestrator/src/prompt.rs +++ b/swap-orchestrator/src/command/prompt.rs @@ -1,9 +1,6 @@ use anyhow::bail; use dialoguer::{Input, Select, theme::ColorfulTheme}; -use swap_env::{ - config::Monero, - prompt::{self as config_prompt, print_info_box}, -}; +use swap_env::prompt::print_info_box; use url::Url; #[derive(Debug)] diff --git a/swap-orchestrator/src/command/start.rs b/swap-orchestrator/src/command/start.rs new file mode 100644 index 0000000000..8a55a41736 --- /dev/null +++ b/swap-orchestrator/src/command/start.rs @@ -0,0 +1,20 @@ +use anyhow::bail; + +use crate::{ + command, flag, + util::{CommandExt, probe_docker, probe_maker_config}, +}; + +pub async fn start() -> anyhow::Result<()> { + if !matches!(probe_maker_config().await, Some(Ok(_))) { + bail!("No valid maker config.toml file found. Please run `orchestrator init` first."); + } + + probe_docker().await?; + + let mut command = command!("docker", flag!("compose"), flag!("up"), flag!("-d")) + .to_tokio_command() + .expect("non-empty command"); + + command.exec_piped().await.map(|_| ()) +} diff --git a/swap-orchestrator/src/docker.rs b/swap-orchestrator/src/docker.rs new file mode 100644 index 0000000000..6ad865d8b1 --- /dev/null +++ b/swap-orchestrator/src/docker.rs @@ -0,0 +1,37 @@ +use std::path::PathBuf; + +use anyhow::{Context, bail}; +use tokio::{fs::File, io::AsyncReadExt}; + +use crate::util::probe_docker; + +pub mod compose; +pub mod containers; +pub mod images; + +/// Get the path of a volume in the docker compose config. +/// Errors if the volume or the docker-compose.yml file doesn't exist. +pub async fn get_volume_path(volume_name: &str) -> anyhow::Result { + probe_docker().await?; + + let mut compose_config_string = String::new(); + File::open(crate::DOCKER_COMPOSE_PATH) + .await + .context("Failed to open docker-compose.yml. Are you in the right directory?")? + .read_to_string(&mut compose_config_string) + .await?; + let compose_config: compose_spec::Compose = serde_yaml::from_str(&compose_config_string)?; + + if !compose_config.volumes.keys().any(|key| key == volume_name) { + bail!("Volume {volume_name} not found in docker-compose.yml"); + } + + let project_name = compose_config + .name + .context("docker-compose.yml doesn't have a name")? + .to_string(); + + Ok(PathBuf::from(format!( + "/var/lib/docker/volumes/{project_name}_{volume_name}/_data" + ))) +} diff --git a/swap-orchestrator/src/compose.rs b/swap-orchestrator/src/docker/compose.rs similarity index 91% rename from swap-orchestrator/src/compose.rs rename to swap-orchestrator/src/docker/compose.rs index dec2c39a44..1264cf113f 100644 --- a/swap-orchestrator/src/compose.rs +++ b/swap-orchestrator/src/docker/compose.rs @@ -4,6 +4,7 @@ use std::{ sync::Arc, }; +use anyhow::Context; use url::Url; use crate::writer::IndentedWriter; @@ -20,6 +21,7 @@ trait WriteConfig { /// using [`ComposeConfig::add_volume`] and [`ComposeConfig::add_service`]. #[derive(Debug, Clone)] pub struct ComposeConfig { + name: String, services: Vec>, volumes: Vec>, } @@ -45,6 +47,7 @@ pub struct Service { pub struct Mount { host_path: VolumeOrPath, container_path: PathBuf, + read_only: bool, } /// Host side of a mount expression. @@ -79,11 +82,11 @@ pub enum ImageSource { #[derive(Debug, Clone)] pub struct Command { - flags: Vec, + pub flags: Vec, } #[derive(Debug, Clone)] -pub struct Flag(String); +pub struct Flag(pub String); /// Configure when to restart the service. #[derive(Debug, Clone, Copy)] @@ -92,6 +95,14 @@ pub enum RestartType { } impl ComposeConfig { + pub fn new(name: impl Into) -> ComposeConfig { + ComposeConfig { + name: name.into(), + services: Vec::new(), + volumes: Vec::new(), + } + } + /// Add a volume to the config. /// Returns a handle which can be used to reference this volume later. pub fn add_volume(&mut self, name: impl Into) -> Arc { @@ -126,15 +137,6 @@ impl ComposeConfig { } } -impl Default for ComposeConfig { - fn default() -> ComposeConfig { - ComposeConfig { - services: Vec::new(), - volumes: Vec::new(), - } - } -} - impl WriteConfig for ComposeConfig { fn write_to(&self, writer: &mut IndentedWriter) { writeln!(writer, "# This file is automatically @generated by the eigenwallet orchestrator.\n# It is not intended for manual editing.").unwrap(); @@ -373,6 +375,7 @@ impl Mount { Mount { host_path: VolumeOrPath::Path(host_path.into()), container_path: container_path.into(), + read_only: false, } } @@ -383,6 +386,7 @@ impl Mount { Mount { host_path: VolumeOrPath::Volume(volume.clone()), container_path: volume.as_root_dir(), + read_only: false, } } @@ -392,6 +396,15 @@ impl Mount { Mount { host_path: VolumeOrPath::Volume(volume.clone()), container_path: container_path.into(), + read_only: false, + } + } + + /// Make a `Mount` read only for the container. + pub fn read_only(self) -> Mount { + Mount { + read_only: true, + ..self } } } @@ -405,7 +418,9 @@ impl WriteConfig for Mount { let container = self.container_path.to_string_lossy().to_string(); - writeln!(writer, "- {host}:{container}").expect("writing to a string doesn't fail") + let read_only = if self.read_only { "ro" } else { "rw" }; + writeln!(writer, "- {host}:{container}:{read_only}") + .expect("writing to a string doesn't fail") } } @@ -466,6 +481,18 @@ impl Command { pub fn add_flag(&mut self, flag: impl Into) { self.flags.push(flag.into()); } + + /// Convert to a tokio::process::Command. + /// Fails if the command is empty. + pub fn to_tokio_command(&self) -> anyhow::Result { + let mut parts = self.flags.iter().map(|flag| flag.0.clone()); + let binary_name = parts.next().context("Can't run empty command")?; + + let mut command = tokio::process::Command::new(binary_name); + command.args(parts); + + Ok(command) + } } impl WriteConfig for Command { @@ -483,17 +510,17 @@ impl WriteConfig for Command { #[macro_export] macro_rules! flag { ($flag:expr) => { - crate::compose::Flag::new(format!($flag)) + crate::docker::compose::Flag::new(format!($flag)) }; ($flag:expr, $($args:expr),*) => { - crate::compose::Flag::new(format!($flag, $($args),*)) + crate::docker::compose::Flag::new(format!($flag, $($args),*)) }; } #[macro_export] macro_rules! command { ($command:expr $(, $flag:expr)* $(,)?) => { - crate::compose::Command::new(vec![flag!($command) $(, $flag)*]) + crate::docker::compose::Command::new(vec![crate::flag!($command) $(, $flag)*]) }; } diff --git a/swap-orchestrator/src/containers.rs b/swap-orchestrator/src/docker/containers.rs similarity index 98% rename from swap-orchestrator/src/containers.rs rename to swap-orchestrator/src/docker/containers.rs index 2000f2c215..e64dc2e4f1 100644 --- a/swap-orchestrator/src/containers.rs +++ b/swap-orchestrator/src/docker/containers.rs @@ -7,9 +7,9 @@ use url::Url; /// Currently this only includes which flags we need to pass to the binaries use crate::{ command, - compose::{ComposeConfig, Flag, ImageSource, Mount, Service, Volume}, + docker::compose::{ComposeConfig, Flag, ImageSource, Mount, Service, Volume}, + docker::images::{self, PINNED_GIT_REPOSITORY}, flag, - images::{self, PINNED_GIT_REPOSITORY}, }; // Important: don't add slashes or anything here diff --git a/swap-orchestrator/src/images.rs b/swap-orchestrator/src/docker/images.rs similarity index 100% rename from swap-orchestrator/src/images.rs rename to swap-orchestrator/src/docker/images.rs diff --git a/swap-orchestrator/src/lib.rs b/swap-orchestrator/src/lib.rs index a9073d00e9..40e5476b74 100644 --- a/swap-orchestrator/src/lib.rs +++ b/swap-orchestrator/src/lib.rs @@ -1,5 +1,7 @@ -pub mod compose; -pub mod containers; -pub mod images; -pub mod prompt; +pub mod command; +pub mod docker; +pub mod util; pub mod writer; + +pub const CONFIG_PATH: &str = "config.toml"; +pub const DOCKER_COMPOSE_PATH: &str = "docker-compose.yml"; diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index b4a7f263f2..1f37b7516c 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -1,43 +1,5 @@ -use anyhow::{Context, Result, anyhow, bail}; -use bollard::container::{self, CreateContainerOptions}; use clap::Parser; -use dialoguer::Select; -use dialoguer::theme::ColorfulTheme; -use std::fs::File; -use std::io::{Read, Write}; -use std::os::unix::process::CommandExt; -use std::path::PathBuf; -use std::process::Command; -use swap_env::config::{ - Bitcoin, Config, ConfigNotInitialized, Data, Maker, Monero, Network, TorConf, -}; -use swap_env::prompt::{self as config_prompt}; -use swap_env::{defaults::GetDefaults, env::Mainnet, env::Testnet}; -use swap_orchestrator::compose::ComposeConfig; -use swap_orchestrator::containers::add_maker_services; - -use swap_orchestrator::prompt::{self, ElectrumServerType, MoneroNodeType}; - -const CONFIG_PATH: &str = "config.toml"; -const DOCKER_COMPOSE_PATH: &str = "docker-compose.yml"; - -#[derive(clap::Parser)] -struct Args { - #[arg( - long, - default_value = "false", - long_help = "Specify this flag when you want to run on Bitcoin Testnet and Monero Stagenet. Mainly used for development." - )] - testnet: bool, - #[command(subcommand)] - command: Option, -} - -#[derive(clap::Subcommand)] -enum Commands { - Init, - Start, -} +use swap_orchestrator::command::{self, Args, Command}; #[tokio::main] async fn main() { @@ -51,8 +13,10 @@ async fn main() { }; let result = match args.command { - None | Some(Commands::Init) => handle_init_command(bitcoin_network, monero_network).await, - Some(Commands::Start) => handle_start_command().await, + None | Some(Command::Init) => command::init(bitcoin_network, monero_network).await, + Some(Command::Start) => command::start().await, + Some(Command::Build) => command::build().await, + Some(Command::Export) => command::export().await, }; if let Err(err) = result { @@ -62,233 +26,3 @@ async fn main() { ); } } - -async fn handle_start_command() -> anyhow::Result<()> { - println!("connecting to docker daemon..."); - - let docker = bollard::Docker::connect_with_local_defaults() - .context("Couldn't connect to docker daemon")?; - - let info = match docker.info().await { - Ok(info) => info, - Err(error) => { - if matches!(error, bollard::errors::Error::IOError { .. }) { - println!("Couldn't connect to your docker daemon. Is it running?"); - return Ok(()); - } - - bail!(error); - } - }; - - println!("connected to daemon"); - - let mut unparsed_compose_config = String::new(); - File::open(DOCKER_COMPOSE_PATH)?.read_to_string(&mut unparsed_compose_config)?; - - let compose_config: compose_spec::Compose = serde_yaml::from_str(&unparsed_compose_config) - .context("invalid docker-compose.yml syntax")?; - - let services: Vec = compose_config.services.values().cloned().collect(); - - let probe_result = Command::new("docker") - .args(&["compose", "version"]) - .output(); - - if let Err(err) = probe_result { - bail!(anyhow!(err).context("Couldn't call docker compose cli - is the daemon running?")); - } - - Ok(()) -} - -async fn handle_init_command( - bitcoin_network: bitcoin::Network, - monero_network: monero::Network, -) -> anyhow::Result<()> { - // Read the already existing config, if it's there - let existing_config: Option> = - match swap_env::config::read_config(PathBuf::from(CONFIG_PATH)) { - Ok(Ok(config)) => Some(Ok(config)), - Ok(Err(_)) => None, - Err(err) => Some(Err(err)), - }; - - // Run the wizard and generate the configs, if necessary. - // If the maker config already exists, it will be returned as-is and only the docker config re-generated. - let (maker_config, compose_config) = - setup_wizard(existing_config, bitcoin_network, monero_network) - .context("Couldn't execute swap wizard")?; - - // Write the configs to their files. - let maker_config_stringified = toml::to_string(&maker_config)?; - File::create(CONFIG_PATH) - .context("Can't create maker config.toml file")? - .write_all(maker_config_stringified.as_bytes())?; - let compose_config_stringified = compose_config.build(); - File::create(DOCKER_COMPOSE_PATH) - .context("Can't create docker-compose.yml file")? - .write_all(compose_config_stringified.as_bytes())?; - - Ok(()) -} - -/// Take a possibly already existing config.toml and (if necessary) go through the wizard steps necessary to -/// (if necessary) generate it and the docker-compose.yml -/// -/// # Returns -/// The complete maker config.toml and docker compose config. -fn setup_wizard( - existing_config: Option>, - bitcoin_network: bitcoin::Network, - monero_network: monero::Network, -) -> Result<(Config, ComposeConfig)> { - // If we already have a valid config, just use it and deduce the monero/bitcoin settings - if let Some(Ok(config)) = existing_config { - // If the config points to our local electrs node, we must have previously created it - let create_full_bitcoin_node = config - .bitcoin - .electrum_rpc_urls - .iter() - .any(|url| url.as_str().contains("tcp://electrs:")); - // Same for monero - let create_full_monero_node = config - .monero - .daemon_url - .as_ref() - .is_some_and(|url| url.as_str().contains("http://monerod:")); - - let mut compose = ComposeConfig::default(); - add_maker_services( - &mut compose, - config.bitcoin.network, - config.monero.network, - create_full_bitcoin_node, - create_full_monero_node, - ); - - return Ok((config, compose)); - } - - // If we have an invalid config we offer to procede as if there was no config and rename the old one - if let Some(Err(err)) = existing_config { - println!( - "Error: We couldn't parse your existing config.toml file (`{}`)", - err - ); - - let proposed_filename = format!("config.toml.invalid-{}", unix_epoch_secs()); - - let choice = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("How do you want to procede?") - .item(format!( - "Start wizard from scratch and rename my existing `config.toml` to `{proposed_filename}`" - )) - .item("Abort and leave my `config.toml` alone") - .interact()?; - - if choice != 0 { - println!("Stopping wizard. Goodbye!"); - bail!("User doesn't want to procede.") - } - - std::fs::rename(CONFIG_PATH, &proposed_filename)?; - println!("Renamed your old config to `{proposed_filename}`.") - } - - let defaults = match (bitcoin_network, monero_network) { - (bitcoin::Network::Bitcoin, monero::Network::Mainnet) => { - Mainnet::get_config_file_defaults()? - } - (bitcoin::Network::Testnet, monero::Network::Stagenet) => { - Testnet::get_config_file_defaults()? - } - (a, b) => bail!("unsupported network combo (bitocoin={a}, monero={b:?}"), - }; - - // At this point we either have no or an invalid config, so we do the whole wizard. - - // Maker questions (spread, max, min etc) - let min_buy = config_prompt::min_buy_amount()?; - let max_buy = config_prompt::max_buy_amount()?; - let markup = config_prompt::ask_spread()?; - // Networking: rendezvous points, hidden service, etc. - let rendezvous_points = config_prompt::rendezvous_points()?; - let hidden_service = config_prompt::tor_hidden_service()?; - let listen_addresses = config_prompt::listen_addresses(&defaults.listen_address_tcp)?; - // Monero and Electrum node types (local vs remote) - let monero_node_type = prompt::monero_node_type(); - let electrum_node_type = prompt::electrum_server_type(&defaults.electrum_rpc_urls); - // Whether to tip the devs - let tip = config_prompt::developer_tip()?; - - // Derive docker compose config from - let create_full_bitcoin_node = matches!(electrum_node_type, ElectrumServerType::Included); - let create_full_monero_node = matches!(monero_node_type, MoneroNodeType::Included); - - let mut compose = ComposeConfig::default(); - let (asb_data, compose_electrs_url, compose_monerd_rpc_url) = add_maker_services( - &mut compose, - bitcoin_network, - monero_network, - create_full_bitcoin_node, - create_full_monero_node, - ); - - let actual_electrum_rpc_urls = match electrum_node_type { - ElectrumServerType::Included => vec![compose_electrs_url], - ElectrumServerType::Remote(remote_nodes) => remote_nodes, - }; - // None means Monero RPC pool - let actual_monerod_url = match monero_node_type { - MoneroNodeType::Included => Some(compose_monerd_rpc_url), - MoneroNodeType::Remote(remote_node) => Some(remote_node), - MoneroNodeType::Pool => None, - }; - - let config = Config { - data: Data { - dir: asb_data.as_root_dir(), - }, - maker: Maker { - max_buy_btc: max_buy, - min_buy_btc: min_buy, - ask_spread: markup, - external_bitcoin_redeem_address: None, - price_ticker_ws_url: defaults.price_ticker_ws_url, - developer_tip: tip, - }, - bitcoin: Bitcoin { - electrum_rpc_urls: actual_electrum_rpc_urls, - target_block: defaults.bitcoin_confirmation_target, - // None means use default from env.rs - finality_confirmations: None, - network: bitcoin_network, - use_mempool_space_fee_estimation: defaults.use_mempool_space_fee_estimation, - }, - monero: Monero { - daemon_url: actual_monerod_url, - // None means use default from env.rs - finality_confirmations: None, - network: monero_network, - }, - network: Network { - listen: listen_addresses, - rendezvous_point: rendezvous_points, - external_addresses: vec![], - }, - tor: TorConf { - register_hidden_service: hidden_service, - ..Default::default() - }, - }; - - Ok((config, compose)) -} - -fn unix_epoch_secs() -> u64 { - std::time::UNIX_EPOCH - .elapsed() - .expect("unix epoch to be elapsed") - .as_secs() -} diff --git a/swap-orchestrator/src/util.rs b/swap-orchestrator/src/util.rs new file mode 100644 index 0000000000..00271a156f --- /dev/null +++ b/swap-orchestrator/src/util.rs @@ -0,0 +1,63 @@ +use std::{path::PathBuf, process::Stdio}; + +use anyhow::{Context, Result}; +use swap_env::config::Config; + +use crate::{command, flag}; + +/// Get the number of seconds since unix epoch. +pub fn unix_epoch_secs() -> u64 { + std::time::UNIX_EPOCH + .elapsed() + .expect("unix epoch to be elapsed") + .as_secs() +} + +/// Probe that docker compose is available and the daemon is running. +pub async fn probe_docker() -> Result<()> { + // Just a random docker command which requires the daemon to be running. + match command!("docker", flag!("compose"), flag!("ps")) + .to_tokio_command() + .expect("non-empty command") + .output() + .await + { + Ok(output) if output.status.success() => Ok(()), + Ok(output) => Err(anyhow::anyhow!( + "Docker compose is not available. Are you sure it's installed and running?\n\nerror: {}", + String::from_utf8(output.stderr).unwrap_or_else(|_| "unknown error".to_string()) + )), + Err(err) => Err(anyhow::anyhow!( + "Failed to probe docker compose. Are you sure it's installed and running?\n\nerror: {}", + err + )), + } +} + +/// Check whether there's a valid maker config.toml file in the current directory. +/// `None` if there isn't, `Some(Err(err))` if there is but it's invalid, `Some(Ok(config))` if there is and it's valid. +pub async fn probe_maker_config() -> Option> { + // Read the already existing config, if it's there + match swap_env::config::read_config(PathBuf::from(crate::CONFIG_PATH)) { + Ok(Ok(config)) => Some(Ok(config)), + Ok(Err(_)) => None, + Err(err) => Some(Err(err)), + } +} + +#[allow(async_fn_in_trait)] +pub trait CommandExt { + async fn exec_piped(&mut self) -> anyhow::Result; +} + +impl CommandExt for tokio::process::Command { + async fn exec_piped(&mut self) -> anyhow::Result { + self.stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()? + .wait() + .await + .context("Failed to execute command") + } +} diff --git a/swap-orchestrator/tests/spec.rs b/swap-orchestrator/tests/spec.rs index 1865e7f7dd..af6371f6dc 100644 --- a/swap-orchestrator/tests/spec.rs +++ b/swap-orchestrator/tests/spec.rs @@ -32,7 +32,9 @@ fn test_orchestrator_spec_generation() { ), }, directories: OrchestratorDirectories { - asb_data_dir: std::path::PathBuf::from(swap_orchestrator::compose::ASB_DATA_DIR), + asb_data_dir: std::path::PathBuf::from( + swap_orchestrator::docker::compose::ASB_DATA_DIR, + ), }, }; From 9a2811b74cdd4203d3a8d98d60163f8c660bce77 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 12 Nov 2025 15:17:15 +0100 Subject: [PATCH 09/16] add example output and doc comment to Service struct. also fix compilation errors --- swap-orchestrator/Cargo.toml | 4 +- swap-orchestrator/src/docker/compose.rs | 14 +++ swap-orchestrator/tests/output/config.toml | 28 +++++ .../tests/output/docker-compose.yml | 105 ++++++++++++++++++ swap-orchestrator/tests/spec.rs | 45 -------- 5 files changed, 149 insertions(+), 47 deletions(-) create mode 100644 swap-orchestrator/tests/output/config.toml create mode 100644 swap-orchestrator/tests/output/docker-compose.yml diff --git a/swap-orchestrator/Cargo.toml b/swap-orchestrator/Cargo.toml index 10e0b38005..d7f9bbce1a 100644 --- a/swap-orchestrator/Cargo.toml +++ b/swap-orchestrator/Cargo.toml @@ -13,9 +13,9 @@ bitcoin = { workspace = true } clap = { version = "4.5.48", features = ["derive"] } dialoguer = { workspace = true } monero = { workspace = true } -serde_yaml = "0.9.33" +serde_yaml = "0.9.34" swap-env = { path = "../swap-env" } -tokio = { workspace = true, features = ["macros", "process"] } +tokio = { workspace = true, features = ["macros", "process", "fs", "io-util"] } toml = { workspace = true } url = { workspace = true } diff --git a/swap-orchestrator/src/docker/compose.rs b/swap-orchestrator/src/docker/compose.rs index 1264cf113f..f8ae39eda4 100644 --- a/swap-orchestrator/src/docker/compose.rs +++ b/swap-orchestrator/src/docker/compose.rs @@ -27,6 +27,20 @@ pub struct ComposeConfig { } /// A service which may be added to a [`ComposeConfig`]. +/// You will then get an `Arc` back, which you can use to +/// specify inter-service dependencies. +/// +/// # Example +/// ```ignore +/// let mut compose_config = ComposeConfig::new("my-docker-compose-project") +/// let foo = Service::new("foo", ImageSource::PullFromRegistry { ... }); +/// let foo: Arc = compose_config.add_service(foo); +/// +/// let bar = Service::new("bar", ImageSource::PullFromRegistry { ... }) +/// .with_dependency(foo.clone()); +/// let bar: Arc = compose_config.add_service(bar); +/// // Now bar will have `depends_on: foo` in it's generated output +/// ``` #[derive(Debug, Clone)] pub struct Service { name: String, diff --git a/swap-orchestrator/tests/output/config.toml b/swap-orchestrator/tests/output/config.toml new file mode 100644 index 0000000000..7d60001c99 --- /dev/null +++ b/swap-orchestrator/tests/output/config.toml @@ -0,0 +1,28 @@ +[data] +dir = "/asb-data" + +[network] +listen = ["/ip4/0.0.0.0/tcp/9939"] +rendezvous_point = ["/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", "/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw", "/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU", "/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT", "/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp", "/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa", "/dns4/aswap.click/tcp/8888/p2p/12D3KooWQzW52mdsLHTMu1EPiz3APumG6vGwpCuyy494MAQoEa5X", "/dns4/getxmr.st/tcp/8888/p2p/12D3KooWHHwiz6WDThPT8cEurstomg3kDSxzL2L8pwxfyX2fpxVk"] +external_addresses = [] + +[bitcoin] +electrum_rpc_urls = ["tcp://electrs:50001"] +target_block = 1 +network = "Mainnet" +use_mempool_space_fee_estimation = true + +[monero] +daemon_url = "http://monerod:18081/" +network = "Mainnet" + +[tor] +register_hidden_service = true +hidden_service_num_intro_points = 5 + +[maker] +min_buy_btc = 0.002 +max_buy_btc = 0.02 +ask_spread = 0.02 +price_ticker_ws_url = "wss://ws.kraken.com/" +developer_tip = 0.01 diff --git a/swap-orchestrator/tests/output/docker-compose.yml b/swap-orchestrator/tests/output/docker-compose.yml new file mode 100644 index 0000000000..604c6fb52d --- /dev/null +++ b/swap-orchestrator/tests/output/docker-compose.yml @@ -0,0 +1,105 @@ +# This file was auto-generated by `orchestrator` on 2025-11-10 14:52:27 UTC +# +# It is pinned to build the `asb` and `asb-controller` images from this commit: +# https://github.com/eigenwallet/core.git#833fc0ab24e40555d53f05e6e04728460dab5988 +# +# If the code does not match the hash, the build will fail. This ensures that the code cannot be altered by Github. +# The compiled `orchestrator` has this hash burned into the binary. +# +# To update the `asb` and `asb-controller` images, you need to either: +# - re-compile the `orchestrator` binary from a commit from Github +# - download a newer pre-compiled version of the `orchestrator` binary from Github. +# +# After updating the `orchestrator` binary, re-generate the compose file by running `orchestrator` again. +# +# The used images for `bitcoind`, `monerod`, `electrs` are pinned to specific hashes which prevents them from being altered by the Docker registry. +# +# Please check for new releases regularly. Breaking network changes are rare, but they do happen from time to time. +name: mainnet_monero_mainnet_bitcoin +services: + monerod: + container_name: monerod + image: ghcr.io/sethforprivacy/simple-monerod@sha256:f30e5706a335c384e4cf420215cbffd1196f0b3a11d4dd4e819fe3e0bca41ec5 + restart: unless-stopped + user: root + volumes: + - 'monerod-data:/monerod-data/' + expose: + - 18081 + entrypoint: '' + command: ["monerod","--rpc-bind-ip=0.0.0.0","--rpc-bind-port=18081","--data-dir=/monerod-data/","--confirm-external-bind","--restricted-rpc","--non-interactive","--enable-dns-blocklist"] + bitcoind: + container_name: bitcoind + image: getumbrel/bitcoind@sha256:c565266ea302c9ab2fc490f04ff14e584210cde3d0d991b8309157e5dfae9e8d + restart: unless-stopped + volumes: + - 'bitcoind-data:/bitcoind-data/' + expose: + - 8332 + - 8333 + user: root + entrypoint: '' + command: ["bitcoind","-chain=main","-rpcallowip=0.0.0.0/0","-rpcbind=0.0.0.0:8332","-bind=0.0.0.0:8333","-datadir=/bitcoind-data/","-dbcache=16384","-server=1","-prune=0","-txindex=1"] + electrs: + container_name: electrs + image: getumbrel/electrs@sha256:622657fbdc7331a69f5b3444e6f87867d51ac27d90c399c8bf25d9aab020052b + restart: unless-stopped + user: root + depends_on: + - bitcoind + volumes: + - 'bitcoind-data:/bitcoind-data' + - 'electrs-data:/electrs-data' + expose: + - 50001 + entrypoint: '' + command: ["electrs","--network=bitcoin","--daemon-dir=/bitcoind-data/","--db-dir=/electrs-data/db","--daemon-rpc-addr=bitcoind:8332","--daemon-p2p-addr=bitcoind:8333","--electrum-rpc-addr=0.0.0.0:50001","--log-filters=INFO"] + asb: + container_name: asb + build: { context: "https://github.com/eigenwallet/core.git#833fc0ab24e40555d53f05e6e04728460dab5988", dockerfile: "./swap-asb/Dockerfile" } + restart: unless-stopped + depends_on: + - electrs + volumes: + - './config.toml:/asb-data/config.toml' + - 'asb-data:/asb-data' + ports: + - '0.0.0.0:9939:9939' + entrypoint: '' + command: ["asb","--config=/asb-data/config.toml","start","--rpc-bind-port=9944","--rpc-bind-host=0.0.0.0"] + asb-controller: + container_name: asb-controller + build: { context: "https://github.com/eigenwallet/core.git#833fc0ab24e40555d53f05e6e04728460dab5988", dockerfile: "./swap-controller/Dockerfile" } + stdin_open: true + tty: true + restart: unless-stopped + depends_on: + - asb + entrypoint: '' + command: ["asb-controller","--url=http://asb:9944"] + asb-tracing-logger: + container_name: asb-tracing-logger + image: alpine@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 + restart: unless-stopped + depends_on: + - asb + volumes: + - 'asb-data:/asb-data:ro' + entrypoint: '' + command: ["sh","-c","tail -f /asb-data/logs/tracing*.log"] + rendezvous-node: + container_name: rendezvous-node + build: { context: "https://github.com/eigenwallet/core.git#833fc0ab24e40555d53f05e6e04728460dab5988", dockerfile: "./libp2p-rendezvous-node/Dockerfile" } + restart: unless-stopped + volumes: + - 'rendezvous-data:/rendezvous-data' + ports: + - '0.0.0.0:8888:8888' + entrypoint: '' + command: ["rendezvous-node","--data-dir=/rendezvous-data","--port=8888"] +volumes: + monerod-data: + bitcoind-data: + electrs-data: + asb-data: + rendezvous-data: diff --git a/swap-orchestrator/tests/spec.rs b/swap-orchestrator/tests/spec.rs index af6371f6dc..8b13789179 100644 --- a/swap-orchestrator/tests/spec.rs +++ b/swap-orchestrator/tests/spec.rs @@ -1,46 +1 @@ -use swap_orchestrator::compose::{ - IntoSpec, OrchestratorDirectories, OrchestratorImage, OrchestratorImages, OrchestratorInput, - OrchestratorNetworks, OrchestratorPorts, -}; -use swap_orchestrator::images; -#[test] -fn test_orchestrator_spec_generation() { - let input = OrchestratorInput { - ports: OrchestratorPorts { - monerod_rpc: 38081, - bitcoind_rpc: 18332, - bitcoind_p2p: 18333, - electrs: 60001, - asb_libp2p: 9839, - asb_rpc_port: 9944, - }, - networks: OrchestratorNetworks { - monero: monero::Network::Stagenet, - bitcoin: bitcoin::Network::Testnet, - }, - images: OrchestratorImages { - monerod: OrchestratorImage::Registry(images::MONEROD_IMAGE.to_string()), - electrs: OrchestratorImage::Registry(images::ELECTRS_IMAGE.to_string()), - bitcoind: OrchestratorImage::Registry(images::BITCOIND_IMAGE.to_string()), - asb: OrchestratorImage::Build(images::ASB_IMAGE_FROM_SOURCE.clone()), - asb_controller: OrchestratorImage::Build( - images::ASB_CONTROLLER_IMAGE_FROM_SOURCE.clone(), - ), - asb_tracing_logger: OrchestratorImage::Registry( - images::ASB_TRACING_LOGGER_IMAGE.to_string(), - ), - }, - directories: OrchestratorDirectories { - asb_data_dir: std::path::PathBuf::from( - swap_orchestrator::docker::compose::ASB_DATA_DIR, - ), - }, - }; - - let spec = input.to_spec(); - - println!("{}", spec); - - // TODO: Here we should use the docker binary to verify the compose file -} From 2a04f17f720ac600508115be46c13557d9ec0e60 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 12 Nov 2025 15:36:21 +0100 Subject: [PATCH 10/16] add test to make sure the orchestrator generates the expected docker-compose.yml from a given config --- swap-orchestrator/src/command/init.rs | 39 ++----------- swap-orchestrator/src/util.rs | 40 +++++++++++++ .../tests/produces_expected_outcome.rs | 57 +++++++++++++++++++ swap-orchestrator/tests/spec.rs | 1 - 4 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 swap-orchestrator/tests/produces_expected_outcome.rs delete mode 100644 swap-orchestrator/tests/spec.rs diff --git a/swap-orchestrator/src/command/init.rs b/swap-orchestrator/src/command/init.rs index 541dd5acd2..dff80face9 100644 --- a/swap-orchestrator/src/command/init.rs +++ b/swap-orchestrator/src/command/init.rs @@ -56,20 +56,10 @@ fn setup_wizard( ) -> Result<(Config, ComposeConfig)> { // If we already have a valid config, just use it and deduce the monero/bitcoin settings if let Some(Ok(config)) = existing_config { - // If the config points to our local electrs node, we must have previously created it - let create_full_bitcoin_node = config - .bitcoin - .electrum_rpc_urls - .iter() - .any(|url| url.as_str().contains("tcp://electrs:")); - // Same for monero - let create_full_monero_node = config - .monero - .daemon_url - .as_ref() - .is_some_and(|url| url.as_str().contains("http://monerod:")); - - let name = compose_name(config.bitcoin.network, config.monero.network)?; + let create_full_bitcoin_node = crate::util::should_create_full_bitcoin_node(&config); + let create_full_monero_node = crate::util::should_create_full_monero_node(&config); + + let name = crate::util::compose_name(config.bitcoin.network, config.monero.network)?; let mut compose = ComposeConfig::new(name); crate::docker::containers::add_maker_services( &mut compose, @@ -138,7 +128,7 @@ fn setup_wizard( let create_full_bitcoin_node = matches!(electrum_node_type, ElectrumServerType::Included); let create_full_monero_node = matches!(monero_node_type, MoneroNodeType::Included); - let mut compose = ComposeConfig::new(compose_name(bitcoin_network, monero_network)?); + let mut compose = ComposeConfig::new(crate::util::compose_name(bitcoin_network, monero_network)?); let (asb_data, compose_electrs_url, compose_monerd_rpc_url) = add_maker_services( &mut compose, bitcoin_network, @@ -197,22 +187,3 @@ fn setup_wizard( Ok((config, compose)) } - -fn compose_name( - bitcoin_network: bitcoin::Network, - monero_network: monero::Network, -) -> Result { - let monero_network_str = match monero_network { - monero::Network::Mainnet => "mainnet", - monero::Network::Stagenet => "stagenet", - _ => bail!("unknown monero network"), - }; - let bitcoin_network_str = match bitcoin_network { - bitcoin::Network::Bitcoin => "bitcoin", - bitcoin::Network::Testnet => "testnet", - _ => bail!("unknown bitcoin network"), - }; - Ok(format!( - "bitcoin_{bitcoin_network_str}_monero_{monero_network_str}" - )) -} diff --git a/swap-orchestrator/src/util.rs b/swap-orchestrator/src/util.rs index 00271a156f..f3cb62aa32 100644 --- a/swap-orchestrator/src/util.rs +++ b/swap-orchestrator/src/util.rs @@ -45,6 +45,46 @@ pub async fn probe_maker_config() -> Option> { } } +/// Determine whether we should create a full Bitcoin node in the docker compose. +/// Returns true if the config points to the local electrs node. +pub fn should_create_full_bitcoin_node(config: &Config) -> bool { + config + .bitcoin + .electrum_rpc_urls + .iter() + .any(|url| url.as_str().contains("tcp://electrs:")) +} + +/// Determine whether we should create a full Monero node in the docker compose. +/// Returns true if the config points to the local monerod node. +pub fn should_create_full_monero_node(config: &Config) -> bool { + config + .monero + .daemon_url + .as_ref() + .is_some_and(|url| url.as_str().contains("http://monerod:")) +} + +/// Generate the docker compose project name based on the Bitcoin and Monero networks. +pub fn compose_name( + bitcoin_network: bitcoin::Network, + monero_network: monero::Network, +) -> Result { + let monero_network_str = match monero_network { + monero::Network::Mainnet => "mainnet", + monero::Network::Stagenet => "stagenet", + _ => anyhow::bail!("unknown monero network"), + }; + let bitcoin_network_str = match bitcoin_network { + bitcoin::Network::Bitcoin => "bitcoin", + bitcoin::Network::Testnet => "testnet", + _ => anyhow::bail!("unknown bitcoin network"), + }; + Ok(format!( + "bitcoin_{bitcoin_network_str}_monero_{monero_network_str}" + )) +} + #[allow(async_fn_in_trait)] pub trait CommandExt { async fn exec_piped(&mut self) -> anyhow::Result; diff --git a/swap-orchestrator/tests/produces_expected_outcome.rs b/swap-orchestrator/tests/produces_expected_outcome.rs new file mode 100644 index 0000000000..291f09f75a --- /dev/null +++ b/swap-orchestrator/tests/produces_expected_outcome.rs @@ -0,0 +1,57 @@ +use std::path::PathBuf; +use swap_orchestrator::docker::compose::ComposeConfig; +use swap_orchestrator::docker::containers::add_maker_services; + +#[tokio::test] +async fn test_config_generates_expected_compose() { + // 1. Read and parse the example config from tests/output/config.toml + let test_config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/output/config.toml"); + + let config = swap_env::config::Config::read(&test_config_path) + .expect("Failed to read test config.toml"); + + // 2. Determine if we should create full nodes based on the config + let create_full_bitcoin_node = swap_orchestrator::util::should_create_full_bitcoin_node(&config); + let create_full_monero_node = swap_orchestrator::util::should_create_full_monero_node(&config); + + // 3. Generate the compose config using the actual code path + let compose_name = swap_orchestrator::util::compose_name( + config.bitcoin.network, + config.monero.network, + ) + .expect("Failed to generate compose name"); + + let mut compose = ComposeConfig::new(compose_name); + add_maker_services( + &mut compose, + config.bitcoin.network, + config.monero.network, + create_full_bitcoin_node, + create_full_monero_node, + ); + + // 4. Build the YAML string + let generated_yaml = compose.build(); + + // 5. Read the expected YAML + let expected_yaml_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/output/docker-compose.yml"); + let expected_yaml = std::fs::read_to_string(expected_yaml_path) + .expect("Failed to read expected docker-compose.yml"); + + // 6. Parse both as serde_yaml::Value to compare structure (ignoring comments/whitespace) + let generated_value: serde_yaml::Value = serde_yaml::from_str(&generated_yaml) + .expect("Failed to parse generated YAML"); + let expected_value: serde_yaml::Value = serde_yaml::from_str(&expected_yaml) + .expect("Failed to parse expected YAML"); + + // 7. Assert they match + assert_eq!( + generated_value, expected_value, + "Generated docker-compose.yml does not match expected output.\n\ + Generated:\n{}\n\n\ + Expected:\n{}", + generated_yaml, expected_yaml + ); +} diff --git a/swap-orchestrator/tests/spec.rs b/swap-orchestrator/tests/spec.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/swap-orchestrator/tests/spec.rs +++ /dev/null @@ -1 +0,0 @@ - From 106ac8ea182bb0090bc797b703cc7dbf21a55d05 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 12 Nov 2025 15:38:45 +0100 Subject: [PATCH 11/16] add test to ci --- .github/workflows/ci.yml | 2 ++ ..._expected_outcome.rs => produces_expected_compose_config.rs} | 0 2 files changed, 2 insertions(+) rename swap-orchestrator/tests/{produces_expected_outcome.rs => produces_expected_compose_config.rs} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7c961ac36..3ee8f168fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,8 @@ jobs: test_name: alice_empty_balance_after_started_btc_early_refund - package: swap test_name: alice_broken_wallet_rpc_after_started_btc_early_refund + - package: swap-orchestrator + test_name: produces_expected_compose_config runs-on: ubuntu-22.04 if: github.event_name == 'push' || !github.event.pull_request.draft diff --git a/swap-orchestrator/tests/produces_expected_outcome.rs b/swap-orchestrator/tests/produces_expected_compose_config.rs similarity index 100% rename from swap-orchestrator/tests/produces_expected_outcome.rs rename to swap-orchestrator/tests/produces_expected_compose_config.rs From 1e67ed223eac912e9dad922cd4689ea4bf8edd4b Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 12 Nov 2025 17:36:10 +0100 Subject: [PATCH 12/16] rename test, make it pass --- .github/workflows/ci.yml | 2 +- swap-orchestrator/src/docker/compose.rs | 104 ++++++++++++++---- swap-orchestrator/src/docker/containers.rs | 46 ++++++-- swap-orchestrator/src/util.rs | 4 +- ...rator_produces_expected_compose_config.rs} | 39 ++++--- .../tests/output/docker-compose.yml | 13 ++- 6 files changed, 153 insertions(+), 55 deletions(-) rename swap-orchestrator/tests/{produces_expected_compose_config.rs => orchestrator_produces_expected_compose_config.rs} (50%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ee8f168fc..99e73d3c1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,7 @@ jobs: - package: swap test_name: alice_broken_wallet_rpc_after_started_btc_early_refund - package: swap-orchestrator - test_name: produces_expected_compose_config + test_name: orchestrator_produces_expected_compose_config runs-on: ubuntu-22.04 if: github.event_name == 'push' || !github.event.pull_request.draft diff --git a/swap-orchestrator/src/docker/compose.rs b/swap-orchestrator/src/docker/compose.rs index f8ae39eda4..525c1e4858 100644 --- a/swap-orchestrator/src/docker/compose.rs +++ b/swap-orchestrator/src/docker/compose.rs @@ -46,9 +46,15 @@ pub struct Service { name: String, depends_on: Vec>, image_source: ImageSource, + /// Ports that are exposed externally (mapped to host) - becomes `ports:` in docker-compose.yml exposed_ports: Vec, + /// Ports that are only documented (not actually exposed) - becomes `expose:` in docker-compose.yml + /// Note: These ports are still accessible from other services in the same Docker network + /// regardless of whether they're listed here. This is purely for documentation. + unexposed_ports: Vec, volumes: Vec, restart_type: RestartType, + user: Option, entrypoint: Option, command: Option, stdin_open: Option, @@ -61,7 +67,7 @@ pub struct Service { pub struct Mount { host_path: VolumeOrPath, container_path: PathBuf, - read_only: bool, + access_mode: AccessMode, } /// Host side of a mount expression. @@ -79,6 +85,15 @@ pub struct Volume { name: String, } +/// How a volume is accessed. +/// Default is ReadAndWrite, but we use ReadOnly for +/// the `asb-tracing-logger` for example. +#[derive(Debug, Clone)] +pub enum AccessMode { + ReadAndWrite, + ReadOnly, +} + /// Configure how to obtain the container image. #[derive(Debug, Clone)] pub enum ImageSource { @@ -154,7 +169,7 @@ impl ComposeConfig { impl WriteConfig for ComposeConfig { fn write_to(&self, writer: &mut IndentedWriter) { writeln!(writer, "# This file is automatically @generated by the eigenwallet orchestrator.\n# It is not intended for manual editing.").unwrap(); - writeln!(writer, "name: eigenwallet-maker").unwrap(); + writeln!(writer, "name: {}", self.name).unwrap(); writeln!(writer, "services:").unwrap(); writer.indented(|writer| { @@ -183,10 +198,12 @@ impl Service { name, depends_on: Vec::new(), exposed_ports: Vec::new(), + unexposed_ports: Vec::new(), image_source, command: None, restart_type: RestartType::UnlessStopped, volumes: Vec::new(), + user: None, entrypoint: None, stdin_open: None, tty: None, @@ -194,12 +211,12 @@ impl Service { } } - /// Expose a specific port of this service. + /// Expose a specific port of this service externally (mapped to host). /// Can be called multiple times. /// /// Expands to the following: /// ```docker ignore - /// expose: + /// ports: /// - 0.0.0.0:{port}:{port} /// ``` /// @@ -211,6 +228,23 @@ impl Service { self } + /// Add an unexposed port (for documentation only). + /// Can be called multiple times. + /// + /// Expands to the following: + /// ```docker ignore + /// expose: + /// - {port} + /// ``` + /// + /// Note: These ports are still accessible from other services in the same Docker network + /// regardless of whether they're listed here. This is purely for documentation. + pub fn with_unexposed_port(mut self, port: u16) -> Service { + self.unexposed_ports.push(port); + + self + } + /// Add a volume or other path to this service's container by /// mounting it from the host system. pub fn with_mount(mut self, mount: Mount) -> Service { @@ -262,6 +296,13 @@ impl Service { self } + /// Set the user to run the container as. + pub fn with_user(mut self, user: impl Into) -> Service { + self.user = Some(user.into()); + + self + } + /// Get the name of the service. pub fn name(&self) -> &str { &self.name @@ -286,6 +327,11 @@ impl WriteConfig for Service { // restart self.restart_type.write_to(writer); + // user (if specified) + if let Some(user) = &self.user { + writeln!(writer, "user: {}", user).unwrap(); + } + // depends_on (if specified) if !self.depends_on.is_empty() { writeln!(writer, "depends_on:").unwrap(); @@ -306,16 +352,6 @@ impl WriteConfig for Service { writeln!(writer, "tty: {tty}").unwrap(); } - // entrypoint (if specified) - if let Some(entrypoint) = &self.entrypoint { - writeln!(writer, "entrypoint: \"{}\"", entrypoint).unwrap(); - } - - // command (if specified) - if let Some(command) = &self.command { - command.write_to(writer); - } - // volumes (if specified) if !self.volumes.is_empty() { writeln!(writer, "volumes:").unwrap(); @@ -327,17 +363,37 @@ impl WriteConfig for Service { }); } - // exposed ports (if specified) + // ports - externally exposed ports (if specified) if !self.exposed_ports.is_empty() { - writeln!(writer, "expose:").unwrap(); + writeln!(writer, "ports:").unwrap(); writer.indented(|writer| { for port in &self.exposed_ports { + writeln!(writer, "- '0.0.0.0:{port}:{port}'").unwrap(); + } + }) + } + + // expose - unexposed ports for documentation (if specified) + if !self.unexposed_ports.is_empty() { + writeln!(writer, "expose:").unwrap(); + + writer.indented(|writer| { + for port in &self.unexposed_ports { writeln!(writer, "- {port}").unwrap(); } }) } + // entrypoint (always write, even if empty) + let entrypoint = self.entrypoint.as_deref().unwrap_or(""); + writeln!(writer, "entrypoint: '{}'", entrypoint).unwrap(); + + // command (if specified) + if let Some(command) = &self.command { + command.write_to(writer); + } + // Add the "disabled" profile if the service was disabled // -> service isn't started unless specifically specified if !self.enabled { @@ -389,7 +445,7 @@ impl Mount { Mount { host_path: VolumeOrPath::Path(host_path.into()), container_path: container_path.into(), - read_only: false, + access_mode: AccessMode::ReadAndWrite, } } @@ -400,7 +456,7 @@ impl Mount { Mount { host_path: VolumeOrPath::Volume(volume.clone()), container_path: volume.as_root_dir(), - read_only: false, + access_mode: AccessMode::ReadAndWrite, } } @@ -410,14 +466,14 @@ impl Mount { Mount { host_path: VolumeOrPath::Volume(volume.clone()), container_path: container_path.into(), - read_only: false, + access_mode: AccessMode::ReadAndWrite, } } /// Make a `Mount` read only for the container. pub fn read_only(self) -> Mount { Mount { - read_only: true, + access_mode: AccessMode::ReadOnly, ..self } } @@ -432,8 +488,12 @@ impl WriteConfig for Mount { let container = self.container_path.to_string_lossy().to_string(); - let read_only = if self.read_only { "ro" } else { "rw" }; - writeln!(writer, "- {host}:{container}:{read_only}") + let access_mode_suffix = if matches!(self.access_mode, AccessMode::ReadOnly) { + ":ro" + } else { + "" + }; + writeln!(writer, "- {host}:{container}{access_mode_suffix}") .expect("writing to a string doesn't fail") } } diff --git a/swap-orchestrator/src/docker/containers.rs b/swap-orchestrator/src/docker/containers.rs index e64dc2e4f1..356ad0b509 100644 --- a/swap-orchestrator/src/docker/containers.rs +++ b/swap-orchestrator/src/docker/containers.rs @@ -15,9 +15,12 @@ use crate::{ // Important: don't add slashes or anything here // Todo: find better way to do that const MONEROD_DATA: &str = "monerod-data"; -const BITCOIN_DATA: &str = "bitcoin-data"; +const BITCOIN_DATA: &str = "bitcoind-data"; const ELECTRS_DATA: &str = "electrs-data"; const ASB_DATA: &str = "asb-data"; +const RENDEZVOUS_DATA: &str = "rendezvous-data"; + +const ROOT_USER: &str = "root"; /// Add all the services/volumes to the compose config. /// Returns urls for the electrum rpc and monero rpc endpoints. @@ -53,6 +56,7 @@ pub fn add_maker_services( ); let asb_controller = asb_controller(compose, asb_rpc_port, asb.clone()); let asb_tracing_logger = asb_tracing_logger(compose, asb, asb_data.clone()); + let rendezvous = rendezvous_node(compose); let electrum_rpc_url: Url = format!( "tcp://{electrs_name}:{electrs_port}", @@ -102,7 +106,8 @@ pub fn monerod( Service::new("monerod", ImageSource::from_registry(images::MONEROD_IMAGE)) .with_enabled(enabled) .with_mount(Mount::volume(&monerod_data)) - .with_exposed_port(monerod_rpc_port) + .with_unexposed_port(monerod_rpc_port) + .with_user(ROOT_USER) .with_command(monerod_command); let monerod_service = compose.add_service(monerod_service); @@ -141,8 +146,9 @@ pub fn bitcoind( ImageSource::from_registry(images::BITCOIND_IMAGE), ) .with_mount(Mount::volume(&bitcoind_data)) - .with_exposed_port(rpc_port) - .with_exposed_port(p2p_port) + .with_unexposed_port(rpc_port) + .with_unexposed_port(p2p_port) + .with_user(ROOT_USER) .with_command(bitcoind_command) .with_enabled(enabled); @@ -184,9 +190,11 @@ pub fn electrs( ); let service = Service::new("electrs", ImageSource::from_registry(images::ELECTRS_IMAGE)) .with_dependency(bitcoind.clone()) - .with_exposed_port(port) - .with_command(command) + .with_mount(Mount::volume(&bitcoind_data)) .with_mount(Mount::volume(&electrs_data)) + .with_unexposed_port(port) + .with_user(ROOT_USER) + .with_command(command) .with_enabled(enabled); let service = compose.add_service(service); @@ -290,7 +298,31 @@ pub fn asb_tracing_logger( ImageSource::from_registry(images::ASB_TRACING_LOGGER_IMAGE), ) .with_dependency(asb) - .with_mount(Mount::volume(&asb_data)) + .with_mount(Mount::volume(&asb_data).read_only()) + .with_command(command); + + compose.add_service(service) +} + +pub fn rendezvous_node(compose: &mut ComposeConfig) -> Arc { + let rendezvous_data = compose.add_volume(RENDEZVOUS_DATA); + let port = 8888; + + let command = command![ + "rendezvous-node", + flag!("--data-dir={}", rendezvous_data.as_root_dir().display()), + flag!("--port={port}") + ]; + + let service = Service::new( + "rendezvous-node", + ImageSource::from_source( + PINNED_GIT_REPOSITORY.parse().expect("valid url"), + "./libp2p-rendezvous-node/Dockerfile", + ), + ) + .with_mount(Mount::volume(&rendezvous_data)) + .with_exposed_port(port) .with_command(command); compose.add_service(service) diff --git a/swap-orchestrator/src/util.rs b/swap-orchestrator/src/util.rs index f3cb62aa32..f6905a1135 100644 --- a/swap-orchestrator/src/util.rs +++ b/swap-orchestrator/src/util.rs @@ -76,12 +76,12 @@ pub fn compose_name( _ => anyhow::bail!("unknown monero network"), }; let bitcoin_network_str = match bitcoin_network { - bitcoin::Network::Bitcoin => "bitcoin", + bitcoin::Network::Bitcoin => "mainnet", bitcoin::Network::Testnet => "testnet", _ => anyhow::bail!("unknown bitcoin network"), }; Ok(format!( - "bitcoin_{bitcoin_network_str}_monero_{monero_network_str}" + "{bitcoin_network_str}_monero_{monero_network_str}_bitcoin" )) } diff --git a/swap-orchestrator/tests/produces_expected_compose_config.rs b/swap-orchestrator/tests/orchestrator_produces_expected_compose_config.rs similarity index 50% rename from swap-orchestrator/tests/produces_expected_compose_config.rs rename to swap-orchestrator/tests/orchestrator_produces_expected_compose_config.rs index 291f09f75a..a4eab504ce 100644 --- a/swap-orchestrator/tests/produces_expected_compose_config.rs +++ b/swap-orchestrator/tests/orchestrator_produces_expected_compose_config.rs @@ -1,26 +1,26 @@ use std::path::PathBuf; +use swap_orchestrator::docker; use swap_orchestrator::docker::compose::ComposeConfig; use swap_orchestrator::docker::containers::add_maker_services; #[tokio::test] async fn test_config_generates_expected_compose() { // 1. Read and parse the example config from tests/output/config.toml - let test_config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests/output/config.toml"); + let test_config_path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/output/config.toml"); - let config = swap_env::config::Config::read(&test_config_path) - .expect("Failed to read test config.toml"); + let config = + swap_env::config::Config::read(&test_config_path).expect("Failed to read test config.toml"); // 2. Determine if we should create full nodes based on the config - let create_full_bitcoin_node = swap_orchestrator::util::should_create_full_bitcoin_node(&config); + let create_full_bitcoin_node = + swap_orchestrator::util::should_create_full_bitcoin_node(&config); let create_full_monero_node = swap_orchestrator::util::should_create_full_monero_node(&config); // 3. Generate the compose config using the actual code path - let compose_name = swap_orchestrator::util::compose_name( - config.bitcoin.network, - config.monero.network, - ) - .expect("Failed to generate compose name"); + let compose_name = + swap_orchestrator::util::compose_name(config.bitcoin.network, config.monero.network) + .expect("Failed to generate compose name"); let mut compose = ComposeConfig::new(compose_name); add_maker_services( @@ -35,16 +35,21 @@ async fn test_config_generates_expected_compose() { let generated_yaml = compose.build(); // 5. Read the expected YAML - let expected_yaml_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests/output/docker-compose.yml"); + let expected_yaml_path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/output/docker-compose.yml"); let expected_yaml = std::fs::read_to_string(expected_yaml_path) - .expect("Failed to read expected docker-compose.yml"); + .expect("Failed to read expected docker-compose.yml") + // Replace the git tag to match the current one. It is expected to be different + .replace( + "https://github.com/eigenwallet/core.git#833fc0ab24e40555d53f05e6e04728460dab5988", + docker::images::PINNED_GIT_REPOSITORY, + ); // 6. Parse both as serde_yaml::Value to compare structure (ignoring comments/whitespace) - let generated_value: serde_yaml::Value = serde_yaml::from_str(&generated_yaml) - .expect("Failed to parse generated YAML"); - let expected_value: serde_yaml::Value = serde_yaml::from_str(&expected_yaml) - .expect("Failed to parse expected YAML"); + let generated_value: serde_yaml::Value = + serde_yaml::from_str(&generated_yaml).expect("Failed to parse generated YAML"); + let expected_value: serde_yaml::Value = + serde_yaml::from_str(&expected_yaml).expect("Failed to parse expected YAML"); // 7. Assert they match assert_eq!( diff --git a/swap-orchestrator/tests/output/docker-compose.yml b/swap-orchestrator/tests/output/docker-compose.yml index 604c6fb52d..79ba245f12 100644 --- a/swap-orchestrator/tests/output/docker-compose.yml +++ b/swap-orchestrator/tests/output/docker-compose.yml @@ -23,23 +23,23 @@ services: restart: unless-stopped user: root volumes: - - 'monerod-data:/monerod-data/' + - 'monerod-data:/monerod-data' expose: - 18081 entrypoint: '' - command: ["monerod","--rpc-bind-ip=0.0.0.0","--rpc-bind-port=18081","--data-dir=/monerod-data/","--confirm-external-bind","--restricted-rpc","--non-interactive","--enable-dns-blocklist"] + command: ["monerod","--rpc-bind-ip=0.0.0.0","--rpc-bind-port=18081","--data-dir=/monerod-data","--confirm-external-bind","--restricted-rpc","--non-interactive","--enable-dns-blocklist"] bitcoind: container_name: bitcoind image: getumbrel/bitcoind@sha256:c565266ea302c9ab2fc490f04ff14e584210cde3d0d991b8309157e5dfae9e8d restart: unless-stopped volumes: - - 'bitcoind-data:/bitcoind-data/' + - 'bitcoind-data:/bitcoind-data' expose: - 8332 - 8333 user: root entrypoint: '' - command: ["bitcoind","-chain=main","-rpcallowip=0.0.0.0/0","-rpcbind=0.0.0.0:8332","-bind=0.0.0.0:8333","-datadir=/bitcoind-data/","-dbcache=16384","-server=1","-prune=0","-txindex=1"] + command: ["bitcoind","-chain=main","-rpcallowip=0.0.0.0/0","-rpcbind=0.0.0.0:8332","-bind=0.0.0.0:8333","-datadir=/bitcoind-data","-dbcache=16384","-server=1","-prune=0","-txindex=1"] electrs: container_name: electrs image: getumbrel/electrs@sha256:622657fbdc7331a69f5b3444e6f87867d51ac27d90c399c8bf25d9aab020052b @@ -53,16 +53,17 @@ services: expose: - 50001 entrypoint: '' - command: ["electrs","--network=bitcoin","--daemon-dir=/bitcoind-data/","--db-dir=/electrs-data/db","--daemon-rpc-addr=bitcoind:8332","--daemon-p2p-addr=bitcoind:8333","--electrum-rpc-addr=0.0.0.0:50001","--log-filters=INFO"] + command: ["electrs","--network=bitcoin","--daemon-dir=/bitcoind-data","--db-dir=/electrs-data/db","--daemon-rpc-addr=bitcoind:8332","--daemon-p2p-addr=bitcoind:8333","--electrum-rpc-addr=0.0.0.0:50001","--log-filters=INFO"] asb: container_name: asb build: { context: "https://github.com/eigenwallet/core.git#833fc0ab24e40555d53f05e6e04728460dab5988", dockerfile: "./swap-asb/Dockerfile" } restart: unless-stopped depends_on: - electrs + - monerod volumes: - - './config.toml:/asb-data/config.toml' - 'asb-data:/asb-data' + - './config.toml:/asb-data/config.toml' ports: - '0.0.0.0:9939:9939' entrypoint: '' From 9dee3edbf28cf183a2d4fb344749f9d7bc85c6fa Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 12 Nov 2025 17:50:54 +0100 Subject: [PATCH 13/16] merge master --- .cargo/config.toml | 3 + .dockerignore | 6 +- .github/actions/set-monero-env/action.yml | 5 +- .../setup-build-environment/action.yml | 2 +- .../workflows/build-gui-release-binaries.yml | 87 +- .github/workflows/build-release-binaries.yml | 6 +- .github/workflows/ci.yml | 6 + .github/workflows/draft-new-release.yml | 25 +- .gitignore | 3 +- .helix/ignore | 2 + .vscode/settings.json | 14 +- CHANGELOG.md | 87 +- Cargo.lock | 1580 ++++++++-------- Cargo.toml | 20 +- README.md | 23 +- .../brew_dependencies_install.sh | 0 {dev_scripts => dev-scripts}/bump-version.sh | 0 .../code2prompt_as_file_mac_os.sh | 0 .../code2prompt_to_clipboard.sh | 0 .../health_check_default_electrum_servers.py | 212 +++ dev-scripts/homebrew/eigenwallet.rb.template | 26 + .../publish_flatpak.sh | 181 +- .../regenerate_sqlx_cache.sh | 8 +- .../ubuntu_build_x86_86-w64-mingw32-gcc.sh | 8 +- docs/pages/usage/first_swap.mdx | 2 +- dprint.json | 1 - flatpak/eigenwallet.flatpakrepo | 11 + flatpak/net.unstoppableswap.gui.json | 36 - flatpak/org.eigenwallet.app.appdata.xml | 16 + flatpak/org.eigenwallet.app.flatpakref | 12 + flatpak/org.eigenwallet.app.json | 8 +- justfile | 33 +- .../Cargo.toml | 23 +- .../Dockerfile | 6 +- .../README.md | 2 +- libp2p-rendezvous-node/src/behaviour.rs | 62 + libp2p-rendezvous-node/src/main.rs | 173 ++ .../src/swarm.rs | 50 +- libp2p-rendezvous-node/src/tor.rs | 57 + libp2p-rendezvous-node/src/tracing_util.rs | 25 + libp2p-rendezvous-server/src/main.rs | 222 --- monero-harness/src/lib.rs | 57 +- monero-seed/Cargo.toml | 36 - monero-seed/LICENSE | 21 - monero-seed/README.md | 11 - monero-seed/src/lib.rs | 400 ---- monero-seed/src/tests.rs | 257 --- monero-seed/src/words/ang.rs | 1628 ----------------- monero-seed/src/words/de.rs | 1628 ----------------- monero-seed/src/words/en.rs | 1628 ----------------- monero-seed/src/words/eo.rs | 1628 ----------------- monero-seed/src/words/es.rs | 1628 ----------------- monero-seed/src/words/fr.rs | 1628 ----------------- monero-seed/src/words/it.rs | 1628 ----------------- monero-seed/src/words/ja.rs | 1628 ----------------- monero-seed/src/words/jbo.rs | 1628 ----------------- monero-seed/src/words/nl.rs | 1628 ----------------- monero-seed/src/words/pt.rs | 1628 ----------------- monero-seed/src/words/ru.rs | 1628 ----------------- monero-seed/src/words/zh.rs | 1628 ----------------- monero-sys/build.rs | 30 +- ...let_0003_pendingTransaction_getTxKey.patch | 64 - ...let_0003_pending_transaction_tx_keys.patch | 140 ++ monero-sys/src/bridge.h | 383 ++-- monero-sys/src/bridge.rs | 15 +- monero-sys/src/lib.rs | 140 +- monero-sys/tests/transaction_keys_testnet.rs | 90 + monero-tests/Cargo.toml | 17 + monero-tests/src/lib.rs | 1 + monero-tests/tests/transaction_keys.rs | 62 + monero-tests/tests/transfers.rs | 83 + monero-tests/tests/transfers_wrong_key.rs | 64 + rust-toolchain.toml | 2 +- src-gui/eslint.config.js | 5 + src-gui/knip.json | 7 + src-gui/package.json | 13 +- src-gui/src/models/rpcModel.ts | 110 +- src-gui/src/models/tauriModelExt.ts | 51 +- src-gui/src/renderer/background.ts | 44 +- src-gui/src/renderer/components/App.tsx | 102 +- .../components/PromiseInvokeButton.tsx | 4 +- .../alert/FundsLeftInWalletAlert.tsx | 30 - .../alert/SwapStatusAlert/SwapStatusAlert.tsx | 47 +- .../SwapStatusAlert/TimelockTimeline.tsx | 9 +- .../renderer/components/icons/IdentIcon.tsx | 30 - .../components/modal/PaperTextBox.tsx | 27 - .../slides/Slide01_GettingStarted.tsx | 3 +- .../slides/Slide02_ChooseAMaker.tsx | 3 +- .../slides/Slide03_PrepareSwap.tsx | 3 +- .../slides/Slide04_ExecuteSwap.tsx | 3 +- .../slides/Slide05_KeepAnEyeOnYourSwaps.tsx | 5 +- .../slides/Slide06_FiatPricePreference.tsx | 12 +- .../introduction/slides/Slide07_ReachOut.tsx | 3 +- .../modal/introduction/slides/SlideTypes.ts | 2 +- .../components/modal/provider/MakerInfo.tsx | 115 -- .../modal/provider/MakerListDialog.tsx | 85 - .../components/modal/provider/MakerSelect.tsx | 38 - .../modal/provider/MakerSubmitDialog.tsx | 111 -- .../modal/swap/SwapStateStepper.tsx | 1 - .../modal/swap/pages/FeedbackSubmitBadge.tsx | 22 - .../modal/updater/UpdaterDialog.tsx | 28 +- .../modal/wallet/WithdrawDialog.tsx | 4 +- .../modal/wallet/WithdrawDialogContent.tsx | 1 + .../modal/wallet/pages/AddressInputPage.tsx | 3 +- .../navigation/NavigationFooter.tsx | 2 - .../other/ActionableMonospaceTextBox.tsx | 66 +- .../components/other/ErrorBoundary.tsx | 58 + .../components/other/JSONViewTree.tsx | 51 - .../components/other/LoadingButton.tsx | 26 - .../components/other/MonospaceTextBox.tsx | 15 +- .../other/ScrollablePaperTextBox.tsx | 23 +- .../components/other/TruncatedText.tsx | 2 +- .../src/renderer/components/other/Units.tsx | 8 +- .../pages/help/ConversationsBox.tsx | 5 - .../components/pages/help/ExportDataBox.tsx | 126 -- .../components/pages/help/SettingsBox.tsx | 11 +- .../components/pages/help/SettingsPage.tsx | 3 - .../table/SwapMoneroRecoveryButton.tsx | 6 +- .../monero/components/ConfirmationsBadge.tsx | 2 +- .../monero/components/SendAmountInput.tsx | 26 +- .../monero/components/SendSuccessContent.tsx | 16 +- .../components/SendTransactionContent.tsx | 6 +- .../monero/components/StateIndicator.tsx | 4 +- .../monero/components/TransactionHistory.tsx | 4 +- .../monero/components/WalletOverview.tsx | 27 +- .../components/WalletPageLoadingState.tsx | 4 +- .../components/pages/swap/swap/SwapWidget.tsx | 18 +- .../pages/swap/swap/components/InfoBox.tsx | 6 +- .../swap/in_progress/BitcoinRedeemedPage.tsx | 5 - .../DepositAndChooseOfferPage.tsx | 4 +- .../MakerOfferItem.tsx | 7 +- .../components/pages/wallet/WalletPage.tsx | 29 +- .../pages/wallet/WithdrawWidget.tsx | 51 - .../wallet/components/WalletActionButtons.tsx | 37 + .../components/WalletDescriptorButton.tsx | 108 ++ .../wallet/components/WalletOverview.tsx | 80 + src-gui/src/renderer/components/theme.tsx | 12 +- src-gui/src/renderer/rpc.ts | 363 ++-- src-gui/src/store/combinedReducer.ts | 2 + src-gui/src/store/config.ts | 3 +- src-gui/src/store/defaults.ts | 62 + .../src/store/features/bitcoinWalletSlice.ts | 32 + src-gui/src/store/features/makersSlice.ts | 43 - src-gui/src/store/features/nodesSlice.ts | 2 +- src-gui/src/store/features/rpcSlice.ts | 19 +- src-gui/src/store/features/settingsSlice.ts | 120 +- src-gui/src/store/features/walletSlice.ts | 4 +- src-gui/src/store/hooks.ts | 20 +- src-gui/src/store/middleware/storeListener.ts | 25 +- src-gui/src/store/selectors.ts | 49 + src-gui/src/store/types.ts | 9 + src-gui/src/utils/cryptoUtils.ts | 5 - src-gui/src/utils/event.ts | 21 - src-gui/src/utils/logger.ts | 13 +- src-gui/src/utils/multiAddrUtils.ts | 2 +- src-gui/src/utils/sortUtils.ts | 45 +- src-gui/tsconfig.json | 2 +- src-gui/yarn.lock | 376 ++-- src-tauri/Cargo.toml | 6 +- src-tauri/src/commands.rs | 13 +- src-tauri/tauri.conf.json | 4 +- swap-asb/Cargo.toml | 2 +- swap-asb/src/command.rs | 2 +- swap-asb/src/main.rs | 6 +- swap-controller-api/src/lib.rs | 28 + swap-controller/Cargo.toml | 2 +- swap-controller/src/cli.rs | 2 + swap-controller/src/main.rs | 14 + swap-core/src/bitcoin.rs | 3 +- swap-core/src/bitcoin/cancel.rs | 113 +- swap-core/src/bitcoin/refund.rs | 4 +- swap-core/src/bitcoin/timelocks.rs | 102 ++ swap-core/src/monero/primitives.rs | 3 +- swap-db/Cargo.toml | 20 + {swap/src/database => swap-db/src}/alice.rs | 35 +- {swap/src/database => swap-db/src}/bob.rs | 7 +- swap-db/src/lib.rs | 2 + swap-env/src/defaults.rs | 50 +- swap-env/src/env.rs | 20 +- swap-env/src/prompt.rs | 2 +- swap-machine/src/alice/mod.rs | 40 +- swap-orchestrator/README.md | 8 +- swap-orchestrator/src/compose.rs | 435 +++++ swap-p2p/Cargo.toml | 50 + swap-p2p/src/futures_util.rs | 71 + .../src}/impl_from_rr_event.rs | 0 swap-p2p/src/lib.rs | 7 + swap-p2p/src/out_event.rs | 2 + swap-p2p/src/out_event/alice.rs | 103 ++ swap-p2p/src/out_event/bob.rs | 100 + swap-p2p/src/protocols.rs | 7 + .../cooperative_xmr_redeem_after_punish.rs | 12 +- .../src/protocols}/encrypted_signature.rs | 11 +- .../src/protocols}/quote.rs | 10 +- swap-p2p/src/protocols/redial.rs | 264 +++ swap-p2p/src/protocols/rendezvous.rs | 637 +++++++ .../src/protocols}/swap_setup.rs | 3 +- .../src/protocols}/swap_setup/alice.rs | 70 +- swap-p2p/src/protocols/swap_setup/bob.rs | 668 +++++++ .../protocols}/swap_setup/vendor_from_fn.rs | 28 - .../src/protocols}/transfer_proof.rs | 17 +- {swap/src/network => swap-p2p/src}/test.rs | 3 + swap/Cargo.toml | 98 +- swap/src/asb.rs | 5 +- swap/src/asb/event_loop.rs | 110 +- swap/src/asb/network.rs | 668 +------ swap/src/asb/recovery/cancel.rs | 2 + swap/src/asb/recovery/punish.rs | 1 + swap/src/asb/recovery/redeem.rs | 1 + swap/src/asb/recovery/refund.rs | 1 + swap/src/asb/recovery/safely_abort.rs | 1 + swap/src/asb/rpc/server.rs | 43 +- swap/src/bitcoin/wallet.rs | 2 +- swap/src/cli.rs | 2 +- swap/src/cli/api.rs | 24 +- swap/src/cli/api/request.rs | 95 +- swap/src/cli/behaviour.rs | 102 +- swap/src/cli/command.rs | 18 +- swap/src/cli/event_loop.rs | 729 +++++--- swap/src/common/mod.rs | 58 +- swap/src/common/tor.rs | 8 + swap/src/common/tracing_util.rs | 276 ++- swap/src/database.rs | 5 +- swap/src/network.rs | 17 +- swap/src/network/redial.rs | 143 -- swap/src/network/rendezvous.rs | 39 - swap/src/network/swap_setup/bob.rs | 383 ---- swap/src/network/swarm.rs | 10 +- swap/src/network/transport.rs | 4 +- swap/src/protocol/alice/swap.rs | 226 ++- swap/src/protocol/bob.rs | 6 +- swap/src/protocol/bob/swap.rs | 231 ++- swap/tests/happy_path_alice_developer_tip.rs | 2 +- ...ppy_path_alice_developer_tip_subaddress.rs | 31 + swap/tests/harness/mod.rs | 49 +- throttle/src/throttle.rs | 12 +- utils/gpg_keys/einliterflasche.asc | 14 + 237 files changed, 8048 insertions(+), 27420 deletions(-) create mode 100644 .helix/ignore rename {dev_scripts => dev-scripts}/brew_dependencies_install.sh (100%) rename {dev_scripts => dev-scripts}/bump-version.sh (100%) rename {dev_scripts => dev-scripts}/code2prompt_as_file_mac_os.sh (100%) rename {dev_scripts => dev-scripts}/code2prompt_to_clipboard.sh (100%) create mode 100644 dev-scripts/health_check_default_electrum_servers.py create mode 100644 dev-scripts/homebrew/eigenwallet.rb.template rename {dev_scripts => dev-scripts}/publish_flatpak.sh (63%) rename regenerate_sqlx_cache.sh => dev-scripts/regenerate_sqlx_cache.sh (97%) rename {dev_scripts => dev-scripts}/ubuntu_build_x86_86-w64-mingw32-gcc.sh (98%) create mode 100644 flatpak/eigenwallet.flatpakrepo delete mode 100644 flatpak/net.unstoppableswap.gui.json create mode 100644 flatpak/org.eigenwallet.app.appdata.xml create mode 100644 flatpak/org.eigenwallet.app.flatpakref rename {libp2p-rendezvous-server => libp2p-rendezvous-node}/Cargo.toml (75%) rename {libp2p-rendezvous-server => libp2p-rendezvous-node}/Dockerfile (76%) rename {libp2p-rendezvous-server => libp2p-rendezvous-node}/README.md (97%) create mode 100644 libp2p-rendezvous-node/src/behaviour.rs create mode 100644 libp2p-rendezvous-node/src/main.rs rename {libp2p-rendezvous-server => libp2p-rendezvous-node}/src/swarm.rs (77%) create mode 100644 libp2p-rendezvous-node/src/tor.rs create mode 100644 libp2p-rendezvous-node/src/tracing_util.rs delete mode 100644 libp2p-rendezvous-server/src/main.rs delete mode 100644 monero-seed/Cargo.toml delete mode 100644 monero-seed/LICENSE delete mode 100644 monero-seed/README.md delete mode 100644 monero-seed/src/lib.rs delete mode 100644 monero-seed/src/tests.rs delete mode 100644 monero-seed/src/words/ang.rs delete mode 100644 monero-seed/src/words/de.rs delete mode 100644 monero-seed/src/words/en.rs delete mode 100644 monero-seed/src/words/eo.rs delete mode 100644 monero-seed/src/words/es.rs delete mode 100644 monero-seed/src/words/fr.rs delete mode 100644 monero-seed/src/words/it.rs delete mode 100644 monero-seed/src/words/ja.rs delete mode 100644 monero-seed/src/words/jbo.rs delete mode 100644 monero-seed/src/words/nl.rs delete mode 100644 monero-seed/src/words/pt.rs delete mode 100644 monero-seed/src/words/ru.rs delete mode 100644 monero-seed/src/words/zh.rs delete mode 100644 monero-sys/patches/eigenwallet_0003_pendingTransaction_getTxKey.patch create mode 100644 monero-sys/patches/eigenwallet_0003_pending_transaction_tx_keys.patch create mode 100644 monero-sys/tests/transaction_keys_testnet.rs create mode 100644 monero-tests/Cargo.toml create mode 100644 monero-tests/src/lib.rs create mode 100644 monero-tests/tests/transaction_keys.rs create mode 100644 monero-tests/tests/transfers.rs create mode 100644 monero-tests/tests/transfers_wrong_key.rs create mode 100644 src-gui/knip.json delete mode 100644 src-gui/src/renderer/components/alert/FundsLeftInWalletAlert.tsx delete mode 100644 src-gui/src/renderer/components/icons/IdentIcon.tsx delete mode 100644 src-gui/src/renderer/components/modal/PaperTextBox.tsx delete mode 100644 src-gui/src/renderer/components/modal/provider/MakerInfo.tsx delete mode 100644 src-gui/src/renderer/components/modal/provider/MakerListDialog.tsx delete mode 100644 src-gui/src/renderer/components/modal/provider/MakerSelect.tsx delete mode 100644 src-gui/src/renderer/components/modal/provider/MakerSubmitDialog.tsx delete mode 100644 src-gui/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx create mode 100644 src-gui/src/renderer/components/other/ErrorBoundary.tsx delete mode 100644 src-gui/src/renderer/components/other/JSONViewTree.tsx delete mode 100644 src-gui/src/renderer/components/other/LoadingButton.tsx delete mode 100644 src-gui/src/renderer/components/pages/help/ExportDataBox.tsx delete mode 100644 src-gui/src/renderer/components/pages/swap/swap/in_progress/BitcoinRedeemedPage.tsx delete mode 100644 src-gui/src/renderer/components/pages/wallet/WithdrawWidget.tsx create mode 100644 src-gui/src/renderer/components/pages/wallet/components/WalletActionButtons.tsx create mode 100644 src-gui/src/renderer/components/pages/wallet/components/WalletDescriptorButton.tsx create mode 100644 src-gui/src/renderer/components/pages/wallet/components/WalletOverview.tsx create mode 100644 src-gui/src/store/defaults.ts create mode 100644 src-gui/src/store/features/bitcoinWalletSlice.ts create mode 100644 src-gui/src/store/selectors.ts create mode 100644 src-gui/src/store/types.ts delete mode 100644 src-gui/src/utils/cryptoUtils.ts delete mode 100644 src-gui/src/utils/event.ts create mode 100644 swap-db/Cargo.toml rename {swap/src/database => swap-db/src}/alice.rs (91%) rename {swap/src/database => swap-db/src}/bob.rs (98%) create mode 100644 swap-db/src/lib.rs create mode 100644 swap-orchestrator/src/compose.rs create mode 100644 swap-p2p/Cargo.toml create mode 100644 swap-p2p/src/futures_util.rs rename {swap/src/network => swap-p2p/src}/impl_from_rr_event.rs (100%) create mode 100644 swap-p2p/src/lib.rs create mode 100644 swap-p2p/src/out_event.rs create mode 100644 swap-p2p/src/out_event/alice.rs create mode 100644 swap-p2p/src/out_event/bob.rs create mode 100644 swap-p2p/src/protocols.rs rename {swap/src/network => swap-p2p/src/protocols}/cooperative_xmr_redeem_after_punish.rs (90%) rename {swap/src/network => swap-p2p/src/protocols}/encrypted_signature.rs (87%) rename {swap/src/network => swap-p2p/src/protocols}/quote.rs (91%) create mode 100644 swap-p2p/src/protocols/redial.rs create mode 100644 swap-p2p/src/protocols/rendezvous.rs rename {swap/src/network => swap-p2p/src/protocols}/swap_setup.rs (98%) rename {swap/src/network => swap-p2p/src/protocols}/swap_setup/alice.rs (91%) create mode 100644 swap-p2p/src/protocols/swap_setup/bob.rs rename {swap/src/network => swap-p2p/src/protocols}/swap_setup/vendor_from_fn.rs (71%) rename {swap/src/network => swap-p2p/src/protocols}/transfer_proof.rs (84%) rename {swap/src/network => swap-p2p/src}/test.rs (97%) delete mode 100644 swap/src/network/redial.rs delete mode 100644 swap/src/network/rendezvous.rs delete mode 100644 swap/src/network/swap_setup/bob.rs create mode 100644 swap/tests/happy_path_alice_developer_tip_subaddress.rs create mode 100644 utils/gpg_keys/einliterflasche.asc diff --git a/.cargo/config.toml b/.cargo/config.toml index b8e076e9d9..ed4a212434 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ +[build] +target-dir = "target" + # Cross-compilation support for armv7 [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" diff --git a/.dockerignore b/.dockerignore index 11174d447b..95bcfdd1dc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,13 +19,13 @@ docs/_build/ node_modules/ # Development scripts -dev_scripts/ +dev-scripts/ dev-docs/ # Tauri development files src-tauri/target/ -# GUI development files +# GUI development files src-gui/node_modules/ src-gui/dist/ src-gui/.next/ @@ -38,4 +38,4 @@ src-gui/.next/ *.temp # OS files -Thumbs.db \ No newline at end of file +Thumbs.db diff --git a/.github/actions/set-monero-env/action.yml b/.github/actions/set-monero-env/action.yml index 4207d8d059..b1d6735bf7 100644 --- a/.github/actions/set-monero-env/action.yml +++ b/.github/actions/set-monero-env/action.yml @@ -8,7 +8,8 @@ runs: run: | # GUI-specific Ubuntu dependencies - echo "DEPS_GUI_UBUNTU_SPECIFIC=libgtk-3-dev libappindicator3-dev librsvg2-dev libwebkit2gtk-4.1-0 libwebkit2gtk-4.1-dev libjavascriptcoregtk-4.1-0 libjavascriptcoregtk-4.1-dev gir1.2-javascriptcoregtk-4.1 gir1.2-webkit2-4.1" >> $GITHUB_ENV + grep -q noble /etc/os-release && pin==2.44.0-2 || pin= + echo "DEPS_GUI_UBUNTU_SPECIFIC=flatpak-builder jq libgtk-3-dev libappindicator3-dev librsvg2-dev" {libwebkit2gtk-4.1-0,libwebkit2gtk-4.1-dev,libjavascriptcoregtk-4.1-0,libjavascriptcoregtk-4.1-dev,gir1.2-javascriptcoregtk-4.1,gir1.2-webkit2-4.1}"$pin" >> $GITHUB_ENV # Build tooling (Linux) echo "DEPS_BUILD_LINUX=autoconf nsis mingw-w64 build-essential pkg-config libtool ccache make cmake gcc g++ git curl lbzip2 gperf g++-mingw-w64-x86-64" >> $GITHUB_ENV @@ -28,4 +29,4 @@ runs: - name: Crabnebula identifier shell: bash run: | - echo "CN_APPLICATION=unstoppableswap/unstoppableswap-gui-rs" >> $GITHUB_ENV \ No newline at end of file + echo "CN_APPLICATION=unstoppableswap/unstoppableswap-gui-rs" >> $GITHUB_ENV diff --git a/.github/actions/setup-build-environment/action.yml b/.github/actions/setup-build-environment/action.yml index e45bc0648c..38f16755d7 100644 --- a/.github/actions/setup-build-environment/action.yml +++ b/.github/actions/setup-build-environment/action.yml @@ -32,7 +32,7 @@ runs: npm install --global yarn@1 - name: Install Rust stable - uses: dtolnay/rust-toolchain@1.87 + uses: dtolnay/rust-toolchain@1.88 with: targets: ${{ inputs.target }} diff --git a/.github/workflows/build-gui-release-binaries.yml b/.github/workflows/build-gui-release-binaries.yml index 803b63253e..84b91b0427 100644 --- a/.github/workflows/build-gui-release-binaries.yml +++ b/.github/workflows/build-gui-release-binaries.yml @@ -101,8 +101,7 @@ jobs: exit 1 fi - export GNUPGHOME="$(mktemp -d)" - chmod 700 "$GNUPGHOME" + export GNUPGHOME="$(umask 077; mktemp -d)" # Allow loopback pinentry when passphrase is provided echo "allow-loopback-pinentry" >> "$GNUPGHOME/gpg-agent.conf" @@ -152,15 +151,50 @@ jobs: # Find all .asc signature files we just created find target/${{ matrix.target }}/release/bundle -type f -name "*.asc" -print0 | while IFS= read -r -d '' sig_file; do echo "Uploading signature: $sig_file" - # Get just the filename for the asset name - asset_name=$(basename "$sig_file") - gh release upload "${{ github.event.release.tag_name }}" \ "$sig_file" \ --clobber done - - name: upload to crabnebula release (not for previews) + - name: Upload flatpak release + if: ${{ github.event_name != 'pull_request' && !contains(github.ref_name, 'preview') && matrix.target == 'x86_64-unknown-linux-gnu' }} + shell: bash + run: | + set -euxo pipefail + + deb=$(find "$PWD" -name *.deb -print -quit) + jq --arg deb_path "$deb" --arg PWD "$PWD" ' + .modules[0].sources[0] = { + "type": "file", + "path": $deb_path + } | + .modules[0].sources[1].path = $PWD + "/" + .modules[0].sources[1].path + ' < flatpak/org.eigenwallet.app.json > target/manifest.json + + outdir=target/flatpak-repo + flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak-builder build-dir --gpg-sign="$GPG_FPR" --user --install-deps-from=flathub --disable-rofiles-fuse --disable-updates --repo="$outdir" target/manifest.json + flatpak build-update-repo --gpg-sign="$GPG_FPR" --generate-static-deltas --prune "$outdir" + flatpak build-bundle --gpg-sign="$GPG_FPR" "$outdir" "$outdir/org.eigenwallet.app.flatpak" org.eigenwallet.app stable + + ln flatpak/index.html flatpak/*.flatpakre* src-tauri/icons/icon.png README.md "$outdir/" + > "$outdir/.nojekyll" + + IFS=/ read -r user repo <<< "$GITHUB_REPOSITORY" + sed -e "s|%Url%|https://$user.github.io/$repo|" \ + -e "s|%Homepage%|https://github.com/$GITHUB_REPOSITORY|" \ + -e "s|%GPGKey%|$(gpg --export "$GPG_FPR" | base64 -w0)|" \ + -i "$outdir"/*.flatpakre* + + git -C "$outdir" init + git -C "$outdir" add . + git -C "$outdir" config user.name "${{ secrets.BOTTY_NAME }}" + git -C "$outdir" config user.email ${{ secrets.BOTTY_EMAIL }} + git -C "$outdir" commit -m "Build Flatpak repository from $GITHUB_REF_NAME ($GITHUB_SHA)" + git fetch -f "$outdir" HEAD:gh-pages + git push -f origin gh-pages + + - name: Upload to crabnebula release (not for previews) if: ${{ github.event_name != 'pull_request' && !contains(github.ref_name, 'preview') }} uses: crabnebula-dev/cloud-release@v0 with: @@ -168,6 +202,47 @@ jobs: api-key: ${{ secrets.CN_API_KEY }} args: --target ${{ matrix.target }} + generate-homebrew-formula: + needs: [build_gui] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Download DMG files and generate formula + env: + GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }} + run: | + set -euo pipefail + + VERSION="${{ github.event.release.tag_name }}" + + # Download the DMG files + gh release download "$VERSION" --pattern "*.dmg" --dir /tmp/dmgs + + # Calculate SHA256 checksums + AARCH64_DMG=/tmp/dmgs/*_aarch64_darwin.dmg + X64_DMG=/tmp/dmgs/*_x64_darwin.dmg + + AARCH64_SHA256=$(sha256sum "$AARCH64_DMG" | awk '{print $1}') + X64_SHA256=$(sha256sum "$X64_DMG" | awk '{print $1}') + + echo "aarch64 SHA256: $AARCH64_SHA256" + echo "x64 SHA256: $X64_SHA256" + + # Generate the Homebrew formula from template + sed -e "s/VERSION_PLACEHOLDER/$VERSION/g" \ + -e "s/AARCH64_SHA256_PLACEHOLDER/$AARCH64_SHA256/g" \ + -e "s/X64_SHA256_PLACEHOLDER/$X64_SHA256/g" \ + dev-scripts/homebrew/eigenwallet.rb.template | tee eigenwallet.rb + + - name: Upload Homebrew formula to release + env: + GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }} + run: | + gh release upload "${{ github.event.release.tag_name }}" \ + eigenwallet.rb \ + --clobber + publish: # don't publish previews to crabnebula if: ${{ github.event_name != 'pull_request' && !contains(github.ref_name, 'preview') }} diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index bb54b1de4f..4d13110f20 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -1,4 +1,4 @@ -name: "Build swap, asb, asb-controller, orchestrator and rendezvous-server release binaries" +name: "Build swap, asb, asb-controller, orchestrator and rendezvous-node release binaries" on: pull_request: @@ -52,7 +52,7 @@ jobs: - name: asb-controller smoke_test_args: "" smoke_test_fake_interactive: false - - name: rendezvous-server + - name: rendezvous-node smoke_test_args: "--help" smoke_test_fake_interactive: false - name: orchestrator @@ -109,7 +109,7 @@ jobs: target_os = triple[2].lower() os_mapping = {"linux": "Linux", "windows": "Windows", "darwin": "Darwin"} - + if target_os not in os_mapping: raise ValueError(f"Unknown target OS: {target_os}") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99e73d3c1c..63a82e9abd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,6 +111,8 @@ jobs: test_name: happy_path - package: swap test_name: happy_path_alice_developer_tip + - package: swap + test_name: happy_path_alice_developer_tip_subaddress - package: swap test_name: happy_path_restart_bob_after_xmr_locked - package: swap @@ -149,6 +151,10 @@ jobs: test_name: alice_broken_wallet_rpc_after_started_btc_early_refund - package: swap-orchestrator test_name: orchestrator_produces_expected_compose_config + - package: monero-tests + test_name: transfers + - package: monero-tests + test_name: transfers_wrong_key runs-on: ubuntu-22.04 if: github.event_name == 'push' || !github.event.pull_request.draft diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 157f2b2750..0524fcfe2d 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -34,22 +34,39 @@ jobs: git config user.name "${{ secrets.BOTTY_NAME }}" git config user.email ${{ secrets.BOTTY_EMAIL }} - - name: Bump version in Cargo.toml + - name: Bump version in swap/Cargo.toml uses: thomaseizinger/set-crate-version@1.0.0 with: version: ${{ github.event.inputs.version }} manifest: swap/Cargo.toml - - name: Bump version in Cargo.toml for GUI + - name: Bump version in src-tauri/Cargo.toml uses: thomaseizinger/set-crate-version@1.0.0 with: version: ${{ github.event.inputs.version }} manifest: src-tauri/Cargo.toml + - name: Bump version in swap-asb/Cargo.toml + uses: thomaseizinger/set-crate-version@1.0.0 + with: + version: ${{ github.event.inputs.version }} + manifest: swap-asb/Cargo.toml + + - name: Bump version in swap-controller/Cargo.toml + uses: thomaseizinger/set-crate-version@1.0.0 + with: + version: ${{ github.event.inputs.version }} + manifest: swap-controller/Cargo.toml + - name: Update version in tauri.conf.json for GUI run: | sed -i 's/"version": "[^"]*"/"version": "${{ github.event.inputs.version }}"/' src-tauri/tauri.conf.json + - name: Update version in flatpak AppStream metadata for GUI + run: | + sed -i '//a\ + ' flatpak/*.appdata.xml + - name: Update Cargo.lock run: cargo update --workspace @@ -57,13 +74,13 @@ jobs: id: make-commit env: DPRINT_VERSION: "0.50.0" - RUST_TOOLCHAIN: "1.87.0" + RUST_TOOLCHAIN: "1.88.0" run: | rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu" curl -fsSL https://dprint.dev/install.sh | sh -s $DPRINT_VERSION /home/runner/.dprint/bin/dprint fmt - git add CHANGELOG.md Cargo.lock swap/Cargo.toml src-tauri/Cargo.toml src-tauri/tauri.conf.json + git add CHANGELOG.md Cargo.lock swap/Cargo.toml src-tauri/Cargo.toml swap-asb/Cargo.toml swap-controller/Cargo.toml src-tauri/tauri.conf.json git commit --message "Prepare release ${{ github.event.inputs.version }}" echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT diff --git a/.gitignore b/.gitignore index 611e58f769..dd2013c042 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ swap-orchestrator/config.toml # release build generator scripts release-build.sh -cn_macos \ No newline at end of file +cn_macos +libp2p-rendezvous-node/rendezvous-data diff --git a/.helix/ignore b/.helix/ignore new file mode 100644 index 0000000000..47f73afa1a --- /dev/null +++ b/.helix/ignore @@ -0,0 +1,2 @@ +src-tauri/gen/ + diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ee485c360..104cee660d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -73,9 +73,17 @@ "shared_mutex": "cpp", "source_location": "cpp", "strstream": "cpp", - "typeindex": "cpp" + "typeindex": "cpp", + "valarray": "cpp", + "*.ipp": "cpp" }, "rust-analyzer.cargo.extraEnv": { "CARGO_TARGET_DIR": "target-check" - } -} + }, + "[cpp]": { + "editor.formatOnSave": false + }, + "[c]": { + "editor.formatOnSave": false + }, +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e6fba7dfdf..5b0f577429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.3.1] - 2025-11-11 + +- GUI: Fix the Flatpak images to ensure they are kept up to date and the correct version is displayed. Also fixes an issue where a blank screen would sometimes be rendered. Big thanks to [nabijaczleweli](https://github.com/nabijaczleweli) for spending their time on this! Consider sending a Monero tip to the donation address pinned on their [Github profile](https://github.com/nabijaczleweli). + +## [3.3.0] - 2025-11-10 + +- GUI + SWAP: Retry sending the encrypted signature more aggressively. This might help with an issue where we would be stuck on the "Sending encrypted signature" screen for a longer time than necessary. +- GUI + SWAP + ASB: Require 10 Monero confirmations again + +## [3.2.11] - 2025-11-09 + +- GUI + SWAP: Assume double spend safety of Monero transactions after 6 confirmations. This means we are assuming that there won't be any re-orgs deeper than 5 blocks. We believe this is a safe assumption given that there were almost no orphaned blocks over the last two weeks. Qubic (which was behind the re-orgs) has mined less than 1% of the last 1000 blocks. +- GUI: Remove the following default Electrum servers: `tcp://electrum.blockstream.info:50001`, `tcp://electrum.coinucopia.io:50001`, `tcp://se-mma-crypto-payments-001.mullvad.net:50001`, `tcp://electrum2.bluewallet.io:50777` due to them being unreliable. Add the following new default Electrum servers: `tcp://electrum1.bluewallet.io:50001`, `tcp://electrum2.bluewallet.io:50001`, `tcp://electrum3.bluewallet.io:50001`, `ssl://btc-electrum.cakewallet.com:50002`, `tcp://bitcoin.aranguren.org:50001`. + +## [3.2.10] - 2025-11-08 + +- GUI + SWAP + ASB: Reduce the confirmations required to spend a Monero transaction from 22 to 15. We believe the risks of re-orgs is low again and this is safe to do. This may increase the chances of swap being successful and will reduce the time a swap takes. +- GUI: Fix an issue where we a manual resume of a swap would be necessary if we failed to fetch certain Bitcoin transactions due to network issues. +- + +## [3.2.9] - 2025-11-05 + +- GUI: Fix an issue where an error in the UI runtime would cause a white screen to be displayed and nothing would be rendered. +- GUI(Linux): Fix an issue where the GUI would display a white screen on some systems (among others Fedora 43) + +## [3.2.8] - 2025-11-02 + +- ASB + CONTROLLER: Add a `registration-status` command to the controller shell. You can use it to get the registration status of the ASB at the configured rendezvous points. +- ASB + GUI + CLI + SWAP: Split high-verbosity tracing into separate hourly-rotating JSON log files per subsystem to reduce noise and aid debugging: `tracing*.log` (core things), `tracing-tor*.log` (purely tor related), `tracing-libp2p*.log` (low level networking), `tracing-monero-wallet*.log` (low level Monero wallet related). `swap-all.log` remains for non-verbose logs. +- ASB: Fix an issue where we would not redeem the Bitcoin and force a refund even though it was still possible to do so. +- GUI: Potentially fix issue here swaps would not be displayed + +## [3.2.7] - 2025-10-28 + +## [3.2.6] - 2025-10-27 + +## [3.2.5] - 2025-10-26 + +- ASB: Fixed an issue where we would be forced to refund a swap if Bobs acknowledgement of the transfer proof did not reach us. +- RENDEZVOUS-NODE: Fix an issue where the `--data-dir` argument was not accepted + +## [3.2.4] - 2025-10-26 + +## [3.2.3] - 2025-10-26 + +- RENDEZVOUS-NODE: Fix a spelling mistake in the Dockerfile + +## [3.2.2] - 2025-10-25 + +- RENDEZVOUS-NODE: Now takes a `--data-dir` argument and has been renamed to "rendezvous-node" (previously "rendezvous-server") +- RENDEZVOUS-NODE: Rendezvous servers now register themselves at bootstrap rendezvous points to make them discoverable. +- ORCHESTRATOR: The orchestrator will now also add a `rendezvous-node` service to the `docker-compose.yml` file. Rendezvous nodes help with peer discovery in the network. +- ASB + GUI + CLI: Upgrade arti-client to 1.6.0 + +## [3.2.1] - 2025-10-21 + +- ASB + GUI + CLI: Fix an issue where the internal Tor client would fail to choose guards. This would prevent all Tor traffic from working. We temporarily fix this by forcing new guards to be chosen on every startup. This will be reverted once the issue is fixed [upstream](https://gitlab.torproject.org/tpo/core/arti/-/issues/2079) +- CLI: Remove the `--debug` flag + +## [3.2.0-rc.4] - 2025-10-17 + +- ASB + CLI + GUI: Reduce redial interval to 30s; set idle connection timeout to 2h; increase auth and multiplex timeout to 60s +- ASB: Explicitly retry publishing the Bitcoin punish transaction +- GUI + ASB: Adress to `4A1tNBcsxhQA7NkswREXTD1QGz8mRyA7fGnCzPyTwqzKdDFMNje7iHUbGhCetfVUZa1PTuZCoPKj8gnJuRrFYJ2R2CEzqbJ`. This was done because the previous donation address was a subaddress which complicates transaction building. + +## [3.2.0-rc.2] - 2025-10-14 + +- ASB: Fix an issue where the compiled binary would not know its own version + ## [3.2.0-rc.1] - 2025-10-14 - ASB: Fixed a rare race condition where it would be possible for the Monero lock step to fail but the funds to still be transferred. This would require manual intervention to recover. @@ -683,7 +752,23 @@ It is possible to migrate critical data from the old db to the sqlite but there - Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them. Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version. -[unreleased]: https://github.com/eigenwallet/core/compare/3.2.0-rc.1...HEAD +[unreleased]: https://github.com/eigenwallet/core/compare/3.3.1...HEAD +[3.3.1]: https://github.com/eigenwallet/core/compare/3.3.0...3.3.1 +[3.3.0]: https://github.com/eigenwallet/core/compare/3.2.11...3.3.0 +[3.2.11]: https://github.com/eigenwallet/core/compare/3.2.10...3.2.11 +[3.2.10]: https://github.com/eigenwallet/core/compare/3.2.9...3.2.10 +[3.2.9]: https://github.com/eigenwallet/core/compare/3.2.8...3.2.9 +[3.2.8]: https://github.com/eigenwallet/core/compare/3.2.7...3.2.8 +[3.2.7]: https://github.com/eigenwallet/core/compare/3.2.6...3.2.7 +[3.2.6]: https://github.com/eigenwallet/core/compare/3.2.5...3.2.6 +[3.2.5]: https://github.com/eigenwallet/core/compare/3.2.4...3.2.5 +[3.2.4]: https://github.com/eigenwallet/core/compare/3.2.3...3.2.4 +[3.2.3]: https://github.com/eigenwallet/core/compare/3.2.2...3.2.3 +[3.2.2]: https://github.com/eigenwallet/core/compare/3.2.1...3.2.2 +[3.2.1]: https://github.com/eigenwallet/core/compare/3.2.0-rc.4...3.2.1 +[3.2.0-rc.4]: https://github.com/eigenwallet/core/compare/3.0.0-rc.3...3.2.0-rc.4 +[3.0.0-rc.3]: https://github.com/eigenwallet/core/compare/3.2.0-rc.2...3.0.0-rc.3 +[3.2.0-rc.2]: https://github.com/eigenwallet/core/compare/3.2.0-rc.1...3.2.0-rc.2 [3.2.0-rc.1]: https://github.com/eigenwallet/core/compare/3.1.3...3.2.0-rc.1 [3.1.3]: https://github.com/eigenwallet/core/compare/3.1.2...3.1.3 [3.1.2]: https://github.com/eigenwallet/core/compare/3.1.1...3.1.2 diff --git a/Cargo.lock b/Cargo.lock index 8bba28e253..b1c4cb4912 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,15 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] +version = 4 [[package]] name = "adler2" @@ -24,7 +15,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array 0.14.9", + "generic-array 0.14.7", ] [[package]] @@ -78,9 +69,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -116,7 +107,7 @@ dependencies = [ "amplify_num", "ascii", "getrandom 0.2.16", - "getrandom 0.3.3", + "getrandom 0.3.4", "wasm-bindgen", ] @@ -276,12 +267,12 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arti-client" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "async-trait", "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "derive_more 2.0.1", "educe", @@ -304,6 +295,7 @@ dependencies = [ "tor-circmgr", "tor-config", "tor-config-path", + "tor-dircommon", "tor-dirmgr", "tor-error", "tor-guardmgr", @@ -418,7 +410,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure 0.13.2", ] @@ -430,7 +422,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure 0.13.2", ] @@ -453,7 +445,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -519,9 +511,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2" dependencies = [ "compression-codecs", "compression-core", @@ -599,7 +591,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -639,7 +631,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -656,7 +648,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -793,16 +785,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b715a6010afb9e457ca2b7c9d2b9c344baa8baed7b38dc476034c171b32575" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", - "libloading 0.8.8", ] [[package]] @@ -819,7 +810,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.0", "hyper-util", "itoa", "matchit", @@ -866,7 +857,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -883,21 +874,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - [[package]] name = "base-x" version = "0.2.11" @@ -1032,7 +1008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b59a3f7fbe678874fa34354097644a171276e02a49934c13b3d61c54610ddf39" dependencies = [ "bdk_core", - "electrum-client 0.24.0", + "electrum-client 0.24.1", ] [[package]] @@ -1110,7 +1086,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1121,7 +1097,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1271,11 +1247,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1307,7 +1283,7 @@ checksum = "e0b121a9fe0df916e362fb3271088d071159cdf11db0e4182d02152850756eff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1316,7 +1292,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.9", + "generic-array 0.14.7", ] [[package]] @@ -1325,7 +1301,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.9", + "generic-array 0.14.7", ] [[package]] @@ -1399,15 +1375,9 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] -[[package]] -name = "bounded-vec-deque" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2225b558afc76c596898f5f1b3fc35cfce0eb1b13635cbd7d1b2a7177dc10ccd" - [[package]] name = "brotli" version = "3.5.0" @@ -1461,9 +1431,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -1543,9 +1513,9 @@ dependencies = [ [[package]] name = "bzip2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" dependencies = [ "libbz2-rs-sys", ] @@ -1566,7 +1536,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cairo-sys-rs", "glib", "libc", @@ -1596,8 +1566,8 @@ dependencies = [ [[package]] name = "caret" -version = "0.6.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.7.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" [[package]] name = "cargo-platform" @@ -1649,9 +1619,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.41" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "jobserver", @@ -1697,9 +1667,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1801,7 +1771,7 @@ dependencies = [ "rand_core 0.6.4", "sha2 0.10.9", "sha3", - "std-shims 0.1.5", + "std-shims", "subtle", "zeroize", ] @@ -1814,7 +1784,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.8", + "libloading 0.8.9", ] [[package]] @@ -1834,9 +1804,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.49" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -1844,9 +1814,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.49" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -1863,7 +1833,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1903,9 +1873,9 @@ dependencies = [ [[package]] name = "codespan-reporting" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", @@ -1955,7 +1925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fd9b9dc67f2b3024582ec6d861950f0af0aeaabb8350ccda1f0e51ff8e5895c" dependencies = [ "compose_spec_macros", - "indexmap 2.11.4", + "indexmap 2.12.0", "ipnet", "itoa", "serde", @@ -1972,14 +1942,14 @@ checksum = "b77735bd89be8da01c8d7e61faec5a9ccb0e313cece3c773c6b3ae251b90c7d4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "compression-codecs" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b" dependencies = [ "compression-core", "flate2", @@ -1987,9 +1957,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" +checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582" [[package]] name = "concurrent-queue" @@ -2137,7 +2107,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -2150,7 +2120,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "libc", ] @@ -2206,7 +2176,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.49", + "clap 4.5.51", "criterion-plot", "itertools 0.13.0", "num-traits", @@ -2302,7 +2272,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crossterm_winapi", "document-features", "parking_lot 0.12.5", @@ -2330,7 +2300,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array 0.14.9", + "generic-array 0.14.7", "rand_core 0.6.4", "serdect", "subtle", @@ -2339,11 +2309,11 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array 0.14.9", + "generic-array 0.14.7", "rand_core 0.6.4", "typenum", ] @@ -2372,7 +2342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2382,7 +2352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2397,7 +2367,7 @@ dependencies = [ [[package]] name = "cuprate-epee-encoding" version = "0.5.0" -source = "git+https://github.com/Cuprate/cuprate.git#fb0ffbe331036fa436f8d21ad89fc8eb7c06629f" +source = "git+https://github.com/Cuprate/cuprate.git#023700ae2ae18d9feac9e1fe123344ab48447cd1" dependencies = [ "bytes", "cuprate-fixed-bytes", @@ -2410,7 +2380,7 @@ dependencies = [ [[package]] name = "cuprate-fixed-bytes" version = "0.1.0" -source = "git+https://github.com/Cuprate/cuprate.git#fb0ffbe331036fa436f8d21ad89fc8eb7c06629f" +source = "git+https://github.com/Cuprate/cuprate.git#023700ae2ae18d9feac9e1fe123344ab48447cd1" dependencies = [ "bytes", "thiserror 2.0.17", @@ -2419,7 +2389,7 @@ dependencies = [ [[package]] name = "cuprate-hex" version = "0.0.0" -source = "git+https://github.com/Cuprate/cuprate.git#fb0ffbe331036fa436f8d21ad89fc8eb7c06629f" +source = "git+https://github.com/Cuprate/cuprate.git#023700ae2ae18d9feac9e1fe123344ab48447cd1" dependencies = [ "hex", "serde", @@ -2465,7 +2435,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2484,9 +2454,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9c4fe7f2f5dc5c62871a1b43992d197da6fa1394656a94276ac2894a90a6fe" +checksum = "47ac4eaf7ebe29e92f1b091ceefec7710a53a6f6154b2460afda626c113b65b9" dependencies = [ "cc", "cxx-build", @@ -2499,50 +2469,49 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5cf2909d37d80633ddd208676fc27c2608a7f035fff69c882421168038b26dd" +checksum = "2abd4c3021eefbac5149f994c117b426852bca3a0aad227698527bca6d4ea657" dependencies = [ "cc", "codespan-reporting", - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro2", "quote", "scratch", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "cxxbridge-cmd" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "077f5ee3d3bfd8d27f83208fdaa96ddd50af7f096c77077cc4b94da10bfacefd" +checksum = "6f12fbc5888b2311f23e52a601e11ad7790d8f0dbb903ec26e2513bf5373ed70" dependencies = [ - "clap 4.5.49", + "clap 4.5.51", "codespan-reporting", - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "cxxbridge-flags" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0108748615125b9f2e915dfafdffcbdabbca9b15102834f6d7e9a768f2f2864" +checksum = "83d3dd7870af06e283f3f8ce0418019c96171c9ce122cfb9c8879de3d84388fd" [[package]] name = "cxxbridge-macro" -version = "1.0.186" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e896681ef9b8dc462cfa6961d61909704bde0984b30bcb4082fe102b478890" +checksum = "a26f0d82da663316786791c3d0e9f9edc7d1ee1f04bdad3d2643086a69d6256c" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro2", "quote", - "rustversion", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2661,7 +2630,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2675,7 +2644,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2708,7 +2677,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2719,7 +2688,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2745,7 +2714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2809,9 +2778,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", "serde_core", @@ -2819,57 +2788,29 @@ dependencies = [ [[package]] name = "derive-deftly" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ea84d0109517cc2253d4a679bdda1e8989e9bd86987e9e4f75ffdda0095fd1" -dependencies = [ - "derive-deftly-macros 0.14.6", - "heck 0.5.0", -] - -[[package]] -name = "derive-deftly" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957bb73a3a9c0bbcac67e129b81954661b3cfcb9e28873d8441f91b54852e77a" -dependencies = [ - "derive-deftly-macros 1.2.0", - "heck 0.5.0", -] - -[[package]] -name = "derive-deftly-macros" -version = "0.14.6" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357422a457ccb850dc8f1c1680e0670079560feaad6c2e247e3f345c4fab8a3f" +checksum = "7d308ebe4b10924331bd079044b418da7b227d724d3e2408567a47ad7c3da2a0" dependencies = [ + "derive-deftly-macros", "heck 0.5.0", - "indexmap 2.11.4", - "itertools 0.14.0", - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "sha3", - "strum 0.27.2", - "syn 2.0.106", - "void", ] [[package]] name = "derive-deftly-macros" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea41269bd490d251b9eca50ccb43117e641cc68b129849757c15ece88fe0574" +checksum = "dd5f2b7218a51c827a11d22d1439b598121fac94bf9b99452e4afffe512d78c9" dependencies = [ "heck 0.5.0", - "indexmap 2.11.4", + "indexmap 2.12.0", "itertools 0.14.0", "proc-macro-crate 3.4.0", "proc-macro2", "quote", "sha3", "strum 0.27.2", - "syn 2.0.106", + "syn 2.0.110", "void", ] @@ -2881,7 +2822,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2902,7 +2843,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2933,7 +2874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2956,7 +2897,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -2977,14 +2918,14 @@ dependencies = [ "convert_case 0.7.1", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "unicode-xid", ] [[package]] name = "dfx-swiss-sdk" version = "1.0.0" -source = "git+https://github.com/eigenwallet/dfx-swiss-rs#0b7d5dc88e7c6481c527fb8fb246e863b415e45f" +source = "git+https://github.com/eigenwallet/dfx-swiss-rs#30de8a1f0046b82f09ee8eb26912ed6161fa6a31" dependencies = [ "dfx-swiss-sdk-raw", "serde", @@ -2997,7 +2938,7 @@ dependencies = [ [[package]] name = "dfx-swiss-sdk-raw" version = "1.0.0" -source = "git+https://github.com/eigenwallet/dfx-swiss-rs#0b7d5dc88e7c6481c527fb8fb246e863b415e45f" +source = "git+https://github.com/eigenwallet/dfx-swiss-rs#30de8a1f0046b82f09ee8eb26912ed6161fa6a31" dependencies = [ "reqwest 0.11.27", "serde", @@ -3035,7 +2976,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.9", + "generic-array 0.14.7", ] [[package]] @@ -3126,7 +3067,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.6.2", "libc", "objc2 0.6.3", @@ -3140,7 +3081,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3149,7 +3090,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.8", + "libloading 0.8.9", ] [[package]] @@ -3172,14 +3113,14 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -3354,15 +3295,15 @@ dependencies = [ [[package]] name = "electrum-client" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7b07e2578a6df0093b101915c79dca0119d7f7810099ad9eef11341d2ae57" +checksum = "a5059f13888a90486e7268bbce59b175f5f76b1c55e5b9c568ceaa42d2b8507c" dependencies = [ "bitcoin 0.32.7", "byteorder", "libc", "log", - "rustls 0.23.32", + "rustls 0.23.35", "serde", "serde_json", "webpki-roots 0.25.4", @@ -3393,7 +3334,7 @@ dependencies = [ "crypto-bigint", "digest 0.10.7", "ff", - "generic-array 0.14.9", + "generic-array 0.14.7", "group", "pkcs8", "rand_core 0.6.4", @@ -3459,7 +3400,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3472,7 +3413,19 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.110", ] [[package]] @@ -3493,7 +3446,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3514,9 +3467,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -3606,7 +3559,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3707,9 +3660,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", @@ -3725,7 +3678,7 @@ dependencies = [ "blake2", "digest 0.10.7", "merlin", - "std-shims 0.1.5", + "std-shims", "subtle", "zeroize", ] @@ -3747,15 +3700,6 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "fns" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d318f82a68feac152dab48e8a6f1eb665a915ea6a0c76e4ad5ed137f80c368" -dependencies = [ - "log", -] - [[package]] name = "fnv" version = "1.0.7" @@ -3801,7 +3745,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -3827,8 +3771,8 @@ dependencies = [ [[package]] name = "fs-mistrust" -version = "0.11.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.12.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "derive_builder_fork_arti", "dirs", @@ -3877,8 +3821,8 @@ dependencies = [ [[package]] name = "fslock-guard" -version = "0.3.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.4.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "fslock-arti-fork", "thiserror 2.0.17", @@ -3992,7 +3936,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -4012,7 +3956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.32", + "rustls 0.23.35", "rustls-pki-types", ] @@ -4173,9 +4117,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "serde", "typenum", @@ -4185,9 +4129,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.3.3" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42bb3faf529935fbba0684910e1a71ecd271d618549d58f430b878619b7f4cf" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" dependencies = [ "rustversion", "typenum", @@ -4201,12 +4145,12 @@ checksum = "ac6c41a39c60ae1fc5bf0e220347ce90fa1e4bb0fcdac65b09bb5f4576bebc84" [[package]] name = "gethostname" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix 1.1.2", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -4235,18 +4179,30 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "ghash" version = "0.5.1" @@ -4257,12 +4213,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "gio" version = "0.18.4" @@ -4301,7 +4251,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "libgit2-sys", "log", @@ -4314,7 +4264,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "futures-channel", "futures-core", "futures-executor", @@ -4342,7 +4292,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -4450,7 +4400,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -4465,7 +4415,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -4484,7 +4434,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -4635,9 +4585,9 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hex-literal" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hex_fmt" @@ -4717,11 +4667,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4858,9 +4808,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" dependencies = [ "atomic-waker", "bytes", @@ -4886,16 +4836,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.7.0", + "hyper 1.8.0", "hyper-util", "log", - "rustls 0.23.32", - "rustls-native-certs 0.8.1", + "rustls 0.23.35", + "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -4924,7 +4874,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.0", "ipnet", "libc", "percent-encoding", @@ -4973,9 +4923,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -4986,9 +4936,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -4999,11 +4949,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -5014,42 +4963,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -5163,9 +5108,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -5188,7 +5133,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "inotify-sys", "libc", ] @@ -5208,7 +5153,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array 0.14.9", + "generic-array 0.14.7", ] [[package]] @@ -5229,17 +5174,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipconfig" version = "0.3.2" @@ -5272,9 +5206,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -5301,9 +5235,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -5380,15 +5314,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -5497,12 +5431,12 @@ checksum = "6962d2bd295f75e97dd328891e58fce166894b974c1f7ce2e7597f02eeceb791" dependencies = [ "base64 0.22.1", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.0", "hyper-rustls", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", - "rustls 0.23.32", + "rustls 0.23.35", "rustls-platform-verifier", "serde", "serde_json", @@ -5522,7 +5456,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -5535,7 +5469,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.0", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -5611,7 +5545,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "serde", "unicode-segmentation", ] @@ -5644,7 +5578,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 2.11.4", + "indexmap 2.12.0", "selectors", ] @@ -5717,12 +5651,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.5", + "windows-link 0.2.1", ] [[package]] @@ -6064,7 +5998,7 @@ dependencies = [ "quinn", "rand 0.8.5", "ring 0.17.14", - "rustls 0.23.32", + "rustls 0.23.35", "socket2 0.5.10", "thiserror 1.0.69", "tokio", @@ -6151,7 +6085,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6202,7 +6136,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.17.14", - "rustls 0.23.32", + "rustls 0.23.35", "rustls-webpki 0.101.7", "thiserror 1.0.69", "x509-parser 0.16.0", @@ -6279,7 +6213,7 @@ dependencies = [ "thiserror 1.0.69", "tracing", "yamux 0.12.1", - "yamux 0.13.7", + "yamux 0.13.8", ] [[package]] @@ -6288,7 +6222,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall 0.5.18", ] @@ -6315,9 +6249,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -6354,15 +6288,15 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" @@ -6442,7 +6376,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -6484,9 +6418,9 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -6536,7 +6470,7 @@ checksum = "0209ec7c2b5573660a3c5d0f64c90f8ff286171b15ad2234e7a3fbf8f26180be" dependencies = [ "crypto-bigint", "ff", - "generic-array 1.3.3", + "generic-array 1.3.5", "group", "rand_core 0.6.4", "rustversion", @@ -6589,14 +6523,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6612,7 +6546,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.0", "hyper-util", "log", "rand 0.9.2", @@ -6663,7 +6597,7 @@ dependencies = [ [[package]] name = "monero-address" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", "monero-base58", @@ -6676,36 +6610,36 @@ dependencies = [ [[package]] name = "monero-base58" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "monero-primitives", - "std-shims 0.1.5", + "std-shims", ] [[package]] name = "monero-borromean" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", "monero-generators", "monero-io", "monero-primitives", - "std-shims 0.1.5", + "std-shims", "zeroize", ] [[package]] name = "monero-bulletproofs" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", "monero-generators", "monero-io", "monero-primitives", "rand_core 0.6.4", - "std-shims 0.1.5", + "std-shims", "thiserror 2.0.17", "zeroize", ] @@ -6713,7 +6647,7 @@ dependencies = [ [[package]] name = "monero-clsag" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", "group", @@ -6722,7 +6656,7 @@ dependencies = [ "monero-primitives", "rand_chacha 0.3.1", "rand_core 0.6.4", - "std-shims 0.1.5", + "std-shims", "subtle", "thiserror 2.0.17", "zeroize", @@ -6741,7 +6675,7 @@ dependencies = [ [[package]] name = "monero-generators" version = "0.4.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "crypto-bigint", "curve25519-dalek 4.1.3", @@ -6749,7 +6683,7 @@ dependencies = [ "group", "monero-io", "sha3", - "std-shims 0.1.5", + "std-shims", "subtle", ] @@ -6773,23 +6707,23 @@ dependencies = [ [[package]] name = "monero-io" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", - "std-shims 0.1.5", + "std-shims", "zeroize", ] [[package]] name = "monero-mlsag" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", "monero-generators", "monero-io", "monero-primitives", - "std-shims 0.1.5", + "std-shims", "thiserror 2.0.17", "zeroize", ] @@ -6797,10 +6731,10 @@ dependencies = [ [[package]] name = "monero-oxide" version = "0.1.4-alpha" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", - "hex-literal 1.0.0", + "hex-literal 1.1.0", "monero-borromean", "monero-bulletproofs", "monero-clsag", @@ -6808,20 +6742,20 @@ dependencies = [ "monero-io", "monero-mlsag", "monero-primitives", - "std-shims 0.1.5", + "std-shims", "zeroize", ] [[package]] name = "monero-primitives" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", "monero-generators", "monero-io", "sha3", - "std-shims 0.1.5", + "std-shims", "zeroize", ] @@ -6848,7 +6782,7 @@ dependencies = [ [[package]] name = "monero-rpc" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "curve25519-dalek 4.1.3", "hex", @@ -6856,7 +6790,7 @@ dependencies = [ "monero-oxide", "serde", "serde_json", - "std-shims 0.1.5", + "std-shims", "thiserror 2.0.17", "zeroize", ] @@ -6869,12 +6803,12 @@ dependencies = [ "arti-client", "axum", "chrono", - "clap 4.5.49", + "clap 4.5.51", "crossbeam", "cuprate-epee-encoding", "futures", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.0", "hyper-util", "monero", "monero-rpc 0.1.0", @@ -6897,18 +6831,17 @@ dependencies = [ "typeshare", "url", "uuid", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] name = "monero-seed" version = "0.1.0" +source = "git+https://github.com/monero-oxide/monero-wallet-util.git#c6ac8cbfca7aa8002ac0afee9e9e3a3620aba852" dependencies = [ "curve25519-dalek 4.1.3", - "hex", - "monero-oxide", "rand_core 0.6.4", - "std-shims 0.1.4", + "std-shims", "thiserror 1.0.69", "zeroize", ] @@ -6916,11 +6849,11 @@ dependencies = [ [[package]] name = "monero-simple-request-rpc" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" +source = "git+https://github.com/monero-oxide/monero-oxide.git#7c288b058ae7f1e3d36600a2af11b3176102bcfc" dependencies = [ "digest_auth", "hex", - "monero-rpc 0.1.0 (git+https://github.com/monero-oxide/monero-oxide)", + "monero-rpc 0.1.0 (git+https://github.com/monero-oxide/monero-oxide.git)", "simple-request", "tokio", "zeroize", @@ -6956,11 +6889,23 @@ dependencies = [ "uuid", ] +[[package]] +name = "monero-tests" +version = "0.1.0" +dependencies = [ + "anyhow", + "monero", + "monero-harness", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "moxcms" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" dependencies = [ "num-traits", "pxfm", @@ -7066,7 +7011,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys", @@ -7186,7 +7131,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -7215,13 +7160,19 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonany" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b8866ec53810a9a4b3d434a29801e78c707430a9ae11c2db4b8b62bb9675a0" + [[package]] name = "notify" version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "inotify", "kqueue", "libc", @@ -7268,11 +7219,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -7331,9 +7281,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -7341,14 +7291,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -7392,7 +7342,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.6.2", "libc", "objc2 0.6.3", @@ -7413,7 +7363,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-foundation 0.3.2", ] @@ -7424,7 +7374,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-foundation 0.3.2", ] @@ -7435,7 +7385,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", "objc2 0.6.3", ] @@ -7446,7 +7396,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", "objc2 0.6.3", "objc2-core-foundation", @@ -7469,7 +7419,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", "objc2-core-graphics", @@ -7481,7 +7431,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", "objc2-core-graphics", @@ -7509,7 +7459,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -7521,7 +7471,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.6.2", "libc", "objc2 0.6.3", @@ -7544,7 +7494,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -7565,7 +7515,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -7577,7 +7527,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-app-kit", "objc2-foundation 0.3.2", @@ -7589,7 +7539,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -7602,7 +7552,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-foundation 0.3.2", ] @@ -7613,7 +7563,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -7624,7 +7574,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -7636,7 +7586,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.6.2", "objc2 0.6.3", "objc2-app-kit", @@ -7646,15 +7596,6 @@ dependencies = [ "objc2-security", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.6.1" @@ -7681,14 +7622,14 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oneshot-fused-workaround" -version = "0.3.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.4.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "futures", ] @@ -7719,11 +7660,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -7740,7 +7681,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -7751,9 +7692,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -8013,7 +7954,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8033,7 +7974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.11.4", + "indexmap 2.12.0", ] [[package]] @@ -8161,7 +8102,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8174,7 +8115,7 @@ dependencies = [ "phf_shared 0.13.1", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8230,7 +8171,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8290,8 +8231,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.11.4", - "quick-xml 0.38.3", + "indexmap 2.12.0", + "quick-xml 0.38.4", "serde", "time 0.3.44", ] @@ -8343,7 +8284,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -8410,9 +8351,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -8425,9 +8366,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppmd-rust" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" +checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" [[package]] name = "ppv-lite86" @@ -8451,7 +8392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8480,7 +8421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" dependencies = [ "equivalent", - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", ] @@ -8537,6 +8478,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -8545,9 +8508,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -8572,19 +8535,18 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "proptest" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.4", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -8617,14 +8579,14 @@ dependencies = [ [[package]] name = "pwd-grp" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94fdf3867b7f2889a736f0022ea9386766280d2cca4bdbe41629ada9e4f3b8f" +checksum = "0e2023f41b5fcb7c30eb5300a5733edfaa9e0e0d502d51b586f65633fd39e40c" dependencies = [ - "derive-deftly 0.14.6", + "derive-deftly", "libc", "paste", - "thiserror 1.0.69", + "thiserror 2.0.17", ] [[package]] @@ -8694,9 +8656,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] @@ -8720,7 +8682,7 @@ checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -8736,7 +8698,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.32", + "rustls 0.23.35", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -8751,12 +8713,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring 0.17.14", "rustc-hash", - "rustls 0.23.32", + "rustls 0.23.35", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -8781,9 +8743,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -8899,7 +8861,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -9011,7 +8973,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -9053,7 +9015,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -9095,17 +9057,22 @@ dependencies = [ ] [[package]] -name = "rendezvous-server" +name = "rendezvous-node" version = "0.2.0" dependencies = [ "anyhow", + "arti-client", "atty", + "backoff", "futures", "libp2p", "libp2p-tor", "structopt", + "swap-env", + "swap-p2p", "tokio", "tor-hsservice", + "tor-rtcompat", "tracing", "tracing-subscriber", ] @@ -9165,7 +9132,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.0", "hyper-rustls", "hyper-util", "js-sys", @@ -9173,8 +9140,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.32", - "rustls-native-certs 0.8.1", + "rustls 0.23.35", + "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", "serde_json", @@ -9191,7 +9158,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -9202,8 +9169,8 @@ checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" [[package]] name = "retry-error" -version = "0.7.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.8.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" [[package]] name = "rfc6979" @@ -9349,7 +9316,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -9381,15 +9348,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae8c0cb48f413ebe24dc2d148788e0efbe09ba3e011d9277162f2eaf8e1069a3" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.110", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -9426,7 +9387,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -9439,7 +9400,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -9485,16 +9446,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] @@ -9513,9 +9474,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -9534,9 +9495,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -9553,10 +9514,10 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.32", - "rustls-native-certs 0.8.1", + "rustls 0.23.35", + "rustls-native-certs 0.8.2", "rustls-platform-verifier-android", - "rustls-webpki 0.103.7", + "rustls-webpki 0.103.8", "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 0.26.11", @@ -9581,9 +9542,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -9615,7 +9576,7 @@ version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "clipboard-win", "fd-lock", @@ -9650,8 +9611,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "safelog" -version = "0.5.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.6.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "derive_more 2.0.1", "educe", @@ -9725,9 +9686,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -9744,7 +9705,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -9805,7 +9766,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array 0.14.9", + "generic-array 0.14.7", "pkcs8", "subtle", "zeroize", @@ -9923,7 +9884,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -9936,7 +9897,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -10059,7 +10020,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -10070,7 +10031,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -10115,7 +10076,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -10160,20 +10121,20 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" +checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.1.0", "serde_core", "serde_json", - "serde_with_macros 3.15.0", + "serde_with_macros 3.15.1", "time 0.3.44", ] @@ -10191,14 +10152,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" +checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -10207,7 +10168,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "ryu", "serde", @@ -10246,7 +10207,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -10268,7 +10229,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -10401,7 +10362,7 @@ checksum = "0b8e9462de42c6f14c7e20154d18d8e9e8683750798885e76f06973317b1cb1d" dependencies = [ "curve25519-dalek-ng", "digest 0.10.7", - "generic-array 0.14.9", + "generic-array 0.14.7", "rand_core 0.6.4", "secp256kfun", "serde", @@ -10468,7 +10429,7 @@ checksum = "5c2b7792ed2409a25b01606121e65579dcacbc574e90f275a8cb6a4d78006986" dependencies = [ "futures-util", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.0", "hyper-rustls", "hyper-util", "tokio", @@ -10521,8 +10482,8 @@ dependencies = [ [[package]] name = "slotmap-careful" -version = "0.3.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.4.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "paste", "serde", @@ -10717,7 +10678,7 @@ dependencies = [ "hashbrown 0.14.5", "hashlink", "hex", - "indexmap 2.11.4", + "indexmap 2.12.0", "log", "memchr", "once_cell", @@ -10748,7 +10709,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -10771,7 +10732,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.106", + "syn 2.0.110", "tempfile", "tokio", "url", @@ -10785,7 +10746,7 @@ checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -10797,7 +10758,7 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "generic-array 0.14.9", + "generic-array 0.14.7", "hex", "hkdf", "hmac", @@ -10828,7 +10789,7 @@ checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -10910,6 +10871,7 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" dependencies = [ + "num-bigint-dig", "p256", "p384", "p521", @@ -10936,16 +10898,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "std-shims" -version = "0.1.4" -source = "git+https://github.com/serai-dex/serai#1b781b4b576d4481845604899ea1334a2cf18252" -dependencies = [ - "hashbrown 0.14.5", - "rustversion", - "spin 0.10.0", -] - [[package]] name = "std-shims" version = "0.1.5" @@ -11063,7 +11015,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11075,7 +11027,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11092,13 +11044,12 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "swap" -version = "3.2.0-rc.1" +version = "3.3.1" dependencies = [ "anyhow", "arti-client", "async-compression 0.3.15", "async-trait", - "asynchronous-codec 0.7.0", "atty", "backoff", "base64 0.22.1", @@ -11123,7 +11074,6 @@ dependencies = [ "ecdsa_fun", "ed25519-dalek 1.0.1", "electrum-pool", - "fns", "futures", "get-port", "hex", @@ -11135,7 +11085,7 @@ dependencies = [ "monero", "monero-harness", "monero-rpc 0.1.0", - "monero-rpc 0.1.0 (git+https://github.com/monero-oxide/monero-oxide)", + "monero-rpc 0.1.0 (git+https://github.com/monero-oxide/monero-oxide.git)", "monero-rpc-pool", "monero-seed", "monero-simple-request-rpc", @@ -11149,7 +11099,7 @@ dependencies = [ "reqwest 0.12.24", "rust_decimal", "rust_decimal_macros", - "rustls 0.23.32", + "rustls 0.23.35", "semver", "serde", "serde_cbor", @@ -11163,10 +11113,12 @@ dependencies = [ "strum 0.26.3", "swap-controller-api", "swap-core", + "swap-db", "swap-env", "swap-feed", "swap-fs", "swap-machine", + "swap-p2p", "swap-serde", "tauri", "tempfile", @@ -11185,7 +11137,6 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "typeshare", - "unsigned-varint 0.8.0", "url", "uuid", "vergen-git2", @@ -11196,7 +11147,7 @@ dependencies = [ [[package]] name = "swap-asb" -version = "3.0.0-beta.3" +version = "3.3.1" dependencies = [ "anyhow", "bitcoin 0.32.7", @@ -11205,7 +11156,7 @@ dependencies = [ "monero-rpc-pool", "monero-sys", "rust_decimal", - "rustls 0.23.32", + "rustls 0.23.35", "serde", "serde_json", "structopt", @@ -11225,10 +11176,10 @@ dependencies = [ [[package]] name = "swap-controller" -version = "0.1.0" +version = "3.3.1" dependencies = [ "anyhow", - "clap 4.5.49", + "clap 4.5.51", "jsonrpsee", "monero", "rustyline", @@ -11275,6 +11226,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "swap-db" +version = "0.1.0" +dependencies = [ + "bitcoin 0.32.7", + "serde", + "strum 0.26.3", + "swap-core", + "swap-machine", + "swap-serde", +] + [[package]] name = "swap-env" version = "0.1.0" @@ -11359,7 +11322,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bitcoin 0.32.7", - "clap 4.5.49", + "clap 4.5.51", "compose_spec", "dialoguer", "monero", @@ -11371,6 +11334,37 @@ dependencies = [ "vergen 8.3.2", ] +[[package]] +name = "swap-p2p" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "asynchronous-codec 0.7.0", + "backoff", + "bitcoin 0.32.7", + "bitcoin-wallet", + "bmrng", + "futures", + "libp2p", + "monero", + "rand 0.8.5", + "serde", + "serde_cbor", + "swap-core", + "swap-env", + "swap-feed", + "swap-machine", + "swap-serde", + "thiserror 1.0.69", + "tokio", + "tracing", + "typeshare", + "unsigned-varint 0.8.0", + "uuid", + "void", +] + [[package]] name = "swap-serde" version = "0.1.0" @@ -11410,9 +11404,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -11454,7 +11448,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11488,7 +11482,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -11534,11 +11528,11 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tao" -version = "0.34.3" +version = "0.34.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.6.2", "core-foundation 0.10.1", "core-graphics", @@ -11580,7 +11574,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -11608,9 +11602,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.8.5" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5" dependencies = [ "anyhow", "bytes", @@ -11618,7 +11612,7 @@ dependencies = [ "dirs", "dunce", "embed_plist", - "getrandom 0.3.3", + "getrandom 0.3.4", "glob", "gtk", "heck 0.5.0", @@ -11651,7 +11645,6 @@ dependencies = [ "tokio", "tray-icon", "url", - "urlpattern", "webkit2gtk", "webview2-com", "window-vibrancy", @@ -11660,9 +11653,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38" dependencies = [ "anyhow", "cargo_toml", @@ -11682,9 +11675,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190" dependencies = [ "base64 0.22.1", "brotli 8.0.2", @@ -11698,7 +11691,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "syn 2.0.106", + "syn 2.0.110", "tauri-utils", "thiserror 2.0.17", "time 0.3.44", @@ -11709,23 +11702,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.4.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" dependencies = [ "anyhow", "glob", @@ -11740,11 +11733,11 @@ dependencies = [ [[package]] name = "tauri-plugin-cli" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e76101cc9848adfb6a04aae48a389062be457a785bb4349ae1423ddab5a82d" +checksum = "28e78fb2c09a81546bcd376d34db4bda5769270d00990daa9f0d6e7ac1107e25" dependencies = [ - "clap 4.5.49", + "clap 4.5.51", "log", "serde", "serde_json", @@ -11755,9 +11748,9 @@ dependencies = [ [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.3.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adddd9e9275b20e77af3061d100a25a884cced3c4c9ef680bd94dd0f7e26c1ca" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" dependencies = [ "arboard", "log", @@ -11770,9 +11763,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" dependencies = [ "log", "raw-window-handle", @@ -11788,9 +11781,9 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.4.2" +version = "2.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" dependencies = [ "anyhow", "dunce", @@ -11810,9 +11803,9 @@ dependencies = [ [[package]] name = "tauri-plugin-opener" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" +checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b" dependencies = [ "dunce", "glob", @@ -11832,9 +11825,9 @@ dependencies = [ [[package]] name = "tauri-plugin-process" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" dependencies = [ "tauri", "tauri-plugin", @@ -11842,9 +11835,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.3.1" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54777d0c0d8add34eea3ced84378619ef5b97996bd967d3038c668feefd21071" +checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" dependencies = [ "encoding_rs", "log", @@ -11863,9 +11856,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.3.4" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb9cac815bf11c4a80fb498666bcdad66d65b89e3ae24669e47806febb76389c" +checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710" dependencies = [ "serde", "serde_json", @@ -11878,9 +11871,9 @@ dependencies = [ [[package]] name = "tauri-plugin-store" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19" +checksum = "59a77036340a97eb5bbe1b3209c31e5f27f75e6f92a52fd9dd4b211ef08bf310" dependencies = [ "dunce", "serde", @@ -11926,9 +11919,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" +checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" dependencies = [ "cookie", "dpi", @@ -11951,9 +11944,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.8.1" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" +checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" dependencies = [ "gtk", "http 1.3.1", @@ -11978,9 +11971,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" +checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" dependencies = [ "anyhow", "brotli 8.0.2", @@ -12005,7 +11998,7 @@ dependencies = [ "serde", "serde-untagged", "serde_json", - "serde_with 3.15.0", + "serde_with 3.15.1", "swift-rs", "thiserror 2.0.17", "toml 0.9.8", @@ -12017,10 +12010,11 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ + "dunce", "embed-resource", "toml 0.9.8", ] @@ -12032,7 +12026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.2", "windows-sys 0.61.2", @@ -12120,7 +12114,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -12131,7 +12125,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -12219,11 +12213,12 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -12254,34 +12249,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.1", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -12311,7 +12303,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.32", + "rustls 0.23.35", "tokio", ] @@ -12374,9 +12366,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -12405,7 +12397,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde_core", "serde_spanned 1.0.3", "toml_datetime 0.7.3", @@ -12438,7 +12430,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -12449,7 +12441,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -12462,7 +12454,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "toml_datetime 0.7.3", "toml_parser", "winnow 0.7.13", @@ -12485,10 +12477,10 @@ checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tor-async-utils" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ - "derive-deftly 1.2.0", + "derive-deftly", "educe", "futures", "oneshot-fused-workaround", @@ -12500,8 +12492,8 @@ dependencies = [ [[package]] name = "tor-basic-utils" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "derive_more 2.0.1", "hex", @@ -12518,14 +12510,14 @@ dependencies = [ [[package]] name = "tor-bytes" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "bytes", - "derive-deftly 1.2.0", + "derive-deftly", "digest 0.10.7", "educe", - "getrandom 0.3.3", + "getrandom 0.3.4", "safelog", "thiserror 2.0.17", "tor-error", @@ -12535,14 +12527,14 @@ dependencies = [ [[package]] name = "tor-cell" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "caret", - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "educe", "itertools 0.14.0", @@ -12565,8 +12557,8 @@ dependencies = [ [[package]] name = "tor-cert" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "caret", "derive_builder_fork_arti", @@ -12580,8 +12572,8 @@ dependencies = [ [[package]] name = "tor-chanmgr" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "async-trait", "caret", @@ -12615,8 +12607,8 @@ dependencies = [ [[package]] name = "tor-checkable" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "humantime", "signature 2.2.0", @@ -12626,14 +12618,13 @@ dependencies = [ [[package]] name = "tor-circmgr" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "async-trait", - "bounded-vec-deque", "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "derive_more 2.0.1", "downcast-rs 2.0.2", @@ -12649,13 +12640,13 @@ dependencies = [ "retry-error", "safelog", "serde", - "static_assertions", "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-cell", "tor-chanmgr", "tor-config", + "tor-dircommon", "tor-error", "tor-guardmgr", "tor-linkspec", @@ -12675,12 +12666,12 @@ dependencies = [ [[package]] name = "tor-config" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "educe", "either", @@ -12707,8 +12698,8 @@ dependencies = [ [[package]] name = "tor-config-path" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "directories", "serde", @@ -12720,8 +12711,8 @@ dependencies = [ [[package]] name = "tor-consdiff" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "digest 0.10.7", "hex", @@ -12731,10 +12722,10 @@ dependencies = [ [[package]] name = "tor-dirclient" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ - "async-compression 0.4.32", + "async-compression 0.4.33", "base64ct", "derive_more 2.0.1", "futures", @@ -12756,10 +12747,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "tor-dircommon" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" +dependencies = [ + "base64ct", + "derive_builder_fork_arti", + "getset", + "humantime", + "humantime-serde", + "serde", + "tor-basic-utils", + "tor-checkable", + "tor-config", + "tor-linkspec", + "tor-llcrypto", + "tor-netdoc", + "tracing", +] + [[package]] name = "tor-dirmgr" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "async-trait", "base64ct", @@ -12797,6 +12808,7 @@ dependencies = [ "tor-config", "tor-consdiff", "tor-dirclient", + "tor-dircommon", "tor-error", "tor-guardmgr", "tor-llcrypto", @@ -12811,8 +12823,8 @@ dependencies = [ [[package]] name = "tor-error" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "derive_more 2.0.1", "futures", @@ -12827,8 +12839,8 @@ dependencies = [ [[package]] name = "tor-general-addr" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "derive_more 2.0.1", "thiserror 2.0.17", @@ -12837,12 +12849,12 @@ dependencies = [ [[package]] name = "tor-guardmgr" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "base64ct", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "derive_more 2.0.1", "dyn-clone", @@ -12863,6 +12875,7 @@ dependencies = [ "tor-async-utils", "tor-basic-utils", "tor-config", + "tor-dircommon", "tor-error", "tor-linkspec", "tor-llcrypto", @@ -12878,11 +12891,11 @@ dependencies = [ [[package]] name = "tor-hsclient" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "async-trait", - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "educe", "either", @@ -12921,12 +12934,12 @@ dependencies = [ [[package]] name = "tor-hscrypto" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "cipher", "data-encoding", - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "digest 0.10.7", "hex", @@ -12952,14 +12965,14 @@ dependencies = [ [[package]] name = "tor-hsservice" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "async-trait", "base64ct", "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "derive_more 2.0.1", "digest 0.10.7", @@ -12979,7 +12992,7 @@ dependencies = [ "retry-error", "safelog", "serde", - "serde_with 3.15.0", + "serde_with 3.15.1", "strum 0.27.2", "thiserror 2.0.17", "tor-async-utils", @@ -13009,14 +13022,15 @@ dependencies = [ [[package]] name = "tor-key-forge" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "downcast-rs 2.0.2", "paste", "rand 0.9.2", + "rsa", "signature 2.2.0", "ssh-key", "thiserror 2.0.17", @@ -13029,13 +13043,13 @@ dependencies = [ [[package]] name = "tor-keymgr" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "arrayvec", "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "derive_more 2.0.1", "downcast-rs 2.0.2", @@ -13068,20 +13082,20 @@ dependencies = [ [[package]] name = "tor-linkspec" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "base64ct", "by_address", "caret", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "derive_more 2.0.1", "hex", "itertools 0.14.0", "safelog", "serde", - "serde_with 3.15.0", + "serde_with 3.15.1", "strum 0.27.2", "thiserror 2.0.17", "tor-basic-utils", @@ -13094,20 +13108,20 @@ dependencies = [ [[package]] name = "tor-llcrypto" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "aes", "base64ct", "ctr", "curve25519-dalek 4.1.3", "der-parser 10.0.0", - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "digest 0.10.7", "ed25519-dalek 2.2.0", "educe", - "getrandom 0.3.3", + "getrandom 0.3.4", "hex", "rand 0.9.2", "rand_chacha 0.9.0", @@ -13124,6 +13138,7 @@ dependencies = [ "signature 2.2.0", "subtle", "thiserror 2.0.17", + "tor-error", "tor-memquota", "visibility", "x25519-dalek", @@ -13132,8 +13147,8 @@ dependencies = [ [[package]] name = "tor-log-ratelim" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "futures", "humantime", @@ -13146,11 +13161,11 @@ dependencies = [ [[package]] name = "tor-memquota" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "cfg-if", - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "dyn-clone", "educe", @@ -13175,11 +13190,11 @@ dependencies = [ [[package]] name = "tor-netdir" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "async-trait", - "bitflags 2.9.4", + "bitflags 2.10.0", "derive_more 2.0.1", "digest 0.10.7", "futures", @@ -13189,7 +13204,6 @@ dependencies = [ "num_enum", "rand 0.9.2", "serde", - "static_assertions", "strum 0.27.2", "thiserror 2.0.17", "time 0.3.44", @@ -13207,14 +13221,14 @@ dependencies = [ [[package]] name = "tor-netdoc" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "base64ct", - "bitflags 2.9.4", + "bitflags 2.10.0", "cipher", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "derive_more 2.0.1", "digest 0.10.7", @@ -13227,7 +13241,7 @@ dependencies = [ "phf 0.13.1", "rand 0.9.2", "serde", - "serde_with 3.15.0", + "serde_with 3.15.1", "signature 2.2.0", "smallvec", "strum 0.27.2", @@ -13253,11 +13267,11 @@ dependencies = [ [[package]] name = "tor-persist" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "filetime", "fs-mistrust", @@ -13281,8 +13295,8 @@ dependencies = [ [[package]] name = "tor-proto" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "asynchronous-codec 0.7.0", @@ -13293,16 +13307,18 @@ dependencies = [ "cipher", "coarsetime", "criterion-cycles-per-byte", - "derive-deftly 1.2.0", + "derive-deftly", "derive_builder_fork_arti", "derive_more 2.0.1", "digest 0.10.7", "educe", + "enum_dispatch", "futures", "futures-util", "hkdf", "hmac", "itertools 0.14.0", + "nonany", "oneshot-fused-workaround", "pin-project", "postage", @@ -13343,20 +13359,20 @@ dependencies = [ [[package]] name = "tor-protover" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "caret", "paste", - "serde_with 3.15.0", + "serde_with 3.15.1", "thiserror 2.0.17", "tor-bytes", ] [[package]] name = "tor-relay-selection" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "rand 0.9.2", "serde", @@ -13368,8 +13384,8 @@ dependencies = [ [[package]] name = "tor-rtcompat" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "async-trait", "async_executors", @@ -13385,7 +13401,7 @@ dependencies = [ "paste", "pin-project", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki 0.103.8", "thiserror 2.0.17", "tokio", "tokio-util", @@ -13397,13 +13413,13 @@ dependencies = [ [[package]] name = "tor-rtmock" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "assert_matches", "async-trait", - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "educe", "futures", @@ -13425,12 +13441,12 @@ dependencies = [ [[package]] name = "tor-socksproto" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ "amplify", "caret", - "derive-deftly 1.2.0", + "derive-deftly", "educe", "safelog", "subtle", @@ -13441,10 +13457,10 @@ dependencies = [ [[package]] name = "tor-units" -version = "0.34.0" -source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" +version = "0.35.0" +source = "git+https://github.com/eigenwallet/arti?rev=537dd8755817aa2b21c41e66edbd5f05f3c04373#537dd8755817aa2b21c41e66edbd5f05f3c04373" dependencies = [ - "derive-deftly 1.2.0", + "derive-deftly", "derive_more 2.0.1", "serde", "thiserror 2.0.17", @@ -13524,12 +13540,12 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-util", "http 1.3.1", "http-body 1.0.1", - "iri-string 0.7.8", + "iri-string 0.7.9", "pin-project-lite", "tower 0.5.2", "tower-layer", @@ -13580,7 +13596,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -13655,14 +13671,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "tray-icon" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" dependencies = [ "crossbeam-channel", "dirs", @@ -13677,7 +13693,7 @@ dependencies = [ "png 0.17.16", "serde", "thiserror 2.0.17", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -13761,7 +13777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -13863,24 +13879,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -13951,12 +13967,12 @@ dependencies = [ [[package]] name = "unstoppableswap-gui-rs" -version = "3.2.0-rc.1" +version = "3.3.1" dependencies = [ "anyhow", "dfx-swiss-sdk", "monero-rpc-pool", - "rustls 0.23.32", + "rustls 0.23.35", "serde", "serde_json", "swap", @@ -14044,7 +14060,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "serde", "wasm-bindgen", @@ -14122,9 +14138,9 @@ dependencies = [ [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -14146,7 +14162,7 @@ checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -14221,15 +14237,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -14256,9 +14263,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -14267,25 +14274,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -14296,9 +14289,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -14306,22 +14299,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -14359,7 +14352,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "rustix 1.1.2", "wayland-backend", "wayland-scanner", @@ -14371,7 +14364,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -14383,7 +14376,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -14420,9 +14413,9 @@ checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -14508,14 +14501,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.3", + "webpki-root-certs 1.0.4", ] [[package]] name = "webpki-root-certs" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" dependencies = [ "rustls-pki-types", ] @@ -14546,9 +14539,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -14575,7 +14568,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -14591,9 +14584,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "whoami" @@ -14744,7 +14737,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -14755,7 +14748,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -15216,15 +15209,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" dependencies = [ "base64 0.22.1", "block2 0.6.2", @@ -15370,9 +15363,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "xmltree" @@ -15406,9 +15399,9 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.7" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6927cfe0edfae4b26a369df6bad49cd0ef088c0ec48f4045b2084bcaedc10246" +checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" dependencies = [ "futures", "log", @@ -15431,11 +15424,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -15443,21 +15435,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure 0.13.2", ] [[package]] name = "zbus" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" dependencies = [ "async-broadcast", "async-executor", @@ -15480,7 +15472,8 @@ dependencies = [ "tokio", "tracing", "uds_windows", - "windows-sys 0.60.2", + "uuid", + "windows-sys 0.61.2", "winnow 0.7.13", "zbus_macros", "zbus_names", @@ -15489,14 +15482,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "zbus_names", "zvariant", "zvariant_utils", @@ -15531,7 +15524,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -15551,7 +15544,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "synstructure 0.13.2", ] @@ -15572,14 +15565,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -15588,10 +15581,11 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -15599,13 +15593,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -15630,14 +15624,14 @@ checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "aes", "arbitrary", - "bzip2 0.6.0", + "bzip2 0.6.1", "constant_time_eq", "crc32fast", "deflate64", "flate2", - "getrandom 0.3.3", + "getrandom 0.3.4", "hmac", - "indexmap 2.11.4", + "indexmap 2.12.0", "liblzma", "memchr", "pbkdf2", @@ -15657,9 +15651,9 @@ checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", @@ -15712,9 +15706,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.7.0" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" dependencies = [ "endi", "enumflags2", @@ -15727,14 +15721,14 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.7.0" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "zvariant_utils", ] @@ -15747,6 +15741,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.110", "winnow 0.7.13", ] diff --git a/Cargo.toml b/Cargo.toml index 99e42c3b89..486f46300c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,23 +3,25 @@ resolver = "2" members = [ "bitcoin-wallet", "electrum-pool", - "libp2p-rendezvous-server", + "libp2p-rendezvous-node", "libp2p-tor", "monero-rpc", "monero-rpc-pool", - "monero-seed", "monero-sys", + "monero-tests", "src-tauri", "swap", "swap-asb", "swap-controller", "swap-controller-api", "swap-core", + "swap-db", "swap-env", "swap-feed", "swap-fs", "swap-machine", "swap-orchestrator", + "swap-p2p", "swap-serde", "throttle", ] @@ -47,9 +49,11 @@ async-trait = "0.1" # Serialization serde_cbor = "0.11" +strum = { version = "0.26" } anyhow = "1" backoff = { version = "0.4", features = ["futures", "tokio"] } +bmrng = "0.5.2" futures = { version = "0.3", default-features = false, features = ["std"] } hex = "0.4" jsonrpsee = { version = "0.25", default-features = false } @@ -75,12 +79,12 @@ tokio = { version = "1", features = ["rt-multi-thread", "time", "macros", "sync" tokio-util = { version = "0.7", features = ["io", "codec", "rt"] } # Tor/Arti crates -arti-client = { git = "https://github.com/eigenwallet/arti", rev = "d909ada56d6b2b7ed7b4d0edd4c14e048f72a088", default-features = false } -safelog = { git = "https://github.com/eigenwallet/arti", rev = "d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" } -tor-cell = { git = "https://github.com/eigenwallet/arti", rev = "d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" } -tor-hsservice = { git = "https://github.com/eigenwallet/arti", rev = "d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" } -tor-proto = { git = "https://github.com/eigenwallet/arti", rev = "d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" } -tor-rtcompat = { git = "https://github.com/eigenwallet/arti", rev = "d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" } +arti-client = { git = "https://github.com/eigenwallet/arti", rev = "537dd8755817aa2b21c41e66edbd5f05f3c04373", default-features = false } +safelog = { git = "https://github.com/eigenwallet/arti", rev = "537dd8755817aa2b21c41e66edbd5f05f3c04373" } +tor-cell = { git = "https://github.com/eigenwallet/arti", rev = "537dd8755817aa2b21c41e66edbd5f05f3c04373" } +tor-hsservice = { git = "https://github.com/eigenwallet/arti", rev = "537dd8755817aa2b21c41e66edbd5f05f3c04373" } +tor-proto = { git = "https://github.com/eigenwallet/arti", rev = "537dd8755817aa2b21c41e66edbd5f05f3c04373" } +tor-rtcompat = { git = "https://github.com/eigenwallet/arti", rev = "537dd8755817aa2b21c41e66edbd5f05f3c04373" } # Terminal Utilities dialoguer = "0.11" diff --git a/README.md b/README.md index ce14b416d0..e709db6095 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ cargo nextest run If you want to donate to the project, you can use the following address. Donations will be used to fund development. -Please only do so if you do not need the money. We'd rather you keep it but people ask from time to time so we're adding it here. +Please only do so if you do not need the money. We'd rather you keep it but people ask from time to time so we're adding it here. Either one of the address below can be used to donate. ```gpg -----BEGIN PGP SIGNED MESSAGE----- @@ -53,3 +53,24 @@ bhasAQDGrAkZu+FFwDZDUEZzrIVS42he+GeMiS+ykpXyL5I7RQD/dXCR3f39zFsK =j+Vz -----END PGP SIGNATURE----- ``` + +``` +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +4A1tNBcsxhQA7NkswREXTD1QGz8mRyA7fGnCzPyTwqzKdDFMNje7iHUbGhCetfVUZa1PTuZCoPKj8gnJuRrFYJ2R2CEzqbJ is our donation address (signed by binarybaron) +-----BEGIN PGP SIGNATURE----- + +iQGzBAEBCAAdFiEEBRhGD+vsHaFKFVp7RK5vCxZqrVoFAmjxV4YACgkQRK5vCxZq +rVrFogv9F650Um1TsPlqQ+7kdobCwa7yH5uXOp1p22YaiwWGHKRU5rUSb6Ac+zI0 +3Io39VEoZufQqXqEqaiH7Q/08ABQR5r0TTPtSLNjOSEQ+ecClwv7MeF5CIXZYDdB +AlEOnlL0CPfA24GQMhfp9lvjNiTBA2NikLARWJrc1JsLrFMK5rHesv7VHJEtm/gu +We5eAuNOM2k3nAABTWzLiMJkH+G1amJmfkCKkBCk04inA6kZ5COUikMupyQDtsE4 +hrr/KrskMuXzGY+rjP6NhWqr/twKj819TrOxlYD4vK68cZP+jx9m+vSBE6mxgMbN +tBVdo9xFVCVymOYQCV8BRY8ScqP+YPNV5d6BMyDH9tvHJrGqZTNQiFhVX03Tw6mg +hccEqYP1J/TaAlFg/P4HtqsxPBZD6x3IdSxXhrJ0IjrqLpVtKyQlTZGsJuNjFWG8 +LKixaxxR7iWsyRZVCnEqCgDN8hzKZIE3Ph+kLTa4z4mTNEYyWUNeKRrFrSxKvEOK +KM0Pp53f +=O/zf +-----END PGP SIGNATURE----- +``` diff --git a/dev_scripts/brew_dependencies_install.sh b/dev-scripts/brew_dependencies_install.sh similarity index 100% rename from dev_scripts/brew_dependencies_install.sh rename to dev-scripts/brew_dependencies_install.sh diff --git a/dev_scripts/bump-version.sh b/dev-scripts/bump-version.sh similarity index 100% rename from dev_scripts/bump-version.sh rename to dev-scripts/bump-version.sh diff --git a/dev_scripts/code2prompt_as_file_mac_os.sh b/dev-scripts/code2prompt_as_file_mac_os.sh similarity index 100% rename from dev_scripts/code2prompt_as_file_mac_os.sh rename to dev-scripts/code2prompt_as_file_mac_os.sh diff --git a/dev_scripts/code2prompt_to_clipboard.sh b/dev-scripts/code2prompt_to_clipboard.sh similarity index 100% rename from dev_scripts/code2prompt_to_clipboard.sh rename to dev-scripts/code2prompt_to_clipboard.sh diff --git a/dev-scripts/health_check_default_electrum_servers.py b/dev-scripts/health_check_default_electrum_servers.py new file mode 100644 index 0000000000..49713833e1 --- /dev/null +++ b/dev-scripts/health_check_default_electrum_servers.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// + +""" +Check electrum server availability. +Usage: uv run check_electrum_servers.py +""" + +import json +import re +import socket +import ssl +import sys +from pathlib import Path +from urllib.parse import urlparse +from typing import List, Tuple +from concurrent.futures import ThreadPoolExecutor, as_completed + + +def extract_servers_from_rust(file_path: Path) -> List[str]: + """Extract electrum server URLs from Rust defaults.rs file.""" + servers = [] + content = file_path.read_text() + + # Find all Url::parse() calls + pattern = r'Url::parse\("([^"]+)"\)' + matches = re.findall(pattern, content) + servers.extend(matches) + + return servers + + +def extract_servers_from_typescript(file_path: Path) -> List[str]: + """Extract electrum server URLs from TypeScript defaults.ts file.""" + servers = [] + content = file_path.read_text() + + # Find all server strings in the arrays + pattern = r'"((?:tcp|ssl)://[^"]+)"' + matches = re.findall(pattern, content) + servers.extend(matches) + + return servers + + +def check_tcp_server(host: str, port: int, timeout: int = 2) -> Tuple[bool, str]: + """Check TCP electrum server.""" + request = json.dumps({"id": 1, "method": "blockchain.headers.subscribe", "params": []}) + "\n" + + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(timeout) + sock.connect((host, port)) + sock.sendall(request.encode()) + + response = b"" + while True: + chunk = sock.recv(4096) + if not chunk: + break + response += chunk + if b"\n" in response: + break + + response_str = response.decode().strip() + if response_str: + data = json.loads(response_str) + if 'result' in data or 'error' in data: + return True, "OK" + + return False, "No valid response" + except socket.timeout: + return False, "Timeout" + except ConnectionRefusedError: + return False, "Connection refused" + except Exception as e: + return False, str(e) + + +def check_ssl_server(host: str, port: int, timeout: int = 2) -> Tuple[bool, str]: + """Check SSL/TLS electrum server.""" + request = json.dumps({"id": 1, "method": "blockchain.headers.subscribe", "params": []}) + "\n" + + try: + context = ssl.create_default_context() + # Don't verify certificates for testing purposes + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(timeout) + with context.wrap_socket(sock, server_hostname=host) as ssock: + ssock.connect((host, port)) + ssock.sendall(request.encode()) + + response = b"" + while True: + chunk = ssock.recv(4096) + if not chunk: + break + response += chunk + if b"\n" in response: + break + + response_str = response.decode().strip() + if response_str: + data = json.loads(response_str) + if 'result' in data or 'error' in data: + return True, "OK" + + return False, "No valid response" + except socket.timeout: + return False, "Timeout" + except ConnectionRefusedError: + return False, "Connection refused" + except ssl.SSLError as e: + return False, f"SSL error: {str(e)}" + except Exception as e: + return False, str(e) + + +def check_server(url: str) -> Tuple[str, bool, str]: + """Check a single electrum server.""" + parsed = urlparse(url) + protocol = parsed.scheme + host = parsed.hostname + port = parsed.port + + if not host or not port: + return url, False, "Invalid URL format" + + if protocol == "tcp": + success, message = check_tcp_server(host, port) + elif protocol == "ssl": + success, message = check_ssl_server(host, port) + else: + return url, False, f"Unknown protocol: {protocol}" + + return url, success, message + + +def main(): + # Find the files + base_dir = Path(__file__).parent + rust_file = base_dir / "swap-env" / "src" / "defaults.rs" + ts_file = base_dir / "src-gui" / "src" / "store" / "features" / "defaults.ts" + + if not rust_file.exists(): + print(f"❌ Rust defaults file not found: {rust_file}") + sys.exit(1) + + if not ts_file.exists(): + print(f"❌ TypeScript defaults file not found: {ts_file}") + sys.exit(1) + + # Extract servers + print("📋 Extracting server URLs...") + rust_servers = extract_servers_from_rust(rust_file) + ts_servers = extract_servers_from_typescript(ts_file) + + # Combine and deduplicate + all_servers = list(set(rust_servers + ts_servers)) + all_servers.sort() + + print(f"\n📊 Found {len(all_servers)} unique servers\n") + + # Check servers in parallel + print("🔍 Checking servers (this may take a minute)...\n") + + working = [] + broken = [] + + with ThreadPoolExecutor(max_workers=10) as executor: + futures = {executor.submit(check_server, url): url for url in all_servers} + + for future in as_completed(futures): + url, success, message = future.result() + + status = "✅" if success else "❌" + print(f"{status} {url:60s} {message}") + + if success: + working.append(url) + else: + broken.append((url, message)) + + # Summary + print("\n" + "="*80) + print(f"\n📊 Summary:") + print(f" ✅ Working: {len(working)}/{len(all_servers)}") + print(f" ❌ Broken: {len(broken)}/{len(all_servers)}") + + if broken: + print(f"\n⚠️ Broken servers:") + for url, reason in broken: + print(f" • {url} - {reason}") + + # Check if the newly added server is working + new_server = "tcp://bitcoin.aranguren.org:50001" + if new_server in all_servers: + is_working = new_server in working + status = "✅ WORKING" if is_working else "❌ BROKEN" + print(f"\n🆕 Newly added server: {new_server} - {status}") + + sys.exit(0 if len(broken) == 0 else 1) + + +if __name__ == "__main__": + main() diff --git a/dev-scripts/homebrew/eigenwallet.rb.template b/dev-scripts/homebrew/eigenwallet.rb.template new file mode 100644 index 0000000000..76baf7c91e --- /dev/null +++ b/dev-scripts/homebrew/eigenwallet.rb.template @@ -0,0 +1,26 @@ +cask "eigenwallet" do + version "VERSION_PLACEHOLDER" + + on_arm do + sha256 "AARCH64_SHA256_PLACEHOLDER" + url "https://github.com/eigenwallet/core/releases/download/#{version}/eigenwallet_#{version}_aarch64_darwin.dmg" + end + + on_intel do + sha256 "X64_SHA256_PLACEHOLDER" + url "https://github.com/eigenwallet/core/releases/download/#{version}/eigenwallet_#{version}_x64_darwin.dmg" + end + + name "Eigenwallet" + desc "GUI for XMR<>BTC Atomic Swaps" + homepage "https://github.com/eigenwallet/core" + + livecheck do + url :url + strategy :github_latest + end + + auto_updates true + + app "eigenwallet.app" +end diff --git a/dev_scripts/publish_flatpak.sh b/dev-scripts/publish_flatpak.sh similarity index 63% rename from dev_scripts/publish_flatpak.sh rename to dev-scripts/publish_flatpak.sh index 0437ea062a..dd871cf964 100755 --- a/dev_scripts/publish_flatpak.sh +++ b/dev-scripts/publish_flatpak.sh @@ -11,7 +11,6 @@ BRANCH="gh-pages" GPG_SIGN="" NO_GPG_FLAG="" REPO_DIR="flatpak-repo" -TEMP_DIR="$(mktemp -d)" # Parse arguments while [[ $# -gt 0 ]]; do @@ -39,13 +38,11 @@ done # Function to list available GPG keys list_gpg_keys() { echo "📋 Available GPG keys:" - gpg --list-secret-keys --keyid-format=long 2>/dev/null | grep -E "^(sec|uid)" | while IFS= read -r line; do - if [[ $line =~ ^sec ]]; then - key_info=$(echo "$line" | awk '{print $2}') + gpg --list-secret-keys --keyid-format=long 2>/dev/null | while read -r type key_info name; do + if [[ $type = sec ]]; then echo " 🔑 Key: $key_info" - elif [[ $line =~ ^uid ]]; then - uid=$(echo "$line" | sed 's/uid[[:space:]]*\[[^]]*\][[:space:]]*//') - echo " 👤 $uid" + elif [[ $type = uid ]]; then + echo " 👤 $name" echo "" fi done @@ -58,7 +55,7 @@ select_gpg_key() { exit 1 fi - local keys=($(gpg --list-secret-keys --keyid-format=long 2>/dev/null | grep "^sec" | awk '{print $2}' | cut -d'/' -f2)) + local keys=($(gpg --list-secret-keys --keyid-format=long 2>/dev/null | awk -F "[$IFS/]*" '/^sec/ {print $3}')) if [ ${#keys[@]} -eq 0 ]; then echo "🔑 No GPG keys found." @@ -70,7 +67,6 @@ select_gpg_key() { select_gpg_key else echo "⚠️ Proceeding without GPG signing (not recommended for production)" - GPG_SIGN="" return fi else @@ -80,7 +76,7 @@ select_gpg_key() { echo "Please select a GPG key for signing:" for i in "${!keys[@]}"; do local key_id="${keys[i]}" - local user_info=$(gpg --list-secret-keys --keyid-format=long "$key_id" 2>/dev/null | grep "^uid" | head -1 | sed 's/uid[[:space:]]*\[[^]]*\][[:space:]]*//') + local user_info=$(gpg --list-secret-keys --keyid-format=long "$key_id" 2>/dev/null | awk '/^uid/ {$1=""; $2="\b"; print; exit}') echo " $((i+1))) ${key_id} - ${user_info}" done echo " $((${#keys[@]}+1))) Skip GPG signing" @@ -93,7 +89,6 @@ select_gpg_key() { if [[ $choice =~ ^[0-9]+$ ]] && [ $choice -ge 1 ] && [ $choice -le $((${#keys[@]}+2)) ]; then if [ $choice -eq $((${#keys[@]}+1)) ]; then echo "⚠️ Proceeding without GPG signing" - GPG_SIGN="" break elif [ $choice -eq $((${#keys[@]}+2)) ]; then import_gpg_key @@ -101,7 +96,7 @@ select_gpg_key() { break else GPG_SIGN="${keys[$((choice-1))]}" - local selected_user=$(gpg --list-secret-keys --keyid-format=long "$GPG_SIGN" 2>/dev/null | grep "^uid" | head -1 | sed 's/uid[[:space:]]*\[[^]]*\][[:space:]]*//') + local selected_user=$(gpg --list-secret-keys --keyid-format=long "$GPG_SIGN" 2>/dev/null | awk '/^uid/ {$1=""; $2="\b"; print; exit}') echo "✅ Selected key: $GPG_SIGN - $selected_user" break fi @@ -122,28 +117,19 @@ import_gpg_key() { echo " Press Ctrl+D when finished:" echo "" - local temp_key_file=$(mktemp) - cat > "$temp_key_file" - - echo "" - echo "🔄 Importing key..." - - if gpg --import "$temp_key_file" 2>/dev/null; then + if gpg --import - 2>/dev/null; then echo "✅ GPG key imported successfully!" else echo "❌ Failed to import GPG key. Please check the format and try again." - rm -f "$temp_key_file" exit 1 fi - - rm -f "$temp_key_file" } # Check requirements if ! command -v flatpak-builder &> /dev/null; then echo "❌ flatpak-builder is required but not installed" echo "Install with: sudo apt install flatpak-builder (Ubuntu/Debian)" - echo " sudo dnf install flatpak-builder (Fedora)" + echo " sudo dnf install flatpak-builder (Fedora)" exit 1 fi @@ -155,12 +141,12 @@ fi if ! command -v jq &> /dev/null; then echo "❌ jq is required but not installed" echo "Install with: sudo apt install jq (Ubuntu/Debian)" - echo " sudo dnf install jq (Fedora)" + echo " sudo dnf install jq (Fedora)" exit 1 fi # Get repository info -REPO_URL=$(git remote get-url origin 2>/dev/null || echo "") +REPO_URL=$(git remote get-url origin 2>/dev/null || :) if [[ $REPO_URL =~ github\.com[:/]([^/]+)/([^/.]+) ]]; then GITHUB_USER="${BASH_REMATCH[1]}" REPO_NAME="${BASH_REMATCH[2]}" @@ -203,12 +189,13 @@ echo "" # Always use local .deb file - build if needed echo "🔍 Ensuring local .deb file exists..." MANIFEST_FILE="flatpak/org.eigenwallet.app.json" -TEMP_MANIFEST="" +trap 'rm -f "$TEMP_MANIFEST"' EXIT INT +TEMP_MANIFEST=$(mktemp --suffix=.json) # Look for the .deb file in the expected location -DEB_FILE=$(find ./target/release/bundle/deb/ -name "*.deb" -not -name "*.deb.sig" 2>/dev/null | head -1) +DEB_FILE=$(find "$PWD/target/debug/bundle/deb/" -name "*.deb" -print -quit) -if [ -n "$DEB_FILE" ] && [ -f "$DEB_FILE" ]; then +if [ -f "$DEB_FILE" ]; then echo "✅ Found local .deb file: $DEB_FILE" else echo "🏗️ No local .deb file found, building locally..." @@ -219,7 +206,7 @@ else fi # Extract version from Cargo.toml - VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*= "//' | sed 's/".*//') + VERSION=$(awk -F "[$IFS\"]*" '/^version/ { print $3; exit; }' Cargo.toml) if [ -z "$VERSION" ]; then echo "❌ Could not determine version from Cargo.toml" exit 1 @@ -229,8 +216,8 @@ else ./release-build.sh "$VERSION" # Look for the .deb file again - DEB_FILE=$(find ./target/release/bundle/deb/ -name "*.deb" -not -name "*.deb.sig" 2>/dev/null | head -1) - if [ -z "$DEB_FILE" ] || [ ! -f "$DEB_FILE" ]; then + DEB_FILE=$(find "$PWD/target/debug/bundle/deb/" -name "*.deb" -print -quit) + if ! [ -f "$DEB_FILE" ]; then echo "❌ Failed to build .deb file" exit 1 fi @@ -238,39 +225,19 @@ else echo "✅ Local build completed: $DEB_FILE" fi -# Get the absolute path -DEB_ABSOLUTE_PATH=$(realpath "$DEB_FILE") - -# Calculate SHA256 hash of the .deb file -echo "🔢 Calculating SHA256 hash..." -DEB_SHA256=$(sha256sum "$DEB_ABSOLUTE_PATH" | cut -d' ' -f1) -echo " Hash: $DEB_SHA256" - -# Create a temporary manifest with the local file -TEMP_MANIFEST=$(mktemp --suffix=.json) - echo "📝 Creating manifest with local .deb..." # Modify the manifest to use the local file -jq --arg deb_path "file://$DEB_ABSOLUTE_PATH" --arg deb_hash "$DEB_SHA256" ' - .modules[0].sources = [ - { - "type": "file", - "url": $deb_path, - "sha256": $deb_hash, - "dest": ".", - "dest-filename": "eigenwallet.deb" - } - ] | - .modules[0]."build-commands" = [ - "ar -x eigenwallet.deb", - "tar -xf data.tar.gz", - "install -Dm755 usr/bin/unstoppableswap-gui-rs /app/bin/unstoppableswap-gui-rs" - ] +jq --arg deb_path "$DEB_FILE" --arg PWD "$PWD" ' + .modules[0].sources[0] = { + "type": "file", + "path": $deb_path + } | + .modules[0].sources[1].path = $PWD + "/" + .modules[0].sources[1].path ' "$MANIFEST_FILE" > "$TEMP_MANIFEST" MANIFEST_FILE="$TEMP_MANIFEST" -echo "📦 Using local build: $(basename "$DEB_FILE")" +echo "📦 Using local build: ${DEB_FILE##*/}" echo "" @@ -318,59 +285,25 @@ if [ -n "$GPG_SIGN" ]; then fi flatpak build-bundle "${BUNDLE_ARGS[@]}" org.eigenwallet.app -# Generate .flatpakrepo file -echo "📝 Generating .flatpakrepo file..." -cat > "$REPO_DIR/eigenwallet.flatpakrepo" << EOF -[Flatpak Repo] -Title=eigenwallet -Name=eigenwallet -Url=${PAGES_URL}/ -Homepage=https://github.com/${GITHUB_USER}/${REPO_NAME} -Comment=Unstoppable cross-chain atomic swaps -Description=Repository for eigenwallet applications - providing secure and decentralized XMR-BTC atomic swaps -Icon=${PAGES_URL}/icon.png -SuggestRemoteName=eigenwallet -EOF - -# Add GPG key if signing +# Add GPG key only if signing if [ -n "$GPG_SIGN" ]; then - echo "🔑 Adding GPG key to .flatpakrepo..." - GPG_KEY_B64=$(gpg --export "$GPG_SIGN" | base64 -w 0) - echo "GPGKey=$GPG_KEY_B64" >> "$REPO_DIR/eigenwallet.flatpakrepo" + echo "🔑 Adding GPG keys to .flatpakrepo and .flatpakref..." + GPGKey="s|%GPGKey%|$(gpg --export "$GPG_SIGN" | base64 -w 0)|" +else + GPGKey="/%GPGKey%/d" fi -# Generate .flatpakref file -echo "📝 Generating .flatpakref file..." -cat > "$REPO_DIR/org.eigenwallet.app.flatpakref" << EOF -[Flatpak Ref] -Title=eigenwallet GUI -Name=org.eigenwallet.app -Branch=stable -Url=${PAGES_URL}/ -SuggestRemoteName=eigenwallet -Homepage=https://github.com/${GITHUB_USER}/${REPO_NAME} -Icon=${PAGES_URL}/icon.png -RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo -IsRuntime=false -EOF - -# Add GPG key if signing -if [ -n "$GPG_SIGN" ]; then - GPG_KEY_B64=$(gpg --export "$GPG_SIGN" | base64 -w 0) - echo "GPGKey=$GPG_KEY_B64" >> "$REPO_DIR/org.eigenwallet.app.flatpakref" -fi +cp -v flatpak/*.flatpakre* "$REPO_DIR/" +sed -e "s|%Url%|${PAGES_URL}|" \ + -e "s|%Homepage%|https://github.com/${GITHUB_USER}/${REPO_NAME}|" \ + -e "$GPGKey" \ + -i "$REPO_DIR"/*.flatpakre* # Copy bundle to repo directory cp org.eigenwallet.app.flatpak "$REPO_DIR/" # Use index.html from flatpak directory -if [ -f "flatpak/index.html" ]; then - echo "Copying index.html from flatpak directory..." - cp flatpak/index.html "$REPO_DIR/index.html" -else - echo "Error: flatpak/index.html not found" - exit 1 -fi +cp -v flatpak/index.html "$REPO_DIR/" # Copy any additional files if [ -f "icon.png" ]; then @@ -382,44 +315,27 @@ if [ -f "README.md" ]; then fi # Add .nojekyll file to skip Jekyll processing -touch "$REPO_DIR/.nojekyll" +>> "$REPO_DIR/.nojekyll" echo "✅ Flatpak repository built successfully!" -echo "📊 Repository size: $(du -sh $REPO_DIR | cut -f1)" +echo "📊 Repository size: $(du -sh "$REPO_DIR" | { read -r s _; echo "$s"; })" echo "📁 Repository files are in: $REPO_DIR/" if [ "$PUSH_FLAG" = "--push" ]; then echo "" echo "🚀 Deploying to GitHub Pages..." - # Store current branch - CURRENT_BRANCH=$(git branch --show-current) - - # Create a temporary directory for deployment - DEPLOY_DIR=$(mktemp -d) - - # Copy flatpak repo to deploy directory (including hidden files) - echo "📁 Preparing deployment files..." - cp -r "$REPO_DIR"/. "$DEPLOY_DIR/" - # Initialize fresh git repo in deploy directory - cd "$DEPLOY_DIR" - git init - git add . - git commit -m "Update Flatpak repository $(date -u '+%Y-%m-%d %H:%M:%S UTC')" - - # Go back to original directory - cd - > /dev/null + git -C "$REPO_DIR" init + git -C "$REPO_DIR" add . + git -C "$REPO_DIR" commit -m "Update Flatpak repository $(date -u '+%F %T %Z')" # Push to GitHub Pages branch echo "🚀 Force pushing to $BRANCH..." - cd "$DEPLOY_DIR" - git remote add origin "$(cd - > /dev/null && git remote get-url origin)" - git push --force origin HEAD:"$BRANCH" + git -C "$REPO_DIR" push --force "$REPO_URL" HEAD:"$BRANCH" - # Return to original directory and clean up - cd - > /dev/null - rm -rf "$DEPLOY_DIR" + # Clean up + rm -rf "$REPO_DIR/.git" echo "🎉 Deployed successfully!" echo "🌐 Your Flatpak repository is available at: $PAGES_URL" @@ -438,12 +354,3 @@ else echo "" echo "📋 Or manually copy the contents of $REPO_DIR/ to your gh-pages branch" fi - -# Cleanup temporary manifest if created -if [ -n "$TEMP_MANIFEST" ] && [ -f "$TEMP_MANIFEST" ]; then - rm -f "$TEMP_MANIFEST" -fi - -# Cleanup -rm -rf "$TEMP_DIR" -echo "🧹 Cleanup completed" \ No newline at end of file diff --git a/regenerate_sqlx_cache.sh b/dev-scripts/regenerate_sqlx_cache.sh similarity index 97% rename from regenerate_sqlx_cache.sh rename to dev-scripts/regenerate_sqlx_cache.sh index 87130bc4e5..659b9cc8a4 100755 --- a/regenerate_sqlx_cache.sh +++ b/dev-scripts/regenerate_sqlx_cache.sh @@ -1,9 +1,9 @@ #!/bin/bash # regenerate_sqlx_cache.sh -# +# # Script to regenerate SQLx query cache for monero-rpc-pool -# +# # This script: # 1. Creates a temporary SQLite database in the workspace root # 2. Runs all database migrations to set up the schema @@ -25,7 +25,7 @@ set -e # Exit on any error echo "🔄 Regenerating SQLx query cache..." -WORKSPACE_ROOT="$(pwd)" +WORKSPACE_ROOT="$PWD" # Use shared temporary database in workspace root TEMP_DB="$WORKSPACE_ROOT/tempdb.sqlite" @@ -51,4 +51,4 @@ cargo sqlx prepare --workspace echo "✅ SQLx query cache regenerated successfully!" echo "📝 The .sqlx directory has been updated with the latest query metadata." -echo "💡 Make sure to commit the .sqlx directory to version control." \ No newline at end of file +echo "💡 Make sure to commit the .sqlx directory to version control." diff --git a/dev_scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh b/dev-scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh similarity index 98% rename from dev_scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh rename to dev-scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh index aa9d99be47..fabf002546 100755 --- a/dev_scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh +++ b/dev-scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh @@ -39,13 +39,13 @@ esac set -euo pipefail -# Get the current project root (this file is in /dev_scripts/ and gets called via just (just file is at /justfile)) -SRC_TAURI_DIR="$(pwd)/../src-tauri" +# Get the current project root (this file is in /dev-scripts/ and gets called via just (just file is at /justfile)) +SRC_TAURI_DIR="$PWD/../src-tauri" # Check if src-tauri directory exists if [ ! -d "$SRC_TAURI_DIR" ]; then echo "Error: must be called from project root -> src-tauri must be subdir" - echo "Current directory: $(pwd)" + echo "Current directory: $PWD" exit 1 fi @@ -93,7 +93,7 @@ download_if_missing() { echo "Already present: $dest" else echo "Downloading: $url" - wget -q "$url" -O "$dest" + wget --retry-connrefused -t 20 -nv "$url" -O "$dest" fi } diff --git a/docs/pages/usage/first_swap.mdx b/docs/pages/usage/first_swap.mdx index 2a23a5ed1b..6397f2dee3 100644 --- a/docs/pages/usage/first_swap.mdx +++ b/docs/pages/usage/first_swap.mdx @@ -8,7 +8,7 @@ Although the process is quite intuitive, there are some nuances to be aware of, To complete an Atomic Swap, you'll need to have the following: 1. A Monero wallet you can use to receive funds -2. _UnstoppableSwap GUI_ installed on your computer. +2. _eigenwallet GUI_ installed on your computer. We'll refer to this as _GUI_ from now on. View the [installation instructions](../getting_started/install_instructions) if you haven't already installed the GUI. 3. A Bitcoin wallet you can use with some funds in it which you want to convert to Monero. diff --git a/dprint.json b/dprint.json index fea9f204ac..c66d527706 100644 --- a/dprint.json +++ b/dprint.json @@ -24,7 +24,6 @@ ".git/**", "**/node_modules/**", "**/dist/**", - "monero-seed/**", "monero-sys/monero_c", "monero-sys/monero" ], diff --git a/flatpak/eigenwallet.flatpakrepo b/flatpak/eigenwallet.flatpakrepo new file mode 100644 index 0000000000..e608fa047c --- /dev/null +++ b/flatpak/eigenwallet.flatpakrepo @@ -0,0 +1,11 @@ +[Flatpak Repo] +Url=%Url% +Icon=%Url%/icon.png +SuggestRemoteName=eigenwallet +Homepage=%Homepage% +GPGKey=%GPGKey% + +Title=eigenwallet +Name=eigenwallet +Comment=Unstoppable cross-chain atomic swaps +Description=Repository for eigenwallet applications - providing secure and decentralized XMR-BTC atomic swaps diff --git a/flatpak/net.unstoppableswap.gui.json b/flatpak/net.unstoppableswap.gui.json deleted file mode 100644 index faf916ca75..0000000000 --- a/flatpak/net.unstoppableswap.gui.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "id": "net.unstoppableswap.gui", - "runtime": "org.gnome.Platform", - "runtime-version": "47", - "sdk": "org.gnome.Sdk", - "command": "unstoppableswap-gui-rs", - "finish-args": [ - "--socket=wayland", - "--socket=fallback-x11", - "--device=dri", - "--share=ipc", - "--share=network", - "--talk-name=org.kde.StatusNotifierWatcher", - "--filesystem=xdg-run/tray-icon:create", - "--filesystem=~/.local/share/xmr-btc-swap" - ], - "modules": [ - { - "name": "binary", - "buildsystem": "simple", - "sources": [ - { - "type": "file", - "url": "https://cdn.crabnebula.app/download/unstoppableswap/unstoppableswap-gui-rs/latest/platform/debian-x86_64", - "sha256": "f1fdb9dc164ed45e31fbba7209da23fa4cb1d461442c9dd0b6bc763b8bf1bb59", - "only-arches": ["x86_64"] - } - ], - "build-commands": [ - "ar -x *.deb", - "tar -xf data.tar.gz", - "install -Dm755 usr/bin/unstoppableswap-gui-rs /app/bin/unstoppableswap-gui-rs" - ] - } - ] -} diff --git a/flatpak/org.eigenwallet.app.appdata.xml b/flatpak/org.eigenwallet.app.appdata.xml new file mode 100644 index 0000000000..ea501efb1c --- /dev/null +++ b/flatpak/org.eigenwallet.app.appdata.xml @@ -0,0 +1,16 @@ + + + org.eigenwallet.app + org.eigenwallet.app + eigenwallet GUI + GUI for XMR<>BTC Atomic Swaps written in Rust +

GUI for XMR<>BTC Atomic Swaps written in Rust

+ CC0-1.0 + unstoppableswap-gui-rs + + Utility + + + + +
diff --git a/flatpak/org.eigenwallet.app.flatpakref b/flatpak/org.eigenwallet.app.flatpakref new file mode 100644 index 0000000000..0a45e48e89 --- /dev/null +++ b/flatpak/org.eigenwallet.app.flatpakref @@ -0,0 +1,12 @@ +[Flatpak Ref] +Url=%Url% +Icon=%Url%/icon.png +SuggestRemoteName=eigenwallet +Homepage=%Homepage% +GPGKey=%GPGKey% + +Title=eigenwallet GUI +Name=org.eigenwallet.app +Branch=stable +RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo +IsRuntime=false diff --git a/flatpak/org.eigenwallet.app.json b/flatpak/org.eigenwallet.app.json index 7b59f3aca0..33763d7ff7 100644 --- a/flatpak/org.eigenwallet.app.json +++ b/flatpak/org.eigenwallet.app.json @@ -24,12 +24,18 @@ "url": "https://cdn.crabnebula.app/download/unstoppableswap/unstoppableswap-gui-rs/latest/platform/debian-x86_64", "sha256": "f1fdb9dc164ed45e31fbba7209da23fa4cb1d461442c9dd0b6bc763b8bf1bb59", "only-arches": ["x86_64"] + }, + { + "type": "file", + "path": "flatpak/org.eigenwallet.app.appdata.xml" } ], "build-commands": [ "ar -x *.deb", "tar -xf data.tar.gz", - "install -Dm755 usr/bin/unstoppableswap-gui-rs /app/bin/unstoppableswap-gui-rs" + "install -Dm755 usr/bin/unstoppableswap-gui-rs -t /app/bin", + "install -Dm644 *.appdata.xml -t /app/share/metainfo", + "mv usr/share/icons /app/share" ] } ] diff --git a/justfile b/justfile index 964ac6cf41..10279976b3 100644 --- a/justfile +++ b/justfile @@ -18,6 +18,9 @@ monero_sys: just update_submodules cd monero-sys && cargo build +undo-monero-changes: + cd monero-sys/monero && git restore . + # Test the FFI bindings using various sanitizers, that can detect memory safety issues. test-ffi: test-ffi-address @@ -57,17 +60,18 @@ build-gui-windows: tests: cargo nextest run +# Run docker tests (e.g., "just docker_test happy_path_alice_developer_tip") +docker_test test_name: + cargo test --package swap --test {{test_name}} -- --nocapture + docker_test_happy_path: - cargo test --package swap --test happy_path -- --nocapture + just docker_test happy_path docker_test_happy_path_with_developer_tip: - cargo test --package swap --test happy_path_alice_developer_tip -- --nocapture + just docker_test happy_path_alice_developer_tip docker_test_refund_path: - cargo test --package swap --test alice_refunds_after_restart_bob_refunded -- --nocapture - -docker_test_all: - cargo test --package swap --test all -- --nocapture + just docker_test alice_refunds_after_restart_bob_refunded # Tests the Rust bindings for Monero test_monero_sys: @@ -79,7 +83,11 @@ swap: # Run the asb on testnet asb-testnet: - ASB_DEV_ADDR_OUTPUT_PATH="$(pwd)/src-gui/.env.development" cargo run -p swap-asb --bin asb -- --trace --testnet start --rpc-bind-port 9944 --rpc-bind-host 0.0.0.0 + ASB_DEV_ADDR_OUTPUT_PATH="$PWD/src-gui/.env.development" cargo run -p swap-asb --bin asb -- --testnet --trace start --rpc-bind-port 9944 --rpc-bind-host 0.0.0.0 + +# Launch the ASB controller REPL against a local testnet ASB instance +asb-testnet-controller: + cargo run -p swap-controller --bin asb-controller -- --url http://127.0.0.1:9944 # Updates our submodules (currently only Monero C++ codebase) update_submodules: @@ -98,7 +106,7 @@ fmt: dprint fmt generate-sqlx-cache: - ./regenerate_sqlx_cache.sh + ./dev-scripts/regenerate_sqlx_cache.sh # Run eslint for the GUI frontend check_gui_eslint: @@ -108,6 +116,10 @@ check_gui_eslint: check_gui_tsc: cd src-gui && yarn run tsc --noEmit +# Check for unused code in the GUI frontend +check_gui_unused_code: + cd src-gui && npx knip + test test_name: cargo test --test {{test_name}} -- --nocapture @@ -122,7 +134,7 @@ docker-prune-network: # Install dependencies required for building monero-sys prepare_mac_os_brew_dependencies: - cd dev_scripts && chmod +x ./brew_dependencies_install.sh && ./brew_dependencies_install.sh + cd dev-scripts && chmod +x ./brew_dependencies_install.sh && ./brew_dependencies_install.sh # Takes a crate (e.g monero-rpc-pool) and uses code2prompt to copy to clipboard # E.g code2prompt . --exclude "*.lock" --exclude ".sqlx/*" --exclude "target" @@ -130,4 +142,5 @@ code2prompt_single_crate crate: cd {{crate}} && code2prompt . --exclude "*.lock" --exclude ".sqlx/*" --exclude "target" prepare-windows-build: - cd dev_scripts && ./ubuntu_build_x86_86-w64-mingw32-gcc.sh + cd dev-scripts && ./ubuntu_build_x86_86-w64-mingw32-gcc.sh + diff --git a/libp2p-rendezvous-server/Cargo.toml b/libp2p-rendezvous-node/Cargo.toml similarity index 75% rename from libp2p-rendezvous-server/Cargo.toml rename to libp2p-rendezvous-node/Cargo.toml index 4c9be4bdfe..49812b4271 100644 --- a/libp2p-rendezvous-server/Cargo.toml +++ b/libp2p-rendezvous-node/Cargo.toml @@ -1,18 +1,33 @@ [package] -name = "rendezvous-server" +name = "rendezvous-node" version = "0.2.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1" +# Tor +arti-client = { workspace = true } +tor-hsservice = { workspace = true } +tor-rtcompat = { workspace = true } + +# Async +backoff = { workspace = true, features = ["tokio"] } futures = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "io-util"] } + +# Libp2p libp2p = { workspace = true, features = ["rendezvous", "tcp", "yamux", "dns", "noise", "ping", "websocket", "tokio", "macros"] } libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } -tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "io-util"] } -tor-hsservice = { workspace = true } +swap-p2p = { path = "../swap-p2p" } + +# Tracing tracing = { workspace = true, features = ["attributes"] } tracing-subscriber = { workspace = true, default-features = false, features = ["fmt", "ansi", "env-filter", "chrono", "tracing-log", "json"] } +# Error +anyhow = "1" + +# Other atty = "0.2" structopt = { version = "0.3", default-features = false } +swap-env = { path = "../swap-env" } diff --git a/libp2p-rendezvous-server/Dockerfile b/libp2p-rendezvous-node/Dockerfile similarity index 76% rename from libp2p-rendezvous-server/Dockerfile rename to libp2p-rendezvous-node/Dockerfile index 571194f103..90821def8c 100644 --- a/libp2p-rendezvous-server/Dockerfile +++ b/libp2p-rendezvous-node/Dockerfile @@ -10,14 +10,14 @@ RUN apt-get install -y git clang cmake libsnappy-dev # Build the rendezvous server binary COPY . . -RUN cargo build --release --package rendezvous-server --bin rendezvous-server +RUN cargo build --release --package rendezvous-node --bin rendezvous-node # Latest Debian Bookworm image as of Tue, 05 Aug 2025 15:34:08 GMT FROM debian:bookworm@sha256:b6507e340c43553136f5078284c8c68d86ec8262b1724dde73c325e8d3dcdeba AS runner # Copy the compiled binary from the previous stage -COPY --from=builder /build/target/release/rendezvous-server /bin/rendezvous-server +COPY --from=builder /build/target/release/rendezvous-node /bin/rendezvous-node EXPOSE 8888 -ENTRYPOINT ["rendezvous-server"] +ENTRYPOINT ["rendezvous-node"] diff --git a/libp2p-rendezvous-server/README.md b/libp2p-rendezvous-node/README.md similarity index 97% rename from libp2p-rendezvous-server/README.md rename to libp2p-rendezvous-node/README.md index 67a681841b..592f32ce5e 100644 --- a/libp2p-rendezvous-server/README.md +++ b/libp2p-rendezvous-node/README.md @@ -10,7 +10,7 @@ Build the binary: cargo build --release ``` -Run the `libp2p-rendezvous-server`: +Run the `libp2p-rendezvous-node`: ``` cargo run --release diff --git a/libp2p-rendezvous-node/src/behaviour.rs b/libp2p-rendezvous-node/src/behaviour.rs new file mode 100644 index 0000000000..78206d037c --- /dev/null +++ b/libp2p-rendezvous-node/src/behaviour.rs @@ -0,0 +1,62 @@ +use anyhow::{Context, Result}; +use libp2p::swarm::NetworkBehaviour; +use libp2p::{identity, Multiaddr, PeerId}; +use swap_p2p::protocols::rendezvous::{register, XmrBtcNamespace}; + +/// Acts as both a rendezvous server and registers at other rendezvous points +#[derive(NetworkBehaviour)] +pub struct Behaviour { + pub server: libp2p::rendezvous::server::Behaviour, + pub register: register::Behaviour, +} + +impl Behaviour { + pub fn new( + identity: identity::Keypair, + rendezvous_addrs: Vec, + namespace: XmrBtcNamespace, + registration_ttl: Option, + ) -> Result { + let server = libp2p::rendezvous::server::Behaviour::new( + libp2p::rendezvous::server::Config::default(), + ); + + let rendezvous_nodes = + build_rendezvous_nodes(rendezvous_addrs, namespace, registration_ttl)?; + let register = register::Behaviour::new(identity, rendezvous_nodes); + + Ok(Self { server, register }) + } +} + +/// Builds a list of RendezvousNode from multiaddrs and namespace +fn build_rendezvous_nodes( + addrs: Vec, + namespace: XmrBtcNamespace, + registration_ttl: Option, +) -> Result> { + addrs + .into_iter() + .map(|addr| { + let peer_id = extract_peer_id(&addr)?; + Ok(register::RendezvousNode::new( + &addr, + peer_id, + namespace, + registration_ttl, + )) + }) + .collect() +} + +fn extract_peer_id(addr: &Multiaddr) -> Result { + addr.iter() + .find_map(|protocol| { + if let libp2p::multiaddr::Protocol::P2p(peer_id) = protocol { + Some(peer_id) + } else { + None + } + }) + .context("No peer_id found in multiaddr") +} diff --git a/libp2p-rendezvous-node/src/main.rs b/libp2p-rendezvous-node/src/main.rs new file mode 100644 index 0000000000..4ed660747c --- /dev/null +++ b/libp2p-rendezvous-node/src/main.rs @@ -0,0 +1,173 @@ +use anyhow::{Context, Result}; +use futures::StreamExt; +use libp2p::identity::{self, ed25519}; +use libp2p::rendezvous; +use libp2p::swarm::SwarmEvent; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; +use tokio::fs; +use tokio::fs::{DirBuilder, OpenOptions}; +use tokio::io::AsyncWriteExt; +use tracing::level_filters::LevelFilter; + +use crate::swarm::{create_swarm, create_swarm_with_onion, Addresses}; + +pub mod behaviour; +pub mod swarm; +pub mod tor; +pub mod tracing_util; + +#[derive(Debug, StructOpt)] +struct Cli { + /// If the directory does not exist, it will be created + /// Contains Tor state and LibP2P identity + #[structopt(long, default_value = "./rendezvous-data")] + data_dir: PathBuf, + + /// Port used for listening on TCP and onion service + #[structopt(long, default_value = "8888")] + port: u16, + + /// Enable listening on Tor onion service + #[structopt(long)] + no_onion: bool, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::from_args(); + + tracing_util::init_tracing(LevelFilter::TRACE); + + // Create data directory if it doesn't exist + DirBuilder::new() + .recursive(true) + .create(&cli.data_dir) + .await + .with_context(|| { + format!( + "Could not create data directory: {}", + cli.data_dir.display() + ) + })?; + + let identity_file = cli.data_dir.join("identity.secret"); + let secret_key = load_secret_key_from_file(&identity_file).await?; + + let identity = identity::Keypair::from(ed25519::Keypair::from(secret_key)); + + let rendezvous_addrs = swap_env::defaults::default_rendezvous_points(); + + let mut swarm = if cli.no_onion { + create_swarm(identity, rendezvous_addrs)? + } else { + create_swarm_with_onion(identity, cli.port, &cli.data_dir, rendezvous_addrs).await? + }; + + tracing::info!(peer_id=%swarm.local_peer_id(), "Rendezvous server peer id"); + + swarm + .listen_on( + format!("/ip4/0.0.0.0/tcp/{}", cli.port) + .parse() + .expect("static string is valid MultiAddress"), + ) + .context("Failed to initialize listener")?; + + loop { + match swarm.select_next_some().await { + SwarmEvent::Behaviour(behaviour::BehaviourEvent::Server( + rendezvous::server::Event::PeerRegistered { peer, registration }, + )) => { + tracing::info!(%peer, namespace=%registration.namespace, addresses=?registration.record.addresses(), ttl=registration.ttl, "Peer registered"); + } + SwarmEvent::Behaviour(behaviour::BehaviourEvent::Server( + rendezvous::server::Event::PeerNotRegistered { + peer, + namespace, + error, + }, + )) => { + tracing::info!(%peer, %namespace, ?error, "Peer failed to register"); + } + SwarmEvent::Behaviour(behaviour::BehaviourEvent::Server( + rendezvous::server::Event::RegistrationExpired(registration), + )) => { + tracing::info!(peer=%registration.record.peer_id(), namespace=%registration.namespace, addresses=%Addresses(registration.record.addresses()), ttl=registration.ttl, "Registration expired"); + } + SwarmEvent::Behaviour(behaviour::BehaviourEvent::Server( + rendezvous::server::Event::PeerUnregistered { peer, namespace }, + )) => { + tracing::info!(%peer, %namespace, "Peer unregistered"); + } + SwarmEvent::Behaviour(behaviour::BehaviourEvent::Server( + rendezvous::server::Event::DiscoverServed { enquirer, .. }, + )) => { + tracing::info!(peer=%enquirer, "Discovery served"); + } + SwarmEvent::Behaviour(behaviour::BehaviourEvent::Register( + rendezvous::client::Event::Registered { + rendezvous_node, + ttl, + namespace, + }, + )) => { + tracing::info!(%rendezvous_node, %namespace, ttl, "Registered at rendezvous point"); + } + SwarmEvent::Behaviour(behaviour::BehaviourEvent::Register( + rendezvous::client::Event::RegisterFailed { + rendezvous_node, + namespace, + error, + }, + )) => { + tracing::warn!(%rendezvous_node, %namespace, ?error, "Failed to register at rendezvous point"); + } + SwarmEvent::NewListenAddr { address, .. } => { + tracing::info!(%address, "New listening address reported"); + } + other => { + tracing::debug!(?other, "Unhandled event"); + } + } + } +} + +async fn load_secret_key_from_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + + match fs::read(path).await { + Ok(bytes) => { + // File exists, try to load the secret key + let secret_key = ed25519::SecretKey::try_from_bytes(bytes)?; + Ok(secret_key) + } + Err(_) => { + // File doesn't exist, generate a new secret key + tracing::info!( + "Identity file not found at {}, generating new key", + path.display() + ); + let secret_key = ed25519::SecretKey::generate(); + + // Save the new key to file + write_secret_key_to_file(&secret_key, path.to_path_buf()).await?; + tracing::info!("New identity saved to {}", path.display()); + + Ok(secret_key) + } + } +} + +async fn write_secret_key_to_file(secret_key: &ed25519::SecretKey, path: PathBuf) -> Result<()> { + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .await + .with_context(|| format!("Could not generate identity file at {}", path.display()))?; + + file.write_all(secret_key.as_ref()).await?; + + Ok(()) +} diff --git a/libp2p-rendezvous-server/src/swarm.rs b/libp2p-rendezvous-node/src/swarm.rs similarity index 77% rename from libp2p-rendezvous-server/src/swarm.rs rename to libp2p-rendezvous-node/src/swarm.rs index 96e8cc764b..daf2b4a4cd 100644 --- a/libp2p-rendezvous-server/src/swarm.rs +++ b/libp2p-rendezvous-node/src/swarm.rs @@ -3,15 +3,18 @@ use futures::{AsyncRead, AsyncWrite}; use libp2p::core::transport::Boxed; use libp2p::core::upgrade::Version; use libp2p::identity::{self}; -use libp2p::rendezvous::server::Behaviour; use libp2p::tcp; use libp2p::yamux; use libp2p::{core::muxing::StreamMuxerBox, SwarmBuilder}; -use libp2p::{dns, noise, rendezvous, Multiaddr, PeerId, Swarm, Transport}; +use libp2p::{dns, noise, Multiaddr, PeerId, Swarm, Transport}; use libp2p_tor::{AddressConversion, TorTransport}; use std::fmt; +use std::path::Path; use tor_hsservice::config::OnionServiceConfigBuilder; +use crate::behaviour::Behaviour; +use crate::tor; + /// Defaults we use for the networking mod defaults { use std::time::Duration; @@ -23,16 +26,26 @@ mod defaults { pub const HIDDEN_SERVICE_NUM_INTRO_POINTS: u8 = 5; pub const MULTIPLEX_TIMEOUT: Duration = Duration::from_secs(60); + + pub const REGISTRATION_TTL: Option = None; } -pub fn create_swarm(identity: identity::Keypair) -> Result> { +pub fn create_swarm( + identity: identity::Keypair, + rendezvous_addrs: Vec, +) -> Result> { let transport = create_transport(&identity).context("Failed to create transport")?; - let rendezvous = rendezvous::server::Behaviour::new(rendezvous::server::Config::default()); + let behaviour = Behaviour::new( + identity.clone(), + rendezvous_addrs, + swap_p2p::protocols::rendezvous::XmrBtcNamespace::RendezvousPoint, + defaults::REGISTRATION_TTL, + )?; let swarm = SwarmBuilder::with_existing_identity(identity) .with_tokio() .with_other_transport(|_| transport)? - .with_behaviour(|_| rendezvous)? + .with_behaviour(|_| behaviour)? .with_swarm_config(|cfg| { cfg.with_idle_connection_timeout(defaults::IDLE_CONNECTION_TIMEOUT) }) @@ -44,16 +57,23 @@ pub fn create_swarm(identity: identity::Keypair) -> Result> { pub async fn create_swarm_with_onion( identity: identity::Keypair, onion_port: u16, + data_dir: &Path, + rendezvous_addrs: Vec, ) -> Result> { - let (transport, onion_address) = create_transport_with_onion(&identity, onion_port) + let (transport, onion_address) = create_transport_with_onion(&identity, onion_port, data_dir) .await .context("Failed to create transport with onion")?; - let rendezvous = rendezvous::server::Behaviour::new(rendezvous::server::Config::default()); + let behaviour = Behaviour::new( + identity.clone(), + rendezvous_addrs, + swap_p2p::protocols::rendezvous::XmrBtcNamespace::RendezvousPoint, + defaults::REGISTRATION_TTL, + )?; let mut swarm = SwarmBuilder::with_existing_identity(identity) .with_tokio() .with_other_transport(|_| transport)? - .with_behaviour(|_| rendezvous)? + .with_behaviour(|_| behaviour)? .with_swarm_config(|cfg| { cfg.with_idle_connection_timeout(defaults::IDLE_CONNECTION_TIMEOUT) }) @@ -64,6 +84,8 @@ pub async fn create_swarm_with_onion( .listen_on(onion_address.clone()) .context("Failed to listen on onion address")?; + swarm.add_external_address(onion_address.clone()); + tracing::info!(%onion_address, "Onion service configured"); Ok(swarm) @@ -81,15 +103,19 @@ fn create_transport(identity: &identity::Keypair) -> Result Result<(Boxed<(PeerId, StreamMuxerBox)>, Multiaddr)> { // Create TCP transport let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)); let tcp_with_dns = dns::tokio::Transport::system(tcp)?; - // Create Tor transport - let mut tor_transport = TorTransport::unbootstrapped() - .await? - .with_address_conversion(AddressConversion::IpAndDns); + // Create and bootstrap Tor client + let tor_client = tor::create_tor_client(data_dir).await?; + + tokio::task::spawn(tor::bootstrap_tor_client(tor_client.clone())); + + // Create Tor transport from the bootstrapped client + let mut tor_transport = TorTransport::from_client(tor_client, AddressConversion::IpAndDns); // Create onion service configuration let onion_service_config = OnionServiceConfigBuilder::default() diff --git a/libp2p-rendezvous-node/src/tor.rs b/libp2p-rendezvous-node/src/tor.rs new file mode 100644 index 0000000000..c8605562b4 --- /dev/null +++ b/libp2p-rendezvous-node/src/tor.rs @@ -0,0 +1,57 @@ +// TODO: This is essentially vendored from swap/src/common/tor.rs +// TODO: Consider extracting this into a common swap-tor crate +use anyhow::{Context, Result}; +use arti_client::{config::TorClientConfigBuilder, TorClient}; +use std::path::Path; +use std::sync::Arc; +use tor_rtcompat::tokio::TokioRustlsRuntime; + +/// Creates an unbootstrapped Tor client with custom data directories +pub async fn create_tor_client(data_dir: &Path) -> Result>> { + // We store the Tor state in the data directory + let tor_data_dir = data_dir.join("tor"); + let state_dir = tor_data_dir.join("state"); + let cache_dir = tor_data_dir.join("cache"); + + // Workaround for https://gitlab.torproject.org/tpo/core/arti/-/issues/2224 + // We delete guards.json (if it exists) on startup to prevent an issue where arti will not find any guards to connect to + // This forces new guards on every startup + // + // TODO: This is not good for privacy and should be removed as soon as this is fixed in arti itself. + let guards_file = state_dir.join("state").join("guards.json"); + let _ = tokio::fs::remove_file(&guards_file).await; + + // The client configuration describes how to connect to the Tor network, + // and what directories to use for storing persistent state. + let config = TorClientConfigBuilder::from_directories(&state_dir, &cache_dir); + + let config = config + .build() + .context("Failed to build Tor client config")?; + + // Create the Arti client without bootstrapping + let runtime = TokioRustlsRuntime::current().context("We are always running with tokio")?; + + tracing::debug!("Creating unbootstrapped Tor client"); + + let tor_client = TorClient::with_runtime(runtime) + .config(config) + .create_unbootstrapped_async() + .await + .context("Failed to create unbootstrapped Tor client")?; + + Ok(Arc::new(tor_client)) +} + +/// Bootstraps an existing Tor client +pub async fn bootstrap_tor_client(tor_client: Arc>) -> Result<()> { + tracing::debug!("Bootstrapping Tor client"); + + // Run the bootstrap until it's complete + tor_client + .bootstrap() + .await + .context("Failed to bootstrap Tor client")?; + + Ok(()) +} diff --git a/libp2p-rendezvous-node/src/tracing_util.rs b/libp2p-rendezvous-node/src/tracing_util.rs new file mode 100644 index 0000000000..e6180cc56c --- /dev/null +++ b/libp2p-rendezvous-node/src/tracing_util.rs @@ -0,0 +1,25 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::FmtSubscriber; + +pub fn init_tracing(level: LevelFilter) { + if level == LevelFilter::OFF { + return; + } + + let is_terminal = atty::is(atty::Stream::Stderr); + + FmtSubscriber::builder() + .with_env_filter(format!( + "rendezvous_server={},\ + swap_p2p={},\ + libp2p={}, + libp2p_rendezvous={},\ + libp2p_swarm={},\ + libp2p_tor={}", + level, level, level, level, level, level + )) + .with_writer(std::io::stderr) + .with_ansi(is_terminal) + .with_target(false) + .init(); +} diff --git a/libp2p-rendezvous-server/src/main.rs b/libp2p-rendezvous-server/src/main.rs deleted file mode 100644 index 0aa0c27fa6..0000000000 --- a/libp2p-rendezvous-server/src/main.rs +++ /dev/null @@ -1,222 +0,0 @@ -use anyhow::{Context, Result}; -use futures::StreamExt; -use libp2p::identity::{self, ed25519}; -use libp2p::rendezvous; -use libp2p::swarm::SwarmEvent; -use std::path::{Path, PathBuf}; -use structopt::StructOpt; -use tokio::fs; -use tokio::fs::{DirBuilder, OpenOptions}; -use tokio::io::AsyncWriteExt; -use tracing::level_filters::LevelFilter; -use tracing_subscriber::FmtSubscriber; - -use crate::swarm::{create_swarm, create_swarm_with_onion, Addresses}; - -pub mod swarm; - -#[derive(Debug, StructOpt)] -struct Cli { - /// Path to the file that contains the secret key of the rendezvous server's - /// identity keypair - /// If the file does not exist, a new secret key will be generated and saved to the file - #[structopt(long, default_value = "./rendezvous-server-secret.key")] - secret_file: PathBuf, - - /// Port used for listening on TCP (default) - #[structopt(long, default_value = "8888")] - listen_tcp: u16, - - /// Enable listening on Tor onion service - #[structopt(long)] - no_onion: bool, - - /// Port for the onion service (only used if --onion is enabled) - #[structopt(long, default_value = "8888")] - onion_port: u16, - - /// Format logs as JSON - #[structopt(long)] - json: bool, - - /// Don't include timestamp in logs. Useful if captured logs already get - /// timestamped, e.g. through journald. - #[structopt(long)] - no_timestamp: bool, -} - -#[tokio::main] -async fn main() -> Result<()> { - let cli = Cli::from_args(); - - init_tracing(LevelFilter::TRACE, cli.json, cli.no_timestamp); - - let secret_key = load_secret_key_from_file(&cli.secret_file).await?; - - let identity = identity::Keypair::from(ed25519::Keypair::from(secret_key)); - - let mut swarm = if cli.no_onion { - create_swarm(identity)? - } else { - create_swarm_with_onion(identity, cli.onion_port).await? - }; - - tracing::info!(peer_id=%swarm.local_peer_id(), "Rendezvous server peer id"); - - swarm - .listen_on( - format!("/ip4/0.0.0.0/tcp/{}", cli.listen_tcp) - .parse() - .expect("static string is valid MultiAddress"), - ) - .context("Failed to initialize listener")?; - - loop { - match swarm.select_next_some().await { - SwarmEvent::Behaviour(rendezvous::server::Event::PeerRegistered { - peer, - registration, - }) => { - tracing::info!(%peer, namespace=%registration.namespace, addresses=?registration.record.addresses(), ttl=registration.ttl, "Peer registered"); - } - SwarmEvent::Behaviour(rendezvous::server::Event::PeerNotRegistered { - peer, - namespace, - error, - }) => { - tracing::info!(%peer, %namespace, ?error, "Peer failed to register"); - } - SwarmEvent::Behaviour(rendezvous::server::Event::RegistrationExpired(registration)) => { - tracing::info!(peer=%registration.record.peer_id(), namespace=%registration.namespace, addresses=%Addresses(registration.record.addresses()), ttl=registration.ttl, "Registration expired"); - } - SwarmEvent::Behaviour(rendezvous::server::Event::PeerUnregistered { - peer, - namespace, - }) => { - tracing::info!(%peer, %namespace, "Peer unregistered"); - } - SwarmEvent::Behaviour(rendezvous::server::Event::DiscoverServed { - enquirer, .. - }) => { - tracing::info!(peer=%enquirer, "Discovery served"); - } - SwarmEvent::NewListenAddr { address, .. } => { - tracing::info!(%address, "New listening address reported"); - } - other => { - tracing::debug!(?other, "Unhandled event"); - } - } - } -} - -fn init_tracing(level: LevelFilter, json_format: bool, no_timestamp: bool) { - if level == LevelFilter::OFF { - return; - } - - let is_terminal = atty::is(atty::Stream::Stderr); - - let builder = FmtSubscriber::builder() - .with_env_filter(format!( - "rendezvous_server={},\ - libp2p={},\ - libp2p_allow_block_list={},\ - libp2p_connection_limits={},\ - libp2p_core={},\ - libp2p_dns={},\ - libp2p_identity={},\ - libp2p_noise={},\ - libp2p_ping={},\ - libp2p_rendezvous={},\ - libp2p_request_response={},\ - libp2p_swarm={},\ - libp2p_tcp={},\ - libp2p_tls={},\ - libp2p_tor={},\ - libp2p_websocket={},\ - libp2p_yamux={}", - level, - level, - level, - level, - level, - level, - level, - level, - level, - level, - level, - level, - level, - level, - level, - level, - level - )) - .with_writer(std::io::stderr) - .with_ansi(is_terminal) - .with_target(false); - - if json_format { - builder.json().init(); - return; - } - - if no_timestamp { - builder.without_time().init(); - return; - } - builder.init(); -} - -async fn load_secret_key_from_file(path: impl AsRef) -> Result { - let path = path.as_ref(); - - match fs::read(path).await { - Ok(bytes) => { - // File exists, try to load the secret key - let secret_key = ed25519::SecretKey::try_from_bytes(bytes)?; - Ok(secret_key) - } - Err(_) => { - // File doesn't exist, generate a new secret key - tracing::info!( - "Secret file not found at {}, generating new key", - path.display() - ); - let secret_key = ed25519::SecretKey::generate(); - - // Save the new key to file - write_secret_key_to_file(&secret_key, path.to_path_buf()).await?; - tracing::info!("New secret key saved to {}", path.display()); - - Ok(secret_key) - } - } -} - -async fn write_secret_key_to_file(secret_key: &ed25519::SecretKey, path: PathBuf) -> Result<()> { - if let Some(parent) = path.parent() { - DirBuilder::new() - .recursive(true) - .create(parent) - .await - .with_context(|| { - format!( - "Could not create directory for secret file: {}", - parent.display() - ) - })?; - } - let mut file = OpenOptions::new() - .write(true) - .create_new(true) - .open(&path) - .await - .with_context(|| format!("Could not generate secret file at {}", path.display()))?; - - file.write_all(secret_key.as_ref()).await?; - - Ok(()) -} diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index 889f549d1d..5160232aa6 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -23,14 +23,14 @@ use std::time::Duration; use anyhow::{anyhow, bail, Context, Result}; -use testcontainers::clients::Cli; +pub use testcontainers::clients::Cli; use testcontainers::{Container, RunnableImage}; use tokio::time; use monero::{Address, Amount}; use monero_rpc::monerod::MonerodRpc as _; use monero_rpc::monerod::{self, GenerateBlocks}; -use monero_sys::{no_listener, Daemon, SyncProgress, TxReceipt, WalletHandle}; +use monero_sys::{no_listener, Daemon, SyncProgress, TxReceipt, TxStatus, WalletHandle}; use crate::image::{MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, RPC_PORT}; @@ -47,6 +47,18 @@ pub struct Monero { } impl<'c> Monero { + /// Same as `new_with_sync_specified` but with sync specified as true. + pub async fn new( + cli: &'c Cli, + additional_wallets: Vec<&'static str>, + ) -> Result<( + Self, + Container<'c, image::Monerod>, + Vec>, + )> { + Self::new_with_sync_specified(cli, additional_wallets, true).await + } + /// Starts a new regtest monero container setup consisting out of 1 monerod /// node and n wallets. The docker container and network will be prefixed /// with a randomly generated `prefix`. One miner wallet is started @@ -54,9 +66,10 @@ impl<'c> Monero { /// monerod container name is: `prefix`_`monerod` /// network is: `prefix`_`monero` /// miner wallet container name is: `miner` - pub async fn new( + pub async fn new_with_sync_specified( cli: &'c Cli, additional_wallets: Vec<&'static str>, + background_synced: bool, ) -> Result<( Self, Container<'c, image::Monerod>, @@ -118,7 +131,14 @@ impl<'c> Monero { let wallet_instance = tokio::time::timeout(Duration::from_secs(300), async { loop { - match MoneroWallet::new(wallet, daemon.clone(), prefix.clone()).await { + match MoneroWallet::new_with_sync_specified( + wallet, + daemon.clone(), + prefix.clone(), + background_synced, + ) + .await + { Ok(w) => break w, Err(e) => { tracing::warn!( @@ -411,6 +431,16 @@ impl<'c> Monerod { impl MoneroWallet { /// Create a new wallet using monero-sys bindings connected to the provided monerod instance. async fn new(name: &str, daemon: Daemon, prefix: String) -> Result { + Self::new_with_sync_specified(name, daemon, prefix, true).await + } + + /// Create a new wallet using monero-sys bindings connected to the provided monerod instance. + async fn new_with_sync_specified( + name: &str, + daemon: Daemon, + prefix: String, + background_sync: bool, + ) -> Result { // Wallet files will be stored in the system temporary directory with the prefix to avoid clashes let mut wallet_path = std::env::temp_dir(); wallet_path.push(format!("{}{}", prefix, name)); @@ -421,7 +451,7 @@ impl MoneroWallet { wallet_path.display().to_string(), daemon, monero::Network::Mainnet, - true, + background_sync, ) .await .context("Failed to create or open wallet")?; @@ -451,7 +481,7 @@ impl MoneroWallet { tracing::debug!("Wallet connected to daemon: {}", connected); // Force a refresh first - self.refresh().await?; + // self.refresh().await?; let total = self.wallet.total_balance().await.as_pico(); tracing::debug!( @@ -462,6 +492,17 @@ impl MoneroWallet { Ok(total) } + pub async fn check_tx_key(&self, txid: String, txkey: monero::PrivateKey) -> Result { + let status = self + .wallet + .check_tx_status(txid.clone(), txkey, &self.address().await?) + .await?; + + self.wallet.scan_transaction(txid).await?; + + Ok(status) + } + pub async fn unlocked_balance(&self) -> Result { Ok(self.wallet.unlocked_balance().await.as_pico()) } @@ -516,10 +557,6 @@ impl MoneroWallet { .await .context("Failed to perform sweep") } - - pub async fn blockchain_height(&self) -> Result { - self.wallet.blockchain_height().await - } } /// Mine a block ever BLOCK_TIME_SECS seconds. diff --git a/monero-seed/Cargo.toml b/monero-seed/Cargo.toml deleted file mode 100644 index 04b6466b49..0000000000 --- a/monero-seed/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "monero-seed" -version = "0.1.0" -authors = ["Luke Parker "] -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 = { workspace = true, 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-oxide = { git = "https://github.com/monero-oxide/monero-oxide", default-features = false, features = ["std"] } - -[features] -std = [ - "std-shims/std", - "thiserror", - "zeroize/std", - "rand_core/std", -] -default = ["std"] diff --git a/monero-seed/LICENSE b/monero-seed/LICENSE deleted file mode 100644 index 91d893c119..0000000000 --- a/monero-seed/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -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. diff --git a/monero-seed/README.md b/monero-seed/README.md deleted file mode 100644 index dded413315..0000000000 --- a/monero-seed/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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). diff --git a/monero-seed/src/lib.rs b/monero-seed/src/lib.rs deleted file mode 100644 index fa1618274b..0000000000 --- a/monero-seed/src/lib.rs +++ /dev/null @@ -1,400 +0,0 @@ -#![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 { - Zeroizing::new(word.chars().take(len).collect()) -} - -struct WordList { - word_list: &'static [&'static str], - word_map: HashMap<&'static str, usize>, - trimmed_word_map: HashMap, - 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> = 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], 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) -> 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, SeedError> { - // get seed words - let words = words - .split_whitespace() - .map(|w| Zeroizing::new(w.to_string())) - .collect::>(); - 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); - -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(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) -> Result { - 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 { - 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 { - self.1.clone() - } - - /// Return the entropy underlying this seed. - pub fn entropy(&self) -> Zeroizing<[u8; 32]> { - seed_to_bytes(self.0, &self.1).unwrap() - } -} diff --git a/monero-seed/src/tests.rs b/monero-seed/src/tests.rs deleted file mode 100644 index 152616ed53..0000000000 --- a/monero-seed/src/tests.rs +++ /dev/null @@ -1,257 +0,0 @@ -use rand_core::OsRng; -use zeroize::Zeroizing; - -use curve25519_dalek::scalar::Scalar; - -use monero_oxide::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::>() - .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::::from(Scalar::from_canonical_bytes(*seed.entropy())), - Option::::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() - ); - } - } -} diff --git a/monero-seed/src/words/ang.rs b/monero-seed/src/words/ang.rs deleted file mode 100644 index 2800b1a999..0000000000 --- a/monero-seed/src/words/ang.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "like", - "just", - "love", - "know", - "never", - "want", - "time", - "out", - "there", - "make", - "look", - "eye", - "down", - "only", - "think", - "heart", - "back", - "then", - "into", - "about", - "more", - "away", - "still", - "them", - "take", - "thing", - "even", - "through", - "long", - "always", - "world", - "too", - "friend", - "tell", - "try", - "hand", - "thought", - "over", - "here", - "other", - "need", - "smile", - "again", - "much", - "cry", - "been", - "night", - "ever", - "little", - "said", - "end", - "some", - "those", - "around", - "mind", - "people", - "girl", - "leave", - "dream", - "left", - "turn", - "myself", - "give", - "nothing", - "really", - "off", - "before", - "something", - "find", - "walk", - "wish", - "good", - "once", - "place", - "ask", - "stop", - "keep", - "watch", - "seem", - "everything", - "wait", - "got", - "yet", - "made", - "remember", - "start", - "alone", - "run", - "hope", - "maybe", - "believe", - "body", - "hate", - "after", - "close", - "talk", - "stand", - "own", - "each", - "hurt", - "help", - "home", - "god", - "soul", - "new", - "many", - "two", - "inside", - "should", - "true", - "first", - "fear", - "mean", - "better", - "play", - "another", - "gone", - "change", - "use", - "wonder", - "someone", - "hair", - "cold", - "open", - "best", - "any", - "behind", - "happen", - "water", - "dark", - "laugh", - "stay", - "forever", - "name", - "work", - "show", - "sky", - "break", - "came", - "deep", - "door", - "put", - "black", - "together", - "upon", - "happy", - "such", - "great", - "white", - "matter", - "fill", - "past", - "please", - "burn", - "cause", - "enough", - "touch", - "moment", - "soon", - "voice", - "scream", - "anything", - "stare", - "sound", - "red", - "everyone", - "hide", - "kiss", - "truth", - "death", - "beautiful", - "mine", - "blood", - "broken", - "very", - "pass", - "next", - "forget", - "tree", - "wrong", - "air", - "mother", - "understand", - "lip", - "hit", - "wall", - "memory", - "sleep", - "free", - "high", - "realize", - "school", - "might", - "skin", - "sweet", - "perfect", - "blue", - "kill", - "breath", - "dance", - "against", - "fly", - "between", - "grow", - "strong", - "under", - "listen", - "bring", - "sometimes", - "speak", - "pull", - "person", - "become", - "family", - "begin", - "ground", - "real", - "small", - "father", - "sure", - "feet", - "rest", - "young", - "finally", - "land", - "across", - "today", - "different", - "guy", - "line", - "fire", - "reason", - "reach", - "second", - "slowly", - "write", - "eat", - "smell", - "mouth", - "step", - "learn", - "three", - "floor", - "promise", - "breathe", - "darkness", - "push", - "earth", - "guess", - "save", - "song", - "above", - "along", - "both", - "color", - "house", - "almost", - "sorry", - "anymore", - "brother", - "okay", - "dear", - "game", - "fade", - "already", - "apart", - "warm", - "beauty", - "heard", - "notice", - "question", - "shine", - "began", - "piece", - "whole", - "shadow", - "secret", - "street", - "within", - "finger", - "point", - "morning", - "whisper", - "child", - "moon", - "green", - "story", - "glass", - "kid", - "silence", - "since", - "soft", - "yourself", - "empty", - "shall", - "angel", - "answer", - "baby", - "bright", - "dad", - "path", - "worry", - "hour", - "drop", - "follow", - "power", - "war", - "half", - "flow", - "heaven", - "act", - "chance", - "fact", - "least", - "tired", - "children", - "near", - "quite", - "afraid", - "rise", - "sea", - "taste", - "window", - "cover", - "nice", - "trust", - "lot", - "sad", - "cool", - "force", - "peace", - "return", - "blind", - "easy", - "ready", - "roll", - "rose", - "drive", - "held", - "music", - "beneath", - "hang", - "mom", - "paint", - "emotion", - "quiet", - "clear", - "cloud", - "few", - "pretty", - "bird", - "outside", - "paper", - "picture", - "front", - "rock", - "simple", - "anyone", - "meant", - "reality", - "road", - "sense", - "waste", - "bit", - "leaf", - "thank", - "happiness", - "meet", - "men", - "smoke", - "truly", - "decide", - "self", - "age", - "book", - "form", - "alive", - "carry", - "escape", - "damn", - "instead", - "able", - "ice", - "minute", - "throw", - "catch", - "leg", - "ring", - "course", - "goodbye", - "lead", - "poem", - "sick", - "corner", - "desire", - "known", - "problem", - "remind", - "shoulder", - "suppose", - "toward", - "wave", - "drink", - "jump", - "woman", - "pretend", - "sister", - "week", - "human", - "joy", - "crack", - "grey", - "pray", - "surprise", - "dry", - "knee", - "less", - "search", - "bleed", - "caught", - "clean", - "embrace", - "future", - "king", - "son", - "sorrow", - "chest", - "hug", - "remain", - "sat", - "worth", - "blow", - "daddy", - "final", - "parent", - "tight", - "also", - "create", - "lonely", - "safe", - "cross", - "dress", - "evil", - "silent", - "bone", - "fate", - "perhaps", - "anger", - "class", - "scar", - "snow", - "tiny", - "tonight", - "continue", - "control", - "dog", - "edge", - "mirror", - "month", - "suddenly", - "comfort", - "given", - "loud", - "quickly", - "gaze", - "plan", - "rush", - "stone", - "town", - "battle", - "ignore", - "spirit", - "stood", - "stupid", - "yours", - "brown", - "build", - "dust", - "hey", - "kept", - "pay", - "phone", - "twist", - "although", - "ball", - "beyond", - "hidden", - "nose", - "taken", - "fail", - "float", - "pure", - "somehow", - "wash", - "wrap", - "angry", - "cheek", - "creature", - "forgotten", - "heat", - "rip", - "single", - "space", - "special", - "weak", - "whatever", - "yell", - "anyway", - "blame", - "job", - "choose", - "country", - "curse", - "drift", - "echo", - "figure", - "grew", - "laughter", - "neck", - "suffer", - "worse", - "yeah", - "disappear", - "foot", - "forward", - "knife", - "mess", - "somewhere", - "stomach", - "storm", - "beg", - "idea", - "lift", - "offer", - "breeze", - "field", - "five", - "often", - "simply", - "stuck", - "win", - "allow", - "confuse", - "enjoy", - "except", - "flower", - "seek", - "strength", - "calm", - "grin", - "gun", - "heavy", - "hill", - "large", - "ocean", - "shoe", - "sigh", - "straight", - "summer", - "tongue", - "accept", - "crazy", - "everyday", - "exist", - "grass", - "mistake", - "sent", - "shut", - "surround", - "table", - "ache", - "brain", - "destroy", - "heal", - "nature", - "shout", - "sign", - "stain", - "choice", - "doubt", - "glance", - "glow", - "mountain", - "queen", - "stranger", - "throat", - "tomorrow", - "city", - "either", - "fish", - "flame", - "rather", - "shape", - "spin", - "spread", - "ash", - "distance", - "finish", - "image", - "imagine", - "important", - "nobody", - "shatter", - "warmth", - "became", - "feed", - "flesh", - "funny", - "lust", - "shirt", - "trouble", - "yellow", - "attention", - "bare", - "bite", - "money", - "protect", - "amaze", - "appear", - "born", - "choke", - "completely", - "daughter", - "fresh", - "friendship", - "gentle", - "probably", - "six", - "deserve", - "expect", - "grab", - "middle", - "nightmare", - "river", - "thousand", - "weight", - "worst", - "wound", - "barely", - "bottle", - "cream", - "regret", - "relationship", - "stick", - "test", - "crush", - "endless", - "fault", - "itself", - "rule", - "spill", - "art", - "circle", - "join", - "kick", - "mask", - "master", - "passion", - "quick", - "raise", - "smooth", - "unless", - "wander", - "actually", - "broke", - "chair", - "deal", - "favorite", - "gift", - "note", - "number", - "sweat", - "box", - "chill", - "clothes", - "lady", - "mark", - "park", - "poor", - "sadness", - "tie", - "animal", - "belong", - "brush", - "consume", - "dawn", - "forest", - "innocent", - "pen", - "pride", - "stream", - "thick", - "clay", - "complete", - "count", - "draw", - "faith", - "press", - "silver", - "struggle", - "surface", - "taught", - "teach", - "wet", - "bless", - "chase", - "climb", - "enter", - "letter", - "melt", - "metal", - "movie", - "stretch", - "swing", - "vision", - "wife", - "beside", - "crash", - "forgot", - "guide", - "haunt", - "joke", - "knock", - "plant", - "pour", - "prove", - "reveal", - "steal", - "stuff", - "trip", - "wood", - "wrist", - "bother", - "bottom", - "crawl", - "crowd", - "fix", - "forgive", - "frown", - "grace", - "loose", - "lucky", - "party", - "release", - "surely", - "survive", - "teacher", - "gently", - "grip", - "speed", - "suicide", - "travel", - "treat", - "vein", - "written", - "cage", - "chain", - "conversation", - "date", - "enemy", - "however", - "interest", - "million", - "page", - "pink", - "proud", - "sway", - "themselves", - "winter", - "church", - "cruel", - "cup", - "demon", - "experience", - "freedom", - "pair", - "pop", - "purpose", - "respect", - "shoot", - "softly", - "state", - "strange", - "bar", - "birth", - "curl", - "dirt", - "excuse", - "lord", - "lovely", - "monster", - "order", - "pack", - "pants", - "pool", - "scene", - "seven", - "shame", - "slide", - "ugly", - "among", - "blade", - "blonde", - "closet", - "creek", - "deny", - "drug", - "eternity", - "gain", - "grade", - "handle", - "key", - "linger", - "pale", - "prepare", - "swallow", - "swim", - "tremble", - "wheel", - "won", - "cast", - "cigarette", - "claim", - "college", - "direction", - "dirty", - "gather", - "ghost", - "hundred", - "loss", - "lung", - "orange", - "present", - "swear", - "swirl", - "twice", - "wild", - "bitter", - "blanket", - "doctor", - "everywhere", - "flash", - "grown", - "knowledge", - "numb", - "pressure", - "radio", - "repeat", - "ruin", - "spend", - "unknown", - "buy", - "clock", - "devil", - "early", - "false", - "fantasy", - "pound", - "precious", - "refuse", - "sheet", - "teeth", - "welcome", - "add", - "ahead", - "block", - "bury", - "caress", - "content", - "depth", - "despite", - "distant", - "marry", - "purple", - "threw", - "whenever", - "bomb", - "dull", - "easily", - "grasp", - "hospital", - "innocence", - "normal", - "receive", - "reply", - "rhyme", - "shade", - "someday", - "sword", - "toe", - "visit", - "asleep", - "bought", - "center", - "consider", - "flat", - "hero", - "history", - "ink", - "insane", - "muscle", - "mystery", - "pocket", - "reflection", - "shove", - "silently", - "smart", - "soldier", - "spot", - "stress", - "train", - "type", - "view", - "whether", - "bus", - "energy", - "explain", - "holy", - "hunger", - "inch", - "magic", - "mix", - "noise", - "nowhere", - "prayer", - "presence", - "shock", - "snap", - "spider", - "study", - "thunder", - "trail", - "admit", - "agree", - "bag", - "bang", - "bound", - "butterfly", - "cute", - "exactly", - "explode", - "familiar", - "fold", - "further", - "pierce", - "reflect", - "scent", - "selfish", - "sharp", - "sink", - "spring", - "stumble", - "universe", - "weep", - "women", - "wonderful", - "action", - "ancient", - "attempt", - "avoid", - "birthday", - "branch", - "chocolate", - "core", - "depress", - "drunk", - "especially", - "focus", - "fruit", - "honest", - "match", - "palm", - "perfectly", - "pillow", - "pity", - "poison", - "roar", - "shift", - "slightly", - "thump", - "truck", - "tune", - "twenty", - "unable", - "wipe", - "wrote", - "coat", - "constant", - "dinner", - "drove", - "egg", - "eternal", - "flight", - "flood", - "frame", - "freak", - "gasp", - "glad", - "hollow", - "motion", - "peer", - "plastic", - "root", - "screen", - "season", - "sting", - "strike", - "team", - "unlike", - "victim", - "volume", - "warn", - "weird", - "attack", - "await", - "awake", - "built", - "charm", - "crave", - "despair", - "fought", - "grant", - "grief", - "horse", - "limit", - "message", - "ripple", - "sanity", - "scatter", - "serve", - "split", - "string", - "trick", - "annoy", - "blur", - "boat", - "brave", - "clearly", - "cling", - "connect", - "fist", - "forth", - "imagination", - "iron", - "jock", - "judge", - "lesson", - "milk", - "misery", - "nail", - "naked", - "ourselves", - "poet", - "possible", - "princess", - "sail", - "size", - "snake", - "society", - "stroke", - "torture", - "toss", - "trace", - "wise", - "bloom", - "bullet", - "cell", - "check", - "cost", - "darling", - "during", - "footstep", - "fragile", - "hallway", - "hardly", - "horizon", - "invisible", - "journey", - "midnight", - "mud", - "nod", - "pause", - "relax", - "shiver", - "sudden", - "value", - "youth", - "abuse", - "admire", - "blink", - "breast", - "bruise", - "constantly", - "couple", - "creep", - "curve", - "difference", - "dumb", - "emptiness", - "gotta", - "honor", - "plain", - "planet", - "recall", - "rub", - "ship", - "slam", - "soar", - "somebody", - "tightly", - "weather", - "adore", - "approach", - "bond", - "bread", - "burst", - "candle", - "coffee", - "cousin", - "crime", - "desert", - "flutter", - "frozen", - "grand", - "heel", - "hello", - "language", - "level", - "movement", - "pleasure", - "powerful", - "random", - "rhythm", - "settle", - "silly", - "slap", - "sort", - "spoken", - "steel", - "threaten", - "tumble", - "upset", - "aside", - "awkward", - "bee", - "blank", - "board", - "button", - "card", - "carefully", - "complain", - "crap", - "deeply", - "discover", - "drag", - "dread", - "effort", - "entire", - "fairy", - "giant", - "gotten", - "greet", - "illusion", - "jeans", - "leap", - "liquid", - "march", - "mend", - "nervous", - "nine", - "replace", - "rope", - "spine", - "stole", - "terror", - "accident", - "apple", - "balance", - "boom", - "childhood", - "collect", - "demand", - "depression", - "eventually", - "faint", - "glare", - "goal", - "group", - "honey", - "kitchen", - "laid", - "limb", - "machine", - "mere", - "mold", - "murder", - "nerve", - "painful", - "poetry", - "prince", - "rabbit", - "shelter", - "shore", - "shower", - "soothe", - "stair", - "steady", - "sunlight", - "tangle", - "tease", - "treasure", - "uncle", - "begun", - "bliss", - "canvas", - "cheer", - "claw", - "clutch", - "commit", - "crimson", - "crystal", - "delight", - "doll", - "existence", - "express", - "fog", - "football", - "gay", - "goose", - "guard", - "hatred", - "illuminate", - "mass", - "math", - "mourn", - "rich", - "rough", - "skip", - "stir", - "student", - "style", - "support", - "thorn", - "tough", - "yard", - "yearn", - "yesterday", - "advice", - "appreciate", - "autumn", - "bank", - "beam", - "bowl", - "capture", - "carve", - "collapse", - "confusion", - "creation", - "dove", - "feather", - "girlfriend", - "glory", - "government", - "harsh", - "hop", - "inner", - "loser", - "moonlight", - "neighbor", - "neither", - "peach", - "pig", - "praise", - "screw", - "shield", - "shimmer", - "sneak", - "stab", - "subject", - "throughout", - "thrown", - "tower", - "twirl", - "wow", - "army", - "arrive", - "bathroom", - "bump", - "cease", - "cookie", - "couch", - "courage", - "dim", - "guilt", - "howl", - "hum", - "husband", - "insult", - "led", - "lunch", - "mock", - "mostly", - "natural", - "nearly", - "needle", - "nerd", - "peaceful", - "perfection", - "pile", - "price", - "remove", - "roam", - "sanctuary", - "serious", - "shiny", - "shook", - "sob", - "stolen", - "tap", - "vain", - "void", - "warrior", - "wrinkle", - "affection", - "apologize", - "blossom", - "bounce", - "bridge", - "cheap", - "crumble", - "decision", - "descend", - "desperately", - "dig", - "dot", - "flip", - "frighten", - "heartbeat", - "huge", - "lazy", - "lick", - "odd", - "opinion", - "process", - "puzzle", - "quietly", - "retreat", - "score", - "sentence", - "separate", - "situation", - "skill", - "soak", - "square", - "stray", - "taint", - "task", - "tide", - "underneath", - "veil", - "whistle", - "anywhere", - "bedroom", - "bid", - "bloody", - "burden", - "careful", - "compare", - "concern", - "curtain", - "decay", - "defeat", - "describe", - "double", - "dreamer", - "driver", - "dwell", - "evening", - "flare", - "flicker", - "grandma", - "guitar", - "harm", - "horrible", - "hungry", - "indeed", - "lace", - "melody", - "monkey", - "nation", - "object", - "obviously", - "rainbow", - "salt", - "scratch", - "shown", - "shy", - "stage", - "stun", - "third", - "tickle", - "useless", - "weakness", - "worship", - "worthless", - "afternoon", - "beard", - "boyfriend", - "bubble", - "busy", - "certain", - "chin", - "concrete", - "desk", - "diamond", - "doom", - "drawn", - "due", - "felicity", - "freeze", - "frost", - "garden", - "glide", - "harmony", - "hopefully", - "hunt", - "jealous", - "lightning", - "mama", - "mercy", - "peel", - "physical", - "position", - "pulse", - "punch", - "quit", - "rant", - "respond", - "salty", - "sane", - "satisfy", - "savior", - "sheep", - "slept", - "social", - "sport", - "tuck", - "utter", - "valley", - "wolf", - "aim", - "alas", - "alter", - "arrow", - "awaken", - "beaten", - "belief", - "brand", - "ceiling", - "cheese", - "clue", - "confidence", - "connection", - "daily", - "disguise", - "eager", - "erase", - "essence", - "everytime", - "expression", - "fan", - "flag", - "flirt", - "foul", - "fur", - "giggle", - "glorious", - "ignorance", - "law", - "lifeless", - "measure", - "mighty", - "muse", - "north", - "opposite", - "paradise", - "patience", - "patient", - "pencil", - "petal", - "plate", - "ponder", - "possibly", - "practice", - "slice", - "spell", - "stock", - "strife", - "strip", - "suffocate", - "suit", - "tender", - "tool", - "trade", - "velvet", - "verse", - "waist", - "witch", - "aunt", - "bench", - "bold", - "cap", - "certainly", - "click", - "companion", - "creator", - "dart", - "delicate", - "determine", - "dish", - "dragon", - "drama", - "drum", - "dude", - "everybody", - "feast", - "forehead", - "former", - "fright", - "fully", - "gas", - "hook", - "hurl", - "invite", - "juice", - "manage", - "moral", - "possess", - "raw", - "rebel", - "royal", - "scale", - "scary", - "several", - "slight", - "stubborn", - "swell", - "talent", - "tea", - "terrible", - "thread", - "torment", - "trickle", - "usually", - "vast", - "violence", - "weave", - "acid", - "agony", - "ashamed", - "awe", - "belly", - "blend", - "blush", - "character", - "cheat", - "common", - "company", - "coward", - "creak", - "danger", - "deadly", - "defense", - "define", - "depend", - "desperate", - "destination", - "dew", - "duck", - "dusty", - "embarrass", - "engine", - "example", - "explore", - "foe", - "freely", - "frustrate", - "generation", - "glove", - "guilty", - "health", - "hurry", - "idiot", - "impossible", - "inhale", - "jaw", - "kingdom", - "mention", - "mist", - "moan", - "mumble", - "mutter", - "observe", - "ode", - "pathetic", - "pattern", - "pie", - "prefer", - "puff", - "rape", - "rare", - "revenge", - "rude", - "scrape", - "spiral", - "squeeze", - "strain", - "sunset", - "suspend", - "sympathy", - "thigh", - "throne", - "total", - "unseen", - "weapon", - "weary" -] diff --git a/monero-seed/src/words/de.rs b/monero-seed/src/words/de.rs deleted file mode 100644 index 85dee08199..0000000000 --- a/monero-seed/src/words/de.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "Abakus", - "Abart", - "abbilden", - "Abbruch", - "Abdrift", - "Abendrot", - "Abfahrt", - "abfeuern", - "Abflug", - "abfragen", - "Abglanz", - "abhärten", - "abheben", - "Abhilfe", - "Abitur", - "Abkehr", - "Ablauf", - "ablecken", - "Ablösung", - "Abnehmer", - "abnutzen", - "Abonnent", - "Abrasion", - "Abrede", - "abrüsten", - "Absicht", - "Absprung", - "Abstand", - "absuchen", - "Abteil", - "Abundanz", - "abwarten", - "Abwurf", - "Abzug", - "Achse", - "Achtung", - "Acker", - "Aderlass", - "Adler", - "Admiral", - "Adresse", - "Affe", - "Affront", - "Afrika", - "Aggregat", - "Agilität", - "ähneln", - "Ahnung", - "Ahorn", - "Akazie", - "Akkord", - "Akrobat", - "Aktfoto", - "Aktivist", - "Albatros", - "Alchimie", - "Alemanne", - "Alibi", - "Alkohol", - "Allee", - "Allüre", - "Almosen", - "Almweide", - "Aloe", - "Alpaka", - "Alpental", - "Alphabet", - "Alpinist", - "Alraune", - "Altbier", - "Alter", - "Altflöte", - "Altruist", - "Alublech", - "Aludose", - "Amateur", - "Amazonas", - "Ameise", - "Amnesie", - "Amok", - "Ampel", - "Amphibie", - "Ampulle", - "Amsel", - "Amulett", - "Anakonda", - "Analogie", - "Ananas", - "Anarchie", - "Anatomie", - "Anbau", - "Anbeginn", - "anbieten", - "Anblick", - "ändern", - "andocken", - "Andrang", - "anecken", - "Anflug", - "Anfrage", - "Anführer", - "Angebot", - "Angler", - "Anhalter", - "Anhöhe", - "Animator", - "Anis", - "Anker", - "ankleben", - "Ankunft", - "Anlage", - "anlocken", - "Anmut", - "Annahme", - "Anomalie", - "Anonymus", - "Anorak", - "anpeilen", - "Anrecht", - "Anruf", - "Ansage", - "Anschein", - "Ansicht", - "Ansporn", - "Anteil", - "Antlitz", - "Antrag", - "Antwort", - "Anwohner", - "Aorta", - "Apfel", - "Appetit", - "Applaus", - "Aquarium", - "Arbeit", - "Arche", - "Argument", - "Arktis", - "Armband", - "Aroma", - "Asche", - "Askese", - "Asphalt", - "Asteroid", - "Ästhetik", - "Astronom", - "Atelier", - "Athlet", - "Atlantik", - "Atmung", - "Audienz", - "aufatmen", - "Auffahrt", - "aufholen", - "aufregen", - "Aufsatz", - "Auftritt", - "Aufwand", - "Augapfel", - "Auktion", - "Ausbruch", - "Ausflug", - "Ausgabe", - "Aushilfe", - "Ausland", - "Ausnahme", - "Aussage", - "Autobahn", - "Avocado", - "Axthieb", - "Bach", - "backen", - "Badesee", - "Bahnhof", - "Balance", - "Balkon", - "Ballett", - "Balsam", - "Banane", - "Bandage", - "Bankett", - "Barbar", - "Barde", - "Barett", - "Bargeld", - "Barkasse", - "Barriere", - "Bart", - "Bass", - "Bastler", - "Batterie", - "Bauch", - "Bauer", - "Bauholz", - "Baujahr", - "Baum", - "Baustahl", - "Bauteil", - "Bauweise", - "Bazar", - "beachten", - "Beatmung", - "beben", - "Becher", - "Becken", - "bedanken", - "beeilen", - "beenden", - "Beere", - "befinden", - "Befreier", - "Begabung", - "Begierde", - "begrüßen", - "Beiboot", - "Beichte", - "Beifall", - "Beigabe", - "Beil", - "Beispiel", - "Beitrag", - "beizen", - "bekommen", - "beladen", - "Beleg", - "bellen", - "belohnen", - "Bemalung", - "Bengel", - "Benutzer", - "Benzin", - "beraten", - "Bereich", - "Bergluft", - "Bericht", - "Bescheid", - "Besitz", - "besorgen", - "Bestand", - "Besuch", - "betanken", - "beten", - "betören", - "Bett", - "Beule", - "Beute", - "Bewegung", - "bewirken", - "Bewohner", - "bezahlen", - "Bezug", - "biegen", - "Biene", - "Bierzelt", - "bieten", - "Bikini", - "Bildung", - "Billard", - "binden", - "Biobauer", - "Biologe", - "Bionik", - "Biotop", - "Birke", - "Bison", - "Bitte", - "Biwak", - "Bizeps", - "blasen", - "Blatt", - "Blauwal", - "Blende", - "Blick", - "Blitz", - "Blockade", - "Blödelei", - "Blondine", - "Blues", - "Blume", - "Blut", - "Bodensee", - "Bogen", - "Boje", - "Bollwerk", - "Bonbon", - "Bonus", - "Boot", - "Bordarzt", - "Börse", - "Böschung", - "Boudoir", - "Boxkampf", - "Boykott", - "Brahms", - "Brandung", - "Brauerei", - "Brecher", - "Breitaxt", - "Bremse", - "brennen", - "Brett", - "Brief", - "Brigade", - "Brillanz", - "bringen", - "brodeln", - "Brosche", - "Brötchen", - "Brücke", - "Brunnen", - "Brüste", - "Brutofen", - "Buch", - "Büffel", - "Bugwelle", - "Bühne", - "Buletten", - "Bullauge", - "Bumerang", - "bummeln", - "Buntglas", - "Bürde", - "Burgherr", - "Bursche", - "Busen", - "Buslinie", - "Bussard", - "Butangas", - "Butter", - "Cabrio", - "campen", - "Captain", - "Cartoon", - "Cello", - "Chalet", - "Charisma", - "Chefarzt", - "Chiffon", - "Chipsatz", - "Chirurg", - "Chor", - "Chronik", - "Chuzpe", - "Clubhaus", - "Cockpit", - "Codewort", - "Cognac", - "Coladose", - "Computer", - "Coupon", - "Cousin", - "Cracking", - "Crash", - "Curry", - "Dach", - "Dackel", - "daddeln", - "daliegen", - "Dame", - "Dammbau", - "Dämon", - "Dampflok", - "Dank", - "Darm", - "Datei", - "Datsche", - "Datteln", - "Datum", - "Dauer", - "Daunen", - "Deckel", - "Decoder", - "Defekt", - "Degen", - "Dehnung", - "Deiche", - "Dekade", - "Dekor", - "Delfin", - "Demut", - "denken", - "Deponie", - "Design", - "Desktop", - "Dessert", - "Detail", - "Detektiv", - "Dezibel", - "Diadem", - "Diagnose", - "Dialekt", - "Diamant", - "Dichter", - "Dickicht", - "Diesel", - "Diktat", - "Diplom", - "Direktor", - "Dirne", - "Diskurs", - "Distanz", - "Docht", - "Dohle", - "Dolch", - "Domäne", - "Donner", - "Dorade", - "Dorf", - "Dörrobst", - "Dorsch", - "Dossier", - "Dozent", - "Drachen", - "Draht", - "Drama", - "Drang", - "Drehbuch", - "Dreieck", - "Dressur", - "Drittel", - "Drossel", - "Druck", - "Duell", - "Duft", - "Düne", - "Dünung", - "dürfen", - "Duschbad", - "Düsenjet", - "Dynamik", - "Ebbe", - "Echolot", - "Echse", - "Eckball", - "Edding", - "Edelweiß", - "Eden", - "Edition", - "Efeu", - "Effekte", - "Egoismus", - "Ehre", - "Eiablage", - "Eiche", - "Eidechse", - "Eidotter", - "Eierkopf", - "Eigelb", - "Eiland", - "Eilbote", - "Eimer", - "einatmen", - "Einband", - "Eindruck", - "Einfall", - "Eingang", - "Einkauf", - "einladen", - "Einöde", - "Einrad", - "Eintopf", - "Einwurf", - "Einzug", - "Eisbär", - "Eisen", - "Eishöhle", - "Eismeer", - "Eiweiß", - "Ekstase", - "Elan", - "Elch", - "Elefant", - "Eleganz", - "Element", - "Elfe", - "Elite", - "Elixier", - "Ellbogen", - "Eloquenz", - "Emigrant", - "Emission", - "Emotion", - "Empathie", - "Empfang", - "Endzeit", - "Energie", - "Engpass", - "Enkel", - "Enklave", - "Ente", - "entheben", - "Entität", - "entladen", - "Entwurf", - "Episode", - "Epoche", - "erachten", - "Erbauer", - "erblühen", - "Erdbeere", - "Erde", - "Erdgas", - "Erdkunde", - "Erdnuss", - "Erdöl", - "Erdteil", - "Ereignis", - "Eremit", - "erfahren", - "Erfolg", - "erfreuen", - "erfüllen", - "Ergebnis", - "erhitzen", - "erkalten", - "erkennen", - "erleben", - "Erlösung", - "ernähren", - "erneuern", - "Ernte", - "Eroberer", - "eröffnen", - "Erosion", - "Erotik", - "Erpel", - "erraten", - "Erreger", - "erröten", - "Ersatz", - "Erstflug", - "Ertrag", - "Eruption", - "erwarten", - "erwidern", - "Erzbau", - "Erzeuger", - "erziehen", - "Esel", - "Eskimo", - "Eskorte", - "Espe", - "Espresso", - "essen", - "Etage", - "Etappe", - "Etat", - "Ethik", - "Etikett", - "Etüde", - "Eule", - "Euphorie", - "Europa", - "Everest", - "Examen", - "Exil", - "Exodus", - "Extrakt", - "Fabel", - "Fabrik", - "Fachmann", - "Fackel", - "Faden", - "Fagott", - "Fahne", - "Faible", - "Fairness", - "Fakt", - "Fakultät", - "Falke", - "Fallobst", - "Fälscher", - "Faltboot", - "Familie", - "Fanclub", - "Fanfare", - "Fangarm", - "Fantasie", - "Farbe", - "Farmhaus", - "Farn", - "Fasan", - "Faser", - "Fassung", - "fasten", - "Faulheit", - "Fauna", - "Faust", - "Favorit", - "Faxgerät", - "Fazit", - "fechten", - "Federboa", - "Fehler", - "Feier", - "Feige", - "feilen", - "Feinripp", - "Feldbett", - "Felge", - "Fellpony", - "Felswand", - "Ferien", - "Ferkel", - "Fernweh", - "Ferse", - "Fest", - "Fettnapf", - "Feuer", - "Fiasko", - "Fichte", - "Fiktion", - "Film", - "Filter", - "Filz", - "Finanzen", - "Findling", - "Finger", - "Fink", - "Finnwal", - "Fisch", - "Fitness", - "Fixpunkt", - "Fixstern", - "Fjord", - "Flachbau", - "Flagge", - "Flamenco", - "Flanke", - "Flasche", - "Flaute", - "Fleck", - "Flegel", - "flehen", - "Fleisch", - "fliegen", - "Flinte", - "Flirt", - "Flocke", - "Floh", - "Floskel", - "Floß", - "Flöte", - "Flugzeug", - "Flunder", - "Flusstal", - "Flutung", - "Fockmast", - "Fohlen", - "Föhnlage", - "Fokus", - "folgen", - "Foliant", - "Folklore", - "Fontäne", - "Förde", - "Forelle", - "Format", - "Forscher", - "Fortgang", - "Forum", - "Fotograf", - "Frachter", - "Fragment", - "Fraktion", - "fräsen", - "Frauenpo", - "Freak", - "Fregatte", - "Freiheit", - "Freude", - "Frieden", - "Frohsinn", - "Frosch", - "Frucht", - "Frühjahr", - "Fuchs", - "Fügung", - "fühlen", - "Füller", - "Fundbüro", - "Funkboje", - "Funzel", - "Furnier", - "Fürsorge", - "Fusel", - "Fußbad", - "Futteral", - "Gabelung", - "gackern", - "Gage", - "gähnen", - "Galaxie", - "Galeere", - "Galopp", - "Gameboy", - "Gamsbart", - "Gandhi", - "Gang", - "Garage", - "Gardine", - "Garküche", - "Garten", - "Gasthaus", - "Gattung", - "gaukeln", - "Gazelle", - "Gebäck", - "Gebirge", - "Gebräu", - "Geburt", - "Gedanke", - "Gedeck", - "Gedicht", - "Gefahr", - "Gefieder", - "Geflügel", - "Gefühl", - "Gegend", - "Gehirn", - "Gehöft", - "Gehweg", - "Geige", - "Geist", - "Gelage", - "Geld", - "Gelenk", - "Gelübde", - "Gemälde", - "Gemeinde", - "Gemüse", - "genesen", - "Genuss", - "Gepäck", - "Geranie", - "Gericht", - "Germane", - "Geruch", - "Gesang", - "Geschenk", - "Gesetz", - "Gesindel", - "Gesöff", - "Gespan", - "Gestade", - "Gesuch", - "Getier", - "Getränk", - "Getümmel", - "Gewand", - "Geweih", - "Gewitter", - "Gewölbe", - "Geysir", - "Giftzahn", - "Gipfel", - "Giraffe", - "Gitarre", - "glänzen", - "Glasauge", - "Glatze", - "Gleis", - "Globus", - "Glück", - "glühen", - "Glutofen", - "Goldzahn", - "Gondel", - "gönnen", - "Gottheit", - "graben", - "Grafik", - "Grashalm", - "Graugans", - "greifen", - "Grenze", - "grillen", - "Groschen", - "Grotte", - "Grube", - "Grünalge", - "Gruppe", - "gruseln", - "Gulasch", - "Gummibär", - "Gurgel", - "Gürtel", - "Güterzug", - "Haarband", - "Habicht", - "hacken", - "hadern", - "Hafen", - "Hagel", - "Hähnchen", - "Haifisch", - "Haken", - "Halbaffe", - "Halsader", - "halten", - "Halunke", - "Handbuch", - "Hanf", - "Harfe", - "Harnisch", - "härten", - "Harz", - "Hasenohr", - "Haube", - "hauchen", - "Haupt", - "Haut", - "Havarie", - "Hebamme", - "hecheln", - "Heck", - "Hedonist", - "Heiler", - "Heimat", - "Heizung", - "Hektik", - "Held", - "helfen", - "Helium", - "Hemd", - "hemmen", - "Hengst", - "Herd", - "Hering", - "Herkunft", - "Hermelin", - "Herrchen", - "Herzdame", - "Heulboje", - "Hexe", - "Hilfe", - "Himbeere", - "Himmel", - "Hingabe", - "hinhören", - "Hinweis", - "Hirsch", - "Hirte", - "Hitzkopf", - "Hobel", - "Hochform", - "Hocker", - "hoffen", - "Hofhund", - "Hofnarr", - "Höhenzug", - "Hohlraum", - "Hölle", - "Holzboot", - "Honig", - "Honorar", - "horchen", - "Hörprobe", - "Höschen", - "Hotel", - "Hubraum", - "Hufeisen", - "Hügel", - "huldigen", - "Hülle", - "Humbug", - "Hummer", - "Humor", - "Hund", - "Hunger", - "Hupe", - "Hürde", - "Hurrikan", - "Hydrant", - "Hypnose", - "Ibis", - "Idee", - "Idiot", - "Igel", - "Illusion", - "Imitat", - "impfen", - "Import", - "Inferno", - "Ingwer", - "Inhalte", - "Inland", - "Insekt", - "Ironie", - "Irrfahrt", - "Irrtum", - "Isolator", - "Istwert", - "Jacke", - "Jade", - "Jagdhund", - "Jäger", - "Jaguar", - "Jahr", - "Jähzorn", - "Jazzfest", - "Jetpilot", - "jobben", - "Jochbein", - "jodeln", - "Jodsalz", - "Jolle", - "Journal", - "Jubel", - "Junge", - "Junimond", - "Jupiter", - "Jutesack", - "Juwel", - "Kabarett", - "Kabine", - "Kabuff", - "Käfer", - "Kaffee", - "Kahlkopf", - "Kaimauer", - "Kajüte", - "Kaktus", - "Kaliber", - "Kaltluft", - "Kamel", - "kämmen", - "Kampagne", - "Kanal", - "Känguru", - "Kanister", - "Kanone", - "Kante", - "Kanu", - "kapern", - "Kapitän", - "Kapuze", - "Karneval", - "Karotte", - "Käsebrot", - "Kasper", - "Kastanie", - "Katalog", - "Kathode", - "Katze", - "kaufen", - "Kaugummi", - "Kauz", - "Kehle", - "Keilerei", - "Keksdose", - "Kellner", - "Keramik", - "Kerze", - "Kessel", - "Kette", - "keuchen", - "kichern", - "Kielboot", - "Kindheit", - "Kinnbart", - "Kinosaal", - "Kiosk", - "Kissen", - "Klammer", - "Klang", - "Klapprad", - "Klartext", - "kleben", - "Klee", - "Kleinod", - "Klima", - "Klingel", - "Klippe", - "Klischee", - "Kloster", - "Klugheit", - "Klüngel", - "kneten", - "Knie", - "Knöchel", - "knüpfen", - "Kobold", - "Kochbuch", - "Kohlrabi", - "Koje", - "Kokosöl", - "Kolibri", - "Kolumne", - "Kombüse", - "Komiker", - "kommen", - "Konto", - "Konzept", - "Kopfkino", - "Kordhose", - "Korken", - "Korsett", - "Kosename", - "Krabbe", - "Krach", - "Kraft", - "Krähe", - "Kralle", - "Krapfen", - "Krater", - "kraulen", - "Kreuz", - "Krokodil", - "Kröte", - "Kugel", - "Kuhhirt", - "Kühnheit", - "Künstler", - "Kurort", - "Kurve", - "Kurzfilm", - "kuscheln", - "küssen", - "Kutter", - "Labor", - "lachen", - "Lackaffe", - "Ladeluke", - "Lagune", - "Laib", - "Lakritze", - "Lammfell", - "Land", - "Langmut", - "Lappalie", - "Last", - "Laterne", - "Latzhose", - "Laubsäge", - "laufen", - "Laune", - "Lausbub", - "Lavasee", - "Leben", - "Leder", - "Leerlauf", - "Lehm", - "Lehrer", - "leihen", - "Lektüre", - "Lenker", - "Lerche", - "Leseecke", - "Leuchter", - "Lexikon", - "Libelle", - "Libido", - "Licht", - "Liebe", - "liefern", - "Liftboy", - "Limonade", - "Lineal", - "Linoleum", - "List", - "Liveband", - "Lobrede", - "locken", - "Löffel", - "Logbuch", - "Logik", - "Lohn", - "Loipe", - "Lokal", - "Lorbeer", - "Lösung", - "löten", - "Lottofee", - "Löwe", - "Luchs", - "Luder", - "Luftpost", - "Luke", - "Lümmel", - "Lunge", - "lutschen", - "Luxus", - "Macht", - "Magazin", - "Magier", - "Magnet", - "mähen", - "Mahlzeit", - "Mahnmal", - "Maibaum", - "Maisbrei", - "Makel", - "malen", - "Mammut", - "Maniküre", - "Mantel", - "Marathon", - "Marder", - "Marine", - "Marke", - "Marmor", - "Märzluft", - "Maske", - "Maßanzug", - "Maßkrug", - "Mastkorb", - "Material", - "Matratze", - "Mauerbau", - "Maulkorb", - "Mäuschen", - "Mäzen", - "Medium", - "Meinung", - "melden", - "Melodie", - "Mensch", - "Merkmal", - "Messe", - "Metall", - "Meteor", - "Methode", - "Metzger", - "Mieze", - "Milchkuh", - "Mimose", - "Minirock", - "Minute", - "mischen", - "Missetat", - "mitgehen", - "Mittag", - "Mixtape", - "Möbel", - "Modul", - "mögen", - "Möhre", - "Molch", - "Moment", - "Monat", - "Mondflug", - "Monitor", - "Monokini", - "Monster", - "Monument", - "Moorhuhn", - "Moos", - "Möpse", - "Moral", - "Mörtel", - "Motiv", - "Motorrad", - "Möwe", - "Mühe", - "Mulatte", - "Müller", - "Mumie", - "Mund", - "Münze", - "Muschel", - "Muster", - "Mythos", - "Nabel", - "Nachtzug", - "Nackedei", - "Nagel", - "Nähe", - "Nähnadel", - "Namen", - "Narbe", - "Narwal", - "Nasenbär", - "Natur", - "Nebel", - "necken", - "Neffe", - "Neigung", - "Nektar", - "Nenner", - "Neptun", - "Nerz", - "Nessel", - "Nestbau", - "Netz", - "Neubau", - "Neuerung", - "Neugier", - "nicken", - "Niere", - "Nilpferd", - "nisten", - "Nocke", - "Nomade", - "Nordmeer", - "Notdurft", - "Notstand", - "Notwehr", - "Nudismus", - "Nuss", - "Nutzhanf", - "Oase", - "Obdach", - "Oberarzt", - "Objekt", - "Oboe", - "Obsthain", - "Ochse", - "Odyssee", - "Ofenholz", - "öffnen", - "Ohnmacht", - "Ohrfeige", - "Ohrwurm", - "Ökologie", - "Oktave", - "Ölberg", - "Olive", - "Ölkrise", - "Omelett", - "Onkel", - "Oper", - "Optiker", - "Orange", - "Orchidee", - "ordnen", - "Orgasmus", - "Orkan", - "Ortskern", - "Ortung", - "Ostasien", - "Ozean", - "Paarlauf", - "Packeis", - "paddeln", - "Paket", - "Palast", - "Pandabär", - "Panik", - "Panorama", - "Panther", - "Papagei", - "Papier", - "Paprika", - "Paradies", - "Parka", - "Parodie", - "Partner", - "Passant", - "Patent", - "Patzer", - "Pause", - "Pavian", - "Pedal", - "Pegel", - "peilen", - "Perle", - "Person", - "Pfad", - "Pfau", - "Pferd", - "Pfleger", - "Physik", - "Pier", - "Pilotwal", - "Pinzette", - "Piste", - "Plakat", - "Plankton", - "Platin", - "Plombe", - "plündern", - "Pobacke", - "Pokal", - "polieren", - "Popmusik", - "Porträt", - "Posaune", - "Postamt", - "Pottwal", - "Pracht", - "Pranke", - "Preis", - "Primat", - "Prinzip", - "Protest", - "Proviant", - "Prüfung", - "Pubertät", - "Pudding", - "Pullover", - "Pulsader", - "Punkt", - "Pute", - "Putsch", - "Puzzle", - "Python", - "quaken", - "Qualle", - "Quark", - "Quellsee", - "Querkopf", - "Quitte", - "Quote", - "Rabauke", - "Rache", - "Radclub", - "Radhose", - "Radio", - "Radtour", - "Rahmen", - "Rampe", - "Randlage", - "Ranzen", - "Rapsöl", - "Raserei", - "rasten", - "Rasur", - "Rätsel", - "Raubtier", - "Raumzeit", - "Rausch", - "Reaktor", - "Realität", - "Rebell", - "Rede", - "Reetdach", - "Regatta", - "Regen", - "Rehkitz", - "Reifen", - "Reim", - "Reise", - "Reizung", - "Rekord", - "Relevanz", - "Rennboot", - "Respekt", - "Restmüll", - "retten", - "Reue", - "Revolte", - "Rhetorik", - "Rhythmus", - "Richtung", - "Riegel", - "Rindvieh", - "Rippchen", - "Ritter", - "Robbe", - "Roboter", - "Rockband", - "Rohdaten", - "Roller", - "Roman", - "röntgen", - "Rose", - "Rosskur", - "Rost", - "Rotahorn", - "Rotglut", - "Rotznase", - "Rubrik", - "Rückweg", - "Rufmord", - "Ruhe", - "Ruine", - "Rumpf", - "Runde", - "Rüstung", - "rütteln", - "Saaltür", - "Saatguts", - "Säbel", - "Sachbuch", - "Sack", - "Saft", - "sagen", - "Sahneeis", - "Salat", - "Salbe", - "Salz", - "Sammlung", - "Samt", - "Sandbank", - "Sanftmut", - "Sardine", - "Satire", - "Sattel", - "Satzbau", - "Sauerei", - "Saum", - "Säure", - "Schall", - "Scheitel", - "Schiff", - "Schlager", - "Schmied", - "Schnee", - "Scholle", - "Schrank", - "Schulbus", - "Schwan", - "Seeadler", - "Seefahrt", - "Seehund", - "Seeufer", - "segeln", - "Sehnerv", - "Seide", - "Seilzug", - "Senf", - "Sessel", - "Seufzer", - "Sexgott", - "Sichtung", - "Signal", - "Silber", - "singen", - "Sinn", - "Sirup", - "Sitzbank", - "Skandal", - "Skikurs", - "Skipper", - "Skizze", - "Smaragd", - "Socke", - "Sohn", - "Sommer", - "Songtext", - "Sorte", - "Spagat", - "Spannung", - "Spargel", - "Specht", - "Speiseöl", - "Spiegel", - "Sport", - "spülen", - "Stadtbus", - "Stall", - "Stärke", - "Stativ", - "staunen", - "Stern", - "Stiftung", - "Stollen", - "Strömung", - "Sturm", - "Substanz", - "Südalpen", - "Sumpf", - "surfen", - "Tabak", - "Tafel", - "Tagebau", - "takeln", - "Taktung", - "Talsohle", - "Tand", - "Tanzbär", - "Tapir", - "Tarantel", - "Tarnname", - "Tasse", - "Tatnacht", - "Tatsache", - "Tatze", - "Taube", - "tauchen", - "Taufpate", - "Taumel", - "Teelicht", - "Teich", - "teilen", - "Tempo", - "Tenor", - "Terrasse", - "Testflug", - "Theater", - "Thermik", - "ticken", - "Tiefflug", - "Tierart", - "Tigerhai", - "Tinte", - "Tischler", - "toben", - "Toleranz", - "Tölpel", - "Tonband", - "Topf", - "Topmodel", - "Torbogen", - "Torlinie", - "Torte", - "Tourist", - "Tragesel", - "trampeln", - "Trapez", - "Traum", - "treffen", - "Trennung", - "Treue", - "Trick", - "trimmen", - "Trödel", - "Trost", - "Trumpf", - "tüfteln", - "Turban", - "Turm", - "Übermut", - "Ufer", - "Uhrwerk", - "umarmen", - "Umbau", - "Umfeld", - "Umgang", - "Umsturz", - "Unart", - "Unfug", - "Unimog", - "Unruhe", - "Unwucht", - "Uranerz", - "Urlaub", - "Urmensch", - "Utopie", - "Vakuum", - "Valuta", - "Vandale", - "Vase", - "Vektor", - "Ventil", - "Verb", - "Verdeck", - "Verfall", - "Vergaser", - "verhexen", - "Verlag", - "Vers", - "Vesper", - "Vieh", - "Viereck", - "Vinyl", - "Virus", - "Vitrine", - "Vollblut", - "Vorbote", - "Vorrat", - "Vorsicht", - "Vulkan", - "Wachstum", - "Wade", - "Wagemut", - "Wahlen", - "Wahrheit", - "Wald", - "Walhai", - "Wallach", - "Walnuss", - "Walzer", - "wandeln", - "Wanze", - "wärmen", - "Warnruf", - "Wäsche", - "Wasser", - "Weberei", - "wechseln", - "Wegegeld", - "wehren", - "Weiher", - "Weinglas", - "Weißbier", - "Weitwurf", - "Welle", - "Weltall", - "Werkbank", - "Werwolf", - "Wetter", - "wiehern", - "Wildgans", - "Wind", - "Wohl", - "Wohnort", - "Wolf", - "Wollust", - "Wortlaut", - "Wrack", - "Wunder", - "Wurfaxt", - "Wurst", - "Yacht", - "Yeti", - "Zacke", - "Zahl", - "zähmen", - "Zahnfee", - "Zäpfchen", - "Zaster", - "Zaumzeug", - "Zebra", - "zeigen", - "Zeitlupe", - "Zellkern", - "Zeltdach", - "Zensor", - "Zerfall", - "Zeug", - "Ziege", - "Zielfoto", - "Zimteis", - "Zobel", - "Zollhund", - "Zombie", - "Zöpfe", - "Zucht", - "Zufahrt", - "Zugfahrt", - "Zugvogel", - "Zündung", - "Zweck", - "Zyklop" -] diff --git a/monero-seed/src/words/en.rs b/monero-seed/src/words/en.rs deleted file mode 100644 index c6f9a454e7..0000000000 --- a/monero-seed/src/words/en.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "abbey", - "abducts", - "ability", - "ablaze", - "abnormal", - "abort", - "abrasive", - "absorb", - "abyss", - "academy", - "aces", - "aching", - "acidic", - "acoustic", - "acquire", - "across", - "actress", - "acumen", - "adapt", - "addicted", - "adept", - "adhesive", - "adjust", - "adopt", - "adrenalin", - "adult", - "adventure", - "aerial", - "afar", - "affair", - "afield", - "afloat", - "afoot", - "afraid", - "after", - "against", - "agenda", - "aggravate", - "agile", - "aglow", - "agnostic", - "agony", - "agreed", - "ahead", - "aided", - "ailments", - "aimless", - "airport", - "aisle", - "ajar", - "akin", - "alarms", - "album", - "alchemy", - "alerts", - "algebra", - "alkaline", - "alley", - "almost", - "aloof", - "alpine", - "already", - "also", - "altitude", - "alumni", - "always", - "amaze", - "ambush", - "amended", - "amidst", - "ammo", - "amnesty", - "among", - "amply", - "amused", - "anchor", - "android", - "anecdote", - "angled", - "ankle", - "annoyed", - "answers", - "antics", - "anvil", - "anxiety", - "anybody", - "apart", - "apex", - "aphid", - "aplomb", - "apology", - "apply", - "apricot", - "aptitude", - "aquarium", - "arbitrary", - "archer", - "ardent", - "arena", - "argue", - "arises", - "army", - "around", - "arrow", - "arsenic", - "artistic", - "ascend", - "ashtray", - "aside", - "asked", - "asleep", - "aspire", - "assorted", - "asylum", - "athlete", - "atlas", - "atom", - "atrium", - "attire", - "auburn", - "auctions", - "audio", - "august", - "aunt", - "austere", - "autumn", - "avatar", - "avidly", - "avoid", - "awakened", - "awesome", - "awful", - "awkward", - "awning", - "awoken", - "axes", - "axis", - "axle", - "aztec", - "azure", - "baby", - "bacon", - "badge", - "baffles", - "bagpipe", - "bailed", - "bakery", - "balding", - "bamboo", - "banjo", - "baptism", - "basin", - "batch", - "bawled", - "bays", - "because", - "beer", - "befit", - "begun", - "behind", - "being", - "below", - "bemused", - "benches", - "berries", - "bested", - "betting", - "bevel", - "beware", - "beyond", - "bias", - "bicycle", - "bids", - "bifocals", - "biggest", - "bikini", - "bimonthly", - "binocular", - "biology", - "biplane", - "birth", - "biscuit", - "bite", - "biweekly", - "blender", - "blip", - "bluntly", - "boat", - "bobsled", - "bodies", - "bogeys", - "boil", - "boldly", - "bomb", - "border", - "boss", - "both", - "bounced", - "bovine", - "bowling", - "boxes", - "boyfriend", - "broken", - "brunt", - "bubble", - "buckets", - "budget", - "buffet", - "bugs", - "building", - "bulb", - "bumper", - "bunch", - "business", - "butter", - "buying", - "buzzer", - "bygones", - "byline", - "bypass", - "cabin", - "cactus", - "cadets", - "cafe", - "cage", - "cajun", - "cake", - "calamity", - "camp", - "candy", - "casket", - "catch", - "cause", - "cavernous", - "cease", - "cedar", - "ceiling", - "cell", - "cement", - "cent", - "certain", - "chlorine", - "chrome", - "cider", - "cigar", - "cinema", - "circle", - "cistern", - "citadel", - "civilian", - "claim", - "click", - "clue", - "coal", - "cobra", - "cocoa", - "code", - "coexist", - "coffee", - "cogs", - "cohesive", - "coils", - "colony", - "comb", - "cool", - "copy", - "corrode", - "costume", - "cottage", - "cousin", - "cowl", - "criminal", - "cube", - "cucumber", - "cuddled", - "cuffs", - "cuisine", - "cunning", - "cupcake", - "custom", - "cycling", - "cylinder", - "cynical", - "dabbing", - "dads", - "daft", - "dagger", - "daily", - "damp", - "dangerous", - "dapper", - "darted", - "dash", - "dating", - "dauntless", - "dawn", - "daytime", - "dazed", - "debut", - "decay", - "dedicated", - "deepest", - "deftly", - "degrees", - "dehydrate", - "deity", - "dejected", - "delayed", - "demonstrate", - "dented", - "deodorant", - "depth", - "desk", - "devoid", - "dewdrop", - "dexterity", - "dialect", - "dice", - "diet", - "different", - "digit", - "dilute", - "dime", - "dinner", - "diode", - "diplomat", - "directed", - "distance", - "ditch", - "divers", - "dizzy", - "doctor", - "dodge", - "does", - "dogs", - "doing", - "dolphin", - "domestic", - "donuts", - "doorway", - "dormant", - "dosage", - "dotted", - "double", - "dove", - "down", - "dozen", - "dreams", - "drinks", - "drowning", - "drunk", - "drying", - "dual", - "dubbed", - "duckling", - "dude", - "duets", - "duke", - "dullness", - "dummy", - "dunes", - "duplex", - "duration", - "dusted", - "duties", - "dwarf", - "dwelt", - "dwindling", - "dying", - "dynamite", - "dyslexic", - "each", - "eagle", - "earth", - "easy", - "eating", - "eavesdrop", - "eccentric", - "echo", - "eclipse", - "economics", - "ecstatic", - "eden", - "edgy", - "edited", - "educated", - "eels", - "efficient", - "eggs", - "egotistic", - "eight", - "either", - "eject", - "elapse", - "elbow", - "eldest", - "eleven", - "elite", - "elope", - "else", - "eluded", - "emails", - "ember", - "emerge", - "emit", - "emotion", - "empty", - "emulate", - "energy", - "enforce", - "enhanced", - "enigma", - "enjoy", - "enlist", - "enmity", - "enough", - "enraged", - "ensign", - "entrance", - "envy", - "epoxy", - "equip", - "erase", - "erected", - "erosion", - "error", - "eskimos", - "espionage", - "essential", - "estate", - "etched", - "eternal", - "ethics", - "etiquette", - "evaluate", - "evenings", - "evicted", - "evolved", - "examine", - "excess", - "exhale", - "exit", - "exotic", - "exquisite", - "extra", - "exult", - "fabrics", - "factual", - "fading", - "fainted", - "faked", - "fall", - "family", - "fancy", - "farming", - "fatal", - "faulty", - "fawns", - "faxed", - "fazed", - "feast", - "february", - "federal", - "feel", - "feline", - "females", - "fences", - "ferry", - "festival", - "fetches", - "fever", - "fewest", - "fiat", - "fibula", - "fictional", - "fidget", - "fierce", - "fifteen", - "fight", - "films", - "firm", - "fishing", - "fitting", - "five", - "fixate", - "fizzle", - "fleet", - "flippant", - "flying", - "foamy", - "focus", - "foes", - "foggy", - "foiled", - "folding", - "fonts", - "foolish", - "fossil", - "fountain", - "fowls", - "foxes", - "foyer", - "framed", - "friendly", - "frown", - "fruit", - "frying", - "fudge", - "fuel", - "fugitive", - "fully", - "fuming", - "fungal", - "furnished", - "fuselage", - "future", - "fuzzy", - "gables", - "gadget", - "gags", - "gained", - "galaxy", - "gambit", - "gang", - "gasp", - "gather", - "gauze", - "gave", - "gawk", - "gaze", - "gearbox", - "gecko", - "geek", - "gels", - "gemstone", - "general", - "geometry", - "germs", - "gesture", - "getting", - "geyser", - "ghetto", - "ghost", - "giant", - "giddy", - "gifts", - "gigantic", - "gills", - "gimmick", - "ginger", - "girth", - "giving", - "glass", - "gleeful", - "glide", - "gnaw", - "gnome", - "goat", - "goblet", - "godfather", - "goes", - "goggles", - "going", - "goldfish", - "gone", - "goodbye", - "gopher", - "gorilla", - "gossip", - "gotten", - "gourmet", - "governing", - "gown", - "greater", - "grunt", - "guarded", - "guest", - "guide", - "gulp", - "gumball", - "guru", - "gusts", - "gutter", - "guys", - "gymnast", - "gypsy", - "gyrate", - "habitat", - "hacksaw", - "haggled", - "hairy", - "hamburger", - "happens", - "hashing", - "hatchet", - "haunted", - "having", - "hawk", - "haystack", - "hazard", - "hectare", - "hedgehog", - "heels", - "hefty", - "height", - "hemlock", - "hence", - "heron", - "hesitate", - "hexagon", - "hickory", - "hiding", - "highway", - "hijack", - "hiker", - "hills", - "himself", - "hinder", - "hippo", - "hire", - "history", - "hitched", - "hive", - "hoax", - "hobby", - "hockey", - "hoisting", - "hold", - "honked", - "hookup", - "hope", - "hornet", - "hospital", - "hotel", - "hounded", - "hover", - "howls", - "hubcaps", - "huddle", - "huge", - "hull", - "humid", - "hunter", - "hurried", - "husband", - "huts", - "hybrid", - "hydrogen", - "hyper", - "iceberg", - "icing", - "icon", - "identity", - "idiom", - "idled", - "idols", - "igloo", - "ignore", - "iguana", - "illness", - "imagine", - "imbalance", - "imitate", - "impel", - "inactive", - "inbound", - "incur", - "industrial", - "inexact", - "inflamed", - "ingested", - "initiate", - "injury", - "inkling", - "inline", - "inmate", - "innocent", - "inorganic", - "input", - "inquest", - "inroads", - "insult", - "intended", - "inundate", - "invoke", - "inwardly", - "ionic", - "irate", - "iris", - "irony", - "irritate", - "island", - "isolated", - "issued", - "italics", - "itches", - "items", - "itinerary", - "itself", - "ivory", - "jabbed", - "jackets", - "jaded", - "jagged", - "jailed", - "jamming", - "january", - "jargon", - "jaunt", - "javelin", - "jaws", - "jazz", - "jeans", - "jeers", - "jellyfish", - "jeopardy", - "jerseys", - "jester", - "jetting", - "jewels", - "jigsaw", - "jingle", - "jittery", - "jive", - "jobs", - "jockey", - "jogger", - "joining", - "joking", - "jolted", - "jostle", - "journal", - "joyous", - "jubilee", - "judge", - "juggled", - "juicy", - "jukebox", - "july", - "jump", - "junk", - "jury", - "justice", - "juvenile", - "kangaroo", - "karate", - "keep", - "kennel", - "kept", - "kernels", - "kettle", - "keyboard", - "kickoff", - "kidneys", - "king", - "kiosk", - "kisses", - "kitchens", - "kiwi", - "knapsack", - "knee", - "knife", - "knowledge", - "knuckle", - "koala", - "laboratory", - "ladder", - "lagoon", - "lair", - "lakes", - "lamb", - "language", - "laptop", - "large", - "last", - "later", - "launching", - "lava", - "lawsuit", - "layout", - "lazy", - "lectures", - "ledge", - "leech", - "left", - "legion", - "leisure", - "lemon", - "lending", - "leopard", - "lesson", - "lettuce", - "lexicon", - "liar", - "library", - "licks", - "lids", - "lied", - "lifestyle", - "light", - "likewise", - "lilac", - "limits", - "linen", - "lion", - "lipstick", - "liquid", - "listen", - "lively", - "loaded", - "lobster", - "locker", - "lodge", - "lofty", - "logic", - "loincloth", - "long", - "looking", - "lopped", - "lordship", - "losing", - "lottery", - "loudly", - "love", - "lower", - "loyal", - "lucky", - "luggage", - "lukewarm", - "lullaby", - "lumber", - "lunar", - "lurk", - "lush", - "luxury", - "lymph", - "lynx", - "lyrics", - "macro", - "madness", - "magically", - "mailed", - "major", - "makeup", - "malady", - "mammal", - "maps", - "masterful", - "match", - "maul", - "maverick", - "maximum", - "mayor", - "maze", - "meant", - "mechanic", - "medicate", - "meeting", - "megabyte", - "melting", - "memoir", - "menu", - "merger", - "mesh", - "metro", - "mews", - "mice", - "midst", - "mighty", - "mime", - "mirror", - "misery", - "mittens", - "mixture", - "moat", - "mobile", - "mocked", - "mohawk", - "moisture", - "molten", - "moment", - "money", - "moon", - "mops", - "morsel", - "mostly", - "motherly", - "mouth", - "movement", - "mowing", - "much", - "muddy", - "muffin", - "mugged", - "mullet", - "mumble", - "mundane", - "muppet", - "mural", - "musical", - "muzzle", - "myriad", - "mystery", - "myth", - "nabbing", - "nagged", - "nail", - "names", - "nanny", - "napkin", - "narrate", - "nasty", - "natural", - "nautical", - "navy", - "nearby", - "necklace", - "needed", - "negative", - "neither", - "neon", - "nephew", - "nerves", - "nestle", - "network", - "neutral", - "never", - "newt", - "nexus", - "nibs", - "niche", - "niece", - "nifty", - "nightly", - "nimbly", - "nineteen", - "nirvana", - "nitrogen", - "nobody", - "nocturnal", - "nodes", - "noises", - "nomad", - "noodles", - "northern", - "nostril", - "noted", - "nouns", - "novelty", - "nowhere", - "nozzle", - "nuance", - "nucleus", - "nudged", - "nugget", - "nuisance", - "null", - "number", - "nuns", - "nurse", - "nutshell", - "nylon", - "oaks", - "oars", - "oasis", - "oatmeal", - "obedient", - "object", - "obliged", - "obnoxious", - "observant", - "obtains", - "obvious", - "occur", - "ocean", - "october", - "odds", - "odometer", - "offend", - "often", - "oilfield", - "ointment", - "okay", - "older", - "olive", - "olympics", - "omega", - "omission", - "omnibus", - "onboard", - "oncoming", - "oneself", - "ongoing", - "onion", - "online", - "onslaught", - "onto", - "onward", - "oozed", - "opacity", - "opened", - "opposite", - "optical", - "opus", - "orange", - "orbit", - "orchid", - "orders", - "organs", - "origin", - "ornament", - "orphans", - "oscar", - "ostrich", - "otherwise", - "otter", - "ouch", - "ought", - "ounce", - "ourselves", - "oust", - "outbreak", - "oval", - "oven", - "owed", - "owls", - "owner", - "oxidant", - "oxygen", - "oyster", - "ozone", - "pact", - "paddles", - "pager", - "pairing", - "palace", - "pamphlet", - "pancakes", - "paper", - "paradise", - "pastry", - "patio", - "pause", - "pavements", - "pawnshop", - "payment", - "peaches", - "pebbles", - "peculiar", - "pedantic", - "peeled", - "pegs", - "pelican", - "pencil", - "people", - "pepper", - "perfect", - "pests", - "petals", - "phase", - "pheasants", - "phone", - "phrases", - "physics", - "piano", - "picked", - "pierce", - "pigment", - "piloted", - "pimple", - "pinched", - "pioneer", - "pipeline", - "pirate", - "pistons", - "pitched", - "pivot", - "pixels", - "pizza", - "playful", - "pledge", - "pliers", - "plotting", - "plus", - "plywood", - "poaching", - "pockets", - "podcast", - "poetry", - "point", - "poker", - "polar", - "ponies", - "pool", - "popular", - "portents", - "possible", - "potato", - "pouch", - "poverty", - "powder", - "pram", - "present", - "pride", - "problems", - "pruned", - "prying", - "psychic", - "public", - "puck", - "puddle", - "puffin", - "pulp", - "pumpkins", - "punch", - "puppy", - "purged", - "push", - "putty", - "puzzled", - "pylons", - "pyramid", - "python", - "queen", - "quick", - "quote", - "rabbits", - "racetrack", - "radar", - "rafts", - "rage", - "railway", - "raking", - "rally", - "ramped", - "randomly", - "rapid", - "rarest", - "rash", - "rated", - "ravine", - "rays", - "razor", - "react", - "rebel", - "recipe", - "reduce", - "reef", - "refer", - "regular", - "reheat", - "reinvest", - "rejoices", - "rekindle", - "relic", - "remedy", - "renting", - "reorder", - "repent", - "request", - "reruns", - "rest", - "return", - "reunion", - "revamp", - "rewind", - "rhino", - "rhythm", - "ribbon", - "richly", - "ridges", - "rift", - "rigid", - "rims", - "ringing", - "riots", - "ripped", - "rising", - "ritual", - "river", - "roared", - "robot", - "rockets", - "rodent", - "rogue", - "roles", - "romance", - "roomy", - "roped", - "roster", - "rotate", - "rounded", - "rover", - "rowboat", - "royal", - "ruby", - "rudely", - "ruffled", - "rugged", - "ruined", - "ruling", - "rumble", - "runway", - "rural", - "rustled", - "ruthless", - "sabotage", - "sack", - "sadness", - "safety", - "saga", - "sailor", - "sake", - "salads", - "sample", - "sanity", - "sapling", - "sarcasm", - "sash", - "satin", - "saucepan", - "saved", - "sawmill", - "saxophone", - "sayings", - "scamper", - "scenic", - "school", - "science", - "scoop", - "scrub", - "scuba", - "seasons", - "second", - "sedan", - "seeded", - "segments", - "seismic", - "selfish", - "semifinal", - "sensible", - "september", - "sequence", - "serving", - "session", - "setup", - "seventh", - "sewage", - "shackles", - "shelter", - "shipped", - "shocking", - "shrugged", - "shuffled", - "shyness", - "siblings", - "sickness", - "sidekick", - "sieve", - "sifting", - "sighting", - "silk", - "simplest", - "sincerely", - "sipped", - "siren", - "situated", - "sixteen", - "sizes", - "skater", - "skew", - "skirting", - "skulls", - "skydive", - "slackens", - "sleepless", - "slid", - "slower", - "slug", - "smash", - "smelting", - "smidgen", - "smog", - "smuggled", - "snake", - "sneeze", - "sniff", - "snout", - "snug", - "soapy", - "sober", - "soccer", - "soda", - "software", - "soggy", - "soil", - "solved", - "somewhere", - "sonic", - "soothe", - "soprano", - "sorry", - "southern", - "sovereign", - "sowed", - "soya", - "space", - "speedy", - "sphere", - "spiders", - "splendid", - "spout", - "sprig", - "spud", - "spying", - "square", - "stacking", - "stellar", - "stick", - "stockpile", - "strained", - "stunning", - "stylishly", - "subtly", - "succeed", - "suddenly", - "suede", - "suffice", - "sugar", - "suitcase", - "sulking", - "summon", - "sunken", - "superior", - "surfer", - "sushi", - "suture", - "swagger", - "swept", - "swiftly", - "sword", - "swung", - "syllabus", - "symptoms", - "syndrome", - "syringe", - "system", - "taboo", - "tacit", - "tadpoles", - "tagged", - "tail", - "taken", - "talent", - "tamper", - "tanks", - "tapestry", - "tarnished", - "tasked", - "tattoo", - "taunts", - "tavern", - "tawny", - "taxi", - "teardrop", - "technical", - "tedious", - "teeming", - "tell", - "template", - "tender", - "tepid", - "tequila", - "terminal", - "testing", - "tether", - "textbook", - "thaw", - "theatrics", - "thirsty", - "thorn", - "threaten", - "thumbs", - "thwart", - "ticket", - "tidy", - "tiers", - "tiger", - "tilt", - "timber", - "tinted", - "tipsy", - "tirade", - "tissue", - "titans", - "toaster", - "tobacco", - "today", - "toenail", - "toffee", - "together", - "toilet", - "token", - "tolerant", - "tomorrow", - "tonic", - "toolbox", - "topic", - "torch", - "tossed", - "total", - "touchy", - "towel", - "toxic", - "toyed", - "trash", - "trendy", - "tribal", - "trolling", - "truth", - "trying", - "tsunami", - "tubes", - "tucks", - "tudor", - "tuesday", - "tufts", - "tugs", - "tuition", - "tulips", - "tumbling", - "tunnel", - "turnip", - "tusks", - "tutor", - "tuxedo", - "twang", - "tweezers", - "twice", - "twofold", - "tycoon", - "typist", - "tyrant", - "ugly", - "ulcers", - "ultimate", - "umbrella", - "umpire", - "unafraid", - "unbending", - "uncle", - "under", - "uneven", - "unfit", - "ungainly", - "unhappy", - "union", - "unjustly", - "unknown", - "unlikely", - "unmask", - "unnoticed", - "unopened", - "unplugs", - "unquoted", - "unrest", - "unsafe", - "until", - "unusual", - "unveil", - "unwind", - "unzip", - "upbeat", - "upcoming", - "update", - "upgrade", - "uphill", - "upkeep", - "upload", - "upon", - "upper", - "upright", - "upstairs", - "uptight", - "upwards", - "urban", - "urchins", - "urgent", - "usage", - "useful", - "usher", - "using", - "usual", - "utensils", - "utility", - "utmost", - "utopia", - "uttered", - "vacation", - "vague", - "vain", - "value", - "vampire", - "vane", - "vapidly", - "vary", - "vastness", - "vats", - "vaults", - "vector", - "veered", - "vegan", - "vehicle", - "vein", - "velvet", - "venomous", - "verification", - "vessel", - "veteran", - "vexed", - "vials", - "vibrate", - "victim", - "video", - "viewpoint", - "vigilant", - "viking", - "village", - "vinegar", - "violin", - "vipers", - "virtual", - "visited", - "vitals", - "vivid", - "vixen", - "vocal", - "vogue", - "voice", - "volcano", - "vortex", - "voted", - "voucher", - "vowels", - "voyage", - "vulture", - "wade", - "waffle", - "wagtail", - "waist", - "waking", - "wallets", - "wanted", - "warped", - "washing", - "water", - "waveform", - "waxing", - "wayside", - "weavers", - "website", - "wedge", - "weekday", - "weird", - "welders", - "went", - "wept", - "were", - "western", - "wetsuit", - "whale", - "when", - "whipped", - "whole", - "wickets", - "width", - "wield", - "wife", - "wiggle", - "wildly", - "winter", - "wipeout", - "wiring", - "wise", - "withdrawn", - "wives", - "wizard", - "wobbly", - "woes", - "woken", - "wolf", - "womanly", - "wonders", - "woozy", - "worry", - "wounded", - "woven", - "wrap", - "wrist", - "wrong", - "yacht", - "yahoo", - "yanks", - "yard", - "yawning", - "yearbook", - "yellow", - "yesterday", - "yeti", - "yields", - "yodel", - "yoga", - "younger", - "yoyo", - "zapped", - "zeal", - "zebra", - "zero", - "zesty", - "zigzags", - "zinger", - "zippers", - "zodiac", - "zombie", - "zones", - "zoom" -] \ No newline at end of file diff --git a/monero-seed/src/words/eo.rs b/monero-seed/src/words/eo.rs deleted file mode 100644 index d9d6ff40e5..0000000000 --- a/monero-seed/src/words/eo.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "abako", - "abdiki", - "abelo", - "abituriento", - "ablativo", - "abnorma", - "abonantoj", - "abrikoto", - "absoluta", - "abunda", - "acetono", - "acida", - "adapti", - "adekvata", - "adheri", - "adicii", - "adjektivo", - "administri", - "adolesko", - "adreso", - "adstringa", - "adulto", - "advokato", - "adzo", - "aeroplano", - "aferulo", - "afgana", - "afiksi", - "aflaba", - "aforismo", - "afranki", - "aftozo", - "afusto", - "agavo", - "agento", - "agiti", - "aglo", - "agmaniero", - "agnoski", - "agordo", - "agrabla", - "agtipo", - "agutio", - "aikido", - "ailanto", - "aina", - "ajatolo", - "ajgenvaloro", - "ajlobulbo", - "ajnlitera", - "ajuto", - "ajzi", - "akademio", - "akcepti", - "akeo", - "akiri", - "aklamado", - "akmeo", - "akno", - "akompani", - "akrobato", - "akselo", - "aktiva", - "akurata", - "akvofalo", - "alarmo", - "albumo", - "alcedo", - "aldoni", - "aleo", - "alfabeto", - "algo", - "alhasti", - "aligatoro", - "alkoholo", - "almozo", - "alnomo", - "alojo", - "alpinisto", - "alrigardi", - "alskribi", - "alta", - "alumeto", - "alveni", - "alzaca", - "amaso", - "ambasado", - "amdeklaro", - "amebo", - "amfibio", - "amhara", - "amiko", - "amkanto", - "amletero", - "amnestio", - "amoranto", - "amplekso", - "amrakonto", - "amsterdama", - "amuzi", - "ananaso", - "androido", - "anekdoto", - "anfrakto", - "angulo", - "anheli", - "animo", - "anjono", - "ankro", - "anonci", - "anpriskribo", - "ansero", - "antikva", - "anuitato", - "aorto", - "aparta", - "aperti", - "apika", - "aplikado", - "apneo", - "apogi", - "aprobi", - "apsido", - "apterigo", - "apudesto", - "araneo", - "arbo", - "ardeco", - "aresti", - "argilo", - "aristokrato", - "arko", - "arlekeno", - "armi", - "arniko", - "aromo", - "arpio", - "arsenalo", - "artisto", - "aruba", - "arvorto", - "asaio", - "asbesto", - "ascendi", - "asekuri", - "asfalto", - "asisti", - "askalono", - "asocio", - "aspekti", - "astro", - "asulo", - "atakonto", - "atendi", - "atingi", - "atleto", - "atmosfero", - "atomo", - "atropino", - "atuto", - "avataro", - "aventuro", - "aviadilo", - "avokado", - "azaleo", - "azbuko", - "azenino", - "azilpetanto", - "azoto", - "azteka", - "babili", - "bacilo", - "badmintono", - "bagatelo", - "bahama", - "bajoneto", - "baki", - "balai", - "bambuo", - "bani", - "baobabo", - "bapti", - "baro", - "bastono", - "batilo", - "bavara", - "bazalto", - "beata", - "bebofono", - "bedo", - "begonio", - "behaviorismo", - "bejlo", - "bekero", - "belarto", - "bemolo", - "benko", - "bereto", - "besto", - "betulo", - "bevelo", - "bezoni", - "biaso", - "biblioteko", - "biciklo", - "bidaro", - "bieno", - "bifsteko", - "bigamiulo", - "bijekcio", - "bikino", - "bildo", - "bimetalismo", - "bindi", - "biografio", - "birdo", - "biskvito", - "bitlibro", - "bivako", - "bizara", - "bjalistoka", - "blanka", - "bleki", - "blinda", - "blovi", - "blua", - "boato", - "bobsledo", - "bocvanano", - "bodisatvo", - "bofratino", - "bogefratoj", - "bohema", - "boji", - "bokalo", - "boli", - "bombono", - "bona", - "bopatrino", - "bordo", - "bosko", - "botelo", - "bovido", - "brakpleno", - "bretaro", - "brikmuro", - "broso", - "brulema", - "bubalo", - "buctrapi", - "budo", - "bufedo", - "bugio", - "bujabeso", - "buklo", - "buldozo", - "bumerango", - "bunta", - "burokrataro", - "busbileto", - "butero", - "buzuko", - "caro", - "cebo", - "ceceo", - "cedro", - "cefalo", - "cejana", - "cekumo", - "celebri", - "cemento", - "cent", - "cepo", - "certa", - "cetera", - "cezio", - "ciano", - "cibeto", - "cico", - "cidro", - "cifero", - "cigaredo", - "ciklo", - "cilindro", - "cimbalo", - "cinamo", - "cipreso", - "cirkonstanco", - "cisterno", - "citrono", - "ciumi", - "civilizado", - "colo", - "congo", - "cunamo", - "cvana", - "dabi", - "daco", - "dadaismo", - "dafodilo", - "dago", - "daimio", - "dajmono", - "daktilo", - "dalio", - "damo", - "danki", - "darmo", - "datumoj", - "dazipo", - "deadmoni", - "debeto", - "decidi", - "dedukti", - "deerigi", - "defendi", - "degeli", - "dehaki", - "deirpunkto", - "deklaracio", - "delikata", - "demandi", - "dento", - "dependi", - "derivi", - "desegni", - "detrui", - "devi", - "deziri", - "dialogo", - "dicentro", - "didaktika", - "dieto", - "diferenci", - "digesti", - "diino", - "dikfingro", - "diligenta", - "dimensio", - "dinamo", - "diodo", - "diplomo", - "direkte", - "diskuti", - "diurno", - "diversa", - "dizajno", - "dobrogitaro", - "docento", - "dogano", - "dojeno", - "doktoro", - "dolori", - "domego", - "donaci", - "dopado", - "dormi", - "dosierujo", - "dotita", - "dozeno", - "drato", - "dresi", - "drinki", - "droni", - "druido", - "duaranga", - "dubi", - "ducent", - "dudek", - "duelo", - "dufoje", - "dugongo", - "duhufa", - "duilo", - "dujare", - "dukato", - "duloka", - "dumtempe", - "dungi", - "duobla", - "dupiedulo", - "dura", - "dusenca", - "dutaga", - "duuma", - "duvalvuloj", - "duzo", - "ebena", - "eblecoj", - "ebono", - "ebria", - "eburo", - "ecaro", - "ecigi", - "ecoj", - "edelvejso", - "editoro", - "edro", - "eduki", - "edzino", - "efektiva", - "efiki", - "efloreski", - "egala", - "egeco", - "egiptologo", - "eglefino", - "egoista", - "egreto", - "ejakuli", - "ejlo", - "ekarto", - "ekbruligi", - "ekceli", - "ekde", - "ekesti", - "ekfirmao", - "ekgliti", - "ekhavi", - "ekipi", - "ekkapti", - "eklezio", - "ekmalsati", - "ekonomio", - "ekpluvi", - "ekrano", - "ekster", - "ektiri", - "ekumeno", - "ekvilibro", - "ekzemplo", - "elasta", - "elbalai", - "elcento", - "eldoni", - "elektro", - "elfari", - "elgliti", - "elhaki", - "elipso", - "elkovi", - "ellasi", - "elmeti", - "elnutri", - "elokventa", - "elparoli", - "elrevigi", - "elstari", - "elteni", - "eluzita", - "elvoki", - "elzasa", - "emajlo", - "embaraso", - "emerito", - "emfazo", - "eminenta", - "emocio", - "empiria", - "emulsio", - "enarkivigi", - "enboteligi", - "enciklopedio", - "endorfino", - "energio", - "enfermi", - "engluti", - "enhavo", - "enigmo", - "enjekcio", - "enketi", - "enlanda", - "enmeti", - "enorma", - "enplanti", - "enradiki", - "enspezo", - "entrepreni", - "enui", - "envolvi", - "enzimo", - "eono", - "eosto", - "epitafo", - "epoko", - "epriskribebla", - "epsilono", - "erari", - "erbio", - "erco", - "erekti", - "ergonomia", - "erikejo", - "ermito", - "erotika", - "erpilo", - "erupcio", - "esameno", - "escepti", - "esenco", - "eskapi", - "esotera", - "esperi", - "estonto", - "etapo", - "etendi", - "etfingro", - "etikedo", - "etlitero", - "etmakleristo", - "etnika", - "etoso", - "etradio", - "etskala", - "etullernejo", - "evakui", - "evento", - "eviti", - "evolui", - "ezoko", - "fabriko", - "facila", - "fadeno", - "fagoto", - "fajro", - "fakto", - "fali", - "familio", - "fanatiko", - "farbo", - "fasko", - "fatala", - "favora", - "fazeolo", - "febro", - "federacio", - "feino", - "fekunda", - "felo", - "femuro", - "fenestro", - "fermi", - "festi", - "fetora", - "fezo", - "fiasko", - "fibro", - "fidela", - "fiera", - "fifama", - "figuro", - "fiherbo", - "fiinsekto", - "fiksa", - "filmo", - "fimensa", - "finalo", - "fiolo", - "fiparoli", - "firmao", - "fisko", - "fitingo", - "fiuzanto", - "fivorto", - "fiziko", - "fjordo", - "flago", - "flegi", - "flirti", - "floro", - "flugi", - "fobio", - "foceno", - "foirejo", - "fojfoje", - "fokuso", - "folio", - "fomenti", - "fonto", - "formulo", - "fosforo", - "fotografi", - "fratino", - "fremda", - "friti", - "frosto", - "frua", - "ftizo", - "fuelo", - "fugo", - "fuksia", - "fulmilo", - "fumanto", - "fundamento", - "fuorto", - "furioza", - "fusilo", - "futbalo", - "fuzio", - "gabardino", - "gado", - "gaela", - "gafo", - "gagato", - "gaja", - "gaki", - "galanta", - "gamao", - "ganto", - "gapulo", - "gardi", - "gasto", - "gavio", - "gazeto", - "geamantoj", - "gebani", - "geedzeco", - "gefratoj", - "geheno", - "gejsero", - "geko", - "gelateno", - "gemisto", - "geniulo", - "geografio", - "gepardo", - "geranio", - "gestolingvo", - "geto", - "geumo", - "gibono", - "giganta", - "gildo", - "gimnastiko", - "ginekologo", - "gipsi", - "girlando", - "gistfungo", - "gitaro", - "glazuro", - "glebo", - "gliti", - "globo", - "gluti", - "gnafalio", - "gnejso", - "gnomo", - "gnuo", - "gobio", - "godetio", - "goeleto", - "gojo", - "golfludejo", - "gombo", - "gondolo", - "gorilo", - "gospelo", - "gotika", - "granda", - "greno", - "griza", - "groto", - "grupo", - "guano", - "gubernatoro", - "gudrotuko", - "gufo", - "gujavo", - "guldeno", - "gumi", - "gupio", - "guruo", - "gusto", - "guto", - "guvernistino", - "gvardio", - "gverilo", - "gvidanto", - "habitato", - "hadito", - "hafnio", - "hagiografio", - "haitiano", - "hajlo", - "hakbloko", - "halti", - "hamstro", - "hangaro", - "hapalo", - "haro", - "hasta", - "hati", - "havebla", - "hazardo", - "hebrea", - "hedero", - "hegemonio", - "hejmo", - "hektaro", - "helpi", - "hemisfero", - "heni", - "hepato", - "herbo", - "hesa", - "heterogena", - "heziti", - "hiacinto", - "hibrida", - "hidrogeno", - "hieroglifo", - "higieno", - "hihii", - "hilumo", - "himno", - "hindino", - "hiperteksto", - "hirundo", - "historio", - "hobio", - "hojli", - "hokeo", - "hologramo", - "homido", - "honesta", - "hopi", - "horizonto", - "hospitalo", - "hotelo", - "huadi", - "hubo", - "hufumo", - "hugenoto", - "hukero", - "huligano", - "humana", - "hundo", - "huoj", - "hupilo", - "hurai", - "husaro", - "hutuo", - "huzo", - "iafoje", - "iagrade", - "iamaniere", - "iarelate", - "iaspeca", - "ibekso", - "ibiso", - "idaro", - "ideala", - "idiomo", - "idolo", - "iele", - "igluo", - "ignori", - "iguamo", - "igvano", - "ikono", - "iksodo", - "ikto", - "iliaflanke", - "ilkomputilo", - "ilobreto", - "ilremedo", - "ilumini", - "imagi", - "imitado", - "imperio", - "imuna", - "incidento", - "industrio", - "inerta", - "infano", - "ingenra", - "inhali", - "iniciati", - "injekti", - "inklino", - "inokuli", - "insekto", - "inteligenta", - "inundi", - "inviti", - "ioma", - "ionosfero", - "iperito", - "ipomeo", - "irana", - "irejo", - "irigacio", - "ironio", - "isato", - "islamo", - "istempo", - "itinero", - "itrio", - "iuloke", - "iumaniere", - "iutempe", - "izolita", - "jado", - "jaguaro", - "jakto", - "jama", - "januaro", - "japano", - "jarringo", - "jazo", - "jenoj", - "jesulo", - "jetavio", - "jezuito", - "jodli", - "joviala", - "juano", - "jubileo", - "judismo", - "jufto", - "juki", - "julio", - "juneca", - "jupo", - "juristo", - "juste", - "juvelo", - "kabineto", - "kadrato", - "kafo", - "kahelo", - "kajako", - "kakao", - "kalkuli", - "kampo", - "kanti", - "kapitalo", - "karaktero", - "kaserolo", - "katapulto", - "kaverna", - "kazino", - "kebabo", - "kefiro", - "keglo", - "kejlo", - "kekso", - "kelka", - "kemio", - "kerno", - "kesto", - "kiamaniere", - "kibuco", - "kidnapi", - "kielo", - "kikero", - "kilogramo", - "kimono", - "kinejo", - "kiosko", - "kirurgo", - "kisi", - "kitelo", - "kivio", - "klavaro", - "klerulo", - "klini", - "klopodi", - "klubo", - "knabo", - "knedi", - "koalo", - "kobalto", - "kodigi", - "kofro", - "kohera", - "koincidi", - "kojoto", - "kokoso", - "koloro", - "komenci", - "kontrakto", - "kopio", - "korekte", - "kosti", - "kotono", - "kovri", - "krajono", - "kredi", - "krii", - "krom", - "kruco", - "ksantino", - "ksenono", - "ksilofono", - "ksosa", - "kubuto", - "kudri", - "kuglo", - "kuiri", - "kuko", - "kulero", - "kumuluso", - "kuneco", - "kupro", - "kuri", - "kuseno", - "kutimo", - "kuvo", - "kuzino", - "kvalito", - "kverko", - "kvin", - "kvoto", - "labori", - "laculo", - "ladbotelo", - "lafo", - "laguno", - "laikino", - "laktobovino", - "lampolumo", - "landkarto", - "laosa", - "lapono", - "larmoguto", - "lastjare", - "latitudo", - "lavejo", - "lazanjo", - "leciono", - "ledosako", - "leganto", - "lekcio", - "lemura", - "lentuga", - "leopardo", - "leporo", - "lerni", - "lesivo", - "letero", - "levilo", - "lezi", - "liano", - "libera", - "liceo", - "lieno", - "lifto", - "ligilo", - "likvoro", - "lila", - "limono", - "lingvo", - "lipo", - "lirika", - "listo", - "literatura", - "liveri", - "lobio", - "logika", - "lojala", - "lokalo", - "longa", - "lordo", - "lotado", - "loza", - "luanto", - "lubriki", - "lucida", - "ludema", - "luigi", - "lukso", - "luli", - "lumbilda", - "lunde", - "lupago", - "lustro", - "lutilo", - "luzerno", - "maato", - "maceri", - "madono", - "mafiano", - "magazeno", - "mahometano", - "maizo", - "majstro", - "maketo", - "malgranda", - "mamo", - "mandareno", - "maorio", - "mapigi", - "marini", - "masko", - "mateno", - "mazuto", - "meandro", - "meblo", - "mecenato", - "medialo", - "mefito", - "megafono", - "mejlo", - "mekanika", - "melodia", - "membro", - "mendi", - "mergi", - "mespilo", - "metoda", - "mevo", - "mezuri", - "miaflanke", - "micelio", - "mielo", - "migdalo", - "mikrofilmo", - "militi", - "mimiko", - "mineralo", - "miopa", - "miri", - "mistera", - "mitralo", - "mizeri", - "mjelo", - "mnemoniko", - "mobilizi", - "mocio", - "moderna", - "mohajro", - "mokadi", - "molaro", - "momento", - "monero", - "mopso", - "mordi", - "moskito", - "motoro", - "movimento", - "mozaiko", - "mueli", - "mukozo", - "muldi", - "mumio", - "munti", - "muro", - "muskolo", - "mutacio", - "muzikisto", - "nabo", - "nacio", - "nadlo", - "nafto", - "naiva", - "najbaro", - "nanometro", - "napo", - "narciso", - "naski", - "naturo", - "navigi", - "naztruo", - "neatendite", - "nebulo", - "necesa", - "nedankinde", - "neebla", - "nefari", - "negoco", - "nehavi", - "neimagebla", - "nektaro", - "nelonga", - "nematura", - "nenia", - "neordinara", - "nepra", - "nervuro", - "nesto", - "nete", - "neulo", - "nevino", - "nifo", - "nigra", - "nihilisto", - "nikotino", - "nilono", - "nimfeo", - "nitrogeno", - "nivelo", - "nobla", - "nocio", - "nodozo", - "nokto", - "nomkarto", - "norda", - "nostalgio", - "notbloko", - "novico", - "nuanco", - "nuboza", - "nuda", - "nugato", - "nuklea", - "nuligi", - "numero", - "nuntempe", - "nupto", - "nura", - "nutri", - "oazo", - "obei", - "objekto", - "oblikva", - "obolo", - "observi", - "obtuza", - "obuso", - "oceano", - "odekolono", - "odori", - "oferti", - "oficiala", - "ofsajdo", - "ofte", - "ogivo", - "ogro", - "ojstredoj", - "okaze", - "okcidenta", - "okro", - "oksido", - "oktobro", - "okulo", - "oldulo", - "oleo", - "olivo", - "omaro", - "ombro", - "omego", - "omikrono", - "omleto", - "omnibuso", - "onagro", - "ondo", - "oneco", - "onidire", - "onklino", - "onlajna", - "onomatopeo", - "ontologio", - "opaka", - "operacii", - "opinii", - "oportuna", - "opresi", - "optimisto", - "oratoro", - "orbito", - "ordinara", - "orelo", - "orfino", - "organizi", - "orienta", - "orkestro", - "orlo", - "orminejo", - "ornami", - "ortangulo", - "orumi", - "oscedi", - "osmozo", - "ostocerbo", - "ovalo", - "ovingo", - "ovoblanko", - "ovri", - "ovulado", - "ozono", - "pacama", - "padeli", - "pafilo", - "pagigi", - "pajlo", - "paketo", - "palaco", - "pampelmo", - "pantalono", - "papero", - "paroli", - "pasejo", - "patro", - "pavimo", - "peco", - "pedalo", - "peklita", - "pelikano", - "pensiono", - "peplomo", - "pesilo", - "petanto", - "pezoforto", - "piano", - "picejo", - "piede", - "pigmento", - "pikema", - "pilkoludo", - "pimento", - "pinglo", - "pioniro", - "pipromento", - "pirato", - "pistolo", - "pitoreska", - "piulo", - "pivoti", - "pizango", - "planko", - "plektita", - "plibonigi", - "ploradi", - "plurlingva", - "pobo", - "podio", - "poeto", - "pogranda", - "pohora", - "pokalo", - "politekniko", - "pomarbo", - "ponevosto", - "populara", - "porcelana", - "postkompreno", - "poteto", - "poviga", - "pozitiva", - "prapatroj", - "precize", - "pridemandi", - "probable", - "pruntanto", - "psalmo", - "psikologio", - "psoriazo", - "pterido", - "publiko", - "pudro", - "pufo", - "pugnobato", - "pulovero", - "pumpi", - "punkto", - "pupo", - "pureo", - "puso", - "putrema", - "puzlo", - "rabate", - "racionala", - "radiko", - "rafinado", - "raguo", - "rajto", - "rakonti", - "ralio", - "rampi", - "rando", - "rapida", - "rastruma", - "ratifiki", - "raviolo", - "razeno", - "reakcio", - "rebildo", - "recepto", - "redakti", - "reenigi", - "reformi", - "regiono", - "rehavi", - "reinspekti", - "rejesi", - "reklamo", - "relativa", - "rememori", - "renkonti", - "reorganizado", - "reprezenti", - "respondi", - "retumilo", - "reuzebla", - "revidi", - "rezulti", - "rialo", - "ribeli", - "ricevi", - "ridiga", - "rifuginto", - "rigardi", - "rikolti", - "rilati", - "rimarki", - "rinocero", - "ripozi", - "riski", - "ritmo", - "rivero", - "rizokampo", - "roboto", - "rododendro", - "rojo", - "rokmuziko", - "rolvorto", - "romantika", - "ronroni", - "rosino", - "rotondo", - "rovero", - "rozeto", - "rubando", - "rudimenta", - "rufa", - "rugbeo", - "ruino", - "ruleto", - "rumoro", - "runo", - "rupio", - "rura", - "rustimuna", - "ruzulo", - "sabato", - "sadismo", - "safario", - "sagaca", - "sakfluto", - "salti", - "samtage", - "sandalo", - "sapejo", - "sarongo", - "satelito", - "savano", - "sbiro", - "sciado", - "seanco", - "sebo", - "sedativo", - "segligno", - "sekretario", - "selektiva", - "semajno", - "senpeza", - "separeo", - "servilo", - "sesangulo", - "setli", - "seurigi", - "severa", - "sezono", - "sfagno", - "sfero", - "sfinkso", - "siatempe", - "siblado", - "sidejo", - "siesto", - "sifono", - "signalo", - "siklo", - "silenti", - "simpla", - "sinjoro", - "siropo", - "sistemo", - "situacio", - "siverto", - "sizifa", - "skatolo", - "skemo", - "skianto", - "sklavo", - "skorpio", - "skribisto", - "skulpti", - "skvamo", - "slango", - "sledeto", - "sliparo", - "smeraldo", - "smirgi", - "smokingo", - "smuto", - "snoba", - "snufegi", - "sobra", - "sociano", - "sodakvo", - "sofo", - "soifi", - "sojlo", - "soklo", - "soldato", - "somero", - "sonilo", - "sopiri", - "sorto", - "soulo", - "soveto", - "sparkado", - "speciala", - "spiri", - "splito", - "sporto", - "sprita", - "spuro", - "stabila", - "stelfiguro", - "stimulo", - "stomako", - "strato", - "studanto", - "subgrupo", - "suden", - "suferanta", - "sugesti", - "suito", - "sukero", - "sulko", - "sume", - "sunlumo", - "super", - "surskribeto", - "suspekti", - "suturo", - "svati", - "svenfali", - "svingi", - "svopo", - "tabako", - "taglumo", - "tajloro", - "taksimetro", - "talento", - "tamen", - "tanko", - "taoismo", - "tapioko", - "tarifo", - "tasko", - "tatui", - "taverno", - "teatro", - "tedlaboro", - "tegmento", - "tehoro", - "teknika", - "telefono", - "tempo", - "tenisejo", - "teorie", - "teraso", - "testudo", - "tetablo", - "teujo", - "tezo", - "tialo", - "tibio", - "tielnomata", - "tifono", - "tigro", - "tikli", - "timida", - "tinkturo", - "tiom", - "tiparo", - "tirkesto", - "titolo", - "tiutempe", - "tizano", - "tobogano", - "tofeo", - "togo", - "toksa", - "tolerema", - "tombolo", - "tondri", - "topografio", - "tordeti", - "tosti", - "totalo", - "traduko", - "tredi", - "triangulo", - "tropika", - "trumpeto", - "tualeto", - "tubisto", - "tufgrebo", - "tuja", - "tukano", - "tulipo", - "tumulto", - "tunelo", - "turisto", - "tusi", - "tutmonda", - "tvisto", - "udono", - "uesto", - "ukazo", - "ukelelo", - "ulcero", - "ulmo", - "ultimato", - "ululi", - "umbiliko", - "unco", - "ungego", - "uniformo", - "unkti", - "unukolora", - "uragano", - "urbano", - "uretro", - "urino", - "ursido", - "uskleco", - "usonigi", - "utero", - "utila", - "utopia", - "uverturo", - "uzadi", - "uzeblo", - "uzino", - "uzkutimo", - "uzofini", - "uzurpi", - "uzvaloro", - "vadejo", - "vafleto", - "vagono", - "vahabismo", - "vajco", - "vakcino", - "valoro", - "vampiro", - "vangharoj", - "vaporo", - "varma", - "vasta", - "vato", - "vazaro", - "veaspekta", - "vedismo", - "vegetalo", - "vehiklo", - "vejno", - "vekita", - "velstango", - "vemieno", - "vendi", - "vepro", - "verando", - "vespero", - "veturi", - "veziko", - "viando", - "vibri", - "vico", - "videbla", - "vifio", - "vigla", - "viktimo", - "vila", - "vimeno", - "vintro", - "violo", - "vippuno", - "virtuala", - "viskoza", - "vitro", - "viveca", - "viziti", - "vobli", - "vodko", - "vojeto", - "vokegi", - "volbo", - "vomema", - "vono", - "vortaro", - "vosto", - "voti", - "vrako", - "vringi", - "vualo", - "vulkano", - "vundo", - "vuvuzelo", - "zamenhofa", - "zapi", - "zebro", - "zefiro", - "zeloto", - "zenismo", - "zeolito", - "zepelino", - "zeto", - "zigzagi", - "zinko", - "zipo", - "zirkonio", - "zodiako", - "zoeto", - "zombio", - "zono", - "zoologio", - "zorgi", - "zukino", - "zumilo" -] \ No newline at end of file diff --git a/monero-seed/src/words/es.rs b/monero-seed/src/words/es.rs deleted file mode 100644 index 09fb346df4..0000000000 --- a/monero-seed/src/words/es.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "ábaco", - "abdomen", - "abeja", - "abierto", - "abogado", - "abono", - "aborto", - "abrazo", - "abrir", - "abuelo", - "abuso", - "acabar", - "academia", - "acceso", - "acción", - "aceite", - "acelga", - "acento", - "aceptar", - "ácido", - "aclarar", - "acné", - "acoger", - "acoso", - "activo", - "acto", - "actriz", - "actuar", - "acudir", - "acuerdo", - "acusar", - "adicto", - "admitir", - "adoptar", - "adorno", - "aduana", - "adulto", - "aéreo", - "afectar", - "afición", - "afinar", - "afirmar", - "ágil", - "agitar", - "agonía", - "agosto", - "agotar", - "agregar", - "agrio", - "agua", - "agudo", - "águila", - "aguja", - "ahogo", - "ahorro", - "aire", - "aislar", - "ajedrez", - "ajeno", - "ajuste", - "alacrán", - "alambre", - "alarma", - "alba", - "álbum", - "alcalde", - "aldea", - "alegre", - "alejar", - "alerta", - "aleta", - "alfiler", - "alga", - "algodón", - "aliado", - "aliento", - "alivio", - "alma", - "almeja", - "almíbar", - "altar", - "alteza", - "altivo", - "alto", - "altura", - "alumno", - "alzar", - "amable", - "amante", - "amapola", - "amargo", - "amasar", - "ámbar", - "ámbito", - "ameno", - "amigo", - "amistad", - "amor", - "amparo", - "amplio", - "ancho", - "anciano", - "ancla", - "andar", - "andén", - "anemia", - "ángulo", - "anillo", - "ánimo", - "anís", - "anotar", - "antena", - "antiguo", - "antojo", - "anual", - "anular", - "anuncio", - "añadir", - "añejo", - "año", - "apagar", - "aparato", - "apetito", - "apio", - "aplicar", - "apodo", - "aporte", - "apoyo", - "aprender", - "aprobar", - "apuesta", - "apuro", - "arado", - "araña", - "arar", - "árbitro", - "árbol", - "arbusto", - "archivo", - "arco", - "arder", - "ardilla", - "arduo", - "área", - "árido", - "aries", - "armonía", - "arnés", - "aroma", - "arpa", - "arpón", - "arreglo", - "arroz", - "arruga", - "arte", - "artista", - "asa", - "asado", - "asalto", - "ascenso", - "asegurar", - "aseo", - "asesor", - "asiento", - "asilo", - "asistir", - "asno", - "asombro", - "áspero", - "astilla", - "astro", - "astuto", - "asumir", - "asunto", - "atajo", - "ataque", - "atar", - "atento", - "ateo", - "ático", - "atleta", - "átomo", - "atraer", - "atroz", - "atún", - "audaz", - "audio", - "auge", - "aula", - "aumento", - "ausente", - "autor", - "aval", - "avance", - "avaro", - "ave", - "avellana", - "avena", - "avestruz", - "avión", - "aviso", - "ayer", - "ayuda", - "ayuno", - "azafrán", - "azar", - "azote", - "azúcar", - "azufre", - "azul", - "baba", - "babor", - "bache", - "bahía", - "baile", - "bajar", - "balanza", - "balcón", - "balde", - "bambú", - "banco", - "banda", - "baño", - "barba", - "barco", - "barniz", - "barro", - "báscula", - "bastón", - "basura", - "batalla", - "batería", - "batir", - "batuta", - "baúl", - "bazar", - "bebé", - "bebida", - "bello", - "besar", - "beso", - "bestia", - "bicho", - "bien", - "bingo", - "blanco", - "bloque", - "blusa", - "boa", - "bobina", - "bobo", - "boca", - "bocina", - "boda", - "bodega", - "boina", - "bola", - "bolero", - "bolsa", - "bomba", - "bondad", - "bonito", - "bono", - "bonsái", - "borde", - "borrar", - "bosque", - "bote", - "botín", - "bóveda", - "bozal", - "bravo", - "brazo", - "brecha", - "breve", - "brillo", - "brinco", - "brisa", - "broca", - "broma", - "bronce", - "brote", - "bruja", - "brusco", - "bruto", - "buceo", - "bucle", - "bueno", - "buey", - "bufanda", - "bufón", - "búho", - "buitre", - "bulto", - "burbuja", - "burla", - "burro", - "buscar", - "butaca", - "buzón", - "caballo", - "cabeza", - "cabina", - "cabra", - "cacao", - "cadáver", - "cadena", - "caer", - "café", - "caída", - "caimán", - "caja", - "cajón", - "cal", - "calamar", - "calcio", - "caldo", - "calidad", - "calle", - "calma", - "calor", - "calvo", - "cama", - "cambio", - "camello", - "camino", - "campo", - "cáncer", - "candil", - "canela", - "canguro", - "canica", - "canto", - "caña", - "cañón", - "caoba", - "caos", - "capaz", - "capitán", - "capote", - "captar", - "capucha", - "cara", - "carbón", - "cárcel", - "careta", - "carga", - "cariño", - "carne", - "carpeta", - "carro", - "carta", - "casa", - "casco", - "casero", - "caspa", - "castor", - "catorce", - "catre", - "caudal", - "causa", - "cazo", - "cebolla", - "ceder", - "cedro", - "celda", - "célebre", - "celoso", - "célula", - "cemento", - "ceniza", - "centro", - "cerca", - "cerdo", - "cereza", - "cero", - "cerrar", - "certeza", - "césped", - "cetro", - "chacal", - "chaleco", - "champú", - "chancla", - "chapa", - "charla", - "chico", - "chiste", - "chivo", - "choque", - "choza", - "chuleta", - "chupar", - "ciclón", - "ciego", - "cielo", - "cien", - "cierto", - "cifra", - "cigarro", - "cima", - "cinco", - "cine", - "cinta", - "ciprés", - "circo", - "ciruela", - "cisne", - "cita", - "ciudad", - "clamor", - "clan", - "claro", - "clase", - "clave", - "cliente", - "clima", - "clínica", - "cobre", - "cocción", - "cochino", - "cocina", - "coco", - "código", - "codo", - "cofre", - "coger", - "cohete", - "cojín", - "cojo", - "cola", - "colcha", - "colegio", - "colgar", - "colina", - "collar", - "colmo", - "columna", - "combate", - "comer", - "comida", - "cómodo", - "compra", - "conde", - "conejo", - "conga", - "conocer", - "consejo", - "contar", - "copa", - "copia", - "corazón", - "corbata", - "corcho", - "cordón", - "corona", - "correr", - "coser", - "cosmos", - "costa", - "cráneo", - "cráter", - "crear", - "crecer", - "creído", - "crema", - "cría", - "crimen", - "cripta", - "crisis", - "cromo", - "crónica", - "croqueta", - "crudo", - "cruz", - "cuadro", - "cuarto", - "cuatro", - "cubo", - "cubrir", - "cuchara", - "cuello", - "cuento", - "cuerda", - "cuesta", - "cueva", - "cuidar", - "culebra", - "culpa", - "culto", - "cumbre", - "cumplir", - "cuna", - "cuneta", - "cuota", - "cupón", - "cúpula", - "curar", - "curioso", - "curso", - "curva", - "cutis", - "dama", - "danza", - "dar", - "dardo", - "dátil", - "deber", - "débil", - "década", - "decir", - "dedo", - "defensa", - "definir", - "dejar", - "delfín", - "delgado", - "delito", - "demora", - "denso", - "dental", - "deporte", - "derecho", - "derrota", - "desayuno", - "deseo", - "desfile", - "desnudo", - "destino", - "desvío", - "detalle", - "detener", - "deuda", - "día", - "diablo", - "diadema", - "diamante", - "diana", - "diario", - "dibujo", - "dictar", - "diente", - "dieta", - "diez", - "difícil", - "digno", - "dilema", - "diluir", - "dinero", - "directo", - "dirigir", - "disco", - "diseño", - "disfraz", - "diva", - "divino", - "doble", - "doce", - "dolor", - "domingo", - "don", - "donar", - "dorado", - "dormir", - "dorso", - "dos", - "dosis", - "dragón", - "droga", - "ducha", - "duda", - "duelo", - "dueño", - "dulce", - "dúo", - "duque", - "durar", - "dureza", - "duro", - "ébano", - "ebrio", - "echar", - "eco", - "ecuador", - "edad", - "edición", - "edificio", - "editor", - "educar", - "efecto", - "eficaz", - "eje", - "ejemplo", - "elefante", - "elegir", - "elemento", - "elevar", - "elipse", - "élite", - "elixir", - "elogio", - "eludir", - "embudo", - "emitir", - "emoción", - "empate", - "empeño", - "empleo", - "empresa", - "enano", - "encargo", - "enchufe", - "encía", - "enemigo", - "enero", - "enfado", - "enfermo", - "engaño", - "enigma", - "enlace", - "enorme", - "enredo", - "ensayo", - "enseñar", - "entero", - "entrar", - "envase", - "envío", - "época", - "equipo", - "erizo", - "escala", - "escena", - "escolar", - "escribir", - "escudo", - "esencia", - "esfera", - "esfuerzo", - "espada", - "espejo", - "espía", - "esposa", - "espuma", - "esquí", - "estar", - "este", - "estilo", - "estufa", - "etapa", - "eterno", - "ética", - "etnia", - "evadir", - "evaluar", - "evento", - "evitar", - "exacto", - "examen", - "exceso", - "excusa", - "exento", - "exigir", - "exilio", - "existir", - "éxito", - "experto", - "explicar", - "exponer", - "extremo", - "fábrica", - "fábula", - "fachada", - "fácil", - "factor", - "faena", - "faja", - "falda", - "fallo", - "falso", - "faltar", - "fama", - "familia", - "famoso", - "faraón", - "farmacia", - "farol", - "farsa", - "fase", - "fatiga", - "fauna", - "favor", - "fax", - "febrero", - "fecha", - "feliz", - "feo", - "feria", - "feroz", - "fértil", - "fervor", - "festín", - "fiable", - "fianza", - "fiar", - "fibra", - "ficción", - "ficha", - "fideo", - "fiebre", - "fiel", - "fiera", - "fiesta", - "figura", - "fijar", - "fijo", - "fila", - "filete", - "filial", - "filtro", - "fin", - "finca", - "fingir", - "finito", - "firma", - "flaco", - "flauta", - "flecha", - "flor", - "flota", - "fluir", - "flujo", - "flúor", - "fobia", - "foca", - "fogata", - "fogón", - "folio", - "folleto", - "fondo", - "forma", - "forro", - "fortuna", - "forzar", - "fosa", - "foto", - "fracaso", - "frágil", - "franja", - "frase", - "fraude", - "freír", - "freno", - "fresa", - "frío", - "frito", - "fruta", - "fuego", - "fuente", - "fuerza", - "fuga", - "fumar", - "función", - "funda", - "furgón", - "furia", - "fusil", - "fútbol", - "futuro", - "gacela", - "gafas", - "gaita", - "gajo", - "gala", - "galería", - "gallo", - "gamba", - "ganar", - "gancho", - "ganga", - "ganso", - "garaje", - "garza", - "gasolina", - "gastar", - "gato", - "gavilán", - "gemelo", - "gemir", - "gen", - "género", - "genio", - "gente", - "geranio", - "gerente", - "germen", - "gesto", - "gigante", - "gimnasio", - "girar", - "giro", - "glaciar", - "globo", - "gloria", - "gol", - "golfo", - "goloso", - "golpe", - "goma", - "gordo", - "gorila", - "gorra", - "gota", - "goteo", - "gozar", - "grada", - "gráfico", - "grano", - "grasa", - "gratis", - "grave", - "grieta", - "grillo", - "gripe", - "gris", - "grito", - "grosor", - "grúa", - "grueso", - "grumo", - "grupo", - "guante", - "guapo", - "guardia", - "guerra", - "guía", - "guiño", - "guion", - "guiso", - "guitarra", - "gusano", - "gustar", - "haber", - "hábil", - "hablar", - "hacer", - "hacha", - "hada", - "hallar", - "hamaca", - "harina", - "haz", - "hazaña", - "hebilla", - "hebra", - "hecho", - "helado", - "helio", - "hembra", - "herir", - "hermano", - "héroe", - "hervir", - "hielo", - "hierro", - "hígado", - "higiene", - "hijo", - "himno", - "historia", - "hocico", - "hogar", - "hoguera", - "hoja", - "hombre", - "hongo", - "honor", - "honra", - "hora", - "hormiga", - "horno", - "hostil", - "hoyo", - "hueco", - "huelga", - "huerta", - "hueso", - "huevo", - "huida", - "huir", - "humano", - "húmedo", - "humilde", - "humo", - "hundir", - "huracán", - "hurto", - "icono", - "ideal", - "idioma", - "ídolo", - "iglesia", - "iglú", - "igual", - "ilegal", - "ilusión", - "imagen", - "imán", - "imitar", - "impar", - "imperio", - "imponer", - "impulso", - "incapaz", - "índice", - "inerte", - "infiel", - "informe", - "ingenio", - "inicio", - "inmenso", - "inmune", - "innato", - "insecto", - "instante", - "interés", - "íntimo", - "intuir", - "inútil", - "invierno", - "ira", - "iris", - "ironía", - "isla", - "islote", - "jabalí", - "jabón", - "jamón", - "jarabe", - "jardín", - "jarra", - "jaula", - "jazmín", - "jefe", - "jeringa", - "jinete", - "jornada", - "joroba", - "joven", - "joya", - "juerga", - "jueves", - "juez", - "jugador", - "jugo", - "juguete", - "juicio", - "junco", - "jungla", - "junio", - "juntar", - "júpiter", - "jurar", - "justo", - "juvenil", - "juzgar", - "kilo", - "koala", - "labio", - "lacio", - "lacra", - "lado", - "ladrón", - "lagarto", - "lágrima", - "laguna", - "laico", - "lamer", - "lámina", - "lámpara", - "lana", - "lancha", - "langosta", - "lanza", - "lápiz", - "largo", - "larva", - "lástima", - "lata", - "látex", - "latir", - "laurel", - "lavar", - "lazo", - "leal", - "lección", - "leche", - "lector", - "leer", - "legión", - "legumbre", - "lejano", - "lengua", - "lento", - "leña", - "león", - "leopardo", - "lesión", - "letal", - "letra", - "leve", - "leyenda", - "libertad", - "libro", - "licor", - "líder", - "lidiar", - "lienzo", - "liga", - "ligero", - "lima", - "límite", - "limón", - "limpio", - "lince", - "lindo", - "línea", - "lingote", - "lino", - "linterna", - "líquido", - "liso", - "lista", - "litera", - "litio", - "litro", - "llaga", - "llama", - "llanto", - "llave", - "llegar", - "llenar", - "llevar", - "llorar", - "llover", - "lluvia", - "lobo", - "loción", - "loco", - "locura", - "lógica", - "logro", - "lombriz", - "lomo", - "lonja", - "lote", - "lucha", - "lucir", - "lugar", - "lujo", - "luna", - "lunes", - "lupa", - "lustro", - "luto", - "luz", - "maceta", - "macho", - "madera", - "madre", - "maduro", - "maestro", - "mafia", - "magia", - "mago", - "maíz", - "maldad", - "maleta", - "malla", - "malo", - "mamá", - "mambo", - "mamut", - "manco", - "mando", - "manejar", - "manga", - "maniquí", - "manjar", - "mano", - "manso", - "manta", - "mañana", - "mapa", - "máquina", - "mar", - "marco", - "marea", - "marfil", - "margen", - "marido", - "mármol", - "marrón", - "martes", - "marzo", - "masa", - "máscara", - "masivo", - "matar", - "materia", - "matiz", - "matriz", - "máximo", - "mayor", - "mazorca", - "mecha", - "medalla", - "medio", - "médula", - "mejilla", - "mejor", - "melena", - "melón", - "memoria", - "menor", - "mensaje", - "mente", - "menú", - "mercado", - "merengue", - "mérito", - "mes", - "mesón", - "meta", - "meter", - "método", - "metro", - "mezcla", - "miedo", - "miel", - "miembro", - "miga", - "mil", - "milagro", - "militar", - "millón", - "mimo", - "mina", - "minero", - "mínimo", - "minuto", - "miope", - "mirar", - "misa", - "miseria", - "misil", - "mismo", - "mitad", - "mito", - "mochila", - "moción", - "moda", - "modelo", - "moho", - "mojar", - "molde", - "moler", - "molino", - "momento", - "momia", - "monarca", - "moneda", - "monja", - "monto", - "moño", - "morada", - "morder", - "moreno", - "morir", - "morro", - "morsa", - "mortal", - "mosca", - "mostrar", - "motivo", - "mover", - "móvil", - "mozo", - "mucho", - "mudar", - "mueble", - "muela", - "muerte", - "muestra", - "mugre", - "mujer", - "mula", - "muleta", - "multa", - "mundo", - "muñeca", - "mural", - "muro", - "músculo", - "museo", - "musgo", - "música", - "muslo", - "nácar", - "nación", - "nadar", - "naipe", - "naranja", - "nariz", - "narrar", - "nasal", - "natal", - "nativo", - "natural", - "náusea", - "naval", - "nave", - "navidad", - "necio", - "néctar", - "negar", - "negocio", - "negro", - "neón", - "nervio", - "neto", - "neutro", - "nevar", - "nevera", - "nicho", - "nido", - "niebla", - "nieto", - "niñez", - "niño", - "nítido", - "nivel", - "nobleza", - "noche", - "nómina", - "noria", - "norma", - "norte", - "nota", - "noticia", - "novato", - "novela", - "novio", - "nube", - "nuca", - "núcleo", - "nudillo", - "nudo", - "nuera", - "nueve", - "nuez", - "nulo", - "número", - "nutria", - "oasis", - "obeso", - "obispo", - "objeto", - "obra", - "obrero", - "observar", - "obtener", - "obvio", - "oca", - "ocaso", - "océano", - "ochenta", - "ocho", - "ocio", - "ocre", - "octavo", - "octubre", - "oculto", - "ocupar", - "ocurrir", - "odiar", - "odio", - "odisea", - "oeste", - "ofensa", - "oferta", - "oficio", - "ofrecer", - "ogro", - "oído", - "oír", - "ojo", - "ola", - "oleada", - "olfato", - "olivo", - "olla", - "olmo", - "olor", - "olvido", - "ombligo", - "onda", - "onza", - "opaco", - "opción", - "ópera", - "opinar", - "oponer", - "optar", - "óptica", - "opuesto", - "oración", - "orador", - "oral", - "órbita", - "orca", - "orden", - "oreja", - "órgano", - "orgía", - "orgullo", - "oriente", - "origen", - "orilla", - "oro", - "orquesta", - "oruga", - "osadía", - "oscuro", - "osezno", - "oso", - "ostra", - "otoño", - "otro", - "oveja", - "óvulo", - "óxido", - "oxígeno", - "oyente", - "ozono", - "pacto", - "padre", - "paella", - "página", - "pago", - "país", - "pájaro", - "palabra", - "palco", - "paleta", - "pálido", - "palma", - "paloma", - "palpar", - "pan", - "panal", - "pánico", - "pantera", - "pañuelo", - "papá", - "papel", - "papilla", - "paquete", - "parar", - "parcela", - "pared", - "parir", - "paro", - "párpado", - "parque", - "párrafo", - "parte", - "pasar", - "paseo", - "pasión", - "paso", - "pasta", - "pata", - "patio", - "patria", - "pausa", - "pauta", - "pavo", - "payaso", - "peatón", - "pecado", - "pecera", - "pecho", - "pedal", - "pedir", - "pegar", - "peine", - "pelar", - "peldaño", - "pelea", - "peligro", - "pellejo", - "pelo", - "peluca", - "pena", - "pensar", - "peñón", - "peón", - "peor", - "pepino", - "pequeño", - "pera", - "percha", - "perder", - "pereza", - "perfil", - "perico", - "perla", - "permiso", - "perro", - "persona", - "pesa", - "pesca", - "pésimo", - "pestaña", - "pétalo", - "petróleo", - "pez", - "pezuña", - "picar", - "pichón", - "pie", - "piedra", - "pierna", - "pieza", - "pijama", - "pilar", - "piloto", - "pimienta", - "pino", - "pintor", - "pinza", - "piña", - "piojo", - "pipa", - "pirata", - "pisar", - "piscina", - "piso", - "pista", - "pitón", - "pizca", - "placa", - "plan", - "plata", - "playa", - "plaza", - "pleito", - "pleno", - "plomo", - "pluma", - "plural", - "pobre", - "poco", - "poder", - "podio", - "poema", - "poesía", - "poeta", - "polen", - "policía", - "pollo", - "polvo", - "pomada", - "pomelo", - "pomo", - "pompa", - "poner", - "porción", - "portal", - "posada", - "poseer", - "posible", - "poste", - "potencia", - "potro", - "pozo", - "prado", - "precoz", - "pregunta", - "premio", - "prensa", - "preso", - "previo", - "primo", - "príncipe", - "prisión", - "privar", - "proa", - "probar", - "proceso", - "producto", - "proeza", - "profesor", - "programa", - "prole", - "promesa", - "pronto", - "propio", - "próximo", - "prueba", - "público", - "puchero", - "pudor", - "pueblo", - "puerta", - "puesto", - "pulga", - "pulir", - "pulmón", - "pulpo", - "pulso", - "puma", - "punto", - "puñal", - "puño", - "pupa", - "pupila", - "puré", - "quedar", - "queja", - "quemar", - "querer", - "queso", - "quieto", - "química", - "quince", - "quitar", - "rábano", - "rabia", - "rabo", - "ración", - "radical", - "raíz", - "rama", - "rampa", - "rancho", - "rango", - "rapaz", - "rápido", - "rapto", - "rasgo", - "raspa", - "rato", - "rayo", - "raza", - "razón", - "reacción", - "realidad", - "rebaño", - "rebote", - "recaer", - "receta", - "rechazo", - "recoger", - "recreo", - "recto", - "recurso", - "red", - "redondo", - "reducir", - "reflejo", - "reforma", - "refrán", - "refugio", - "regalo", - "regir", - "regla", - "regreso", - "rehén", - "reino", - "reír", - "reja", - "relato", - "relevo", - "relieve", - "relleno", - "reloj", - "remar", - "remedio", - "remo", - "rencor", - "rendir", - "renta", - "reparto", - "repetir", - "reposo", - "reptil", - "res", - "rescate", - "resina", - "respeto", - "resto", - "resumen", - "retiro", - "retorno", - "retrato", - "reunir", - "revés", - "revista", - "rey", - "rezar", - "rico", - "riego", - "rienda", - "riesgo", - "rifa", - "rígido", - "rigor", - "rincón", - "riñón", - "río", - "riqueza", - "risa", - "ritmo", - "rito" -] \ No newline at end of file diff --git a/monero-seed/src/words/fr.rs b/monero-seed/src/words/fr.rs deleted file mode 100644 index 338eeb3877..0000000000 --- a/monero-seed/src/words/fr.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "abandon", - "abattre", - "aboi", - "abolir", - "aborder", - "abri", - "absence", - "absolu", - "abuser", - "acacia", - "acajou", - "accent", - "accord", - "accrocher", - "accuser", - "acerbe", - "achat", - "acheter", - "acide", - "acier", - "acquis", - "acte", - "action", - "adage", - "adepte", - "adieu", - "admettre", - "admis", - "adorer", - "adresser", - "aduler", - "affaire", - "affirmer", - "afin", - "agacer", - "agent", - "agir", - "agiter", - "agonie", - "agrafe", - "agrume", - "aider", - "aigle", - "aigre", - "aile", - "ailleurs", - "aimant", - "aimer", - "ainsi", - "aise", - "ajouter", - "alarme", - "album", - "alcool", - "alerte", - "algue", - "alibi", - "aller", - "allumer", - "alors", - "amande", - "amener", - "amie", - "amorcer", - "amour", - "ample", - "amuser", - "ananas", - "ancien", - "anglais", - "angoisse", - "animal", - "anneau", - "annoncer", - "apercevoir", - "apparence", - "appel", - "apporter", - "apprendre", - "appuyer", - "arbre", - "arcade", - "arceau", - "arche", - "ardeur", - "argent", - "argile", - "aride", - "arme", - "armure", - "arracher", - "arriver", - "article", - "asile", - "aspect", - "assaut", - "assez", - "assister", - "assurer", - "astre", - "astuce", - "atlas", - "atroce", - "attacher", - "attente", - "attirer", - "aube", - "aucun", - "audace", - "auparavant", - "auquel", - "aurore", - "aussi", - "autant", - "auteur", - "autoroute", - "autre", - "aval", - "avant", - "avec", - "avenir", - "averse", - "aveu", - "avide", - "avion", - "avis", - "avoir", - "avouer", - "avril", - "azote", - "azur", - "badge", - "bagage", - "bague", - "bain", - "baisser", - "balai", - "balcon", - "balise", - "balle", - "bambou", - "banane", - "banc", - "bandage", - "banjo", - "banlieue", - "bannir", - "banque", - "baobab", - "barbe", - "barque", - "barrer", - "bassine", - "bataille", - "bateau", - "battre", - "baver", - "bavoir", - "bazar", - "beau", - "beige", - "berger", - "besoin", - "beurre", - "biais", - "biceps", - "bidule", - "bien", - "bijou", - "bilan", - "billet", - "blanc", - "blason", - "bleu", - "bloc", - "blond", - "bocal", - "boire", - "boiserie", - "boiter", - "bonbon", - "bondir", - "bonheur", - "bordure", - "borgne", - "borner", - "bosse", - "bouche", - "bouder", - "bouger", - "boule", - "bourse", - "bout", - "boxe", - "brader", - "braise", - "branche", - "braquer", - "bras", - "brave", - "brebis", - "brevet", - "brider", - "briller", - "brin", - "brique", - "briser", - "broche", - "broder", - "bronze", - "brosser", - "brouter", - "bruit", - "brute", - "budget", - "buffet", - "bulle", - "bureau", - "buriner", - "buste", - "buter", - "butiner", - "cabas", - "cabinet", - "cabri", - "cacao", - "cacher", - "cadeau", - "cadre", - "cage", - "caisse", - "caler", - "calme", - "camarade", - "camion", - "campagne", - "canal", - "canif", - "capable", - "capot", - "carat", - "caresser", - "carie", - "carpe", - "cartel", - "casier", - "casque", - "casserole", - "cause", - "cavale", - "cave", - "ceci", - "cela", - "celui", - "cendre", - "cent", - "cependant", - "cercle", - "cerise", - "cerner", - "certes", - "cerveau", - "cesser", - "chacun", - "chair", - "chaleur", - "chamois", - "chanson", - "chaque", - "charge", - "chasse", - "chat", - "chaud", - "chef", - "chemin", - "cheveu", - "chez", - "chicane", - "chien", - "chiffre", - "chiner", - "chiot", - "chlore", - "choc", - "choix", - "chose", - "chou", - "chute", - "cibler", - "cidre", - "ciel", - "cigale", - "cinq", - "cintre", - "cirage", - "cirque", - "ciseau", - "citation", - "citer", - "citron", - "civet", - "clairon", - "clan", - "classe", - "clavier", - "clef", - "climat", - "cloche", - "cloner", - "clore", - "clos", - "clou", - "club", - "cobra", - "cocon", - "coiffer", - "coin", - "colline", - "colon", - "combat", - "comme", - "compte", - "conclure", - "conduire", - "confier", - "connu", - "conseil", - "contre", - "convenir", - "copier", - "cordial", - "cornet", - "corps", - "cosmos", - "coton", - "couche", - "coude", - "couler", - "coupure", - "cour", - "couteau", - "couvrir", - "crabe", - "crainte", - "crampe", - "cran", - "creuser", - "crever", - "crier", - "crime", - "crin", - "crise", - "crochet", - "croix", - "cruel", - "cuisine", - "cuite", - "culot", - "culte", - "cumul", - "cure", - "curieux", - "cuve", - "dame", - "danger", - "dans", - "davantage", - "debout", - "dedans", - "dehors", - "delta", - "demain", - "demeurer", - "demi", - "dense", - "dent", - "depuis", - "dernier", - "descendre", - "dessus", - "destin", - "dette", - "deuil", - "deux", - "devant", - "devenir", - "devin", - "devoir", - "dicton", - "dieu", - "difficile", - "digestion", - "digue", - "diluer", - "dimanche", - "dinde", - "diode", - "dire", - "diriger", - "discours", - "disposer", - "distance", - "divan", - "divers", - "docile", - "docteur", - "dodu", - "dogme", - "doigt", - "dominer", - "donation", - "donjon", - "donner", - "dopage", - "dorer", - "dormir", - "doseur", - "douane", - "double", - "douche", - "douleur", - "doute", - "doux", - "douzaine", - "draguer", - "drame", - "drap", - "dresser", - "droit", - "duel", - "dune", - "duper", - "durant", - "durcir", - "durer", - "eaux", - "effacer", - "effet", - "effort", - "effrayant", - "elle", - "embrasser", - "emmener", - "emparer", - "empire", - "employer", - "emporter", - "enclos", - "encore", - "endive", - "endormir", - "endroit", - "enduit", - "enfant", - "enfermer", - "enfin", - "enfler", - "enfoncer", - "enfuir", - "engager", - "engin", - "enjeu", - "enlever", - "ennemi", - "ennui", - "ensemble", - "ensuite", - "entamer", - "entendre", - "entier", - "entourer", - "entre", - "envelopper", - "envie", - "envoyer", - "erreur", - "escalier", - "espace", - "espoir", - "esprit", - "essai", - "essor", - "essuyer", - "estimer", - "exact", - "examiner", - "excuse", - "exemple", - "exiger", - "exil", - "exister", - "exode", - "expliquer", - "exposer", - "exprimer", - "extase", - "fable", - "facette", - "facile", - "fade", - "faible", - "faim", - "faire", - "fait", - "falloir", - "famille", - "faner", - "farce", - "farine", - "fatigue", - "faucon", - "faune", - "faute", - "faux", - "faveur", - "favori", - "faxer", - "feinter", - "femme", - "fendre", - "fente", - "ferme", - "festin", - "feuille", - "feutre", - "fiable", - "fibre", - "ficher", - "fier", - "figer", - "figure", - "filet", - "fille", - "filmer", - "fils", - "filtre", - "final", - "finesse", - "finir", - "fiole", - "firme", - "fixe", - "flacon", - "flair", - "flamme", - "flan", - "flaque", - "fleur", - "flocon", - "flore", - "flot", - "flou", - "fluide", - "fluor", - "flux", - "focus", - "foin", - "foire", - "foison", - "folie", - "fonction", - "fondre", - "fonte", - "force", - "forer", - "forger", - "forme", - "fort", - "fosse", - "fouet", - "fouine", - "foule", - "four", - "foyer", - "frais", - "franc", - "frapper", - "freiner", - "frimer", - "friser", - "frite", - "froid", - "froncer", - "fruit", - "fugue", - "fuir", - "fuite", - "fumer", - "fureur", - "furieux", - "fuser", - "fusil", - "futile", - "futur", - "gagner", - "gain", - "gala", - "galet", - "galop", - "gamme", - "gant", - "garage", - "garde", - "garer", - "gauche", - "gaufre", - "gaule", - "gaver", - "gazon", - "geler", - "genou", - "genre", - "gens", - "gercer", - "germer", - "geste", - "gibier", - "gicler", - "gilet", - "girafe", - "givre", - "glace", - "glisser", - "globe", - "gloire", - "gluant", - "gober", - "golf", - "gommer", - "gorge", - "gosier", - "goutte", - "grain", - "gramme", - "grand", - "gras", - "grave", - "gredin", - "griffure", - "griller", - "gris", - "gronder", - "gros", - "grotte", - "groupe", - "grue", - "guerrier", - "guetter", - "guider", - "guise", - "habiter", - "hache", - "haie", - "haine", - "halte", - "hamac", - "hanche", - "hangar", - "hanter", - "haras", - "hareng", - "harpe", - "hasard", - "hausse", - "haut", - "havre", - "herbe", - "heure", - "hibou", - "hier", - "histoire", - "hiver", - "hochet", - "homme", - "honneur", - "honte", - "horde", - "horizon", - "hormone", - "houle", - "housse", - "hublot", - "huile", - "huit", - "humain", - "humble", - "humide", - "humour", - "hurler", - "idole", - "igloo", - "ignorer", - "illusion", - "image", - "immense", - "immobile", - "imposer", - "impression", - "incapable", - "inconnu", - "index", - "indiquer", - "infime", - "injure", - "inox", - "inspirer", - "instant", - "intention", - "intime", - "inutile", - "inventer", - "inviter", - "iode", - "iris", - "issue", - "ivre", - "jade", - "jadis", - "jamais", - "jambe", - "janvier", - "jardin", - "jauge", - "jaunisse", - "jeter", - "jeton", - "jeudi", - "jeune", - "joie", - "joindre", - "joli", - "joueur", - "journal", - "judo", - "juge", - "juillet", - "juin", - "jument", - "jungle", - "jupe", - "jupon", - "jurer", - "juron", - "jury", - "jusque", - "juste", - "kayak", - "ketchup", - "kilo", - "kiwi", - "koala", - "label", - "lacet", - "lacune", - "laine", - "laisse", - "lait", - "lame", - "lancer", - "lande", - "laque", - "lard", - "largeur", - "larme", - "larve", - "lasso", - "laver", - "lendemain", - "lentement", - "lequel", - "lettre", - "leur", - "lever", - "levure", - "liane", - "libre", - "lien", - "lier", - "lieutenant", - "ligne", - "ligoter", - "liguer", - "limace", - "limer", - "limite", - "lingot", - "lion", - "lire", - "lisser", - "litre", - "livre", - "lobe", - "local", - "logis", - "loin", - "loisir", - "long", - "loque", - "lors", - "lotus", - "louer", - "loup", - "lourd", - "louve", - "loyer", - "lubie", - "lucide", - "lueur", - "luge", - "luire", - "lundi", - "lune", - "lustre", - "lutin", - "lutte", - "luxe", - "machine", - "madame", - "magie", - "magnifique", - "magot", - "maigre", - "main", - "mairie", - "maison", - "malade", - "malheur", - "malin", - "manche", - "manger", - "manier", - "manoir", - "manquer", - "marche", - "mardi", - "marge", - "mariage", - "marquer", - "mars", - "masque", - "masse", - "matin", - "mauvais", - "meilleur", - "melon", - "membre", - "menacer", - "mener", - "mensonge", - "mentir", - "menu", - "merci", - "merlu", - "mesure", - "mettre", - "meuble", - "meunier", - "meute", - "miche", - "micro", - "midi", - "miel", - "miette", - "mieux", - "milieu", - "mille", - "mimer", - "mince", - "mineur", - "ministre", - "minute", - "mirage", - "miroir", - "miser", - "mite", - "mixte", - "mobile", - "mode", - "module", - "moins", - "mois", - "moment", - "momie", - "monde", - "monsieur", - "monter", - "moquer", - "moral", - "morceau", - "mordre", - "morose", - "morse", - "mortier", - "morue", - "motif", - "motte", - "moudre", - "moule", - "mourir", - "mousse", - "mouton", - "mouvement", - "moyen", - "muer", - "muette", - "mugir", - "muguet", - "mulot", - "multiple", - "munir", - "muret", - "muse", - "musique", - "muter", - "nacre", - "nager", - "nain", - "naissance", - "narine", - "narrer", - "naseau", - "nasse", - "nation", - "nature", - "naval", - "navet", - "naviguer", - "navrer", - "neige", - "nerf", - "nerveux", - "neuf", - "neutre", - "neuve", - "neveu", - "niche", - "nier", - "niveau", - "noble", - "noce", - "nocif", - "noir", - "nomade", - "nombre", - "nommer", - "nord", - "norme", - "notaire", - "notice", - "notre", - "nouer", - "nougat", - "nourrir", - "nous", - "nouveau", - "novice", - "noyade", - "noyer", - "nuage", - "nuance", - "nuire", - "nuit", - "nulle", - "nuque", - "oasis", - "objet", - "obliger", - "obscur", - "observer", - "obtenir", - "obus", - "occasion", - "occuper", - "ocre", - "octet", - "odeur", - "odorat", - "offense", - "officier", - "offrir", - "ogive", - "oiseau", - "olive", - "ombre", - "onctueux", - "onduler", - "ongle", - "onze", - "opter", - "option", - "orageux", - "oral", - "orange", - "orbite", - "ordinaire", - "ordre", - "oreille", - "organe", - "orgie", - "orgueil", - "orient", - "origan", - "orner", - "orteil", - "ortie", - "oser", - "osselet", - "otage", - "otarie", - "ouate", - "oublier", - "ouest", - "ours", - "outil", - "outre", - "ouvert", - "ouvrir", - "ovale", - "ozone", - "pacte", - "page", - "paille", - "pain", - "paire", - "paix", - "palace", - "palissade", - "palmier", - "palpiter", - "panda", - "panneau", - "papa", - "papier", - "paquet", - "parc", - "pardi", - "parfois", - "parler", - "parmi", - "parole", - "partir", - "parvenir", - "passer", - "pastel", - "patin", - "patron", - "paume", - "pause", - "pauvre", - "paver", - "pavot", - "payer", - "pays", - "peau", - "peigne", - "peinture", - "pelage", - "pelote", - "pencher", - "pendre", - "penser", - "pente", - "percer", - "perdu", - "perle", - "permettre", - "personne", - "perte", - "peser", - "pesticide", - "petit", - "peuple", - "peur", - "phase", - "photo", - "phrase", - "piano", - "pied", - "pierre", - "pieu", - "pile", - "pilier", - "pilote", - "pilule", - "piment", - "pincer", - "pinson", - "pinte", - "pion", - "piquer", - "pirate", - "pire", - "piste", - "piton", - "pitre", - "pivot", - "pizza", - "placer", - "plage", - "plaire", - "plan", - "plaque", - "plat", - "plein", - "pleurer", - "pliage", - "plier", - "plonger", - "plot", - "pluie", - "plume", - "plus", - "pneu", - "poche", - "podium", - "poids", - "poil", - "point", - "poire", - "poison", - "poitrine", - "poivre", - "police", - "pollen", - "pomme", - "pompier", - "poncer", - "pondre", - "pont", - "portion", - "poser", - "position", - "possible", - "poste", - "potage", - "potin", - "pouce", - "poudre", - "poulet", - "poumon", - "poupe", - "pour", - "pousser", - "poutre", - "pouvoir", - "prairie", - "premier", - "prendre", - "presque", - "preuve", - "prier", - "primeur", - "prince", - "prison", - "priver", - "prix", - "prochain", - "produire", - "profond", - "proie", - "projet", - "promener", - "prononcer", - "propre", - "prose", - "prouver", - "prune", - "public", - "puce", - "pudeur", - "puiser", - "pull", - "pulpe", - "puma", - "punir", - "purge", - "putois", - "quand", - "quartier", - "quasi", - "quatre", - "quel", - "question", - "queue", - "quiche", - "quille", - "quinze", - "quitter", - "quoi", - "rabais", - "raboter", - "race", - "racheter", - "racine", - "racler", - "raconter", - "radar", - "radio", - "rafale", - "rage", - "ragot", - "raideur", - "raie", - "rail", - "raison", - "ramasser", - "ramener", - "rampe", - "rance", - "rang", - "rapace", - "rapide", - "rapport", - "rarement", - "rasage", - "raser", - "rasoir", - "rassurer", - "rater", - "ratio", - "rature", - "ravage", - "ravir", - "rayer", - "rayon", - "rebond", - "recevoir", - "recherche", - "record", - "reculer", - "redevenir", - "refuser", - "regard", - "regretter", - "rein", - "rejeter", - "rejoindre", - "relation", - "relever", - "religion", - "remarquer", - "remettre", - "remise", - "remonter", - "remplir", - "remuer", - "rencontre", - "rendre", - "renier", - "renoncer", - "rentrer", - "renverser", - "repas", - "repli", - "reposer", - "reproche", - "requin", - "respect", - "ressembler", - "reste", - "retard", - "retenir", - "retirer", - "retour", - "retrouver", - "revenir", - "revoir", - "revue", - "rhume", - "ricaner", - "riche", - "rideau", - "ridicule", - "rien", - "rigide", - "rincer", - "rire", - "risquer", - "rituel", - "rivage", - "rive", - "robe", - "robot", - "robuste", - "rocade", - "roche", - "rodeur", - "rogner", - "roman", - "rompre", - "ronce", - "rondeur", - "ronger", - "roque", - "rose", - "rosir", - "rotation", - "rotule", - "roue", - "rouge", - "rouler", - "route", - "ruban", - "rubis", - "ruche", - "rude", - "ruelle", - "ruer", - "rugby", - "rugir", - "ruine", - "rumeur", - "rural", - "ruse", - "rustre", - "sable", - "sabot", - "sabre", - "sacre", - "sage", - "saint", - "saisir", - "salade", - "salive", - "salle", - "salon", - "salto", - "salut", - "salve", - "samba", - "sandale", - "sanguin", - "sapin", - "sarcasme", - "satisfaire", - "sauce", - "sauf", - "sauge", - "saule", - "sauna", - "sauter", - "sauver", - "savoir", - "science", - "scoop", - "score", - "second", - "secret", - "secte", - "seigneur", - "sein", - "seize", - "selle", - "selon", - "semaine", - "sembler", - "semer", - "semis", - "sensuel", - "sentir", - "sept", - "serpe", - "serrer", - "sertir", - "service", - "seuil", - "seulement", - "short", - "sien", - "sigle", - "signal", - "silence", - "silo", - "simple", - "singe", - "sinon", - "sinus", - "sioux", - "sirop", - "site", - "situation", - "skier", - "snob", - "sobre", - "social", - "socle", - "sodium", - "soigner", - "soir", - "soixante", - "soja", - "solaire", - "soldat", - "soleil", - "solide", - "solo", - "solvant", - "sombre", - "somme", - "somnoler", - "sondage", - "songeur", - "sonner", - "sorte", - "sosie", - "sottise", - "souci", - "soudain", - "souffrir", - "souhaiter", - "soulever", - "soumettre", - "soupe", - "sourd", - "soustraire", - "soutenir", - "souvent", - "soyeux", - "spectacle", - "sport", - "stade", - "stagiaire", - "stand", - "star", - "statue", - "stock", - "stop", - "store", - "style", - "suave", - "subir", - "sucre", - "suer", - "suffire", - "suie", - "suite", - "suivre", - "sujet", - "sulfite", - "supposer", - "surf", - "surprendre", - "surtout", - "surveiller", - "tabac", - "table", - "tabou", - "tache", - "tacler", - "tacot", - "tact", - "taie", - "taille", - "taire", - "talon", - "talus", - "tandis", - "tango", - "tanin", - "tant", - "taper", - "tapis", - "tard", - "tarif", - "tarot", - "tarte", - "tasse", - "taureau", - "taux", - "taverne", - "taxer", - "taxi", - "tellement", - "temple", - "tendre", - "tenir", - "tenter", - "tenu", - "terme", - "ternir", - "terre", - "test", - "texte", - "thym", - "tibia", - "tiers", - "tige", - "tipi", - "tique", - "tirer", - "tissu", - "titre", - "toast", - "toge", - "toile", - "toiser", - "toiture", - "tomber", - "tome", - "tonne", - "tonte", - "toque", - "torse", - "tortue", - "totem", - "toucher", - "toujours", - "tour", - "tousser", - "tout", - "toux", - "trace", - "train", - "trame", - "tranquille", - "travail", - "trembler", - "trente", - "tribu", - "trier", - "trio", - "tripe", - "triste", - "troc", - "trois", - "tromper", - "tronc", - "trop", - "trotter", - "trouer", - "truc", - "truite", - "tuba", - "tuer", - "tuile", - "turbo", - "tutu", - "tuyau", - "type", - "union", - "unique", - "unir", - "unisson", - "untel", - "urne", - "usage", - "user", - "usiner", - "usure", - "utile", - "vache", - "vague", - "vaincre", - "valeur", - "valoir", - "valser", - "valve", - "vampire", - "vaseux", - "vaste", - "veau", - "veille", - "veine", - "velours", - "velu", - "vendre", - "venir", - "vent", - "venue", - "verbe", - "verdict", - "version", - "vertige", - "verve", - "veste", - "veto", - "vexer", - "vice", - "victime", - "vide", - "vieil", - "vieux", - "vigie", - "vigne", - "ville", - "vingt", - "violent", - "virer", - "virus", - "visage", - "viser", - "visite", - "visuel", - "vitamine", - "vitrine", - "vivant", - "vivre", - "vocal", - "vodka", - "vogue", - "voici", - "voile", - "voir", - "voisin", - "voiture", - "volaille", - "volcan", - "voler", - "volt", - "votant", - "votre", - "vouer", - "vouloir", - "vous", - "voyage", - "voyou", - "vrac", - "vrai", - "yacht", - "yeti", - "yeux", - "yoga", - "zeste", - "zinc", - "zone", - "zoom" -] \ No newline at end of file diff --git a/monero-seed/src/words/it.rs b/monero-seed/src/words/it.rs deleted file mode 100644 index 343984e6ce..0000000000 --- a/monero-seed/src/words/it.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "abbinare", - "abbonato", - "abisso", - "abitare", - "abominio", - "accadere", - "accesso", - "acciaio", - "accordo", - "accumulo", - "acido", - "acqua", - "acrobata", - "acustico", - "adattare", - "addetto", - "addio", - "addome", - "adeguato", - "aderire", - "adorare", - "adottare", - "adozione", - "adulto", - "aereo", - "aerobica", - "affare", - "affetto", - "affidare", - "affogato", - "affronto", - "africano", - "afrodite", - "agenzia", - "aggancio", - "aggeggio", - "aggiunta", - "agio", - "agire", - "agitare", - "aglio", - "agnello", - "agosto", - "aiutare", - "albero", - "albo", - "alce", - "alchimia", - "alcool", - "alfabeto", - "algebra", - "alimento", - "allarme", - "alleanza", - "allievo", - "alloggio", - "alluce", - "alpi", - "alterare", - "altro", - "aluminio", - "amante", - "amarezza", - "ambiente", - "ambrosia", - "america", - "amico", - "ammalare", - "ammirare", - "amnesia", - "amnistia", - "amore", - "ampliare", - "amputare", - "analisi", - "anamnesi", - "ananas", - "anarchia", - "anatra", - "anca", - "ancorato", - "andare", - "androide", - "aneddoto", - "anello", - "angelo", - "angolino", - "anguilla", - "anidride", - "anima", - "annegare", - "anno", - "annuncio", - "anomalia", - "antenna", - "anticipo", - "aperto", - "apostolo", - "appalto", - "appello", - "appiglio", - "applauso", - "appoggio", - "appurare", - "aprile", - "aquila", - "arabo", - "arachidi", - "aragosta", - "arancia", - "arbitrio", - "archivio", - "arco", - "argento", - "argilla", - "aria", - "ariete", - "arma", - "armonia", - "aroma", - "arrivare", - "arrosto", - "arsenale", - "arte", - "artiglio", - "asfalto", - "asfissia", - "asino", - "asparagi", - "aspirina", - "assalire", - "assegno", - "assolto", - "assurdo", - "asta", - "astratto", - "atlante", - "atletica", - "atomo", - "atropina", - "attacco", - "attesa", - "attico", - "atto", - "attrarre", - "auguri", - "aula", - "aumento", - "aurora", - "auspicio", - "autista", - "auto", - "autunno", - "avanzare", - "avarizia", - "avere", - "aviatore", - "avido", - "avorio", - "avvenire", - "avviso", - "avvocato", - "azienda", - "azione", - "azzardo", - "azzurro", - "babbuino", - "bacio", - "badante", - "baffi", - "bagaglio", - "bagliore", - "bagno", - "balcone", - "balena", - "ballare", - "balordo", - "balsamo", - "bambola", - "bancomat", - "banda", - "barato", - "barba", - "barista", - "barriera", - "basette", - "basilico", - "bassista", - "bastare", - "battello", - "bavaglio", - "beccare", - "beduino", - "bellezza", - "bene", - "benzina", - "berretto", - "bestia", - "bevitore", - "bianco", - "bibbia", - "biberon", - "bibita", - "bici", - "bidone", - "bilancia", - "biliardo", - "binario", - "binocolo", - "biologia", - "biondina", - "biopsia", - "biossido", - "birbante", - "birra", - "biscotto", - "bisogno", - "bistecca", - "bivio", - "blindare", - "bloccare", - "bocca", - "bollire", - "bombola", - "bonifico", - "borghese", - "borsa", - "bottino", - "botulino", - "braccio", - "bradipo", - "branco", - "bravo", - "bresaola", - "bretelle", - "brevetto", - "briciola", - "brigante", - "brillare", - "brindare", - "brivido", - "broccoli", - "brontolo", - "bruciare", - "brufolo", - "bucare", - "buddista", - "budino", - "bufera", - "buffo", - "bugiardo", - "buio", - "buono", - "burrone", - "bussola", - "bustina", - "buttare", - "cabernet", - "cabina", - "cacao", - "cacciare", - "cactus", - "cadavere", - "caffe", - "calamari", - "calcio", - "caldaia", - "calmare", - "calunnia", - "calvario", - "calzone", - "cambiare", - "camera", - "camion", - "cammello", - "campana", - "canarino", - "cancello", - "candore", - "cane", - "canguro", - "cannone", - "canoa", - "cantare", - "canzone", - "caos", - "capanna", - "capello", - "capire", - "capo", - "capperi", - "capra", - "capsula", - "caraffa", - "carbone", - "carciofo", - "cardigan", - "carenza", - "caricare", - "carota", - "carrello", - "carta", - "casa", - "cascare", - "caserma", - "cashmere", - "casino", - "cassetta", - "castello", - "catalogo", - "catena", - "catorcio", - "cattivo", - "causa", - "cauzione", - "cavallo", - "caverna", - "caviglia", - "cavo", - "cazzotto", - "celibato", - "cemento", - "cenare", - "centrale", - "ceramica", - "cercare", - "ceretta", - "cerniera", - "certezza", - "cervello", - "cessione", - "cestino", - "cetriolo", - "chiave", - "chiedere", - "chilo", - "chimera", - "chiodo", - "chirurgo", - "chitarra", - "chiudere", - "ciabatta", - "ciao", - "cibo", - "ciccia", - "cicerone", - "ciclone", - "cicogna", - "cielo", - "cifra", - "cigno", - "ciliegia", - "cimitero", - "cinema", - "cinque", - "cintura", - "ciondolo", - "ciotola", - "cipolla", - "cippato", - "circuito", - "cisterna", - "citofono", - "ciuccio", - "civetta", - "civico", - "clausola", - "cliente", - "clima", - "clinica", - "cobra", - "coccole", - "cocktail", - "cocomero", - "codice", - "coesione", - "cogliere", - "cognome", - "colla", - "colomba", - "colpire", - "coltello", - "comando", - "comitato", - "commedia", - "comodino", - "compagna", - "comune", - "concerto", - "condotto", - "conforto", - "congiura", - "coniglio", - "consegna", - "conto", - "convegno", - "coperta", - "copia", - "coprire", - "corazza", - "corda", - "corleone", - "cornice", - "corona", - "corpo", - "corrente", - "corsa", - "cortesia", - "corvo", - "coso", - "costume", - "cotone", - "cottura", - "cozza", - "crampo", - "cratere", - "cravatta", - "creare", - "credere", - "crema", - "crescere", - "crimine", - "criterio", - "croce", - "crollare", - "cronaca", - "crostata", - "croupier", - "cubetto", - "cucciolo", - "cucina", - "cultura", - "cuoco", - "cuore", - "cupido", - "cupola", - "cura", - "curva", - "cuscino", - "custode", - "danzare", - "data", - "decennio", - "decidere", - "decollo", - "dedicare", - "dedurre", - "definire", - "delegare", - "delfino", - "delitto", - "demone", - "dentista", - "denuncia", - "deposito", - "derivare", - "deserto", - "designer", - "destino", - "detonare", - "dettagli", - "diagnosi", - "dialogo", - "diamante", - "diario", - "diavolo", - "dicembre", - "difesa", - "digerire", - "digitare", - "diluvio", - "dinamica", - "dipinto", - "diploma", - "diramare", - "dire", - "dirigere", - "dirupo", - "discesa", - "disdetta", - "disegno", - "disporre", - "dissenso", - "distacco", - "dito", - "ditta", - "diva", - "divenire", - "dividere", - "divorare", - "docente", - "dolcetto", - "dolore", - "domatore", - "domenica", - "dominare", - "donatore", - "donna", - "dorato", - "dormire", - "dorso", - "dosaggio", - "dottore", - "dovere", - "download", - "dragone", - "dramma", - "dubbio", - "dubitare", - "duetto", - "durata", - "ebbrezza", - "eccesso", - "eccitare", - "eclissi", - "economia", - "edera", - "edificio", - "editore", - "edizione", - "educare", - "effetto", - "egitto", - "egiziano", - "elastico", - "elefante", - "eleggere", - "elemento", - "elenco", - "elezione", - "elmetto", - "elogio", - "embrione", - "emergere", - "emettere", - "eminenza", - "emisfero", - "emozione", - "empatia", - "energia", - "enfasi", - "enigma", - "entrare", - "enzima", - "epidemia", - "epilogo", - "episodio", - "epoca", - "equivoco", - "erba", - "erede", - "eroe", - "erotico", - "errore", - "eruzione", - "esaltare", - "esame", - "esaudire", - "eseguire", - "esempio", - "esigere", - "esistere", - "esito", - "esperto", - "espresso", - "essere", - "estasi", - "esterno", - "estrarre", - "eterno", - "etica", - "euforico", - "europa", - "evacuare", - "evasione", - "evento", - "evidenza", - "evitare", - "evolvere", - "fabbrica", - "facciata", - "fagiano", - "fagotto", - "falco", - "fame", - "famiglia", - "fanale", - "fango", - "fantasia", - "farfalla", - "farmacia", - "faro", - "fase", - "fastidio", - "faticare", - "fatto", - "favola", - "febbre", - "femmina", - "femore", - "fenomeno", - "fermata", - "feromoni", - "ferrari", - "fessura", - "festa", - "fiaba", - "fiamma", - "fianco", - "fiat", - "fibbia", - "fidare", - "fieno", - "figa", - "figlio", - "figura", - "filetto", - "filmato", - "filosofo", - "filtrare", - "finanza", - "finestra", - "fingere", - "finire", - "finta", - "finzione", - "fiocco", - "fioraio", - "firewall", - "firmare", - "fisico", - "fissare", - "fittizio", - "fiume", - "flacone", - "flagello", - "flirtare", - "flusso", - "focaccia", - "foglio", - "fognario", - "follia", - "fonderia", - "fontana", - "forbici", - "forcella", - "foresta", - "forgiare", - "formare", - "fornace", - "foro", - "fortuna", - "forzare", - "fosforo", - "fotoni", - "fracasso", - "fragola", - "frantumi", - "fratello", - "frazione", - "freccia", - "freddo", - "frenare", - "fresco", - "friggere", - "frittata", - "frivolo", - "frizione", - "fronte", - "frullato", - "frumento", - "frusta", - "frutto", - "fucile", - "fuggire", - "fulmine", - "fumare", - "funzione", - "fuoco", - "furbizia", - "furgone", - "furia", - "furore", - "fusibile", - "fuso", - "futuro", - "gabbiano", - "galassia", - "gallina", - "gamba", - "gancio", - "garanzia", - "garofano", - "gasolio", - "gatto", - "gazebo", - "gazzetta", - "gelato", - "gemelli", - "generare", - "genitori", - "gennaio", - "geologia", - "germania", - "gestire", - "gettare", - "ghepardo", - "ghiaccio", - "giaccone", - "giaguaro", - "giallo", - "giappone", - "giardino", - "gigante", - "gioco", - "gioiello", - "giorno", - "giovane", - "giraffa", - "giudizio", - "giurare", - "giusto", - "globo", - "gloria", - "glucosio", - "gnocca", - "gocciola", - "godere", - "gomito", - "gomma", - "gonfiare", - "gorilla", - "governo", - "gradire", - "graffiti", - "granchio", - "grappolo", - "grasso", - "grattare", - "gridare", - "grissino", - "grondaia", - "grugnito", - "gruppo", - "guadagno", - "guaio", - "guancia", - "guardare", - "gufo", - "guidare", - "guscio", - "gusto", - "icona", - "idea", - "identico", - "idolo", - "idoneo", - "idrante", - "idrogeno", - "igiene", - "ignoto", - "imbarco", - "immagine", - "immobile", - "imparare", - "impedire", - "impianto", - "importo", - "impresa", - "impulso", - "incanto", - "incendio", - "incidere", - "incontro", - "incrocia", - "incubo", - "indagare", - "indice", - "indotto", - "infanzia", - "inferno", - "infinito", - "infranto", - "ingerire", - "inglese", - "ingoiare", - "ingresso", - "iniziare", - "innesco", - "insalata", - "inserire", - "insicuro", - "insonnia", - "insulto", - "interno", - "introiti", - "invasori", - "inverno", - "invito", - "invocare", - "ipnosi", - "ipocrita", - "ipotesi", - "ironia", - "irrigare", - "iscritto", - "isola", - "ispirare", - "isterico", - "istinto", - "istruire", - "italiano", - "jazz", - "labbra", - "labrador", - "ladro", - "lago", - "lamento", - "lampone", - "lancetta", - "lanterna", - "lapide", - "larva", - "lasagne", - "lasciare", - "lastra", - "latte", - "laurea", - "lavagna", - "lavorare", - "leccare", - "legare", - "leggere", - "lenzuolo", - "leone", - "lepre", - "letargo", - "lettera", - "levare", - "levitare", - "lezione", - "liberare", - "libidine", - "libro", - "licenza", - "lievito", - "limite", - "lince", - "lingua", - "liquore", - "lire", - "listino", - "litigare", - "litro", - "locale", - "lottare", - "lucciola", - "lucidare", - "luglio", - "luna", - "macchina", - "madama", - "madre", - "maestro", - "maggio", - "magico", - "maglione", - "magnolia", - "mago", - "maialino", - "maionese", - "malattia", - "male", - "malloppo", - "mancare", - "mandorla", - "mangiare", - "manico", - "manopola", - "mansarda", - "mantello", - "manubrio", - "manzo", - "mappa", - "mare", - "margine", - "marinaio", - "marmotta", - "marocco", - "martello", - "marzo", - "maschera", - "matrice", - "maturare", - "mazzetta", - "meandri", - "medaglia", - "medico", - "medusa", - "megafono", - "melone", - "membrana", - "menta", - "mercato", - "meritare", - "merluzzo", - "mese", - "mestiere", - "metafora", - "meteo", - "metodo", - "mettere", - "miele", - "miglio", - "miliardo", - "mimetica", - "minatore", - "minuto", - "miracolo", - "mirtillo", - "missile", - "mistero", - "misura", - "mito", - "mobile", - "moda", - "moderare", - "moglie", - "molecola", - "molle", - "momento", - "moneta", - "mongolia", - "monologo", - "montagna", - "morale", - "morbillo", - "mordere", - "mosaico", - "mosca", - "mostro", - "motivare", - "moto", - "mulino", - "mulo", - "muovere", - "muraglia", - "muscolo", - "museo", - "musica", - "mutande", - "nascere", - "nastro", - "natale", - "natura", - "nave", - "navigare", - "negare", - "negozio", - "nemico", - "nero", - "nervo", - "nessuno", - "nettare", - "neutroni", - "neve", - "nevicare", - "nicotina", - "nido", - "nipote", - "nocciola", - "noleggio", - "nome", - "nonno", - "norvegia", - "notare", - "notizia", - "nove", - "nucleo", - "nuda", - "nuotare", - "nutrire", - "obbligo", - "occhio", - "occupare", - "oceano", - "odissea", - "odore", - "offerta", - "officina", - "offrire", - "oggetto", - "oggi", - "olfatto", - "olio", - "oliva", - "ombelico", - "ombrello", - "omuncolo", - "ondata", - "onore", - "opera", - "opinione", - "opuscolo", - "opzione", - "orario", - "orbita", - "orchidea", - "ordine", - "orecchio", - "orgasmo", - "orgoglio", - "origine", - "orologio", - "oroscopo", - "orso", - "oscurare", - "ospedale", - "ospite", - "ossigeno", - "ostacolo", - "ostriche", - "ottenere", - "ottimo", - "ottobre", - "ovest", - "pacco", - "pace", - "pacifico", - "padella", - "pagare", - "pagina", - "pagnotta", - "palazzo", - "palestra", - "palpebre", - "pancetta", - "panfilo", - "panino", - "pannello", - "panorama", - "papa", - "paperino", - "paradiso", - "parcella", - "parente", - "parlare", - "parodia", - "parrucca", - "partire", - "passare", - "pasta", - "patata", - "patente", - "patogeno", - "patriota", - "pausa", - "pazienza", - "peccare", - "pecora", - "pedalare", - "pelare", - "pena", - "pendenza", - "penisola", - "pennello", - "pensare", - "pentirsi", - "percorso", - "perdono", - "perfetto", - "perizoma", - "perla", - "permesso", - "persona", - "pesare", - "pesce", - "peso", - "petardo", - "petrolio", - "pezzo", - "piacere", - "pianeta", - "piastra", - "piatto", - "piazza", - "piccolo", - "piede", - "piegare", - "pietra", - "pigiama", - "pigliare", - "pigrizia", - "pilastro", - "pilota", - "pinguino", - "pioggia", - "piombo", - "pionieri", - "piovra", - "pipa", - "pirata", - "pirolisi", - "piscina", - "pisolino", - "pista", - "pitone", - "piumino", - "pizza", - "plastica", - "platino", - "poesia", - "poiana", - "polaroid", - "polenta", - "polimero", - "pollo", - "polmone", - "polpetta", - "poltrona", - "pomodoro", - "pompa", - "popolo", - "porco", - "porta", - "porzione", - "possesso", - "postino", - "potassio", - "potere", - "poverino", - "pranzo", - "prato", - "prefisso", - "prelievo", - "premio", - "prendere", - "prestare", - "pretesa", - "prezzo", - "primario", - "privacy", - "problema", - "processo", - "prodotto", - "profeta", - "progetto", - "promessa", - "pronto", - "proposta", - "proroga", - "prossimo", - "proteina", - "prova", - "prudenza", - "pubblico", - "pudore", - "pugilato", - "pulire", - "pulsante", - "puntare", - "pupazzo", - "puzzle", - "quaderno", - "qualcuno", - "quarzo", - "quercia", - "quintale", - "rabbia", - "racconto", - "radice", - "raffica", - "ragazza", - "ragione", - "rammento", - "ramo", - "rana", - "randagio", - "rapace", - "rapinare", - "rapporto", - "rasatura", - "ravioli", - "reagire", - "realista", - "reattore", - "reazione", - "recitare", - "recluso", - "record", - "recupero", - "redigere", - "regalare", - "regina", - "regola", - "relatore", - "reliquia", - "remare", - "rendere", - "reparto", - "resina", - "resto", - "rete", - "retorica", - "rettile", - "revocare", - "riaprire", - "ribadire", - "ribelle", - "ricambio", - "ricetta", - "richiamo", - "ricordo", - "ridurre", - "riempire", - "riferire", - "riflesso", - "righello", - "rilancio", - "rilevare", - "rilievo", - "rimanere", - "rimborso", - "rinforzo", - "rinuncia", - "riparo", - "ripetere", - "riposare", - "ripulire", - "risalita", - "riscatto", - "riserva", - "riso", - "rispetto", - "ritaglio", - "ritmo", - "ritorno", - "ritratto", - "rituale", - "riunione", - "riuscire", - "riva", - "robotica", - "rondine", - "rosa", - "rospo", - "rosso", - "rotonda", - "rotta", - "roulotte", - "rubare", - "rubrica", - "ruffiano", - "rumore", - "ruota", - "ruscello", - "sabbia", - "sacco", - "saggio", - "sale", - "salire", - "salmone", - "salto", - "salutare", - "salvia", - "sangue", - "sanzioni", - "sapere", - "sapienza", - "sarcasmo", - "sardine", - "sartoria", - "sbalzo", - "sbarcare", - "sberla", - "sborsare", - "scadenza", - "scafo", - "scala", - "scambio", - "scappare", - "scarpa", - "scatola", - "scelta", - "scena", - "sceriffo", - "scheggia", - "schiuma", - "sciarpa", - "scienza", - "scimmia", - "sciopero", - "scivolo", - "sclerare", - "scolpire", - "sconto", - "scopa", - "scordare", - "scossa", - "scrivere", - "scrupolo", - "scuderia", - "scultore", - "scuola", - "scusare", - "sdraiare", - "secolo", - "sedativo", - "sedere", - "sedia", - "segare", - "segreto", - "seguire", - "semaforo", - "seme", - "senape", - "seno", - "sentiero", - "separare", - "sepolcro", - "sequenza", - "serata", - "serpente", - "servizio", - "sesso", - "seta", - "settore", - "sfamare", - "sfera", - "sfidare", - "sfiorare", - "sfogare", - "sgabello", - "sicuro", - "siepe", - "sigaro", - "silenzio", - "silicone", - "simbiosi", - "simpatia", - "simulare", - "sinapsi", - "sindrome", - "sinergia", - "sinonimo", - "sintonia", - "sirena", - "siringa", - "sistema", - "sito", - "smalto", - "smentire", - "smontare", - "soccorso", - "socio", - "soffitto", - "software", - "soggetto", - "sogliola", - "sognare", - "soldi", - "sole", - "sollievo", - "solo", - "sommario", - "sondare", - "sonno", - "sorpresa", - "sorriso", - "sospiro", - "sostegno", - "sovrano", - "spaccare", - "spada", - "spagnolo", - "spalla", - "sparire", - "spavento", - "spazio", - "specchio", - "spedire", - "spegnere", - "spendere", - "speranza", - "spessore", - "spezzare", - "spiaggia", - "spiccare", - "spiegare", - "spiffero", - "spingere", - "sponda", - "sporcare", - "spostare", - "spremuta", - "spugna", - "spumante", - "spuntare", - "squadra", - "squillo", - "staccare", - "stadio", - "stagione", - "stallone", - "stampa", - "stancare", - "starnuto", - "statura", - "stella", - "stendere", - "sterzo", - "stilista", - "stimolo", - "stinco", - "stiva", - "stoffa", - "storia", - "strada", - "stregone", - "striscia", - "studiare", - "stufa", - "stupendo", - "subire", - "successo", - "sudare", - "suono", - "superare", - "supporto", - "surfista", - "sussurro", - "svelto", - "svenire", - "sviluppo", - "svolta", - "svuotare", - "tabacco", - "tabella", - "tabu", - "tacchino", - "tacere", - "taglio", - "talento", - "tangente", - "tappeto", - "tartufo", - "tassello", - "tastiera", - "tavolo", - "tazza", - "teatro", - "tedesco", - "telaio", - "telefono", - "tema", - "temere", - "tempo", - "tendenza", - "tenebre", - "tensione", - "tentare", - "teologia", - "teorema", - "termica", - "terrazzo", - "teschio", - "tesi", - "tesoro", - "tessera", - "testa", - "thriller", - "tifoso", - "tigre", - "timbrare", - "timido", - "tinta", - "tirare", - "tisana", - "titano", - "titolo", - "toccare", - "togliere", - "topolino", - "torcia", - "torrente", - "tovaglia", - "traffico", - "tragitto", - "training", - "tramonto", - "transito", - "trapezio", - "trasloco", - "trattore", - "trazione", - "treccia", - "tregua", - "treno", - "triciclo", - "tridente", - "trilogia", - "tromba", - "troncare", - "trota", - "trovare", - "trucco", - "tubo", - "tulipano", - "tumulto", - "tunisia", - "tuono", - "turista", - "tuta", - "tutelare", - "tutore", - "ubriaco", - "uccello", - "udienza", - "udito", - "uffa", - "umanoide", - "umore", - "unghia", - "unguento", - "unicorno", - "unione", - "universo", - "uomo", - "uragano", - "uranio", - "urlare", - "uscire", - "utente", - "utilizzo", - "vacanza", - "vacca", - "vaglio", - "vagonata", - "valle", - "valore", - "valutare", - "valvola", - "vampiro", - "vaniglia", - "vanto", - "vapore", - "variante", - "vasca", - "vaselina", - "vassoio", - "vedere", - "vegetale", - "veglia", - "veicolo", - "vela", - "veleno", - "velivolo", - "velluto", - "vendere", - "venerare", - "venire", - "vento", - "veranda", - "verbo", - "verdura", - "vergine", - "verifica", - "vernice", - "vero", - "verruca", - "versare", - "vertebra", - "vescica", - "vespaio", - "vestito", - "vesuvio", - "veterano", - "vetro", - "vetta", - "viadotto", - "viaggio", - "vibrare", - "vicenda", - "vichingo", - "vietare", - "vigilare", - "vigneto", - "villa", - "vincere", - "violino", - "vipera", - "virgola", - "virtuoso", - "visita", - "vita", - "vitello", - "vittima", - "vivavoce", - "vivere", - "viziato", - "voglia", - "volare", - "volpe", - "volto", - "volume", - "vongole", - "voragine", - "vortice", - "votare", - "vulcano", - "vuotare", - "zabaione", - "zaffiro", - "zainetto", - "zampa", - "zanzara", - "zattera", - "zavorra", - "zenzero", - "zero", - "zingaro", - "zittire", - "zoccolo", - "zolfo", - "zombie", - "zucchero" -] \ No newline at end of file diff --git a/monero-seed/src/words/ja.rs b/monero-seed/src/words/ja.rs deleted file mode 100644 index da2d9fb607..0000000000 --- a/monero-seed/src/words/ja.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "あいこくしん", - "あいさつ", - "あいだ", - "あおぞら", - "あかちゃん", - "あきる", - "あけがた", - "あける", - "あこがれる", - "あさい", - "あさひ", - "あしあと", - "あじわう", - "あずかる", - "あずき", - "あそぶ", - "あたえる", - "あたためる", - "あたりまえ", - "あたる", - "あつい", - "あつかう", - "あっしゅく", - "あつまり", - "あつめる", - "あてな", - "あてはまる", - "あひる", - "あぶら", - "あぶる", - "あふれる", - "あまい", - "あまど", - "あまやかす", - "あまり", - "あみもの", - "あめりか", - "あやまる", - "あゆむ", - "あらいぐま", - "あらし", - "あらすじ", - "あらためる", - "あらゆる", - "あらわす", - "ありがとう", - "あわせる", - "あわてる", - "あんい", - "あんがい", - "あんこ", - "あんぜん", - "あんてい", - "あんない", - "あんまり", - "いいだす", - "いおん", - "いがい", - "いがく", - "いきおい", - "いきなり", - "いきもの", - "いきる", - "いくじ", - "いくぶん", - "いけばな", - "いけん", - "いこう", - "いこく", - "いこつ", - "いさましい", - "いさん", - "いしき", - "いじゅう", - "いじょう", - "いじわる", - "いずみ", - "いずれ", - "いせい", - "いせえび", - "いせかい", - "いせき", - "いぜん", - "いそうろう", - "いそがしい", - "いだい", - "いだく", - "いたずら", - "いたみ", - "いたりあ", - "いちおう", - "いちじ", - "いちど", - "いちば", - "いちぶ", - "いちりゅう", - "いつか", - "いっしゅん", - "いっせい", - "いっそう", - "いったん", - "いっち", - "いってい", - "いっぽう", - "いてざ", - "いてん", - "いどう", - "いとこ", - "いない", - "いなか", - "いねむり", - "いのち", - "いのる", - "いはつ", - "いばる", - "いはん", - "いびき", - "いひん", - "いふく", - "いへん", - "いほう", - "いみん", - "いもうと", - "いもたれ", - "いもり", - "いやがる", - "いやす", - "いよかん", - "いよく", - "いらい", - "いらすと", - "いりぐち", - "いりょう", - "いれい", - "いれもの", - "いれる", - "いろえんぴつ", - "いわい", - "いわう", - "いわかん", - "いわば", - "いわゆる", - "いんげんまめ", - "いんさつ", - "いんしょう", - "いんよう", - "うえき", - "うえる", - "うおざ", - "うがい", - "うかぶ", - "うかべる", - "うきわ", - "うくらいな", - "うくれれ", - "うけたまわる", - "うけつけ", - "うけとる", - "うけもつ", - "うける", - "うごかす", - "うごく", - "うこん", - "うさぎ", - "うしなう", - "うしろがみ", - "うすい", - "うすぎ", - "うすぐらい", - "うすめる", - "うせつ", - "うちあわせ", - "うちがわ", - "うちき", - "うちゅう", - "うっかり", - "うつくしい", - "うったえる", - "うつる", - "うどん", - "うなぎ", - "うなじ", - "うなずく", - "うなる", - "うねる", - "うのう", - "うぶげ", - "うぶごえ", - "うまれる", - "うめる", - "うもう", - "うやまう", - "うよく", - "うらがえす", - "うらぐち", - "うらない", - "うりあげ", - "うりきれ", - "うるさい", - "うれしい", - "うれゆき", - "うれる", - "うろこ", - "うわき", - "うわさ", - "うんこう", - "うんちん", - "うんてん", - "うんどう", - "えいえん", - "えいが", - "えいきょう", - "えいご", - "えいせい", - "えいぶん", - "えいよう", - "えいわ", - "えおり", - "えがお", - "えがく", - "えきたい", - "えくせる", - "えしゃく", - "えすて", - "えつらん", - "えのぐ", - "えほうまき", - "えほん", - "えまき", - "えもじ", - "えもの", - "えらい", - "えらぶ", - "えりあ", - "えんえん", - "えんかい", - "えんぎ", - "えんげき", - "えんしゅう", - "えんぜつ", - "えんそく", - "えんちょう", - "えんとつ", - "おいかける", - "おいこす", - "おいしい", - "おいつく", - "おうえん", - "おうさま", - "おうじ", - "おうせつ", - "おうたい", - "おうふく", - "おうべい", - "おうよう", - "おえる", - "おおい", - "おおう", - "おおどおり", - "おおや", - "おおよそ", - "おかえり", - "おかず", - "おがむ", - "おかわり", - "おぎなう", - "おきる", - "おくさま", - "おくじょう", - "おくりがな", - "おくる", - "おくれる", - "おこす", - "おこなう", - "おこる", - "おさえる", - "おさない", - "おさめる", - "おしいれ", - "おしえる", - "おじぎ", - "おじさん", - "おしゃれ", - "おそらく", - "おそわる", - "おたがい", - "おたく", - "おだやか", - "おちつく", - "おっと", - "おつり", - "おでかけ", - "おとしもの", - "おとなしい", - "おどり", - "おどろかす", - "おばさん", - "おまいり", - "おめでとう", - "おもいで", - "おもう", - "おもたい", - "おもちゃ", - "おやつ", - "おやゆび", - "およぼす", - "おらんだ", - "おろす", - "おんがく", - "おんけい", - "おんしゃ", - "おんせん", - "おんだん", - "おんちゅう", - "おんどけい", - "かあつ", - "かいが", - "がいき", - "がいけん", - "がいこう", - "かいさつ", - "かいしゃ", - "かいすいよく", - "かいぜん", - "かいぞうど", - "かいつう", - "かいてん", - "かいとう", - "かいふく", - "がいへき", - "かいほう", - "かいよう", - "がいらい", - "かいわ", - "かえる", - "かおり", - "かかえる", - "かがく", - "かがし", - "かがみ", - "かくご", - "かくとく", - "かざる", - "がぞう", - "かたい", - "かたち", - "がちょう", - "がっきゅう", - "がっこう", - "がっさん", - "がっしょう", - "かなざわし", - "かのう", - "がはく", - "かぶか", - "かほう", - "かほご", - "かまう", - "かまぼこ", - "かめれおん", - "かゆい", - "かようび", - "からい", - "かるい", - "かろう", - "かわく", - "かわら", - "がんか", - "かんけい", - "かんこう", - "かんしゃ", - "かんそう", - "かんたん", - "かんち", - "がんばる", - "きあい", - "きあつ", - "きいろ", - "ぎいん", - "きうい", - "きうん", - "きえる", - "きおう", - "きおく", - "きおち", - "きおん", - "きかい", - "きかく", - "きかんしゃ", - "ききて", - "きくばり", - "きくらげ", - "きけんせい", - "きこう", - "きこえる", - "きこく", - "きさい", - "きさく", - "きさま", - "きさらぎ", - "ぎじかがく", - "ぎしき", - "ぎじたいけん", - "ぎじにってい", - "ぎじゅつしゃ", - "きすう", - "きせい", - "きせき", - "きせつ", - "きそう", - "きぞく", - "きぞん", - "きたえる", - "きちょう", - "きつえん", - "ぎっちり", - "きつつき", - "きつね", - "きてい", - "きどう", - "きどく", - "きない", - "きなが", - "きなこ", - "きぬごし", - "きねん", - "きのう", - "きのした", - "きはく", - "きびしい", - "きひん", - "きふく", - "きぶん", - "きぼう", - "きほん", - "きまる", - "きみつ", - "きむずかしい", - "きめる", - "きもだめし", - "きもち", - "きもの", - "きゃく", - "きやく", - "ぎゅうにく", - "きよう", - "きょうりゅう", - "きらい", - "きらく", - "きりん", - "きれい", - "きれつ", - "きろく", - "ぎろん", - "きわめる", - "ぎんいろ", - "きんかくじ", - "きんじょ", - "きんようび", - "ぐあい", - "くいず", - "くうかん", - "くうき", - "くうぐん", - "くうこう", - "ぐうせい", - "くうそう", - "ぐうたら", - "くうふく", - "くうぼ", - "くかん", - "くきょう", - "くげん", - "ぐこう", - "くさい", - "くさき", - "くさばな", - "くさる", - "くしゃみ", - "くしょう", - "くすのき", - "くすりゆび", - "くせげ", - "くせん", - "ぐたいてき", - "くださる", - "くたびれる", - "くちこみ", - "くちさき", - "くつした", - "ぐっすり", - "くつろぐ", - "くとうてん", - "くどく", - "くなん", - "くねくね", - "くのう", - "くふう", - "くみあわせ", - "くみたてる", - "くめる", - "くやくしょ", - "くらす", - "くらべる", - "くるま", - "くれる", - "くろう", - "くわしい", - "ぐんかん", - "ぐんしょく", - "ぐんたい", - "ぐんて", - "けあな", - "けいかく", - "けいけん", - "けいこ", - "けいさつ", - "げいじゅつ", - "けいたい", - "げいのうじん", - "けいれき", - "けいろ", - "けおとす", - "けおりもの", - "げきか", - "げきげん", - "げきだん", - "げきちん", - "げきとつ", - "げきは", - "げきやく", - "げこう", - "げこくじょう", - "げざい", - "けさき", - "げざん", - "けしき", - "けしごむ", - "けしょう", - "げすと", - "けたば", - "けちゃっぷ", - "けちらす", - "けつあつ", - "けつい", - "けつえき", - "けっこん", - "けつじょ", - "けっせき", - "けってい", - "けつまつ", - "げつようび", - "げつれい", - "けつろん", - "げどく", - "けとばす", - "けとる", - "けなげ", - "けなす", - "けなみ", - "けぬき", - "げねつ", - "けねん", - "けはい", - "げひん", - "けぶかい", - "げぼく", - "けまり", - "けみかる", - "けむし", - "けむり", - "けもの", - "けらい", - "けろけろ", - "けわしい", - "けんい", - "けんえつ", - "けんお", - "けんか", - "げんき", - "けんげん", - "けんこう", - "けんさく", - "けんしゅう", - "けんすう", - "げんそう", - "けんちく", - "けんてい", - "けんとう", - "けんない", - "けんにん", - "げんぶつ", - "けんま", - "けんみん", - "けんめい", - "けんらん", - "けんり", - "こあくま", - "こいぬ", - "こいびと", - "ごうい", - "こうえん", - "こうおん", - "こうかん", - "ごうきゅう", - "ごうけい", - "こうこう", - "こうさい", - "こうじ", - "こうすい", - "ごうせい", - "こうそく", - "こうたい", - "こうちゃ", - "こうつう", - "こうてい", - "こうどう", - "こうない", - "こうはい", - "ごうほう", - "ごうまん", - "こうもく", - "こうりつ", - "こえる", - "こおり", - "ごかい", - "ごがつ", - "ごかん", - "こくご", - "こくさい", - "こくとう", - "こくない", - "こくはく", - "こぐま", - "こけい", - "こける", - "ここのか", - "こころ", - "こさめ", - "こしつ", - "こすう", - "こせい", - "こせき", - "こぜん", - "こそだて", - "こたい", - "こたえる", - "こたつ", - "こちょう", - "こっか", - "こつこつ", - "こつばん", - "こつぶ", - "こてい", - "こてん", - "ことがら", - "ことし", - "ことば", - "ことり", - "こなごな", - "こねこね", - "このまま", - "このみ", - "このよ", - "ごはん", - "こひつじ", - "こふう", - "こふん", - "こぼれる", - "ごまあぶら", - "こまかい", - "ごますり", - "こまつな", - "こまる", - "こむぎこ", - "こもじ", - "こもち", - "こもの", - "こもん", - "こやく", - "こやま", - "こゆう", - "こゆび", - "こよい", - "こよう", - "こりる", - "これくしょん", - "ころっけ", - "こわもて", - "こわれる", - "こんいん", - "こんかい", - "こんき", - "こんしゅう", - "こんすい", - "こんだて", - "こんとん", - "こんなん", - "こんびに", - "こんぽん", - "こんまけ", - "こんや", - "こんれい", - "こんわく", - "ざいえき", - "さいかい", - "さいきん", - "ざいげん", - "ざいこ", - "さいしょ", - "さいせい", - "ざいたく", - "ざいちゅう", - "さいてき", - "ざいりょう", - "さうな", - "さかいし", - "さがす", - "さかな", - "さかみち", - "さがる", - "さぎょう", - "さくし", - "さくひん", - "さくら", - "さこく", - "さこつ", - "さずかる", - "ざせき", - "さたん", - "さつえい", - "ざつおん", - "ざっか", - "ざつがく", - "さっきょく", - "ざっし", - "さつじん", - "ざっそう", - "さつたば", - "さつまいも", - "さてい", - "さといも", - "さとう", - "さとおや", - "さとし", - "さとる", - "さのう", - "さばく", - "さびしい", - "さべつ", - "さほう", - "さほど", - "さます", - "さみしい", - "さみだれ", - "さむけ", - "さめる", - "さやえんどう", - "さゆう", - "さよう", - "さよく", - "さらだ", - "ざるそば", - "さわやか", - "さわる", - "さんいん", - "さんか", - "さんきゃく", - "さんこう", - "さんさい", - "ざんしょ", - "さんすう", - "さんせい", - "さんそ", - "さんち", - "さんま", - "さんみ", - "さんらん", - "しあい", - "しあげ", - "しあさって", - "しあわせ", - "しいく", - "しいん", - "しうち", - "しえい", - "しおけ", - "しかい", - "しかく", - "じかん", - "しごと", - "しすう", - "じだい", - "したうけ", - "したぎ", - "したて", - "したみ", - "しちょう", - "しちりん", - "しっかり", - "しつじ", - "しつもん", - "してい", - "してき", - "してつ", - "じてん", - "じどう", - "しなぎれ", - "しなもの", - "しなん", - "しねま", - "しねん", - "しのぐ", - "しのぶ", - "しはい", - "しばかり", - "しはつ", - "しはらい", - "しはん", - "しひょう", - "しふく", - "じぶん", - "しへい", - "しほう", - "しほん", - "しまう", - "しまる", - "しみん", - "しむける", - "じむしょ", - "しめい", - "しめる", - "しもん", - "しゃいん", - "しゃうん", - "しゃおん", - "じゃがいも", - "しやくしょ", - "しゃくほう", - "しゃけん", - "しゃこ", - "しゃざい", - "しゃしん", - "しゃせん", - "しゃそう", - "しゃたい", - "しゃちょう", - "しゃっきん", - "じゃま", - "しゃりん", - "しゃれい", - "じゆう", - "じゅうしょ", - "しゅくはく", - "じゅしん", - "しゅっせき", - "しゅみ", - "しゅらば", - "じゅんばん", - "しょうかい", - "しょくたく", - "しょっけん", - "しょどう", - "しょもつ", - "しらせる", - "しらべる", - "しんか", - "しんこう", - "じんじゃ", - "しんせいじ", - "しんちく", - "しんりん", - "すあげ", - "すあし", - "すあな", - "ずあん", - "すいえい", - "すいか", - "すいとう", - "ずいぶん", - "すいようび", - "すうがく", - "すうじつ", - "すうせん", - "すおどり", - "すきま", - "すくう", - "すくない", - "すける", - "すごい", - "すこし", - "ずさん", - "すずしい", - "すすむ", - "すすめる", - "すっかり", - "ずっしり", - "ずっと", - "すてき", - "すてる", - "すねる", - "すのこ", - "すはだ", - "すばらしい", - "ずひょう", - "ずぶぬれ", - "すぶり", - "すふれ", - "すべて", - "すべる", - "ずほう", - "すぼん", - "すまい", - "すめし", - "すもう", - "すやき", - "すらすら", - "するめ", - "すれちがう", - "すろっと", - "すわる", - "すんぜん", - "すんぽう", - "せあぶら", - "せいかつ", - "せいげん", - "せいじ", - "せいよう", - "せおう", - "せかいかん", - "せきにん", - "せきむ", - "せきゆ", - "せきらんうん", - "せけん", - "せこう", - "せすじ", - "せたい", - "せたけ", - "せっかく", - "せっきゃく", - "ぜっく", - "せっけん", - "せっこつ", - "せっさたくま", - "せつぞく", - "せつだん", - "せつでん", - "せっぱん", - "せつび", - "せつぶん", - "せつめい", - "せつりつ", - "せなか", - "せのび", - "せはば", - "せびろ", - "せぼね", - "せまい", - "せまる", - "せめる", - "せもたれ", - "せりふ", - "ぜんあく", - "せんい", - "せんえい", - "せんか", - "せんきょ", - "せんく", - "せんげん", - "ぜんご", - "せんさい", - "せんしゅ", - "せんすい", - "せんせい", - "せんぞ", - "せんたく", - "せんちょう", - "せんてい", - "せんとう", - "せんぬき", - "せんねん", - "せんぱい", - "ぜんぶ", - "ぜんぽう", - "せんむ", - "せんめんじょ", - "せんもん", - "せんやく", - "せんゆう", - "せんよう", - "ぜんら", - "ぜんりゃく", - "せんれい", - "せんろ", - "そあく", - "そいとげる", - "そいね", - "そうがんきょう", - "そうき", - "そうご", - "そうしん", - "そうだん", - "そうなん", - "そうび", - "そうめん", - "そうり", - "そえもの", - "そえん", - "そがい", - "そげき", - "そこう", - "そこそこ", - "そざい", - "そしな", - "そせい", - "そせん", - "そそぐ", - "そだてる", - "そつう", - "そつえん", - "そっかん", - "そつぎょう", - "そっけつ", - "そっこう", - "そっせん", - "そっと", - "そとがわ", - "そとづら", - "そなえる", - "そなた", - "そふぼ", - "そぼく", - "そぼろ", - "そまつ", - "そまる", - "そむく", - "そむりえ", - "そめる", - "そもそも", - "そよかぜ", - "そらまめ", - "そろう", - "そんかい", - "そんけい", - "そんざい", - "そんしつ", - "そんぞく", - "そんちょう", - "ぞんび", - "ぞんぶん", - "そんみん", - "たあい", - "たいいん", - "たいうん", - "たいえき", - "たいおう", - "だいがく", - "たいき", - "たいぐう", - "たいけん", - "たいこ", - "たいざい", - "だいじょうぶ", - "だいすき", - "たいせつ", - "たいそう", - "だいたい", - "たいちょう", - "たいてい", - "だいどころ", - "たいない", - "たいねつ", - "たいのう", - "たいはん", - "だいひょう", - "たいふう", - "たいへん", - "たいほ", - "たいまつばな", - "たいみんぐ", - "たいむ", - "たいめん", - "たいやき", - "たいよう", - "たいら", - "たいりょく", - "たいる", - "たいわん", - "たうえ", - "たえる", - "たおす", - "たおる", - "たおれる", - "たかい", - "たかね", - "たきび", - "たくさん", - "たこく", - "たこやき", - "たさい", - "たしざん", - "だじゃれ", - "たすける", - "たずさわる", - "たそがれ", - "たたかう", - "たたく", - "ただしい", - "たたみ", - "たちばな", - "だっかい", - "だっきゃく", - "だっこ", - "だっしゅつ", - "だったい", - "たてる", - "たとえる", - "たなばた", - "たにん", - "たぬき", - "たのしみ", - "たはつ", - "たぶん", - "たべる", - "たぼう", - "たまご", - "たまる", - "だむる", - "ためいき", - "ためす", - "ためる", - "たもつ", - "たやすい", - "たよる", - "たらす", - "たりきほんがん", - "たりょう", - "たりる", - "たると", - "たれる", - "たれんと", - "たろっと", - "たわむれる", - "だんあつ", - "たんい", - "たんおん", - "たんか", - "たんき", - "たんけん", - "たんご", - "たんさん", - "たんじょうび", - "だんせい", - "たんそく", - "たんたい", - "だんち", - "たんてい", - "たんとう", - "だんな", - "たんにん", - "だんねつ", - "たんのう", - "たんぴん", - "だんぼう", - "たんまつ", - "たんめい", - "だんれつ", - "だんろ", - "だんわ", - "ちあい", - "ちあん", - "ちいき", - "ちいさい", - "ちえん", - "ちかい", - "ちから", - "ちきゅう", - "ちきん", - "ちけいず", - "ちけん", - "ちこく", - "ちさい", - "ちしき", - "ちしりょう", - "ちせい", - "ちそう", - "ちたい", - "ちたん", - "ちちおや", - "ちつじょ", - "ちてき", - "ちてん", - "ちぬき", - "ちぬり", - "ちのう", - "ちひょう", - "ちへいせん", - "ちほう", - "ちまた", - "ちみつ", - "ちみどろ", - "ちめいど", - "ちゃんこなべ", - "ちゅうい", - "ちゆりょく", - "ちょうし", - "ちょさくけん", - "ちらし", - "ちらみ", - "ちりがみ", - "ちりょう", - "ちるど", - "ちわわ", - "ちんたい", - "ちんもく", - "ついか", - "ついたち", - "つうか", - "つうじょう", - "つうはん", - "つうわ", - "つかう", - "つかれる", - "つくね", - "つくる", - "つけね", - "つける", - "つごう", - "つたえる", - "つづく", - "つつじ", - "つつむ", - "つとめる", - "つながる", - "つなみ", - "つねづね", - "つのる", - "つぶす", - "つまらない", - "つまる", - "つみき", - "つめたい", - "つもり", - "つもる", - "つよい", - "つるぼ", - "つるみく", - "つわもの", - "つわり", - "てあし", - "てあて", - "てあみ", - "ていおん", - "ていか", - "ていき", - "ていけい", - "ていこく", - "ていさつ", - "ていし", - "ていせい", - "ていたい", - "ていど", - "ていねい", - "ていひょう", - "ていへん", - "ていぼう", - "てうち", - "ておくれ", - "てきとう", - "てくび", - "でこぼこ", - "てさぎょう", - "てさげ", - "てすり", - "てそう", - "てちがい", - "てちょう", - "てつがく", - "てつづき", - "でっぱ", - "てつぼう", - "てつや", - "でぬかえ", - "てぬき", - "てぬぐい", - "てのひら", - "てはい", - "てぶくろ", - "てふだ", - "てほどき", - "てほん", - "てまえ", - "てまきずし", - "てみじか", - "てみやげ", - "てらす", - "てれび", - "てわけ", - "てわたし", - "でんあつ", - "てんいん", - "てんかい", - "てんき", - "てんぐ", - "てんけん", - "てんごく", - "てんさい", - "てんし", - "てんすう", - "でんち", - "てんてき", - "てんとう", - "てんない", - "てんぷら", - "てんぼうだい", - "てんめつ", - "てんらんかい", - "でんりょく", - "でんわ", - "どあい", - "といれ", - "どうかん", - "とうきゅう", - "どうぐ", - "とうし", - "とうむぎ", - "とおい", - "とおか", - "とおく", - "とおす", - "とおる", - "とかい", - "とかす", - "ときおり", - "ときどき", - "とくい", - "とくしゅう", - "とくてん", - "とくに", - "とくべつ", - "とけい", - "とける", - "とこや", - "とさか", - "としょかん", - "とそう", - "とたん", - "とちゅう", - "とっきゅう", - "とっくん", - "とつぜん", - "とつにゅう", - "とどける", - "ととのえる", - "とない", - "となえる", - "となり", - "とのさま", - "とばす", - "どぶがわ", - "とほう", - "とまる", - "とめる", - "ともだち", - "ともる", - "どようび", - "とらえる", - "とんかつ", - "どんぶり", - "ないかく", - "ないこう", - "ないしょ", - "ないす", - "ないせん", - "ないそう", - "なおす", - "ながい", - "なくす", - "なげる", - "なこうど", - "なさけ", - "なたでここ", - "なっとう", - "なつやすみ", - "ななおし", - "なにごと", - "なにもの", - "なにわ", - "なのか", - "なふだ", - "なまいき", - "なまえ", - "なまみ", - "なみだ", - "なめらか", - "なめる", - "なやむ", - "ならう", - "ならび", - "ならぶ", - "なれる", - "なわとび", - "なわばり", - "にあう", - "にいがた", - "にうけ", - "におい", - "にかい", - "にがて", - "にきび", - "にくしみ", - "にくまん", - "にげる", - "にさんかたんそ", - "にしき", - "にせもの", - "にちじょう", - "にちようび", - "にっか", - "にっき", - "にっけい", - "にっこう", - "にっさん", - "にっしょく", - "にっすう", - "にっせき", - "にってい", - "になう", - "にほん", - "にまめ", - "にもつ", - "にやり", - "にゅういん", - "にりんしゃ", - "にわとり", - "にんい", - "にんか", - "にんき", - "にんげん", - "にんしき", - "にんずう", - "にんそう", - "にんたい", - "にんち", - "にんてい", - "にんにく", - "にんぷ", - "にんまり", - "にんむ", - "にんめい", - "にんよう", - "ぬいくぎ", - "ぬかす", - "ぬぐいとる", - "ぬぐう", - "ぬくもり", - "ぬすむ", - "ぬまえび", - "ぬめり", - "ぬらす", - "ぬんちゃく", - "ねあげ", - "ねいき", - "ねいる", - "ねいろ", - "ねぐせ", - "ねくたい", - "ねくら", - "ねこぜ", - "ねこむ", - "ねさげ", - "ねすごす", - "ねそべる", - "ねだん", - "ねつい", - "ねっしん", - "ねつぞう", - "ねったいぎょ", - "ねぶそく", - "ねふだ", - "ねぼう", - "ねほりはほり", - "ねまき", - "ねまわし", - "ねみみ", - "ねむい", - "ねむたい", - "ねもと", - "ねらう", - "ねわざ", - "ねんいり", - "ねんおし", - "ねんかん", - "ねんきん", - "ねんぐ", - "ねんざ", - "ねんし", - "ねんちゃく", - "ねんど", - "ねんぴ", - "ねんぶつ", - "ねんまつ", - "ねんりょう", - "ねんれい", - "のいず", - "のおづま", - "のがす", - "のきなみ", - "のこぎり", - "のこす", - "のこる", - "のせる", - "のぞく", - "のぞむ", - "のたまう", - "のちほど", - "のっく", - "のばす", - "のはら", - "のべる", - "のぼる", - "のみもの", - "のやま", - "のらいぬ", - "のらねこ", - "のりもの", - "のりゆき", - "のれん", - "のんき", - "ばあい", - "はあく", - "ばあさん", - "ばいか", - "ばいく", - "はいけん", - "はいご", - "はいしん", - "はいすい", - "はいせん", - "はいそう", - "はいち", - "ばいばい", - "はいれつ", - "はえる", - "はおる", - "はかい", - "ばかり", - "はかる", - "はくしゅ", - "はけん", - "はこぶ", - "はさみ", - "はさん", - "はしご", - "ばしょ", - "はしる", - "はせる", - "ぱそこん", - "はそん", - "はたん", - "はちみつ", - "はつおん", - "はっかく", - "はづき", - "はっきり", - "はっくつ", - "はっけん", - "はっこう", - "はっさん", - "はっしん", - "はったつ", - "はっちゅう", - "はってん", - "はっぴょう", - "はっぽう", - "はなす", - "はなび", - "はにかむ", - "はぶらし", - "はみがき", - "はむかう", - "はめつ", - "はやい", - "はやし", - "はらう", - "はろうぃん", - "はわい", - "はんい", - "はんえい", - "はんおん", - "はんかく", - "はんきょう", - "ばんぐみ", - "はんこ", - "はんしゃ", - "はんすう", - "はんだん", - "ぱんち", - "ぱんつ", - "はんてい", - "はんとし", - "はんのう", - "はんぱ", - "はんぶん", - "はんぺん", - "はんぼうき", - "はんめい", - "はんらん", - "はんろん", - "ひいき", - "ひうん", - "ひえる", - "ひかく", - "ひかり", - "ひかる", - "ひかん", - "ひくい", - "ひけつ", - "ひこうき", - "ひこく", - "ひさい", - "ひさしぶり", - "ひさん", - "びじゅつかん", - "ひしょ" -] \ No newline at end of file diff --git a/monero-seed/src/words/jbo.rs b/monero-seed/src/words/jbo.rs deleted file mode 100644 index a58f8d11a3..0000000000 --- a/monero-seed/src/words/jbo.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "backi", - "bacru", - "badna", - "badri", - "bajra", - "bakfu", - "bakni", - "bakri", - "baktu", - "balji", - "balni", - "balre", - "balvi", - "bambu", - "bancu", - "bandu", - "banfi", - "bangu", - "banli", - "banro", - "banxa", - "banzu", - "bapli", - "barda", - "bargu", - "barja", - "barna", - "bartu", - "basfa", - "basna", - "basti", - "batci", - "batke", - "bavmi", - "baxso", - "bebna", - "bekpi", - "bemro", - "bende", - "bengo", - "benji", - "benre", - "benzo", - "bergu", - "bersa", - "berti", - "besna", - "besto", - "betfu", - "betri", - "bevri", - "bidju", - "bifce", - "bikla", - "bilga", - "bilma", - "bilni", - "bindo", - "binra", - "binxo", - "birje", - "birka", - "birti", - "bisli", - "bitmu", - "bitni", - "blabi", - "blaci", - "blanu", - "bliku", - "bloti", - "bolci", - "bongu", - "boske", - "botpi", - "boxfo", - "boxna", - "bradi", - "brano", - "bratu", - "brazo", - "bredi", - "bridi", - "brife", - "briju", - "brito", - "brivo", - "broda", - "bruna", - "budjo", - "bukpu", - "bumru", - "bunda", - "bunre", - "burcu", - "burna", - "cabna", - "cabra", - "cacra", - "cadga", - "cadzu", - "cafne", - "cagna", - "cakla", - "calku", - "calse", - "canci", - "cando", - "cange", - "canja", - "canko", - "canlu", - "canpa", - "canre", - "canti", - "carce", - "carfu", - "carmi", - "carna", - "cartu", - "carvi", - "casnu", - "catke", - "catlu", - "catni", - "catra", - "caxno", - "cecla", - "cecmu", - "cedra", - "cenba", - "censa", - "centi", - "cerda", - "cerni", - "certu", - "cevni", - "cfale", - "cfari", - "cfika", - "cfila", - "cfine", - "cfipu", - "ciblu", - "cicna", - "cidja", - "cidni", - "cidro", - "cifnu", - "cigla", - "cikna", - "cikre", - "ciksi", - "cilce", - "cilfu", - "cilmo", - "cilre", - "cilta", - "cimde", - "cimni", - "cinba", - "cindu", - "cinfo", - "cinje", - "cinki", - "cinla", - "cinmo", - "cinri", - "cinse", - "cinta", - "cinza", - "cipni", - "cipra", - "cirko", - "cirla", - "ciska", - "cisma", - "cisni", - "ciste", - "citka", - "citno", - "citri", - "citsi", - "civla", - "cizra", - "ckabu", - "ckafi", - "ckaji", - "ckana", - "ckape", - "ckasu", - "ckeji", - "ckiku", - "ckilu", - "ckini", - "ckire", - "ckule", - "ckunu", - "cladu", - "clani", - "claxu", - "cletu", - "clika", - "clinu", - "clira", - "clite", - "cliva", - "clupa", - "cmaci", - "cmalu", - "cmana", - "cmavo", - "cmene", - "cmeta", - "cmevo", - "cmila", - "cmima", - "cmoni", - "cnano", - "cnebo", - "cnemu", - "cnici", - "cnino", - "cnisa", - "cnita", - "cokcu", - "condi", - "conka", - "corci", - "cortu", - "cpacu", - "cpana", - "cpare", - "cpedu", - "cpina", - "cradi", - "crane", - "creka", - "crepu", - "cribe", - "crida", - "crino", - "cripu", - "crisa", - "critu", - "ctaru", - "ctebi", - "cteki", - "ctile", - "ctino", - "ctuca", - "cukla", - "cukre", - "cukta", - "culno", - "cumki", - "cumla", - "cunmi", - "cunso", - "cuntu", - "cupra", - "curmi", - "curnu", - "curve", - "cusku", - "cusna", - "cutci", - "cutne", - "cuxna", - "dacru", - "dacti", - "dadjo", - "dakfu", - "dakli", - "damba", - "damri", - "dandu", - "danfu", - "danlu", - "danmo", - "danre", - "dansu", - "danti", - "daplu", - "dapma", - "darca", - "dargu", - "darlu", - "darno", - "darsi", - "darxi", - "daski", - "dasni", - "daspo", - "dasri", - "datka", - "datni", - "datro", - "decti", - "degji", - "dejni", - "dekpu", - "dekto", - "delno", - "dembi", - "denci", - "denmi", - "denpa", - "dertu", - "derxi", - "desku", - "detri", - "dicma", - "dicra", - "didni", - "digno", - "dikca", - "diklo", - "dikni", - "dilcu", - "dilma", - "dilnu", - "dimna", - "dindi", - "dinju", - "dinko", - "dinso", - "dirba", - "dirce", - "dirgo", - "disko", - "ditcu", - "divzi", - "dizlo", - "djacu", - "djedi", - "djica", - "djine", - "djuno", - "donri", - "dotco", - "draci", - "drani", - "drata", - "drudi", - "dugri", - "dukse", - "dukti", - "dunda", - "dunja", - "dunku", - "dunli", - "dunra", - "dutso", - "dzena", - "dzipo", - "facki", - "fadni", - "fagri", - "falnu", - "famti", - "fancu", - "fange", - "fanmo", - "fanri", - "fanta", - "fanva", - "fanza", - "fapro", - "farka", - "farlu", - "farna", - "farvi", - "fasnu", - "fatci", - "fatne", - "fatri", - "febvi", - "fegli", - "femti", - "fendi", - "fengu", - "fenki", - "fenra", - "fenso", - "fepni", - "fepri", - "ferti", - "festi", - "fetsi", - "figre", - "filso", - "finpe", - "finti", - "firca", - "fisli", - "fizbu", - "flaci", - "flalu", - "flani", - "flecu", - "flese", - "fliba", - "flira", - "foldi", - "fonmo", - "fonxa", - "forca", - "forse", - "fraso", - "frati", - "fraxu", - "frica", - "friko", - "frili", - "frinu", - "friti", - "frumu", - "fukpi", - "fulta", - "funca", - "fusra", - "fuzme", - "gacri", - "gadri", - "galfi", - "galtu", - "galxe", - "ganlo", - "ganra", - "ganse", - "ganti", - "ganxo", - "ganzu", - "gapci", - "gapru", - "garna", - "gasnu", - "gaspo", - "gasta", - "genja", - "gento", - "genxu", - "gerku", - "gerna", - "gidva", - "gigdo", - "ginka", - "girzu", - "gismu", - "glare", - "gleki", - "gletu", - "glico", - "glife", - "glosa", - "gluta", - "gocti", - "gomsi", - "gotro", - "gradu", - "grafu", - "grake", - "grana", - "grasu", - "grava", - "greku", - "grusi", - "grute", - "gubni", - "gugde", - "gugle", - "gumri", - "gundi", - "gunka", - "gunma", - "gunro", - "gunse", - "gunta", - "gurni", - "guska", - "gusni", - "gusta", - "gutci", - "gutra", - "guzme", - "jabre", - "jadni", - "jakne", - "jalge", - "jalna", - "jalra", - "jamfu", - "jamna", - "janbe", - "janco", - "janli", - "jansu", - "janta", - "jarbu", - "jarco", - "jarki", - "jaspu", - "jatna", - "javni", - "jbama", - "jbari", - "jbena", - "jbera", - "jbini", - "jdari", - "jdice", - "jdika", - "jdima", - "jdini", - "jduli", - "jecta", - "jeftu", - "jegvo", - "jelca", - "jemna", - "jenca", - "jendu", - "jenmi", - "jensi", - "jerna", - "jersi", - "jerxo", - "jesni", - "jetce", - "jetnu", - "jgalu", - "jganu", - "jgari", - "jgena", - "jgina", - "jgira", - "jgita", - "jibni", - "jibri", - "jicla", - "jicmu", - "jijnu", - "jikca", - "jikfi", - "jikni", - "jikru", - "jilka", - "jilra", - "jimca", - "jimpe", - "jimte", - "jinci", - "jinda", - "jinga", - "jinku", - "jinme", - "jinru", - "jinsa", - "jinto", - "jinvi", - "jinzi", - "jipci", - "jipno", - "jirna", - "jisra", - "jitfa", - "jitro", - "jivbu", - "jivna", - "jmaji", - "jmifa", - "jmina", - "jmive", - "jonse", - "jordo", - "jorne", - "jubme", - "judri", - "jufra", - "jukni", - "jukpa", - "julne", - "julro", - "jundi", - "jungo", - "junla", - "junri", - "junta", - "jurme", - "jursa", - "jutsi", - "juxre", - "jvinu", - "jviso", - "kabri", - "kacma", - "kadno", - "kafke", - "kagni", - "kajde", - "kajna", - "kakne", - "kakpa", - "kalci", - "kalri", - "kalsa", - "kalte", - "kamju", - "kamni", - "kampu", - "kamre", - "kanba", - "kancu", - "kandi", - "kanji", - "kanla", - "kanpe", - "kanro", - "kansa", - "kantu", - "kanxe", - "karbi", - "karce", - "karda", - "kargu", - "karli", - "karni", - "katci", - "katna", - "kavbu", - "kazra", - "kecti", - "kekli", - "kelci", - "kelvo", - "kenka", - "kenra", - "kensa", - "kerfa", - "kerlo", - "kesri", - "ketco", - "ketsu", - "kevna", - "kibro", - "kicne", - "kijno", - "kilto", - "kinda", - "kinli", - "kisto", - "klaji", - "klaku", - "klama", - "klani", - "klesi", - "kliki", - "klina", - "kliru", - "kliti", - "klupe", - "kluza", - "kobli", - "kogno", - "kojna", - "kokso", - "kolme", - "komcu", - "konju", - "korbi", - "korcu", - "korka", - "korvo", - "kosmu", - "kosta", - "krali", - "kramu", - "krasi", - "krati", - "krefu", - "krici", - "krili", - "krinu", - "krixa", - "kruca", - "kruji", - "kruvi", - "kubli", - "kucli", - "kufra", - "kukte", - "kulnu", - "kumfa", - "kumte", - "kunra", - "kunti", - "kurfa", - "kurji", - "kurki", - "kuspe", - "kusru", - "labno", - "lacni", - "lacpu", - "lacri", - "ladru", - "lafti", - "lakne", - "lakse", - "laldo", - "lalxu", - "lamji", - "lanbi", - "lanci", - "landa", - "lanka", - "lanli", - "lanme", - "lante", - "lanxe", - "lanzu", - "larcu", - "larva", - "lasna", - "lastu", - "latmo", - "latna", - "lazni", - "lebna", - "lelxe", - "lenga", - "lenjo", - "lenku", - "lerci", - "lerfu", - "libjo", - "lidne", - "lifri", - "lijda", - "limfa", - "limna", - "lince", - "lindi", - "linga", - "linji", - "linsi", - "linto", - "lisri", - "liste", - "litce", - "litki", - "litru", - "livga", - "livla", - "logji", - "loglo", - "lojbo", - "loldi", - "lorxu", - "lubno", - "lujvo", - "luksi", - "lumci", - "lunbe", - "lunra", - "lunsa", - "luska", - "lusto", - "mabla", - "mabru", - "macnu", - "majga", - "makcu", - "makfa", - "maksi", - "malsi", - "mamta", - "manci", - "manfo", - "mango", - "manku", - "manri", - "mansa", - "manti", - "mapku", - "mapni", - "mapra", - "mapti", - "marbi", - "marce", - "marde", - "margu", - "marji", - "marna", - "marxa", - "masno", - "masti", - "matci", - "matli", - "matne", - "matra", - "mavji", - "maxri", - "mebri", - "megdo", - "mekso", - "melbi", - "meljo", - "melmi", - "menli", - "menre", - "mensi", - "mentu", - "merko", - "merli", - "metfo", - "mexno", - "midju", - "mifra", - "mikce", - "mikri", - "milti", - "milxe", - "minde", - "minji", - "minli", - "minra", - "mintu", - "mipri", - "mirli", - "misno", - "misro", - "mitre", - "mixre", - "mlana", - "mlatu", - "mleca", - "mledi", - "mluni", - "mogle", - "mokca", - "moklu", - "molki", - "molro", - "morji", - "morko", - "morna", - "morsi", - "mosra", - "mraji", - "mrilu", - "mruli", - "mucti", - "mudri", - "mugle", - "mukti", - "mulno", - "munje", - "mupli", - "murse", - "murta", - "muslo", - "mutce", - "muvdu", - "muzga", - "nabmi", - "nakni", - "nalci", - "namcu", - "nanba", - "nanca", - "nandu", - "nanla", - "nanmu", - "nanvi", - "narge", - "narju", - "natfe", - "natmi", - "natsi", - "navni", - "naxle", - "nazbi", - "nejni", - "nelci", - "nenri", - "nerde", - "nibli", - "nicfa", - "nicte", - "nikle", - "nilce", - "nimre", - "ninja", - "ninmu", - "nirna", - "nitcu", - "nivji", - "nixli", - "nobli", - "norgo", - "notci", - "nudle", - "nukni", - "nunmu", - "nupre", - "nurma", - "nusna", - "nutka", - "nutli", - "nuzba", - "nuzlo", - "pacna", - "pagbu", - "pagre", - "pajni", - "palci", - "palku", - "palma", - "palne", - "palpi", - "palta", - "pambe", - "pamga", - "panci", - "pandi", - "panje", - "panka", - "panlo", - "panpi", - "panra", - "pante", - "panzi", - "papri", - "parbi", - "pardu", - "parji", - "pastu", - "patfu", - "patlu", - "patxu", - "paznu", - "pelji", - "pelxu", - "pemci", - "penbi", - "pencu", - "pendo", - "penmi", - "pensi", - "pentu", - "perli", - "pesxu", - "petso", - "pevna", - "pezli", - "picti", - "pijne", - "pikci", - "pikta", - "pilda", - "pilji", - "pilka", - "pilno", - "pimlu", - "pinca", - "pindi", - "pinfu", - "pinji", - "pinka", - "pinsi", - "pinta", - "pinxe", - "pipno", - "pixra", - "plana", - "platu", - "pleji", - "plibu", - "plini", - "plipe", - "plise", - "plita", - "plixa", - "pluja", - "pluka", - "pluta", - "pocli", - "polje", - "polno", - "ponjo", - "ponse", - "poplu", - "porpi", - "porsi", - "porto", - "prali", - "prami", - "prane", - "preja", - "prenu", - "preri", - "preti", - "prije", - "prina", - "pritu", - "proga", - "prosa", - "pruce", - "pruni", - "pruri", - "pruxi", - "pulce", - "pulji", - "pulni", - "punji", - "punli", - "pupsu", - "purci", - "purdi", - "purmo", - "racli", - "ractu", - "radno", - "rafsi", - "ragbi", - "ragve", - "rakle", - "rakso", - "raktu", - "ralci", - "ralju", - "ralte", - "randa", - "rango", - "ranji", - "ranmi", - "ransu", - "ranti", - "ranxi", - "rapli", - "rarna", - "ratcu", - "ratni", - "rebla", - "rectu", - "rekto", - "remna", - "renro", - "renvi", - "respa", - "rexsa", - "ricfu", - "rigni", - "rijno", - "rilti", - "rimni", - "rinci", - "rindo", - "rinju", - "rinka", - "rinsa", - "rirci", - "rirni", - "rirxe", - "rismi", - "risna", - "ritli", - "rivbi", - "rokci", - "romge", - "romlo", - "ronte", - "ropno", - "rorci", - "rotsu", - "rozgu", - "ruble", - "rufsu", - "runme", - "runta", - "rupnu", - "rusko", - "rutni", - "sabji", - "sabnu", - "sacki", - "saclu", - "sadjo", - "sakci", - "sakli", - "sakta", - "salci", - "salpo", - "salri", - "salta", - "samcu", - "sampu", - "sanbu", - "sance", - "sanga", - "sanji", - "sanli", - "sanmi", - "sanso", - "santa", - "sarcu", - "sarji", - "sarlu", - "sarni", - "sarxe", - "saske", - "satci", - "satre", - "savru", - "sazri", - "sefsi", - "sefta", - "sekre", - "selci", - "selfu", - "semto", - "senci", - "sengi", - "senpi", - "senta", - "senva", - "sepli", - "serti", - "sesre", - "setca", - "sevzi", - "sfani", - "sfasa", - "sfofa", - "sfubu", - "sibli", - "siclu", - "sicni", - "sicpi", - "sidbo", - "sidju", - "sigja", - "sigma", - "sikta", - "silka", - "silna", - "simlu", - "simsa", - "simxu", - "since", - "sinma", - "sinso", - "sinxa", - "sipna", - "sirji", - "sirxo", - "sisku", - "sisti", - "sitna", - "sivni", - "skaci", - "skami", - "skapi", - "skari", - "skicu", - "skiji", - "skina", - "skori", - "skoto", - "skuba", - "skuro", - "slabu", - "slaka", - "slami", - "slanu", - "slari", - "slasi", - "sligu", - "slilu", - "sliri", - "slovo", - "sluji", - "sluni", - "smacu", - "smadi", - "smaji", - "smaka", - "smani", - "smela", - "smoka", - "smuci", - "smuni", - "smusu", - "snada", - "snanu", - "snidu", - "snime", - "snipa", - "snuji", - "snura", - "snuti", - "sobde", - "sodna", - "sodva", - "softo", - "solji", - "solri", - "sombo", - "sonci", - "sorcu", - "sorgu", - "sorni", - "sorta", - "sovda", - "spaji", - "spali", - "spano", - "spati", - "speni", - "spero", - "spisa", - "spita", - "spofu", - "spoja", - "spuda", - "sputu", - "sraji", - "sraku", - "sralo", - "srana", - "srasu", - "srera", - "srito", - "sruma", - "sruri", - "stace", - "stagi", - "staku", - "stali", - "stani", - "stapa", - "stasu", - "stati", - "steba", - "steci", - "stedu", - "stela", - "stero", - "stici", - "stidi", - "stika", - "stizu", - "stodi", - "stuna", - "stura", - "stuzi", - "sucta", - "sudga", - "sufti", - "suksa", - "sumji", - "sumne", - "sumti", - "sunga", - "sunla", - "surla", - "sutra", - "tabno", - "tabra", - "tadji", - "tadni", - "tagji", - "taksi", - "talsa", - "tamca", - "tamji", - "tamne", - "tanbo", - "tance", - "tanjo", - "tanko", - "tanru", - "tansi", - "tanxe", - "tapla", - "tarbi", - "tarci", - "tarla", - "tarmi", - "tarti", - "taske", - "tasmi", - "tasta", - "tatpi", - "tatru", - "tavla", - "taxfu", - "tcaci", - "tcadu", - "tcana", - "tcati", - "tcaxe", - "tcena", - "tcese", - "tcica", - "tcidu", - "tcika", - "tcila", - "tcima", - "tcini", - "tcita", - "temci", - "temse", - "tende", - "tenfa", - "tengu", - "terdi", - "terpa", - "terto", - "tifri", - "tigni", - "tigra", - "tikpa", - "tilju", - "tinbe", - "tinci", - "tinsa", - "tirna", - "tirse", - "tirxu", - "tisna", - "titla", - "tivni", - "tixnu", - "toknu", - "toldi", - "tonga", - "tordu", - "torni", - "torso", - "traji", - "trano", - "trati", - "trene", - "tricu", - "trina", - "trixe", - "troci", - "tsaba", - "tsali", - "tsani", - "tsapi", - "tsiju", - "tsina", - "tsuku", - "tubnu", - "tubra", - "tugni", - "tujli", - "tumla", - "tunba", - "tunka", - "tunlo", - "tunta", - "tuple", - "turko", - "turni", - "tutci", - "tutle", - "tutra", - "vacri", - "vajni", - "valsi", - "vamji", - "vamtu", - "vanbi", - "vanci", - "vanju", - "vasru", - "vasxu", - "vecnu", - "vedli", - "venfu", - "vensa", - "vente", - "vepre", - "verba", - "vibna", - "vidni", - "vidru", - "vifne", - "vikmi", - "viknu", - "vimcu", - "vindu", - "vinji", - "vinta", - "vipsi", - "virnu", - "viska", - "vitci", - "vitke", - "vitno", - "vlagi", - "vlile", - "vlina", - "vlipa", - "vofli", - "voksa", - "volve", - "vorme", - "vraga", - "vreji", - "vreta", - "vrici", - "vrude", - "vrusi", - "vubla", - "vujnu", - "vukna", - "vukro", - "xabju", - "xadba", - "xadji", - "xadni", - "xagji", - "xagri", - "xajmi", - "xaksu", - "xalbo", - "xalka", - "xalni", - "xamgu", - "xampo", - "xamsi", - "xance", - "xango", - "xanka", - "xanri", - "xansa", - "xanto", - "xarci", - "xarju", - "xarnu", - "xasli", - "xasne", - "xatra", - "xatsi", - "xazdo", - "xebni", - "xebro", - "xecto", - "xedja", - "xekri", - "xelso", - "xendo", - "xenru", - "xexso", - "xigzo", - "xindo", - "xinmo", - "xirma", - "xislu", - "xispo", - "xlali", - "xlura", - "xorbo", - "xorlo", - "xotli", - "xrabo", - "xrani", - "xriso", - "xrotu", - "xruba", - "xruki", - "xrula", - "xruti", - "xukmi", - "xulta", - "xunre", - "xurdo", - "xusra", - "xutla", - "zabna", - "zajba", - "zalvi", - "zanru", - "zarci", - "zargu", - "zasni", - "zasti", - "zbabu", - "zbani", - "zbasu", - "zbepi", - "zdani", - "zdile", - "zekri", - "zenba", - "zepti", - "zetro", - "zevla", - "zgadi", - "zgana", - "zgike", - "zifre", - "zinki", - "zirpu", - "zivle", - "zmadu", - "zmiku", - "zucna", - "zukte", - "zumri", - "zungi", - "zunle", - "zunti", - "zutse", - "zvati", - "zviki", - "jbobau", - "jbopre", - "karsna", - "cabdei", - "zunsna", - "gendra", - "glibau", - "nintadni", - "pavyseljirna", - "vlaste", - "selbri", - "latro'a", - "zdakemkulgu'a", - "mriste", - "selsku", - "fu'ivla", - "tolmo'i", - "snavei", - "xagmau", - "retsku", - "ckupau", - "skudji", - "smudra", - "prulamdei", - "vokta'a", - "tinju'i", - "jefyfa'o", - "bavlamdei", - "kinzga", - "jbocre", - "jbovla", - "xauzma", - "selkei", - "xuncku", - "spusku", - "jbogu'e", - "pampe'o", - "bripre", - "jbosnu", - "zi'evla", - "gimste", - "tolzdi", - "velski", - "samselpla", - "cnegau", - "velcki", - "selja'e", - "fasybau", - "zanfri", - "reisku", - "favgau", - "jbota'a", - "rejgau", - "malgli", - "zilkai", - "keidji", - "tersu'i", - "jbofi'e", - "cnima'o", - "mulgau", - "ningau", - "ponbau", - "mrobi'o", - "rarbau", - "zmanei", - "famyma'o", - "vacysai", - "jetmlu", - "jbonunsla", - "nunpe'i", - "fa'orma'o", - "crezenzu'e", - "jbojbe", - "cmicu'a", - "zilcmi", - "tolcando", - "zukcfu", - "depybu'i", - "mencre", - "matmau", - "nunctu", - "selma'o", - "titnanba", - "naldra", - "jvajvo", - "nunsnu", - "nerkla", - "cimjvo", - "muvgau", - "zipcpi", - "runbau", - "faumlu", - "terbri", - "balcu'e", - "dragau", - "smuvelcki", - "piksku", - "selpli", - "bregau", - "zvafa'i", - "ci'izra", - "noltruti'u", - "samtci", - "snaxa'a" -] \ No newline at end of file diff --git a/monero-seed/src/words/nl.rs b/monero-seed/src/words/nl.rs deleted file mode 100644 index 0c191e7f07..0000000000 --- a/monero-seed/src/words/nl.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "aalglad", - "aalscholver", - "aambeeld", - "aangeef", - "aanlandig", - "aanvaard", - "aanwakker", - "aapmens", - "aarten", - "abdicatie", - "abnormaal", - "abrikoos", - "accu", - "acuut", - "adjudant", - "admiraal", - "advies", - "afbidding", - "afdracht", - "affaire", - "affiche", - "afgang", - "afkick", - "afknap", - "aflees", - "afmijner", - "afname", - "afpreekt", - "afrader", - "afspeel", - "aftocht", - "aftrek", - "afzijdig", - "ahornboom", - "aktetas", - "akzo", - "alchemist", - "alcohol", - "aldaar", - "alexander", - "alfabet", - "alfredo", - "alice", - "alikruik", - "allrisk", - "altsax", - "alufolie", - "alziend", - "amai", - "ambacht", - "ambieer", - "amina", - "amnestie", - "amok", - "ampul", - "amuzikaal", - "angela", - "aniek", - "antje", - "antwerpen", - "anya", - "aorta", - "apache", - "apekool", - "appelaar", - "arganolie", - "argeloos", - "armoede", - "arrenslee", - "artritis", - "arubaan", - "asbak", - "ascii", - "asgrauw", - "asjes", - "asml", - "aspunt", - "asurn", - "asveld", - "aterling", - "atomair", - "atrium", - "atsma", - "atypisch", - "auping", - "aura", - "avifauna", - "axiaal", - "azoriaan", - "azteek", - "azuur", - "bachelor", - "badderen", - "badhotel", - "badmantel", - "badsteden", - "balie", - "ballans", - "balvers", - "bamibal", - "banneling", - "barracuda", - "basaal", - "batelaan", - "batje", - "beambte", - "bedlamp", - "bedwelmd", - "befaamd", - "begierd", - "begraaf", - "behield", - "beijaard", - "bejaagd", - "bekaaid", - "beks", - "bektas", - "belaad", - "belboei", - "belderbos", - "beloerd", - "beluchten", - "bemiddeld", - "benadeeld", - "benijd", - "berechten", - "beroemd", - "besef", - "besseling", - "best", - "betichten", - "bevind", - "bevochten", - "bevraagd", - "bewust", - "bidplaats", - "biefstuk", - "biemans", - "biezen", - "bijbaan", - "bijeenkom", - "bijfiguur", - "bijkaart", - "bijlage", - "bijpaard", - "bijtgaar", - "bijweg", - "bimmel", - "binck", - "bint", - "biobak", - "biotisch", - "biseks", - "bistro", - "bitter", - "bitumen", - "bizar", - "blad", - "bleken", - "blender", - "bleu", - "blief", - "blijven", - "blozen", - "bock", - "boef", - "boei", - "boks", - "bolder", - "bolus", - "bolvormig", - "bomaanval", - "bombarde", - "bomma", - "bomtapijt", - "bookmaker", - "boos", - "borg", - "bosbes", - "boshuizen", - "bosloop", - "botanicus", - "bougie", - "bovag", - "boxspring", - "braad", - "brasem", - "brevet", - "brigade", - "brinckman", - "bruid", - "budget", - "buffel", - "buks", - "bulgaar", - "buma", - "butaan", - "butler", - "buuf", - "cactus", - "cafeetje", - "camcorder", - "cannabis", - "canyon", - "capoeira", - "capsule", - "carkit", - "casanova", - "catalaan", - "ceintuur", - "celdeling", - "celplasma", - "cement", - "censeren", - "ceramisch", - "cerberus", - "cerebraal", - "cesium", - "cirkel", - "citeer", - "civiel", - "claxon", - "clenbuterol", - "clicheren", - "clijsen", - "coalitie", - "coassistentschap", - "coaxiaal", - "codetaal", - "cofinanciering", - "cognac", - "coltrui", - "comfort", - "commandant", - "condensaat", - "confectie", - "conifeer", - "convector", - "copier", - "corfu", - "correct", - "coup", - "couvert", - "creatie", - "credit", - "crematie", - "cricket", - "croupier", - "cruciaal", - "cruijff", - "cuisine", - "culemborg", - "culinair", - "curve", - "cyrano", - "dactylus", - "dading", - "dagblind", - "dagje", - "daglicht", - "dagprijs", - "dagranden", - "dakdekker", - "dakpark", - "dakterras", - "dalgrond", - "dambord", - "damkat", - "damlengte", - "damman", - "danenberg", - "debbie", - "decibel", - "defect", - "deformeer", - "degelijk", - "degradant", - "dejonghe", - "dekken", - "deppen", - "derek", - "derf", - "derhalve", - "detineren", - "devalueer", - "diaken", - "dicht", - "dictaat", - "dief", - "digitaal", - "dijbreuk", - "dijkmans", - "dimbaar", - "dinsdag", - "diode", - "dirigeer", - "disbalans", - "dobermann", - "doenbaar", - "doerak", - "dogma", - "dokhaven", - "dokwerker", - "doling", - "dolphijn", - "dolven", - "dombo", - "dooraderd", - "dopeling", - "doping", - "draderig", - "drama", - "drenkbak", - "dreumes", - "drol", - "drug", - "duaal", - "dublin", - "duplicaat", - "durven", - "dusdanig", - "dutchbat", - "dutje", - "dutten", - "duur", - "duwwerk", - "dwaal", - "dweil", - "dwing", - "dyslexie", - "ecostroom", - "ecotaks", - "educatie", - "eeckhout", - "eede", - "eemland", - "eencellig", - "eeneiig", - "eenruiter", - "eenwinter", - "eerenberg", - "eerrover", - "eersel", - "eetmaal", - "efteling", - "egaal", - "egtberts", - "eickhoff", - "eidooier", - "eiland", - "eind", - "eisden", - "ekster", - "elburg", - "elevatie", - "elfkoppig", - "elfrink", - "elftal", - "elimineer", - "elleboog", - "elma", - "elodie", - "elsa", - "embleem", - "embolie", - "emoe", - "emonds", - "emplooi", - "enduro", - "enfin", - "engageer", - "entourage", - "entstof", - "epileer", - "episch", - "eppo", - "erasmus", - "erboven", - "erebaan", - "erelijst", - "ereronden", - "ereteken", - "erfhuis", - "erfwet", - "erger", - "erica", - "ermitage", - "erna", - "ernie", - "erts", - "ertussen", - "eruitzien", - "ervaar", - "erven", - "erwt", - "esbeek", - "escort", - "esdoorn", - "essing", - "etage", - "eter", - "ethanol", - "ethicus", - "etholoog", - "eufonisch", - "eurocent", - "evacuatie", - "exact", - "examen", - "executant", - "exen", - "exit", - "exogeen", - "exotherm", - "expeditie", - "expletief", - "expres", - "extase", - "extinctie", - "faal", - "faam", - "fabel", - "facultair", - "fakir", - "fakkel", - "faliekant", - "fallisch", - "famke", - "fanclub", - "fase", - "fatsoen", - "fauna", - "federaal", - "feedback", - "feest", - "feilbaar", - "feitelijk", - "felblauw", - "figurante", - "fiod", - "fitheid", - "fixeer", - "flap", - "fleece", - "fleur", - "flexibel", - "flits", - "flos", - "flow", - "fluweel", - "foezelen", - "fokkelman", - "fokpaard", - "fokvee", - "folder", - "follikel", - "folmer", - "folteraar", - "fooi", - "foolen", - "forfait", - "forint", - "formule", - "fornuis", - "fosfaat", - "foxtrot", - "foyer", - "fragiel", - "frater", - "freak", - "freddie", - "fregat", - "freon", - "frijnen", - "fructose", - "frunniken", - "fuiven", - "funshop", - "furieus", - "fysica", - "gadget", - "galder", - "galei", - "galg", - "galvlieg", - "galzuur", - "ganesh", - "gaswet", - "gaza", - "gazelle", - "geaaid", - "gebiecht", - "gebufferd", - "gedijd", - "geef", - "geflanst", - "gefreesd", - "gegaan", - "gegijzeld", - "gegniffel", - "gegraaid", - "gehikt", - "gehobbeld", - "gehucht", - "geiser", - "geiten", - "gekaakt", - "gekheid", - "gekijf", - "gekmakend", - "gekocht", - "gekskap", - "gekte", - "gelubberd", - "gemiddeld", - "geordend", - "gepoederd", - "gepuft", - "gerda", - "gerijpt", - "geseald", - "geshockt", - "gesierd", - "geslaagd", - "gesnaaid", - "getracht", - "getwijfel", - "geuit", - "gevecht", - "gevlagd", - "gewicht", - "gezaagd", - "gezocht", - "ghanees", - "giebelen", - "giechel", - "giepmans", - "gips", - "giraal", - "gistachtig", - "gitaar", - "glaasje", - "gletsjer", - "gleuf", - "glibberen", - "glijbaan", - "gloren", - "gluipen", - "gluren", - "gluur", - "gnoe", - "goddelijk", - "godgans", - "godschalk", - "godzalig", - "goeierd", - "gogme", - "goklustig", - "gokwereld", - "gonggrijp", - "gonje", - "goor", - "grabbel", - "graf", - "graveer", - "grif", - "grolleman", - "grom", - "groosman", - "grubben", - "gruijs", - "grut", - "guacamole", - "guido", - "guppy", - "haazen", - "hachelijk", - "haex", - "haiku", - "hakhout", - "hakken", - "hanegem", - "hans", - "hanteer", - "harrie", - "hazebroek", - "hedonist", - "heil", - "heineken", - "hekhuis", - "hekman", - "helbig", - "helga", - "helwegen", - "hengelaar", - "herkansen", - "hermafrodiet", - "hertaald", - "hiaat", - "hikspoors", - "hitachi", - "hitparade", - "hobo", - "hoeve", - "holocaust", - "hond", - "honnepon", - "hoogacht", - "hotelbed", - "hufter", - "hugo", - "huilbier", - "hulk", - "humus", - "huwbaar", - "huwelijk", - "hype", - "iconisch", - "idema", - "ideogram", - "idolaat", - "ietje", - "ijker", - "ijkheid", - "ijklijn", - "ijkmaat", - "ijkwezen", - "ijmuiden", - "ijsbox", - "ijsdag", - "ijselijk", - "ijskoud", - "ilse", - "immuun", - "impliceer", - "impuls", - "inbijten", - "inbuigen", - "indijken", - "induceer", - "indy", - "infecteer", - "inhaak", - "inkijk", - "inluiden", - "inmijnen", - "inoefenen", - "inpolder", - "inrijden", - "inslaan", - "invitatie", - "inwaaien", - "ionisch", - "isaac", - "isolatie", - "isotherm", - "isra", - "italiaan", - "ivoor", - "jacobs", - "jakob", - "jammen", - "jampot", - "jarig", - "jehova", - "jenever", - "jezus", - "joana", - "jobdienst", - "josua", - "joule", - "juich", - "jurk", - "juut", - "kaas", - "kabelaar", - "kabinet", - "kagenaar", - "kajuit", - "kalebas", - "kalm", - "kanjer", - "kapucijn", - "karregat", - "kart", - "katvanger", - "katwijk", - "kegelaar", - "keiachtig", - "keizer", - "kenletter", - "kerdijk", - "keus", - "kevlar", - "kezen", - "kickback", - "kieviet", - "kijken", - "kikvors", - "kilheid", - "kilobit", - "kilsdonk", - "kipschnitzel", - "kissebis", - "klad", - "klagelijk", - "klak", - "klapbaar", - "klaver", - "klene", - "klets", - "klijnhout", - "klit", - "klok", - "klonen", - "klotefilm", - "kluif", - "klumper", - "klus", - "knabbel", - "knagen", - "knaven", - "kneedbaar", - "knmi", - "knul", - "knus", - "kokhals", - "komiek", - "komkommer", - "kompaan", - "komrij", - "komvormig", - "koning", - "kopbal", - "kopklep", - "kopnagel", - "koppejan", - "koptekst", - "kopwand", - "koraal", - "kosmisch", - "kostbaar", - "kram", - "kraneveld", - "kras", - "kreling", - "krengen", - "kribbe", - "krik", - "kruid", - "krulbol", - "kuijper", - "kuipbank", - "kuit", - "kuiven", - "kutsmoes", - "kuub", - "kwak", - "kwatong", - "kwetsbaar", - "kwezelaar", - "kwijnen", - "kwik", - "kwinkslag", - "kwitantie", - "lading", - "lakbeits", - "lakken", - "laklaag", - "lakmoes", - "lakwijk", - "lamheid", - "lamp", - "lamsbout", - "lapmiddel", - "larve", - "laser", - "latijn", - "latuw", - "lawaai", - "laxeerpil", - "lebberen", - "ledeboer", - "leefbaar", - "leeman", - "lefdoekje", - "lefhebber", - "legboor", - "legsel", - "leguaan", - "leiplaat", - "lekdicht", - "lekrijden", - "leksteen", - "lenen", - "leraar", - "lesbienne", - "leugenaar", - "leut", - "lexicaal", - "lezing", - "lieten", - "liggeld", - "lijdzaam", - "lijk", - "lijmstang", - "lijnschip", - "likdoorn", - "likken", - "liksteen", - "limburg", - "link", - "linoleum", - "lipbloem", - "lipman", - "lispelen", - "lissabon", - "litanie", - "liturgie", - "lochem", - "loempia", - "loesje", - "logheid", - "lonen", - "lonneke", - "loom", - "loos", - "losbaar", - "loslaten", - "losplaats", - "loting", - "lotnummer", - "lots", - "louie", - "lourdes", - "louter", - "lowbudget", - "luijten", - "luikenaar", - "luilak", - "luipaard", - "luizenbos", - "lulkoek", - "lumen", - "lunzen", - "lurven", - "lutjeboer", - "luttel", - "lutz", - "luuk", - "luwte", - "luyendijk", - "lyceum", - "lynx", - "maakbaar", - "magdalena", - "malheid", - "manchet", - "manfred", - "manhaftig", - "mank", - "mantel", - "marion", - "marxist", - "masmeijer", - "massaal", - "matsen", - "matverf", - "matze", - "maude", - "mayonaise", - "mechanica", - "meifeest", - "melodie", - "meppelink", - "midvoor", - "midweeks", - "midzomer", - "miezel", - "mijnraad", - "minus", - "mirck", - "mirte", - "mispakken", - "misraden", - "miswassen", - "mitella", - "moker", - "molecule", - "mombakkes", - "moonen", - "mopperaar", - "moraal", - "morgana", - "mormel", - "mosselaar", - "motregen", - "mouw", - "mufheid", - "mutueel", - "muzelman", - "naaidoos", - "naald", - "nadeel", - "nadruk", - "nagy", - "nahon", - "naima", - "nairobi", - "napalm", - "napels", - "napijn", - "napoleon", - "narigheid", - "narratief", - "naseizoen", - "nasibal", - "navigatie", - "nawijn", - "negatief", - "nekletsel", - "nekwervel", - "neolatijn", - "neonataal", - "neptunus", - "nerd", - "nest", - "neuzelaar", - "nihiliste", - "nijenhuis", - "nijging", - "nijhoff", - "nijl", - "nijptang", - "nippel", - "nokkenas", - "noordam", - "noren", - "normaal", - "nottelman", - "notulant", - "nout", - "nuance", - "nuchter", - "nudorp", - "nulde", - "nullijn", - "nulmeting", - "nunspeet", - "nylon", - "obelisk", - "object", - "oblie", - "obsceen", - "occlusie", - "oceaan", - "ochtend", - "ockhuizen", - "oerdom", - "oergezond", - "oerlaag", - "oester", - "okhuijsen", - "olifant", - "olijfboer", - "omaans", - "ombudsman", - "omdat", - "omdijken", - "omdoen", - "omgebouwd", - "omkeer", - "omkomen", - "ommegaand", - "ommuren", - "omroep", - "omruil", - "omslaan", - "omsmeden", - "omvaar", - "onaardig", - "onedel", - "onenig", - "onheilig", - "onrecht", - "onroerend", - "ontcijfer", - "onthaal", - "ontvallen", - "ontzadeld", - "onzacht", - "onzin", - "onzuiver", - "oogappel", - "ooibos", - "ooievaar", - "ooit", - "oorarts", - "oorhanger", - "oorijzer", - "oorklep", - "oorschelp", - "oorworm", - "oorzaak", - "opdagen", - "opdien", - "opdweilen", - "opel", - "opgebaard", - "opinie", - "opjutten", - "opkijken", - "opklaar", - "opkuisen", - "opkwam", - "opnaaien", - "opossum", - "opsieren", - "opsmeer", - "optreden", - "opvijzel", - "opvlammen", - "opwind", - "oraal", - "orchidee", - "orkest", - "ossuarium", - "ostendorf", - "oublie", - "oudachtig", - "oudbakken", - "oudnoors", - "oudshoorn", - "oudtante", - "oven", - "over", - "oxidant", - "pablo", - "pacht", - "paktafel", - "pakzadel", - "paljas", - "panharing", - "papfles", - "paprika", - "parochie", - "paus", - "pauze", - "paviljoen", - "peek", - "pegel", - "peigeren", - "pekela", - "pendant", - "penibel", - "pepmiddel", - "peptalk", - "periferie", - "perron", - "pessarium", - "peter", - "petfles", - "petgat", - "peuk", - "pfeifer", - "picknick", - "pief", - "pieneman", - "pijlkruid", - "pijnacker", - "pijpelink", - "pikdonker", - "pikeer", - "pilaar", - "pionier", - "pipet", - "piscine", - "pissebed", - "pitchen", - "pixel", - "plamuren", - "plan", - "plausibel", - "plegen", - "plempen", - "pleonasme", - "plezant", - "podoloog", - "pofmouw", - "pokdalig", - "ponywagen", - "popachtig", - "popidool", - "porren", - "positie", - "potten", - "pralen", - "prezen", - "prijzen", - "privaat", - "proef", - "prooi", - "prozawerk", - "pruik", - "prul", - "publiceer", - "puck", - "puilen", - "pukkelig", - "pulveren", - "pupil", - "puppy", - "purmerend", - "pustjens", - "putemmer", - "puzzelaar", - "queenie", - "quiche", - "raam", - "raar", - "raat", - "raes", - "ralf", - "rally", - "ramona", - "ramselaar", - "ranonkel", - "rapen", - "rapunzel", - "rarekiek", - "rarigheid", - "rattenhol", - "ravage", - "reactie", - "recreant", - "redacteur", - "redster", - "reewild", - "regie", - "reijnders", - "rein", - "replica", - "revanche", - "rigide", - "rijbaan", - "rijdansen", - "rijgen", - "rijkdom", - "rijles", - "rijnwijn", - "rijpma", - "rijstafel", - "rijtaak", - "rijzwepen", - "rioleer", - "ripdeal", - "riphagen", - "riskant", - "rits", - "rivaal", - "robbedoes", - "robot", - "rockact", - "rodijk", - "rogier", - "rohypnol", - "rollaag", - "rolpaal", - "roltafel", - "roof", - "roon", - "roppen", - "rosbief", - "rosharig", - "rosielle", - "rotan", - "rotleven", - "rotten", - "rotvaart", - "royaal", - "royeer", - "rubato", - "ruby", - "ruche", - "rudge", - "ruggetje", - "rugnummer", - "rugpijn", - "rugtitel", - "rugzak", - "ruilbaar", - "ruis", - "ruit", - "rukwind", - "rulijs", - "rumoeren", - "rumsdorp", - "rumtaart", - "runnen", - "russchen", - "ruwkruid", - "saboteer", - "saksisch", - "salade", - "salpeter", - "sambabal", - "samsam", - "satelliet", - "satineer", - "saus", - "scampi", - "scarabee", - "scenario", - "schobben", - "schubben", - "scout", - "secessie", - "secondair", - "seculair", - "sediment", - "seeland", - "settelen", - "setwinst", - "sheriff", - "shiatsu", - "siciliaan", - "sidderaal", - "sigma", - "sijben", - "silvana", - "simkaart", - "sinds", - "situatie", - "sjaak", - "sjardijn", - "sjezen", - "sjor", - "skinhead", - "skylab", - "slamixen", - "sleijpen", - "slijkerig", - "slordig", - "slowaak", - "sluieren", - "smadelijk", - "smiecht", - "smoel", - "smos", - "smukken", - "snackcar", - "snavel", - "sneaker", - "sneu", - "snijdbaar", - "snit", - "snorder", - "soapbox", - "soetekouw", - "soigneren", - "sojaboon", - "solo", - "solvabel", - "somber", - "sommatie", - "soort", - "soppen", - "sopraan", - "soundbar", - "spanen", - "spawater", - "spijgat", - "spinaal", - "spionage", - "spiraal", - "spleet", - "splijt", - "spoed", - "sporen", - "spul", - "spuug", - "spuw", - "stalen", - "standaard", - "star", - "stefan", - "stencil", - "stijf", - "stil", - "stip", - "stopdas", - "stoten", - "stoven", - "straat", - "strobbe", - "strubbel", - "stucadoor", - "stuif", - "stukadoor", - "subhoofd", - "subregent", - "sudoku", - "sukade", - "sulfaat", - "surinaams", - "suus", - "syfilis", - "symboliek", - "sympathie", - "synagoge", - "synchroon", - "synergie", - "systeem", - "taanderij", - "tabak", - "tachtig", - "tackelen", - "taiwanees", - "talman", - "tamheid", - "tangaslip", - "taps", - "tarkan", - "tarwe", - "tasman", - "tatjana", - "taxameter", - "teil", - "teisman", - "telbaar", - "telco", - "telganger", - "telstar", - "tenant", - "tepel", - "terzet", - "testament", - "ticket", - "tiesinga", - "tijdelijk", - "tika", - "tiksel", - "tilleman", - "timbaal", - "tinsteen", - "tiplijn", - "tippelaar", - "tjirpen", - "toezeggen", - "tolbaas", - "tolgeld", - "tolhek", - "tolo", - "tolpoort", - "toltarief", - "tolvrij", - "tomaat", - "tondeuse", - "toog", - "tooi", - "toonbaar", - "toos", - "topclub", - "toppen", - "toptalent", - "topvrouw", - "toque", - "torment", - "tornado", - "tosti", - "totdat", - "toucheer", - "toulouse", - "tournedos", - "tout", - "trabant", - "tragedie", - "trailer", - "traject", - "traktaat", - "trauma", - "tray", - "trechter", - "tred", - "tref", - "treur", - "troebel", - "tros", - "trucage", - "truffel", - "tsaar", - "tucht", - "tuenter", - "tuitelig", - "tukje", - "tuktuk", - "tulp", - "tuma", - "tureluurs", - "twijfel", - "twitteren", - "tyfoon", - "typograaf", - "ugandees", - "uiachtig", - "uier", - "uisnipper", - "ultiem", - "unitair", - "uranium", - "urbaan", - "urendag", - "ursula", - "uurcirkel", - "uurglas", - "uzelf", - "vaat", - "vakantie", - "vakleraar", - "valbijl", - "valpartij", - "valreep", - "valuatie", - "vanmiddag", - "vanonder", - "varaan", - "varken", - "vaten", - "veenbes", - "veeteler", - "velgrem", - "vellekoop", - "velvet", - "veneberg", - "venlo", - "vent", - "venusberg", - "venw", - "veredeld", - "verf", - "verhaaf", - "vermaak", - "vernaaid", - "verraad", - "vers", - "veruit", - "verzaagd", - "vetachtig", - "vetlok", - "vetmesten", - "veto", - "vetrek", - "vetstaart", - "vetten", - "veurink", - "viaduct", - "vibrafoon", - "vicariaat", - "vieux", - "vieveen", - "vijfvoud", - "villa", - "vilt", - "vimmetje", - "vindbaar", - "vips", - "virtueel", - "visdieven", - "visee", - "visie", - "vlaag", - "vleugel", - "vmbo", - "vocht", - "voesenek", - "voicemail", - "voip", - "volg", - "vork", - "vorselaar", - "voyeur", - "vracht", - "vrekkig", - "vreten", - "vrije", - "vrozen", - "vrucht", - "vucht", - "vugt", - "vulkaan", - "vulmiddel", - "vulva", - "vuren", - "waas", - "wacht", - "wadvogel", - "wafel", - "waffel", - "walhalla", - "walnoot", - "walraven", - "wals", - "walvis", - "wandaad", - "wanen", - "wanmolen", - "want", - "warklomp", - "warm", - "wasachtig", - "wasteil", - "watt", - "webhandel", - "weblog", - "webpagina", - "webzine", - "wedereis", - "wedstrijd", - "weeda", - "weert", - "wegmaaien", - "wegscheer", - "wekelijks", - "wekken", - "wekroep", - "wektoon", - "weldaad", - "welwater", - "wendbaar", - "wenkbrauw", - "wens", - "wentelaar", - "wervel", - "wesseling", - "wetboek", - "wetmatig", - "whirlpool", - "wijbrands", - "wijdbeens", - "wijk", - "wijnbes", - "wijting", - "wild", - "wimpelen", - "wingebied", - "winplaats", - "winter", - "winzucht", - "wipstaart", - "wisgerhof", - "withaar", - "witmaker", - "wokkel", - "wolf", - "wonenden", - "woning", - "worden", - "worp", - "wortel", - "wrat", - "wrijf", - "wringen", - "yoghurt", - "ypsilon", - "zaaijer", - "zaak", - "zacharias", - "zakelijk", - "zakkam", - "zakwater", - "zalf", - "zalig", - "zaniken", - "zebracode", - "zeeblauw", - "zeef", - "zeegaand", - "zeeuw", - "zege", - "zegje", - "zeil", - "zesbaans", - "zesenhalf", - "zeskantig", - "zesmaal", - "zetbaas", - "zetpil", - "zeulen", - "ziezo", - "zigzag", - "zijaltaar", - "zijbeuk", - "zijlijn", - "zijmuur", - "zijn", - "zijwaarts", - "zijzelf", - "zilt", - "zimmerman", - "zinledig", - "zinnelijk", - "zionist", - "zitdag", - "zitruimte", - "zitzak", - "zoal", - "zodoende", - "zoekbots", - "zoem", - "zoiets", - "zojuist", - "zondaar", - "zotskap", - "zottebol", - "zucht", - "zuivel", - "zulk", - "zult", - "zuster", - "zuur", - "zweedijk", - "zwendel", - "zwepen", - "zwiep", - "zwijmel", - "zworen" -] \ No newline at end of file diff --git a/monero-seed/src/words/pt.rs b/monero-seed/src/words/pt.rs deleted file mode 100644 index cede0ac548..0000000000 --- a/monero-seed/src/words/pt.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "abaular", - "abdominal", - "abeto", - "abissinio", - "abjeto", - "ablucao", - "abnegar", - "abotoar", - "abrutalhar", - "absurdo", - "abutre", - "acautelar", - "accessorios", - "acetona", - "achocolatado", - "acirrar", - "acne", - "acovardar", - "acrostico", - "actinomicete", - "acustico", - "adaptavel", - "adeus", - "adivinho", - "adjunto", - "admoestar", - "adnominal", - "adotivo", - "adquirir", - "adriatico", - "adsorcao", - "adutora", - "advogar", - "aerossol", - "afazeres", - "afetuoso", - "afixo", - "afluir", - "afortunar", - "afrouxar", - "aftosa", - "afunilar", - "agentes", - "agito", - "aglutinar", - "aiatola", - "aimore", - "aino", - "aipo", - "airoso", - "ajeitar", - "ajoelhar", - "ajudante", - "ajuste", - "alazao", - "albumina", - "alcunha", - "alegria", - "alexandre", - "alforriar", - "alguns", - "alhures", - "alivio", - "almoxarife", - "alotropico", - "alpiste", - "alquimista", - "alsaciano", - "altura", - "aluviao", - "alvura", - "amazonico", - "ambulatorio", - "ametodico", - "amizades", - "amniotico", - "amovivel", - "amurada", - "anatomico", - "ancorar", - "anexo", - "anfora", - "aniversario", - "anjo", - "anotar", - "ansioso", - "anturio", - "anuviar", - "anverso", - "anzol", - "aonde", - "apaziguar", - "apito", - "aplicavel", - "apoteotico", - "aprimorar", - "aprumo", - "apto", - "apuros", - "aquoso", - "arauto", - "arbusto", - "arduo", - "aresta", - "arfar", - "arguto", - "aritmetico", - "arlequim", - "armisticio", - "aromatizar", - "arpoar", - "arquivo", - "arrumar", - "arsenio", - "arturiano", - "aruaque", - "arvores", - "asbesto", - "ascorbico", - "aspirina", - "asqueroso", - "assustar", - "astuto", - "atazanar", - "ativo", - "atletismo", - "atmosferico", - "atormentar", - "atroz", - "aturdir", - "audivel", - "auferir", - "augusto", - "aula", - "aumento", - "aurora", - "autuar", - "avatar", - "avexar", - "avizinhar", - "avolumar", - "avulso", - "axiomatico", - "azerbaijano", - "azimute", - "azoto", - "azulejo", - "bacteriologista", - "badulaque", - "baforada", - "baixote", - "bajular", - "balzaquiana", - "bambuzal", - "banzo", - "baoba", - "baqueta", - "barulho", - "bastonete", - "batuta", - "bauxita", - "bavaro", - "bazuca", - "bcrepuscular", - "beato", - "beduino", - "begonia", - "behaviorista", - "beisebol", - "belzebu", - "bemol", - "benzido", - "beocio", - "bequer", - "berro", - "besuntar", - "betume", - "bexiga", - "bezerro", - "biatlon", - "biboca", - "bicuspide", - "bidirecional", - "bienio", - "bifurcar", - "bigorna", - "bijuteria", - "bimotor", - "binormal", - "bioxido", - "bipolarizacao", - "biquini", - "birutice", - "bisturi", - "bituca", - "biunivoco", - "bivalve", - "bizarro", - "blasfemo", - "blenorreia", - "blindar", - "bloqueio", - "blusao", - "boazuda", - "bofete", - "bojudo", - "bolso", - "bombordo", - "bonzo", - "botina", - "boquiaberto", - "bostoniano", - "botulismo", - "bourbon", - "bovino", - "boximane", - "bravura", - "brevidade", - "britar", - "broxar", - "bruno", - "bruxuleio", - "bubonico", - "bucolico", - "buda", - "budista", - "bueiro", - "buffer", - "bugre", - "bujao", - "bumerangue", - "burundines", - "busto", - "butique", - "buzios", - "caatinga", - "cabuqui", - "cacunda", - "cafuzo", - "cajueiro", - "camurca", - "canudo", - "caquizeiro", - "carvoeiro", - "casulo", - "catuaba", - "cauterizar", - "cebolinha", - "cedula", - "ceifeiro", - "celulose", - "cerzir", - "cesto", - "cetro", - "ceus", - "cevar", - "chavena", - "cheroqui", - "chita", - "chovido", - "chuvoso", - "ciatico", - "cibernetico", - "cicuta", - "cidreira", - "cientistas", - "cifrar", - "cigarro", - "cilio", - "cimo", - "cinzento", - "cioso", - "cipriota", - "cirurgico", - "cisto", - "citrico", - "ciumento", - "civismo", - "clavicula", - "clero", - "clitoris", - "cluster", - "coaxial", - "cobrir", - "cocota", - "codorniz", - "coexistir", - "cogumelo", - "coito", - "colusao", - "compaixao", - "comutativo", - "contentamento", - "convulsivo", - "coordenativa", - "coquetel", - "correto", - "corvo", - "costureiro", - "cotovia", - "covil", - "cozinheiro", - "cretino", - "cristo", - "crivo", - "crotalo", - "cruzes", - "cubo", - "cucuia", - "cueiro", - "cuidar", - "cujo", - "cultural", - "cunilingua", - "cupula", - "curvo", - "custoso", - "cutucar", - "czarismo", - "dablio", - "dacota", - "dados", - "daguerreotipo", - "daiquiri", - "daltonismo", - "damista", - "dantesco", - "daquilo", - "darwinista", - "dasein", - "dativo", - "deao", - "debutantes", - "decurso", - "deduzir", - "defunto", - "degustar", - "dejeto", - "deltoide", - "demover", - "denunciar", - "deputado", - "deque", - "dervixe", - "desvirtuar", - "deturpar", - "deuteronomio", - "devoto", - "dextrose", - "dezoito", - "diatribe", - "dicotomico", - "didatico", - "dietista", - "difuso", - "digressao", - "diluvio", - "diminuto", - "dinheiro", - "dinossauro", - "dioxido", - "diplomatico", - "dique", - "dirimivel", - "disturbio", - "diurno", - "divulgar", - "dizivel", - "doar", - "dobro", - "docura", - "dodoi", - "doer", - "dogue", - "doloso", - "domo", - "donzela", - "doping", - "dorsal", - "dossie", - "dote", - "doutro", - "doze", - "dravidico", - "dreno", - "driver", - "dropes", - "druso", - "dubnio", - "ducto", - "dueto", - "dulija", - "dundum", - "duodeno", - "duquesa", - "durou", - "duvidoso", - "duzia", - "ebano", - "ebrio", - "eburneo", - "echarpe", - "eclusa", - "ecossistema", - "ectoplasma", - "ecumenismo", - "eczema", - "eden", - "editorial", - "edredom", - "edulcorar", - "efetuar", - "efigie", - "efluvio", - "egiptologo", - "egresso", - "egua", - "einsteiniano", - "eira", - "eivar", - "eixos", - "ejetar", - "elastomero", - "eldorado", - "elixir", - "elmo", - "eloquente", - "elucidativo", - "emaranhar", - "embutir", - "emerito", - "emfa", - "emitir", - "emotivo", - "empuxo", - "emulsao", - "enamorar", - "encurvar", - "enduro", - "enevoar", - "enfurnar", - "enguico", - "enho", - "enigmista", - "enlutar", - "enormidade", - "enpreendimento", - "enquanto", - "enriquecer", - "enrugar", - "entusiastico", - "enunciar", - "envolvimento", - "enxuto", - "enzimatico", - "eolico", - "epiteto", - "epoxi", - "epura", - "equivoco", - "erario", - "erbio", - "ereto", - "erguido", - "erisipela", - "ermo", - "erotizar", - "erros", - "erupcao", - "ervilha", - "esburacar", - "escutar", - "esfuziante", - "esguio", - "esloveno", - "esmurrar", - "esoterismo", - "esperanca", - "espirito", - "espurio", - "essencialmente", - "esturricar", - "esvoacar", - "etario", - "eterno", - "etiquetar", - "etnologo", - "etos", - "etrusco", - "euclidiano", - "euforico", - "eugenico", - "eunuco", - "europio", - "eustaquio", - "eutanasia", - "evasivo", - "eventualidade", - "evitavel", - "evoluir", - "exaustor", - "excursionista", - "exercito", - "exfoliado", - "exito", - "exotico", - "expurgo", - "exsudar", - "extrusora", - "exumar", - "fabuloso", - "facultativo", - "fado", - "fagulha", - "faixas", - "fajuto", - "faltoso", - "famoso", - "fanzine", - "fapesp", - "faquir", - "fartura", - "fastio", - "faturista", - "fausto", - "favorito", - "faxineira", - "fazer", - "fealdade", - "febril", - "fecundo", - "fedorento", - "feerico", - "feixe", - "felicidade", - "felpudo", - "feltro", - "femur", - "fenotipo", - "fervura", - "festivo", - "feto", - "feudo", - "fevereiro", - "fezinha", - "fiasco", - "fibra", - "ficticio", - "fiduciario", - "fiesp", - "fifa", - "figurino", - "fijiano", - "filtro", - "finura", - "fiorde", - "fiquei", - "firula", - "fissurar", - "fitoteca", - "fivela", - "fixo", - "flavio", - "flexor", - "flibusteiro", - "flotilha", - "fluxograma", - "fobos", - "foco", - "fofura", - "foguista", - "foie", - "foliculo", - "fominha", - "fonte", - "forum", - "fosso", - "fotossintese", - "foxtrote", - "fraudulento", - "frevo", - "frivolo", - "frouxo", - "frutose", - "fuba", - "fucsia", - "fugitivo", - "fuinha", - "fujao", - "fulustreco", - "fumo", - "funileiro", - "furunculo", - "fustigar", - "futurologo", - "fuxico", - "fuzue", - "gabriel", - "gado", - "gaelico", - "gafieira", - "gaguejo", - "gaivota", - "gajo", - "galvanoplastico", - "gamo", - "ganso", - "garrucha", - "gastronomo", - "gatuno", - "gaussiano", - "gaviao", - "gaxeta", - "gazeteiro", - "gear", - "geiser", - "geminiano", - "generoso", - "genuino", - "geossinclinal", - "gerundio", - "gestual", - "getulista", - "gibi", - "gigolo", - "gilete", - "ginseng", - "giroscopio", - "glaucio", - "glacial", - "gleba", - "glifo", - "glote", - "glutonia", - "gnostico", - "goela", - "gogo", - "goitaca", - "golpista", - "gomo", - "gonzo", - "gorro", - "gostou", - "goticula", - "gourmet", - "governo", - "gozo", - "graxo", - "grevista", - "grito", - "grotesco", - "gruta", - "guaxinim", - "gude", - "gueto", - "guizo", - "guloso", - "gume", - "guru", - "gustativo", - "grelhado", - "gutural", - "habitue", - "haitiano", - "halterofilista", - "hamburguer", - "hanseniase", - "happening", - "harpista", - "hastear", - "haveres", - "hebreu", - "hectometro", - "hedonista", - "hegira", - "helena", - "helminto", - "hemorroidas", - "henrique", - "heptassilabo", - "hertziano", - "hesitar", - "heterossexual", - "heuristico", - "hexagono", - "hiato", - "hibrido", - "hidrostatico", - "hieroglifo", - "hifenizar", - "higienizar", - "hilario", - "himen", - "hino", - "hippie", - "hirsuto", - "historiografia", - "hitlerista", - "hodometro", - "hoje", - "holograma", - "homus", - "honroso", - "hoquei", - "horto", - "hostilizar", - "hotentote", - "huguenote", - "humilde", - "huno", - "hurra", - "hutu", - "iaia", - "ialorixa", - "iambico", - "iansa", - "iaque", - "iara", - "iatista", - "iberico", - "ibis", - "icar", - "iceberg", - "icosagono", - "idade", - "ideologo", - "idiotice", - "idoso", - "iemenita", - "iene", - "igarape", - "iglu", - "ignorar", - "igreja", - "iguaria", - "iidiche", - "ilativo", - "iletrado", - "ilharga", - "ilimitado", - "ilogismo", - "ilustrissimo", - "imaturo", - "imbuzeiro", - "imerso", - "imitavel", - "imovel", - "imputar", - "imutavel", - "inaveriguavel", - "incutir", - "induzir", - "inextricavel", - "infusao", - "ingua", - "inhame", - "iniquo", - "injusto", - "inning", - "inoxidavel", - "inquisitorial", - "insustentavel", - "intumescimento", - "inutilizavel", - "invulneravel", - "inzoneiro", - "iodo", - "iogurte", - "ioio", - "ionosfera", - "ioruba", - "iota", - "ipsilon", - "irascivel", - "iris", - "irlandes", - "irmaos", - "iroques", - "irrupcao", - "isca", - "isento", - "islandes", - "isotopo", - "isqueiro", - "israelita", - "isso", - "isto", - "iterbio", - "itinerario", - "itrio", - "iuane", - "iugoslavo", - "jabuticabeira", - "jacutinga", - "jade", - "jagunco", - "jainista", - "jaleco", - "jambo", - "jantarada", - "japones", - "jaqueta", - "jarro", - "jasmim", - "jato", - "jaula", - "javel", - "jazz", - "jegue", - "jeitoso", - "jejum", - "jenipapo", - "jeova", - "jequitiba", - "jersei", - "jesus", - "jetom", - "jiboia", - "jihad", - "jilo", - "jingle", - "jipe", - "jocoso", - "joelho", - "joguete", - "joio", - "jojoba", - "jorro", - "jota", - "joule", - "joviano", - "jubiloso", - "judoca", - "jugular", - "juizo", - "jujuba", - "juliano", - "jumento", - "junto", - "jururu", - "justo", - "juta", - "juventude", - "labutar", - "laguna", - "laico", - "lajota", - "lanterninha", - "lapso", - "laquear", - "lastro", - "lauto", - "lavrar", - "laxativo", - "lazer", - "leasing", - "lebre", - "lecionar", - "ledo", - "leguminoso", - "leitura", - "lele", - "lemure", - "lento", - "leonardo", - "leopardo", - "lepton", - "leque", - "leste", - "letreiro", - "leucocito", - "levitico", - "lexicologo", - "lhama", - "lhufas", - "liame", - "licoroso", - "lidocaina", - "liliputiano", - "limusine", - "linotipo", - "lipoproteina", - "liquidos", - "lirismo", - "lisura", - "liturgico", - "livros", - "lixo", - "lobulo", - "locutor", - "lodo", - "logro", - "lojista", - "lombriga", - "lontra", - "loop", - "loquaz", - "lorota", - "losango", - "lotus", - "louvor", - "luar", - "lubrificavel", - "lucros", - "lugubre", - "luis", - "luminoso", - "luneta", - "lustroso", - "luto", - "luvas", - "luxuriante", - "luzeiro", - "maduro", - "maestro", - "mafioso", - "magro", - "maiuscula", - "majoritario", - "malvisto", - "mamute", - "manutencao", - "mapoteca", - "maquinista", - "marzipa", - "masturbar", - "matuto", - "mausoleu", - "mavioso", - "maxixe", - "mazurca", - "meandro", - "mecha", - "medusa", - "mefistofelico", - "megera", - "meirinho", - "melro", - "memorizar", - "menu", - "mequetrefe", - "mertiolate", - "mestria", - "metroviario", - "mexilhao", - "mezanino", - "miau", - "microssegundo", - "midia", - "migratorio", - "mimosa", - "minuto", - "miosotis", - "mirtilo", - "misturar", - "mitzvah", - "miudos", - "mixuruca", - "mnemonico", - "moagem", - "mobilizar", - "modulo", - "moer", - "mofo", - "mogno", - "moita", - "molusco", - "monumento", - "moqueca", - "morubixaba", - "mostruario", - "motriz", - "mouse", - "movivel", - "mozarela", - "muarra", - "muculmano", - "mudo", - "mugir", - "muitos", - "mumunha", - "munir", - "muon", - "muquira", - "murros", - "musselina", - "nacoes", - "nado", - "naftalina", - "nago", - "naipe", - "naja", - "nalgum", - "namoro", - "nanquim", - "napolitano", - "naquilo", - "nascimento", - "nautilo", - "navios", - "nazista", - "nebuloso", - "nectarina", - "nefrologo", - "negus", - "nelore", - "nenufar", - "nepotismo", - "nervura", - "neste", - "netuno", - "neutron", - "nevoeiro", - "newtoniano", - "nexo", - "nhenhenhem", - "nhoque", - "nigeriano", - "niilista", - "ninho", - "niobio", - "niponico", - "niquelar", - "nirvana", - "nisto", - "nitroglicerina", - "nivoso", - "nobreza", - "nocivo", - "noel", - "nogueira", - "noivo", - "nojo", - "nominativo", - "nonuplo", - "noruegues", - "nostalgico", - "noturno", - "nouveau", - "nuanca", - "nublar", - "nucleotideo", - "nudista", - "nulo", - "numismatico", - "nunquinha", - "nupcias", - "nutritivo", - "nuvens", - "oasis", - "obcecar", - "obeso", - "obituario", - "objetos", - "oblongo", - "obnoxio", - "obrigatorio", - "obstruir", - "obtuso", - "obus", - "obvio", - "ocaso", - "occipital", - "oceanografo", - "ocioso", - "oclusivo", - "ocorrer", - "ocre", - "octogono", - "odalisca", - "odisseia", - "odorifico", - "oersted", - "oeste", - "ofertar", - "ofidio", - "oftalmologo", - "ogiva", - "ogum", - "oigale", - "oitavo", - "oitocentos", - "ojeriza", - "olaria", - "oleoso", - "olfato", - "olhos", - "oliveira", - "olmo", - "olor", - "olvidavel", - "ombudsman", - "omeleteira", - "omitir", - "omoplata", - "onanismo", - "ondular", - "oneroso", - "onomatopeico", - "ontologico", - "onus", - "onze", - "opalescente", - "opcional", - "operistico", - "opio", - "oposto", - "oprobrio", - "optometrista", - "opusculo", - "oratorio", - "orbital", - "orcar", - "orfao", - "orixa", - "orla", - "ornitologo", - "orquidea", - "ortorrombico", - "orvalho", - "osculo", - "osmotico", - "ossudo", - "ostrogodo", - "otario", - "otite", - "ouro", - "ousar", - "outubro", - "ouvir", - "ovario", - "overnight", - "oviparo", - "ovni", - "ovoviviparo", - "ovulo", - "oxala", - "oxente", - "oxiuro", - "oxossi", - "ozonizar", - "paciente", - "pactuar", - "padronizar", - "paete", - "pagodeiro", - "paixao", - "pajem", - "paludismo", - "pampas", - "panturrilha", - "papudo", - "paquistanes", - "pastoso", - "patua", - "paulo", - "pauzinhos", - "pavoroso", - "paxa", - "pazes", - "peao", - "pecuniario", - "pedunculo", - "pegaso", - "peixinho", - "pejorativo", - "pelvis", - "penuria", - "pequno", - "petunia", - "pezada", - "piauiense", - "pictorico", - "pierro", - "pigmeu", - "pijama", - "pilulas", - "pimpolho", - "pintura", - "piorar", - "pipocar", - "piqueteiro", - "pirulito", - "pistoleiro", - "pituitaria", - "pivotar", - "pixote", - "pizzaria", - "plistoceno", - "plotar", - "pluviometrico", - "pneumonico", - "poco", - "podridao", - "poetisa", - "pogrom", - "pois", - "polvorosa", - "pomposo", - "ponderado", - "pontudo", - "populoso", - "poquer", - "porvir", - "posudo", - "potro", - "pouso", - "povoar", - "prazo", - "prezar", - "privilegios", - "proximo", - "prussiano", - "pseudopode", - "psoriase", - "pterossauros", - "ptialina", - "ptolemaico", - "pudor", - "pueril", - "pufe", - "pugilista", - "puir", - "pujante", - "pulverizar", - "pumba", - "punk", - "purulento", - "pustula", - "putsch", - "puxe", - "quatrocentos", - "quetzal", - "quixotesco", - "quotizavel", - "rabujice", - "racista", - "radonio", - "rafia", - "ragu", - "rajado", - "ralo", - "rampeiro", - "ranzinza", - "raptor", - "raquitismo", - "raro", - "rasurar", - "ratoeira", - "ravioli", - "razoavel", - "reavivar", - "rebuscar", - "recusavel", - "reduzivel", - "reexposicao", - "refutavel", - "regurgitar", - "reivindicavel", - "rejuvenescimento", - "relva", - "remuneravel", - "renunciar", - "reorientar", - "repuxo", - "requisito", - "resumo", - "returno", - "reutilizar", - "revolvido", - "rezonear", - "riacho", - "ribossomo", - "ricota", - "ridiculo", - "rifle", - "rigoroso", - "rijo", - "rimel", - "rins", - "rios", - "riqueza", - "respeito", - "rissole", - "ritualistico", - "rivalizar", - "rixa", - "robusto", - "rococo", - "rodoviario", - "roer", - "rogo", - "rojao", - "rolo", - "rompimento", - "ronronar", - "roqueiro", - "rorqual", - "rosto", - "rotundo", - "rouxinol", - "roxo", - "royal", - "ruas", - "rucula", - "rudimentos", - "ruela", - "rufo", - "rugoso", - "ruivo", - "rule", - "rumoroso", - "runico", - "ruptura", - "rural", - "rustico", - "rutilar", - "saariano", - "sabujo", - "sacudir", - "sadomasoquista", - "safra", - "sagui", - "sais", - "samurai", - "santuario", - "sapo", - "saquear", - "sartriano", - "saturno", - "saude", - "sauva", - "saveiro", - "saxofonista", - "sazonal", - "scherzo", - "script", - "seara", - "seborreia", - "secura", - "seduzir", - "sefardim", - "seguro", - "seja", - "selvas", - "sempre", - "senzala", - "sepultura", - "sequoia", - "sestercio", - "setuplo", - "seus", - "seviciar", - "sezonismo", - "shalom", - "siames", - "sibilante", - "sicrano", - "sidra", - "sifilitico", - "signos", - "silvo", - "simultaneo", - "sinusite", - "sionista", - "sirio", - "sisudo", - "situar", - "sivan", - "slide", - "slogan", - "soar", - "sobrio", - "socratico", - "sodomizar", - "soerguer", - "software", - "sogro", - "soja", - "solver", - "somente", - "sonso", - "sopro", - "soquete", - "sorveteiro", - "sossego", - "soturno", - "sousafone", - "sovinice", - "sozinho", - "suavizar", - "subverter", - "sucursal", - "sudoriparo", - "sufragio", - "sugestoes", - "suite", - "sujo", - "sultao", - "sumula", - "suntuoso", - "suor", - "supurar", - "suruba", - "susto", - "suturar", - "suvenir", - "tabuleta", - "taco", - "tadjique", - "tafeta", - "tagarelice", - "taitiano", - "talvez", - "tampouco", - "tanzaniano", - "taoista", - "tapume", - "taquion", - "tarugo", - "tascar", - "tatuar", - "tautologico", - "tavola", - "taxionomista", - "tchecoslovaco", - "teatrologo", - "tectonismo", - "tedioso", - "teflon", - "tegumento", - "teixo", - "telurio", - "temporas", - "tenue", - "teosofico", - "tepido", - "tequila", - "terrorista", - "testosterona", - "tetrico", - "teutonico", - "teve", - "texugo", - "tiara", - "tibia", - "tiete", - "tifoide", - "tigresa", - "tijolo", - "tilintar", - "timpano", - "tintureiro", - "tiquete", - "tiroteio", - "tisico", - "titulos", - "tive", - "toar", - "toboga", - "tofu", - "togoles", - "toicinho", - "tolueno", - "tomografo", - "tontura", - "toponimo", - "toquio", - "torvelinho", - "tostar", - "toto", - "touro", - "toxina", - "trazer", - "trezentos", - "trivialidade", - "trovoar", - "truta", - "tuaregue", - "tubular", - "tucano", - "tudo", - "tufo", - "tuiste", - "tulipa", - "tumultuoso", - "tunisino", - "tupiniquim", - "turvo", - "tutu", - "ucraniano", - "udenista", - "ufanista", - "ufologo", - "ugaritico", - "uiste", - "uivo", - "ulceroso", - "ulema", - "ultravioleta", - "umbilical", - "umero", - "umido", - "umlaut", - "unanimidade", - "unesco", - "ungulado", - "unheiro", - "univoco", - "untuoso", - "urano", - "urbano", - "urdir", - "uretra", - "urgente", - "urinol", - "urna", - "urologo", - "urro", - "ursulina", - "urtiga", - "urupe", - "usavel", - "usbeque", - "usei", - "usineiro", - "usurpar", - "utero", - "utilizar", - "utopico", - "uvular", - "uxoricidio", - "vacuo", - "vadio", - "vaguear", - "vaivem", - "valvula", - "vampiro", - "vantajoso", - "vaporoso", - "vaquinha", - "varziano", - "vasto", - "vaticinio", - "vaudeville", - "vazio", - "veado", - "vedico", - "veemente", - "vegetativo", - "veio", - "veja", - "veludo", - "venusiano", - "verdade", - "verve", - "vestuario", - "vetusto", - "vexatorio", - "vezes", - "viavel", - "vibratorio", - "victor", - "vicunha", - "vidros", - "vietnamita", - "vigoroso", - "vilipendiar", - "vime", - "vintem", - "violoncelo", - "viquingue", - "virus", - "visualizar", - "vituperio", - "viuvo", - "vivo", - "vizir", - "voar", - "vociferar", - "vodu", - "vogar", - "voile", - "volver", - "vomito", - "vontade", - "vortice", - "vosso", - "voto", - "vovozinha", - "voyeuse", - "vozes", - "vulva", - "vupt", - "western", - "xadrez", - "xale", - "xampu", - "xango", - "xarope", - "xaual", - "xavante", - "xaxim", - "xenonio", - "xepa", - "xerox", - "xicara", - "xifopago", - "xiita", - "xilogravura", - "xinxim", - "xistoso", - "xixi", - "xodo", - "xogum", - "xucro", - "zabumba", - "zagueiro", - "zambiano", - "zanzar", - "zarpar", - "zebu", - "zefiro", - "zeloso", - "zenite", - "zumbi" -] \ No newline at end of file diff --git a/monero-seed/src/words/ru.rs b/monero-seed/src/words/ru.rs deleted file mode 100644 index 609fa4cbef..0000000000 --- a/monero-seed/src/words/ru.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "абажур", - "абзац", - "абонент", - "абрикос", - "абсурд", - "авангард", - "август", - "авиация", - "авоська", - "автор", - "агат", - "агент", - "агитатор", - "агнец", - "агония", - "агрегат", - "адвокат", - "адмирал", - "адрес", - "ажиотаж", - "азарт", - "азбука", - "азот", - "аист", - "айсберг", - "академия", - "аквариум", - "аккорд", - "акробат", - "аксиома", - "актер", - "акула", - "акция", - "алгоритм", - "алебарда", - "аллея", - "алмаз", - "алтарь", - "алфавит", - "алхимик", - "алый", - "альбом", - "алюминий", - "амбар", - "аметист", - "амнезия", - "ампула", - "амфора", - "анализ", - "ангел", - "анекдот", - "анимация", - "анкета", - "аномалия", - "ансамбль", - "антенна", - "апатия", - "апельсин", - "апофеоз", - "аппарат", - "апрель", - "аптека", - "арабский", - "арбуз", - "аргумент", - "арест", - "ария", - "арка", - "армия", - "аромат", - "арсенал", - "артист", - "архив", - "аршин", - "асбест", - "аскетизм", - "аспект", - "ассорти", - "астроном", - "асфальт", - "атака", - "ателье", - "атлас", - "атом", - "атрибут", - "аудитор", - "аукцион", - "аура", - "афера", - "афиша", - "ахинея", - "ацетон", - "аэропорт", - "бабушка", - "багаж", - "бадья", - "база", - "баклажан", - "балкон", - "бампер", - "банк", - "барон", - "бассейн", - "батарея", - "бахрома", - "башня", - "баян", - "бегство", - "бедро", - "бездна", - "бекон", - "белый", - "бензин", - "берег", - "беседа", - "бетонный", - "биатлон", - "библия", - "бивень", - "бигуди", - "бидон", - "бизнес", - "бикини", - "билет", - "бинокль", - "биология", - "биржа", - "бисер", - "битва", - "бицепс", - "благо", - "бледный", - "близкий", - "блок", - "блуждать", - "блюдо", - "бляха", - "бобер", - "богатый", - "бодрый", - "боевой", - "бокал", - "большой", - "борьба", - "босой", - "ботинок", - "боцман", - "бочка", - "боярин", - "брать", - "бревно", - "бригада", - "бросать", - "брызги", - "брюки", - "бублик", - "бугор", - "будущее", - "буква", - "бульвар", - "бумага", - "бунт", - "бурный", - "бусы", - "бутылка", - "буфет", - "бухта", - "бушлат", - "бывалый", - "быль", - "быстрый", - "быть", - "бюджет", - "бюро", - "бюст", - "вагон", - "важный", - "ваза", - "вакцина", - "валюта", - "вампир", - "ванная", - "вариант", - "вассал", - "вата", - "вафля", - "вахта", - "вдова", - "вдыхать", - "ведущий", - "веер", - "вежливый", - "везти", - "веко", - "великий", - "вена", - "верить", - "веселый", - "ветер", - "вечер", - "вешать", - "вещь", - "веяние", - "взаимный", - "взбучка", - "взвод", - "взгляд", - "вздыхать", - "взлетать", - "взмах", - "взнос", - "взор", - "взрыв", - "взывать", - "взятка", - "вибрация", - "визит", - "вилка", - "вино", - "вирус", - "висеть", - "витрина", - "вихрь", - "вишневый", - "включать", - "вкус", - "власть", - "влечь", - "влияние", - "влюблять", - "внешний", - "внимание", - "внук", - "внятный", - "вода", - "воевать", - "вождь", - "воздух", - "войти", - "вокзал", - "волос", - "вопрос", - "ворота", - "восток", - "впадать", - "впускать", - "врач", - "время", - "вручать", - "всадник", - "всеобщий", - "вспышка", - "встреча", - "вторник", - "вулкан", - "вурдалак", - "входить", - "въезд", - "выбор", - "вывод", - "выгодный", - "выделять", - "выезжать", - "выживать", - "вызывать", - "выигрыш", - "вылезать", - "выносить", - "выпивать", - "высокий", - "выходить", - "вычет", - "вышка", - "выяснять", - "вязать", - "вялый", - "гавань", - "гадать", - "газета", - "гаишник", - "галстук", - "гамма", - "гарантия", - "гастроли", - "гвардия", - "гвоздь", - "гектар", - "гель", - "генерал", - "геолог", - "герой", - "гешефт", - "гибель", - "гигант", - "гильза", - "гимн", - "гипотеза", - "гитара", - "глаз", - "глина", - "глоток", - "глубокий", - "глыба", - "глядеть", - "гнать", - "гнев", - "гнить", - "гном", - "гнуть", - "говорить", - "годовой", - "голова", - "гонка", - "город", - "гость", - "готовый", - "граница", - "грех", - "гриб", - "громкий", - "группа", - "грызть", - "грязный", - "губа", - "гудеть", - "гулять", - "гуманный", - "густой", - "гуща", - "давать", - "далекий", - "дама", - "данные", - "дарить", - "дать", - "дача", - "дверь", - "движение", - "двор", - "дебют", - "девушка", - "дедушка", - "дежурный", - "дезертир", - "действие", - "декабрь", - "дело", - "демократ", - "день", - "депутат", - "держать", - "десяток", - "детский", - "дефицит", - "дешевый", - "деятель", - "джаз", - "джинсы", - "джунгли", - "диалог", - "диван", - "диета", - "дизайн", - "дикий", - "динамика", - "диплом", - "директор", - "диск", - "дитя", - "дичь", - "длинный", - "дневник", - "добрый", - "доверие", - "договор", - "дождь", - "доза", - "документ", - "должен", - "домашний", - "допрос", - "дорога", - "доход", - "доцент", - "дочь", - "дощатый", - "драка", - "древний", - "дрожать", - "друг", - "дрянь", - "дубовый", - "дуга", - "дудка", - "дукат", - "дуло", - "думать", - "дупло", - "дурак", - "дуть", - "духи", - "душа", - "дуэт", - "дымить", - "дыня", - "дыра", - "дыханье", - "дышать", - "дьявол", - "дюжина", - "дюйм", - "дюна", - "дядя", - "дятел", - "егерь", - "единый", - "едкий", - "ежевика", - "ежик", - "езда", - "елка", - "емкость", - "ерунда", - "ехать", - "жадный", - "жажда", - "жалеть", - "жанр", - "жара", - "жать", - "жгучий", - "ждать", - "жевать", - "желание", - "жемчуг", - "женщина", - "жертва", - "жесткий", - "жечь", - "живой", - "жидкость", - "жизнь", - "жилье", - "жирный", - "житель", - "журнал", - "жюри", - "забывать", - "завод", - "загадка", - "задача", - "зажечь", - "зайти", - "закон", - "замечать", - "занимать", - "западный", - "зарплата", - "засыпать", - "затрата", - "захват", - "зацепка", - "зачет", - "защита", - "заявка", - "звать", - "звезда", - "звонить", - "звук", - "здание", - "здешний", - "здоровье", - "зебра", - "зевать", - "зеленый", - "земля", - "зенит", - "зеркало", - "зефир", - "зигзаг", - "зима", - "зиять", - "злак", - "злой", - "змея", - "знать", - "зной", - "зодчий", - "золотой", - "зомби", - "зона", - "зоопарк", - "зоркий", - "зрачок", - "зрение", - "зритель", - "зубной", - "зыбкий", - "зять", - "игла", - "иголка", - "играть", - "идея", - "идиот", - "идол", - "идти", - "иерархия", - "избрать", - "известие", - "изгонять", - "издание", - "излагать", - "изменять", - "износ", - "изоляция", - "изрядный", - "изучать", - "изымать", - "изящный", - "икона", - "икра", - "иллюзия", - "имбирь", - "иметь", - "имидж", - "иммунный", - "империя", - "инвестор", - "индивид", - "инерция", - "инженер", - "иномарка", - "институт", - "интерес", - "инфекция", - "инцидент", - "ипподром", - "ирис", - "ирония", - "искать", - "история", - "исходить", - "исчезать", - "итог", - "июль", - "июнь", - "кабинет", - "кавалер", - "кадр", - "казарма", - "кайф", - "кактус", - "калитка", - "камень", - "канал", - "капитан", - "картина", - "касса", - "катер", - "кафе", - "качество", - "каша", - "каюта", - "квартира", - "квинтет", - "квота", - "кедр", - "кекс", - "кенгуру", - "кепка", - "керосин", - "кетчуп", - "кефир", - "кибитка", - "кивнуть", - "кидать", - "километр", - "кино", - "киоск", - "кипеть", - "кирпич", - "кисть", - "китаец", - "класс", - "клетка", - "клиент", - "клоун", - "клуб", - "клык", - "ключ", - "клятва", - "книга", - "кнопка", - "кнут", - "князь", - "кобура", - "ковер", - "коготь", - "кодекс", - "кожа", - "козел", - "койка", - "коктейль", - "колено", - "компания", - "конец", - "копейка", - "короткий", - "костюм", - "котел", - "кофе", - "кошка", - "красный", - "кресло", - "кричать", - "кровь", - "крупный", - "крыша", - "крючок", - "кубок", - "кувшин", - "кудрявый", - "кузов", - "кукла", - "культура", - "кумир", - "купить", - "курс", - "кусок", - "кухня", - "куча", - "кушать", - "кювет", - "лабиринт", - "лавка", - "лагерь", - "ладонь", - "лазерный", - "лайнер", - "лакей", - "лампа", - "ландшафт", - "лапа", - "ларек", - "ласковый", - "лауреат", - "лачуга", - "лаять", - "лгать", - "лебедь", - "левый", - "легкий", - "ледяной", - "лежать", - "лекция", - "лента", - "лепесток", - "лесной", - "лето", - "лечь", - "леший", - "лживый", - "либерал", - "ливень", - "лига", - "лидер", - "ликовать", - "лиловый", - "лимон", - "линия", - "липа", - "лирика", - "лист", - "литр", - "лифт", - "лихой", - "лицо", - "личный", - "лишний", - "лобовой", - "ловить", - "логика", - "лодка", - "ложка", - "лозунг", - "локоть", - "ломать", - "лоно", - "лопата", - "лорд", - "лось", - "лоток", - "лохматый", - "лошадь", - "лужа", - "лукавый", - "луна", - "лупить", - "лучший", - "лыжный", - "лысый", - "львиный", - "льгота", - "льдина", - "любить", - "людской", - "люстра", - "лютый", - "лягушка", - "магазин", - "мадам", - "мазать", - "майор", - "максимум", - "мальчик", - "манера", - "март", - "масса", - "мать", - "мафия", - "махать", - "мачта", - "машина", - "маэстро", - "маяк", - "мгла", - "мебель", - "медведь", - "мелкий", - "мемуары", - "менять", - "мера", - "место", - "метод", - "механизм", - "мечтать", - "мешать", - "миграция", - "мизинец", - "микрофон", - "миллион", - "минута", - "мировой", - "миссия", - "митинг", - "мишень", - "младший", - "мнение", - "мнимый", - "могила", - "модель", - "мозг", - "мойка", - "мокрый", - "молодой", - "момент", - "монах", - "море", - "мост", - "мотор", - "мохнатый", - "мочь", - "мошенник", - "мощный", - "мрачный", - "мстить", - "мудрый", - "мужчина", - "музыка", - "мука", - "мумия", - "мундир", - "муравей", - "мусор", - "мутный", - "муфта", - "муха", - "мучить", - "мушкетер", - "мыло", - "мысль", - "мыть", - "мычать", - "мышь", - "мэтр", - "мюзикл", - "мягкий", - "мякиш", - "мясо", - "мятый", - "мячик", - "набор", - "навык", - "нагрузка", - "надежда", - "наемный", - "нажать", - "называть", - "наивный", - "накрыть", - "налог", - "намерен", - "наносить", - "написать", - "народ", - "натура", - "наука", - "нация", - "начать", - "небо", - "невеста", - "негодяй", - "неделя", - "нежный", - "незнание", - "нелепый", - "немалый", - "неправда", - "нервный", - "нести", - "нефть", - "нехватка", - "нечистый", - "неясный", - "нива", - "нижний", - "низкий", - "никель", - "нирвана", - "нить", - "ничья", - "ниша", - "нищий", - "новый", - "нога", - "ножницы", - "ноздря", - "ноль", - "номер", - "норма", - "нота", - "ночь", - "ноша", - "ноябрь", - "нрав", - "нужный", - "нутро", - "нынешний", - "нырнуть", - "ныть", - "нюанс", - "нюхать", - "няня", - "оазис", - "обаяние", - "обвинять", - "обгонять", - "обещать", - "обжигать", - "обзор", - "обида", - "область", - "обмен", - "обнимать", - "оборона", - "образ", - "обучение", - "обходить", - "обширный", - "общий", - "объект", - "обычный", - "обязать", - "овальный", - "овес", - "овощи", - "овраг", - "овца", - "овчарка", - "огненный", - "огонь", - "огромный", - "огурец", - "одежда", - "одинокий", - "одобрить", - "ожидать", - "ожог", - "озарение", - "озеро", - "означать", - "оказать", - "океан", - "оклад", - "окно", - "округ", - "октябрь", - "окурок", - "олень", - "опасный", - "операция", - "описать", - "оплата", - "опора", - "оппонент", - "опрос", - "оптимизм", - "опускать", - "опыт", - "орать", - "орбита", - "орган", - "орден", - "орел", - "оригинал", - "оркестр", - "орнамент", - "оружие", - "осадок", - "освещать", - "осень", - "осина", - "осколок", - "осмотр", - "основной", - "особый", - "осуждать", - "отбор", - "отвечать", - "отдать", - "отец", - "отзыв", - "открытие", - "отмечать", - "относить", - "отпуск", - "отрасль", - "отставка", - "оттенок", - "отходить", - "отчет", - "отъезд", - "офицер", - "охапка", - "охота", - "охрана", - "оценка", - "очаг", - "очередь", - "очищать", - "очки", - "ошейник", - "ошибка", - "ощущение", - "павильон", - "падать", - "паек", - "пакет", - "палец", - "память", - "панель", - "папка", - "партия", - "паспорт", - "патрон", - "пауза", - "пафос", - "пахнуть", - "пациент", - "пачка", - "пашня", - "певец", - "педагог", - "пейзаж", - "пельмень", - "пенсия", - "пепел", - "период", - "песня", - "петля", - "пехота", - "печать", - "пешеход", - "пещера", - "пианист", - "пиво", - "пиджак", - "пиковый", - "пилот", - "пионер", - "пирог", - "писать", - "пить", - "пицца", - "пишущий", - "пища", - "план", - "плечо", - "плита", - "плохой", - "плыть", - "плюс", - "пляж", - "победа", - "повод", - "погода", - "подумать", - "поехать", - "пожимать", - "позиция", - "поиск", - "покой", - "получать", - "помнить", - "пони", - "поощрять", - "попадать", - "порядок", - "пост", - "поток", - "похожий", - "поцелуй", - "почва", - "пощечина", - "поэт", - "пояснить", - "право", - "предмет", - "проблема", - "пруд", - "прыгать", - "прямой", - "психолог", - "птица", - "публика", - "пугать", - "пудра", - "пузырь", - "пуля", - "пункт", - "пурга", - "пустой", - "путь", - "пухлый", - "пучок", - "пушистый", - "пчела", - "пшеница", - "пыль", - "пытка", - "пыхтеть", - "пышный", - "пьеса", - "пьяный", - "пятно", - "работа", - "равный", - "радость", - "развитие", - "район", - "ракета", - "рамка", - "ранний", - "рапорт", - "рассказ", - "раунд", - "рация", - "рвать", - "реальный", - "ребенок", - "реветь", - "регион", - "редакция", - "реестр", - "режим", - "резкий", - "рейтинг", - "река", - "религия", - "ремонт", - "рента", - "реплика", - "ресурс", - "реформа", - "рецепт", - "речь", - "решение", - "ржавый", - "рисунок", - "ритм", - "рифма", - "робкий", - "ровный", - "рогатый", - "родитель", - "рождение", - "розовый", - "роковой", - "роль", - "роман", - "ронять", - "рост", - "рота", - "роща", - "рояль", - "рубль", - "ругать", - "руда", - "ружье", - "руины", - "рука", - "руль", - "румяный", - "русский", - "ручка", - "рыба", - "рывок", - "рыдать", - "рыжий", - "рынок", - "рысь", - "рыть", - "рыхлый", - "рыцарь", - "рычаг", - "рюкзак", - "рюмка", - "рябой", - "рядовой", - "сабля", - "садовый", - "сажать", - "салон", - "самолет", - "сани", - "сапог", - "сарай", - "сатира", - "сауна", - "сахар", - "сбегать", - "сбивать", - "сбор", - "сбыт", - "свадьба", - "свет", - "свидание", - "свобода", - "связь", - "сгорать", - "сдвигать", - "сеанс", - "северный", - "сегмент", - "седой", - "сезон", - "сейф", - "секунда", - "сельский", - "семья", - "сентябрь", - "сердце", - "сеть", - "сечение", - "сеять", - "сигнал", - "сидеть", - "сизый", - "сила", - "символ", - "синий", - "сирота", - "система", - "ситуация", - "сиять", - "сказать", - "скважина", - "скелет", - "скидка", - "склад", - "скорый", - "скрывать", - "скучный", - "слава", - "слеза", - "слияние", - "слово", - "случай", - "слышать", - "слюна", - "смех", - "смирение", - "смотреть", - "смутный", - "смысл", - "смятение", - "снаряд", - "снег", - "снижение", - "сносить", - "снять", - "событие", - "совет", - "согласие", - "сожалеть", - "сойти", - "сокол", - "солнце", - "сомнение", - "сонный", - "сообщать", - "соперник", - "сорт", - "состав", - "сотня", - "соус", - "социолог", - "сочинять", - "союз", - "спать", - "спешить", - "спина", - "сплошной", - "способ", - "спутник", - "средство", - "срок", - "срывать", - "стать", - "ствол", - "стена", - "стихи", - "сторона", - "страна", - "студент", - "стыд", - "субъект", - "сувенир", - "сугроб", - "судьба", - "суета", - "суждение", - "сукно", - "сулить", - "сумма", - "сунуть", - "супруг", - "суровый", - "сустав", - "суть", - "сухой", - "суша", - "существо", - "сфера", - "схема", - "сцена", - "счастье", - "счет", - "считать", - "сшивать", - "съезд", - "сынок", - "сыпать", - "сырье", - "сытый", - "сыщик", - "сюжет", - "сюрприз", - "таблица", - "таежный", - "таинство", - "тайна", - "такси", - "талант", - "таможня", - "танец", - "тарелка", - "таскать", - "тахта", - "тачка", - "таять", - "тварь", - "твердый", - "творить", - "театр", - "тезис", - "текст", - "тело", - "тема", - "тень", - "теория", - "теплый", - "терять", - "тесный", - "тетя", - "техника", - "течение", - "тигр", - "типичный", - "тираж", - "титул", - "тихий", - "тишина", - "ткань", - "товарищ", - "толпа", - "тонкий", - "топливо", - "торговля", - "тоска", - "точка", - "тощий", - "традиция", - "тревога", - "трибуна", - "трогать", - "труд", - "трюк", - "тряпка", - "туалет", - "тугой", - "туловище", - "туман", - "тундра", - "тупой", - "турнир", - "тусклый", - "туфля", - "туча", - "туша", - "тыкать", - "тысяча", - "тьма", - "тюльпан", - "тюрьма", - "тяга", - "тяжелый", - "тянуть", - "убеждать", - "убирать", - "убогий", - "убыток", - "уважение", - "уверять", - "увлекать", - "угнать", - "угол", - "угроза", - "удар", - "удивлять", - "удобный", - "уезд", - "ужас", - "ужин", - "узел", - "узкий", - "узнавать", - "узор", - "уйма", - "уклон", - "укол", - "уксус", - "улетать", - "улица", - "улучшать", - "улыбка", - "уметь", - "умиление", - "умный", - "умолять", - "умысел", - "унижать", - "уносить", - "уныние", - "упасть", - "уплата", - "упор", - "упрекать", - "упускать", - "уран", - "урна", - "уровень", - "усадьба", - "усердие", - "усилие", - "ускорять", - "условие", - "усмешка", - "уснуть", - "успеть", - "усыпать", - "утешать", - "утка", - "уточнять", - "утро", - "утюг", - "уходить", - "уцелеть", - "участие", - "ученый", - "учитель", - "ушко", - "ущерб", - "уютный", - "уяснять", - "фабрика", - "фаворит", - "фаза", - "файл", - "факт", - "фамилия", - "фантазия", - "фара", - "фасад", - "февраль", - "фельдшер", - "феномен", - "ферма", - "фигура", - "физика", - "фильм", - "финал", - "фирма", - "фишка", - "флаг", - "флейта", - "флот", - "фокус", - "фольклор", - "фонд", - "форма", - "фото", - "фраза", - "фреска", - "фронт", - "фрукт", - "функция", - "фуражка", - "футбол", - "фыркать", - "халат", - "хамство", - "хаос", - "характер", - "хата", - "хватать", - "хвост", - "хижина", - "хилый", - "химия", - "хирург", - "хитрый", - "хищник", - "хлам", - "хлеб", - "хлопать", - "хмурый", - "ходить", - "хозяин", - "хоккей", - "холодный", - "хороший", - "хотеть", - "хохотать", - "храм", - "хрен", - "хриплый", - "хроника", - "хрупкий", - "художник", - "хулиган", - "хутор", - "царь", - "цвет", - "цель", - "цемент", - "центр", - "цепь", - "церковь", - "цикл", - "цилиндр", - "циничный", - "цирк", - "цистерна", - "цитата", - "цифра", - "цыпленок", - "чадо", - "чайник", - "часть", - "чашка", - "человек", - "чемодан", - "чепуха", - "черный", - "честь", - "четкий", - "чехол", - "чиновник", - "число", - "читать", - "членство", - "чреватый", - "чтение", - "чувство", - "чугунный", - "чудо", - "чужой", - "чукча", - "чулок", - "чума", - "чуткий", - "чучело", - "чушь", - "шаблон", - "шагать", - "шайка", - "шакал", - "шалаш", - "шампунь", - "шанс", - "шапка", - "шарик", - "шасси", - "шатер", - "шахта", - "шашлык", - "швейный", - "швырять", - "шевелить", - "шедевр", - "шейка", - "шелковый", - "шептать", - "шерсть", - "шестерка", - "шикарный", - "шинель", - "шипеть", - "широкий", - "шить", - "шишка", - "шкаф", - "школа", - "шкура", - "шланг", - "шлем", - "шлюпка", - "шляпа", - "шнур", - "шоколад", - "шорох", - "шоссе", - "шофер", - "шпага", - "шпион", - "шприц", - "шрам", - "шрифт", - "штаб", - "штора", - "штраф", - "штука", - "штык", - "шуба", - "шуметь", - "шуршать", - "шутка", - "щадить", - "щедрый", - "щека", - "щель", - "щенок", - "щепка", - "щетка", - "щука", - "эволюция", - "эгоизм", - "экзамен", - "экипаж", - "экономия", - "экран", - "эксперт", - "элемент", - "элита", - "эмблема", - "эмигрант", - "эмоция", - "энергия", - "эпизод", - "эпоха", - "эскиз", - "эссе", - "эстрада", - "этап", - "этика", - "этюд", - "эфир", - "эффект", - "эшелон", - "юбилей", - "юбка", - "южный", - "юмор", - "юноша", - "юрист", - "яблоко", - "явление", - "ягода", - "ядерный", - "ядовитый", - "ядро", - "язва", - "язык", - "яйцо", - "якорь", - "январь", - "японец", - "яркий", - "ярмарка", - "ярость", - "ярус", - "ясный", - "яхта", - "ячейка", - "ящик" -] \ No newline at end of file diff --git a/monero-seed/src/words/zh.rs b/monero-seed/src/words/zh.rs deleted file mode 100644 index 42f05b4a22..0000000000 --- a/monero-seed/src/words/zh.rs +++ /dev/null @@ -1,1628 +0,0 @@ -&[ - "的", - "一", - "是", - "在", - "不", - "了", - "有", - "和", - "人", - "这", - "中", - "大", - "为", - "上", - "个", - "国", - "我", - "以", - "要", - "他", - "时", - "来", - "用", - "们", - "生", - "到", - "作", - "地", - "于", - "出", - "就", - "分", - "对", - "成", - "会", - "可", - "主", - "发", - "年", - "动", - "同", - "工", - "也", - "能", - "下", - "过", - "子", - "说", - "产", - "种", - "面", - "而", - "方", - "后", - "多", - "定", - "行", - "学", - "法", - "所", - "民", - "得", - "经", - "十", - "三", - "之", - "进", - "着", - "等", - "部", - "度", - "家", - "电", - "力", - "里", - "如", - "水", - "化", - "高", - "自", - "二", - "理", - "起", - "小", - "物", - "现", - "实", - "加", - "量", - "都", - "两", - "体", - "制", - "机", - "当", - "使", - "点", - "从", - "业", - "本", - "去", - "把", - "性", - "好", - "应", - "开", - "它", - "合", - "还", - "因", - "由", - "其", - "些", - "然", - "前", - "外", - "天", - "政", - "四", - "日", - "那", - "社", - "义", - "事", - "平", - "形", - "相", - "全", - "表", - "间", - "样", - "与", - "关", - "各", - "重", - "新", - "线", - "内", - "数", - "正", - "心", - "反", - "你", - "明", - "看", - "原", - "又", - "么", - "利", - "比", - "或", - "但", - "质", - "气", - "第", - "向", - "道", - "命", - "此", - "变", - "条", - "只", - "没", - "结", - "解", - "问", - "意", - "建", - "月", - "公", - "无", - "系", - "军", - "很", - "情", - "者", - "最", - "立", - "代", - "想", - "已", - "通", - "并", - "提", - "直", - "题", - "党", - "程", - "展", - "五", - "果", - "料", - "象", - "员", - "革", - "位", - "入", - "常", - "文", - "总", - "次", - "品", - "式", - "活", - "设", - "及", - "管", - "特", - "件", - "长", - "求", - "老", - "头", - "基", - "资", - "边", - "流", - "路", - "级", - "少", - "图", - "山", - "统", - "接", - "知", - "较", - "将", - "组", - "见", - "计", - "别", - "她", - "手", - "角", - "期", - "根", - "论", - "运", - "农", - "指", - "几", - "九", - "区", - "强", - "放", - "决", - "西", - "被", - "干", - "做", - "必", - "战", - "先", - "回", - "则", - "任", - "取", - "据", - "处", - "队", - "南", - "给", - "色", - "光", - "门", - "即", - "保", - "治", - "北", - "造", - "百", - "规", - "热", - "领", - "七", - "海", - "口", - "东", - "导", - "器", - "压", - "志", - "世", - "金", - "增", - "争", - "济", - "阶", - "油", - "思", - "术", - "极", - "交", - "受", - "联", - "什", - "认", - "六", - "共", - "权", - "收", - "证", - "改", - "清", - "美", - "再", - "采", - "转", - "更", - "单", - "风", - "切", - "打", - "白", - "教", - "速", - "花", - "带", - "安", - "场", - "身", - "车", - "例", - "真", - "务", - "具", - "万", - "每", - "目", - "至", - "达", - "走", - "积", - "示", - "议", - "声", - "报", - "斗", - "完", - "类", - "八", - "离", - "华", - "名", - "确", - "才", - "科", - "张", - "信", - "马", - "节", - "话", - "米", - "整", - "空", - "元", - "况", - "今", - "集", - "温", - "传", - "土", - "许", - "步", - "群", - "广", - "石", - "记", - "需", - "段", - "研", - "界", - "拉", - "林", - "律", - "叫", - "且", - "究", - "观", - "越", - "织", - "装", - "影", - "算", - "低", - "持", - "音", - "众", - "书", - "布", - "复", - "容", - "儿", - "须", - "际", - "商", - "非", - "验", - "连", - "断", - "深", - "难", - "近", - "矿", - "千", - "周", - "委", - "素", - "技", - "备", - "半", - "办", - "青", - "省", - "列", - "习", - "响", - "约", - "支", - "般", - "史", - "感", - "劳", - "便", - "团", - "往", - "酸", - "历", - "市", - "克", - "何", - "除", - "消", - "构", - "府", - "称", - "太", - "准", - "精", - "值", - "号", - "率", - "族", - "维", - "划", - "选", - "标", - "写", - "存", - "候", - "毛", - "亲", - "快", - "效", - "斯", - "院", - "查", - "江", - "型", - "眼", - "王", - "按", - "格", - "养", - "易", - "置", - "派", - "层", - "片", - "始", - "却", - "专", - "状", - "育", - "厂", - "京", - "识", - "适", - "属", - "圆", - "包", - "火", - "住", - "调", - "满", - "县", - "局", - "照", - "参", - "红", - "细", - "引", - "听", - "该", - "铁", - "价", - "严", - "首", - "底", - "液", - "官", - "德", - "随", - "病", - "苏", - "失", - "尔", - "死", - "讲", - "配", - "女", - "黄", - "推", - "显", - "谈", - "罪", - "神", - "艺", - "呢", - "席", - "含", - "企", - "望", - "密", - "批", - "营", - "项", - "防", - "举", - "球", - "英", - "氧", - "势", - "告", - "李", - "台", - "落", - "木", - "帮", - "轮", - "破", - "亚", - "师", - "围", - "注", - "远", - "字", - "材", - "排", - "供", - "河", - "态", - "封", - "另", - "施", - "减", - "树", - "溶", - "怎", - "止", - "案", - "言", - "士", - "均", - "武", - "固", - "叶", - "鱼", - "波", - "视", - "仅", - "费", - "紧", - "爱", - "左", - "章", - "早", - "朝", - "害", - "续", - "轻", - "服", - "试", - "食", - "充", - "兵", - "源", - "判", - "护", - "司", - "足", - "某", - "练", - "差", - "致", - "板", - "田", - "降", - "黑", - "犯", - "负", - "击", - "范", - "继", - "兴", - "似", - "余", - "坚", - "曲", - "输", - "修", - "故", - "城", - "夫", - "够", - "送", - "笔", - "船", - "占", - "右", - "财", - "吃", - "富", - "春", - "职", - "觉", - "汉", - "画", - "功", - "巴", - "跟", - "虽", - "杂", - "飞", - "检", - "吸", - "助", - "升", - "阳", - "互", - "初", - "创", - "抗", - "考", - "投", - "坏", - "策", - "古", - "径", - "换", - "未", - "跑", - "留", - "钢", - "曾", - "端", - "责", - "站", - "简", - "述", - "钱", - "副", - "尽", - "帝", - "射", - "草", - "冲", - "承", - "独", - "令", - "限", - "阿", - "宣", - "环", - "双", - "请", - "超", - "微", - "让", - "控", - "州", - "良", - "轴", - "找", - "否", - "纪", - "益", - "依", - "优", - "顶", - "础", - "载", - "倒", - "房", - "突", - "坐", - "粉", - "敌", - "略", - "客", - "袁", - "冷", - "胜", - "绝", - "析", - "块", - "剂", - "测", - "丝", - "协", - "诉", - "念", - "陈", - "仍", - "罗", - "盐", - "友", - "洋", - "错", - "苦", - "夜", - "刑", - "移", - "频", - "逐", - "靠", - "混", - "母", - "短", - "皮", - "终", - "聚", - "汽", - "村", - "云", - "哪", - "既", - "距", - "卫", - "停", - "烈", - "央", - "察", - "烧", - "迅", - "境", - "若", - "印", - "洲", - "刻", - "括", - "激", - "孔", - "搞", - "甚", - "室", - "待", - "核", - "校", - "散", - "侵", - "吧", - "甲", - "游", - "久", - "菜", - "味", - "旧", - "模", - "湖", - "货", - "损", - "预", - "阻", - "毫", - "普", - "稳", - "乙", - "妈", - "植", - "息", - "扩", - "银", - "语", - "挥", - "酒", - "守", - "拿", - "序", - "纸", - "医", - "缺", - "雨", - "吗", - "针", - "刘", - "啊", - "急", - "唱", - "误", - "训", - "愿", - "审", - "附", - "获", - "茶", - "鲜", - "粮", - "斤", - "孩", - "脱", - "硫", - "肥", - "善", - "龙", - "演", - "父", - "渐", - "血", - "欢", - "械", - "掌", - "歌", - "沙", - "刚", - "攻", - "谓", - "盾", - "讨", - "晚", - "粒", - "乱", - "燃", - "矛", - "乎", - "杀", - "药", - "宁", - "鲁", - "贵", - "钟", - "煤", - "读", - "班", - "伯", - "香", - "介", - "迫", - "句", - "丰", - "培", - "握", - "兰", - "担", - "弦", - "蛋", - "沉", - "假", - "穿", - "执", - "答", - "乐", - "谁", - "顺", - "烟", - "缩", - "征", - "脸", - "喜", - "松", - "脚", - "困", - "异", - "免", - "背", - "星", - "福", - "买", - "染", - "井", - "概", - "慢", - "怕", - "磁", - "倍", - "祖", - "皇", - "促", - "静", - "补", - "评", - "翻", - "肉", - "践", - "尼", - "衣", - "宽", - "扬", - "棉", - "希", - "伤", - "操", - "垂", - "秋", - "宜", - "氢", - "套", - "督", - "振", - "架", - "亮", - "末", - "宪", - "庆", - "编", - "牛", - "触", - "映", - "雷", - "销", - "诗", - "座", - "居", - "抓", - "裂", - "胞", - "呼", - "娘", - "景", - "威", - "绿", - "晶", - "厚", - "盟", - "衡", - "鸡", - "孙", - "延", - "危", - "胶", - "屋", - "乡", - "临", - "陆", - "顾", - "掉", - "呀", - "灯", - "岁", - "措", - "束", - "耐", - "剧", - "玉", - "赵", - "跳", - "哥", - "季", - "课", - "凯", - "胡", - "额", - "款", - "绍", - "卷", - "齐", - "伟", - "蒸", - "殖", - "永", - "宗", - "苗", - "川", - "炉", - "岩", - "弱", - "零", - "杨", - "奏", - "沿", - "露", - "杆", - "探", - "滑", - "镇", - "饭", - "浓", - "航", - "怀", - "赶", - "库", - "夺", - "伊", - "灵", - "税", - "途", - "灭", - "赛", - "归", - "召", - "鼓", - "播", - "盘", - "裁", - "险", - "康", - "唯", - "录", - "菌", - "纯", - "借", - "糖", - "盖", - "横", - "符", - "私", - "努", - "堂", - "域", - "枪", - "润", - "幅", - "哈", - "竟", - "熟", - "虫", - "泽", - "脑", - "壤", - "碳", - "欧", - "遍", - "侧", - "寨", - "敢", - "彻", - "虑", - "斜", - "薄", - "庭", - "纳", - "弹", - "饲", - "伸", - "折", - "麦", - "湿", - "暗", - "荷", - "瓦", - "塞", - "床", - "筑", - "恶", - "户", - "访", - "塔", - "奇", - "透", - "梁", - "刀", - "旋", - "迹", - "卡", - "氯", - "遇", - "份", - "毒", - "泥", - "退", - "洗", - "摆", - "灰", - "彩", - "卖", - "耗", - "夏", - "择", - "忙", - "铜", - "献", - "硬", - "予", - "繁", - "圈", - "雪", - "函", - "亦", - "抽", - "篇", - "阵", - "阴", - "丁", - "尺", - "追", - "堆", - "雄", - "迎", - "泛", - "爸", - "楼", - "避", - "谋", - "吨", - "野", - "猪", - "旗", - "累", - "偏", - "典", - "馆", - "索", - "秦", - "脂", - "潮", - "爷", - "豆", - "忽", - "托", - "惊", - "塑", - "遗", - "愈", - "朱", - "替", - "纤", - "粗", - "倾", - "尚", - "痛", - "楚", - "谢", - "奋", - "购", - "磨", - "君", - "池", - "旁", - "碎", - "骨", - "监", - "捕", - "弟", - "暴", - "割", - "贯", - "殊", - "释", - "词", - "亡", - "壁", - "顿", - "宝", - "午", - "尘", - "闻", - "揭", - "炮", - "残", - "冬", - "桥", - "妇", - "警", - "综", - "招", - "吴", - "付", - "浮", - "遭", - "徐", - "您", - "摇", - "谷", - "赞", - "箱", - "隔", - "订", - "男", - "吹", - "园", - "纷", - "唐", - "败", - "宋", - "玻", - "巨", - "耕", - "坦", - "荣", - "闭", - "湾", - "键", - "凡", - "驻", - "锅", - "救", - "恩", - "剥", - "凝", - "碱", - "齿", - "截", - "炼", - "麻", - "纺", - "禁", - "废", - "盛", - "版", - "缓", - "净", - "睛", - "昌", - "婚", - "涉", - "筒", - "嘴", - "插", - "岸", - "朗", - "庄", - "街", - "藏", - "姑", - "贸", - "腐", - "奴", - "啦", - "惯", - "乘", - "伙", - "恢", - "匀", - "纱", - "扎", - "辩", - "耳", - "彪", - "臣", - "亿", - "璃", - "抵", - "脉", - "秀", - "萨", - "俄", - "网", - "舞", - "店", - "喷", - "纵", - "寸", - "汗", - "挂", - "洪", - "贺", - "闪", - "柬", - "爆", - "烯", - "津", - "稻", - "墙", - "软", - "勇", - "像", - "滚", - "厘", - "蒙", - "芳", - "肯", - "坡", - "柱", - "荡", - "腿", - "仪", - "旅", - "尾", - "轧", - "冰", - "贡", - "登", - "黎", - "削", - "钻", - "勒", - "逃", - "障", - "氨", - "郭", - "峰", - "币", - "港", - "伏", - "轨", - "亩", - "毕", - "擦", - "莫", - "刺", - "浪", - "秘", - "援", - "株", - "健", - "售", - "股", - "岛", - "甘", - "泡", - "睡", - "童", - "铸", - "汤", - "阀", - "休", - "汇", - "舍", - "牧", - "绕", - "炸", - "哲", - "磷", - "绩", - "朋", - "淡", - "尖", - "启", - "陷", - "柴", - "呈", - "徒", - "颜", - "泪", - "稍", - "忘", - "泵", - "蓝", - "拖", - "洞", - "授", - "镜", - "辛", - "壮", - "锋", - "贫", - "虚", - "弯", - "摩", - "泰", - "幼", - "廷", - "尊", - "窗", - "纲", - "弄", - "隶", - "疑", - "氏", - "宫", - "姐", - "震", - "瑞", - "怪", - "尤", - "琴", - "循", - "描", - "膜", - "违", - "夹", - "腰", - "缘", - "珠", - "穷", - "森", - "枝", - "竹", - "沟", - "催", - "绳", - "忆", - "邦", - "剩", - "幸", - "浆", - "栏", - "拥", - "牙", - "贮", - "礼", - "滤", - "钠", - "纹", - "罢", - "拍", - "咱", - "喊", - "袖", - "埃", - "勤", - "罚", - "焦", - "潜", - "伍", - "墨", - "欲", - "缝", - "姓", - "刊", - "饱", - "仿", - "奖", - "铝", - "鬼", - "丽", - "跨", - "默", - "挖", - "链", - "扫", - "喝", - "袋", - "炭", - "污", - "幕", - "诸", - "弧", - "励", - "梅", - "奶", - "洁", - "灾", - "舟", - "鉴", - "苯", - "讼", - "抱", - "毁", - "懂", - "寒", - "智", - "埔", - "寄", - "届", - "跃", - "渡", - "挑", - "丹", - "艰", - "贝", - "碰", - "拔", - "爹", - "戴", - "码", - "梦", - "芽", - "熔", - "赤", - "渔", - "哭", - "敬", - "颗", - "奔", - "铅", - "仲", - "虎", - "稀", - "妹", - "乏", - "珍", - "申", - "桌", - "遵", - "允", - "隆", - "螺", - "仓", - "魏", - "锐", - "晓", - "氮", - "兼", - "隐", - "碍", - "赫", - "拨", - "忠", - "肃", - "缸", - "牵", - "抢", - "博", - "巧", - "壳", - "兄", - "杜", - "讯", - "诚", - "碧", - "祥", - "柯", - "页", - "巡", - "矩", - "悲", - "灌", - "龄", - "伦", - "票", - "寻", - "桂", - "铺", - "圣", - "恐", - "恰", - "郑", - "趣", - "抬", - "荒", - "腾", - "贴", - "柔", - "滴", - "猛", - "阔", - "辆", - "妻", - "填", - "撤", - "储", - "签", - "闹", - "扰", - "紫", - "砂", - "递", - "戏", - "吊", - "陶", - "伐", - "喂", - "疗", - "瓶", - "婆", - "抚", - "臂", - "摸", - "忍", - "虾", - "蜡", - "邻", - "胸", - "巩", - "挤", - "偶", - "弃", - "槽", - "劲", - "乳", - "邓", - "吉", - "仁", - "烂", - "砖", - "租", - "乌", - "舰", - "伴", - "瓜", - "浅", - "丙", - "暂", - "燥", - "橡", - "柳", - "迷", - "暖", - "牌", - "秧", - "胆", - "详", - "簧", - "踏", - "瓷", - "谱", - "呆", - "宾", - "糊", - "洛", - "辉", - "愤", - "竞", - "隙", - "怒", - "粘", - "乃", - "绪", - "肩", - "籍", - "敏", - "涂", - "熙", - "皆", - "侦", - "悬", - "掘", - "享", - "纠", - "醒", - "狂", - "锁", - "淀", - "恨", - "牲", - "霸", - "爬", - "赏", - "逆", - "玩", - "陵", - "祝", - "秒", - "浙", - "貌" -] \ No newline at end of file diff --git a/monero-sys/build.rs b/monero-sys/build.rs index 7fdf8379df..78d6a64955 100644 --- a/monero-sys/build.rs +++ b/monero-sys/build.rs @@ -71,12 +71,30 @@ const EMBEDDED_PATCHES: &[EmbeddedPatch] = &[ "patches/eigenwallet_0002_wallet2_increase_rpc_retries.patch" ), embedded_patch!( - "eigenwallet_0003_pendingTransaction_getTxKey", + "eigenwallet_0003_pending_transaction_tx_keys", "Adds txKeys() to PendingTransaction in wallet2_api.h", - "patches/eigenwallet_0003_pendingTransaction_getTxKey.patch" + "patches/eigenwallet_0003_pending_transaction_tx_keys.patch" ), ]; +/// Find the workspace target directory from OUT_DIR +/// +/// OUT_DIR is something like: /path/to/workspace/target/debug/build/monero-sys-abc123/out +/// We want to extract: /path/to/workspace/target +fn find_workspace_target_dir() -> std::path::PathBuf { + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR to be set"); + let out_path = Path::new(&out_dir); + + // Walk up from OUT_DIR to find "target" directory + for ancestor in out_path.ancestors() { + if ancestor.ends_with("target") { + return ancestor.to_path_buf(); + } + } + + panic!("Could not find target directory from OUT_DIR: {}", out_dir); +} + fn main() { let is_github_actions: bool = std::env::var("GITHUB_ACTIONS").is_ok(); let is_docker_build: bool = std::env::var("DOCKER_BUILD").is_ok(); @@ -102,10 +120,12 @@ fn main() { .expect("current directory to be accessible") .join(MONERO_DEPENDS_DIR); - let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR to be set"); - let out_dir = Path::new(&out_dir); + // Use stable location in target/debug/monero-depends to avoid rebuilding deps unnecessarily + let target_dir = find_workspace_target_dir(); + let stable_depends_dir = target_dir.join("debug").join("monero-depends").join(&std::env::var("TARGET").expect("TARGET env var to be present")); + let (contrib_depends_dir, target) = - compile_dependencies(contrib_depends_dir, out_dir.join("depends")); + compile_dependencies(contrib_depends_dir, stable_depends_dir); // Build with the monero library all dependencies required let mut config = Config::new(MONERO_CPP_DIR); diff --git a/monero-sys/patches/eigenwallet_0003_pendingTransaction_getTxKey.patch b/monero-sys/patches/eigenwallet_0003_pendingTransaction_getTxKey.patch deleted file mode 100644 index 4cbc2e5749..0000000000 --- a/monero-sys/patches/eigenwallet_0003_pendingTransaction_getTxKey.patch +++ /dev/null @@ -1,64 +0,0 @@ -diff --git a/src/wallet/api/pending_transaction.cpp b/src/wallet/api/pending_transaction.cpp -index 2dd118ea3..fd7cbca9b 100644 ---- a/src/wallet/api/pending_transaction.cpp -+++ b/src/wallet/api/pending_transaction.cpp -@@ -78,6 +78,35 @@ std::vector PendingTransactionImpl::txid() const - return txid; - } - -+// This function returns **all** tx keys for the transaction with the given tx hash in the [`PendingTransaction`]. -+// A [`PendingTransaction`] can contain multiple transactions. A single transaction can have multiple tx keys. -+std::vector PendingTransactionImpl::txKeys(const std::string &tx_hash) const -+{ -+ std::vector keys; -+ -+ for (const auto &ptx : m_pending_tx) -+ { -+ std::string current_tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx)); -+ -+ if (current_tx_hash == tx_hash) -+ { -+ std::string key = epee::string_tools::pod_to_hex(unwrap(unwrap(ptx.tx_key))); -+ keys.emplace_back(std::move(key)); -+ -+ // TODO: According to moneromoo, its non standard behavior to have multiple tx keys -+ // We still provide return them here for the sake of correctness. -+ // Our Rust wrapper will then fail if it detects multiple tx keys. -+ for (const auto &additional_key : ptx.additional_tx_keys) { -+ keys.emplace_back(epee::string_tools::pod_to_hex(unwrap(unwrap(additional_key)))); -+ } -+ -+ break; -+ } -+ } -+ -+ return keys; -+} -+ - bool PendingTransactionImpl::commit(const std::string &filename, bool overwrite) - { - -diff --git a/src/wallet/api/pending_transaction.h b/src/wallet/api/pending_transaction.h -index c5f4328a8..f2ee483c9 100644 ---- a/src/wallet/api/pending_transaction.h -+++ b/src/wallet/api/pending_transaction.h -@@ -50,6 +50,7 @@ public: - uint64_t dust() const override; - uint64_t fee() const override; - std::vector txid() const override; -+ std::vector txKeys(const std::string &tx_hash) const override; - uint64_t txCount() const override; - std::vector subaddrAccount() const override; - std::vector> subaddrIndices() const override; -diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h -index ca807ac87..53df045c9 100644 ---- a/src/wallet/api/wallet2_api.h -+++ b/src/wallet/api/wallet2_api.h -@@ -131,6 +131,7 @@ struct PendingTransaction - virtual uint64_t dust() const = 0; - virtual uint64_t fee() const = 0; - virtual std::vector txid() const = 0; -+ virtual std::vector txKeys(const std::string &tx_hash) const = 0; - /*! - * \brief txCount - number of transactions current transaction will be splitted to - * \return diff --git a/monero-sys/patches/eigenwallet_0003_pending_transaction_tx_keys.patch b/monero-sys/patches/eigenwallet_0003_pending_transaction_tx_keys.patch new file mode 100644 index 0000000000..f6c50d91ca --- /dev/null +++ b/monero-sys/patches/eigenwallet_0003_pending_transaction_tx_keys.patch @@ -0,0 +1,140 @@ +diff --git a/src/wallet/api/pending_transaction.cpp b/src/wallet/api/pending_transaction.cpp +index 2dd118ea3..0972089d1 100644 +--- a/src/wallet/api/pending_transaction.cpp ++++ b/src/wallet/api/pending_transaction.cpp +@@ -78,6 +78,103 @@ std::vector PendingTransactionImpl::txid() const + return txid; + } + ++// This function returns **all** tx keys for the transaction with the given tx hash in the [`PendingTransaction`]. ++// A [`PendingTransaction`] can contain multiple transactions. A single transaction can have multiple tx keys. ++std::vector> PendingTransactionImpl::txKeys(const std::string &tx_hash) const ++{ ++ std::vector> keys; ++ ++ for (const auto &ptx : m_pending_tx) ++ { ++ const std::string current_tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx)); ++ ++ if (current_tx_hash != tx_hash) ++ { ++ continue; ++ } ++ ++ const std::string main_tx_key = epee::string_tools::pod_to_hex(unwrap(unwrap(ptx.tx_key))); ++ ++ if (ptx.tx.vout.size() != ptx.construction_data.splitted_dsts.size()) { ++ throw std::runtime_error( ++ "Number of outputs in transaction " ++ + current_tx_hash ++ + " (" + std::to_string(ptx.tx.vout.size()) + ")" ++ + " does not match number of destinations (" ++ + std::to_string(ptx.construction_data.splitted_dsts.size()) + ")"); ++ } ++ ++ // Prepare change-derivation data for change detection ++ const crypto::public_key tx_pub_key = cryptonote::get_tx_pub_key_from_extra(ptx.tx); ++ const crypto::secret_key &view_secret_key = m_wallet.m_wallet->get_account().get_keys().m_view_secret_key; ++ crypto::key_derivation change_derivation; ++ if (!crypto::generate_key_derivation(tx_pub_key, view_secret_key, change_derivation)) { ++ throw std::runtime_error("Failed to generate change derivation"); ++ } ++ const crypto::public_key change_spend_pub = ptx.construction_data.change_dts.addr.m_spend_public_key; ++ ++ for (size_t i = 0; i < ptx.tx.vout.size(); i++) { ++ const cryptonote::tx_out tx_output = ptx.tx.vout[i]; ++ const cryptonote::tx_destination_entry dest_entry = ptx.construction_data.splitted_dsts[i]; ++ ++ crypto::public_key out_pk; ++ if (!cryptonote::get_output_public_key(ptx.tx.vout[i], out_pk)) { ++ throw std::runtime_error("Unable to get output public key from transaction out"); ++ } ++ ++ // Detect change outputs by checking if the output public key matches our private view key. ++ crypto::public_key expected_change_pk; ++ if (!crypto::derive_public_key(change_derivation, i, change_spend_pub, expected_change_pk)) { ++ throw std::runtime_error("Failed to derive change public key"); ++ } ++ // If it's a change output, skip it. Otherwise look for the tx key. ++ if (expected_change_pk == out_pk) { ++ continue; ++ } ++ ++ std::vector keys_to_try = ptx.additional_tx_keys; ++ keys_to_try.push_back(ptx.tx_key); ++ ++ bool found_key = false; ++ crypto::secret_key matched_key = crypto::null_skey; ++ ++ for (const auto &candidate_key: keys_to_try) { ++ // compute derivation from recipient view pubkey and our tx secret key ++ crypto::key_derivation derivation; ++ if (!crypto::generate_key_derivation(dest_entry.addr.m_view_public_key, candidate_key, derivation)) { ++ throw std::runtime_error("Failed to generate key derivation for output " + std::to_string(i)); ++ } ++ ++ // derive expected output public key and compare. We check if the K^0 we derive matches the expected one. ++ crypto::public_key expected_out_pk; ++ if (!crypto::derive_public_key(derivation, i, dest_entry.addr.m_spend_public_key, expected_out_pk)) { ++ throw std::runtime_error("Failed to derive public key for output " + std::to_string(i)); ++ } ++ ++ if (expected_out_pk == out_pk) { ++ found_key = true; ++ matched_key = candidate_key; ++ break; ++ } ++ } ++ ++ if (!found_key || matched_key == crypto::null_skey) { ++ throw std::runtime_error("No matching tx key found for output"); ++ } ++ ++ // push (txid, destination address (human-readable), hex(tx_secret_key)) ++ const std::string dest_addr_str = dest_entry.address(m_wallet.m_wallet->nettype(), crypto::null_hash); ++ keys.emplace_back( ++ current_tx_hash, ++ dest_addr_str, ++ epee::string_tools::pod_to_hex(unwrap(unwrap(matched_key))) ++ ); ++ } ++ } ++ ++ return keys; ++} ++ + bool PendingTransactionImpl::commit(const std::string &filename, bool overwrite) + { + +diff --git a/src/wallet/api/pending_transaction.h b/src/wallet/api/pending_transaction.h +index c5f4328a8..0377eb79c 100644 +--- a/src/wallet/api/pending_transaction.h ++++ b/src/wallet/api/pending_transaction.h +@@ -33,6 +33,7 @@ + + #include + #include ++#include + + + namespace Monero { +@@ -50,6 +51,7 @@ public: + uint64_t dust() const override; + uint64_t fee() const override; + std::vector txid() const override; ++ std::vector> txKeys(const std::string &tx_hash) const override; + uint64_t txCount() const override; + std::vector subaddrAccount() const override; + std::vector> subaddrIndices() const override; +diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h +index ca807ac87..29a89f273 100644 +--- a/src/wallet/api/wallet2_api.h ++++ b/src/wallet/api/wallet2_api.h +@@ -131,6 +131,7 @@ struct PendingTransaction + virtual uint64_t dust() const = 0; + virtual uint64_t fee() const = 0; + virtual std::vector txid() const = 0; ++ virtual std::vector> txKeys(const std::string &tx_hash) const = 0; + /*! + * \brief txCount - number of transactions current transaction will be splitted to + * \return diff --git a/monero-sys/src/bridge.h b/monero-sys/src/bridge.h index 5e47a1a4cc..3aaf4f4738 100644 --- a/monero-sys/src/bridge.h +++ b/monero-sys/src/bridge.h @@ -5,6 +5,7 @@ #include "../monero/src/wallet/api/wallet2_api.h" #include "../monero/src/wallet/api/wallet_manager.h" + /** * This file contains some C++ glue code needed to make the FFI work. * This consists mainly of two use-cases: @@ -60,7 +61,7 @@ namespace Monero auto addr = wallet.address(account_index, address_index); return std::make_unique(addr); } - + inline void rescanBlockchainAsync(Wallet &wallet) { wallet.rescanBlockchainAsync(); @@ -163,8 +164,7 @@ namespace Monero const std::vector &amounts, // If set to true, the fee will be subtracted from output with the highest amount // If set to false, the fee will be paid by the wallet and the exact amounts will be sent to the destinations - bool subtract_fee_from_outputs - ) + bool subtract_fee_from_outputs) { size_t n = dest_addresses.size(); @@ -185,27 +185,28 @@ namespace Monero // Build the actual multi‐dest transaction // No change left -> wallet drops it // N outputs, fee should be the same as the one estimated above - + // Find the highest output and choose it for subtract_fee_indices std::set subtract_fee_indices; // If subtract_fee_from_outputs = false, this will not be executed and // subtract_fee_indices will remain empty which symbolizes that the fee will be paid by the wallet // and the exact amounts will be sent to the destinations - if (subtract_fee_from_outputs) { + if (subtract_fee_from_outputs) + { auto max_it = std::max_element(amounts.begin(), amounts.end()); size_t max_index = std::distance(amounts.begin(), max_it); - subtract_fee_indices.insert(static_cast(max_index)); + subtract_fee_indices.insert(static_cast(max_index)); } - + return wallet.createTransactionMultDest( dest_addresses, "", // No Payment ID Monero::optional>(amounts), 0, // No mixin count PendingTransaction::Priority_Default, - 0, // subaddr_account - {}, // subaddr_indices + 0, // subaddr_account + {}, // subaddr_indices subtract_fee_indices); // Subtract fee from all outputs } @@ -215,15 +216,6 @@ namespace Monero return wallet.setDaemon(daemon_address, ssl); } - /** - * Get the transaction key for a given transaction id - */ - inline std::unique_ptr walletGetTxKey(const Wallet &wallet, const std::string &txid) - { - auto key = wallet.getTxKey(txid); - return std::make_unique(key); - } - /** * Sign a message with the wallet's private key */ @@ -244,7 +236,7 @@ namespace Monero /** * Get the transaction ids of a pending transaction. - * + * * A pending transaction can contain multiple transactions, so we return a vector of txids. */ inline std::unique_ptr> pendingTransactionTxIds(const PendingTransaction &tx) @@ -290,15 +282,9 @@ namespace Monero return static_cast(tx_info.timestamp()); } - inline std::unique_ptr> pendingTransactionTxKeys(const PendingTransaction &tx, const std::string &tx_hash) - { - auto keys = tx.txKeys(tx_hash); - auto vec = std::make_unique>(); - vec->reserve(keys.size()); - for (auto &key : keys) - vec->push_back(std::move(key)); - return vec; - } + + + // bridge.h #pragma once @@ -306,138 +292,156 @@ namespace Monero #include #include "wallet/api/wallet2_api.h" + using CB_StringU64 = uintptr_t; + using CB_U64 = uintptr_t; + using CB_Void = uintptr_t; + using CB_Reorg = uintptr_t; + using CB_String = uintptr_t; + using CB_GetPassword = uintptr_t; -using CB_StringU64 = uintptr_t; -using CB_U64 = uintptr_t; -using CB_Void = uintptr_t; -using CB_Reorg = uintptr_t; -using CB_String = uintptr_t; -using CB_GetPassword = uintptr_t; - -class FunctionBasedListener final : public Monero::WalletListener { -public: - FunctionBasedListener( - CB_StringU64 on_spent, - CB_StringU64 on_received, - CB_StringU64 on_unconfirmed_received, - CB_U64 on_new_block, - CB_Void on_updated, - CB_Void on_refreshed, - CB_Reorg on_reorg, - CB_String on_pool_tx_removed, - CB_GetPassword on_get_password) - : - on_spent_(on_spent), - on_received_(on_received), - on_unconfirmed_received_(on_unconfirmed_received), - on_new_block_(on_new_block), - on_updated_(on_updated), - on_refreshed_(on_refreshed), - on_reorg_(on_reorg), - on_pool_tx_removed_(on_pool_tx_removed), - on_get_password_(on_get_password) {} - - void moneySpent(const std::string& txid, uint64_t amt) override { - if (on_spent_) { - auto* spent = reinterpret_cast(on_spent_); - spent(txid, amt); + class FunctionBasedListener final : public Monero::WalletListener + { + public: + FunctionBasedListener( + CB_StringU64 on_spent, + CB_StringU64 on_received, + CB_StringU64 on_unconfirmed_received, + CB_U64 on_new_block, + CB_Void on_updated, + CB_Void on_refreshed, + CB_Reorg on_reorg, + CB_String on_pool_tx_removed, + CB_GetPassword on_get_password) + : on_spent_(on_spent), + on_received_(on_received), + on_unconfirmed_received_(on_unconfirmed_received), + on_new_block_(on_new_block), + on_updated_(on_updated), + on_refreshed_(on_refreshed), + on_reorg_(on_reorg), + on_pool_tx_removed_(on_pool_tx_removed), + on_get_password_(on_get_password) {} + + void moneySpent(const std::string &txid, uint64_t amt) override + { + if (on_spent_) + { + auto *spent = reinterpret_cast(on_spent_); + spent(txid, amt); + } } - } - void moneyReceived(const std::string& txid, uint64_t amt) override - { if (on_received_) { - auto* received = reinterpret_cast(on_received_); - received(txid, amt); + void moneyReceived(const std::string &txid, uint64_t amt) override + { + if (on_received_) + { + auto *received = reinterpret_cast(on_received_); + received(txid, amt); + } } - } - void unconfirmedMoneyReceived(const std::string& txid, uint64_t amt) override - { if (on_unconfirmed_received_) { - auto* unconfirmed_received = reinterpret_cast(on_unconfirmed_received_); - unconfirmed_received(txid, amt); + void unconfirmedMoneyReceived(const std::string &txid, uint64_t amt) override + { + if (on_unconfirmed_received_) + { + auto *unconfirmed_received = reinterpret_cast(on_unconfirmed_received_); + unconfirmed_received(txid, amt); + } } - } - void newBlock(uint64_t h) override - { if (on_new_block_) { - auto* new_block = reinterpret_cast(on_new_block_); - new_block(h); + void newBlock(uint64_t h) override + { + if (on_new_block_) + { + auto *new_block = reinterpret_cast(on_new_block_); + new_block(h); + } } - } - void updated() override + void updated() override { - if (on_updated_) { - auto* updated = reinterpret_cast(on_updated_); - updated(); + if (on_updated_) + { + auto *updated = reinterpret_cast(on_updated_); + updated(); + } } - } - void refreshed() override - { if (on_refreshed_) { - auto* refreshed = reinterpret_cast(on_refreshed_); - refreshed(); + void refreshed() override + { + if (on_refreshed_) + { + auto *refreshed = reinterpret_cast(on_refreshed_); + refreshed(); + } } - } - void onReorg(uint64_t h, uint64_t d, size_t t) override - { if (on_reorg_) { - auto* reorg = reinterpret_cast(on_reorg_); - reorg(h, d, t); + void onReorg(uint64_t h, uint64_t d, size_t t) override + { + if (on_reorg_) + { + auto *reorg = reinterpret_cast(on_reorg_); + reorg(h, d, t); + } } - } - void onPoolTxRemoved(const std::string& txid) override - { if (on_pool_tx_removed_) { - auto* pool_tx_removed = reinterpret_cast(on_pool_tx_removed_); - pool_tx_removed(txid); + void onPoolTxRemoved(const std::string &txid) override + { + if (on_pool_tx_removed_) + { + auto *pool_tx_removed = reinterpret_cast(on_pool_tx_removed_); + pool_tx_removed(txid); + } } - } - optional onGetPassword(const char* reason) override { - if (on_get_password_) { - auto* get_password = reinterpret_cast(on_get_password_); - return std::string(get_password(reason)); + optional onGetPassword(const char *reason) override + { + if (on_get_password_) + { + auto *get_password = reinterpret_cast(on_get_password_); + return std::string(get_password(reason)); + } + return optional(); } - return optional(); - } -private: - CB_StringU64 on_spent_; - CB_StringU64 on_received_; - CB_StringU64 on_unconfirmed_received_; - CB_U64 on_new_block_; - CB_Void on_updated_; - CB_Void on_refreshed_; - CB_Reorg on_reorg_; - CB_String on_pool_tx_removed_; - CB_GetPassword on_get_password_; -}; - -extern "C" { - WalletListener* create_listener( - CB_StringU64 on_spent, - CB_StringU64 on_received, - CB_StringU64 on_unconfirmed_received, - CB_U64 on_new_block, - CB_Void on_updated, - CB_Void on_refreshed, - CB_Reorg on_reorg, - CB_String on_pool_tx_removed, - CB_GetPassword on_get_password) - { - return new FunctionBasedListener( - on_spent,on_received,on_unconfirmed_received,on_new_block, - on_updated,on_refreshed,on_reorg,on_pool_tx_removed,on_get_password); - } + private: + CB_StringU64 on_spent_; + CB_StringU64 on_received_; + CB_StringU64 on_unconfirmed_received_; + CB_U64 on_new_block_; + CB_Void on_updated_; + CB_Void on_refreshed_; + CB_Reorg on_reorg_; + CB_String on_pool_tx_removed_; + CB_GetPassword on_get_password_; + }; - void destroy_listener(FunctionBasedListener* p) { delete p; } -} + extern "C" + { + WalletListener *create_listener( + CB_StringU64 on_spent, + CB_StringU64 on_received, + CB_StringU64 on_unconfirmed_received, + CB_U64 on_new_block, + CB_Void on_updated, + CB_Void on_refreshed, + CB_Reorg on_reorg, + CB_String on_pool_tx_removed, + CB_GetPassword on_get_password) + { + return new FunctionBasedListener( + on_spent, on_received, on_unconfirmed_received, on_new_block, + on_updated, on_refreshed, on_reorg, on_pool_tx_removed, on_get_password); + } + + void destroy_listener(FunctionBasedListener *p) { delete p; } + } } #include "easylogging++.h" #include "bridge.h" #include "monero-sys/src/bridge.rs.h" + /** * This section is us capturing the log messages from easylogging++ @@ -559,63 +563,94 @@ using StringVec = std::vector; static std::pair _monero_sys_pair_instantiation; -namespace Monero { - -// Adapter class that forwards Monero::WalletListener callbacks to Rust -class RustListenerAdapter final : public Monero::WalletListener { -public: - explicit RustListenerAdapter(rust::Box listener) - : inner_(std::move(listener)) {} +namespace Monero +{ - // --- Required overrides ------------------------------------------------ - void moneySpent(const std::string &txid, uint64_t amount) override { - wallet_listener::money_spent(*inner_, txid, amount); + inline std::unique_ptr> pendingTransactionTxKeys(const PendingTransaction &tx, const std::string &tx_hash) + { + const std::vector> tuple_keys = tx.txKeys(tx_hash); + std::unique_ptr> result = std::make_unique>(); + result->reserve(tuple_keys.size()); + + for (const auto& [tx, addr, key] : tuple_keys) { + result->emplace_back(TxKey{ + std::make_unique(tx), + std::make_unique(addr), + std::make_unique(key) + }); + } + + return result; } - void moneyReceived(const std::string &txid, uint64_t amount) override { - wallet_listener::money_received(*inner_, txid, amount); - } + // Adapter class that forwards Monero::WalletListener callbacks to Rust + class RustListenerAdapter final : public Monero::WalletListener + { + public: + explicit RustListenerAdapter(rust::Box listener) + : inner_(std::move(listener)) {} - void unconfirmedMoneyReceived(const std::string &txid, uint64_t amount) override { - wallet_listener::unconfirmed_money_received(*inner_, txid, amount); - } + // --- Required overrides ------------------------------------------------ + void moneySpent(const std::string &txid, uint64_t amount) override + { + wallet_listener::money_spent(*inner_, txid, amount); + } - void newBlock(uint64_t height) override { - wallet_listener::new_block(*inner_, height); - } + void moneyReceived(const std::string &txid, uint64_t amount) override + { + wallet_listener::money_received(*inner_, txid, amount); + } - void updated() override { - wallet_listener::updated(*inner_); - } + void unconfirmedMoneyReceived(const std::string &txid, uint64_t amount) override + { + wallet_listener::unconfirmed_money_received(*inner_, txid, amount); + } - void refreshed() override { - wallet_listener::refreshed(*inner_); - } + void newBlock(uint64_t height) override + { + wallet_listener::new_block(*inner_, height); + } - void onReorg(std::uint64_t height, std::uint64_t blocks_detached, std::size_t transfers_detached) override { - wallet_listener::on_reorg(*inner_, height, blocks_detached, transfers_detached); - } + void updated() override + { + wallet_listener::updated(*inner_); + } - optional onGetPassword(const char * /*reason*/) override { - return optional(); // Not implemented - } + void refreshed() override + { + wallet_listener::refreshed(*inner_); + } - void onPoolTxRemoved(const std::string &txid) override { - wallet_listener::pool_tx_removed(*inner_, txid); - } + void onReorg(std::uint64_t height, std::uint64_t blocks_detached, std::size_t transfers_detached) override + { + wallet_listener::on_reorg(*inner_, height, blocks_detached, transfers_detached); + } + + optional onGetPassword(const char * /*reason*/) override + { + return optional(); // Not implemented + } + + void onPoolTxRemoved(const std::string &txid) override + { + wallet_listener::pool_tx_removed(*inner_, txid); + } -private: - rust::Box inner_; -}; + private: + rust::Box inner_; + }; } // namespace Monero -namespace wallet_listener { - Monero::WalletListener* create_rust_listener_adapter(rust::Box listener) { +namespace wallet_listener +{ + Monero::WalletListener *create_rust_listener_adapter(rust::Box listener) + { return new Monero::RustListenerAdapter(std::move(listener)); } - void destroy_rust_listener_adapter(Monero::WalletListener* ptr) { + void destroy_rust_listener_adapter(Monero::WalletListener *ptr) + { delete ptr; } } diff --git a/monero-sys/src/bridge.rs b/monero-sys/src/bridge.rs index 14e62759f2..29d38ba824 100644 --- a/monero-sys/src/bridge.rs +++ b/monero-sys/src/bridge.rs @@ -35,6 +35,13 @@ pub mod ffi { ConnectionStatus_WrongVersion = 2, } + /// A transaction key corresponding to a specific output in a specific transaction. + struct TxKey { + txid: UniquePtr, + address: UniquePtr, + key: UniquePtr, + } + unsafe extern "C++" { include!("wallet/api/wallet2_api.h"); include!("bridge.h"); @@ -289,7 +296,7 @@ pub mod ffi { fn pendingTransactionTxKeys( tx: &PendingTransaction, tx_hash: &CxxString, - ) -> Result>>; + ) -> Result>>; /// Get the fee of a pending transaction. fn pendingTransactionFee(tx: &PendingTransaction) -> Result; @@ -297,9 +304,6 @@ pub mod ffi { /// Get the amount of a pending transaction. fn pendingTransactionAmount(tx: &PendingTransaction) -> Result; - /// Get the transaction key (r) for a given txid. - fn walletGetTxKey(wallet: &Wallet, txid: &CxxString) -> Result>; - /// Commit a pending transaction to the blockchain. fn commit( self: Pin<&mut PendingTransaction>, @@ -666,6 +670,9 @@ fn forward_cpp_log( // We don't want to log the performance timer. if func_str.starts_with("tools::LoggingPerformanceTimer") + || func_str.starts_with("void tools::detail::print_source_entry(") + || func_str.starts_with("bool cryptonote::construct_tx_with_tx_key(") + || func_str.starts_with("void tools::wallet2::get_outs(") || msg_str.starts_with("Processed block: <") || msg_str.starts_with("Found new pool tx: <") { diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs index fc7f99ce88..a4c843f10c 100644 --- a/monero-sys/src/lib.rs +++ b/monero-sys/src/lib.rs @@ -127,6 +127,7 @@ pub struct SyncProgress { } /// The status of a transaction. +#[derive(Debug, Clone)] pub struct TxStatus { /// The amount received in the transaction. pub received: monero::Amount, @@ -140,7 +141,11 @@ pub struct TxStatus { /// Contains basic information needed for later verification. pub struct TxReceipt { pub txid: String, - pub tx_key: String, + /// A map that has an entry for each non-change output + /// where the key is the output's address and the value is the transfer key + /// corresponding to that output. We use these for our transfer proofs. + /// In Monero lingo, this is the r for each K^s/K^v. + pub tx_keys: HashMap, /// The blockchain height at the time of publication. pub height: u64, } @@ -310,7 +315,7 @@ impl WalletHandle { network: monero::Network, background_sync: bool, ) -> anyhow::Result { - let password = password.into(); + let password: Option = password.into(); Self::open_with(path.clone(), daemon.clone(), move |manager| { manager.open_or_create_wallet( @@ -436,6 +441,12 @@ impl WalletHandle { self.call(move |wallet| wallet.main_address()).await } + /// Get the address of the wallet for a given account and address index. + pub async fn address(&self, account_index: u32, address_index: u32) -> monero::Address { + self.call(move |wallet| wallet.address(account_index, address_index)) + .await + } + /// Get the current height of the blockchain. /// May involve an RPC call to the daemon. /// Returns `None` if the wallet is not connected to a daemon. @@ -793,7 +804,7 @@ impl WalletHandle { } /// Check the status of a transaction. - async fn check_tx_status( + pub async fn check_tx_status( &self, txid: String, tx_key: monero::PrivateKey, @@ -903,7 +914,7 @@ impl WalletHandle { // Closure that returns (txid, amount, fee) or error let result = (|| -> Result<(String, monero::Amount, monero::Amount), anyhow::Error> { - let (txid, _) = pending_tx.validate_single_txid_single_tx_key() + let (txid, _) = pending_tx.validate_single_txid(&[address]) .context("Failed to validate PendingTransaction to have single txid and single tx key")?; let amount = ffi::pendingTransactionAmount(&pending_tx) @@ -946,7 +957,8 @@ impl WalletHandle { // Publish the transaction if approved { - let receipt_result = wallet.publish_pending_transaction(&mut pending_tx); + let receipt_result = + wallet.publish_pending_transaction(&mut pending_tx, &[address]); // Dispose the pending transaction independent of whether the publish was successful or not wallet.dispose_pending_transaction(pending_tx); @@ -1537,7 +1549,7 @@ impl FfiWallet { /// Get the address for the given account and address index. /// address(0, 0) is the main address. /// We don't use anything besides the main address so this is a private method (for now). - fn address(&self, account_index: u32, address_index: u32) -> monero::Address { + pub fn address(&self, account_index: u32, address_index: u32) -> monero::Address { let address = ffi::address(&self.inner, account_index, address_index) .context("Failed to get wallet address: FFI call failed with exception") .expect("Wallet address should never fail"); @@ -2004,7 +2016,7 @@ impl FfiWallet { // Publish the transaction let result = self - .publish_pending_transaction(&mut pending_tx) + .publish_pending_transaction(&mut pending_tx, &[*address]) .context("Failed to publish sweep transaction"); // Dispose the pending transaction after we're done with it @@ -2060,7 +2072,7 @@ impl FfiWallet { // Publish the transaction let result = self - .publish_pending_transaction(&mut pending_tx) + .publish_pending_transaction(&mut pending_tx, &addresses) .context("Failed to publish multi-sweep transaction"); // Dispose the pending transaction after we're done with it @@ -2080,11 +2092,16 @@ impl FfiWallet { self.ensure_synchronized_blocking() .context("Cannot transfer when wallet is not synchronized")?; + let output_addresses = destinations + .iter() + .map(|(address, _)| *address) + .collect::>(); + // Construct the pending transaction let mut pending_tx = self.create_pending_transaction_multi_dest(destinations, false)?; // Publish the transaction - let result = self.publish_pending_transaction(&mut pending_tx); + let result = self.publish_pending_transaction(&mut pending_tx, &output_addresses); // Dispose the pending transaction after we're done with it // independent of whether the publish was successful or not @@ -2182,15 +2199,19 @@ impl FfiWallet { /// Publish a pending transaction and return a receipt. /// Note: Caller is responsible for disposing the pending transaction afterwards. + /// + /// `output_addresses` is a list of monero address which are mentioned in outputs for which we + /// need a tx key. fn publish_pending_transaction( &mut self, pending_tx: &mut PendingTransaction, + output_addresses: &[monero::Address], ) -> anyhow::Result { // Ensure the transaction only has a single txid and tx key // // We forbid splitting transactions. We forbid multiple tx keys. - let (txid, tx_key) = pending_tx.validate_single_txid_single_tx_key().context( - "Failed to ensure transaction has one txid and one tx key before publishing", + let (txid, tx_keys) = pending_tx.validate_single_txid(output_addresses).context( + "Failed to ensure transaction has one txid and at least one tx key before publishing", )?; // Get current blockchain height @@ -2211,7 +2232,7 @@ impl FfiWallet { Ok(_) => { return Ok(TxReceipt { txid, - tx_key, + tx_keys, height, }); } @@ -2503,9 +2524,12 @@ impl PendingTransaction { } } - fn validate_single_txid_single_tx_key( + /// Validates that the pending tx isn't split and returns the tx id as well as + /// the transfer key for each output. + fn validate_single_txid( self: &mut Self, - ) -> Result<(String, String), anyhow::Error> { + output_addresses: &[monero::Address], + ) -> Result<(String, HashMap), anyhow::Error> { // This can return multiple txids if wallet2 decided to split the transaction let txids = ffi::pendingTransactionTxIds(self) .context("Failed to get txid from pending transaction: FFI call failed with exception")? @@ -2516,10 +2540,13 @@ impl PendingTransaction { // Ensure it only created one transaction let txid = match txids.as_slice() { [txid] => txid.clone(), - _ => anyhow::bail!( - "Expected 1 txid, got {}. We do not allow splitting transactions", - txids.len() - ), + _ => { + tracing::debug!(txids=?txids,"Got the transaction id's"); + anyhow::bail!( + "Expected 1 txid, got {}. We do not allow splitting transactions", + txids.len() + ) + } }; // Sanity check that the txid is at least the correct length @@ -2532,38 +2559,61 @@ impl PendingTransaction { ); } - // This could theoretically return multiple tx keys as Monero does allow multiple tx keys for a single transaction - // According to moneromoo, its non standard behavior though so wallet2 should never do this + // This returns only one tx key, if the destinations included at most one subaddress + // + // If there were more than one subaddress, we will get 1 + number of outputs tx keys + // - one primary tx key + // - one tx key for each output let_cxx_string!(txid_cxx = &txid); - let tx_keys = ffi::pendingTransactionTxKeys(self, &txid_cxx) - .context( - "Failed to get tx key from pending transaction: FFI call failed with exception", - )? - .into_iter() - .map(|s| s.to_string()) - .collect::>(); - - // Ensure we only have one tx key - // If we have multiple tx keys, we would need to create multiple transfer proofs - let tx_key = match tx_keys.as_slice() { - [key] => key.clone(), - _ => anyhow::bail!( - "Expected 1 tx key, got {}. We do not allow splitting transactions", - tx_keys.len() - ), - }; - - // Ensure we didn't get junk from wallet2 - { - monero::PrivateKey::from_str(&tx_key) - .with_context(|| format!("Invalid tx key: {tx_key}"))?; + let tx_keys: Vec<(monero::Address, monero::PrivateKey)> = + ffi::pendingTransactionTxKeys(self, &txid_cxx) + .context( + "Failed to get tx key from pending transaction: FFI call failed with exception", + )? + .into_iter() + .map(|tx_key| -> Result<(monero::Address, monero::PrivateKey)> { + Ok(( + tx_key + .address + .to_str() + .context("Got non-utf8 address string")? + .parse() + .context("Got invalid Monero address")?, + tx_key + .key + .to_str() + .context("Got non-utf8 key string")? + .parse() + .context("Got invalid Monero private key")?, + )) + }) + .collect::, anyhow::Error>>()?; + + if tx_keys.is_empty() { + anyhow::bail!("Expected at least one tx key, got 0"); + } + + let mut keys_map = HashMap::new(); + for (address, tx_key) in tx_keys { + if keys_map.contains_key(&address) { + anyhow::bail!("Address {} is used for multiple outputs", address); + } else { + keys_map.insert(address, tx_key); + } + } - if txid.is_empty() { - anyhow::bail!("Got an empty txid"); + for address in output_addresses { + if !keys_map.contains_key(address) { + anyhow::bail!( + "Output address {} is not mentioned in tx keys for tx {}. tx_keys.len() = {}. Sending funds to your own primary address is NOT supported.", + address, + txid, + keys_map.len() + ); } } - Ok((txid, tx_key)) + Ok((txid, keys_map)) } } diff --git a/monero-sys/tests/transaction_keys_testnet.rs b/monero-sys/tests/transaction_keys_testnet.rs new file mode 100644 index 0000000000..ddb186a534 --- /dev/null +++ b/monero-sys/tests/transaction_keys_testnet.rs @@ -0,0 +1,90 @@ +/// Construct, publish and return the transaction keys of a complex transaction +/// (sending to multiple addresses, some of which are subaddresses) +use monero::Amount; +use monero_sys::{Daemon, SyncProgress, WalletHandle}; + +const STAGENET_REMOTE_NODE: &str = "http://node.sethforprivacy.com:38089"; +const STAGENET_WALLET_SEED: &str = "echo ourselves ruined oven masterful wives enough addicted future cottage illness adopt lucky movement tiger taboo imbalance antics iceberg hobby oval aloof tuesday uttered oval"; +const STAGENET_WALLET_RESTORE_HEIGHT: u64 = 1728128; + +#[tokio::test] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + "info,test=debug,monero_harness=debug,monero_rpc=debug,transaction_keys=trace,monero_sys=trace", + ) + .with_test_writer() + .init(); + + let temp_dir = tempfile::tempdir().unwrap(); + let daemon = Daemon::try_from(STAGENET_REMOTE_NODE).unwrap(); + + let wallet_name = "recovered_wallet"; + let wallet_path = temp_dir.path().join(wallet_name).display().to_string(); + + tracing::info!("Recovering wallet from seed"); + let wallet = WalletHandle::open_or_create_from_seed( + wallet_path, + STAGENET_WALLET_SEED.to_string(), + monero::Network::Stagenet, + STAGENET_WALLET_RESTORE_HEIGHT, + true, + daemon, + ) + .await + .expect("Failed to recover wallet"); + + tracing::info!("Primary address: {}", wallet.main_address().await); + + // Wait for a while to let the wallet sync, checking sync status + tracing::info!("Waiting for wallet to sync..."); + + wallet + .wait_until_synced(Some(|sync_progress: SyncProgress| { + tracing::info!("Sync progress: {}%", sync_progress.percentage()); + })) + .await + .expect("Failed to sync wallet"); + + wallet.store_in_current_file().await?; + + // Test sending to some (sub)addresses + let subaddress1 = wallet.address(1, 0).await; + let subaddress2 = wallet.address(0, 2).await; + let subaddress3 = wallet.address(1, 2).await; + let subaddress4 = wallet.address(2, 2).await; + + let addresses = [ + subaddress1.to_string(), + subaddress2.to_string(), + subaddress3.to_string(), + subaddress4.to_string(), + ]; + tracing::info!(addresses=?addresses, "Got the destination addresses"); + + let amount = Amount::from_xmr(0.02)?; + + let tx_receipt = wallet + .transfer_multi_destination(&[ + (subaddress1, amount), + (subaddress2, amount), + (subaddress3, amount), + (subaddress4, amount), + ]) + .await?; + + // at this point we managed to publish the transaction and + // got all transaction keys (for each output). + // The test passed, the logs are just for debugging. + tracing::info!(tx_id = &tx_receipt.txid, "Transaction published! (good)"); + assert_eq!( + tx_receipt.tx_keys.len(), + 4, + "Expect one tx key per output (none for change)" + ); + for (addr, key) in tx_receipt.tx_keys { + tracing::info!(address=%addr, %key, "Got transaction key"); + } + + Ok(()) +} diff --git a/monero-tests/Cargo.toml b/monero-tests/Cargo.toml new file mode 100644 index 0000000000..18ea2af6d5 --- /dev/null +++ b/monero-tests/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "monero-tests" +version = "0.1.0" +edition = "2024" + +[dependencies] +monero-harness = { path = "../monero-harness" } + +monero = { workspace = true } + +anyhow = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[lints] +workspace = true diff --git a/monero-tests/src/lib.rs b/monero-tests/src/lib.rs new file mode 100644 index 0000000000..2235e70956 --- /dev/null +++ b/monero-tests/src/lib.rs @@ -0,0 +1 @@ +// Empty but necessary for cargo diff --git a/monero-tests/tests/transaction_keys.rs b/monero-tests/tests/transaction_keys.rs new file mode 100644 index 0000000000..46ec8b4648 --- /dev/null +++ b/monero-tests/tests/transaction_keys.rs @@ -0,0 +1,62 @@ +use anyhow::Context; +use monero_harness::Cli; + +/// Create a transaction with transaction proofs, and verify them. +/// Fails if the publishing fails due to the transfer keys not being extracted successfully +/// or due to them not being able to be verified by the recipients. +#[tokio::test] +async fn monero_transfers() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + "info,test=debug,monero_harness=debug,monero_rpc=debug,monero_sys=trace,transfers=trace,monero_cpp=info", + ).init(); + + let cli = Cli::default(); + let wallets = vec!["alice", "bob", "candice"]; + // Disbale background sync for these wallet -- this way we _have_ to use the transfer proof to discover the transactions. + let (monero, _container, _wallet_conainers) = + monero_harness::Monero::new_with_sync_specified(&cli, wallets, false).await?; + + tracing::info!("Starting miner"); + + monero.init_and_start_miner().await?; + + let miner = monero.wallet("miner")?; + let alice = monero.wallet("alice")?; + let bob = monero.wallet("bob")?; + let candice = monero.wallet("candice")?; + + tracing::info!("Checking miner balance"); + + assert!(miner.balance().await? > 0); + + tracing::info!("Sending money"); + + let proof = miner + .sweep_multi( + &[ + alice.address().await?, + bob.address().await?, + candice.address().await?, + ], + &[0.33333333, 0.333333333, 0.3333333333], + ) + .await?; + + assert_eq!( + proof.tx_keys.len(), + 3, + "Expect one transaction key per non-change output" + ); + + alice + .check_tx_key( + proof.txid.clone(), + proof.tx_keys.get(alice.address().await?), + ) + .await?; + + assert_eq!(alice.sweep(bob.address().await?).await?.tx_keys.len(), 1); + + Ok(()) +} diff --git a/monero-tests/tests/transfers.rs b/monero-tests/tests/transfers.rs new file mode 100644 index 0000000000..fac027e1bf --- /dev/null +++ b/monero-tests/tests/transfers.rs @@ -0,0 +1,83 @@ +use anyhow::Context; +use monero_harness::Cli; + +/// Create a transaction with transaction proofs, and verify them. +/// Fails if the publishing fails due to the transfer keys not being extracted successfully +/// or due to them not being able to be verified by the recipients. +#[tokio::test] +async fn monero_transfers() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + "info,test=debug,monero_harness=debug,monero_rpc=debug,monero_sys=trace,transfers=trace,monero_cpp=info", + ).init(); + + let cli = Cli::default(); + let wallets = vec!["alice", "bob"]; + // Disbale background sync for these wallet -- this way we _have_ to use the transfer proof to discover the transactions. + let (monero, _container, _wallet_conainers) = + monero_harness::Monero::new_with_sync_specified(&cli, wallets, false).await?; + + tracing::info!("Starting miner"); + + monero.init_and_start_miner().await?; + + let miner_wallet = monero.wallet("miner")?; + let alice = monero.wallet("alice")?; + let bob = monero.wallet("bob")?; + + tracing::info!("Checking miner balance"); + + assert!(miner_wallet.balance().await? > 0); + + tracing::info!("Sending money"); + + let tx_receipt = miner_wallet + .sweep_multi(&[alice.address().await?, bob.address().await?], &[0.5, 0.5]) + .await?; + + assert_eq!( + tx_receipt.tx_keys.len(), + 2, + "Expect one tx key for each non-change output" + ); + + monero.generate_block().await?; + + let alice_txkey = tx_receipt + .tx_keys + .get(&alice.address().await?) + .context("tx key not found for alice")?; + + let bob_txkey = tx_receipt + .tx_keys + .get(&bob.address().await?) + .context("tx key not found for bob")?; + + tracing::info!("Importing tx keys"); + + let alice_status = alice + .check_tx_key(tx_receipt.txid.clone(), *alice_txkey) + .await?; + let bob_status = bob + .check_tx_key(tx_receipt.txid.clone(), *bob_txkey) + .await?; + + tracing::info!( + ?alice_status, + ?bob_status, + "Successfully checked transactions keys!" + ); + + // sanity check: we should have actually received the money... + + assert!( + alice.balance().await? > 0, + "Alice expected to have received funds" + ); + assert!( + bob.balance().await? > 0, + "Bob expected to have received funds" + ); + + Ok(()) +} diff --git a/monero-tests/tests/transfers_wrong_key.rs b/monero-tests/tests/transfers_wrong_key.rs new file mode 100644 index 0000000000..a6f5ba1e33 --- /dev/null +++ b/monero-tests/tests/transfers_wrong_key.rs @@ -0,0 +1,64 @@ +use anyhow::bail; +use monero_harness::Cli; + +/// Verify that checking a transaction with a wrong/random transfer key fails. +#[tokio::test] +async fn monero_transfers_wrong_key() { + tracing_subscriber::fmt() + .with_env_filter( + "info,test=debug,monero_harness=debug,monero_rpc=debug,monero_sys=trace,transfers=trace,monero_cpp=info", + ).init(); + + let cli = Cli::default(); + let wallets = vec!["alice"]; + // Disable background sync for this wallet -- this way we _have_ to use the transfer proof to discover the transactions. + let (monero, _container, _wallet_conainers) = + monero_harness::Monero::new_with_sync_specified(&cli, wallets, false) + .await + .unwrap(); + + tracing::info!("Starting miner"); + + monero.init_and_start_miner().await.unwrap(); + + let miner_wallet = monero.wallet("miner").unwrap(); + let alice = monero.wallet("alice").unwrap(); + + tracing::info!("Checking miner balance"); + + assert!(miner_wallet.balance().await.unwrap() > 0); + + tracing::info!("Sending money"); + + let tx_receipt = miner_wallet + .sweep(&alice.address().await.unwrap()) + .await + .unwrap(); + + assert_eq!( + tx_receipt.tx_keys.len(), + 1, + "Expect one tx key for the output" + ); + + monero.generate_block().await.unwrap(); + + // Use a wrong private key (just a simple constant key, not the real transfer key) + let wrong_key = monero::PrivateKey::from_slice(&[ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, + ]) + .unwrap(); + + tracing::info!("Importing tx key with wrong key - should fail"); + + let status = alice + .check_tx_key(tx_receipt.txid.clone(), wrong_key) + .await + .unwrap(); + + // Wrong tx key -> amount is zero. + if status.received != monero::Amount::ZERO { + panic!("could decrypt payment - this is not supposed to happen since we got a bogus key"); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 45f5f19cc8..6498d5dd83 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] # also update this in the readme, changelog, and github actions -channel = "1.87.0" +channel = "1.88.0" components = ["clippy"] targets = ["armv7-unknown-linux-gnueabihf"] diff --git a/src-gui/eslint.config.js b/src-gui/eslint.config.js index 0e94c83446..e1fc349853 100644 --- a/src-gui/eslint.config.js +++ b/src-gui/eslint.config.js @@ -2,6 +2,7 @@ import globals from "globals"; import js from "@eslint/js"; import tseslint from "typescript-eslint"; import pluginReact from "eslint-plugin-react"; +import importPlugin from "eslint-plugin-import"; export default [ { ignores: ["node_modules", "dist"] }, @@ -12,12 +13,16 @@ export default [ languageOptions: { globals: globals.browser, }, + plugins: { + import: importPlugin, + }, rules: { "react/react-in-jsx-scope": "off", "react/no-unescaped-entities": "off", "react/no-children-prop": "off", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-empty-object-type": "off", + "import/no-cycle": ["error", { maxDepth: 10 }], "no-restricted-globals": [ "warn", { diff --git a/src-gui/knip.json b/src-gui/knip.json new file mode 100644 index 0000000000..9da2939e2b --- /dev/null +++ b/src-gui/knip.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "entry": ["src/renderer/index.tsx", "index.html"], + "project": ["src/**/*.{ts,tsx,js,jsx}"], + "ignoreExportsUsedInFile": true, + "tags": ["-lintignore"] +} diff --git a/src-gui/package.json b/src-gui/package.json index c1983e3362..961d008cd7 100644 --- a/src-gui/package.json +++ b/src-gui/package.json @@ -4,9 +4,9 @@ "version": "0.7.0", "type": "module", "scripts": { - "check-bindings": "typeshare --lang=typescript --output-file __temp_bindings.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src && dprint fmt __temp_bindings.ts && diff -wbB __temp_bindings.ts ./src/models/tauriModel.ts && rm __temp_bindings.ts", - "gen-bindings-verbose": "RUST_LOG=debug RUST_BACKTRACE=1 typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src && dprint fmt ./src/models/tauriModel.ts", - "gen-bindings": "typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src && dprint fmt ./src/models/tauriModel.ts", + "check-bindings": "typeshare --lang=typescript --output-file __temp_bindings.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src ../swap-core/src ../swap-p2p/src && dprint fmt __temp_bindings.ts && diff -wbB __temp_bindings.ts ./src/models/tauriModel.ts && rm __temp_bindings.ts", + "gen-bindings-verbose": "RUST_LOG=debug RUST_BACKTRACE=1 typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src ../swap-core/src ../swap-p2p/src && dprint fmt ./src/models/tauriModel.ts", + "gen-bindings": "typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src ../swap-core/src ../swap-p2p/src && dprint fmt ./src/models/tauriModel.ts", "test": "vitest", "test:ui": "vitest --ui", "dev": "vite", @@ -20,7 +20,6 @@ "@emotion/styled": "^11.14.0", "@fontsource/roboto": "^5.1.0", "@mui/icons-material": "^7.1.1", - "@mui/lab": "^7.0.0-beta.13", "@mui/material": "^7.1.1", "@mui/x-date-pickers": "^8.8.0", "@reduxjs/toolkit": "^2.3.0", @@ -33,16 +32,12 @@ "@tauri-apps/plugin-shell": "^2.3.0", "@tauri-apps/plugin-store": "^2.4.0", "@tauri-apps/plugin-updater": "^2.9.0", - "@types/react-redux": "^7.1.34", "boring-avatars": "^1.11.2", "dayjs": "^1.11.13", "humanize-duration": "^3.32.1", - "jdenticon": "^3.3.0", "lodash": "^4.17.21", "multiaddr": "^10.0.1", "notistack": "^3.0.1", - "pino": "^9.2.0", - "pino-pretty": "^11.2.1", "react": "^19.1.0", "react-dom": "^19.1.0", "react-qr-code": "^2.0.15", @@ -64,9 +59,11 @@ "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", "@types/react-is": "^19.0.0", + "@types/react-redux": "^7.1.34", "@types/semver": "^7.5.8", "@vitejs/plugin-react": "^4.2.1", "eslint": "^9.9.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.35.0", "globals": "^15.9.0", "internal-ip": "^7.0.0", diff --git a/src-gui/src/models/rpcModel.ts b/src-gui/src/models/rpcModel.ts index 57f9460987..81aa88993f 100644 --- a/src-gui/src/models/rpcModel.ts +++ b/src-gui/src/models/rpcModel.ts @@ -1,112 +1,4 @@ -export enum RpcMethod { - GET_BTC_BALANCE = "get_bitcoin_balance", - WITHDRAW_BTC = "withdraw_btc", - BUY_XMR = "buy_xmr", - RESUME_SWAP = "resume_swap", - LIST_SELLERS = "list_sellers", - CANCEL_REFUND_SWAP = "cancel_refund_swap", - GET_SWAP_INFO = "get_swap_info", - SUSPEND_CURRENT_SWAP = "suspend_current_swap", - GET_HISTORY = "get_history", - GET_MONERO_RECOVERY_KEYS = "get_monero_recovery_info", -} - -export enum RpcProcessStateType { - STARTED = "starting...", - LISTENING_FOR_CONNECTIONS = "running", - EXITED = "exited", - NOT_STARTED = "not started", -} - -export type RawRpcResponseSuccess = { - jsonrpc: string; - id: string; - result: T; -}; - -export type RawRpcResponseError = { - jsonrpc: string; - id: string; - error: { code: number; message: string }; -}; - -export type RawRpcResponse = RawRpcResponseSuccess | RawRpcResponseError; - -export function isSuccessResponse( - response: RawRpcResponse, -): response is RawRpcResponseSuccess { - return "result" in response; -} - -export function isErrorResponse( - response: RawRpcResponse, -): response is RawRpcResponseError { - return "error" in response; -} - -export interface RpcSellerStatus { - status: - | { - Online: { - price: number; - min_quantity: number; - max_quantity: number; - }; - } - | "Unreachable"; - multiaddr: string; -} - -export interface WithdrawBitcoinResponse { - txid: string; -} - -export interface BuyXmrResponse { - swapId: string; -} - -export type SwapTimelockInfoNone = { - None: { - blocks_left: number; - }; -}; - -export type SwapTimelockInfoCancelled = { - Cancel: { - blocks_left: number; - }; -}; - -export type SwapTimelockInfoPunished = "Punish"; - -export type SwapTimelockInfo = - | SwapTimelockInfoNone - | SwapTimelockInfoCancelled - | SwapTimelockInfoPunished; - -export function isSwapTimelockInfoNone( - info: SwapTimelockInfo, -): info is SwapTimelockInfoNone { - return typeof info === "object" && "None" in info; -} - -export function isSwapTimelockInfoCancelled( - info: SwapTimelockInfo, -): info is SwapTimelockInfoCancelled { - return typeof info === "object" && "Cancel" in info; -} - -export function isSwapTimelockInfoPunished( - info: SwapTimelockInfo, -): info is SwapTimelockInfoPunished { - return info === "Punish"; -} - -export type SwapSellerInfo = { - peerId: string; - addresses: string[]; -}; - +// TODO: Auto generate this using typeshare from swap/src/cli/api/request.rs export type MoneroRecoveryResponse = { address: string; spend_key: string; diff --git a/src-gui/src/models/tauriModelExt.ts b/src-gui/src/models/tauriModelExt.ts index 7e3f5c58c5..eb369af532 100644 --- a/src-gui/src/models/tauriModelExt.ts +++ b/src-gui/src/models/tauriModelExt.ts @@ -8,6 +8,8 @@ import { TauriSwapProgressEvent, SendMoneroDetails, ContextStatus, + QuoteWithAddress, + ExportBitcoinWalletResponse, } from "./tauriModel"; import { ContextStatusType, @@ -17,6 +19,16 @@ import { export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"]; +// Wrapper for QuoteWithAddress with an optional approval request +// Approving that request will result in a swap being initiated with that maker +export type SortableQuoteWithAddress = { + quote_with_address: QuoteWithAddress; + approval: { + request_id: string; + expiration_ts: number; + } | null; +}; + export type TauriSwapProgressEventContent< T extends TauriSwapProgressEventType, > = Extract["content"]; @@ -131,10 +143,6 @@ export type GetSwapInfoResponseExtRunningSwap = GetSwapInfoResponseExt & { state_name: BobStateNameRunningSwap; }; -export type GetSwapInfoResponseExtWithTimelock = GetSwapInfoResponseExt & { - timelock: ExpiredTimelocks; -}; - export function isBobStateNameRunningSwap( state: BobStateName, ): state is BobStateNameRunningSwap { @@ -252,17 +260,6 @@ export function isGetSwapInfoResponseRunningSwap( return isBobStateNameRunningSwap(response.state_name); } -/** - * Type guard for GetSwapInfoResponseExt to ensure timelock is not null - * @param response The swap info response to check - * @returns True if the timelock exists, false otherwise - */ -export function isGetSwapInfoResponseWithTimelock( - response: GetSwapInfoResponseExt, -): response is GetSwapInfoResponseExtWithTimelock { - return response.timelock !== null; -} - export type PendingApprovalRequest = ApprovalRequest & { content: Extract; }; @@ -369,9 +366,9 @@ export function isPendingPasswordApprovalEvent( * @returns True if funds have been locked, false otherwise */ export function haveFundsBeenLocked( - event: TauriSwapProgressEvent | null, + event: TauriSwapProgressEvent | null | undefined, ): boolean { - if (event === null) { + if (event === null || event === undefined) { return false; } @@ -387,7 +384,7 @@ export function haveFundsBeenLocked( } export function isContextFullyInitialized( - status: ResultContextStatus, + status: ResultContextStatus | null, ): boolean { if (status == null || status.type === ContextStatusType.Error) { return false; @@ -411,3 +408,21 @@ export function isContextWithMoneroWallet( ): boolean { return status?.monero_wallet_available ?? false; } + +export type ExportBitcoinWalletResponseExt = ExportBitcoinWalletResponse & { + wallet_descriptor: { + descriptor: string; + }; +}; + +export function hasDescriptorProperty( + response: ExportBitcoinWalletResponse, +): response is ExportBitcoinWalletResponseExt { + return ( + typeof response.wallet_descriptor === "object" && + response.wallet_descriptor !== null && + "descriptor" in response.wallet_descriptor && + typeof (response.wallet_descriptor as { descriptor?: unknown }) + .descriptor === "string" + ); +} diff --git a/src-gui/src/renderer/background.ts b/src-gui/src/renderer/background.ts index ecde693927..40eab67623 100644 --- a/src-gui/src/renderer/background.ts +++ b/src-gui/src/renderer/background.ts @@ -3,11 +3,11 @@ import { TauriEvent } from "models/tauriModel"; import { contextStatusEventReceived, contextInitializationFailed, - rpcSetBalance, timelockChangeEventReceived, approvalEventReceived, backgroundProgressEventReceived, } from "store/features/rpcSlice"; +import { setBitcoinBalance } from "store/features/bitcoinWalletSlice"; import { receivedCliLog } from "store/features/logsSlice"; import { poolStatusReceived } from "store/features/poolSlice"; import { swapProgressEventReceived } from "store/features/swapSlice"; @@ -21,6 +21,7 @@ import { import { checkContextStatus, getSwapInfo, + getSwapTimelock, initializeContext, listSellersAtRendezvousPoint, refreshApprovals, @@ -33,6 +34,12 @@ import { setHistory, setSyncProgress, } from "store/features/walletSlice"; +import { applyDefaultNodes } from "store/features/settingsSlice"; +import { + DEFAULT_NODES, + NEGATIVE_NODES_MAINNET, + NEGATIVE_NODES_TESTNET, +} from "store/defaults"; const TAURI_UNIFIED_EVENT_CHANNEL_NAME = "tauri-unified-event"; @@ -63,6 +70,15 @@ function setIntervalImmediate(callback: () => void, interval: number): void { } export async function setupBackgroundTasks(): Promise { + // Apply default nodes on startup (removes broken nodes, adds new ones) + store.dispatch( + applyDefaultNodes({ + defaultNodes: DEFAULT_NODES, + negativeNodesMainnet: NEGATIVE_NODES_MAINNET, + negativeNodesTestnet: NEGATIVE_NODES_TESTNET, + }), + ); + // Setup periodic fetch tasks setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL); setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL); @@ -118,16 +134,36 @@ listen(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => { break; case "BalanceChange": - store.dispatch(rpcSetBalance(eventData.balance)); + store.dispatch(setBitcoinBalance(eventData.balance)); break; case "SwapDatabaseStateUpdate": - getSwapInfo(eventData.swap_id); + getSwapInfo(eventData.swap_id).catch((error) => { + logger.debug( + `Failed to fetch swap info for swap ${eventData.swap_id}: ${error}`, + ); + }); + getSwapTimelock(eventData.swap_id).catch((error) => { + logger.debug( + `Failed to fetch timelock for swap ${eventData.swap_id}: ${error}`, + ); + }); // This is ugly but it's the best we can do for now // Sometimes we are too quick to fetch the swap info and the new state is not yet reflected // in the database. So we wait a bit before fetching the new state - setTimeout(() => getSwapInfo(eventData.swap_id), 3000); + setTimeout(() => { + getSwapInfo(eventData.swap_id).catch((error) => { + logger.debug( + `Failed to fetch swap info for swap ${eventData.swap_id}: ${error}`, + ); + }); + getSwapTimelock(eventData.swap_id).catch((error) => { + logger.debug( + `Failed to fetch timelock for swap ${eventData.swap_id}: ${error}`, + ); + }); + }, 3000); break; case "TimelockChange": diff --git a/src-gui/src/renderer/components/App.tsx b/src-gui/src/renderer/components/App.tsx index 9f9283440d..bd164f269a 100644 --- a/src-gui/src/renderer/components/App.tsx +++ b/src-gui/src/renderer/components/App.tsx @@ -26,6 +26,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog"; import ContextErrorDialog from "./modal/context-error/ContextErrorDialog"; +import ErrorBoundary from "./other/ErrorBoundary"; declare module "@mui/material/styles" { interface Theme { @@ -47,24 +48,26 @@ export default function App() { console.log("Current theme:", { theme, currentTheme }); return ( - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + ); } @@ -79,13 +82,62 @@ function InnerContent() { }} > - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> ); diff --git a/src-gui/src/renderer/components/PromiseInvokeButton.tsx b/src-gui/src/renderer/components/PromiseInvokeButton.tsx index 90d573f9c2..4987b7001a 100644 --- a/src-gui/src/renderer/components/PromiseInvokeButton.tsx +++ b/src-gui/src/renderer/components/PromiseInvokeButton.tsx @@ -34,7 +34,7 @@ interface PromiseInvokeButtonProps { export default function PromiseInvokeButton({ disabled = false, - onSuccess = null, + onSuccess, onInvoke, children, startIcon, @@ -44,7 +44,7 @@ export default function PromiseInvokeButton({ isIconButton = false, isChipButton = false, displayErrorSnackbar = false, - onPendingChange = null, + onPendingChange, contextRequirement = true, tooltipTitle = null, ...rest diff --git a/src-gui/src/renderer/components/alert/FundsLeftInWalletAlert.tsx b/src-gui/src/renderer/components/alert/FundsLeftInWalletAlert.tsx deleted file mode 100644 index c1a571e3c8..0000000000 --- a/src-gui/src/renderer/components/alert/FundsLeftInWalletAlert.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Button } from "@mui/material"; -import Alert from "@mui/material/Alert"; -import { useNavigate } from "react-router-dom"; -import { useAppSelector } from "store/hooks"; - -export default function FundsLeftInWalletAlert() { - const fundsLeft = useAppSelector((state) => state.rpc.state.balance); - const navigate = useNavigate(); - - if (fundsLeft != null && fundsLeft > 0) { - return ( - navigate("/bitcoin-wallet")} - > - View - - } - > - There are some Bitcoin left in your wallet - - ); - } - return null; -} diff --git a/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx b/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx index c2112c7b3a..25d4734ee1 100644 --- a/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx +++ b/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx @@ -5,7 +5,6 @@ import { GetSwapInfoResponseExt, GetSwapInfoResponseExtRunningSwap, isGetSwapInfoResponseRunningSwap, - isGetSwapInfoResponseWithTimelock, TimelockCancel, TimelockNone, } from "models/tauriModelExt"; @@ -15,7 +14,9 @@ import HumanizedBitcoinBlockDuration from "../../other/HumanizedBitcoinBlockDura import TruncatedText from "../../other/TruncatedText"; import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton"; import { TimelockTimeline } from "./TimelockTimeline"; -import { useIsSpecificSwapRunning } from "store/hooks"; +import { useIsSpecificSwapRunning, useAppSelector } from "store/hooks"; +import { selectSwapTimelock } from "store/selectors"; +import { ExpiredTimelocks } from "models/tauriModel"; /** * Component for displaying a list of messages. @@ -167,9 +168,11 @@ function PunishTimelockExpiredAlert() { */ export function StateAlert({ swap, + timelock, isRunning, }: { swap: GetSwapInfoResponseExtRunningSwap; + timelock: ExpiredTimelocks | null; isRunning: boolean; }) { switch (swap.state_name) { @@ -188,12 +191,12 @@ export function StateAlert({ case BobStateName.BtcCancelled: case BobStateName.BtcRefundPublished: // Even if the transactions have been published, it cannot be case BobStateName.BtcEarlyRefundPublished: // guaranteed that they will be confirmed in time - if (swap.timelock != null) { - switch (swap.timelock.type) { + if (timelock != null) { + switch (timelock.type) { case "None": return ( + ); case "Punish": return ; default: - // We have covered all possible timelock states above - // If we reach this point, it means we have missed a case - exhaustiveGuard(swap.timelock); + exhaustiveGuard(timelock); } } return ; @@ -230,6 +228,7 @@ export function StateAlert({ // 72 is the default cancel timelock in blocks // 4 blocks are around 40 minutes // If the swap has taken longer than 40 minutes, we consider it unusual +// See: swap-env/src/env.rs const UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD = 72 - 4; /** @@ -241,35 +240,33 @@ export default function SwapStatusAlert({ swap, onlyShowIfUnusualAmountOfTimeHasPassed, }: { - swap: GetSwapInfoResponseExt; + swap: GetSwapInfoResponseExt | null; onlyShowIfUnusualAmountOfTimeHasPassed?: boolean; }) { + const swapId = swap?.swap_id ?? null; + const timelock = useAppSelector(selectSwapTimelock(swapId)); + const isRunning = useIsSpecificSwapRunning(swapId); + if (swap == null) { return null; } - // If the swap is completed, we do not need to display anything if (!isGetSwapInfoResponseRunningSwap(swap)) { return null; } - // If we don't have a timelock for the swap, we cannot display the alert - if (!isGetSwapInfoResponseWithTimelock(swap)) { + if (timelock == null) { return null; } const hasUnusualAmountOfTimePassed = - swap.timelock.type === "None" && - swap.timelock.content.blocks_left > - UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD; + timelock.type === "None" && + timelock.content.blocks_left > UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD; - // If we are only showing if an unusual amount of time has passed, we need to check if the swap has been running for a while - if (onlyShowIfUnusualAmountOfTimeHasPassed && hasUnusualAmountOfTimePassed) { + if (onlyShowIfUnusualAmountOfTimeHasPassed && !hasUnusualAmountOfTimePassed) { return null; } - const isRunning = useIsSpecificSwapRunning(swap.swap_id); - return ( - - + + {timelock && } ); diff --git a/src-gui/src/renderer/components/alert/SwapStatusAlert/TimelockTimeline.tsx b/src-gui/src/renderer/components/alert/SwapStatusAlert/TimelockTimeline.tsx index fea86f3c2b..247b5fc697 100644 --- a/src-gui/src/renderer/components/alert/SwapStatusAlert/TimelockTimeline.tsx +++ b/src-gui/src/renderer/components/alert/SwapStatusAlert/TimelockTimeline.tsx @@ -50,7 +50,7 @@ function TimelineSegment({ opacity: isActive ? 1 : 0.3, }} > - {isActive && ( + {isActive && durationOfSegment && ( (null); - - useEffect(() => { - if (iconRef.current) { - jdenticon.update(iconRef.current, value); - } - }, [value]); - - return ( - - ); -} - -export default IdentIcon; diff --git a/src-gui/src/renderer/components/modal/PaperTextBox.tsx b/src-gui/src/renderer/components/modal/PaperTextBox.tsx deleted file mode 100644 index 0759dfa09a..0000000000 --- a/src-gui/src/renderer/components/modal/PaperTextBox.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Button, Paper, Typography } from "@mui/material"; - -export default function PaperTextBox({ stdOut }: { stdOut: string }) { - function handleCopyLogs() { - throw new Error("Not implemented"); - } - - return ( - - - {stdOut} - - - - ); -} diff --git a/src-gui/src/renderer/components/modal/introduction/slides/Slide01_GettingStarted.tsx b/src-gui/src/renderer/components/modal/introduction/slides/Slide01_GettingStarted.tsx index 3e5f3803d1..af6bf12fd1 100644 --- a/src-gui/src/renderer/components/modal/introduction/slides/Slide01_GettingStarted.tsx +++ b/src-gui/src/renderer/components/modal/introduction/slides/Slide01_GettingStarted.tsx @@ -1,8 +1,9 @@ import { Typography } from "@mui/material"; import SlideTemplate from "./SlideTemplate"; import imagePath from "assets/walletWithBitcoinAndMonero.png"; +import { IntroSlideProps } from "./SlideTypes"; -export default function Slide01_GettingStarted(props: slideProps) { +export default function Slide01_GettingStarted(props: IntroSlideProps) { return ( diff --git a/src-gui/src/renderer/components/modal/introduction/slides/Slide02_ChooseAMaker.tsx b/src-gui/src/renderer/components/modal/introduction/slides/Slide02_ChooseAMaker.tsx index b5cf016176..f63c39afa6 100644 --- a/src-gui/src/renderer/components/modal/introduction/slides/Slide02_ChooseAMaker.tsx +++ b/src-gui/src/renderer/components/modal/introduction/slides/Slide02_ChooseAMaker.tsx @@ -1,8 +1,9 @@ import { Typography } from "@mui/material"; import SlideTemplate from "./SlideTemplate"; import imagePath from "assets/mockMakerSelection.svg"; +import { IntroSlideProps } from "./SlideTypes"; -export default function Slide02_ChooseAMaker(props: slideProps) { +export default function Slide02_ChooseAMaker(props: IntroSlideProps) { return ( void; }) => { diff --git a/src-gui/src/renderer/components/modal/introduction/slides/Slide07_ReachOut.tsx b/src-gui/src/renderer/components/modal/introduction/slides/Slide07_ReachOut.tsx index 2066da47d7..f18f00c1fa 100644 --- a/src-gui/src/renderer/components/modal/introduction/slides/Slide07_ReachOut.tsx +++ b/src-gui/src/renderer/components/modal/introduction/slides/Slide07_ReachOut.tsx @@ -4,8 +4,9 @@ import imagePath from "assets/groupWithChatbubbles.png"; import GitHubIcon from "@mui/icons-material/GitHub"; import MatrixIcon from "renderer/components/icons/MatrixIcon"; import LinkIconButton from "renderer/components/icons/LinkIconButton"; +import { IntroSlideProps } from "./SlideTypes"; -export default function Slide02_ChooseAMaker(props: slideProps) { +export default function Slide02_ChooseAMaker(props: IntroSlideProps) { return ( void; handlePrevious: () => void; hidePreviousButton?: boolean; diff --git a/src-gui/src/renderer/components/modal/provider/MakerInfo.tsx b/src-gui/src/renderer/components/modal/provider/MakerInfo.tsx deleted file mode 100644 index e695c42550..0000000000 --- a/src-gui/src/renderer/components/modal/provider/MakerInfo.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { Box, Chip, Paper, Tooltip, Typography } from "@mui/material"; -import { VerifiedUser } from "@mui/icons-material"; -import { ExtendedMakerStatus } from "models/apiModel"; -import TruncatedText from "renderer/components/other/TruncatedText"; -import { - MoneroBitcoinExchangeRate, - SatsAmount, -} from "renderer/components/other/Units"; -import { getMarkup, satsToBtc, secondsToDays } from "utils/conversionUtils"; -import { isMakerOutdated, isMakerVersionOutdated } from "utils/multiAddrUtils"; -import WarningIcon from "@mui/icons-material/Warning"; -import { useAppSelector } from "store/hooks"; -import IdentIcon from "renderer/components/icons/IdentIcon"; - -/** - * A chip that displays the markup of the maker's exchange rate compared to the market rate. - */ -function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) { - const marketExchangeRate = useAppSelector((s) => s.rates?.xmrBtcRate); - if (marketExchangeRate == null) return null; - - const makerExchangeRate = satsToBtc(maker.price); - /** The markup of the exchange rate compared to the market rate in percent */ - const markup = getMarkup(makerExchangeRate, marketExchangeRate); - - return ( - - - - ); -} - -export default function MakerInfo({ maker }: { maker: ExtendedMakerStatus }) { - const isOutdated = isMakerOutdated(maker); - - return ( - - - - - - - - - - - {maker.peerId} - - - - {maker.multiAddr} - - - - - - Exchange rate:{" "} - - - - Minimum amount: - - - Maximum amount: - - - - {maker.testnet && } - {maker.uptime && ( - - - - )} - {maker.age && ( - - )} - {maker.recommended === true && ( - - } color="primary" /> - - )} - {isOutdated && ( - - } color="primary" /> - - )} - {maker.version && ( - - - - )} - - - - ); -} diff --git a/src-gui/src/renderer/components/modal/provider/MakerListDialog.tsx b/src-gui/src/renderer/components/modal/provider/MakerListDialog.tsx deleted file mode 100644 index 61c5112d16..0000000000 --- a/src-gui/src/renderer/components/modal/provider/MakerListDialog.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { - Avatar, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - List, - ListItemAvatar, - ListItemText, -} from "@mui/material"; -import AddIcon from "@mui/icons-material/Add"; -import SearchIcon from "@mui/icons-material/Search"; -import { ExtendedMakerStatus } from "models/apiModel"; -import { useState } from "react"; -import { setSelectedMaker } from "store/features/makersSlice"; -import { useAllMakers, useAppDispatch } from "store/hooks"; -import MakerInfo from "./MakerInfo"; -import MakerSubmitDialog from "./MakerSubmitDialog"; - -import ListItemButton from "@mui/material/ListItemButton"; - -type MakerSelectDialogProps = { - open: boolean; - onClose: () => void; -}; - -export function MakerSubmitDialogOpenButton() { - const [open, setOpen] = useState(false); - - return ( - { - // Prevents background from being clicked and reopening dialog - if (!open) { - setOpen(true); - } - }} - > - setOpen(false)} /> - - - - - - - - ); -} - -export default function MakerListDialog({ - open, - onClose, -}: MakerSelectDialogProps) { - const makers = useAllMakers(); - const dispatch = useAppDispatch(); - - function handleMakerChange(maker: ExtendedMakerStatus) { - dispatch(setSelectedMaker(maker)); - onClose(); - } - - return ( - - Select a maker - - - {makers.map((maker) => ( - handleMakerChange(maker)} - key={maker.peerId} - > - - - ))} - - - - - - - - ); -} diff --git a/src-gui/src/renderer/components/modal/provider/MakerSelect.tsx b/src-gui/src/renderer/components/modal/provider/MakerSelect.tsx deleted file mode 100644 index 016dd8aa31..0000000000 --- a/src-gui/src/renderer/components/modal/provider/MakerSelect.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Paper, Card, CardContent, IconButton } from "@mui/material"; -import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; -import { useState } from "react"; -import { useAppSelector } from "store/hooks"; -import MakerInfo from "./MakerInfo"; -import MakerListDialog from "./MakerListDialog"; - -export default function MakerSelect() { - const [selectDialogOpen, setSelectDialogOpen] = useState(false); - const selectedMaker = useAppSelector((state) => state.makers.selectedMaker); - - if (!selectedMaker) return <>No maker selected; - - function handleSelectDialogClose() { - setSelectDialogOpen(false); - } - - function handleSelectDialogOpen() { - setSelectDialogOpen(true); - } - - return ( - - - - - - - - - - - - ); -} diff --git a/src-gui/src/renderer/components/modal/provider/MakerSubmitDialog.tsx b/src-gui/src/renderer/components/modal/provider/MakerSubmitDialog.tsx deleted file mode 100644 index f0eb7e3b13..0000000000 --- a/src-gui/src/renderer/components/modal/provider/MakerSubmitDialog.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - TextField, -} from "@mui/material"; -import { Multiaddr } from "multiaddr"; -import { ChangeEvent, useState } from "react"; - -type MakerSubmitDialogProps = { - open: boolean; - onClose: () => void; -}; - -export default function MakerSubmitDialog({ - open, - onClose, -}: MakerSubmitDialogProps) { - const [multiAddr, setMultiAddr] = useState(""); - const [peerId, setPeerId] = useState(""); - - async function handleMakerSubmit() { - if (multiAddr && peerId) { - await fetch("https://api.unstoppableswap.net/api/submit-provider", { - method: "post", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - multiAddr, - peerId, - }), - }); - setMultiAddr(""); - setPeerId(""); - onClose(); - } - } - - function handleMultiAddrChange(event: ChangeEvent) { - setMultiAddr(event.target.value); - } - - function handlePeerIdChange(event: ChangeEvent) { - setPeerId(event.target.value); - } - - function getMultiAddressError(): string | null { - try { - const multiAddress = new Multiaddr(multiAddr); - if (multiAddress.protoNames().includes("p2p")) { - return "The multi address should not contain the peer id (/p2p/)"; - } - if (multiAddress.protoNames().find((name) => name.includes("onion"))) { - return "It is currently not possible to add a maker that is only reachable via Tor"; - } - return null; - } catch (e) { - return "Not a valid multi address"; - } - } - - return ( - - Submit a maker to the public registry - - - If the maker is valid and reachable, it will be displayed to all other - users to trade with. - - - - - - - - - - ); -} diff --git a/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx b/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx index 35b374ab8b..6131a17cd8 100644 --- a/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx +++ b/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx @@ -53,7 +53,6 @@ function getActiveStep(state: SwapState | null): PathStep | null { // Step 0: Initializing the swap // These states represent the very beginning of the swap process // No funds have been locked - case "RequestingQuote": case "ReceivedQuote": case "WaitingForBtcDeposit": case "SwapSetupInflight": diff --git a/src-gui/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx b/src-gui/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx deleted file mode 100644 index c9045fd30a..0000000000 --- a/src-gui/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { IconButton } from "@mui/material"; -import FeedbackIcon from "@mui/icons-material/Feedback"; -import { useState } from "react"; -import FeedbackDialog from "../../feedback/FeedbackDialog"; - -export default function FeedbackSubmitBadge() { - const [showFeedbackDialog, setShowFeedbackDialog] = useState(false); - - return ( - <> - {showFeedbackDialog && ( - setShowFeedbackDialog(false)} - /> - )} - setShowFeedbackDialog(true)} size="large"> - - - - ); -} diff --git a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx index 259fc859e3..e91f78fd32 100644 --- a/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx +++ b/src-gui/src/renderer/components/modal/updater/UpdaterDialog.tsx @@ -49,7 +49,7 @@ function LinearProgressWithLabel( }} > - {props.label || `${Math.round(props.value)}%`} + {props.label || `${Math.round(props.value ?? 0)}%`} @@ -84,6 +84,8 @@ export default function UpdaterDialog() { } async function handleInstall() { + if (!availableUpdate) return; + try { await availableUpdate.downloadAndInstall((event: DownloadEvent) => { if (event.event === "Started") { @@ -92,10 +94,13 @@ export default function UpdaterDialog() { downloadedBytes: 0, }); } else if (event.event === "Progress") { - setDownloadProgress((prev) => ({ - ...prev, - downloadedBytes: prev.downloadedBytes + event.data.chunkLength, - })); + setDownloadProgress((prev) => { + if (!prev) return null; + return { + contentLength: prev.contentLength, + downloadedBytes: prev.downloadedBytes + event.data.chunkLength, + }; + }); } }); @@ -110,12 +115,13 @@ export default function UpdaterDialog() { const isDownloading = downloadProgress !== null; - const progress = isDownloading - ? Math.round( - (downloadProgress.downloadedBytes / downloadProgress.contentLength) * - 100, - ) - : 0; + const progress = + isDownloading && downloadProgress.contentLength + ? Math.round( + (downloadProgress.downloadedBytes / downloadProgress.contentLength) * + 100, + ) + : 0; return ( withdrawBtc(withdrawAddress)} + onInvoke={() => sweepBtc(withdrawAddress)} onPendingChange={setPending} onSuccess={setWithdrawTxId} contextRequirement={isContextWithBitcoinWallet} diff --git a/src-gui/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx b/src-gui/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx index a6126bb332..4857f7f473 100644 --- a/src-gui/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx +++ b/src-gui/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx @@ -19,6 +19,7 @@ export default function WithdrawDialogContent({ display: "flex", flexDirection: "column", justifyContent: "space-between", + gap: 2, }} > {children} diff --git a/src-gui/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx b/src-gui/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx index cdf5d95284..cf5909de0e 100644 --- a/src-gui/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx +++ b/src-gui/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx @@ -14,7 +14,7 @@ export default function AddressInputPage({ <> To withdraw the Bitcoin inside the internal wallet, please enter an - address. All funds will be sent to that address. + address. All funds (the entire balance) will be sent to that address. diff --git a/src-gui/src/renderer/components/navigation/NavigationFooter.tsx b/src-gui/src/renderer/components/navigation/NavigationFooter.tsx index 2c31925a6e..e598e68df3 100644 --- a/src-gui/src/renderer/components/navigation/NavigationFooter.tsx +++ b/src-gui/src/renderer/components/navigation/NavigationFooter.tsx @@ -1,6 +1,5 @@ import { Box, Tooltip } from "@mui/material"; import { BackgroundProgressAlerts } from "../alert/DaemonStatusAlert"; -import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert"; import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert"; import BackgroundRefundAlert from "../alert/BackgroundRefundAlert"; import ContactInfoBox from "../other/ContactInfoBox"; @@ -15,7 +14,6 @@ export default function NavigationFooter() { gap: 1, }} > - diff --git a/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx b/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx index c321ba9b09..8211ac891c 100644 --- a/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx +++ b/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx @@ -13,7 +13,7 @@ type ModalProps = { }; type Props = { - content: string; + content: string | null; displayCopyIcon?: boolean; enableQrCode?: boolean; light?: boolean; @@ -72,6 +72,7 @@ export default function ActionableMonospaceTextBox({ const [isRevealed, setIsRevealed] = useState(!spoilerText); const handleCopy = async () => { + if (!content) return; await writeText(content); setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -92,40 +93,43 @@ export default function ActionableMonospaceTextBox({ > - - - {content} - {displayCopyIcon && ( - - - - )} - {enableQrCode && ( - - setQrCodeOpen(true)} - onMouseEnter={() => setIsQrCodeButtonHovered(true)} - onMouseLeave={() => setIsQrCodeButtonHovered(false)} - size="small" - sx={{ marginLeft: 1 }} - > - - - - )} - - + + {displayCopyIcon && ( + + + + + + )} + {enableQrCode && ( + + { + e.stopPropagation(); + setQrCodeOpen(true); + }} + onMouseEnter={() => setIsQrCodeButtonHovered(true)} + onMouseLeave={() => setIsQrCodeButtonHovered(false)} + size="small" + > + + + + )} + + } + > + {content} + @@ -157,7 +161,7 @@ export default function ActionableMonospaceTextBox({ )} - {enableQrCode && ( + {enableQrCode && content && ( setQrCodeOpen(false)} diff --git a/src-gui/src/renderer/components/other/ErrorBoundary.tsx b/src-gui/src/renderer/components/other/ErrorBoundary.tsx new file mode 100644 index 0000000000..a2c1782144 --- /dev/null +++ b/src-gui/src/renderer/components/other/ErrorBoundary.tsx @@ -0,0 +1,58 @@ +// Vendored from https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/error_boundaries/ +import React, { Component, ErrorInfo, ReactNode } from "react"; + +interface Props { + children?: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + }; + + public static getDerivedStateFromError(error: Error): State { + // Update state so the next render will show the fallback UI. + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("Uncaught error:", error, errorInfo); + } + + public render() { + if (this.state.hasError) { + return ( +
+

Sorry.. there was an error

+
+            {this.state.error?.message}
+          
+ {this.state.error?.stack && ( +
+ Stack trace +
+                {this.state.error.stack}
+              
+
+ )} +
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src-gui/src/renderer/components/other/JSONViewTree.tsx b/src-gui/src/renderer/components/other/JSONViewTree.tsx deleted file mode 100644 index fefd7b2218..0000000000 --- a/src-gui/src/renderer/components/other/JSONViewTree.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import TreeItem from "@mui/lab/TreeItem"; -import TreeView from "@mui/lab/TreeView"; -import ScrollablePaperTextBox from "./ScrollablePaperTextBox"; - -interface JsonTreeViewProps { - data: unknown; - label: string; -} - -export default function JsonTreeView({ data, label }: JsonTreeViewProps) { - const renderTree = (nodes: unknown, parentId: string) => { - return Object.keys(nodes).map((key, _) => { - const nodeId = `${parentId}.${key}`; - if (typeof nodes[key] === "object" && nodes[key] !== null) { - return ( - - {renderTree(nodes[key], nodeId)} - - ); - } - return ( - - ); - }); - }; - - return ( - } - defaultExpandIcon={} - defaultExpanded={["root"]} - > - - {renderTree(data ?? {}, "root")} - - , - ]} - /> - ); -} diff --git a/src-gui/src/renderer/components/other/LoadingButton.tsx b/src-gui/src/renderer/components/other/LoadingButton.tsx deleted file mode 100644 index aa60ee2f03..0000000000 --- a/src-gui/src/renderer/components/other/LoadingButton.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import Button, { ButtonProps } from "@mui/material/Button"; -import CircularProgress from "@mui/material/CircularProgress"; -import React from "react"; - -interface LoadingButtonProps extends ButtonProps { - loading: boolean; -} - -const LoadingButton: React.FC = ({ - loading, - disabled, - children, - ...props -}) => { - return ( - - ); -}; - -export default LoadingButton; diff --git a/src-gui/src/renderer/components/other/MonospaceTextBox.tsx b/src-gui/src/renderer/components/other/MonospaceTextBox.tsx index 6a95c499d2..bce732a0e8 100644 --- a/src-gui/src/renderer/components/other/MonospaceTextBox.tsx +++ b/src-gui/src/renderer/components/other/MonospaceTextBox.tsx @@ -3,18 +3,25 @@ import { Box, Typography } from "@mui/material"; type Props = { children: React.ReactNode; light?: boolean; + actions?: React.ReactNode; }; -export default function MonospaceTextBox({ children, light = false }: Props) { +export default function MonospaceTextBox({ + children, + light = false, + actions, +}: Props) { return ( ({ display: "flex", alignItems: "center", + justifyContent: "space-between", backgroundColor: light ? "transparent" : theme.palette.grey[900], borderRadius: 2, border: light ? `1px solid ${theme.palette.grey[800]}` : "none", padding: theme.spacing(1), + gap: 1, })} > {children} + {actions && ( + {actions} + )} ); } diff --git a/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx b/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx index f84269a3bc..548673f4e9 100644 --- a/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx +++ b/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx @@ -2,11 +2,12 @@ import { Box, Divider, IconButton, Paper, Typography } from "@mui/material"; import FileCopyOutlinedIcon from "@mui/icons-material/FileCopyOutlined"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import { ReactNode, useEffect, useRef } from "react"; +import { ReactNode, useEffect, useRef, useState } from "react"; import { VList, VListHandle } from "virtua"; import { ExpandableSearchBox } from "./ExpandableSearchBox"; const MIN_HEIGHT = "10rem"; +const NEAR_BOTTOM_THRESHOLD = 3; export default function ScrollablePaperTextBox({ rows, @@ -28,6 +29,7 @@ export default function ScrollablePaperTextBox({ autoScroll?: boolean; }) { const virtuaEl = useRef(null); + const [isNearBottom, setIsNearBottom] = useState(true); function onCopy() { navigator.clipboard.writeText(copyValue); @@ -41,11 +43,18 @@ export default function ScrollablePaperTextBox({ virtuaEl.current?.scrollToIndex(0); } + function handleRangeChange(startIndex: number, endIndex: number) { + // Check if one of the last NEAR_BOTTOM_THRESHOLD elements is visible + const lastThreeStart = Math.max(0, rows.length - NEAR_BOTTOM_THRESHOLD); + const nearBottom = endIndex >= lastThreeStart; + setIsNearBottom(nearBottom); + } + useEffect(() => { - if (autoScroll) { + if (autoScroll && isNearBottom) { scrollToBottom(); } - }, [rows.length, autoScroll]); + }, [rows.length, autoScroll, isNearBottom]); return ( - + {rows} @@ -95,7 +108,7 @@ export default function ScrollablePaperTextBox({ - {searchQuery !== undefined && setSearchQuery !== undefined && ( + {searchQuery !== null && setSearchQuery !== null && ( )} diff --git a/src-gui/src/renderer/components/other/TruncatedText.tsx b/src-gui/src/renderer/components/other/TruncatedText.tsx index b3c4c0eb4a..e0bc7f09f7 100644 --- a/src-gui/src/renderer/components/other/TruncatedText.tsx +++ b/src-gui/src/renderer/components/other/TruncatedText.tsx @@ -9,7 +9,7 @@ export default function TruncatedText({ ellipsis?: string; truncateMiddle?: boolean; }) { - let finalChildren = children ?? ""; + const finalChildren = children ?? ""; const truncatedText = finalChildren.length > limit diff --git a/src-gui/src/renderer/components/other/Units.tsx b/src-gui/src/renderer/components/other/Units.tsx index 78ab3df718..cc485acebe 100644 --- a/src-gui/src/renderer/components/other/Units.tsx +++ b/src-gui/src/renderer/components/other/Units.tsx @@ -9,7 +9,7 @@ export function AmountWithUnit({ unit, fixedPrecision, exchangeRate, - parenthesisText = null, + parenthesisText, labelStyles, amountStyles, disableTooltip = false, @@ -18,7 +18,7 @@ export function AmountWithUnit({ unit: string; fixedPrecision: number; exchangeRate?: Amount; - parenthesisText?: string; + parenthesisText?: string | null; labelStyles?: SxProps; amountStyles?: SxProps; disableTooltip?: boolean; @@ -142,7 +142,7 @@ export function MoneroBitcoinExchangeRate({ }) { const marketRate = useAppSelector((state) => state.rates?.xmrBtcRate); const markup = - displayMarkup && marketRate != null + displayMarkup && marketRate != null && rate != null ? `${getMarkup(rate, marketRate).toFixed(2)}% markup` : null; @@ -179,7 +179,7 @@ export function MoneroSatsExchangeRate({ rate: Amount; displayMarkup?: boolean; }) { - const btc = satsToBtc(rate); + const btc = rate == null ? null : satsToBtc(rate); return ; } diff --git a/src-gui/src/renderer/components/pages/help/ConversationsBox.tsx b/src-gui/src/renderer/components/pages/help/ConversationsBox.tsx index eb8d02ad42..def0072b01 100644 --- a/src-gui/src/renderer/components/pages/help/ConversationsBox.tsx +++ b/src-gui/src/renderer/components/pages/help/ConversationsBox.tsx @@ -29,7 +29,6 @@ import ChatIcon from "@mui/icons-material/Chat"; import SendIcon from "@mui/icons-material/Send"; import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox"; import TruncatedText from "renderer/components/other/TruncatedText"; -import clsx from "clsx"; import { useAppSelector, useAppDispatch, @@ -378,10 +377,6 @@ function MessageBubble({ message }: { message: Message }) { ({ padding: 1.5, - borderRadius: - typeof theme.shape.borderRadius === "number" - ? theme.shape.borderRadius * 2 - : 8, maxWidth: "75%", wordBreak: "break-word", boxShadow: theme.shadows[1], diff --git a/src-gui/src/renderer/components/pages/help/ExportDataBox.tsx b/src-gui/src/renderer/components/pages/help/ExportDataBox.tsx deleted file mode 100644 index 23eb232668..0000000000 --- a/src-gui/src/renderer/components/pages/help/ExportDataBox.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - Box, - Typography, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, - Link, - DialogContentText, -} from "@mui/material"; -import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox"; -import { useState } from "react"; -import { getWalletDescriptor } from "renderer/rpc"; -import { ExportBitcoinWalletResponse } from "models/tauriModel"; -import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; -import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox"; -import { isContextWithBitcoinWallet } from "models/tauriModelExt"; - -export default function ExportDataBox() { - const [walletDescriptor, setWalletDescriptor] = - useState(null); - - const handleCloseDialog = () => { - setWalletDescriptor(null); - }; - - return ( - - - You can export the wallet descriptor of the interal Bitcoin wallet - for backup or recovery purposes. Please make sure to store it - securely. - - - } - additionalContent={ - <> - - Reveal Bitcoin Wallet Private Key - - {walletDescriptor !== null && ( - - )} - - } - /> - ); -} - -function WalletDescriptorModal({ - open, - onClose, - walletDescriptor, -}: { - open: boolean; - onClose: () => void; - walletDescriptor: ExportBitcoinWalletResponse; -}) { - const parsedDescriptor = JSON.parse( - walletDescriptor.wallet_descriptor["descriptor"], - ); - const stringifiedDescriptor = JSON.stringify(parsedDescriptor, null, 4); - - return ( - - Bitcoin Wallet Descriptor - - -
    -
  • - The text below contains the wallet descriptor of the internal - Bitcoin wallet. It contains your private key and can be used to - derive your wallet. It should thus be stored securely. -
  • -
  • - It can be imported into other Bitcoin wallets or services that - support the descriptor format. -
  • -
  • - For more information on what to do with the descriptor, see our{" "} - - documentation - -
  • -
-
- -
- - - -
- ); -} diff --git a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx index ba71bc3365..0e8203cbf9 100644 --- a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx +++ b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx @@ -26,11 +26,9 @@ import { import { addNode, addRendezvousPoint, - Blockchain, DonateToDevelopmentTip, FiatCurrency, moveUpNode, - Network, removeNode, removeRendezvousPoint, resetSettings, @@ -48,6 +46,7 @@ import { RedeemPolicy, RefundPolicy, } from "store/features/settingsSlice"; +import { Blockchain, Network } from "store/types"; import { useAppDispatch, useNodes, useSettings } from "store/hooks"; import ValidatedTextField from "renderer/components/other/ValidatedTextField"; import HelpIcon from "@mui/icons-material/HelpOutline"; @@ -432,7 +431,7 @@ function MoneroNodeUrlSetting() { value && handleNodeUrlChange(value)} placeholder={PLACEHOLDER_MONERO_NODE_URL} disabled={useMoneroRpcPool} fullWidth @@ -675,7 +674,7 @@ function NodeTable({ setNewNode(value ?? "")} placeholder={placeholder} fullWidth isValid={isValid} @@ -843,7 +842,9 @@ function RendezvousPointsSetting() { + setNewPoint(value ?? "") + } placeholder="/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa" fullWidth isValid={isValidMultiAddressWithPeerId} diff --git a/src-gui/src/renderer/components/pages/help/SettingsPage.tsx b/src-gui/src/renderer/components/pages/help/SettingsPage.tsx index 5c684d94bd..e608167b30 100644 --- a/src-gui/src/renderer/components/pages/help/SettingsPage.tsx +++ b/src-gui/src/renderer/components/pages/help/SettingsPage.tsx @@ -1,9 +1,7 @@ import { Box } from "@mui/material"; -import ContactInfoBox from "./ContactInfoBox"; import DonateInfoBox from "./DonateInfoBox"; import DaemonControlBox from "./DaemonControlBox"; import SettingsBox from "./SettingsBox"; -import ExportDataBox from "./ExportDataBox"; import DiscoveryBox from "./DiscoveryBox"; import MoneroPoolHealthBox from "./MoneroPoolHealthBox"; import { useLocation } from "react-router-dom"; @@ -32,7 +30,6 @@ export default function SettingsPage() { - ); diff --git a/src-gui/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx b/src-gui/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx index 4d304bac6d..5e8e2a48ee 100644 --- a/src-gui/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx +++ b/src-gui/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx @@ -119,9 +119,9 @@ export function SwapMoneroRecoveryButton({ onInvoke={(): Promise => getMoneroRecoveryKeys(swap.swap_id) } - onSuccess={(keys: MoneroRecoveryResponse) => - store.dispatch(rpcSetMoneroRecoveryKeys([swap.swap_id, keys])) - } + onSuccess={(keys: MoneroRecoveryResponse) => { + store.dispatch(rpcSetMoneroRecoveryKeys([swap.swap_id, keys])); + }} {...props} > Display Monero Recovery Keys diff --git a/src-gui/src/renderer/components/pages/monero/components/ConfirmationsBadge.tsx b/src-gui/src/renderer/components/pages/monero/components/ConfirmationsBadge.tsx index cc9f27c2f3..271071dc9d 100644 --- a/src-gui/src/renderer/components/pages/monero/components/ConfirmationsBadge.tsx +++ b/src-gui/src/renderer/components/pages/monero/components/ConfirmationsBadge.tsx @@ -13,7 +13,7 @@ export default function ConfirmationsBadge({ return ( } - label="Published" + label="Unconfirmed" color="secondary" size="small" /> diff --git a/src-gui/src/renderer/components/pages/monero/components/SendAmountInput.tsx b/src-gui/src/renderer/components/pages/monero/components/SendAmountInput.tsx index 5cb44bb746..ca23c8cd8e 100644 --- a/src-gui/src/renderer/components/pages/monero/components/SendAmountInput.tsx +++ b/src-gui/src/renderer/components/pages/monero/components/SendAmountInput.tsx @@ -16,7 +16,7 @@ interface SendAmountInputProps { currency: string; onCurrencyChange: (currency: string) => void; fiatCurrency: string; - xmrPrice: number; + xmrPrice: number | null; showFiatRate: boolean; disabled?: boolean; } @@ -48,6 +48,10 @@ export default function SendAmountInput({ return "0.00"; } + if (xmrPrice === null) { + return "?"; + } + const primaryValue = parseFloat(amount); if (currency === "XMR") { // Primary is XMR, secondary is USD @@ -76,7 +80,7 @@ export default function SendAmountInput({ if (currency === "XMR") { onAmountChange(Math.max(0, maxAmountXmr).toString()); - } else { + } else if (xmrPrice !== null) { // Convert to USD for display const maxAmountUsd = maxAmountXmr * xmrPrice; onAmountChange(Math.max(0, maxAmountUsd).toString()); @@ -103,8 +107,9 @@ export default function SendAmountInput({ (currency === "XMR" ? parseFloat(amount) > piconerosToXmr(parseFloat(balance.unlocked_balance)) - : parseFloat(amount) / xmrPrice > - piconerosToXmr(parseFloat(balance.unlocked_balance))); + : xmrPrice !== null && + parseFloat(amount) / xmrPrice > + piconerosToXmr(parseFloat(balance.unlocked_balance))); return ( Available - - - - - XMR - + + + diff --git a/src-gui/src/renderer/components/pages/monero/components/SendTransactionContent.tsx b/src-gui/src/renderer/components/pages/monero/components/SendTransactionContent.tsx index e69827cb07..99f2cd8e8d 100644 --- a/src-gui/src/renderer/components/pages/monero/components/SendTransactionContent.tsx +++ b/src-gui/src/renderer/components/pages/monero/components/SendTransactionContent.tsx @@ -90,7 +90,9 @@ export default function SendTransactionContent({ const moneroAmount = currency === "XMR" ? parseFloat(sendAmount) - : parseFloat(sendAmount) / xmrPrice; + : xmrPrice !== null + ? parseFloat(sendAmount) / xmrPrice + : null; const handleSend = async () => { if (!sendAddress) { @@ -103,7 +105,7 @@ export default function SendTransactionContent({ amount: { type: "Sweep" }, }); } else { - if (!sendAmount || sendAmount === "") { + if (!sendAmount || sendAmount === "" || moneroAmount === null) { throw new Error("Amount is required"); } diff --git a/src-gui/src/renderer/components/pages/monero/components/StateIndicator.tsx b/src-gui/src/renderer/components/pages/monero/components/StateIndicator.tsx index 96e5e466a3..4f6297b88f 100644 --- a/src-gui/src/renderer/components/pages/monero/components/StateIndicator.tsx +++ b/src-gui/src/renderer/components/pages/monero/components/StateIndicator.tsx @@ -1,6 +1,6 @@ import { Box, darken, lighten, useTheme } from "@mui/material"; -function getColor(colorName: string) { +function getColor(colorName: string): string { const theme = useTheme(); switch (colorName) { case "primary": @@ -11,6 +11,8 @@ function getColor(colorName: string) { return theme.palette.success.main; case "warning": return theme.palette.warning.main; + default: + return theme.palette.primary.main; } } diff --git a/src-gui/src/renderer/components/pages/monero/components/TransactionHistory.tsx b/src-gui/src/renderer/components/pages/monero/components/TransactionHistory.tsx index 1075b69363..10adfdbfa1 100644 --- a/src-gui/src/renderer/components/pages/monero/components/TransactionHistory.tsx +++ b/src-gui/src/renderer/components/pages/monero/components/TransactionHistory.tsx @@ -5,9 +5,9 @@ import dayjs from "dayjs"; import TransactionItem from "./TransactionItem"; interface TransactionHistoryProps { - history?: { + history: { transactions: TransactionInfo[]; - }; + } | null; } interface TransactionGroup { diff --git a/src-gui/src/renderer/components/pages/monero/components/WalletOverview.tsx b/src-gui/src/renderer/components/pages/monero/components/WalletOverview.tsx index c69a98ab4b..182bc21e89 100644 --- a/src-gui/src/renderer/components/pages/monero/components/WalletOverview.tsx +++ b/src-gui/src/renderer/components/pages/monero/components/WalletOverview.tsx @@ -4,7 +4,10 @@ import { PiconeroAmount } from "../../../other/Units"; import { FiatPiconeroAmount } from "../../../other/Units"; import StateIndicator from "./StateIndicator"; import humanizeDuration from "humanize-duration"; -import { GetMoneroSyncProgressResponse } from "models/tauriModel"; +import { + GetMoneroBalanceResponse, + GetMoneroSyncProgressResponse, +} from "models/tauriModel"; interface TimeEstimationResult { blocksLeft: number; @@ -16,7 +19,7 @@ interface TimeEstimationResult { const AVG_MONERO_BLOCK_SIZE_KB = 130; function useSyncTimeEstimation( - syncProgress: GetMoneroSyncProgressResponse | undefined, + syncProgress: GetMoneroSyncProgressResponse | null, ): TimeEstimationResult | null { const poolStatus = useAppSelector((state) => state.pool.status); const restoreHeight = useAppSelector( @@ -80,11 +83,8 @@ function useSyncTimeEstimation( } interface WalletOverviewProps { - balance?: { - unlocked_balance: string; - total_balance: string; - }; - syncProgress?: GetMoneroSyncProgressResponse; + balance: GetMoneroBalanceResponse | null; + syncProgress: GetMoneroSyncProgressResponse | null; } // Component for displaying wallet address and balance @@ -99,10 +99,11 @@ export default function WalletOverview({ const poolStatus = useAppSelector((state) => state.pool.status); const timeEstimation = useSyncTimeEstimation(syncProgress); - const pendingBalance = - parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance); + const pendingBalance = balance + ? parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance) + : null; - const isSyncing = syncProgress && syncProgress.progress_percentage < 100; + const isSyncing = !!(syncProgress && syncProgress.progress_percentage < 100); // syncProgress.progress_percentage is not good to display // assuming we have an old wallet, eventually we will always only use the last few cm of the progress bar @@ -184,18 +185,18 @@ export default function WalletOverview({ - {pendingBalance > 0 && ( + {pendingBalance !== null && pendingBalance > 0 && ( - {Array.from({ length: 2 }).map((_) => ( - + {Array.from({ length: 2 }).map((_, i) => ( + ))} diff --git a/src-gui/src/renderer/components/pages/swap/swap/SwapWidget.tsx b/src-gui/src/renderer/components/pages/swap/swap/SwapWidget.tsx index e223b4efe2..0411806372 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/SwapWidget.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/SwapWidget.tsx @@ -18,7 +18,12 @@ export default function SwapWidget() { - + {swapInfo != null && ( + + )} - + + + {swap.state !== null && ( <> diff --git a/src-gui/src/renderer/components/pages/swap/swap/components/InfoBox.tsx b/src-gui/src/renderer/components/pages/swap/swap/components/InfoBox.tsx index 4f6e485b85..b7321c2676 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/components/InfoBox.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/components/InfoBox.tsx @@ -2,7 +2,7 @@ import { Box, LinearProgress, Paper, Typography } from "@mui/material"; import { ReactNode } from "react"; type Props = { - id?: string; + id?: string | null; title: ReactNode | null; mainContent: ReactNode; additionalContent: ReactNode; @@ -11,7 +11,7 @@ type Props = { }; export default function InfoBox({ - id = null, + id, title, mainContent, additionalContent, @@ -21,7 +21,7 @@ export default function InfoBox({ return ( ; -} diff --git a/src-gui/src/renderer/components/pages/swap/swap/init/deposit_and_choose_offer/DepositAndChooseOfferPage.tsx b/src-gui/src/renderer/components/pages/swap/swap/init/deposit_and_choose_offer/DepositAndChooseOfferPage.tsx index c11f615a07..33cc87a93e 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/init/deposit_and_choose_offer/DepositAndChooseOfferPage.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/init/deposit_and_choose_offer/DepositAndChooseOfferPage.tsx @@ -107,8 +107,8 @@ export default function DepositAndChooseOfferPage({ return ( ); })} diff --git a/src-gui/src/renderer/components/pages/swap/swap/init/deposit_and_choose_offer/MakerOfferItem.tsx b/src-gui/src/renderer/components/pages/swap/swap/init/deposit_and_choose_offer/MakerOfferItem.tsx index 035c1c8598..e651826a81 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/init/deposit_and_choose_offer/MakerOfferItem.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/init/deposit_and_choose_offer/MakerOfferItem.tsx @@ -111,7 +111,12 @@ export default function MakerOfferItem({ resolveApproval(requestId, true as unknown as object)} + onInvoke={() => { + if (!requestId) { + throw new Error("Request ID is required"); + } + return resolveApproval(requestId, true as unknown as object); + }} displayErrorSnackbar disabled={!requestId} tooltipTitle={ diff --git a/src-gui/src/renderer/components/pages/wallet/WalletPage.tsx b/src-gui/src/renderer/components/pages/wallet/WalletPage.tsx index d3b4d0b667..3b22e8d159 100644 --- a/src-gui/src/renderer/components/pages/wallet/WalletPage.tsx +++ b/src-gui/src/renderer/components/pages/wallet/WalletPage.tsx @@ -1,21 +1,32 @@ -import { Box, Typography } from "@mui/material"; -import { Alert } from "@mui/material"; -import WithdrawWidget from "./WithdrawWidget"; +import { Box } from "@mui/material"; +import { useAppSelector } from "store/hooks"; +import WalletOverview from "./components/WalletOverview"; +import WalletActionButtons from "./components/WalletActionButtons"; +import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox"; export default function WalletPage() { + const walletBalance = useAppSelector((state) => state.bitcoinWallet.balance); + const bitcoinAddress = useAppSelector((state) => state.bitcoinWallet.address); + return ( - - You do not have to deposit money before starting a swap. Instead, you - will be greeted with a deposit address after you initiate one. - - + + {bitcoinAddress && ( + + )} + ); } diff --git a/src-gui/src/renderer/components/pages/wallet/WithdrawWidget.tsx b/src-gui/src/renderer/components/pages/wallet/WithdrawWidget.tsx deleted file mode 100644 index 047c5e0330..0000000000 --- a/src-gui/src/renderer/components/pages/wallet/WithdrawWidget.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Box, Button, Typography } from "@mui/material"; -import SendIcon from "@mui/icons-material/Send"; -import { useState } from "react"; -import { SatsAmount } from "renderer/components/other/Units"; -import { useAppSelector } from "store/hooks"; -import BitcoinIcon from "../../icons/BitcoinIcon"; -import InfoBox from "../swap/swap/components/InfoBox"; -import WithdrawDialog from "../../modal/wallet/WithdrawDialog"; -import WalletRefreshButton from "./WalletRefreshButton"; - -export default function WithdrawWidget() { - const walletBalance = useAppSelector((state) => state.rpc.state.balance); - const [showDialog, setShowDialog] = useState(false); - - function onShowDialog() { - setShowDialog(true); - } - - return ( - <> - - Wallet Balance - - - } - mainContent={ - - - - } - icon={} - additionalContent={ - - } - loading={false} - /> - setShowDialog(false)} /> - - ); -} diff --git a/src-gui/src/renderer/components/pages/wallet/components/WalletActionButtons.tsx b/src-gui/src/renderer/components/pages/wallet/components/WalletActionButtons.tsx new file mode 100644 index 0000000000..c6ddb55131 --- /dev/null +++ b/src-gui/src/renderer/components/pages/wallet/components/WalletActionButtons.tsx @@ -0,0 +1,37 @@ +import { Box, Chip } from "@mui/material"; +import { Send as SendIcon } from "@mui/icons-material"; +import { useState } from "react"; +import { useAppSelector } from "store/hooks"; +import WithdrawDialog from "../../../modal/wallet/WithdrawDialog"; +import WalletDescriptorButton from "./WalletDescriptorButton"; + +export default function WalletActionButtons() { + const [showDialog, setShowDialog] = useState(false); + const balance = useAppSelector((state) => state.bitcoinWallet.balance); + + return ( + <> + setShowDialog(false)} /> + + + } + label="Sweep" + variant="button" + clickable + onClick={() => setShowDialog(true)} + disabled={balance === null || balance <= 0} + /> + + + + + ); +} diff --git a/src-gui/src/renderer/components/pages/wallet/components/WalletDescriptorButton.tsx b/src-gui/src/renderer/components/pages/wallet/components/WalletDescriptorButton.tsx new file mode 100644 index 0000000000..ab617c1f3d --- /dev/null +++ b/src-gui/src/renderer/components/pages/wallet/components/WalletDescriptorButton.tsx @@ -0,0 +1,108 @@ +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + DialogContentText, + Link, +} from "@mui/material"; +import { Key as KeyIcon } from "@mui/icons-material"; +import { useState } from "react"; +import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox"; +import { getWalletDescriptor } from "renderer/rpc"; +import { ExportBitcoinWalletResponse } from "models/tauriModel"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; +import { + isContextWithBitcoinWallet, + hasDescriptorProperty, +} from "models/tauriModelExt"; + +const WALLET_DESCRIPTOR_DOCS_URL = + "https://github.com/eigenwallet/core/blob/master/dev-docs/asb/README.md#exporting-the-bitcoin-wallet-descriptor"; + +export default function WalletDescriptorButton() { + const [walletDescriptor, setWalletDescriptor] = + useState(null); + + const handleCloseDialog = () => { + setWalletDescriptor(null); + }; + + return ( + <> + } + onInvoke={getWalletDescriptor} + onSuccess={setWalletDescriptor} + displayErrorSnackbar={true} + contextRequirement={isContextWithBitcoinWallet} + > + Reveal Private Key + + {walletDescriptor !== null && ( + + )} + + ); +} + +function WalletDescriptorModal({ + open, + onClose, + walletDescriptor, +}: { + open: boolean; + onClose: () => void; + walletDescriptor: ExportBitcoinWalletResponse; +}) { + if (!hasDescriptorProperty(walletDescriptor)) { + throw new Error("Wallet descriptor does not have descriptor property"); + } + + const parsedDescriptor = JSON.parse( + walletDescriptor.wallet_descriptor.descriptor, + ); + const stringifiedDescriptor = JSON.stringify(parsedDescriptor, null, 4); + + return ( + + Bitcoin Wallet Descriptor + + + The Bitcoin wallet is derived from your Monero wallet. Opening the + same Monero wallet in another Eigenwallet will yield the same Bitcoin + wallet. +
+
+ It contains your private key. Anyone who has it can spend your funds. + It should thus be stored securely. +
+
+ It can be imported into other Bitcoin wallets or services that support + the descriptor format. For more information on what to do with the + descriptor, see our{" "} + + documentation + +
+ +
+ + + +
+ ); +} diff --git a/src-gui/src/renderer/components/pages/wallet/components/WalletOverview.tsx b/src-gui/src/renderer/components/pages/wallet/components/WalletOverview.tsx new file mode 100644 index 0000000000..2f25cebb7d --- /dev/null +++ b/src-gui/src/renderer/components/pages/wallet/components/WalletOverview.tsx @@ -0,0 +1,80 @@ +import { Box, Typography, Card } from "@mui/material"; +import { BitcoinAmount } from "renderer/components/other/Units"; +import { useAppSelector, useSettings } from "store/hooks"; +import { satsToBtc } from "utils/conversionUtils"; +import WalletRefreshButton from "../WalletRefreshButton"; + +interface WalletOverviewProps { + balance: number | null; +} + +function FiatBitcoinAmount({ amount }: { amount: number | null }) { + const btcPrice = useAppSelector((state) => state.rates.btcPrice); + const [fetchFiatPrices, fiatCurrency] = useSettings((settings) => [ + settings.fetchFiatPrices, + settings.fiatCurrency, + ]); + + if ( + !fetchFiatPrices || + fiatCurrency == null || + amount == null || + btcPrice == null + ) { + return ; + } + + return ( + + {(amount * btcPrice).toFixed(2)} {fiatCurrency} + + ); +} + +export default function WalletOverview({ balance }: WalletOverviewProps) { + const btcBalance = balance == null ? null : satsToBtc(balance); + + return ( + + + {/* Left side content */} + + + Available Funds + + + + + + + + + + {/* Right side - Refresh button */} + + + + + + ); +} diff --git a/src-gui/src/renderer/components/theme.tsx b/src-gui/src/renderer/components/theme.tsx index b7f905d044..1f7119463d 100644 --- a/src-gui/src/renderer/components/theme.tsx +++ b/src-gui/src/renderer/components/theme.tsx @@ -24,7 +24,7 @@ declare module "@mui/material/styles" { tint?: string; } - interface PaletteColorOptions { + interface SimplePaletteColorOptions { tint?: string; } } @@ -61,16 +61,6 @@ const baseTheme: ThemeOptions = { backgroundColor: "color-mix(in srgb, #bdbdbd 10%, transparent)", }, }, - sizeTiny: { - fontSize: "0.75rem", - fontWeight: 500, - padding: "4px 8px", - minHeight: "24px", - minWidth: "auto", - lineHeight: 1.2, - textTransform: "none", - borderRadius: "4px", - }, }, variants: [ { diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 66a89ab15e..e4fa7f3388 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -16,6 +16,7 @@ import { WithdrawBtcResponse, GetSwapInfoArgs, ExportBitcoinWalletResponse, + GetBitcoinAddressResponse, CheckMoneroNodeArgs, CheckSeedArgs, CheckSeedResponse, @@ -47,13 +48,17 @@ import { MoneroNodeConfig, GetMoneroSeedResponse, ContextStatus, + GetSwapTimelockArgs, + GetSwapTimelockResponse, } from "models/tauriModel"; import { - rpcSetBalance, rpcSetSwapInfo, approvalRequestsReplaced, contextInitializationFailed, + timelockChangeEventReceived, } from "store/features/rpcSlice"; +import { selectAllSwapIds } from "store/selectors"; +import { setBitcoinBalance } from "store/features/bitcoinWalletSlice"; import { setMainAddress, setBalance, @@ -67,19 +72,17 @@ import { MoneroRecoveryResponse } from "models/rpcModel"; import { ListSellersResponse } from "../models/tauriModel"; import logger from "utils/logger"; import { getNetwork, isTestnet } from "store/config"; -import { - Blockchain, - DonateToDevelopmentTip, - Network, -} from "store/features/settingsSlice"; +import { DonateToDevelopmentTip } from "store/features/settingsSlice"; +import { Blockchain, Network } from "store/types"; import { setStatus } from "store/features/nodesSlice"; import { discoveredMakersByRendezvous } from "store/features/makersSlice"; import { CliLog } from "models/cliModel"; import { logsToRawString, parseLogsFromString } from "utils/parseUtils"; +import { DEFAULT_RENDEZVOUS_POINTS } from "store/defaults"; /// These are the official donation address for the eigenwallet/core project const DONATION_ADDRESS_MAINNET = - "8BR3dW2P5xu5z964Z7J9P3UT9fmzq4MLRH3qGdqHBqTAKnxv8R7B9Kd8s7r9wLdfvAKSc3ETbVRuy1uw5cX5AUic79zZMXq"; + "4A1tNBcsxhQA7NkswREXTD1QGz8mRyA7fGnCzPyTwqzKdDFMNje7iHUbGhCetfVUZa1PTuZCoPKj8gnJuRrFYJ2R2CEzqbJ"; const DONATION_ADDRESS_STAGENET = "56E274CJxTyVuuFG651dLURKyneoJ5LsSA5jMq4By9z9GBNYQKG8y5ejTYkcvZxarZW6if14ve8xXav2byK4aRnvNdKyVxp"; @@ -91,42 +94,25 @@ const DONATION_ADDRESS_STAGENET = /// - https://unstoppableswap.net/binarybaron.asc const DONATION_ADDRESS_MAINNET_SIG = ` -----BEGIN PGP SIGNED MESSAGE----- -Hash: SHA512 +Hash: SHA256 -8BR3dW2P5xu5z964Z7J9P3UT9fmzq4MLRH3qGdqHBqTAKnxv8R7B9Kd8s7r9wLdfvAKSc3ETbVRuy1uw5cX5AUic79zZMXq is our donation address (signed by binarybaron) +4A1tNBcsxhQA7NkswREXTD1QGz8mRyA7fGnCzPyTwqzKdDFMNje7iHUbGhCetfVUZa1PTuZCoPKj8gnJuRrFYJ2R2CEzqbJ is our donation address (signed by binarybaron) -----BEGIN PGP SIGNATURE----- -iHUEARYKAB0WIQQ1qETX9LVbxE4YD/GZt10+FHaibgUCaJTWUAAKCRCZt10+FHai -bsC/AQCkisePNGhApMnwJiOoF79AoSoQVmF98GIKxvLm8SHFvQEA68gb3n/Klt/v -lYP1r+qmB2kRe52F62orp40CV2jSnAM= -=gzXB +iQGzBAEBCAAdFiEEBRhGD+vsHaFKFVp7RK5vCxZqrVoFAmjxV4YACgkQRK5vCxZq +rVrFogv9F650Um1TsPlqQ+7kdobCwa7yH5uXOp1p22YaiwWGHKRU5rUSb6Ac+zI0 +3Io39VEoZufQqXqEqaiH7Q/08ABQR5r0TTPtSLNjOSEQ+ecClwv7MeF5CIXZYDdB +AlEOnlL0CPfA24GQMhfp9lvjNiTBA2NikLARWJrc1JsLrFMK5rHesv7VHJEtm/gu +We5eAuNOM2k3nAABTWzLiMJkH+G1amJmfkCKkBCk04inA6kZ5COUikMupyQDtsE4 +hrr/KrskMuXzGY+rjP6NhWqr/twKj819TrOxlYD4vK68cZP+jx9m+vSBE6mxgMbN +tBVdo9xFVCVymOYQCV8BRY8ScqP+YPNV5d6BMyDH9tvHJrGqZTNQiFhVX03Tw6mg +hccEqYP1J/TaAlFg/P4HtqsxPBZD6x3IdSxXhrJ0IjrqLpVtKyQlTZGsJuNjFWG8 +LKixaxxR7iWsyRZVCnEqCgDN8hzKZIE3Ph+kLTa4z4mTNEYyWUNeKRrFrSxKvEOK +KM0Pp53f +=O/zf -----END PGP SIGNATURE----- `; -export const PRESET_RENDEZVOUS_POINTS = [ - "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", - "/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw", - "/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU", - "/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT", - "/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp", - "/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa", - "/dns4/aswap.click/tcp/8888/p2p/12D3KooWQzW52mdsLHTMu1EPiz3APumG6vGwpCuyy494MAQoEa5X", - "/dns4/getxmr.st/tcp/8888/p2p/12D3KooWHHwiz6WDThPT8cEurstomg3kDSxzL2L8pwxfyX2fpxVk", -]; - -export async function fetchSellersAtPresetRendezvousPoints() { - await Promise.all( - PRESET_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => { - const response = await listSellersAtRendezvousPoint([rendezvousPoint]); - store.dispatch(discoveredMakersByRendezvous(response.sellers)); - - logger.info( - `Discovered ${response.sellers.length} sellers at rendezvous point ${rendezvousPoint} during startup fetch`, - ); - }), - ); -} - async function invoke( command: string, args: ARGS, @@ -140,6 +126,19 @@ async function invokeNoArgs(command: string): Promise { return invokeUnsafe(command) as Promise; } +export async function fetchSellersAtPresetRendezvousPoints() { + await Promise.all( + DEFAULT_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => { + const response = await listSellersAtRendezvousPoint([rendezvousPoint]); + store.dispatch(discoveredMakersByRendezvous(response.sellers)); + + logger.info( + `Discovered ${response.sellers.length} sellers at rendezvous point ${rendezvousPoint} during startup fetch`, + ); + }), + ); +} + export async function checkBitcoinBalance() { // If we are already syncing, don't start a new sync if ( @@ -159,51 +158,7 @@ export async function checkBitcoinBalance() { force_refresh: true, }); - store.dispatch(rpcSetBalance(response.balance)); -} - -export async function cheapCheckBitcoinBalance() { - const response = await invoke("get_balance", { - force_refresh: false, - }); - - store.dispatch(rpcSetBalance(response.balance)); -} - -export async function getAllSwapInfos() { - const response = - await invokeNoArgs("get_swap_infos_all"); - - response.forEach((swapInfo) => { - store.dispatch(rpcSetSwapInfo(swapInfo)); - }); -} - -export async function getSwapInfo(swapId: string) { - const response = await invoke( - "get_swap_info", - { - swap_id: swapId, - }, - ); - - store.dispatch(rpcSetSwapInfo(response)); -} - -export async function withdrawBtc(address: string): Promise { - const response = await invoke( - "withdraw_btc", - { - address, - amount: null, - }, - ); - - // We check the balance, this is cheap and does not sync the wallet - // but instead uses our local cached balance - await cheapCheckBitcoinBalance(); - - return response.txid; + store.dispatch(setBitcoinBalance(response.balance)); } export async function buyXmr() { @@ -242,7 +197,13 @@ export async function buyXmr() { address_pool.push( { - address: moneroReceiveAddress, + // We need to assert this as being not null even though it can be null + // + // This is correct because a LabeledMoneroAddress can actually have a null address but + // typeshare cannot express that yet (easily) + // + // TODO: Let typescript do its job here and not assert it + address: moneroReceiveAddress!, percentage: 1 - donationPercentage, label: "Your wallet", }, @@ -254,18 +215,181 @@ export async function buyXmr() { ); } else { address_pool.push({ - address: moneroReceiveAddress, + // We need to assert this as being not null even though it can be null + // + // This is correct because a LabeledMoneroAddress can actually have a null address but + // typeshare cannot express that yet (easily) + // + // TODO: Let typescript do its job here and not assert it + address: moneroReceiveAddress!, percentage: 1, label: "Your wallet", }); } await invoke("buy_xmr", { - rendezvous_points: PRESET_RENDEZVOUS_POINTS, + rendezvous_points: DEFAULT_RENDEZVOUS_POINTS, sellers, monero_receive_pool: address_pool, - bitcoin_change_address: bitcoinChangeAddress, + // We convert null to undefined because typescript + // expects undefined if the field is optional and does not accept null here + bitcoin_change_address: bitcoinChangeAddress ?? undefined, + }); +} + +export async function initializeContext() { + const network = getNetwork(); + const testnet = isTestnet(); + const useTor = store.getState().settings.enableTor; + + // Get all Bitcoin nodes without checking availability + // The backend ElectrumBalancer will handle load balancing and failover + const bitcoinNodes = + store.getState().settings.nodes[network][Blockchain.Bitcoin]; + + // For Monero nodes, determine whether to use pool or custom node + const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool; + + const useMoneroTor = store.getState().settings.enableMoneroTor; + + const moneroNodeUrl = + store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null; + + // Check the state of the Monero node + const moneroNodeConfig = + useMoneroRpcPool || + moneroNodeUrl == null || + !(await getMoneroNodeStatus(moneroNodeUrl, network)) + ? { type: "Pool" as const } + : { + type: "SingleNode" as const, + content: { + url: moneroNodeUrl, + }, + }; + + // Initialize Tauri settings + const tauriSettings: TauriSettings = { + electrum_rpc_urls: bitcoinNodes, + monero_node_config: moneroNodeConfig, + use_tor: useTor, + enable_monero_tor: useMoneroTor, + }; + + logger.info({ tauriSettings }, "Initializing context with settings"); + + try { + await invokeUnsafe("initialize_context", { + settings: tauriSettings, + testnet, + }); + logger.info("Initialized context"); + } catch (error) { + throw new Error(String(error)); + } +} + +export async function updateAllNodeStatuses() { + const network = getNetwork(); + const settings = store.getState().settings; + + // We pass all electrum servers to the backend without checking them (ElectrumBalancer handles failover), + // but check these anyway since the status appears in the GUI. + // Only check Monero nodes if we're using custom nodes (not RPC pool). + await Promise.all( + (settings.useMoneroRpcPool + ? [Blockchain.Bitcoin] + : [Blockchain.Bitcoin, Blockchain.Monero] + ) + .map((blockchain) => + settings.nodes[network][blockchain].map((node) => + updateNodeStatus(node, blockchain, network), + ), + ) + .flat(), + ); +} + +export async function cheapCheckBitcoinBalance() { + const response = await invoke("get_balance", { + force_refresh: false, }); + + store.dispatch(setBitcoinBalance(response.balance)); +} + +export async function getBitcoinAddress() { + const response = await invokeNoArgs( + "get_bitcoin_address", + ); + + return response.address; +} + +export async function getAllSwapInfos() { + const response = + await invokeNoArgs("get_swap_infos_all"); + + response.forEach((swapInfo) => { + store.dispatch(rpcSetSwapInfo(swapInfo)); + }); +} + +export async function getSwapInfo(swapId: string) { + const response = await invoke( + "get_swap_info", + { + swap_id: swapId, + }, + ); + + store.dispatch(rpcSetSwapInfo(response)); +} + +export async function getSwapTimelock(swapId: string) { + const response = await invoke( + "get_swap_timelock", + { + swap_id: swapId, + }, + ); + + store.dispatch( + timelockChangeEventReceived({ + swap_id: response.swap_id, + timelock: response.timelock, + }), + ); +} + +export async function getAllSwapTimelocks() { + const swapIds = selectAllSwapIds(store.getState()); + + await Promise.all( + swapIds.map(async (swapId) => { + try { + await getSwapTimelock(swapId); + } catch (error) { + logger.debug(`Failed to fetch timelock for swap ${swapId}: ${error}`); + } + }), + ); +} + +export async function sweepBtc(address: string): Promise { + const response = await invoke( + "withdraw_btc", + { + address, + amount: undefined, + }, + ); + + // We check the balance, this is cheap and does not sync the wallet + // but instead uses our local cached balance + await cheapCheckBitcoinBalance(); + + return response.txid; } export async function resumeSwap(swapId: string) { @@ -326,58 +450,6 @@ export async function listSellersAtRendezvousPoint( }); } -export async function initializeContext() { - const network = getNetwork(); - const testnet = isTestnet(); - const useTor = store.getState().settings.enableTor; - - // Get all Bitcoin nodes without checking availability - // The backend ElectrumBalancer will handle load balancing and failover - const bitcoinNodes = - store.getState().settings.nodes[network][Blockchain.Bitcoin]; - - // For Monero nodes, determine whether to use pool or custom node - const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool; - - const useMoneroTor = store.getState().settings.enableMoneroTor; - - const moneroNodeUrl = - store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null; - - // Check the state of the Monero node - const moneroNodeConfig = - useMoneroRpcPool || - moneroNodeUrl == null || - !(await getMoneroNodeStatus(moneroNodeUrl, network)) - ? { type: "Pool" as const } - : { - type: "SingleNode" as const, - content: { - url: moneroNodeUrl, - }, - }; - - // Initialize Tauri settings - const tauriSettings: TauriSettings = { - electrum_rpc_urls: bitcoinNodes, - monero_node_config: moneroNodeConfig, - use_tor: useTor, - enable_monero_tor: useMoneroTor, - }; - - logger.info({ tauriSettings }, "Initializing context with settings"); - - try { - await invokeUnsafe("initialize_context", { - settings: tauriSettings, - testnet, - }); - logger.info("Initialized context"); - } catch (error) { - throw new Error(error); - } -} - export async function getWalletDescriptor() { return await invokeNoArgs( "get_wallet_descriptor", @@ -435,21 +507,6 @@ async function updateNodeStatus( store.dispatch(setStatus({ node, status, blockchain })); } -export async function updateAllNodeStatuses() { - const network = getNetwork(); - const settings = store.getState().settings; - - // Only check Monero nodes if we're using custom nodes (not RPC pool) - // Skip Bitcoin nodes since we pass all electrum servers to the backend without checking them (ElectrumBalancer handles failover) - if (!settings.useMoneroRpcPool) { - await Promise.all( - settings.nodes[network][Blockchain.Monero].map((node) => - updateNodeStatus(node, Blockchain.Monero, network), - ), - ); - } -} - export async function getMoneroAddresses(): Promise { return await invokeNoArgs("get_monero_addresses"); } diff --git a/src-gui/src/store/combinedReducer.ts b/src-gui/src/store/combinedReducer.ts index ca868b1988..3e1514654c 100644 --- a/src-gui/src/store/combinedReducer.ts +++ b/src-gui/src/store/combinedReducer.ts @@ -8,6 +8,7 @@ import nodesSlice from "./features/nodesSlice"; import conversationsSlice from "./features/conversationsSlice"; import poolSlice from "./features/poolSlice"; import walletSlice from "./features/walletSlice"; +import bitcoinWalletSlice from "./features/bitcoinWalletSlice"; import logsSlice from "./features/logsSlice"; export const reducers = { @@ -21,5 +22,6 @@ export const reducers = { conversations: conversationsSlice, pool: poolSlice, wallet: walletSlice, + bitcoinWallet: bitcoinWalletSlice, logs: logsSlice, }; diff --git a/src-gui/src/store/config.ts b/src-gui/src/store/config.ts index 4b77152aaf..7d7f274f3c 100644 --- a/src-gui/src/store/config.ts +++ b/src-gui/src/store/config.ts @@ -1,7 +1,7 @@ import { ExtendedMakerStatus } from "models/apiModel"; import { splitPeerIdFromMultiAddress } from "utils/parseUtils"; import { CliMatches, getMatches } from "@tauri-apps/plugin-cli"; -import { Network } from "./features/settingsSlice"; +import { Network } from "./types"; let matches: CliMatches; try { @@ -9,6 +9,7 @@ try { } catch { matches = { args: {}, + subcommand: null, }; } diff --git a/src-gui/src/store/defaults.ts b/src-gui/src/store/defaults.ts new file mode 100644 index 0000000000..e09f5cbe49 --- /dev/null +++ b/src-gui/src/store/defaults.ts @@ -0,0 +1,62 @@ +import { Network, Blockchain } from "./types"; + +export const DEFAULT_RENDEZVOUS_POINTS = [ + "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", + "/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw", + "/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU", + "/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT", + "/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp", + "/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa", + "/dns4/aswap.click/tcp/8888/p2p/12D3KooWQzW52mdsLHTMu1EPiz3APumG6vGwpCuyy494MAQoEa5X", + "/dns4/getxmr.st/tcp/8888/p2p/12D3KooWHHwiz6WDThPT8cEurstomg3kDSxzL2L8pwxfyX2fpxVk", +]; + +// Known broken nodes to remove when applying defaults +export const NEGATIVE_NODES_MAINNET = [ + "tcp://electrum.blockstream.info:50001", + "tcp://electrum.coinucopia.io:50001", + "tcp://se-mma-crypto-payments-001.mullvad.net:50001", + "tcp://electrum2.bluewallet.io:50777", +]; + +export const NEGATIVE_NODES_TESTNET = [ + "ssl://ax101.blockeng.ch:60002", + "tcp://electrum.blockstream.info:60001", + "tcp://blockstream.info:143", + "ssl://testnet.qtornado.com:50002", + "ssl://testnet.qtornado.com:51002", + "tcp://testnet.qtornado.com:51001", +]; + +export const DEFAULT_NODES: Record> = { + [Network.Testnet]: { + [Blockchain.Bitcoin]: [ + "ssl://blackie.c3-soft.com:57006", + "ssl://v22019051929289916.bestsrv.de:50002", + "tcp://v22019051929289916.bestsrv.de:50001", + "ssl://electrum.blockstream.info:60002", + "ssl://blockstream.info:993", + "tcp://testnet.aranguren.org:51001", + "ssl://testnet.aranguren.org:51002", + "ssl://bitcoin.devmole.eu:5010", + "tcp://bitcoin.devmole.eu:5000", + ], + [Blockchain.Monero]: [], + }, + [Network.Mainnet]: { + [Blockchain.Bitcoin]: [ + "ssl://electrum.blockstream.info:50002", + "ssl://bitcoin.stackwallet.com:50002", + "ssl://b.1209k.com:50002", + "ssl://mainnet.foundationdevices.com:50002", + "tcp://bitcoin.lu.ke:50001", + "ssl://electrum.coinfinity.co:50002", + "tcp://electrum1.bluewallet.io:50001", + "tcp://electrum2.bluewallet.io:50001", + "tcp://electrum3.bluewallet.io:50001", + "ssl://btc-electrum.cakewallet.com:50002", + "tcp://bitcoin.aranguren.org:50001", + ], + [Blockchain.Monero]: [], + }, +}; diff --git a/src-gui/src/store/features/bitcoinWalletSlice.ts b/src-gui/src/store/features/bitcoinWalletSlice.ts new file mode 100644 index 0000000000..1f14804f8a --- /dev/null +++ b/src-gui/src/store/features/bitcoinWalletSlice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface BitcoinWalletState { + address: string | null; + balance: number | null; +} + +const initialState: BitcoinWalletState = { + address: null, + balance: null, +}; + +export const bitcoinWalletSlice = createSlice({ + name: "bitcoinWallet", + initialState, + reducers: { + setBitcoinAddress(state, action: PayloadAction) { + state.address = action.payload; + }, + setBitcoinBalance(state, action: PayloadAction) { + state.balance = action.payload; + }, + resetBitcoinWalletState(state) { + return initialState; + }, + }, +}); + +export const { setBitcoinAddress, setBitcoinBalance, resetBitcoinWalletState } = + bitcoinWalletSlice.actions; + +export default bitcoinWalletSlice.reducer; diff --git a/src-gui/src/store/features/makersSlice.ts b/src-gui/src/store/features/makersSlice.ts index 16221b4d82..73b6ed19bd 100644 --- a/src-gui/src/store/features/makersSlice.ts +++ b/src-gui/src/store/features/makersSlice.ts @@ -30,31 +30,6 @@ const initialState: MakersSlice = { selectedMaker: null, }; -function selectNewSelectedMaker( - slice: MakersSlice, - peerId?: string, -): MakerStatus { - const selectedPeerId = peerId || slice.selectedMaker?.peerId; - - // Check if we still have a record of the currently selected provider - const currentMaker = - slice.registry.makers?.find((prov) => prov.peerId === selectedPeerId) || - slice.rendezvous.makers.find((prov) => prov.peerId === selectedPeerId); - - // If the currently selected provider is not outdated, keep it - if (currentMaker != null && !isMakerOutdated(currentMaker)) { - return currentMaker; - } - - // Otherwise we'd prefer to switch to a provider that has the newest version - const providers = [ - ...(slice.registry.makers ?? []), - ...(slice.rendezvous.makers ?? []), - ]; - - return providers.at(0) || null; -} - export const makersSlice = createSlice({ name: "providers", initialState, @@ -83,32 +58,15 @@ export const makersSlice = createSlice({ slice.rendezvous.makers.push(discoveredMakerStatus); } }); - - // Sort the provider list and select a new provider if needed - slice.selectedMaker = selectNewSelectedMaker(slice); }, setRegistryMakers(slice, action: PayloadAction) { if (stubTestnetMaker) { action.payload.push(stubTestnetMaker); } - - // Sort the provider list and select a new provider if needed - slice.selectedMaker = selectNewSelectedMaker(slice); }, registryConnectionFailed(slice) { slice.registry.connectionFailsCount += 1; }, - setSelectedMaker( - slice, - action: PayloadAction<{ - peerId: string; - }>, - ) { - slice.selectedMaker = selectNewSelectedMaker( - slice, - action.payload.peerId, - ); - }, }, }); @@ -116,7 +74,6 @@ export const { discoveredMakersByRendezvous, setRegistryMakers, registryConnectionFailed, - setSelectedMaker, } = makersSlice.actions; export default makersSlice.reducer; diff --git a/src-gui/src/store/features/nodesSlice.ts b/src-gui/src/store/features/nodesSlice.ts index 18086d0c9c..109bbb3cd0 100644 --- a/src-gui/src/store/features/nodesSlice.ts +++ b/src-gui/src/store/features/nodesSlice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { Blockchain } from "./settingsSlice"; +import { Blockchain } from "../types"; export interface NodesSlice { nodes: Record>; diff --git a/src-gui/src/store/features/rpcSlice.ts b/src-gui/src/store/features/rpcSlice.ts index 74a30e613e..3c4c3b6593 100644 --- a/src-gui/src/store/features/rpcSlice.ts +++ b/src-gui/src/store/features/rpcSlice.ts @@ -8,18 +8,21 @@ import { ApprovalRequest, TauriBackgroundProgressWrapper, TauriBackgroundProgress, + ExpiredTimelocks, } from "models/tauriModel"; import { MoneroRecoveryResponse } from "../../models/rpcModel"; import { GetSwapInfoResponseExt } from "models/tauriModelExt"; import logger from "utils/logger"; interface State { - balance: number | null; withdrawTxId: string | null; rendezvousDiscoveredSellers: (ExtendedMakerStatus | MakerStatus)[]; swapInfos: { [swapId: string]: GetSwapInfoResponseExt; }; + swapTimelocks: { + [swapId: string]: ExpiredTimelocks; + }; moneroRecovery: { swapId: string; keys: MoneroRecoveryResponse; @@ -54,10 +57,10 @@ export interface RPCSlice { const initialState: RPCSlice = { status: null, state: { - balance: null, withdrawTxId: null, rendezvousDiscoveredSellers: [], swapInfos: {}, + swapTimelocks: {}, moneroRecovery: null, background: {}, backgroundRefund: null, @@ -86,18 +89,11 @@ export const rpcSlice = createSlice({ slice: RPCSlice, action: PayloadAction, ) { - if (slice.state.swapInfos[action.payload.swap_id]) { - slice.state.swapInfos[action.payload.swap_id].timelock = + if (action.payload.timelock) { + slice.state.swapTimelocks[action.payload.swap_id] = action.payload.timelock; - } else { - logger.warn( - `Received timelock change event for unknown swap ${action.payload.swap_id}`, - ); } }, - rpcSetBalance(slice, action: PayloadAction) { - slice.state.balance = action.payload; - }, rpcSetWithdrawTxId(slice, action: PayloadAction) { slice.state.withdrawTxId = action.payload; }, @@ -177,7 +173,6 @@ export const rpcSlice = createSlice({ export const { contextStatusEventReceived, contextInitializationFailed, - rpcSetBalance, rpcSetWithdrawTxId, rpcResetWithdrawTxId, rpcSetRendezvousDiscoveredMakers, diff --git a/src-gui/src/store/features/settingsSlice.ts b/src-gui/src/store/features/settingsSlice.ts index ea32c6c4b6..45059613fd 100644 --- a/src-gui/src/store/features/settingsSlice.ts +++ b/src-gui/src/store/features/settingsSlice.ts @@ -1,16 +1,11 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Theme } from "renderer/components/theme"; +import { DEFAULT_NODES, DEFAULT_RENDEZVOUS_POINTS } from "../defaults"; +import { Network, Blockchain } from "../types"; export type DonateToDevelopmentTip = false | 0.0005 | 0.0075; -const DEFAULT_RENDEZVOUS_POINTS = [ - "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", - "/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw", - "/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU", - "/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT", - "/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp", - "/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa", -]; +const MIN_TIME_BETWEEN_DEFAULT_NODES_APPLY = 14 * 24 * 60 * 60 * 1000; // 14 days export interface SettingsState { /// This is an ordered list of node urls for each network and blockchain @@ -42,6 +37,8 @@ export interface SettingsState { externalMoneroRedeemAddress: string; /// The external Bitcoin refund address externalBitcoinRefundAddress: string; + /// UTC timestamp (in milliseconds) when default nodes were last applied + lastAppliedDefaultNodes?: number | null; } export enum RedeemPolicy { @@ -104,53 +101,8 @@ export enum FiatCurrency { Zar = "ZAR", } -export enum Network { - Testnet = "testnet", - Mainnet = "mainnet", -} - -export enum Blockchain { - Bitcoin = "bitcoin", - Monero = "monero", -} - const initialState: SettingsState = { - nodes: { - [Network.Testnet]: { - [Blockchain.Bitcoin]: [ - "ssl://ax101.blockeng.ch:60002", - "ssl://blackie.c3-soft.com:57006", - "ssl://v22019051929289916.bestsrv.de:50002", - "tcp://v22019051929289916.bestsrv.de:50001", - "tcp://electrum.blockstream.info:60001", - "ssl://electrum.blockstream.info:60002", - "ssl://blockstream.info:993", - "tcp://blockstream.info:143", - "ssl://testnet.qtornado.com:51002", - "tcp://testnet.qtornado.com:51001", - "tcp://testnet.aranguren.org:51001", - "ssl://testnet.aranguren.org:51002", - "ssl://testnet.qtornado.com:50002", - "ssl://bitcoin.devmole.eu:5010", - "tcp://bitcoin.devmole.eu:5000", - ], - [Blockchain.Monero]: [], - }, - [Network.Mainnet]: { - [Blockchain.Bitcoin]: [ - "ssl://electrum.blockstream.info:50002", - "tcp://electrum.blockstream.info:50001", - "ssl://bitcoin.stackwallet.com:50002", - "ssl://b.1209k.com:50002", - "tcp://electrum.coinucopia.io:50001", - "ssl://mainnet.foundationdevices.com:50002", - "tcp://bitcoin.lu.ke:50001", - "tcp://se-mma-crypto-payments-001.mullvad.net:50001", - "ssl://electrum.coinfinity.co:50002", - ], - [Blockchain.Monero]: [], - }, - }, + nodes: DEFAULT_NODES, theme: Theme.Dark, fetchFiatPrices: false, fiatCurrency: FiatCurrency.Usd, @@ -158,12 +110,14 @@ const initialState: SettingsState = { enableMoneroTor: false, // Default to not routing Monero traffic through Tor useMoneroRpcPool: true, // Default to using RPC pool userHasSeenIntroduction: false, + // TODO: Apply these regularly (like the default nodes) rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS, donateToDevelopment: false, // Default to no donation moneroRedeemPolicy: RedeemPolicy.Internal, bitcoinRefundPolicy: RefundPolicy.Internal, externalMoneroRedeemAddress: "", externalBitcoinRefundAddress: "", + lastAppliedDefaultNodes: null, }; const alertsSlice = createSlice({ @@ -273,6 +227,63 @@ const alertsSlice = createSlice({ setBitcoinRefundAddress(slice, action: PayloadAction) { slice.externalBitcoinRefundAddress = action.payload; }, + applyDefaultNodes( + slice, + action: PayloadAction<{ + defaultNodes: Record>; + negativeNodesMainnet: string[]; + negativeNodesTestnet: string[]; + }>, + ) { + const now = Date.now(); + const twoWeeksInMs = 14 * 24 * 60 * 60 * 1000; + + // Check if we should apply defaults (first time or more than 2 weeks) + if ( + slice.lastAppliedDefaultNodes == null || + now - slice.lastAppliedDefaultNodes > + MIN_TIME_BETWEEN_DEFAULT_NODES_APPLY + ) { + // Remove negative nodes from mainnet + slice.nodes[Network.Mainnet][Blockchain.Bitcoin] = slice.nodes[ + Network.Mainnet + ][Blockchain.Bitcoin].filter( + (node) => !action.payload.negativeNodesMainnet.includes(node), + ); + + // Remove negative nodes from testnet + slice.nodes[Network.Testnet][Blockchain.Bitcoin] = slice.nodes[ + Network.Testnet + ][Blockchain.Bitcoin].filter( + (node) => !action.payload.negativeNodesTestnet.includes(node), + ); + + // Add new default nodes if they don't exist (mainnet) + action.payload.defaultNodes[Network.Mainnet][ + Blockchain.Bitcoin + ].forEach((node) => { + if ( + !slice.nodes[Network.Mainnet][Blockchain.Bitcoin].includes(node) + ) { + slice.nodes[Network.Mainnet][Blockchain.Bitcoin].push(node); + } + }); + + // Add new default nodes if they don't exist (testnet) + action.payload.defaultNodes[Network.Testnet][ + Blockchain.Bitcoin + ].forEach((node) => { + if ( + !slice.nodes[Network.Testnet][Blockchain.Bitcoin].includes(node) + ) { + slice.nodes[Network.Testnet][Blockchain.Bitcoin].push(node); + } + }); + + // Update the timestamp + slice.lastAppliedDefaultNodes = now; + } + }, }, }); @@ -295,6 +306,7 @@ export const { setBitcoinRefundPolicy, setMoneroRedeemAddress, setBitcoinRefundAddress, + applyDefaultNodes, } = alertsSlice.actions; export default alertsSlice.reducer; diff --git a/src-gui/src/store/features/walletSlice.ts b/src-gui/src/store/features/walletSlice.ts index 73be631975..788abd7bb5 100644 --- a/src-gui/src/store/features/walletSlice.ts +++ b/src-gui/src/store/features/walletSlice.ts @@ -48,8 +48,8 @@ export const walletSlice = createSlice({ slice.state.lowestCurrentBlock = Math.min( // We ignore anything below 10 blocks as this may be something like wallet2 // sending a wrong value when it hasn't initialized yet - slice.state.lowestCurrentBlock < 10 || - slice.state.lowestCurrentBlock === null + slice.state.lowestCurrentBlock === null || + slice.state.lowestCurrentBlock < 10 ? Infinity : slice.state.lowestCurrentBlock, action.payload.current_block, diff --git a/src-gui/src/store/hooks.ts b/src-gui/src/store/hooks.ts index df6db1400f..77a6b46c04 100644 --- a/src-gui/src/store/hooks.ts +++ b/src-gui/src/store/hooks.ts @@ -32,6 +32,11 @@ import { } from "models/tauriModel"; import { Alert } from "models/apiModel"; import { fnv1a } from "utils/hash"; +import { + selectAllSwapInfos, + selectPendingApprovals, + selectSwapInfoWithTimelock, +} from "./selectors"; export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; @@ -159,8 +164,8 @@ export function useAllMakers() { /// This hook returns the all swap infos, as an array /// Excluding those who are in a state where it's better to hide them from the user export function useSaneSwapInfos() { - const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos); - return Object.values(swapInfos).filter((swap) => { + const swapInfos = useAppSelector(selectAllSwapInfos); + return swapInfos.filter((swap) => { // We hide swaps that are in the SwapSetupCompleted state // This is because they are probably ones where: // 1. The user force stopped the swap while we were waiting for their confirmation of the offer @@ -203,10 +208,7 @@ export function useNodes(selector: (nodes: NodesSlice) => T): T { } export function usePendingApprovals(): PendingApprovalRequest[] { - const approvals = useAppSelector((state) => state.rpc.state.approvalRequests); - return Object.values(approvals).filter( - (c) => c.request_status.state === "Pending", - ) as PendingApprovalRequest[]; + return useAppSelector(selectPendingApprovals) as PendingApprovalRequest[]; } export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] { @@ -251,7 +253,11 @@ export function useBitcoinSyncProgress(): TauriBitcoinSyncProgress[] { const syncingProcesses = pendingProcesses .map(([_, c]) => c) .filter(isBitcoinSyncProgress); - return syncingProcesses.map((c) => c.progress.content); + return syncingProcesses + .map((c) => c.progress.content) + .filter( + (content): content is TauriBitcoinSyncProgress => content !== undefined, + ); } export function isSyncingBitcoin(): boolean { diff --git a/src-gui/src/store/middleware/storeListener.ts b/src-gui/src/store/middleware/storeListener.ts index 5a961a2454..b0917d478b 100644 --- a/src-gui/src/store/middleware/storeListener.ts +++ b/src-gui/src/store/middleware/storeListener.ts @@ -2,10 +2,13 @@ import { createListenerMiddleware } from "@reduxjs/toolkit"; import { throttle, debounce } from "lodash"; import { getAllSwapInfos, + getAllSwapTimelocks, checkBitcoinBalance, + getBitcoinAddress, updateAllNodeStatuses, fetchSellersAtPresetRendezvousPoints, getSwapInfo, + getSwapTimelock, initializeMoneroWallet, changeMoneroNode, getCurrentMoneroNodeConfig, @@ -20,9 +23,8 @@ import { setFetchFiatPrices, setFiatCurrency, setUseMoneroRpcPool, - Blockchain, - Network, } from "store/features/settingsSlice"; +import { Blockchain, Network } from "store/types"; import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api"; import { RootState, store } from "renderer/store/storeRenderer"; import { swapProgressEventReceived } from "store/features/swapSlice"; @@ -30,6 +32,7 @@ import { addFeedbackId, setConversation, } from "store/features/conversationsSlice"; +import { setBitcoinAddress } from "store/features/bitcoinWalletSlice"; // Create a Map to store throttled functions per swap_id const throttledGetSwapInfoFunctions = new Map< @@ -44,7 +47,12 @@ const getThrottledSwapInfoUpdater = (swapId: string) => { // but will wait for 3 seconds of quiet during rapid calls (using debounce) const debouncedGetSwapInfo = debounce(() => { logger.debug(`Executing getSwapInfo for swap ${swapId}`); - getSwapInfo(swapId); + getSwapInfo(swapId).catch((error) => { + logger.debug(`Failed to fetch swap info for swap ${swapId}: ${error}`); + }); + getSwapTimelock(swapId).catch((error) => { + logger.debug(`Failed to fetch timelock for swap ${swapId}: ${error}`); + }); }, 3000); // 3 seconds debounce for rapid calls const throttledFunction = throttle(debouncedGetSwapInfo, 2000, { @@ -86,15 +94,21 @@ export function createMainListeners() { if (!status) return; - // If the Bitcoin wallet just came available, check the Bitcoin balance + // If the Bitcoin wallet just came available, check the Bitcoin balance and get address if ( status.bitcoin_wallet_available && !previousContextStatus?.bitcoin_wallet_available ) { logger.info( - "Bitcoin wallet just became available, checking balance...", + "Bitcoin wallet just became available, checking balance and getting address...", ); await checkBitcoinBalance(); + try { + const address = await getBitcoinAddress(); + store.dispatch(setBitcoinAddress(address)); + } catch (error) { + logger.error("Failed to fetch Bitcoin address", error); + } } // If the Monero wallet just came available, initialize the Monero wallet @@ -123,6 +137,7 @@ export function createMainListeners() { "Database & Bitcoin wallet just became available, fetching swap infos...", ); await getAllSwapInfos(); + await getAllSwapTimelocks(); } // If the database just became availiable, fetch sellers at preset rendezvous points diff --git a/src-gui/src/store/selectors.ts b/src-gui/src/store/selectors.ts new file mode 100644 index 0000000000..d1b1e15625 --- /dev/null +++ b/src-gui/src/store/selectors.ts @@ -0,0 +1,49 @@ +import { createSelector } from "@reduxjs/toolkit"; +import { RootState } from "renderer/store/storeRenderer"; +import { GetSwapInfoResponseExt } from "models/tauriModelExt"; +import { ExpiredTimelocks } from "models/tauriModel"; + +const selectRpcState = (state: RootState) => state.rpc.state; + +export const selectAllSwapIds = createSelector([selectRpcState], (rpcState) => + Object.keys(rpcState.swapInfos), +); + +export const selectAllSwapInfos = createSelector([selectRpcState], (rpcState) => + Object.values(rpcState.swapInfos), +); + +export const selectSwapTimelocks = createSelector( + [selectRpcState], + (rpcState) => rpcState.swapTimelocks, +); + +export const selectSwapTimelock = (swapId: string | null) => + createSelector([selectSwapTimelocks], (timelocks) => + swapId ? (timelocks[swapId] ?? null) : null, + ); + +export const selectSwapInfoWithTimelock = (swapId: string) => + createSelector( + [selectRpcState], + ( + rpcState, + ): + | (GetSwapInfoResponseExt & { timelock: ExpiredTimelocks | null }) + | null => { + const swapInfo = rpcState.swapInfos[swapId]; + if (!swapInfo) return null; + return { + ...swapInfo, + timelock: rpcState.swapTimelocks[swapId] ?? null, + }; + }, + ); + +export const selectPendingApprovals = createSelector( + [selectRpcState], + (rpcState) => + Object.values(rpcState.approvalRequests).filter( + (c) => c.request_status.state === "Pending", + ), +); diff --git a/src-gui/src/store/types.ts b/src-gui/src/store/types.ts new file mode 100644 index 0000000000..ec6a46c733 --- /dev/null +++ b/src-gui/src/store/types.ts @@ -0,0 +1,9 @@ +export enum Network { + Testnet = "testnet", + Mainnet = "mainnet", +} + +export enum Blockchain { + Bitcoin = "bitcoin", + Monero = "monero", +} diff --git a/src-gui/src/utils/cryptoUtils.ts b/src-gui/src/utils/cryptoUtils.ts deleted file mode 100644 index 1e6a4c80d3..0000000000 --- a/src-gui/src/utils/cryptoUtils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createHash } from "crypto"; - -export function sha256(data: string): string { - return createHash("md5").update(data).digest("hex"); -} diff --git a/src-gui/src/utils/event.ts b/src-gui/src/utils/event.ts deleted file mode 100644 index 7aace5942b..0000000000 --- a/src-gui/src/utils/event.ts +++ /dev/null @@ -1,21 +0,0 @@ -export class SingleTypeEventEmitter { - private listeners: Array<(data: T) => void> = []; - - // Method to add a listener for the event - on(listener: (data: T) => void) { - this.listeners.push(listener); - } - - // Method to remove a listener - off(listener: (data: T) => void) { - const index = this.listeners.indexOf(listener); - if (index > -1) { - this.listeners.splice(index, 1); - } - } - - // Method to emit the event - emit(data: T) { - this.listeners.forEach((listener) => listener(data)); - } -} diff --git a/src-gui/src/utils/logger.ts b/src-gui/src/utils/logger.ts index 9a14812df2..50a235e045 100644 --- a/src-gui/src/utils/logger.ts +++ b/src-gui/src/utils/logger.ts @@ -1,5 +1,10 @@ -import pino from "pino"; +const logger = { + trace: console.log, + debug: console.log, + info: console.log, + warn: console.warn, + error: console.error, + fatal: console.error, +}; -export default pino({ - level: "trace", -}); +export default logger; diff --git a/src-gui/src/utils/multiAddrUtils.ts b/src-gui/src/utils/multiAddrUtils.ts index 07f6873b29..6223a26098 100644 --- a/src-gui/src/utils/multiAddrUtils.ts +++ b/src-gui/src/utils/multiAddrUtils.ts @@ -6,7 +6,7 @@ import { isTestnet } from "store/config"; // const MIN_ASB_VERSION = "1.0.0-alpha.1" // First version to support new libp2p protocol // const MIN_ASB_VERSION = "1.1.0-rc.3" // First version with support for bdk > 1.0 // const MIN_ASB_VERSION = "2.0.0-beta.1"; // First version with support for tx_early_refund -const MIN_ASB_VERSION = "3.0.0"; +const MIN_ASB_VERSION = "3.2.0-rc.1"; export function providerToConcatenatedMultiAddr(provider: Maker) { return new Multiaddr(provider.multiAddr) diff --git a/src-gui/src/utils/sortUtils.ts b/src-gui/src/utils/sortUtils.ts index 9ab03d7450..7df03b5e1e 100644 --- a/src-gui/src/utils/sortUtils.ts +++ b/src-gui/src/utils/sortUtils.ts @@ -10,48 +10,47 @@ export function sortApprovalsAndKnownQuotes( pendingSelectMakerApprovals: PendingSelectMakerApprovalRequest[], known_quotes: QuoteWithAddress[], ) { - const sortableQuotes = pendingSelectMakerApprovals.map((approval) => { - return { - ...approval.request.content.maker, - expiration_ts: - approval.request_status.state === "Pending" - ? approval.request_status.content.expiration_ts - : undefined, - request_id: approval.request_id, - } as SortableQuoteWithAddress; - }); + const sortableQuotes: SortableQuoteWithAddress[] = + pendingSelectMakerApprovals.map((approval) => { + return { + quote_with_address: approval.request.content.maker, + approval: + approval.request_status.state === "Pending" + ? { + request_id: approval.request_id, + expiration_ts: approval.request_status.content.expiration_ts, + } + : null, + }; + }); sortableQuotes.push( ...known_quotes.map((quote) => ({ - ...quote, - request_id: null, + quote_with_address: quote, + approval: null, })), ); - return sortMakerApprovals(sortableQuotes); -} - -export function sortMakerApprovals(list: SortableQuoteWithAddress[]) { return ( - _(list) + _(sortableQuotes) .orderBy( [ // Prefer makers that have a 'version' attribute // If we don't have a version, we cannot clarify if it's outdated or not - (m) => (m.version ? 0 : 1), + (m) => (m.quote_with_address.version ? 0 : 1), // Prefer makers with a minimum quantity > 0 - (m) => ((m.quote.min_quantity ?? 0) > 0 ? 0 : 1), + (m) => ((m.quote_with_address.quote.min_quantity ?? 0) > 0 ? 0 : 1), // Prefer makers that are not outdated - (m) => (isMakerVersionOutdated(m.version) ? 1 : 0), + (m) => (isMakerVersionOutdated(m.quote_with_address.version) ? 1 : 0), // Prefer approvals over actual quotes - (m) => (m.request_id ? 0 : 1), + (m) => (m.approval ? 0 : 1), // Prefer makers with a lower price - (m) => m.quote.price, + (m) => m.quote_with_address.quote.price, ], ["asc", "asc", "asc", "asc", "asc"], ) // Remove duplicate makers - .uniqBy((m) => m.peer_id) + .uniqBy((m) => m.quote_with_address.peer_id) .value() ); } diff --git a/src-gui/tsconfig.json b/src-gui/tsconfig.json index ec7dd96883..3ddd12a06e 100644 --- a/src-gui/tsconfig.json +++ b/src-gui/tsconfig.json @@ -15,7 +15,7 @@ "jsx": "react-jsx", /* Linting */ - "strict": false, + "strict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noFallthroughCasesInSwitch": false, diff --git a/src-gui/yarn.lock b/src-gui/yarn.lock index db569fe867..6ce90148f0 100644 --- a/src-gui/yarn.lock +++ b/src-gui/yarn.lock @@ -532,18 +532,6 @@ dependencies: "@babel/runtime" "^7.28.2" -"@mui/lab@^7.0.0-beta.13": - version "7.0.0-beta.16" - resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-7.0.0-beta.16.tgz#99045e2840c3f4db0383cdcc477af8c7b60c83a2" - integrity sha512-YiyDU84F6ujjaa5xuItuXa40KN1aPC+8PBkP2OAOJGO2MMvdEicuvkEfVSnikH6uLHtKOwGzOeqEqrfaYxcOxw== - dependencies: - "@babel/runtime" "^7.28.2" - "@mui/system" "^7.3.1" - "@mui/types" "^7.4.5" - "@mui/utils" "^7.3.1" - clsx "^2.1.1" - prop-types "^15.8.1" - "@mui/material@^7.1.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@mui/material/-/material-7.3.1.tgz#bd1bf1344cc7a69b6e459248b544f0ae97945b1d" @@ -838,6 +826,11 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz#e5e0a0bae2c9d4858cc9b8dc508b2e10d7f0df8b" integrity sha512-CpKnYa8eHthJa3c+C38v/E+/KZyF1Jdh2Cz3DyKZqEWYgrM1IHFArXNWvBLPQCKUEsAqqKX27tTqVEFbDNUcOA== +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== + "@standard-schema/spec@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c" @@ -1139,18 +1132,16 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + "@types/lodash@^4.17.6": version "4.17.20" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93" integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA== -"@types/node@*": - version "24.3.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" - integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== - dependencies: - undici-types "~7.10.0" - "@types/node@^22.15.29": version "22.17.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.17.2.tgz#47a93d6f4b79327da63af727e7c54e8cab8c4d33" @@ -1381,13 +1372,6 @@ loupe "^3.1.2" tinyrainbow "^1.2.0" -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1450,7 +1434,7 @@ array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: call-bound "^1.0.3" is-array-buffer "^3.0.5" -array-includes@^3.1.6, array-includes@^3.1.8: +array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: version "3.1.9" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== @@ -1476,7 +1460,20 @@ array.prototype.findlast@^1.2.5: es-object-atoms "^1.0.0" es-shim-unscopables "^1.0.2" -array.prototype.flat@^1.3.1: +array.prototype.findlastindex@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" + integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-shim-unscopables "^1.1.0" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== @@ -1537,11 +1534,6 @@ async-stream-emitter@^7.0.1: dependencies: stream-demux "^10.0.1" -atomic-sleep@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" - integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== - available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -1626,14 +1618,6 @@ buffer@^5.2.1: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - cac@^6.7.14: version "6.7.14" resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" @@ -1675,13 +1659,6 @@ caniuse-lite@^1.0.30001735: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz#8292bb7591932ff09e9a765f12fdf5629a241ccc" integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw== -canvas-renderer@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/canvas-renderer/-/canvas-renderer-2.2.1.tgz#c1d131f78a9799aca8af9679ad0a005052b65550" - integrity sha512-RrBgVL5qCEDIXpJ6NrzyRNoTnXxYarqm/cS/W6ERhUJts5UQtt/XPEosGN3rqUkZ4fjBArlnCbsISJ+KCFnIAg== - dependencies: - "@types/node" "*" - chai@^5.1.2: version "5.3.2" resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.2.tgz#e2c35570b8fa23b5b7129b4114d5dc03b3fd3401" @@ -1737,11 +1714,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^2.0.7: - version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1829,16 +1801,18 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -dateformat@^4.6.3: - version "4.6.3" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" - integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== - dayjs@^1.11.13: version "1.11.13" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: version "4.4.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" @@ -1926,13 +1900,6 @@ electron-to-chromium@^1.5.204: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz#609c29502fd7257b4d721e3446f3ae391a0ca1b3" integrity sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg== -end-of-stream@^1.1.0: - version "1.4.5" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" - integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== - dependencies: - once "^1.4.0" - err-code@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" @@ -2059,7 +2026,7 @@ es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: has-tostringtag "^1.0.2" hasown "^2.0.2" -es-shim-unscopables@^1.0.2: +es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== @@ -2114,6 +2081,47 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-module-utils@^2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff" + integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.32.0: + version "2.32.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" + integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== + dependencies: + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.9" + array.prototype.findlastindex "^1.2.6" + array.prototype.flat "^1.3.3" + array.prototype.flatmap "^1.3.3" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.12.1" + hasown "^2.0.2" + is-core-module "^2.16.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.1" + semver "^6.3.1" + string.prototype.trimend "^1.0.9" + tsconfig-paths "^3.15.0" + eslint-plugin-react@^7.35.0: version "7.37.5" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" @@ -2237,16 +2245,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -events@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -2267,11 +2265,6 @@ expect-type@^1.1.0: resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.2.tgz#c030a329fb61184126c8447585bc75a7ec6fbff3" integrity sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA== -fast-copy@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35" - integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -2298,16 +2291,6 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-redact@^3.1.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" - integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== - -fast-safe-stringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" - integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== - fastq@^1.6.0: version "1.19.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" @@ -2532,11 +2515,6 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" -help-me@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" - integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== - hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -2554,7 +2532,7 @@ humanize-duration@^3.32.1: resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.33.0.tgz#29b3276e68443e513fc85223d094faacdbb8454c" integrity sha512-vYJX7BSzn7EQ4SaP2lPYVy+icHDppB6k7myNeI3wrSRfwMS5+BHyGgzpHR0ptqJ2AQ6UuIKrclSg5ve6Ci4IAQ== -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -2671,7 +2649,7 @@ is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.16.0: +is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== @@ -2864,18 +2842,6 @@ iterator.prototype@^1.1.4: has-symbols "^1.1.0" set-function-name "^2.0.2" -jdenticon@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/jdenticon/-/jdenticon-3.3.0.tgz#64bae9f9b3cf5c2a210e183648117afe3a89b367" - integrity sha512-DhuBRNRIybGPeAjMjdHbkIfiwZCCmf8ggu7C49jhp6aJ7DYsZfudnvnTY5/1vgUhrGA7JaDAx1WevnpjCPvaGg== - dependencies: - canvas-renderer "~2.2.0" - -joycon@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" - integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2918,6 +2884,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -3123,7 +3096,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -3237,6 +3210,15 @@ object.fromentries@^2.0.8: es-abstract "^1.23.2" es-object-atoms "^1.0.0" +object.groupby@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + object.values@^1.1.6, object.values@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" @@ -3247,18 +3229,6 @@ object.values@^1.1.6, object.values@^1.2.1: define-properties "^1.2.1" es-object-atoms "^1.0.0" -on-exit-leak-free@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" - integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== - -once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -3377,55 +3347,6 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pino-abstract-transport@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60" - integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== - dependencies: - split2 "^4.0.0" - -pino-pretty@^11.2.1: - version "11.3.0" - resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.3.0.tgz#390b3be044cf3d2e9192c7d19d44f6b690468f2e" - integrity sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA== - dependencies: - colorette "^2.0.7" - dateformat "^4.6.3" - fast-copy "^3.0.2" - fast-safe-stringify "^2.1.1" - help-me "^5.0.0" - joycon "^3.1.1" - minimist "^1.2.6" - on-exit-leak-free "^2.1.0" - pino-abstract-transport "^2.0.0" - pump "^3.0.0" - readable-stream "^4.0.0" - secure-json-parse "^2.4.0" - sonic-boom "^4.0.1" - strip-json-comments "^3.1.1" - -pino-std-serializers@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" - integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== - -pino@^9.2.0: - version "9.9.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-9.9.0.tgz#0d2667ab4a54b561a4434a321ec595f305ab9cd1" - integrity sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ== - dependencies: - atomic-sleep "^1.0.0" - fast-redact "^3.1.1" - on-exit-leak-free "^2.1.0" - pino-abstract-transport "^2.0.0" - pino-std-serializers "^7.0.0" - process-warning "^5.0.0" - quick-format-unescaped "^4.0.3" - real-require "^0.2.0" - safe-stable-stringify "^2.3.1" - sonic-boom "^4.0.1" - thread-stream "^3.0.0" - possible-typed-array-names@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" @@ -3450,16 +3371,6 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process-warning@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" - integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -3469,14 +3380,6 @@ prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -pump@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" - integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -3492,11 +3395,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quick-format-unescaped@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" - integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== - react-dom@^19.1.0: version "19.1.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893" @@ -3578,22 +3476,6 @@ readable-stream@^2.3.5, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^4.0.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" - integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - -real-require@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" - integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== - receptacle@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/receptacle/-/receptacle-1.3.2.tgz#a7994c7efafc7a01d0e2041839dab6c4951360d2" @@ -3668,6 +3550,15 @@ resolve@^1.19.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.4: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.5: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" @@ -3734,7 +3625,7 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -3761,11 +3652,6 @@ safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" -safe-stable-stringify@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" - integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== - sc-errors@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/sc-errors/-/sc-errors-3.0.0.tgz#df2e124f011be5fdd633e92d1de5ce6a6b4c1b85" @@ -3781,11 +3667,6 @@ scheduler@^0.26.0: resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== -secure-json-parse@^2.4.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" - integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== - semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -3920,13 +3801,6 @@ socketcluster-client@^19.2.3: vinyl-buffer "^1.0.1" ws "^8.18.0" -sonic-boom@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d" - integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww== - dependencies: - atomic-sleep "^1.0.0" - source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" @@ -3937,11 +3811,6 @@ source-map@^0.5.7: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -split2@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" - integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== - stackback@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" @@ -4027,13 +3896,6 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -4041,6 +3903,11 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -4068,13 +3935,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -thread-stream@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" - integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== - dependencies: - real-require "^0.2.0" - through2@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -4125,6 +3985,16 @@ tsconfck@^3.0.3: resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -4214,11 +4084,6 @@ undici-types@~6.21.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== -undici-types@~7.10.0: - version "7.10.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" - integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== - update-browserslist-db@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" @@ -4419,11 +4284,6 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - writable-consumable-stream@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/writable-consumable-stream/-/writable-consumable-stream-4.2.0.tgz#731cb8bc7c16d5e120adfaddd7d41c52179934d7" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8cea8aada0..465e374e8b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "unstoppableswap-gui-rs" -version = "3.2.0-rc.1" +version = "3.3.1" authors = ["binarybaron", "einliterflasche", "unstoppableswap"] edition = "2021" description = "GUI for XMR<>BTC Atomic Swaps written in Rust" @@ -27,9 +27,9 @@ uuid = "1.16.0" zip = "4.0.0" # Tauri -tauri = { version = "2.*", features = [ "config-json5" ] } +tauri = { version = "2.8.0", features = [ "config-json5" ] } tauri-plugin-clipboard-manager = "2.*" -tauri-plugin-dialog = "2.2.2" +tauri-plugin-dialog = "2.3.3" tauri-plugin-opener = "2.*" tauri-plugin-process = "2.*" tauri-plugin-shell = "2.*" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 4f8d11002b..311373e223 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -8,11 +8,11 @@ use swap::cli::{ BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ChangeMoneroNodeArgs, CheckElectrumNodeArgs, CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs, CheckSeedResponse, DfxAuthenticateResponse, - ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs, - GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs, - GetMoneroMainAddressArgs, GetMoneroSeedArgs, GetMoneroSyncProgressArgs, - GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs, - GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs, + ExportBitcoinWalletArgs, GetBitcoinAddressArgs, GetCurrentSwapArgs, GetDataDirArgs, + GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, + GetMoneroHistoryArgs, GetMoneroMainAddressArgs, GetMoneroSeedArgs, + GetMoneroSyncProgressArgs, GetPendingApprovalsResponse, GetRestoreHeightArgs, + GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs, RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs, SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs, }, @@ -35,6 +35,7 @@ macro_rules! generate_command_handlers { () => { tauri::generate_handler![ get_balance, + get_bitcoin_address, get_monero_addresses, get_swap_info, get_swap_infos_all, @@ -173,7 +174,6 @@ pub async fn initialize_context( }) .with_monero(settings.monero_node_config) .with_json(false) - .with_debug(true) .with_tor(settings.use_tor) .with_enable_monero_tor(settings.enable_monero_tor) .with_tauri(tauri_handle.clone()) @@ -436,6 +436,7 @@ tauri_command!(send_monero, SendMoneroArgs); tauri_command!(change_monero_node, ChangeMoneroNodeArgs); // These commands require no arguments +tauri_command!(get_bitcoin_address, GetBitcoinAddressArgs, no_args); tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args); tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args); tauri_command!(get_swap_info, GetSwapInfoArgs); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index edce2fa4ff..28287fe337 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,13 +1,13 @@ { "productName": "eigenwallet", - "version": "3.2.0-rc.1", + "version": "3.3.1", "identifier": "net.unstoppableswap.gui", "build": { "devUrl": "http://localhost:1420", "frontendDist": "../src-gui/dist", "beforeBuildCommand": { "cwd": "../src-gui", - "script": "yarn install && yarn run build" + "script": "yarn install --frozen-lockfile && yarn run build" } }, "app": { diff --git a/swap-asb/Cargo.toml b/swap-asb/Cargo.toml index ac6b102715..3b0d4e25dd 100644 --- a/swap-asb/Cargo.toml +++ b/swap-asb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swap-asb" -version = "3.0.0-beta.3" +version = "3.3.1" authors = ["The eigenwallet guys ", "The COMIT guys "] edition = "2021" description = "ASB (Automated Swap Backend) binary for XMR/BTC atomic swaps." diff --git a/swap-asb/src/command.rs b/swap-asb/src/command.rs index a535b796a8..31b68dddc7 100644 --- a/swap-asb/src/command.rs +++ b/swap-asb/src/command.rs @@ -272,7 +272,7 @@ pub enum Command { name = "asb", about = "Automated Swap Backend for swapping XMR for BTC", author, - version = env!("VERGEN_GIT_DESCRIBE") + version = env!("CARGO_PKG_VERSION") )] pub struct RawArguments { #[structopt(long, help = "Swap on testnet")] diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index 274854e3a1..f1ff2e588a 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -43,7 +43,6 @@ use swap_env::config::{ }; use swap_feed; use swap_machine::alice::is_complete; -use tracing_subscriber::filter::LevelFilter; use uuid::Uuid; const DEFAULT_WALLET_NAME: &str = "asb-wallet"; @@ -53,12 +52,11 @@ fn initialize_tracing(json: bool, config: &Config, trace: bool) -> Result<()> { let format = if json { Format::Json } else { Format::Raw }; let log_dir = config.data.dir.join("logs"); - common::tracing_util::init(LevelFilter::DEBUG, format, log_dir, None, trace) - .expect("initialize tracing"); + common::tracing_util::init(format, log_dir, None, trace).expect("initialize tracing"); tracing::info!( binary = "asb", - version = env!("VERGEN_GIT_DESCRIBE"), + version = env!("CARGO_PKG_VERSION"), os = std::env::consts::OS, arch = std::env::consts::ARCH, "Setting up context" diff --git a/swap-controller-api/src/lib.rs b/swap-controller-api/src/lib.rs index 2e4523d6f4..4d9b815237 100644 --- a/swap-controller-api/src/lib.rs +++ b/swap-controller-api/src/lib.rs @@ -33,6 +33,32 @@ pub struct ActiveConnectionsResponse { pub connections: usize, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum RendezvousConnectionStatus { + Disconnected, + Dialling, + Connected, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum RendezvousRegistrationStatus { + RegisterOnNextConnection, + Pending, + Registered, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RegistrationStatusItem { + pub address: String, + pub connection: RendezvousConnectionStatus, + pub registration: RendezvousRegistrationStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RegistrationStatusResponse { + pub registrations: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Swap { pub id: String, @@ -65,4 +91,6 @@ pub trait AsbApi { async fn active_connections(&self) -> Result; #[method(name = "get_swaps")] async fn get_swaps(&self) -> Result, ErrorObjectOwned>; + #[method(name = "registration_status")] + async fn registration_status(&self) -> Result; } diff --git a/swap-controller/Cargo.toml b/swap-controller/Cargo.toml index 124a173a2d..762cf34e25 100644 --- a/swap-controller/Cargo.toml +++ b/swap-controller/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swap-controller" -version = "0.1.0" +version = "3.3.1" edition = "2021" [[bin]] diff --git a/swap-controller/src/cli.rs b/swap-controller/src/cli.rs index 5aca112893..9f8a8e9150 100644 --- a/swap-controller/src/cli.rs +++ b/swap-controller/src/cli.rs @@ -33,4 +33,6 @@ pub enum Cmd { ActiveConnections, /// Get list of swaps GetSwaps, + /// Show rendezvous registration status + RegistrationStatus, } diff --git a/swap-controller/src/main.rs b/swap-controller/src/main.rs index dd527d2b69..89d87c26c9 100644 --- a/swap-controller/src/main.rs +++ b/swap-controller/src/main.rs @@ -84,6 +84,20 @@ async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> { let response = client.bitcoin_seed().await?; println!("Descriptor (BIP-0382) containing the private keys of the internal Bitcoin wallet: \n{}", response.descriptor); } + Cmd::RegistrationStatus => { + let response = client.registration_status().await?; + println!("Your asb registers at rendezvous to make itself discoverable to takers.\n"); + if response.registrations.is_empty() { + println!("No rendezvous points configured"); + } else { + for item in response.registrations { + println!( + "Connection status to rendezvous point at \"{}\" is \"{:?}\". Registration status is \"{:?}\"", + item.address, item.connection, item.registration + ); + } + } + } } Ok(()) } diff --git a/swap-core/src/bitcoin.rs b/swap-core/src/bitcoin.rs index 3a842e87c6..c58f7a897c 100644 --- a/swap-core/src/bitcoin.rs +++ b/swap-core/src/bitcoin.rs @@ -6,13 +6,14 @@ mod redeem; mod refund; mod timelocks; -pub use crate::bitcoin::cancel::{CancelTimelock, PunishTimelock, TxCancel}; +pub use crate::bitcoin::cancel::TxCancel; pub use crate::bitcoin::early_refund::TxEarlyRefund; pub use crate::bitcoin::lock::TxLock; pub use crate::bitcoin::punish::TxPunish; pub use crate::bitcoin::redeem::TxRedeem; pub use crate::bitcoin::refund::TxRefund; pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks}; +pub use crate::bitcoin::timelocks::{CancelTimelock, PunishTimelock}; pub use ::bitcoin::amount::Amount; pub use ::bitcoin::psbt::Psbt as PartiallySignedTransaction; pub use ::bitcoin::{Address, AddressType, Network, Transaction, Txid}; diff --git a/swap-core/src/bitcoin/cancel.rs b/swap-core/src/bitcoin/cancel.rs index 8a523757d7..c375935e54 100644 --- a/swap-core/src/bitcoin/cancel.rs +++ b/swap-core/src/bitcoin/cancel.rs @@ -1,6 +1,6 @@ -use crate::bitcoin; +use crate::bitcoin::{self, CancelTimelock, PunishTimelock}; use crate::bitcoin::{ - build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, + build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, TxLock, }; use ::bitcoin::sighash::SighashCache; use ::bitcoin::transaction::Version; @@ -13,116 +13,7 @@ use anyhow::Result; use bdk_wallet::miniscript::Descriptor; use bitcoin_wallet::primitives::Watchable; use ecdsa_fun::Signature; -use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; use std::collections::HashMap; -use std::fmt; -use std::ops::Add; -use typeshare::typeshare; - -/// Represent a timelock, expressed in relative block height as defined in -/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). -/// E.g. The timelock expires 10 blocks after the reference transaction is -/// mined. -#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] -#[serde(transparent)] -#[typeshare] -pub struct CancelTimelock(u32); - -impl From for u32 { - fn from(cancel_timelock: CancelTimelock) -> Self { - cancel_timelock.0 - } -} - -impl From for CancelTimelock { - fn from(number_of_blocks: u32) -> Self { - Self(number_of_blocks) - } -} - -impl CancelTimelock { - pub const fn new(number_of_blocks: u32) -> Self { - Self(number_of_blocks) - } - - pub fn half(&self) -> CancelTimelock { - Self(self.0 / 2) - } -} - -impl Add for BlockHeight { - type Output = BlockHeight; - - fn add(self, rhs: CancelTimelock) -> Self::Output { - self + rhs.0 - } -} - -impl PartialOrd for u32 { - fn partial_cmp(&self, other: &CancelTimelock) -> Option { - self.partial_cmp(&other.0) - } -} - -impl PartialEq for u32 { - fn eq(&self, other: &CancelTimelock) -> bool { - self.eq(&other.0) - } -} - -impl fmt::Display for CancelTimelock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} blocks", self.0) - } -} - -/// Represent a timelock, expressed in relative block height as defined in -/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). -/// E.g. The timelock expires 10 blocks after the reference transaction is -/// mined. -#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] -#[serde(transparent)] -#[typeshare] -pub struct PunishTimelock(u32); - -impl From for u32 { - fn from(punish_timelock: PunishTimelock) -> Self { - punish_timelock.0 - } -} - -impl From for PunishTimelock { - fn from(number_of_blocks: u32) -> Self { - Self(number_of_blocks) - } -} - -impl PunishTimelock { - pub const fn new(number_of_blocks: u32) -> Self { - Self(number_of_blocks) - } -} - -impl Add for BlockHeight { - type Output = BlockHeight; - - fn add(self, rhs: PunishTimelock) -> Self::Output { - self + rhs.0 - } -} - -impl PartialOrd for u32 { - fn partial_cmp(&self, other: &PunishTimelock) -> Option { - self.partial_cmp(&other.0) - } -} - -impl PartialEq for u32 { - fn eq(&self, other: &PunishTimelock) -> bool { - self.eq(&other.0) - } -} #[derive(Debug)] pub struct TxCancel { diff --git a/swap-core/src/bitcoin/refund.rs b/swap-core/src/bitcoin/refund.rs index e4371b12a0..5b8d53f7a7 100644 --- a/swap-core/src/bitcoin/refund.rs +++ b/swap-core/src/bitcoin/refund.rs @@ -103,13 +103,13 @@ impl TxRefund { pub fn extract_monero_private_key( &self, - published_refund_tx: Arc, + signed_refund_tx: Arc, s_a: Scalar, a: bitcoin::SecretKey, S_b_bitcoin: bitcoin::PublicKey, ) -> Result { let tx_refund_sig = self - .extract_signature_by_key(published_refund_tx, a.public()) + .extract_signature_by_key(signed_refund_tx, a.public()) .context("Failed to extract signature from Bitcoin refund tx")?; let tx_refund_encsig = a.encsign(S_b_bitcoin, self.digest()); diff --git a/swap-core/src/bitcoin/timelocks.rs b/swap-core/src/bitcoin/timelocks.rs index 3d142a6557..dce3bfa0fd 100644 --- a/swap-core/src/bitcoin/timelocks.rs +++ b/swap-core/src/bitcoin/timelocks.rs @@ -1,7 +1,9 @@ use anyhow::Context; use bdk_electrum::electrum_client::HeaderNotification; use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; use std::convert::{TryFrom, TryInto}; +use std::fmt; use std::ops::Add; use typeshare::typeshare; @@ -46,6 +48,106 @@ impl Add for BlockHeight { } } +/// Represent a timelock, expressed in relative block height as defined in +/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). +/// E.g. The timelock expires 10 blocks after the reference transaction is +/// mined. +#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(transparent)] +#[typeshare] +pub struct CancelTimelock(pub u32); + +impl From for u32 { + fn from(cancel_timelock: CancelTimelock) -> Self { + cancel_timelock.0 + } +} + +impl From for CancelTimelock { + fn from(number_of_blocks: u32) -> Self { + Self(number_of_blocks) + } +} + +impl CancelTimelock { + pub const fn new(number_of_blocks: u32) -> Self { + Self(number_of_blocks) + } +} + +impl Add for BlockHeight { + type Output = BlockHeight; + + fn add(self, rhs: CancelTimelock) -> Self::Output { + self + rhs.0 + } +} + +impl PartialOrd for u32 { + fn partial_cmp(&self, other: &CancelTimelock) -> Option { + self.partial_cmp(&other.0) + } +} + +impl PartialEq for u32 { + fn eq(&self, other: &CancelTimelock) -> bool { + self.eq(&other.0) + } +} + +impl fmt::Display for CancelTimelock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} blocks", self.0) + } +} + +/// Represent a timelock, expressed in relative block height as defined in +/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). +/// E.g. The timelock expires 10 blocks after the reference transaction is +/// mined. +#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(transparent)] +#[typeshare] +pub struct PunishTimelock(pub u32); + +impl From for u32 { + fn from(punish_timelock: PunishTimelock) -> Self { + punish_timelock.0 + } +} + +impl From for PunishTimelock { + fn from(number_of_blocks: u32) -> Self { + Self(number_of_blocks) + } +} + +impl PunishTimelock { + pub const fn new(number_of_blocks: u32) -> Self { + Self(number_of_blocks) + } +} + +impl Add for BlockHeight { + type Output = BlockHeight; + + fn add(self, rhs: PunishTimelock) -> Self::Output { + self + rhs.0 + } +} + +impl PartialOrd for u32 { + fn partial_cmp(&self, other: &PunishTimelock) -> Option { + self.partial_cmp(&other.0) + } +} + +impl PartialEq for u32 { + fn eq(&self, other: &PunishTimelock) -> bool { + self.eq(&other.0) + } +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(tag = "type", content = "content")] diff --git a/swap-core/src/monero/primitives.rs b/swap-core/src/monero/primitives.rs index a079fbb9ba..128a91e2c9 100644 --- a/swap-core/src/monero/primitives.rs +++ b/swap-core/src/monero/primitives.rs @@ -216,6 +216,7 @@ impl Amount { #[typeshare] pub struct LabeledMoneroAddress { // If this is None, we will use an address of the internal Monero wallet + // TODO: This should be string | null but typeshare cannot do that yet #[typeshare(serialized_as = "string")] address: Option, #[typeshare(serialized_as = "number")] @@ -268,7 +269,7 @@ impl LabeledMoneroAddress { /// Returns the Monero address. pub fn address(&self) -> Option { - self.address.clone() + self.address } /// Returns the percentage as a decimal. diff --git a/swap-db/Cargo.toml b/swap-db/Cargo.toml new file mode 100644 index 0000000000..2a65aea576 --- /dev/null +++ b/swap-db/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "swap-db" +version = "0.1.0" +edition = "2024" + +[dependencies] +# Our crates +swap-core = { path = "../swap-core" } +swap-machine = { path = "../swap-machine" } +swap-serde = { path = "../swap-serde" } + +# Crypto +bitcoin = { workspace = true } + +# Serialization +serde = { workspace = true } +strum = { workspace = true, features = ["derive"] } + +[lints] +workspace = true diff --git a/swap/src/database/alice.rs b/swap-db/src/alice.rs similarity index 91% rename from swap/src/database/alice.rs rename to swap-db/src/alice.rs index dcb26b68d1..187a1b7a29 100644 --- a/swap/src/database/alice.rs +++ b/swap-db/src/alice.rs @@ -1,11 +1,10 @@ -use crate::monero; -use crate::monero::BlockHeight; -use crate::monero::TransferProof; -use crate::protocol::alice; -use crate::protocol::alice::AliceState; use serde::{Deserialize, Serialize}; use std::fmt; use swap_core::bitcoin::EncryptedSignature; +use swap_core::monero; +use swap_core::monero::{BlockHeight, TransferProof}; +use swap_machine::alice; +use swap_machine::alice::AliceState; // Large enum variant is fine because this is only used for database // and is dropped once written in DB. @@ -46,6 +45,11 @@ pub enum Alice { state3: alice::State3, transfer_proof: TransferProof, }, + WaitingForCancelTimelockExpiration { + monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, + state3: alice::State3, + }, CancelTimelockExpired { monero_wallet_restore_blockheight: BlockHeight, transfer_proof: TransferProof, @@ -182,6 +186,15 @@ impl From for Alice { state3: state3.as_ref().clone(), }, AliceState::XmrRefunded => Alice::Done(AliceEndState::XmrRefunded), + AliceState::WaitingForCancelTimelockExpiration { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => Alice::WaitingForCancelTimelockExpiration { + monero_wallet_restore_blockheight, + transfer_proof, + state3: state3.as_ref().clone(), + }, AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, @@ -260,6 +273,15 @@ impl From for AliceState { state3: Box::new(state3), transfer_proof, }, + Alice::WaitingForCancelTimelockExpiration { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => AliceState::WaitingForCancelTimelockExpiration { + monero_wallet_restore_blockheight, + transfer_proof, + state3: Box::new(state3), + }, Alice::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, @@ -337,6 +359,9 @@ impl fmt::Display for Alice { Alice::BtcRedeemTransactionPublished { .. } => { f.write_str("Bitcoin redeem transaction published") } + Alice::WaitingForCancelTimelockExpiration { .. } => { + f.write_str("Waiting for cancel timelock to expire") + } Alice::CancelTimelockExpired { .. } => f.write_str("Cancel timelock is expired"), Alice::BtcCancelled { .. } => f.write_str("Bitcoin cancel transaction published"), Alice::BtcPunishable { .. } => f.write_str("Bitcoin punishable"), diff --git a/swap/src/database/bob.rs b/swap-db/src/bob.rs similarity index 98% rename from swap/src/database/bob.rs rename to swap-db/src/bob.rs index 9affda6851..039b325206 100644 --- a/swap/src/database/bob.rs +++ b/swap-db/src/bob.rs @@ -1,9 +1,8 @@ -use crate::monero::BlockHeight; -use crate::monero::TransferProof; -use crate::protocol::bob; -use crate::protocol::bob::BobState; use serde::{Deserialize, Serialize}; use std::fmt; +use swap_core::monero::{BlockHeight, TransferProof}; +use swap_machine::bob; +use swap_machine::bob::BobState; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum Bob { diff --git a/swap-db/src/lib.rs b/swap-db/src/lib.rs new file mode 100644 index 0000000000..9de27854b5 --- /dev/null +++ b/swap-db/src/lib.rs @@ -0,0 +1,2 @@ +pub mod alice; +pub mod bob; diff --git a/swap-env/src/defaults.rs b/swap-env/src/defaults.rs index 601f2490c2..86465e6246 100644 --- a/swap-env/src/defaults.rs +++ b/swap-env/src/defaults.rs @@ -10,21 +10,28 @@ use url::Url; /* Here's the GPG signature of the donation address. -Signed by the public key present in `utils/gpg_keys/binarybaron.asc` +Signed by the public key present in `utils/gpg_keys/binarybaron_and_einliterflasche.asc` -----BEGIN PGP SIGNED MESSAGE----- -Hash: SHA512 +Hash: SHA256 -87QwQmWZQwS6RvuprCqWuJgmystL8Dw6BCx8SrrCjVJhZYGc5s6kf9A2awfFfStvEGCGeNTBNqLGrHzH6d4gi7jLM2aoq9o is our donation address for Github (signed by binarybaron) +4A1tNBcsxhQA7NkswREXTD1QGz8mRyA7fGnCzPyTwqzKdDFMNje7iHUbGhCetfVUZa1PTuZCoPKj8gnJuRrFYJ2R2CEzqbJ is our donation address (signed by binarybaron) -----BEGIN PGP SIGNATURE----- -iHUEARYKAB0WIQQ1qETX9LVbxE4YD/GZt10+FHaibgUCaJTWlQAKCRCZt10+FHai -bhasAQDGrAkZu+FFwDZDUEZzrIVS42he+GeMiS+ykpXyL5I7RQD/dXCR3f39zFsK -1A7y45B3a8ZJYTzC7bbppg6cEnCoWQE= -=j+Vz +iQGzBAEBCAAdFiEEBRhGD+vsHaFKFVp7RK5vCxZqrVoFAmjxV4YACgkQRK5vCxZq +rVrFogv9F650Um1TsPlqQ+7kdobCwa7yH5uXOp1p22YaiwWGHKRU5rUSb6Ac+zI0 +3Io39VEoZufQqXqEqaiH7Q/08ABQR5r0TTPtSLNjOSEQ+ecClwv7MeF5CIXZYDdB +AlEOnlL0CPfA24GQMhfp9lvjNiTBA2NikLARWJrc1JsLrFMK5rHesv7VHJEtm/gu +We5eAuNOM2k3nAABTWzLiMJkH+G1amJmfkCKkBCk04inA6kZ5COUikMupyQDtsE4 +hrr/KrskMuXzGY+rjP6NhWqr/twKj819TrOxlYD4vK68cZP+jx9m+vSBE6mxgMbN +tBVdo9xFVCVymOYQCV8BRY8ScqP+YPNV5d6BMyDH9tvHJrGqZTNQiFhVX03Tw6mg +hccEqYP1J/TaAlFg/P4HtqsxPBZD6x3IdSxXhrJ0IjrqLpVtKyQlTZGsJuNjFWG8 +LKixaxxR7iWsyRZVCnEqCgDN8hzKZIE3Ph+kLTa4z4mTNEYyWUNeKRrFrSxKvEOK +KM0Pp53f +=O/zf -----END PGP SIGNATURE----- */ -pub const DEFAULT_DEVELOPER_TIP_ADDRESS_MAINNET: &str = "87QwQmWZQwS6RvuprCqWuJgmystL8Dw6BCx8SrrCjVJhZYGc5s6kf9A2awfFfStvEGCGeNTBNqLGrHzH6d4gi7jLM2aoq9o"; +pub const DEFAULT_DEVELOPER_TIP_ADDRESS_MAINNET: &str = "4A1tNBcsxhQA7NkswREXTD1QGz8mRyA7fGnCzPyTwqzKdDFMNje7iHUbGhCetfVUZa1PTuZCoPKj8gnJuRrFYJ2R2CEzqbJ"; pub const DEFAULT_DEVELOPER_TIP_ADDRESS_STAGENET: &str = "54ZYC5tgGRoKMJDLviAcJF2aHittSZGGkFZE6wCLkuAdUyHaaiQrjTxeSyfvxycn3yiexL4YNqdUmHuaReAk6JD4DQssQcF"; pub const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64; @@ -50,49 +57,42 @@ pub fn default_electrum_servers_mainnet() -> Vec { vec![ Url::parse("ssl://electrum.blockstream.info:50002") .expect("default electrum server url to be valid"), - Url::parse("tcp://electrum.blockstream.info:50001") - .expect("default electrum server url to be valid"), Url::parse("ssl://bitcoin.stackwallet.com:50002") .expect("default electrum server url to be valid"), Url::parse("ssl://b.1209k.com:50002").expect("default electrum server url to be valid"), - Url::parse("tcp://electrum.coinucopia.io:50001") - .expect("default electrum server url to be valid"), Url::parse("ssl://mainnet.foundationdevices.com:50002") .expect("default electrum server url to be valid"), Url::parse("tcp://bitcoin.lu.ke:50001").expect("default electrum server url to be valid"), - Url::parse("tcp://se-mma-crypto-payments-001.mullvad.net:50001") - .expect("default electrum server url to be valid"), Url::parse("ssl://electrum.coinfinity.co:50002") .expect("default electrum server url to be valid"), + Url::parse("tcp://electrum1.bluewallet.io:50001") + .expect("default electrum server url to be valid"), + Url::parse("tcp://electrum2.bluewallet.io:50001") + .expect("default electrum server url to be valid"), + Url::parse("tcp://electrum3.bluewallet.io:50001") + .expect("default electrum server url to be valid"), + Url::parse("ssl://btc-electrum.cakewallet.com:50002") + .expect("default electrum server url to be valid"), + Url::parse("tcp://bitcoin.aranguren.org:50001") + .expect("default electrum server url to be valid"), ] } pub fn default_electrum_servers_testnet() -> Vec { vec![ - Url::parse("ssl://ax101.blockeng.ch:60002") - .expect("default electrum server url to be valid"), Url::parse("ssl://blackie.c3-soft.com:57006") .expect("default electrum server url to be valid"), Url::parse("ssl://v22019051929289916.bestsrv.de:50002") .expect("default electrum server url to be valid"), Url::parse("tcp://v22019051929289916.bestsrv.de:50001") .expect("default electrum server url to be valid"), - Url::parse("tcp://electrum.blockstream.info:60001") - .expect("default electrum server url to be valid"), Url::parse("ssl://electrum.blockstream.info:60002") .expect("default electrum server url to be valid"), Url::parse("ssl://blockstream.info:993").expect("default electrum server url to be valid"), - Url::parse("tcp://blockstream.info:143").expect("default electrum server url to be valid"), - Url::parse("ssl://testnet.qtornado.com:51002") - .expect("default electrum server url to be valid"), - Url::parse("tcp://testnet.qtornado.com:51001") - .expect("default electrum server url to be valid"), Url::parse("tcp://testnet.aranguren.org:51001") .expect("default electrum server url to be valid"), Url::parse("ssl://testnet.aranguren.org:51002") .expect("default electrum server url to be valid"), - Url::parse("ssl://testnet.qtornado.com:50002") - .expect("default electrum server url to be valid"), Url::parse("ssl://bitcoin.devmole.eu:5010") .expect("default electrum server url to be valid"), Url::parse("tcp://bitcoin.devmole.eu:5000") diff --git a/swap-env/src/env.rs b/swap-env/src/env.rs index 7988105974..61e0bc9d7c 100644 --- a/swap-env/src/env.rs +++ b/swap-env/src/env.rs @@ -9,6 +9,9 @@ pub struct Config { pub bitcoin_lock_mempool_timeout: Duration, pub bitcoin_lock_confirmed_timeout: Duration, pub bitcoin_finality_confirmations: u32, + /// The upper bound for the number of blocks that will be mined before our + /// Bitcoin transaction is included in a block + pub bitcoin_blocks_till_confirmed_upper_bound_assumption: u32, pub bitcoin_avg_block_time: Duration, pub bitcoin_cancel_timelock: u32, pub bitcoin_punish_timelock: u32, @@ -52,6 +55,9 @@ impl GetConfig for Mainnet { bitcoin_lock_mempool_timeout: 10.std_minutes(), bitcoin_lock_confirmed_timeout: 2.std_hours(), bitcoin_finality_confirmations: 1, + // We assume that a transaction that was constructed to be confirmed within one block + // will be confirmed within at most 6 blocks + bitcoin_blocks_till_confirmed_upper_bound_assumption: 6, bitcoin_avg_block_time: 10.std_minutes(), bitcoin_cancel_timelock: 72, bitcoin_punish_timelock: 144, @@ -60,8 +66,8 @@ impl GetConfig for Mainnet { // If Alice cannot lock her Monero within this timeout, // she will initiate an early refund of Bobs Bitcoin monero_lock_retry_timeout: 10.std_minutes(), - monero_finality_confirmations: 22, - monero_double_spend_safe_confirmations: 22, + monero_finality_confirmations: 10, + monero_double_spend_safe_confirmations: 10, monero_network: monero::Network::Mainnet, } } @@ -73,14 +79,15 @@ impl GetConfig for Testnet { bitcoin_lock_mempool_timeout: 10.std_minutes(), bitcoin_lock_confirmed_timeout: 1.std_hours(), bitcoin_finality_confirmations: 1, + bitcoin_blocks_till_confirmed_upper_bound_assumption: 6, bitcoin_avg_block_time: 10.std_minutes(), bitcoin_cancel_timelock: 12 * 3, bitcoin_punish_timelock: 24 * 3, bitcoin_network: bitcoin::Network::Testnet, monero_avg_block_time: 2.std_minutes(), monero_lock_retry_timeout: 10.std_minutes(), - monero_finality_confirmations: 22, - monero_double_spend_safe_confirmations: 22, + monero_finality_confirmations: 10, + monero_double_spend_safe_confirmations: 10, monero_network: monero::Network::Stagenet, } } @@ -92,14 +99,15 @@ impl GetConfig for Regtest { bitcoin_lock_mempool_timeout: 30.std_seconds(), bitcoin_lock_confirmed_timeout: 5.std_minutes(), bitcoin_finality_confirmations: 1, + bitcoin_blocks_till_confirmed_upper_bound_assumption: 6, bitcoin_avg_block_time: 5.std_seconds(), bitcoin_cancel_timelock: 100, bitcoin_punish_timelock: 50, bitcoin_network: bitcoin::Network::Regtest, monero_avg_block_time: 1.std_seconds(), monero_lock_retry_timeout: 1.std_minutes(), - monero_finality_confirmations: 12, - monero_double_spend_safe_confirmations: 12, + monero_finality_confirmations: 10, + monero_double_spend_safe_confirmations: 10, monero_network: monero::Network::Mainnet, // yes this is strange } } diff --git a/swap-env/src/prompt.rs b/swap-env/src/prompt.rs index 70f3ab9cdc..96c3629039 100644 --- a/swap-env/src/prompt.rs +++ b/swap-env/src/prompt.rs @@ -362,7 +362,7 @@ where let top = format!("┌{}", "─".repeat(line_width.saturating_sub(1))); let bottom = format!("└{}", "─".repeat(line_width.saturating_sub(1))); - println!(""); + println!(); println!("{}", border.apply_to(&top)); for l in collected { println!("{} {}", border.apply_to("│"), content.apply_to(l)); diff --git a/swap-machine/src/alice/mod.rs b/swap-machine/src/alice/mod.rs index 5104835133..a3c77423ef 100644 --- a/swap-machine/src/alice/mod.rs +++ b/swap-machine/src/alice/mod.rs @@ -73,6 +73,11 @@ pub enum AliceState { state3: Box, }, XmrRefunded, + WaitingForCancelTimelockExpiration { + monero_wallet_restore_blockheight: BlockHeight, + transfer_proof: TransferProof, + state3: Box, + }, CancelTimelockExpired { monero_wallet_restore_blockheight: BlockHeight, transfer_proof: TransferProof, @@ -120,6 +125,9 @@ impl fmt::Display for AliceState { AliceState::SafelyAborted => write!(f, "safely aborted"), AliceState::BtcPunishable { .. } => write!(f, "btc is punishable"), AliceState::XmrRefunded => write!(f, "xmr is refunded"), + AliceState::WaitingForCancelTimelockExpiration { .. } => { + write!(f, "waiting for cancel timelock expiration") + } AliceState::CancelTimelockExpired { .. } => write!(f, "cancel timelock is expired"), AliceState::BtcEarlyRefundable { .. } => write!(f, "btc is early refundable"), AliceState::BtcEarlyRefunded(_) => write!(f, "btc is early refunded"), @@ -547,11 +555,11 @@ impl State3 { pub fn extract_monero_private_key( &self, - published_refund_tx: Arc, + signed_refund_tx: Arc, ) -> Result { Ok(monero::PrivateKey::from_scalar( self.tx_refund().extract_monero_private_key( - published_refund_tx, + signed_refund_tx, self.s_a, self.a.clone(), self.S_b_bitcoin, @@ -652,6 +660,23 @@ impl State3 { ) } + pub async fn refund_btc( + &self, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, + ) -> Result> { + let refund_tx = bitcoin_wallet + .get_raw_transaction(self.tx_refund().txid()) + .await?; + + match refund_tx { + Some(refund_tx) => { + let spend_key = self.extract_monero_private_key(refund_tx)?; + Ok(Some(spend_key)) + } + None => Ok(None), + } + } + pub async fn watch_for_btc_tx_refund( &self, bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, @@ -665,14 +690,9 @@ impl State3 { .await .context("Failed to monitor refund transaction")?; - let published_refund_tx = bitcoin_wallet - .get_raw_transaction(self.tx_refund().txid()) - .await? - .context("Bitcoin refund transaction not found even though we saw it in the mempool previously. Maybe our Electrum server has cleared its mempool?")?; - - let spend_key = self.extract_monero_private_key(published_refund_tx)?; - - Ok(spend_key) + self.refund_btc(bitcoin_wallet).await?.context( + "Bitcoin refund transaction not found even though we saw it in the mempool previously", + ) } } diff --git a/swap-orchestrator/README.md b/swap-orchestrator/README.md index 334dcb870d..bcab29631d 100644 --- a/swap-orchestrator/README.md +++ b/swap-orchestrator/README.md @@ -44,10 +44,16 @@ Run the command below to start the wizard. It’ll guide you through a bunch of ./orchestrator ``` +To build the images, run this command. Also run this after upgrading the `orchestrator` and re-generating `docker-compose.yml`: + +```bash +docker compose build --no-cache # --no-cache fixes a git caching issue (error: tag clobbered) +``` + To start the environment, run a command [such as](https://docs.docker.com/reference/cli/docker/compose/up/): ```bash -docker compose up -d --build +docker compose up -d ``` To view logs, run commands [such as](https://docs.docker.com/reference/cli/docker/compose/logs/): diff --git a/swap-orchestrator/src/compose.rs b/swap-orchestrator/src/compose.rs new file mode 100644 index 0000000000..01b8ba5226 --- /dev/null +++ b/swap-orchestrator/src/compose.rs @@ -0,0 +1,435 @@ +use crate::containers; +use crate::containers::*; +use crate::images::PINNED_GIT_REPOSITORY; +use compose_spec::Compose; +use std::{ + fmt::{self, Display}, + path::PathBuf, +}; + +pub const ASB_DATA_DIR: &str = "/asb-data"; +pub const ASB_CONFIG_FILE: &str = "config.toml"; +pub const DOCKER_COMPOSE_FILE: &str = "./docker-compose.yml"; + +pub struct OrchestratorInput { + pub ports: OrchestratorPorts, + pub networks: OrchestratorNetworks, + pub images: OrchestratorImages, + pub directories: OrchestratorDirectories, +} + +pub struct OrchestratorDirectories { + pub asb_data_dir: PathBuf, +} + +#[derive(Clone)] +pub struct OrchestratorNetworks { + pub monero: MN, + pub bitcoin: BN, +} + +pub struct OrchestratorImages { + pub monerod: T, + pub electrs: T, + pub bitcoind: T, + pub asb: T, + pub asb_controller: T, + pub asb_tracing_logger: T, + pub rendezvous_node: T, +} + +pub struct OrchestratorPorts { + pub monerod_rpc: u16, + pub bitcoind_rpc: u16, + pub bitcoind_p2p: u16, + pub electrs: u16, + pub asb_libp2p: u16, + pub asb_rpc_port: u16, + pub rendezvous_node_port: u16, +} + +impl From> for OrchestratorPorts { + fn from(val: OrchestratorNetworks) -> Self { + match (val.monero, val.bitcoin) { + (monero::Network::Mainnet, bitcoin::Network::Bitcoin) => OrchestratorPorts { + monerod_rpc: 18081, + bitcoind_rpc: 8332, + bitcoind_p2p: 8333, + electrs: 50001, + asb_libp2p: 9939, + asb_rpc_port: 9944, + rendezvous_node_port: 8888, + }, + (monero::Network::Stagenet, bitcoin::Network::Testnet) => OrchestratorPorts { + monerod_rpc: 38081, + bitcoind_rpc: 18332, + bitcoind_p2p: 18333, + electrs: 50001, + asb_libp2p: 9839, + asb_rpc_port: 9944, + rendezvous_node_port: 8888, + }, + _ => panic!("Unsupported Bitcoin / Monero network combination"), + } + } +} + +impl From> for asb::Network { + fn from(val: OrchestratorNetworks) -> Self { + containers::asb::Network::new(val.monero, val.bitcoin) + } +} + +impl From> for electrs::Network { + fn from(val: OrchestratorNetworks) -> Self { + containers::electrs::Network::new(val.bitcoin) + } +} + +impl OrchestratorDirectories { + pub fn asb_config_path_inside_container(&self) -> PathBuf { + self.asb_data_dir.join(ASB_CONFIG_FILE) + } + + pub fn asb_config_path_on_host(&self) -> &'static str { + // The config file is in the same directory as the docker-compose.yml file + "./config.toml" + } + + pub fn asb_config_path_on_host_as_path_buf(&self) -> PathBuf { + PathBuf::from(self.asb_config_path_on_host()) + } +} + +/// See: https://docs.docker.com/reference/compose-file/build/#illustrative-example +#[derive(Debug, Clone)] +pub struct DockerBuildInput { + // Usually this is the root of the Cargo workspace + pub context: &'static str, + // Usually this is the path to the Dockerfile + pub dockerfile: &'static str, +} + +/// Specified a docker image to use +/// The image can either be pulled from a registry or built from source +pub enum OrchestratorImage { + Registry(String), + Build(DockerBuildInput), +} + +#[macro_export] +macro_rules! flag { + ($flag:expr) => { + Flag(Some($flag.to_string())) + }; + ($flag:expr, $($args:expr),*) => { + flag!(format!($flag, $($args),*)) + }; +} + +macro_rules! command { + ($command:expr $(, $flag:expr)* $(,)?) => { + Flags(vec![flag!($command) $(, $flag)*]) + }; +} + +fn build(input: OrchestratorInput) -> String { + // Every docker compose project has a name + // The name is prefixed to the container names + // See: https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name + let project_name = format!( + "{}_monero_{}_bitcoin", + input.networks.monero.to_display(), + input.networks.bitcoin.to_display() + ); + + let asb_config_path = PathBuf::from(ASB_DATA_DIR).join(ASB_CONFIG_FILE); + let asb_network: asb::Network = input.networks.clone().into(); + + let command_asb = command![ + "asb", + asb_network.to_flag(), + flag!("--config={}", asb_config_path.display()), + flag!("start"), + flag!("--rpc-bind-port={}", input.ports.asb_rpc_port), + flag!("--rpc-bind-host=0.0.0.0"), + ]; + + let command_monerod = command![ + "monerod", + input.networks.monero.to_flag(), + flag!("--rpc-bind-ip=0.0.0.0"), + flag!("--rpc-bind-port={}", input.ports.monerod_rpc), + flag!("--data-dir=/monerod-data/"), + flag!("--confirm-external-bind"), + flag!("--restricted-rpc"), + flag!("--non-interactive"), + flag!("--enable-dns-blocklist"), + ]; + + let command_bitcoind = command![ + "bitcoind", + input.networks.bitcoin.to_flag(), + flag!("-rpcallowip=0.0.0.0/0"), + flag!("-rpcbind=0.0.0.0:{}", input.ports.bitcoind_rpc), + flag!("-bind=0.0.0.0:{}", input.ports.bitcoind_p2p), + flag!("-datadir=/bitcoind-data/"), + flag!("-dbcache=16384"), + // These are required for electrs + // See: See: https://github.com/romanz/electrs/blob/master/doc/config.md#bitcoind-configuration + flag!("-server=1"), + flag!("-prune=0"), + flag!("-txindex=1"), + ]; + + let electrs_network: containers::electrs::Network = input.networks.clone().into(); + + let command_electrs = command![ + "electrs", + electrs_network.to_flag(), + flag!("--daemon-dir=/bitcoind-data/"), + flag!("--db-dir=/electrs-data/db"), + flag!("--daemon-rpc-addr=bitcoind:{}", input.ports.bitcoind_rpc), + flag!("--daemon-p2p-addr=bitcoind:{}", input.ports.bitcoind_p2p), + flag!("--electrum-rpc-addr=0.0.0.0:{}", input.ports.electrs), + flag!("--log-filters=INFO"), + ]; + + let command_asb_controller = command![ + "asb-controller", + flag!("--url=http://asb:{}", input.ports.asb_rpc_port), + ]; + + let command_asb_tracing_logger = command![ + "sh", + flag!("-c"), + flag!("tail -f /asb-data/logs/tracing*.log"), + ]; + + let command_rendezvous_node = command![ + "rendezvous-node", + flag!("--data-dir=/rendezvous-data"), + flag!("--port={}", input.ports.rendezvous_node_port), + ]; + + let date = chrono::Utc::now() + .format("%Y-%m-%d %H:%M:%S UTC") + .to_string(); + + let compose_str = format!( + "\ +# This file was auto-generated by `orchestrator` on {date} +# +# It is pinned to build the `asb` and `asb-controller` images from this commit: +# {PINNED_GIT_REPOSITORY} +# +# If the code does not match the hash, the build will fail. This ensures that the code cannot be altered by Github. +# The compiled `orchestrator` has this hash burned into the binary. +# +# To update the `asb` and `asb-controller` images, you need to either: +# - re-compile the `orchestrator` binary from a commit from Github +# - download a newer pre-compiled version of the `orchestrator` binary from Github. +# +# After updating the `orchestrator` binary, re-generate the compose file by running `orchestrator` again. +# +# The used images for `bitcoind`, `monerod`, `electrs` are pinned to specific hashes which prevents them from being altered by the Docker registry. +# +# Please check for new releases regularly. Breaking network changes are rare, but they do happen from time to time. +name: {project_name} +services: + monerod: + container_name: monerod + {image_monerod} + restart: unless-stopped + user: root + volumes: + - 'monerod-data:/monerod-data/' + expose: + - {port_monerod_rpc} + entrypoint: '' + command: {command_monerod} + bitcoind: + container_name: bitcoind + {image_bitcoind} + restart: unless-stopped + volumes: + - 'bitcoind-data:/bitcoind-data/' + expose: + - {port_bitcoind_rpc} + - {port_bitcoind_p2p} + user: root + entrypoint: '' + command: {command_bitcoind} + electrs: + container_name: electrs + {image_electrs} + restart: unless-stopped + user: root + depends_on: + - bitcoind + volumes: + - 'bitcoind-data:/bitcoind-data' + - 'electrs-data:/electrs-data' + expose: + - {electrs_port} + entrypoint: '' + command: {command_electrs} + asb: + container_name: asb + {image_asb} + restart: unless-stopped + depends_on: + - electrs + volumes: + - '{asb_config_path_on_host}:{asb_config_path_inside_container}' + - 'asb-data:{asb_data_dir}' + ports: + - '0.0.0.0:{asb_port}:{asb_port}' + entrypoint: '' + command: {command_asb} + asb-controller: + container_name: asb-controller + {image_asb_controller} + stdin_open: true + tty: true + restart: unless-stopped + depends_on: + - asb + entrypoint: '' + command: {command_asb_controller} + asb-tracing-logger: + container_name: asb-tracing-logger + {image_asb_tracing_logger} + restart: unless-stopped + depends_on: + - asb + volumes: + - 'asb-data:/asb-data:ro' + entrypoint: '' + command: {command_asb_tracing_logger} + rendezvous-node: + container_name: rendezvous-node + {image_rendezvous_node} + restart: unless-stopped + volumes: + - 'rendezvous-data:/rendezvous-data' + ports: + - '0.0.0.0:{rendezvous_node_port}:{rendezvous_node_port}' + entrypoint: '' + command: {command_rendezvous_node} +volumes: + monerod-data: + bitcoind-data: + electrs-data: + asb-data: + rendezvous-data: +", + port_monerod_rpc = input.ports.monerod_rpc, + port_bitcoind_rpc = input.ports.bitcoind_rpc, + port_bitcoind_p2p = input.ports.bitcoind_p2p, + electrs_port = input.ports.electrs, + asb_port = input.ports.asb_libp2p, + rendezvous_node_port = input.ports.rendezvous_node_port, + image_monerod = input.images.monerod.to_image_attribute(), + image_electrs = input.images.electrs.to_image_attribute(), + image_bitcoind = input.images.bitcoind.to_image_attribute(), + image_asb = input.images.asb.to_image_attribute(), + image_asb_controller = input.images.asb_controller.to_image_attribute(), + image_asb_tracing_logger = input.images.asb_tracing_logger.to_image_attribute(), + image_rendezvous_node = input.images.rendezvous_node.to_image_attribute(), + command_rendezvous_node = command_rendezvous_node, + asb_data_dir = input.directories.asb_data_dir.display(), + asb_config_path_on_host = input.directories.asb_config_path_on_host(), + asb_config_path_inside_container = input.directories.asb_config_path_inside_container().display(), + ); + + validate_compose(&compose_str); + + compose_str +} + +pub struct Flags(Vec); + +/// Displays a list of flags into the "Exec form" supported by Docker +/// This is documented here: +/// https://docs.docker.com/reference/dockerfile/#exec-form +/// +/// E.g ["/bin/bash", "-c", "echo hello"] +impl Display for Flags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Collect all non-none flags + let flags = self + .0 + .iter() + .filter_map(|f| f.0.as_ref()) + .collect::>(); + + // Put the " around each flag, join with a comma, put the whole thing in [] + write!( + f, + "[{}]", + flags + .into_iter() + .map(|f| format!("\"{}\"", f)) + .collect::>() + .join(",") + ) + } +} + +pub struct Flag(pub Option); + +impl Display for Flag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(s) = &self.0 { + return write!(f, "{}", s); + } + + Ok(()) + } +} + +pub trait IntoFlag { + /// Converts into a flag that can be used in a docker compose file + fn to_flag(self) -> Flag; + /// Converts into a string that can be used for display purposes + fn to_display(self) -> &'static str; +} + +pub trait IntoSpec { + fn to_spec(self) -> String; +} + +impl IntoSpec for OrchestratorInput { + fn to_spec(self) -> String { + build(self) + } +} + +/// Converts something into either a: +/// - image: +/// - build: +pub trait IntoImageAttribute { + fn to_image_attribute(self) -> String; +} + +impl IntoImageAttribute for OrchestratorImage { + fn to_image_attribute(self) -> String { + match self { + OrchestratorImage::Registry(image) => format!("image: {}", image), + OrchestratorImage::Build(input) => format!( + r#"build: {{ context: "{}", dockerfile: "{}" }}"#, + input.context, input.dockerfile + ), + } + } +} + +fn validate_compose(compose_str: &str) { + serde_yaml::from_str::(compose_str).unwrap_or_else(|_| { + panic!( + "Expected generated compose spec to be valid. But it was not. This is the spec: \n\n{}", + compose_str + ) + }); +} diff --git a/swap-p2p/Cargo.toml b/swap-p2p/Cargo.toml new file mode 100644 index 0000000000..2ef5b3cd61 --- /dev/null +++ b/swap-p2p/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "swap-p2p" +version = "0.1.0" +edition = "2024" + +[dependencies] +# Our crates +bitcoin-wallet = { path = "../bitcoin-wallet" } +swap-core = { path = "../swap-core" } +swap-env = { path = "../swap-env" } +swap-feed = { path = "../swap-feed" } +swap-machine = { path = "../swap-machine" } +swap-serde = { path = "../swap-serde" } + +# Networking +libp2p = { workspace = true, features = ["serde", "request-response", "rendezvous", "cbor", "json", "identify", "ping"] } + +# Serialization +asynchronous-codec = "0.7.0" +serde = { workspace = true } +serde_cbor = { workspace = true } +typeshare = { workspace = true } +unsigned-varint = { version = "0.8.0", features = ["codec", "asynchronous_codec"] } + +# Crypto +bitcoin = { workspace = true } +monero = { workspace = true } +rand = { workspace = true } + +# Utils +anyhow = { workspace = true } +backoff = { workspace = true } +thiserror = { workspace = true } +uuid = { workspace = true, features = ["serde"] } +void = "1" + +# Async +bmrng = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } + +# Logging +tracing = { workspace = true } + +[dev-dependencies] +async-trait = { workspace = true } +libp2p = { workspace = true, features = ["serde", "request-response", "rendezvous", "cbor", "json", "ping", "identify", "noise", "tcp", "yamux", "tokio"] } + +[lints] +workspace = true diff --git a/swap-p2p/src/futures_util.rs b/swap-p2p/src/futures_util.rs new file mode 100644 index 0000000000..ca22263268 --- /dev/null +++ b/swap-p2p/src/futures_util.rs @@ -0,0 +1,71 @@ +use libp2p::futures::future::BoxFuture; +use libp2p::futures::stream::{FuturesUnordered, StreamExt}; +use std::collections::HashSet; +use std::hash::Hash; +use std::task::{Context, Poll}; + +/// A collection of futures with associated keys that can be checked for presence +/// before completion. +/// +/// This combines a HashSet for key tracking with FuturesUnordered for efficient polling. +/// The key is provided during insertion; the future only needs to yield the value. +pub struct FuturesHashSet { + keys: HashSet, + futures: FuturesUnordered>, +} + +impl FuturesHashSet { + pub fn new() -> Self { + Self { + keys: HashSet::new(), + futures: FuturesUnordered::new(), + } + } + + /// Check if a future with the given key is already pending + pub fn contains_key(&self, key: &K) -> bool { + self.keys.contains(key) + } + + /// Insert a new future with the given key. + /// The future should yield V; the key will be paired with it when it completes. + /// Returns true if the key was newly inserted, false if it was already present. + /// If false is returned, the future is not added. + pub fn insert(&mut self, key: K, future: BoxFuture<'static, V>) -> bool { + if self.keys.insert(key.clone()) { + let key_clone = key; + let wrapped = async move { + let value = future.await; + (key_clone, value) + }; + self.futures.push(Box::pin(wrapped)); + true + } else { + false + } + } + + /// Poll for the next completed future. + /// When a future completes, its key is automatically removed from the tracking set. + pub fn poll_next_unpin(&mut self, cx: &mut Context) -> Poll> { + match self.futures.poll_next_unpin(cx) { + Poll::Ready(Some((k, v))) => { + self.keys.remove(&k); + Poll::Ready(Some((k, v))) + } + other => other, + } + } + + pub fn len(&self) -> usize { + assert_eq!(self.keys.len(), self.futures.len()); + + self.keys.len() + } +} + +impl Default for FuturesHashSet { + fn default() -> Self { + Self::new() + } +} diff --git a/swap/src/network/impl_from_rr_event.rs b/swap-p2p/src/impl_from_rr_event.rs similarity index 100% rename from swap/src/network/impl_from_rr_event.rs rename to swap-p2p/src/impl_from_rr_event.rs diff --git a/swap-p2p/src/lib.rs b/swap-p2p/src/lib.rs new file mode 100644 index 0000000000..39d54c66ca --- /dev/null +++ b/swap-p2p/src/lib.rs @@ -0,0 +1,7 @@ +pub mod futures_util; +pub mod impl_from_rr_event; +pub mod out_event; +pub mod protocols; + +#[cfg(test)] +pub mod test; diff --git a/swap-p2p/src/out_event.rs b/swap-p2p/src/out_event.rs new file mode 100644 index 0000000000..9de27854b5 --- /dev/null +++ b/swap-p2p/src/out_event.rs @@ -0,0 +1,2 @@ +pub mod alice; +pub mod bob; diff --git a/swap-p2p/src/out_event/alice.rs b/swap-p2p/src/out_event/alice.rs new file mode 100644 index 0000000000..a332c5c010 --- /dev/null +++ b/swap-p2p/src/out_event/alice.rs @@ -0,0 +1,103 @@ +use libp2p::{identify, ping}; +use libp2p::{ + request_response::{ + InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel, + }, + PeerId, +}; +use uuid::Uuid; + +use crate::protocols::{ + cooperative_xmr_redeem_after_punish, encrypted_signature, quote::BidQuote, swap_setup, +}; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum OutEvent { + SwapSetupInitiated { + send_wallet_snapshot: + bmrng::RequestReceiver, + }, + SwapSetupCompleted { + peer_id: PeerId, + swap_id: Uuid, + state3: swap_machine::alice::State3, + }, + SwapDeclined { + peer: PeerId, + error: swap_setup::alice::Error, + }, + QuoteRequested { + channel: ResponseChannel, + peer: PeerId, + }, + TransferProofAcknowledged { + peer: PeerId, + id: OutboundRequestId, + }, + EncryptedSignatureReceived { + msg: encrypted_signature::Request, + channel: ResponseChannel<()>, + peer: PeerId, + }, + CooperativeXmrRedeemRequested { + channel: ResponseChannel, + swap_id: Uuid, + peer: PeerId, + }, + Rendezvous(libp2p::rendezvous::client::Event), + OutboundRequestResponseFailure { + peer: PeerId, + error: OutboundFailure, + request_id: OutboundRequestId, + protocol: String, + }, + InboundRequestResponseFailure { + peer: PeerId, + error: InboundFailure, + request_id: InboundRequestId, + protocol: String, + }, + Failure { + peer: PeerId, + error: anyhow::Error, + }, + /// "Fallback" variant that allows the event mapping code to swallow + /// certain events that we don't want the caller to deal with. + Other, +} + +impl OutEvent { + pub fn unexpected_request(peer: PeerId) -> OutEvent { + OutEvent::Failure { + peer, + error: anyhow::anyhow!("Unexpected request received"), + } + } + + pub fn unexpected_response(peer: PeerId) -> OutEvent { + OutEvent::Failure { + peer, + error: anyhow::anyhow!("Unexpected response received"), + } + } +} + +// Some other behaviours which are not worth their own module +impl From for OutEvent { + fn from(_: ping::Event) -> Self { + OutEvent::Other + } +} + +impl From for OutEvent { + fn from(_: identify::Event) -> Self { + OutEvent::Other + } +} + +impl From for OutEvent { + fn from(e: libp2p::rendezvous::client::Event) -> Self { + OutEvent::Rendezvous(e) + } +} diff --git a/swap-p2p/src/out_event/bob.rs b/swap-p2p/src/out_event/bob.rs new file mode 100644 index 0000000000..69fab49d1e --- /dev/null +++ b/swap-p2p/src/out_event/bob.rs @@ -0,0 +1,100 @@ +use libp2p::{identify, ping}; +use libp2p::{ + request_response::{ + InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel, + }, + PeerId, +}; + +use crate::protocols::redial; +use crate::protocols::{ + cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason, quote::BidQuote, + transfer_proof, +}; + +#[derive(Debug)] +pub enum OutEvent { + QuoteReceived { + id: OutboundRequestId, + response: BidQuote, + }, + SwapSetupCompleted { + peer: PeerId, + swap_id: uuid::Uuid, + result: Box>, + }, + TransferProofReceived { + msg: Box, + channel: ResponseChannel<()>, + peer: PeerId, + }, + EncryptedSignatureAcknowledged { + id: OutboundRequestId, + }, + CooperativeXmrRedeemFulfilled { + id: OutboundRequestId, + swap_id: uuid::Uuid, + s_a: swap_core::monero::Scalar, + lock_transfer_proof: swap_core::monero::TransferProof, + }, + CooperativeXmrRedeemRejected { + id: OutboundRequestId, + reason: CooperativeXmrRedeemRejectReason, + swap_id: uuid::Uuid, + }, + Failure { + peer: PeerId, + error: anyhow::Error, + }, + OutboundRequestResponseFailure { + peer: PeerId, + error: OutboundFailure, + request_id: OutboundRequestId, + protocol: String, + }, + InboundRequestResponseFailure { + peer: PeerId, + error: InboundFailure, + request_id: InboundRequestId, + protocol: String, + }, + Redial(redial::Event), + /// "Fallback" variant that allows the event mapping code to swallow certain + /// events that we don't want the caller to deal with. + Other, +} + +impl OutEvent { + pub fn unexpected_request(peer: PeerId) -> OutEvent { + OutEvent::Failure { + peer, + error: anyhow::anyhow!("Unexpected request received"), + } + } + + pub fn unexpected_response(peer: PeerId) -> OutEvent { + OutEvent::Failure { + peer, + error: anyhow::anyhow!("Unexpected response received"), + } + } +} + +// Some other behaviours which are not worth their own module +impl From for OutEvent { + fn from(_: ping::Event) -> Self { + OutEvent::Other + } +} + +impl From for OutEvent { + fn from(_: identify::Event) -> Self { + OutEvent::Other + } +} + +impl From<()> for OutEvent { + fn from(_: ()) -> Self { + OutEvent::Other + } +} diff --git a/swap-p2p/src/protocols.rs b/swap-p2p/src/protocols.rs new file mode 100644 index 0000000000..9207dcdde8 --- /dev/null +++ b/swap-p2p/src/protocols.rs @@ -0,0 +1,7 @@ +pub mod cooperative_xmr_redeem_after_punish; +pub mod encrypted_signature; +pub mod quote; +pub mod redial; +pub mod rendezvous; +pub mod swap_setup; +pub mod transfer_proof; diff --git a/swap/src/network/cooperative_xmr_redeem_after_punish.rs b/swap-p2p/src/protocols/cooperative_xmr_redeem_after_punish.rs similarity index 90% rename from swap/src/network/cooperative_xmr_redeem_after_punish.rs rename to swap-p2p/src/protocols/cooperative_xmr_redeem_after_punish.rs index e54e8c96e8..534126ff03 100644 --- a/swap/src/network/cooperative_xmr_redeem_after_punish.rs +++ b/swap-p2p/src/protocols/cooperative_xmr_redeem_after_punish.rs @@ -1,9 +1,9 @@ -use crate::monero::{Scalar, TransferProof}; -use crate::{asb, cli}; +use crate::out_event; use libp2p::request_response::ProtocolSupport; use libp2p::{request_response, PeerId, StreamProtocol}; use serde::{Deserialize, Serialize}; use std::time::Duration; +use swap_core::monero::{Scalar, TransferProof}; use uuid::Uuid; const PROTOCOL: &str = "/comit/xmr/btc/cooperative_xmr_redeem_after_punish/1.0.0"; @@ -69,7 +69,7 @@ pub fn bob() -> Behaviour { ) } -impl From<(PeerId, Message)> for asb::OutEvent { +impl From<(PeerId, Message)> for out_event::alice::OutEvent { fn from((peer, message): (PeerId, Message)) -> Self { match message { Message::Request { @@ -84,9 +84,9 @@ impl From<(PeerId, Message)> for asb::OutEvent { } } -crate::impl_from_rr_event!(OutEvent, asb::OutEvent, PROTOCOL); +crate::impl_from_rr_event!(OutEvent, out_event::alice::OutEvent, PROTOCOL); -impl From<(PeerId, Message)> for cli::OutEvent { +impl From<(PeerId, Message)> for out_event::bob::OutEvent { fn from((peer, message): (PeerId, Message)) -> Self { match message { Message::Request { .. } => Self::unexpected_request(peer), @@ -117,4 +117,4 @@ impl From<(PeerId, Message)> for cli::OutEvent { } } -crate::impl_from_rr_event!(OutEvent, cli::OutEvent, PROTOCOL); +crate::impl_from_rr_event!(OutEvent, out_event::bob::OutEvent, PROTOCOL); diff --git a/swap/src/network/encrypted_signature.rs b/swap-p2p/src/protocols/encrypted_signature.rs similarity index 87% rename from swap/src/network/encrypted_signature.rs rename to swap-p2p/src/protocols/encrypted_signature.rs index 8f69328caa..77ec5a66cd 100644 --- a/swap/src/network/encrypted_signature.rs +++ b/swap-p2p/src/protocols/encrypted_signature.rs @@ -1,4 +1,4 @@ -use crate::{asb, cli}; +use crate::out_event; use libp2p::request_response::{self}; use libp2p::{PeerId, StreamProtocol}; use serde::{Deserialize, Serialize}; @@ -46,7 +46,7 @@ pub fn bob() -> Behaviour { ) } -impl From<(PeerId, Message)> for asb::OutEvent { +impl From<(PeerId, Message)> for out_event::alice::OutEvent { fn from((peer, message): (PeerId, Message)) -> Self { match message { Message::Request { @@ -60,9 +60,9 @@ impl From<(PeerId, Message)> for asb::OutEvent { } } } -crate::impl_from_rr_event!(OutEvent, asb::OutEvent, PROTOCOL); +crate::impl_from_rr_event!(OutEvent, out_event::alice::OutEvent, PROTOCOL); -impl From<(PeerId, Message)> for cli::OutEvent { +impl From<(PeerId, Message)> for out_event::bob::OutEvent { fn from((peer, message): (PeerId, Message)) -> Self { match message { Message::Request { .. } => Self::unexpected_request(peer), @@ -72,4 +72,5 @@ impl From<(PeerId, Message)> for cli::OutEvent { } } } -crate::impl_from_rr_event!(OutEvent, cli::OutEvent, PROTOCOL); + +crate::impl_from_rr_event!(OutEvent, out_event::bob::OutEvent, PROTOCOL); diff --git a/swap/src/network/quote.rs b/swap-p2p/src/protocols/quote.rs similarity index 91% rename from swap/src/network/quote.rs rename to swap-p2p/src/protocols/quote.rs index dc5523f701..a7fe31f84b 100644 --- a/swap/src/network/quote.rs +++ b/swap-p2p/src/protocols/quote.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::{asb, cli}; +use crate::out_event; use libp2p::request_response::{self, ProtocolSupport}; use libp2p::{PeerId, StreamProtocol}; use serde::{Deserialize, Serialize}; @@ -76,7 +76,7 @@ pub fn cli() -> Behaviour { ) } -impl From<(PeerId, Message)> for asb::OutEvent { +impl From<(PeerId, Message)> for out_event::alice::OutEvent { fn from((peer, message): (PeerId, Message)) -> Self { match message { Message::Request { channel, .. } => Self::QuoteRequested { channel, peer }, @@ -84,9 +84,9 @@ impl From<(PeerId, Message)> for asb::OutEvent { } } } -crate::impl_from_rr_event!(OutEvent, asb::OutEvent, PROTOCOL); +crate::impl_from_rr_event!(OutEvent, out_event::alice::OutEvent, PROTOCOL); -impl From<(PeerId, Message)> for cli::OutEvent { +impl From<(PeerId, Message)> for out_event::bob::OutEvent { fn from((peer, message): (PeerId, Message)) -> Self { match message { Message::Request { .. } => Self::unexpected_request(peer), @@ -100,4 +100,4 @@ impl From<(PeerId, Message)> for cli::OutEvent { } } } -crate::impl_from_rr_event!(OutEvent, cli::OutEvent, PROTOCOL); +crate::impl_from_rr_event!(OutEvent, out_event::bob::OutEvent, PROTOCOL); diff --git a/swap-p2p/src/protocols/redial.rs b/swap-p2p/src/protocols/redial.rs new file mode 100644 index 0000000000..369a1bb538 --- /dev/null +++ b/swap-p2p/src/protocols/redial.rs @@ -0,0 +1,264 @@ +use crate::futures_util::FuturesHashSet; +use crate::out_event; +use backoff::backoff::Backoff; +use backoff::ExponentialBackoff; +use libp2p::core::Multiaddr; +use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; +use libp2p::swarm::{DialError, FromSwarm, NetworkBehaviour, ToSwarm}; +use libp2p::PeerId; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::task::{Context, Poll}; +use std::time::Duration; +use void::Void; + +/// A [`NetworkBehaviour`] that tracks whether we are connected to the given +/// peers and attempts to re-establish a connection with an exponential backoff +/// if we lose the connection. +/// +/// TODO: Allow removing peers from the set after we are done with them. +pub struct Behaviour { + /// The peers we are interested in. + peers: HashSet, + /// Tracks sleep timers for each peer waiting to redial. + /// Futures in here yield the PeerId and when a Future completes we dial that peer + to_dial: FuturesHashSet, + /// Tracks the current backoff state for each peer. + backoff: HashMap, + /// Initial interval for backoff. + initial_interval: Duration, + /// Maximum interval for backoff. + max_interval: Duration, + /// A queue of events to be sent to the swarm. + to_swarm: VecDeque, + /// An identifier for this redial behaviour instance (for logging/tracing). + name: &'static str, +} + +impl Behaviour { + pub fn new(name: &'static str, interval: Duration, max_interval: Duration) -> Self { + Self { + peers: HashSet::default(), + to_dial: FuturesHashSet::new(), + backoff: HashMap::new(), + initial_interval: interval, + max_interval, + to_swarm: VecDeque::new(), + name, + } + } + + /// Adds a peer to the set of peers to track. Returns true if the peer was newly added. + #[tracing::instrument(level = "trace", name = "redial::add_peer", skip(self, peer), fields(redial_type = %self.name, peer = %peer))] + pub fn add_peer(&mut self, peer: PeerId) -> bool { + let newly_added = self.peers.insert(peer); + + // If the peer is newly added, schedule a dial immediately + if newly_added { + self.schedule_redial(&peer, Duration::ZERO); + } + + tracing::trace!("Added a new peer to the set of peers we want to contineously redial"); + + newly_added + } + + fn get_backoff(&mut self, peer: &PeerId) -> &mut ExponentialBackoff { + self.backoff.entry(*peer).or_insert_with(|| { + ExponentialBackoff { + initial_interval: self.initial_interval, + current_interval: self.initial_interval, + max_interval: self.max_interval, + // We never give up on re-dialling + max_elapsed_time: None, + ..ExponentialBackoff::default() + } + }) + } + + #[tracing::instrument(level = "trace", name = "redial::schedule_redial", skip(self, peer, override_next_dial_in), fields(redial_type = %self.name, peer = %peer))] + fn schedule_redial( + &mut self, + peer: &PeerId, + override_next_dial_in: impl Into>, + ) -> bool { + // We first check if there already is a pending scheduled redial + // because want do not want to increment the backoff if there is + if self.to_dial.contains_key(peer) { + return false; + } + + // How long should we wait before we redial the peer? + // If an override is provided, use that, otherwise use the backoff + let next_dial_in = override_next_dial_in.into().unwrap_or_else(|| { + self.get_backoff(peer) + .next_backoff() + .expect("redial backoff should never run out of attempts") + }); + + let did_queue_new_dial = self.to_dial.insert( + peer.clone(), + Box::pin(async move { + tokio::time::sleep(next_dial_in).await; + }), + ); + + // We check if there is an entry before inserting a new one, so this should always be true + // TODO: We could make this a production assert if we want to be more strict + debug_assert!(did_queue_new_dial); + + self.to_swarm.push_back(Event::ScheduledRedial { + peer: peer.clone(), + next_dial_in, + }); + + tracing::trace!( + seconds_until_next_redial = %next_dial_in.as_secs(), + "Scheduled a redial attempt for a peer" + ); + + return true; + } + + pub fn has_pending_redial(&self, peer: &PeerId) -> bool { + self.to_dial.contains_key(peer) + } +} + +#[derive(Debug)] +pub enum Event { + ScheduledRedial { + peer: PeerId, + next_dial_in: Duration, + }, +} + +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = libp2p::swarm::dummy::ConnectionHandler; + type ToSwarm = Event; + + #[tracing::instrument(level = "trace", name = "redial::on_swarm_event", skip(self, event), fields(redial_type = %self.name))] + fn on_swarm_event(&mut self, event: FromSwarm<'_>) { + // Check if the event was for either: + // - a failed dial + // - a closed connection + // + // We will then schedule a redial for the peer + let peer_to_redial = match event { + FromSwarm::ConnectionClosed(event) if self.peers.contains(&event.peer_id) => { + tracing::trace!(peer = %event.peer_id, "A connection was closed for a peer we want to contineously redial. We will schedule a redial."); + + Some(event.peer_id) + } + FromSwarm::DialFailure(event) => match event.peer_id { + Some(peer_id) if self.peers.contains(&peer_id) => { + match event.error { + DialError::DialPeerConditionFalse(_) => { + // TODO: Can this lead to a condition where we will not redial the peer ever again? I don't think so... + // + // Reasoning: + // We always dial with `PeerCondition::DisconnectedAndNotDialing`. + // If we not disconnected, we don't need to redial. + // If we are already dialing, another event will be emitted if that dial fails. + tracing::trace!(peer = %peer_id, dial_error = ?event.error, "A dial failure occurred for a peer we want to contineously redial, but this was due to a dial condition failure. We are not treating this as a failure. We will not schedule a redial."); + None + } + _ => { + tracing::trace!(peer = %peer_id, dial_error = ?event.error, "A dial failure occurred for a peer we want to contineously redial. We will schedule a redial."); + Some(peer_id) + } + } + } + _ => None, + }, + _ => None, + }; + + // Check if the event was for a successful connection + // We will then reset the backoff state for the peer + let peer_to_reset = match event { + FromSwarm::ConnectionEstablished(e) if self.peers.contains(&e.peer_id) => { + tracing::trace!(peer = %e.peer_id, "A connection was established for a peer we want to contineously redial, resetting backoff state"); + + Some(e.peer_id) + } + _ => None, + }; + + // Reset the backoff state for the peer if needed + if let Some(peer) = peer_to_reset { + if let Some(backoff) = self.backoff.get_mut(&peer) { + backoff.reset(); + } + } + + // Schedule a redial if needed + if let Some(peer) = peer_to_redial { + self.schedule_redial(&peer, None); + } + } + + #[tracing::instrument(level = "trace", name = "redial::poll", skip(self, cx), fields(redial_type = %self.name))] + fn poll(&mut self, cx: &mut Context<'_>) -> std::task::Poll> { + // Check if we have any event to send to the swarm + if let Some(event) = self.to_swarm.pop_front() { + return Poll::Ready(ToSwarm::GenerateEvent(event)); + } + + // Check if any peer's sleep timer has completed + // If it has, dial that peer + if let Poll::Ready(Some((peer, _))) = self.to_dial.poll_next_unpin(cx) { + tracing::trace!(peer = %peer, "Instructing swarm to redial a peer we want to contineously redial after the sleep timer completed"); + + // Actually dial the peer + return Poll::Ready(ToSwarm::Dial { + opts: DialOpts::peer_id(peer) + .condition(PeerCondition::DisconnectedAndNotDialing) + .build(), + }); + } + + Poll::Pending + } + + fn on_connection_handler_event( + &mut self, + _peer_id: PeerId, + _connection_id: libp2p::swarm::ConnectionId, + _event: libp2p::swarm::THandlerOutEvent, + ) { + unreachable!("The re-dial dummy connection handler does not produce any events"); + } + + fn handle_established_inbound_connection( + &mut self, + _connection_id: libp2p::swarm::ConnectionId, + _peer: PeerId, + _local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result, libp2p::swarm::ConnectionDenied> { + Ok(Self::ConnectionHandler {}) + } + + fn handle_established_outbound_connection( + &mut self, + _connection_id: libp2p::swarm::ConnectionId, + _peer: PeerId, + _addr: &Multiaddr, + _role_override: libp2p::core::Endpoint, + ) -> Result, libp2p::swarm::ConnectionDenied> { + Ok(Self::ConnectionHandler {}) + } +} + +impl From for out_event::bob::OutEvent { + fn from(event: Event) -> Self { + out_event::bob::OutEvent::Redial(event) + } +} + +impl From for out_event::alice::OutEvent { + fn from(_event: Event) -> Self { + // TODO: Once this is used by Alice, convert this to a proper event + out_event::alice::OutEvent::Other + } +} diff --git a/swap-p2p/src/protocols/rendezvous.rs b/swap-p2p/src/protocols/rendezvous.rs new file mode 100644 index 0000000000..007d3daa1e --- /dev/null +++ b/swap-p2p/src/protocols/rendezvous.rs @@ -0,0 +1,637 @@ +use libp2p::rendezvous::Namespace; +use std::fmt; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum XmrBtcNamespace { + Mainnet, + Testnet, + RendezvousPoint, +} + +const MAINNET: &str = "xmr-btc-swap-mainnet"; +const TESTNET: &str = "xmr-btc-swap-testnet"; +const RENDEZVOUS_POINT: &str = "rendezvous-point"; + +impl fmt::Display for XmrBtcNamespace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + XmrBtcNamespace::Mainnet => write!(f, "{}", MAINNET), + XmrBtcNamespace::Testnet => write!(f, "{}", TESTNET), + XmrBtcNamespace::RendezvousPoint => write!(f, "{}", RENDEZVOUS_POINT), + } + } +} + +impl From for Namespace { + fn from(namespace: XmrBtcNamespace) -> Self { + match namespace { + XmrBtcNamespace::Mainnet => Namespace::from_static(MAINNET), + XmrBtcNamespace::Testnet => Namespace::from_static(TESTNET), + XmrBtcNamespace::RendezvousPoint => Namespace::from_static(RENDEZVOUS_POINT), + } + } +} + +impl XmrBtcNamespace { + pub fn from_is_testnet(testnet: bool) -> XmrBtcNamespace { + if testnet { + XmrBtcNamespace::Testnet + } else { + XmrBtcNamespace::Mainnet + } + } +} + +/// A behaviour that periodically re-registers at multiple rendezvous points as a client +pub mod register { + use super::*; + use backoff::backoff::Backoff; + use backoff::ExponentialBackoff; + use futures::future::BoxFuture; + use futures::stream::FuturesUnordered; + use futures::{FutureExt, StreamExt}; + use libp2p::rendezvous::client::RegisterError; + use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; + use libp2p::swarm::{ + ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, + }; + use libp2p::{identity, Multiaddr, PeerId}; + use std::collections::HashMap; + use std::pin::Pin; + use std::task::{Context, Poll}; + use std::time::Duration; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum ConnectionStatus { + Disconnected, + Dialling, + Connected, + } + + enum RegistrationStatus { + RegisterOnNextConnection, + Pending, + Registered { + re_register_in: Pin>, + }, + } + + pub struct Behaviour { + inner: libp2p::rendezvous::client::Behaviour, + rendezvous_nodes: Vec, + // always use schedule_dial to schedule a dial + // do not insert directly into this future + to_dial: FuturesUnordered>, + backoffs: HashMap, + } + + // Provide a read-only snapshot of rendezvous registrations + impl Behaviour { + /// Returns a snapshot of registration and connection status for all configured rendezvous nodes. + pub fn registrations(&self) -> Vec { + self.rendezvous_nodes + .iter() + .map(|n| RegistrationReport { + address: n.address.clone(), + connection: n.connection_status, + registration: match &n.registration_status { + RegistrationStatus::RegisterOnNextConnection => { + RegistrationStatusReport::RegisterOnNextConnection + } + RegistrationStatus::Pending => RegistrationStatusReport::Pending, + RegistrationStatus::Registered { .. } => { + RegistrationStatusReport::Registered + } + }, + }) + .collect() + } + } + + /// Public representation of a rendezvous node registration status + /// The raw `RegistrationStatus` cannot be exposed because it is not serializable + #[derive(Debug, Clone)] + pub struct RegistrationReport { + pub address: Multiaddr, + pub connection: ConnectionStatus, + pub registration: RegistrationStatusReport, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum RegistrationStatusReport { + RegisterOnNextConnection, + Pending, + Registered, + } + + /// A node running the rendezvous server protocol. + pub struct RendezvousNode { + pub address: Multiaddr, + connection_status: ConnectionStatus, + pub peer_id: PeerId, + registration_status: RegistrationStatus, + pub registration_ttl: Option, + pub namespace: XmrBtcNamespace, + } + + impl RendezvousNode { + pub fn new( + address: &Multiaddr, + peer_id: PeerId, + namespace: XmrBtcNamespace, + registration_ttl: Option, + ) -> Self { + Self { + address: address.to_owned(), + connection_status: ConnectionStatus::Disconnected, + namespace, + peer_id, + registration_status: RegistrationStatus::RegisterOnNextConnection, + registration_ttl, + } + } + + fn set_connection(&mut self, status: ConnectionStatus) { + self.connection_status = status; + } + + fn set_registration(&mut self, status: RegistrationStatus) { + self.registration_status = status; + } + } + + impl Behaviour { + pub fn new(identity: identity::Keypair, rendezvous_nodes: Vec) -> Self { + let our_peer_id = identity.public().to_peer_id(); + let rendezvous_nodes: Vec = rendezvous_nodes + .into_iter() + .filter(|node| node.peer_id != our_peer_id) + .collect(); + + let mut backoffs = HashMap::new(); + + // Initialize backoff for each rendezvous node + for node in &rendezvous_nodes { + backoffs.insert( + node.peer_id, + ExponentialBackoff { + // 5 minutes max interval + max_interval: Duration::from_secs(5 * 60), + // Never give up + max_elapsed_time: None, + // We retry aggressively. We begin with 50ms and increase by 10% per retry. + multiplier: 1.1f64, + initial_interval: Duration::from_millis(50), + current_interval: Duration::from_millis(50), + ..ExponentialBackoff::default() + }, + ); + } + + Self { + inner: libp2p::rendezvous::client::Behaviour::new(identity), + rendezvous_nodes, + to_dial: FuturesUnordered::new(), + backoffs, + } + } + + /// Registers the rendezvous node at the given index. + /// Also sets the registration status to [`RegistrationStatus::Pending`]. + pub fn register(&mut self, node_index: usize) -> Result<(), RegisterError> { + let node = &mut self.rendezvous_nodes[node_index]; + node.set_registration(RegistrationStatus::Pending); + let (namespace, peer_id, ttl) = + (node.namespace.into(), node.peer_id, node.registration_ttl); + self.inner.register(namespace, peer_id, ttl) + } + + /// Schedules a dial to a peer with exponential backoff delay. + fn schedule_dial(&mut self, peer_id: PeerId) { + let backoff = self + .backoffs + .get_mut(&peer_id) + .expect("Backoff should exist for all rendezvous nodes"); + let delay = backoff + .next_backoff() + .expect("Backoff should never run out"); + + // Create a future that sleeps and then returns the peer_id + let future = async move { + tokio::time::sleep(delay).await; + peer_id + }; + + self.to_dial.push(future.boxed()); + + // Set the connection status to Dialling + if let Some(node) = self + .rendezvous_nodes + .iter_mut() + .find(|node| node.peer_id == peer_id) + { + node.set_connection(ConnectionStatus::Dialling); + } + } + } + + impl NetworkBehaviour for Behaviour { + type ConnectionHandler = + ::ConnectionHandler; + type ToSwarm = libp2p::rendezvous::client::Event; + + fn handle_established_inbound_connection( + &mut self, + connection_id: ConnectionId, + peer: PeerId, + local_addr: &Multiaddr, + remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + self.inner.handle_established_inbound_connection( + connection_id, + peer, + local_addr, + remote_addr, + ) + } + + fn handle_established_outbound_connection( + &mut self, + connection_id: ConnectionId, + peer: PeerId, + addr: &Multiaddr, + role_override: libp2p::core::Endpoint, + ) -> Result, ConnectionDenied> { + self.inner.handle_established_outbound_connection( + connection_id, + peer, + addr, + role_override, + ) + } + + fn handle_pending_outbound_connection( + &mut self, + connection_id: ConnectionId, + maybe_peer: Option, + addresses: &[Multiaddr], + effective_role: libp2p::core::Endpoint, + ) -> std::result::Result, ConnectionDenied> { + self.inner.handle_pending_outbound_connection( + connection_id, + maybe_peer, + addresses, + effective_role, + ) + } + + fn on_swarm_event(&mut self, event: FromSwarm<'_>) { + match event { + FromSwarm::ConnectionEstablished(connection) => { + let peer_id = connection.peer_id; + + // Find the rendezvous node that matches the peer id, else do nothing. + if let Some(index) = self + .rendezvous_nodes + .iter_mut() + .position(|node| node.peer_id == peer_id) + { + let rendezvous_node = &mut self.rendezvous_nodes[index]; + rendezvous_node.set_connection(ConnectionStatus::Connected); + + // Reset backoff on successful connection + if let Some(backoff) = self.backoffs.get_mut(&peer_id) { + backoff.reset(); + } + + if let RegistrationStatus::RegisterOnNextConnection = + rendezvous_node.registration_status + { + let _ = self.register(index).inspect_err(|err| { + tracing::error!( + error=%err, + rendezvous_node=%peer_id, + "Failed to register with rendezvous node"); + }); + } + } + } + FromSwarm::ConnectionClosed(connection) => { + let peer_id = connection.peer_id; + + // Update the connection status of the rendezvous node that disconnected. + if let Some(node) = self + .rendezvous_nodes + .iter_mut() + .find(|node| node.peer_id == peer_id) + { + node.set_connection(ConnectionStatus::Disconnected); + self.schedule_dial(peer_id); + } + } + FromSwarm::DialFailure(dial_failure) => { + // Update the connection status of the rendezvous node that failed to connect. + if let Some(peer_id) = dial_failure.peer_id { + if let Some(node) = self + .rendezvous_nodes + .iter_mut() + .find(|node| node.peer_id == peer_id) + { + node.set_connection(ConnectionStatus::Disconnected); + self.schedule_dial(peer_id); + } + } + } + _ => {} + } + self.inner.on_swarm_event(event); + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: THandlerOutEvent, + ) { + self.inner + .on_connection_handler_event(peer_id, connection_id, event) + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + // Check if we need to dial a peer + if let Poll::Ready(Some(peer_id)) = self.to_dial.poll_next_unpin(cx) { + // This should be redundant as this is already set in the schedule_dial function + // we still do it here to be safe + if let Some(node) = self + .rendezvous_nodes + .iter_mut() + .find(|node| node.peer_id == peer_id) + { + node.set_connection(ConnectionStatus::Dialling); + } + + return Poll::Ready(ToSwarm::Dial { + opts: DialOpts::peer_id(peer_id) + .addresses(vec![self + .rendezvous_nodes + .iter() + .find(|node| node.peer_id == peer_id) + .map(|node| node.address.clone()) + .expect("We should have a rendezvous node for the peer id")]) + .condition(PeerCondition::Disconnected) + // TODO: this makes the behaviour call `NetworkBehaviour::handle_pending_outbound_connection` + // but we don't implement it + .extend_addresses_through_behaviour() + .build(), + }); + } + + // Check the status of each rendezvous node + for i in 0..self.rendezvous_nodes.len() { + let connection_status = self.rendezvous_nodes[i].connection_status.clone(); + match &mut self.rendezvous_nodes[i].registration_status { + RegistrationStatus::RegisterOnNextConnection => match connection_status { + ConnectionStatus::Disconnected => { + let peer_id = self.rendezvous_nodes[i].peer_id; + self.schedule_dial(peer_id); + } + ConnectionStatus::Dialling => {} + ConnectionStatus::Connected => { + let _ = self.register(i); + } + }, + RegistrationStatus::Registered { re_register_in } => { + if let Poll::Ready(()) = re_register_in.poll_unpin(cx) { + match connection_status { + ConnectionStatus::Connected => { + let _ = self.register(i).inspect_err(|err| { + tracing::error!( + error=%err, + rendezvous_node=%self.rendezvous_nodes[i].peer_id, + "Failed to register with rendezvous node"); + }); + } + ConnectionStatus::Disconnected => { + let peer_id = self.rendezvous_nodes[i].peer_id; + self.rendezvous_nodes[i].set_registration( + RegistrationStatus::RegisterOnNextConnection, + ); + self.schedule_dial(peer_id); + } + ConnectionStatus::Dialling => {} + } + } + } + RegistrationStatus::Pending => {} + } + } + + let inner_poll = self.inner.poll(cx); + + // reset the timer for the specific rendezvous node if we successfully registered + if let Poll::Ready(ToSwarm::GenerateEvent( + libp2p::rendezvous::client::Event::Registered { + ttl, + rendezvous_node, + .. + }, + )) = &inner_poll + { + if let Some(i) = self + .rendezvous_nodes + .iter() + .position(|n| &n.peer_id == rendezvous_node) + { + let half_of_ttl = Duration::from_secs(*ttl) / 2; + let re_register_in = Box::pin(tokio::time::sleep(half_of_ttl)); + let status = RegistrationStatus::Registered { re_register_in }; + self.rendezvous_nodes[i].set_registration(status); + } + } + + inner_poll + } + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::test::{new_swarm, SwarmExt}; + use futures::StreamExt; + use libp2p::rendezvous; + use libp2p::swarm::SwarmEvent; + use std::collections::HashMap; + + #[tokio::test] + // Due to an issue with the libp2p rendezvous library + // This needs to be fixed upstream and was + // introduced in our codebase by a libp2p refactor which bumped the version of libp2p: + // + // - The new bumped rendezvous client works, and can connect to an old rendezvous server + // - The new rendezvous has an issue, which is why these test (use the new mock server) + // do not work + // + // Ignore this test for now . This works in production :) + async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node( + ) { + let mut rendezvous_node = new_swarm(|_| { + rendezvous::server::Behaviour::new(rendezvous::server::Config::default()) + }); + let address = rendezvous_node.listen_on_random_memory_address().await; + let rendezvous_point = RendezvousNode::new( + &address, + rendezvous_node.local_peer_id().to_owned(), + XmrBtcNamespace::Testnet, + None, + ); + + let mut asb = + new_swarm(|identity| super::Behaviour::new(identity, vec![rendezvous_point])); + asb.listen_on_random_memory_address().await; // this adds an external address + + tokio::spawn(async move { + loop { + rendezvous_node.next().await; + } + }); + let asb_registered = tokio::spawn(async move { + loop { + if let SwarmEvent::Behaviour(rendezvous::client::Event::Registered { .. }) = + asb.select_next_some().await + { + break; + } + } + }); + + tokio::time::timeout(Duration::from_secs(10), asb_registered) + .await + .unwrap() + .unwrap(); + } + + #[tokio::test] + // Due to an issue with the libp2p rendezvous library + // This needs to be fixed upstream and was + // introduced in our codebase by a libp2p refactor which bumped the version of libp2p: + // + // - The new bumped rendezvous client works, and can connect to an old rendezvous server + // - The new rendezvous has an issue, which is why these test (use the new mock server) + // do not work + // + // Ignore this test for now . This works in production :) + async fn asb_automatically_re_registers() { + let mut rendezvous_node = new_swarm(|_| { + rendezvous::server::Behaviour::new( + rendezvous::server::Config::default().with_min_ttl(2), + ) + }); + let address = rendezvous_node.listen_on_random_memory_address().await; + let rendezvous_point = RendezvousNode::new( + &address, + rendezvous_node.local_peer_id().to_owned(), + XmrBtcNamespace::Testnet, + Some(5), + ); + + let mut asb = + new_swarm(|identity| super::Behaviour::new(identity, vec![rendezvous_point])); + asb.listen_on_random_memory_address().await; // this adds an external address + + tokio::spawn(async move { + loop { + rendezvous_node.next().await; + } + }); + let asb_registered_three_times = tokio::spawn(async move { + let mut number_of_registrations = 0; + + loop { + if let SwarmEvent::Behaviour(rendezvous::client::Event::Registered { .. }) = + asb.select_next_some().await + { + number_of_registrations += 1 + } + + if number_of_registrations == 3 { + break; + } + } + }); + + tokio::time::timeout(Duration::from_secs(30), asb_registered_three_times) + .await + .unwrap() + .unwrap(); + } + + #[tokio::test] + // Due to an issue with the libp2p rendezvous library + // This needs to be fixed upstream and was + // introduced in our codebase by a libp2p refactor which bumped the version of libp2p: + // + // - The new bumped rendezvous client works, and can connect to an old rendezvous server + // - The new rendezvous has an issue, which is why these test (use the new mock server) + // do not work + // + // Ignore this test for now . This works in production :) + async fn asb_registers_multiple() { + let registration_ttl = Some(10); + let mut rendezvous_nodes = Vec::new(); + let mut registrations = HashMap::new(); + // register with 5 rendezvous nodes + for _ in 0..5 { + let mut rendezvous = new_swarm(|_| { + rendezvous::server::Behaviour::new( + rendezvous::server::Config::default().with_min_ttl(2), + ) + }); + let address = rendezvous.listen_on_random_memory_address().await; + let id = *rendezvous.local_peer_id(); + registrations.insert(id, 0); + rendezvous_nodes.push(RendezvousNode::new( + &address, + *rendezvous.local_peer_id(), + XmrBtcNamespace::Testnet, + registration_ttl, + )); + tokio::spawn(async move { + loop { + rendezvous.next().await; + } + }); + } + + let mut asb = + new_swarm(|identity| register::Behaviour::new(identity, rendezvous_nodes)); + asb.listen_on_random_memory_address().await; // this adds an external address + + let handle = tokio::spawn(async move { + loop { + if let SwarmEvent::Behaviour(rendezvous::client::Event::Registered { + rendezvous_node, + .. + }) = asb.select_next_some().await + { + registrations + .entry(rendezvous_node) + .and_modify(|counter| *counter += 1); + } + + if registrations.iter().all(|(_, &count)| count >= 4) { + break; + } + } + }); + + tokio::time::timeout(Duration::from_secs(30), handle) + .await + .unwrap() + .unwrap(); + } + } +} diff --git a/swap/src/network/swap_setup.rs b/swap-p2p/src/protocols/swap_setup.rs similarity index 98% rename from swap/src/network/swap_setup.rs rename to swap-p2p/src/protocols/swap_setup.rs index 6617ba6526..60f50ceb1b 100644 --- a/swap/src/network/swap_setup.rs +++ b/swap-p2p/src/protocols/swap_setup.rs @@ -1,4 +1,3 @@ -use crate::monero; use anyhow::{Context, Result}; use asynchronous_codec::{Bytes, Framed}; use futures::{SinkExt, StreamExt}; @@ -51,7 +50,7 @@ pub struct SpotPriceRequest { #[derive(Serialize, Deserialize, Debug, Clone)] pub enum SpotPriceResponse { - Xmr(monero::Amount), + Xmr(swap_core::monero::Amount), Error(SpotPriceError), } diff --git a/swap/src/network/swap_setup/alice.rs b/swap-p2p/src/protocols/swap_setup/alice.rs similarity index 91% rename from swap/src/network/swap_setup/alice.rs rename to swap-p2p/src/protocols/swap_setup/alice.rs index dcd9cf9bd5..42a8bf4cff 100644 --- a/swap/src/network/swap_setup/alice.rs +++ b/swap-p2p/src/protocols/swap_setup/alice.rs @@ -1,13 +1,9 @@ -use crate::asb::LatestRate; -use crate::network::swap_setup; -use crate::network::swap_setup::{ +use crate::out_event; +use crate::protocols::swap_setup; +use crate::protocols::swap_setup::{ protocol, BlockchainNetwork, SpotPriceError, SpotPriceRequest, SpotPriceResponse, }; -use crate::protocol::alice::{State0, State3}; -use crate::protocol::{Message0, Message2, Message4}; -use crate::{asb, monero}; use anyhow::{anyhow, Context, Result}; -use bitcoin_wallet::BitcoinWallet; use futures::future::{BoxFuture, OptionFuture}; use futures::AsyncWriteExt; use futures::FutureExt; @@ -18,11 +14,13 @@ use libp2p::swarm::{ConnectionHandlerEvent, NetworkBehaviour, SubstreamProtocol, use libp2p::{Multiaddr, PeerId}; use std::collections::VecDeque; use std::fmt::Debug; -use std::sync::Arc; use std::task::Poll; use std::time::{Duration, Instant}; use swap_core::bitcoin; use swap_env::env; +use swap_feed::LatestRate; +use swap_machine::alice::{State0, State3}; +use swap_machine::common::{Message0, Message2, Message4}; use uuid::Uuid; #[derive(Debug)] @@ -44,8 +42,8 @@ pub enum OutEvent { #[derive(Debug)] pub struct WalletSnapshot { - unlocked_balance: monero::Amount, - lock_fee: monero::Amount, + unlocked_balance: swap_core::monero::Amount, + lock_fee: swap_core::monero::Amount, // TODO: Consider using the same address for punish and redeem (they are mutually exclusive, so // effectively the address will only be used once) @@ -57,60 +55,42 @@ pub struct WalletSnapshot { } impl WalletSnapshot { - pub async fn capture( - bitcoin_wallet: Arc, - monero_wallet: &monero::Wallets, - external_redeem_address: &Option, - transfer_amount: bitcoin::Amount, - ) -> Result { - let unlocked_balance = monero_wallet.main_wallet().await.unlocked_balance().await; - let total_balance = monero_wallet.main_wallet().await.total_balance().await; - - tracing::info!(%unlocked_balance, %total_balance, "Capturing monero wallet snapshot"); - - let redeem_address = external_redeem_address - .clone() - .unwrap_or(bitcoin_wallet.new_address().await?); - let punish_address = external_redeem_address - .clone() - .unwrap_or(bitcoin_wallet.new_address().await?); - - let redeem_fee = bitcoin_wallet - .estimate_fee(bitcoin::TxRedeem::weight(), Some(transfer_amount)) - .await?; - let punish_fee = bitcoin_wallet - .estimate_fee(bitcoin::TxPunish::weight(), Some(transfer_amount)) - .await?; - - Ok(Self { - unlocked_balance: unlocked_balance.into(), - lock_fee: monero::CONSERVATIVE_MONERO_FEE, + pub fn new( + unlocked_balance: swap_core::monero::Amount, + redeem_address: bitcoin::Address, + punish_address: bitcoin::Address, + redeem_fee: bitcoin::Amount, + punish_fee: bitcoin::Amount, + ) -> Self { + Self { + unlocked_balance, + lock_fee: swap_core::monero::CONSERVATIVE_MONERO_FEE, redeem_address, punish_address, redeem_fee, punish_fee, - }) + } } } -impl From for asb::OutEvent { +impl From for out_event::alice::OutEvent { fn from(event: OutEvent) -> Self { match event { OutEvent::Initiated { send_wallet_snapshot, - } => asb::OutEvent::SwapSetupInitiated { + } => out_event::alice::OutEvent::SwapSetupInitiated { send_wallet_snapshot, }, OutEvent::Completed { peer_id: bob_peer_id, swap_id, state3, - } => asb::OutEvent::SwapSetupCompleted { + } => out_event::alice::OutEvent::SwapSetupCompleted { peer_id: bob_peer_id, swap_id, state3, }, - OutEvent::Error { peer_id, error } => asb::OutEvent::Failure { + OutEvent::Error { peer_id, error } => out_event::alice::OutEvent::Failure { peer: peer_id, error: anyhow!(error), }, @@ -529,7 +509,7 @@ where } impl SpotPriceResponse { - pub fn from_result_ref(result: &Result) -> Self { + pub fn from_result_ref(result: &Result) -> Self { match result { Ok(amount) => SpotPriceResponse::Xmr(*amount), Err(error) => SpotPriceResponse::Error(error.to_error_response()), @@ -553,7 +533,7 @@ pub enum Error { }, #[error("Unlocked balance ({balance}) too low to fulfill swapping {buy}")] BalanceTooLow { - balance: monero::Amount, + balance: swap_core::monero::Amount, buy: bitcoin::Amount, }, #[error("Failed to fetch latest rate")] diff --git a/swap-p2p/src/protocols/swap_setup/bob.rs b/swap-p2p/src/protocols/swap_setup/bob.rs new file mode 100644 index 0000000000..279eb0e746 --- /dev/null +++ b/swap-p2p/src/protocols/swap_setup/bob.rs @@ -0,0 +1,668 @@ +use crate::out_event; +use crate::protocols::swap_setup::{ + protocol, BlockchainNetwork, SpotPriceError, SpotPriceResponse, +}; +use anyhow::{Context, Result}; +use bitcoin_wallet::BitcoinWallet; +use futures::future::{BoxFuture, OptionFuture}; +use futures::AsyncWriteExt; +use futures::FutureExt; +use libp2p::core::upgrade; +use libp2p::swarm::behaviour::ConnectionEstablished; +use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; +use libp2p::swarm::{ + ConnectionClosed, ConnectionDenied, ConnectionHandler, ConnectionHandlerEvent, ConnectionId, + FromSwarm, NetworkBehaviour, SubstreamProtocol, THandler, THandlerInEvent, THandlerOutEvent, + ToSwarm, +}; +use libp2p::{Multiaddr, PeerId}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::sync::Arc; +use std::task::Poll; +use std::time::Duration; +use swap_core::bitcoin; +use swap_env::env; +use swap_machine::bob::{State0, State2}; +use swap_machine::common::{Message1, Message3}; +use uuid::Uuid; + +use super::{read_cbor_message, write_cbor_message, SpotPriceRequest}; + +#[allow(missing_debug_implementations)] +pub struct Behaviour { + env_config: env::Config, + bitcoin_wallet: Arc, + + // Queue of swap setup request that haven't been assigned to a connection handler yet + // (peer_id, swap_id, new_swap) + new_swaps: VecDeque<(PeerId, Uuid, NewSwap)>, + + // Maintains the list of connections handlers for a specific peer + // + // 0. List of connection handlers that are still active but haven't been assigned a swap setup request yet + // 1. List of connection handlers that have died. Once their death is acknowledged / processed, they are removed from the list + connection_handlers: HashMap, VecDeque)>, + + // Queue of completed swaps that we have assigned a connection handler to but where we haven't notified the ConnectionHandler yet + // We notify the ConnectionHandler by emitting a ConnectionHandlerEvent::NotifyBehaviour event + assigned_unnotified_swaps: VecDeque<(ConnectionId, PeerId, Uuid, NewSwap)>, + + // Maintains the list of requests that we have sent to a connection handler but haven't yet received a response + inflight_requests: HashMap, + + // Queue of swap setup results that we want to notify the Swarm about + to_swarm: VecDeque, + + // Queue of peers that we want to instruct the Swarm to dial + to_dial: VecDeque, +} + +impl Behaviour { + pub fn new(env_config: env::Config, bitcoin_wallet: Arc) -> Self { + Self { + env_config, + bitcoin_wallet, + new_swaps: VecDeque::default(), + to_swarm: VecDeque::default(), + assigned_unnotified_swaps: VecDeque::default(), + inflight_requests: HashMap::default(), + connection_handlers: HashMap::default(), + to_dial: VecDeque::default(), + } + } + + pub async fn start(&mut self, alice_peer_id: PeerId, swap: NewSwap) { + tracing::trace!( + %alice_peer_id, + ?swap, + "Queuing new swap setup request inside the Behaviour", + ); + + // TODO: This is a bit redundant because we already have the swap_id in the NewSwap struct + self.new_swaps + .push_back((alice_peer_id, swap.swap_id, swap)); + self.to_dial.push_back(alice_peer_id); + } + + // Returns a mutable reference to the queues of the connection handlers for a specific peer + fn connection_handlers_mut( + &mut self, + peer_id: PeerId, + ) -> &mut (VecDeque, VecDeque) { + self.connection_handlers.entry(peer_id).or_default() + } + + // Returns a mutable reference to the queues of the connection handlers for a specific peer + fn alive_connection_handlers_mut(&mut self, peer_id: PeerId) -> &mut VecDeque { + &mut self.connection_handlers_mut(peer_id).0 + } + + // Returns a mutable reference to the queues of the connection handlers for a specific peer + fn dead_connection_handlers_mut(&mut self, peer_id: PeerId) -> &mut VecDeque { + &mut self.connection_handlers_mut(peer_id).1 + } + + fn known_peers(&self) -> HashSet { + self.connection_handlers.keys().copied().collect() + } +} + +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = Handler; + type ToSwarm = SwapSetupResult; + + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + Ok(Handler::new(self.env_config, self.bitcoin_wallet.clone())) + } + + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _addr: &Multiaddr, + _role_override: libp2p::core::Endpoint, + ) -> Result, ConnectionDenied> { + Ok(Handler::new(self.env_config, self.bitcoin_wallet.clone())) + } + + fn on_swarm_event(&mut self, event: FromSwarm<'_>) { + match event { + FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + }) => { + tracing::trace!( + peer = %peer_id, + connection_id = %connection_id, + endpoint = ?endpoint, + "A new connection handler has been established", + ); + + self.alive_connection_handlers_mut(peer_id) + .push_back(connection_id); + } + FromSwarm::ConnectionClosed(ConnectionClosed { + peer_id, + connection_id, + .. + }) => { + tracing::trace!( + peer = %peer_id, + connection_id = %connection_id, + "A swap setup connection handler has died", + ); + + self.dead_connection_handlers_mut(peer_id) + .push_back(connection_id); + } + _ => {} + } + } + + fn on_connection_handler_event( + &mut self, + event_peer_id: PeerId, + connection_id: libp2p::swarm::ConnectionId, + result: THandlerOutEvent, + ) { + if let Some((swap_id, peer)) = self.inflight_requests.remove(&connection_id) { + assert_eq!(peer, event_peer_id); + + self.to_swarm.push_back(SwapSetupResult { + peer, + swap_id, + result, + }); + } + } + + fn poll( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> Poll>> { + // Forward completed swaps from the connection handler to the swarm + if let Some(completed) = self.to_swarm.pop_front() { + tracing::trace!( + peer = %completed.peer, + "Forwarding completed swap setup from Behaviour to the Swarm", + ); + + return Poll::Ready(ToSwarm::GenerateEvent(completed)); + } + + // Forward any peers that we want to dial to the Swarm + if let Some(peer) = self.to_dial.pop_front() { + tracing::trace!( + peer = %peer, + "Instructing swarm to dial a new connection handler for a swap setup request", + ); + + return Poll::Ready(ToSwarm::Dial { + opts: DialOpts::peer_id(peer) + .condition(PeerCondition::DisconnectedAndNotDialing) + .build(), + }); + } + + // Remove any unused already dead connection handlers that were never assigned a request + for peer in self.known_peers() { + let (alive_connection_handlers, dead_connection_handlers) = + self.connection_handlers_mut(peer); + + // Create sets for efficient lookup + let alive_set: HashSet<_> = alive_connection_handlers.iter().copied().collect(); + let dead_set: HashSet<_> = dead_connection_handlers.iter().copied().collect(); + + // Remove from alive any handlers that are also in dead + alive_connection_handlers.retain(|id| !dead_set.contains(id)); + + // Remove from dead any handlers that were in alive (the overlap we just processed) + dead_connection_handlers.retain(|id| !alive_set.contains(id)); + } + + // Go through our new_swaps and try to assign a request to a connection handler + // + // If we find a connection handler for the peer, it will be removed from new_swaps + // If we don't find a connection handler for the peer, it will remain in new_swaps + { + let new_swaps = &mut self.new_swaps; + let connection_handlers = &mut self.connection_handlers; + let assigned_unnotified_swaps = &mut self.assigned_unnotified_swaps; + + let mut remaining = std::collections::VecDeque::new(); + for (peer, swap_id, new_swap) in new_swaps.drain(..) { + if let Some(connection_id) = + connection_handlers.entry(peer).or_default().0.pop_front() + { + assigned_unnotified_swaps.push_back((connection_id, peer, swap_id, new_swap)); + } else { + remaining.push_back((peer, swap_id, new_swap)); + } + } + + *new_swaps = remaining; + } + + // If a connection handler died which had an assigned swap setup request, + // we need to notify the swarm that the request failed + for peer_id in self.known_peers() { + while let Some(connection_id) = self.dead_connection_handlers_mut(peer_id).pop_front() { + if let Some((swap_id, _)) = self.inflight_requests.remove(&connection_id) { + self.to_swarm.push_back(SwapSetupResult { + peer: peer_id, + swap_id, + result: Err(anyhow::anyhow!("Connection handler for peer {} has died after we notified it of the swap setup request", peer_id)), + }); + } + } + } + + // Iterate through our assigned_unnotified_swaps queue (with popping) + if let Some((connection_id, peer_id, swap_id, new_swap)) = + self.assigned_unnotified_swaps.pop_front() + { + tracing::trace!( + swap_id = %swap_id, + connection_id = %connection_id, + ?new_swap, + "Dispatching swap setup request from Behaviour to a specific connection handler", + ); + + // Check if the connection handler is still alive + if let Some(dead_connection_handler) = self + .dead_connection_handlers_mut(peer_id) + .iter() + .position(|id| *id == connection_id) + { + self.dead_connection_handlers_mut(peer_id) + .remove(dead_connection_handler); + + self.to_swarm.push_back(SwapSetupResult { + peer: peer_id, + swap_id, + result: Err(anyhow::anyhow!("Connection handler for peer {} has died before we could notify it of the swap setup request", peer_id)), + }); + } else { + // ConnectionHandler must still be alive, notify it of the swap setup request + tracing::trace!( + peer = %peer_id, + swap_id = %swap_id, + ?new_swap, + "Notifying connection handler of the swap setup request. We are assuming it is still alive.", + ); + + self.inflight_requests + .insert(connection_id, (swap_id, peer_id)); + + return Poll::Ready(ToSwarm::NotifyHandler { + peer_id, + handler: libp2p::swarm::NotifyHandler::One(connection_id), + event: new_swap, + }); + } + } + + Poll::Pending + } +} + +type OutboundStream = BoxFuture<'static, Result>; + +pub struct Handler { + outbound_stream: OptionFuture, + env_config: env::Config, + timeout: Duration, + new_swaps: VecDeque, + bitcoin_wallet: Arc, + keep_alive: bool, +} + +impl Handler { + fn new(env_config: env::Config, bitcoin_wallet: Arc) -> Self { + Self { + env_config, + outbound_stream: OptionFuture::from(None), + timeout: Duration::from_secs(120), + new_swaps: VecDeque::default(), + bitcoin_wallet, + // TODO: This will keep ALL connections alive indefinitely + // which is not optimal + keep_alive: true, + } + } +} + +#[derive(Debug, Clone)] +pub struct NewSwap { + pub swap_id: Uuid, + pub btc: bitcoin::Amount, + pub tx_lock_fee: bitcoin::Amount, + pub tx_refund_fee: bitcoin::Amount, + pub tx_cancel_fee: bitcoin::Amount, + pub bitcoin_refund_address: bitcoin::Address, +} + +#[derive(Debug)] +pub struct SwapSetupResult { + peer: PeerId, + swap_id: Uuid, + result: Result, +} + +impl ConnectionHandler for Handler { + type FromBehaviour = NewSwap; + type ToBehaviour = Result; + type InboundProtocol = upgrade::DeniedUpgrade; + type OutboundProtocol = protocol::SwapSetup; + type InboundOpenInfo = (); + type OutboundOpenInfo = NewSwap; + + fn listen_protocol(&self) -> SubstreamProtocol { + // Bob does not support inbound substreams + SubstreamProtocol::new(upgrade::DeniedUpgrade, ()) + } + + fn on_connection_event( + &mut self, + event: libp2p::swarm::handler::ConnectionEvent< + '_, + Self::InboundProtocol, + Self::OutboundProtocol, + Self::InboundOpenInfo, + Self::OutboundOpenInfo, + >, + ) { + match event { + libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedInbound(_) => { + // TODO: Maybe warn here as Bob does not support inbound substreams? + } + libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedOutbound(outbound) => { + let mut substream = outbound.protocol; + let new_swap_request = outbound.info; + + let bitcoin_wallet = self.bitcoin_wallet.clone(); + let env_config = self.env_config; + + let protocol = tokio::time::timeout(self.timeout, async move { + let result = run_swap_setup( + &mut substream, + new_swap_request, + env_config, + bitcoin_wallet, + ) + .await; + + result.map_err(|err: anyhow::Error| { + tracing::error!(?err, "Error occurred during swap setup protocol"); + Error::Protocol(format!("{:?}", err)) + }) + }); + + let max_seconds = self.timeout.as_secs(); + + self.outbound_stream = OptionFuture::from(Some(Box::pin(async move { + protocol.await.map_err(|_| Error::Timeout { + seconds: max_seconds, + })? + }) + as OutboundStream)); + } + libp2p::swarm::handler::ConnectionEvent::AddressChange(address_change) => { + tracing::trace!( + ?address_change, + "Connection address changed during swap setup" + ); + } + libp2p::swarm::handler::ConnectionEvent::DialUpgradeError(dial_upgrade_error) => { + tracing::trace!(error = %dial_upgrade_error.error, "Dial upgrade error during swap setup"); + } + libp2p::swarm::handler::ConnectionEvent::ListenUpgradeError(listen_upgrade_error) => { + tracing::trace!( + ?listen_upgrade_error, + "Listen upgrade error during swap setup" + ); + } + _ => { + // We ignore the rest of events + } + } + } + + fn on_behaviour_event(&mut self, new_swap: Self::FromBehaviour) { + self.new_swaps.push_back(new_swap); + } + + fn connection_keep_alive(&self) -> bool { + self.keep_alive + } + + fn poll( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> Poll< + ConnectionHandlerEvent, + > { + // Check if there is a new swap to be started on this connection + // Has the Behaviour assigned us a new swap to be started on this connection? + if let Some(new_swap) = self.new_swaps.pop_front() { + tracing::trace!( + ?new_swap.swap_id, + "Instructing swarm to start a new outbound substream as part of swap setup", + ); + + // Keep the connection alive because we want to use it + self.keep_alive = true; + + // We instruct the swarm to start a new outbound substream + return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new(protocol::new(), new_swap), + }); + } + + // Check if the outbound stream has completed + if let Poll::Ready(Some(result)) = self.outbound_stream.poll_unpin(cx) { + self.outbound_stream = None.into(); + + // Once the outbound stream is completed, we no longer keep the connection alive + self.keep_alive = false; + + // We notify the swarm that the swap setup is completed / failed + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + result.map_err(anyhow::Error::from).into(), + )); + } + + Poll::Pending + } +} + +async fn run_swap_setup( + mut substream: &mut libp2p::swarm::Stream, + new_swap_request: NewSwap, + env_config: env::Config, + bitcoin_wallet: Arc, +) -> Result { + // Here we request the spot price from Alice + write_cbor_message( + &mut substream, + SpotPriceRequest { + btc: new_swap_request.btc, + blockchain_network: BlockchainNetwork { + bitcoin: env_config.bitcoin_network, + monero: env_config.monero_network, + }, + }, + ) + .await + .context("Failed to send spot price request to Alice")?; + + // Here we read the spot price response from Alice + // The outer ? checks if Alice responded with an error (SpotPriceError) + let xmr = Result::from( + // The inner ? is for the read_cbor_message function + // It will return an error if the deserialization fails + read_cbor_message::(&mut substream) + .await + .context("Failed to read spot price response from Alice")?, + )?; + + tracing::trace!( + %new_swap_request.swap_id, + xmr = %xmr, + btc = %new_swap_request.btc, + "Got spot price response from Alice as part of swap setup", + ); + + let state0 = State0::new( + new_swap_request.swap_id, + &mut rand::thread_rng(), + new_swap_request.btc, + xmr, + env_config.bitcoin_cancel_timelock.into(), + env_config.bitcoin_punish_timelock.into(), + new_swap_request.bitcoin_refund_address.clone(), + env_config.monero_finality_confirmations, + new_swap_request.tx_refund_fee, + new_swap_request.tx_cancel_fee, + new_swap_request.tx_lock_fee, + ); + + tracing::trace!( + %new_swap_request.swap_id, + "Transitioned into state0 during swap setup", + ); + + write_cbor_message(&mut substream, state0.next_message()) + .await + .context("Failed to send state0 message to Alice")?; + let message1 = read_cbor_message::(&mut substream) + .await + .context("Failed to read message1 from Alice")?; + let state1 = state0 + .receive(bitcoin_wallet.as_ref(), message1) + .await + .context("Failed to receive state1")?; + + tracing::trace!( + %new_swap_request.swap_id, + "Transitioned into state1 during swap setup", + ); + + write_cbor_message(&mut substream, state1.next_message()) + .await + .context("Failed to send state1 message")?; + let message3 = read_cbor_message::(&mut substream) + .await + .context("Failed to read message3 from Alice")?; + let state2 = state1 + .receive(message3) + .context("Failed to receive state2")?; + + tracing::trace!( + %new_swap_request.swap_id, + "Transitioned into state2 during swap setup", + ); + + write_cbor_message(&mut substream, state2.next_message()) + .await + .context("Failed to send state2 message")?; + + substream + .flush() + .await + .context("Failed to flush substream")?; + substream + .close() + .await + .context("Failed to close substream")?; + + tracing::trace!( + %new_swap_request.swap_id, + "Swap setup completed", + ); + + Ok(state2) +} + +impl From for Result { + fn from(response: SpotPriceResponse) -> Self { + match response { + SpotPriceResponse::Xmr(amount) => Ok(amount), + SpotPriceResponse::Error(e) => Err(e.into()), + } + } +} + +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] +pub enum Error { + #[error("Seller currently does not accept incoming swap requests, please try again later")] + NoSwapsAccepted, + #[error("Seller refused to buy {buy} because the minimum configured buy limit is {min}")] + AmountBelowMinimum { + min: bitcoin::Amount, + buy: bitcoin::Amount, + }, + #[error("Seller refused to buy {buy} because the maximum configured buy limit is {max}")] + AmountAboveMaximum { + max: bitcoin::Amount, + buy: bitcoin::Amount, + }, + #[error("Seller's XMR balance is currently too low to fulfill the swap request to buy {buy}, please try again later")] + BalanceTooLow { buy: bitcoin::Amount }, + + #[error("Seller blockchain network {asb:?} setup did not match your blockchain network setup {cli:?}")] + BlockchainNetworkMismatch { + cli: BlockchainNetwork, + asb: BlockchainNetwork, + }, + + #[error("Failed to complete swap setup within {seconds}s")] + Timeout { seconds: u64 }, + + /// Something went wrong during the swap setup protocol that is not covered by the other errors + /// but where we have some context about the error + #[error("Something went wrong during the swap setup protocol: {0}")] + Protocol(String), + + /// To be used for errors that cannot be explained on the CLI side (e.g. + /// rate update problems on the seller side) + #[error("Seller encountered a problem, please try again later.")] + Other, +} + +impl From for Error { + fn from(error: SpotPriceError) -> Self { + match error { + SpotPriceError::NoSwapsAccepted => Error::NoSwapsAccepted, + SpotPriceError::AmountBelowMinimum { min, buy } => { + Error::AmountBelowMinimum { min, buy } + } + SpotPriceError::AmountAboveMaximum { max, buy } => { + Error::AmountAboveMaximum { max, buy } + } + SpotPriceError::BalanceTooLow { buy } => Error::BalanceTooLow { buy }, + SpotPriceError::BlockchainNetworkMismatch { cli, asb } => { + Error::BlockchainNetworkMismatch { cli, asb } + } + SpotPriceError::Other => Error::Other, + } + } +} + +impl From for out_event::bob::OutEvent { + fn from(completed: SwapSetupResult) -> Self { + out_event::bob::OutEvent::SwapSetupCompleted { + result: Box::new(completed.result), + swap_id: completed.swap_id, + peer: completed.peer, + } + } +} diff --git a/swap/src/network/swap_setup/vendor_from_fn.rs b/swap-p2p/src/protocols/swap_setup/vendor_from_fn.rs similarity index 71% rename from swap/src/network/swap_setup/vendor_from_fn.rs rename to swap-p2p/src/protocols/swap_setup/vendor_from_fn.rs index a453a28a3a..6f67621ece 100644 --- a/swap/src/network/swap_setup/vendor_from_fn.rs +++ b/swap-p2p/src/protocols/swap_setup/vendor_from_fn.rs @@ -30,34 +30,6 @@ use libp2p::{ use std::iter; /// Initializes a new [`FromFnUpgrade`]. -/// -/// # Example -/// -/// ```no_run -/// # use libp2p::core::transport::{Transport, MemoryTransport, memory::Channel}; -/// # use libp2p::core::{upgrade::{self, Negotiated, Version}, Endpoint}; -/// # use libp2p::core::upgrade::length_delimited; -/// # use std::io; -/// # use futures::AsyncWriteExt; -/// # use swap::network::swap_setup::vendor_from_fn::from_fn; -/// -/// let _transport = MemoryTransport::default() -/// .and_then(move |out, endpoint| { // Changed cp to endpoint to match from_fn signature -/// upgrade::apply(out, self::from_fn("/foo/1", move |mut sock: Negotiated>>, endpoint_arg: Endpoint| async move { -/// if endpoint_arg.is_dialer() { -/// length_delimited::write_length_prefixed(&mut sock, b"some handshake data").await?; -/// sock.close().await?; -/// } else { -/// let handshake_data = length_delimited::read_length_prefixed(&mut sock, 1024).await?; -/// if handshake_data != b"some handshake data" { -/// return Err(io::Error::new(io::ErrorKind::Other, "bad handshake")); -/// } -/// } -/// Ok(sock) -/// }), endpoint, Version::V1) // Assuming cp was meant to be endpoint, and Version is needed by apply -/// }); -/// ``` -/// pub fn from_fn(protocol_name: P, fun: F) -> FromFnUpgrade where // Note: these bounds are there in order to help the compiler infer types diff --git a/swap/src/network/transfer_proof.rs b/swap-p2p/src/protocols/transfer_proof.rs similarity index 84% rename from swap/src/network/transfer_proof.rs rename to swap-p2p/src/protocols/transfer_proof.rs index 7b935dd4ed..9e82031540 100644 --- a/swap/src/network/transfer_proof.rs +++ b/swap-p2p/src/protocols/transfer_proof.rs @@ -1,11 +1,11 @@ -use std::time::Duration; - -use crate::{asb, cli, monero}; use libp2p::request_response::{self, ProtocolSupport}; use libp2p::{PeerId, StreamProtocol}; use serde::{Deserialize, Serialize}; +use std::time::Duration; use uuid::Uuid; +use crate::out_event; + const PROTOCOL: &str = "/comit/xmr/btc/transfer_proof/1.0.0"; type OutEvent = request_response::Event; type Message = request_response::Message; @@ -24,7 +24,7 @@ impl AsRef for TransferProofProtocol { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Request { pub swap_id: Uuid, - pub tx_lock_proof: monero::TransferProof, + pub tx_lock_proof: swap_core::monero::TransferProof, } pub fn alice() -> Behaviour { @@ -41,7 +41,7 @@ pub fn bob() -> Behaviour { ) } -impl From<(PeerId, Message)> for asb::OutEvent { +impl From<(PeerId, Message)> for out_event::alice::OutEvent { fn from((peer, message): (PeerId, Message)) -> Self { match message { Message::Request { .. } => Self::unexpected_request(peer), @@ -53,9 +53,9 @@ impl From<(PeerId, Message)> for asb::OutEvent { } } -crate::impl_from_rr_event!(OutEvent, asb::OutEvent, PROTOCOL); +crate::impl_from_rr_event!(OutEvent, out_event::alice::OutEvent, PROTOCOL); -impl From<(PeerId, Message)> for cli::OutEvent { +impl From<(PeerId, Message)> for out_event::bob::OutEvent { fn from((peer, message): (PeerId, Message)) -> Self { match message { Message::Request { @@ -69,4 +69,5 @@ impl From<(PeerId, Message)> for cli::OutEvent { } } } -crate::impl_from_rr_event!(OutEvent, cli::OutEvent, PROTOCOL); + +crate::impl_from_rr_event!(OutEvent, out_event::bob::OutEvent, PROTOCOL); diff --git a/swap/src/network/test.rs b/swap-p2p/src/test.rs similarity index 97% rename from swap/src/network/test.rs rename to swap-p2p/src/test.rs index 1aba146842..d2bfaa9149 100644 --- a/swap/src/network/test.rs +++ b/swap-p2p/src/test.rs @@ -34,12 +34,15 @@ where .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) .boxed(); + const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 2); // 2 hours + SwarmBuilder::with_existing_identity(identity) .with_tokio() .with_other_transport(|_| Ok(transport)) .unwrap() .with_behaviour(|keypair| Ok(behaviour_fn(keypair.clone()))) .unwrap() + .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT)) .build() } diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 7be0034825..3bc2bae596 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swap" -version = "3.2.0-rc.1" +version = "3.3.1" authors = ["The COMIT guys "] edition = "2021" description = "XMR/BTC trustless atomic swaps." @@ -18,96 +18,118 @@ bdk_chain = { workspace = true } bdk_core = { workspace = true } bdk_electrum = { workspace = true, features = ["use-rustls-ring"] } bdk_wallet = { workspace = true, features = ["rusqlite", "test-utils"] } +bitcoin = { workspace = true } -anyhow = { workspace = true } +# Bitcoin Wallet +bitcoin-wallet = { path = "../bitcoin-wallet" } + +# Tor arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls", "onion-service-service"] } -async-compression = { version = "0.3", features = ["bzip2", "tokio"] } -async-trait = { workspace = true } -asynchronous-codec = "0.7.0" -atty = "0.2" +tor-rtcompat = { workspace = true, features = ["tokio"] } + +# LibP2P +libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] } +libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } + +# Error handling +anyhow = { workspace = true } backoff = { workspace = true } +thiserror = { workspace = true } + +# Crypto / Decoding / Encoding base64 = "0.22" big-bytes = "1" -bitcoin = { workspace = true } -bitcoin-wallet = { path = "../bitcoin-wallet" } -bmrng = "0.5.2" +curve25519-dalek = { workspace = true } +data-encoding = "2.6" +ecdsa_fun = { workspace = true, features = ["libsecp_compat", "serde", "adaptor"] } +ed25519-dalek = "1" +hex = { workspace = true } +rustls = { version = "0.23", default-features = false, features = ["ring"] } +sha2 = { workspace = true } +sigma_fun = { workspace = true, default-features = false, features = ["ed25519", "serde", "secp256k1", "alloc"] } + +# Randomness +rand = { workspace = true } +rand_chacha = { workspace = true } + +# CLI +atty = "0.2" comfy-table = "7.1" +dialoguer = "0.11" + +# Other stuff +bmrng = { workspace = true } config = { version = "0.14", default-features = false, features = ["toml"] } conquer-once = "0.4" -curve25519-dalek = { workspace = true } -data-encoding = "2.6" derive_builder = "0.20.2" dfx-swiss-sdk = { workspace = true, optional = true } -dialoguer = "0.11" -ecdsa_fun = { workspace = true, features = ["libsecp_compat", "serde", "adaptor"] } -ed25519-dalek = "1" electrum-pool = { path = "../electrum-pool" } -fns = "0.0.7" -futures = { workspace = true } -hex = { workspace = true } jsonrpsee = { workspace = true, features = ["server"] } -libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] } -libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } moka = { version = "0.12", features = ["sync", "future"] } monero = { workspace = true } monero-rpc = { path = "../monero-rpc" } monero-rpc-pool = { path = "../monero-rpc-pool" } -monero-seed = { version = "0.1.0", path = "../monero-seed" } +monero-seed = { git = "https://github.com/monero-oxide/monero-wallet-util.git", package = "monero-seed" } monero-sys = { path = "../monero-sys" } once_cell = { workspace = true } pem = "3.0" proptest = "1" -rand = { workspace = true } -rand_chacha = { workspace = true } regex = "1.10" reqwest = { workspace = true, features = ["http2", "rustls-tls-native-roots", "stream", "socks"] } rust_decimal = { version = "1", features = ["serde-float"] } rust_decimal_macros = "1" -rustls = { version = "0.23", default-features = false, features = ["ring"] } semver = "1.0" -serde = { workspace = true } -serde_cbor = "0.11" -serde_json = { workspace = true } -serde_with = { version = "1", features = ["macros"] } -sha2 = { workspace = true } -sigma_fun = { workspace = true, default-features = false, features = ["ed25519", "serde", "secp256k1", "alloc"] } -sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls"] } structopt = "0.3" -strum = { version = "0.26", features = ["derive"] } swap-controller-api = { path = "../swap-controller-api" } swap-core = { path = "../swap-core" } +swap-db = { path = "../swap-db" } swap-env = { path = "../swap-env" } swap-feed = { path = "../swap-feed" } swap-fs = { path = "../swap-fs" } swap-machine = { path = "../swap-machine" } +swap-p2p = { path = "../swap-p2p" } swap-serde = { path = "../swap-serde" } tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false } -thiserror = { workspace = true } throttle = { path = "../throttle" } time = "0.3" +url = { workspace = true } +uuid = { workspace = true, features = ["serde"] } +void = "1" +zeroize = "1.8.1" # Tokio tokio = { workspace = true, features = ["process", "fs", "net", "parking_lot", "rt"] } tokio-tungstenite = { version = "0.15", features = ["rustls-tls"] } tokio-util = { workspace = true } -tor-rtcompat = { workspace = true, features = ["tokio"] } +# Async +async-compression = { version = "0.3", features = ["bzip2", "tokio"] } +async-trait = { workspace = true } +futures = { workspace = true } + tower = { version = "0.4.13", features = ["full"] } tower-http = { version = "0.3.4", features = ["full"] } + +# Tracing tracing = { workspace = true } tracing-appender = "0.2" tracing-subscriber = { workspace = true } + +# Serialization +serde = { workspace = true } +serde_cbor = "0.11" +serde_json = { workspace = true } +serde_with = { version = "1", features = ["macros"] } +strum = { workspace = true, features = ["derive"] } typeshare = { workspace = true } -unsigned-varint = { version = "0.8.0", features = ["codec", "asynchronous_codec"] } -url = { workspace = true } -uuid = { workspace = true, features = ["serde"] } -void = "1" -zeroize = "1.8.1" # monero-oxide monero-oxide-rpc = { git = "https://github.com/monero-oxide/monero-oxide.git", package = "monero-rpc" } monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide.git" } +# Database +sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls"] } + [target.'cfg(not(windows))'.dependencies] tokio-tar = "0.3" diff --git a/swap/src/asb.rs b/swap/src/asb.rs index 1187046723..2270014475 100644 --- a/swap/src/asb.rs +++ b/swap/src/asb.rs @@ -3,9 +3,9 @@ mod network; mod recovery; pub mod rpc; +pub use crate::network::rendezvous::register; pub use event_loop::{EventLoop, EventLoopHandle}; -pub use network::behaviour::{Behaviour, OutEvent}; -pub use network::rendezvous::RendezvousNode; +pub use network::behaviour::Behaviour; pub use network::transport; pub use recovery::cancel::cancel; pub use recovery::punish::punish; @@ -14,6 +14,7 @@ pub use recovery::refund::refund; pub use recovery::safely_abort::safely_abort; pub use recovery::{cancel, refund}; pub use swap_feed::{FixedRate, KrakenRate, LatestRate, Rate}; +pub use swap_p2p::out_event::alice::OutEvent; #[cfg(test)] pub use network::rendezvous; diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 60d179700b..f601873ed8 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -246,7 +246,7 @@ where } }; - let wallet_snapshot = match WalletSnapshot::capture(self.bitcoin_wallet.clone(), &self.monero_wallet, &self.external_redeem_address, btc).await { + let wallet_snapshot = match capture_wallet_snapshot(self.bitcoin_wallet.clone(), &self.monero_wallet, &self.external_redeem_address, btc).await { Ok(wallet_snapshot) => wallet_snapshot, Err(error) => { tracing::error!("Swap request will be ignored because we were unable to create wallet snapshot for swap: {:#}", error); @@ -523,6 +523,17 @@ where let count = self.swarm.connected_peers().count(); let _ = respond_to.send(count); } + EventLoopRequest::GetRegistrationStatus { respond_to } => { + let registrations = self + .swarm + .behaviour() + .rendezvous + .as_ref() + .map(|b| b.registrations()) + .unwrap_or_default(); // If rendezvous behaviour is disabled we report empty list + + let _ = respond_to.send(registrations); + } } } } @@ -648,24 +659,28 @@ where EventLoopHandle { swap_id, peer, - recv_encrypted_signature: Some(encrypted_signature_receiver), - transfer_proof_sender: Some(transfer_proof_sender), + recv_encrypted_signature: tokio::sync::Mutex::new(Some(encrypted_signature_receiver)), + transfer_proof_sender: tokio::sync::Mutex::new(Some(transfer_proof_sender)), } } } +// We use a Mutex here to allow recv_encrypted_signature and transfer_proof_sender to be accessed concurrently #[derive(Debug)] pub struct EventLoopHandle { swap_id: Uuid, peer: PeerId, - recv_encrypted_signature: Option>, + recv_encrypted_signature: + tokio::sync::Mutex>>, #[allow(clippy::type_complexity)] - transfer_proof_sender: Option< - tokio::sync::mpsc::UnboundedSender<( - PeerId, - transfer_proof::Request, - oneshot::Sender>, - )>, + transfer_proof_sender: tokio::sync::Mutex< + Option< + tokio::sync::mpsc::UnboundedSender<( + PeerId, + transfer_proof::Request, + oneshot::Sender>, + )>, + >, >, } @@ -681,9 +696,16 @@ impl EventLoopHandle { } /// Wait for an encrypted signature from Bob - pub async fn recv_encrypted_signature(&mut self) -> Result { - let receiver = self + /// + /// This function can not be called concurrently (even though it doesn't take &self mut) + /// It internally acquires a Mutex. If another instance of this is already running, it will fail. + pub async fn recv_encrypted_signature(&self) -> Result { + let mut recv_encrypted_signature_guard = self .recv_encrypted_signature + .try_lock() + .map_err(|_| anyhow!("recv_encrypted_signature is already being called"))?; + + let receiver = recv_encrypted_signature_guard .as_mut() .context("Encrypted signature was already received")?; @@ -697,7 +719,7 @@ impl EventLoopHandle { .context("Failed to acknowledge receipt of encrypted signature")?; // Only take after successful receipt and acknowledgement - self.recv_encrypted_signature.take(); + recv_encrypted_signature_guard.take(); Ok(tx_redeem_encsig) } @@ -710,9 +732,16 @@ impl EventLoopHandle { /// This will fail if /// 1. the transfer proof has already been sent once /// 2. there is an error with the bmrng channel - pub async fn send_transfer_proof(&mut self, msg: monero::TransferProof) -> Result<()> { - let sender = self + /// + /// This function can not be called concurrently (even though it doesn't take &self mut) + /// It internally acquires a Mutex. If another instance of this is already running, it will fail. + pub async fn send_transfer_proof(&self, msg: monero::TransferProof) -> Result<()> { + let mut transfer_proof_sender_guard = self .transfer_proof_sender + .try_lock() + .map_err(|_| anyhow!("send_transfer_proof is already being called"))?; + + let sender = transfer_proof_sender_guard .as_ref() .context("Transfer proof was already sent")?; @@ -759,12 +788,46 @@ impl EventLoopHandle { ) .await?; - self.transfer_proof_sender.take(); + transfer_proof_sender_guard.take(); Ok(()) } } +async fn capture_wallet_snapshot( + bitcoin_wallet: Arc, + monero_wallet: &monero::Wallets, + external_redeem_address: &Option, + transfer_amount: bitcoin::Amount, +) -> Result { + let unlocked_balance = monero_wallet.main_wallet().await.unlocked_balance().await; + let total_balance = monero_wallet.main_wallet().await.total_balance().await; + + tracing::info!(%unlocked_balance, %total_balance, "Capturing monero wallet snapshot"); + + let redeem_address = external_redeem_address + .clone() + .unwrap_or(bitcoin_wallet.new_address().await?); + let punish_address = external_redeem_address + .clone() + .unwrap_or(bitcoin_wallet.new_address().await?); + + let redeem_fee = bitcoin_wallet + .estimate_fee(bitcoin::TxRedeem::weight(), Some(transfer_amount)) + .await?; + let punish_fee = bitcoin_wallet + .estimate_fee(bitcoin::TxPunish::weight(), Some(transfer_amount)) + .await?; + + Ok(WalletSnapshot::new( + unlocked_balance.into(), + redeem_address, + punish_address, + redeem_fee, + punish_fee, + )) +} + mod service { use super::*; @@ -777,6 +840,9 @@ mod service { GetActiveConnections { respond_to: oneshot::Sender, }, + GetRegistrationStatus { + respond_to: oneshot::Sender>, + }, } /// Tower service for communicating with the EventLoop @@ -809,6 +875,18 @@ mod service { rx.await .map_err(|_| anyhow::anyhow!("EventLoop service did not respond")) } + + /// Get the registration status at configured rendezvous points + pub async fn get_registration_status( + &self, + ) -> anyhow::Result> { + let (tx, rx) = oneshot::channel(); + self.sender + .send(EventLoopRequest::GetRegistrationStatus { respond_to: tx }) + .map_err(|_| anyhow::anyhow!("EventLoop service is down"))?; + rx.await + .map_err(|_| anyhow::anyhow!("EventLoop service did not respond")) + } } } diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index b96fab27b2..ee12dd4bba 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -1,24 +1,18 @@ -use crate::network::quote::BidQuote; +use crate::network::rendezvous; use crate::network::rendezvous::XmrBtcNamespace; use crate::network::swap_setup::alice; -use crate::network::swap_setup::alice::WalletSnapshot; use crate::network::transport::authenticate_and_multiplex; use crate::network::{ cooperative_xmr_redeem_after_punish, encrypted_signature, quote, transfer_proof, }; -use crate::protocol::alice::State3; -use anyhow::{anyhow, Error, Result}; +use anyhow::Result; use libp2p::core::muxing::StreamMuxerBox; use libp2p::core::transport::Boxed; -use libp2p::request_response::ResponseChannel; -use libp2p::swarm::dial_opts::PeerCondition; use libp2p::swarm::NetworkBehaviour; use libp2p::{Multiaddr, PeerId}; -use std::task::Poll; use std::time::Duration; use swap_env::env; use swap_feed::LatestRate; -use uuid::Uuid; pub mod transport { use std::sync::Arc; @@ -98,84 +92,11 @@ pub mod transport { } pub mod behaviour { - use libp2p::{ - identify, identity, ping, - request_response::{InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId}, - swarm::behaviour::toggle::Toggle, - }; + use libp2p::{identify, identity, ping, swarm::behaviour::toggle::Toggle}; + use swap_p2p::out_event::alice::OutEvent; - use super::{rendezvous::RendezvousNode, *}; + use super::{rendezvous::register, *}; - #[allow(clippy::large_enum_variant)] - #[derive(Debug)] - pub enum OutEvent { - SwapSetupInitiated { - send_wallet_snapshot: bmrng::RequestReceiver, - }, - SwapSetupCompleted { - peer_id: PeerId, - swap_id: Uuid, - state3: State3, - }, - SwapDeclined { - peer: PeerId, - error: alice::Error, - }, - QuoteRequested { - channel: ResponseChannel, - peer: PeerId, - }, - TransferProofAcknowledged { - peer: PeerId, - id: OutboundRequestId, - }, - EncryptedSignatureReceived { - msg: encrypted_signature::Request, - channel: ResponseChannel<()>, - peer: PeerId, - }, - CooperativeXmrRedeemRequested { - channel: ResponseChannel, - swap_id: Uuid, - peer: PeerId, - }, - Rendezvous(libp2p::rendezvous::client::Event), - OutboundRequestResponseFailure { - peer: PeerId, - error: OutboundFailure, - request_id: OutboundRequestId, - protocol: String, - }, - InboundRequestResponseFailure { - peer: PeerId, - error: InboundFailure, - request_id: InboundRequestId, - protocol: String, - }, - Failure { - peer: PeerId, - error: Error, - }, - /// "Fallback" variant that allows the event mapping code to swallow - /// certain events that we don't want the caller to deal with. - Other, - } - - impl OutEvent { - pub fn unexpected_request(peer: PeerId) -> OutEvent { - OutEvent::Failure { - peer, - error: anyhow!("Unexpected request received"), - } - } - - pub fn unexpected_response(peer: PeerId) -> OutEvent { - OutEvent::Failure { - peer, - error: anyhow!("Unexpected response received"), - } - } - } /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", event_process = false)] @@ -184,7 +105,7 @@ pub mod behaviour { where LR: LatestRate + Send + 'static, { - pub rendezvous: Toggle, + pub rendezvous: Toggle, pub quote: quote::Behaviour, pub swap_setup: alice::Behaviour, pub transfer_proof: transfer_proof::Behaviour, @@ -209,7 +130,7 @@ pub mod behaviour { resume_only: bool, env_config: env::Config, identify_params: (identity::Keypair, XmrBtcNamespace), - rendezvous_nodes: Vec, + rendezvous_nodes: Vec, ) -> Self { let (identity, namespace) = identify_params; let agent_version = format!("asb/{} ({})", env!("CARGO_PKG_VERSION"), namespace); @@ -223,7 +144,10 @@ pub mod behaviour { let behaviour = if rendezvous_nodes.is_empty() { None } else { - Some(rendezvous::Behaviour::new(identity, rendezvous_nodes)) + Some(rendezvous::register::Behaviour::new( + identity, + rendezvous_nodes, + )) }; Self { @@ -244,574 +168,4 @@ pub mod behaviour { } } } - - impl From for OutEvent { - fn from(_: ping::Event) -> Self { - OutEvent::Other - } - } - - impl From for OutEvent { - fn from(_: identify::Event) -> Self { - OutEvent::Other - } - } - - impl From for OutEvent { - fn from(event: libp2p::rendezvous::client::Event) -> Self { - OutEvent::Rendezvous(event) - } - } -} - -pub mod rendezvous { - use super::*; - use backoff::backoff::Backoff; - use backoff::ExponentialBackoff; - use futures::future::BoxFuture; - use futures::stream::FuturesUnordered; - use futures::{FutureExt, StreamExt}; - use libp2p::identity; - use libp2p::rendezvous::client::RegisterError; - use libp2p::swarm::dial_opts::DialOpts; - use libp2p::swarm::{ - ConnectionDenied, ConnectionId, FromSwarm, THandler, THandlerInEvent, THandlerOutEvent, - ToSwarm, - }; - use std::collections::HashMap; - use std::pin::Pin; - use std::task::Context; - - #[derive(Clone, PartialEq)] - enum ConnectionStatus { - Disconnected, - Dialling, - Connected, - } - - enum RegistrationStatus { - RegisterOnNextConnection, - Pending, - Registered { - re_register_in: Pin>, - }, - } - - pub struct Behaviour { - inner: libp2p::rendezvous::client::Behaviour, - rendezvous_nodes: Vec, - // always use schedule_dial to schedule a dial - // do not insert directly into this future - to_dial: FuturesUnordered>, - backoffs: HashMap, - } - - /// A node running the rendezvous server protocol. - pub struct RendezvousNode { - pub address: Multiaddr, - connection_status: ConnectionStatus, - pub peer_id: PeerId, - registration_status: RegistrationStatus, - pub registration_ttl: Option, - pub namespace: XmrBtcNamespace, - } - - impl RendezvousNode { - pub fn new( - address: &Multiaddr, - peer_id: PeerId, - namespace: XmrBtcNamespace, - registration_ttl: Option, - ) -> Self { - Self { - address: address.to_owned(), - connection_status: ConnectionStatus::Disconnected, - namespace, - peer_id, - registration_status: RegistrationStatus::RegisterOnNextConnection, - registration_ttl, - } - } - - fn set_connection(&mut self, status: ConnectionStatus) { - self.connection_status = status; - } - - fn set_registration(&mut self, status: RegistrationStatus) { - self.registration_status = status; - } - } - - impl Behaviour { - pub fn new(identity: identity::Keypair, rendezvous_nodes: Vec) -> Self { - let mut backoffs = HashMap::new(); - - // Initialize backoff for each rendezvous node - for node in &rendezvous_nodes { - backoffs.insert( - node.peer_id, - ExponentialBackoff { - // 5 minutes max interval - max_interval: Duration::from_secs(5 * 60), - // Never give up - max_elapsed_time: None, - // We retry aggressively. We begin with 50ms and increase by 10% per retry. - multiplier: 1.1f64, - initial_interval: Duration::from_millis(50), - current_interval: Duration::from_millis(50), - ..ExponentialBackoff::default() - }, - ); - } - - Self { - inner: libp2p::rendezvous::client::Behaviour::new(identity), - rendezvous_nodes, - to_dial: FuturesUnordered::new(), - backoffs, - } - } - - /// Registers the rendezvous node at the given index. - /// Also sets the registration status to [`RegistrationStatus::Pending`]. - pub fn register(&mut self, node_index: usize) -> Result<(), RegisterError> { - let node = &mut self.rendezvous_nodes[node_index]; - node.set_registration(RegistrationStatus::Pending); - let (namespace, peer_id, ttl) = - (node.namespace.into(), node.peer_id, node.registration_ttl); - self.inner.register(namespace, peer_id, ttl) - } - - /// Schedules a dial to a peer with exponential backoff delay. - fn schedule_dial(&mut self, peer_id: PeerId) { - let backoff = self - .backoffs - .get_mut(&peer_id) - .expect("Backoff should exist for all rendezvous nodes"); - let delay = backoff - .next_backoff() - .expect("Backoff should never run out"); - - // Create a future that sleeps and then returns the peer_id - let future = async move { - tokio::time::sleep(delay).await; - peer_id - }; - - self.to_dial.push(future.boxed()); - - // Set the connection status to Dialling - if let Some(node) = self - .rendezvous_nodes - .iter_mut() - .find(|node| node.peer_id == peer_id) - { - node.set_connection(ConnectionStatus::Dialling); - } - } - } - - impl NetworkBehaviour for Behaviour { - type ConnectionHandler = - ::ConnectionHandler; - type ToSwarm = libp2p::rendezvous::client::Event; - - fn handle_established_inbound_connection( - &mut self, - connection_id: ConnectionId, - peer: PeerId, - local_addr: &Multiaddr, - remote_addr: &Multiaddr, - ) -> Result, ConnectionDenied> { - self.inner.handle_established_inbound_connection( - connection_id, - peer, - local_addr, - remote_addr, - ) - } - - fn handle_established_outbound_connection( - &mut self, - connection_id: ConnectionId, - peer: PeerId, - addr: &Multiaddr, - role_override: libp2p::core::Endpoint, - ) -> Result, ConnectionDenied> { - self.inner.handle_established_outbound_connection( - connection_id, - peer, - addr, - role_override, - ) - } - - fn handle_pending_outbound_connection( - &mut self, - connection_id: ConnectionId, - maybe_peer: Option, - addresses: &[Multiaddr], - effective_role: libp2p::core::Endpoint, - ) -> std::result::Result, ConnectionDenied> { - self.inner.handle_pending_outbound_connection( - connection_id, - maybe_peer, - addresses, - effective_role, - ) - } - - fn on_swarm_event(&mut self, event: FromSwarm<'_>) { - match event { - FromSwarm::ConnectionEstablished(connection) => { - let peer_id = connection.peer_id; - - // Find the rendezvous node that matches the peer id, else do nothing. - if let Some(index) = self - .rendezvous_nodes - .iter_mut() - .position(|node| node.peer_id == peer_id) - { - let rendezvous_node = &mut self.rendezvous_nodes[index]; - rendezvous_node.set_connection(ConnectionStatus::Connected); - - // Reset backoff on successful connection - if let Some(backoff) = self.backoffs.get_mut(&peer_id) { - backoff.reset(); - } - - if let RegistrationStatus::RegisterOnNextConnection = - rendezvous_node.registration_status - { - let _ = self.register(index).inspect_err(|err| { - tracing::error!( - error=%err, - rendezvous_node=%peer_id, - "Failed to register with rendezvous node"); - }); - } - } - } - FromSwarm::ConnectionClosed(connection) => { - let peer_id = connection.peer_id; - - // Update the connection status of the rendezvous node that disconnected. - if let Some(node) = self - .rendezvous_nodes - .iter_mut() - .find(|node| node.peer_id == peer_id) - { - node.set_connection(ConnectionStatus::Disconnected); - self.schedule_dial(peer_id); - } - } - FromSwarm::DialFailure(dial_failure) => { - // Update the connection status of the rendezvous node that failed to connect. - if let Some(peer_id) = dial_failure.peer_id { - if let Some(node) = self - .rendezvous_nodes - .iter_mut() - .find(|node| node.peer_id == peer_id) - { - node.set_connection(ConnectionStatus::Disconnected); - self.schedule_dial(peer_id); - } - } - } - _ => {} - } - self.inner.on_swarm_event(event); - } - - fn on_connection_handler_event( - &mut self, - peer_id: PeerId, - connection_id: ConnectionId, - event: THandlerOutEvent, - ) { - self.inner - .on_connection_handler_event(peer_id, connection_id, event) - } - - fn poll( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>> { - // Check if we need to dial a peer - if let Poll::Ready(Some(peer_id)) = self.to_dial.poll_next_unpin(cx) { - // This should be redundant as this is already set in the schedule_dial function - // we still do it here to be safe - if let Some(node) = self - .rendezvous_nodes - .iter_mut() - .find(|node| node.peer_id == peer_id) - { - node.set_connection(ConnectionStatus::Dialling); - } - - return Poll::Ready(ToSwarm::Dial { - opts: DialOpts::peer_id(peer_id) - .addresses(vec![self - .rendezvous_nodes - .iter() - .find(|node| node.peer_id == peer_id) - .map(|node| node.address.clone()) - .expect("We should have a rendezvous node for the peer id")]) - .condition(PeerCondition::Disconnected) - // TODO: this makes the behaviour call `NetworkBehaviour::handle_pending_outbound_connection` - // but we don't implement it - .extend_addresses_through_behaviour() - .build(), - }); - } - - // Check the status of each rendezvous node - for i in 0..self.rendezvous_nodes.len() { - let connection_status = self.rendezvous_nodes[i].connection_status.clone(); - match &mut self.rendezvous_nodes[i].registration_status { - RegistrationStatus::RegisterOnNextConnection => match connection_status { - ConnectionStatus::Disconnected => { - let peer_id = self.rendezvous_nodes[i].peer_id; - self.schedule_dial(peer_id); - } - ConnectionStatus::Dialling => {} - ConnectionStatus::Connected => { - let _ = self.register(i); - } - }, - RegistrationStatus::Registered { re_register_in } => { - if let Poll::Ready(()) = re_register_in.poll_unpin(cx) { - match connection_status { - ConnectionStatus::Connected => { - let _ = self.register(i).inspect_err(|err| { - tracing::error!( - error=%err, - rendezvous_node=%self.rendezvous_nodes[i].peer_id, - "Failed to register with rendezvous node"); - }); - } - ConnectionStatus::Disconnected => { - let peer_id = self.rendezvous_nodes[i].peer_id; - self.rendezvous_nodes[i].set_registration( - RegistrationStatus::RegisterOnNextConnection, - ); - self.schedule_dial(peer_id); - } - ConnectionStatus::Dialling => {} - } - } - } - RegistrationStatus::Pending => {} - } - } - - let inner_poll = self.inner.poll(cx); - - // reset the timer for the specific rendezvous node if we successfully registered - if let Poll::Ready(ToSwarm::GenerateEvent( - libp2p::rendezvous::client::Event::Registered { - ttl, - rendezvous_node, - .. - }, - )) = &inner_poll - { - if let Some(i) = self - .rendezvous_nodes - .iter() - .position(|n| &n.peer_id == rendezvous_node) - { - let half_of_ttl = Duration::from_secs(*ttl) / 2; - let re_register_in = Box::pin(tokio::time::sleep(half_of_ttl)); - let status = RegistrationStatus::Registered { re_register_in }; - self.rendezvous_nodes[i].set_registration(status); - } - } - - inner_poll - } - } - - #[cfg(test)] - mod tests { - use super::*; - use crate::network::test::{new_swarm, SwarmExt}; - use futures::StreamExt; - use libp2p::rendezvous; - use libp2p::swarm::SwarmEvent; - use std::collections::HashMap; - - #[tokio::test] - #[ignore] - // Due to an issue with the libp2p rendezvous library - // This needs to be fixed upstream and was - // introduced in our codebase by a libp2p refactor which bumped the version of libp2p: - // - // - The new bumped rendezvous client works, and can connect to an old rendezvous server - // - The new rendezvous has an issue, which is why these test (use the new mock server) - // do not work - // - // Ignore this test for now . This works in production :) - async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node( - ) { - let mut rendezvous_node = new_swarm(|_| { - rendezvous::server::Behaviour::new(rendezvous::server::Config::default()) - }); - let address = rendezvous_node.listen_on_random_memory_address().await; - let rendezvous_point = RendezvousNode::new( - &address, - rendezvous_node.local_peer_id().to_owned(), - XmrBtcNamespace::Testnet, - None, - ); - - let mut asb = new_swarm(|identity| { - super::rendezvous::Behaviour::new(identity, vec![rendezvous_point]) - }); - asb.listen_on_random_memory_address().await; // this adds an external address - - tokio::spawn(async move { - loop { - rendezvous_node.next().await; - } - }); - let asb_registered = tokio::spawn(async move { - loop { - if let SwarmEvent::Behaviour(rendezvous::client::Event::Registered { .. }) = - asb.select_next_some().await - { - break; - } - } - }); - - tokio::time::timeout(Duration::from_secs(10), asb_registered) - .await - .unwrap() - .unwrap(); - } - - #[tokio::test] - #[ignore] - // Due to an issue with the libp2p rendezvous library - // This needs to be fixed upstream and was - // introduced in our codebase by a libp2p refactor which bumped the version of libp2p: - // - // - The new bumped rendezvous client works, and can connect to an old rendezvous server - // - The new rendezvous has an issue, which is why these test (use the new mock server) - // do not work - // - // Ignore this test for now . This works in production :) - async fn asb_automatically_re_registers() { - let mut rendezvous_node = new_swarm(|_| { - rendezvous::server::Behaviour::new( - rendezvous::server::Config::default().with_min_ttl(2), - ) - }); - let address = rendezvous_node.listen_on_random_memory_address().await; - let rendezvous_point = RendezvousNode::new( - &address, - rendezvous_node.local_peer_id().to_owned(), - XmrBtcNamespace::Testnet, - Some(5), - ); - - let mut asb = new_swarm(|identity| { - super::rendezvous::Behaviour::new(identity, vec![rendezvous_point]) - }); - asb.listen_on_random_memory_address().await; // this adds an external address - - tokio::spawn(async move { - loop { - rendezvous_node.next().await; - } - }); - let asb_registered_three_times = tokio::spawn(async move { - let mut number_of_registrations = 0; - - loop { - if let SwarmEvent::Behaviour(rendezvous::client::Event::Registered { .. }) = - asb.select_next_some().await - { - number_of_registrations += 1 - } - - if number_of_registrations == 3 { - break; - } - } - }); - - tokio::time::timeout(Duration::from_secs(30), asb_registered_three_times) - .await - .unwrap() - .unwrap(); - } - - #[tokio::test] - #[ignore] - // Due to an issue with the libp2p rendezvous library - // This needs to be fixed upstream and was - // introduced in our codebase by a libp2p refactor which bumped the version of libp2p: - // - // - The new bumped rendezvous client works, and can connect to an old rendezvous server - // - The new rendezvous has an issue, which is why these test (use the new mock server) - // do not work - // - // Ignore this test for now . This works in production :) - async fn asb_registers_multiple() { - let registration_ttl = Some(10); - let mut rendezvous_nodes = Vec::new(); - let mut registrations = HashMap::new(); - // register with 5 rendezvous nodes - for _ in 0..5 { - let mut rendezvous = new_swarm(|_| { - rendezvous::server::Behaviour::new( - rendezvous::server::Config::default().with_min_ttl(2), - ) - }); - let address = rendezvous.listen_on_random_memory_address().await; - let id = *rendezvous.local_peer_id(); - registrations.insert(id, 0); - rendezvous_nodes.push(RendezvousNode::new( - &address, - *rendezvous.local_peer_id(), - XmrBtcNamespace::Testnet, - registration_ttl, - )); - tokio::spawn(async move { - loop { - rendezvous.next().await; - } - }); - } - - let mut asb = - new_swarm(|identity| super::rendezvous::Behaviour::new(identity, rendezvous_nodes)); - asb.listen_on_random_memory_address().await; // this adds an external address - - let handle = tokio::spawn(async move { - loop { - if let SwarmEvent::Behaviour(rendezvous::client::Event::Registered { - rendezvous_node, - .. - }) = asb.select_next_some().await - { - registrations - .entry(rendezvous_node) - .and_modify(|counter| *counter += 1); - } - - if registrations.iter().all(|(_, &count)| count >= 4) { - break; - } - } - }); - - tokio::time::timeout(Duration::from_secs(30), handle) - .await - .unwrap() - .unwrap(); - } - } } diff --git a/swap/src/asb/recovery/cancel.rs b/swap/src/asb/recovery/cancel.rs index 2aafff1cc1..2674283cf6 100644 --- a/swap/src/asb/recovery/cancel.rs +++ b/swap/src/asb/recovery/cancel.rs @@ -24,8 +24,10 @@ pub async fn cancel( AliceState::XmrLockTransactionSent { monero_wallet_restore_blockheight, transfer_proof, state3, } | AliceState::XmrLocked { monero_wallet_restore_blockheight, transfer_proof, state3 } | AliceState::XmrLockTransferProofSent { monero_wallet_restore_blockheight, transfer_proof, state3 } + // in cancel mode we do not care about the fact that we could redeem, but always wait for cancellation (leading either refund or punish) | AliceState::EncSigLearned { monero_wallet_restore_blockheight, transfer_proof, state3, .. } + | AliceState::WaitingForCancelTimelockExpiration { monero_wallet_restore_blockheight, transfer_proof, state3} | AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, state3} | AliceState::BtcCancelled { monero_wallet_restore_blockheight, transfer_proof, state3 } | AliceState::BtcRefunded { monero_wallet_restore_blockheight, transfer_proof, state3 ,.. } diff --git a/swap/src/asb/recovery/punish.rs b/swap/src/asb/recovery/punish.rs index bd5596b6ab..a71a1b58a3 100644 --- a/swap/src/asb/recovery/punish.rs +++ b/swap/src/asb/recovery/punish.rs @@ -26,6 +26,7 @@ pub async fn punish( | AliceState::XmrLocked {state3, transfer_proof, ..} | AliceState::XmrLockTransferProofSent {state3, transfer_proof, ..} | AliceState::EncSigLearned {state3, transfer_proof, ..} + | AliceState::WaitingForCancelTimelockExpiration {state3, transfer_proof, ..} | AliceState::CancelTimelockExpired {state3, transfer_proof, ..} // Punish possible due to cancel transaction already being published | AliceState::BtcCancelled {state3, transfer_proof, ..} diff --git a/swap/src/asb/recovery/redeem.rs b/swap/src/asb/recovery/redeem.rs index ea334b8369..586463bbee 100644 --- a/swap/src/asb/recovery/redeem.rs +++ b/swap/src/asb/recovery/redeem.rs @@ -83,6 +83,7 @@ pub async fn redeem( | AliceState::XmrLockTransactionSent { .. } | AliceState::XmrLocked { .. } | AliceState::XmrLockTransferProofSent { .. } + | AliceState::WaitingForCancelTimelockExpiration { .. } | AliceState::CancelTimelockExpired { .. } | AliceState::BtcCancelled { .. } | AliceState::BtcRefunded { .. } diff --git a/swap/src/asb/recovery/refund.rs b/swap/src/asb/recovery/refund.rs index 33caabc7cc..b747af5bc7 100644 --- a/swap/src/asb/recovery/refund.rs +++ b/swap/src/asb/recovery/refund.rs @@ -45,6 +45,7 @@ pub async fn refund( | AliceState::XmrLocked { transfer_proof, state3, .. } | AliceState::XmrLockTransferProofSent { transfer_proof, state3, .. } | AliceState::EncSigLearned { transfer_proof, state3, .. } + | AliceState::WaitingForCancelTimelockExpiration { transfer_proof, state3, .. } | AliceState::CancelTimelockExpired { transfer_proof, state3, .. } // Refund possible due to cancel transaction already being published diff --git a/swap/src/asb/recovery/safely_abort.rs b/swap/src/asb/recovery/safely_abort.rs index b73422663e..f9e5812a34 100644 --- a/swap/src/asb/recovery/safely_abort.rs +++ b/swap/src/asb/recovery/safely_abort.rs @@ -25,6 +25,7 @@ pub async fn safely_abort(swap_id: Uuid, db: Arc) -> Result Result { + let regs = self + .event_loop_service + .get_registration_status() + .await + .into_json_rpc_result()?; + + let registrations = regs + .into_iter() + .map(|r| RegistrationStatusItem { + address: r.address.to_string(), + connection: match r.connection { + crate::asb::register::ConnectionStatus::Disconnected => { + RendezvousConnectionStatus::Disconnected + } + crate::asb::register::ConnectionStatus::Dialling => { + RendezvousConnectionStatus::Dialling + } + crate::asb::register::ConnectionStatus::Connected => { + RendezvousConnectionStatus::Connected + } + }, + registration: match r.registration { + crate::asb::register::RegistrationStatusReport::RegisterOnNextConnection => { + RendezvousRegistrationStatus::RegisterOnNextConnection + } + crate::asb::register::RegistrationStatusReport::Pending => { + RendezvousRegistrationStatus::Pending + } + crate::asb::register::RegistrationStatusReport::Registered => { + RendezvousRegistrationStatus::Registered + } + }, + }) + .collect(); + + Ok(RegistrationStatusResponse { registrations }) + } } trait IntoJsonRpcResult { diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index e8ed7c43bb..41046a4a61 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -800,7 +800,7 @@ impl Wallet { .status_of_script(&tx, false) .await .unwrap_or_else(|error| { - tracing::warn!(%txid, "Failed to get status of script: {:#}", error); + tracing::warn!(%txid, error = ?error, "Failed to get status of script"); ScriptStatus::Retrying }); diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 77be9affce..64b8b0966b 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -9,7 +9,7 @@ pub mod watcher; pub use behaviour::{Behaviour, OutEvent}; pub use cancel_and_refund::{cancel, cancel_and_refund, refund}; -pub use event_loop::{EventLoop, EventLoopHandle}; +pub use event_loop::{EventLoop, EventLoopHandle, SwapEventLoopHandle}; pub use list_sellers::{list_sellers, SellerStatus}; #[cfg(test)] diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 5d34f53c27..2bc5ec02e3 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -24,8 +24,6 @@ use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock}; use tokio::task::JoinHandle; use tokio_util::task::AbortOnDropHandle; use tor_rtcompat::tokio::TokioRustlsRuntime; -use tracing::level_filters::LevelFilter; -use tracing::Level; use uuid::Uuid; use super::watcher::Watcher; @@ -40,7 +38,6 @@ mod config { pub(super) namespace: XmrBtcNamespace, pub env_config: EnvConfig, pub(super) seed: Option, - pub(super) debug: bool, pub(super) json: bool, pub(super) log_dir: PathBuf, pub(super) data_dir: PathBuf, @@ -57,7 +54,6 @@ mod config { namespace: XmrBtcNamespace::from_is_testnet(false), env_config, seed: seed.into(), - debug: false, json: false, is_testnet: false, data_dir, @@ -417,7 +413,6 @@ mod builder { bitcoin: Option, data: Option, is_testnet: bool, - debug: bool, json: bool, tor: bool, enable_monero_tor: bool, @@ -441,7 +436,6 @@ mod builder { bitcoin: None, data: None, is_testnet: false, - debug: false, json: false, tor: false, enable_monero_tor: false, @@ -480,12 +474,6 @@ mod builder { self } - /// Whether to include debug level logging messages (default false) - pub fn with_debug(mut self, debug: bool) -> Self { - self.debug = debug; - self - } - /// Set logging format to json (default false) pub fn with_json(mut self, json: bool) -> Self { self.json = json; @@ -515,23 +503,17 @@ mod builder { // Initialize logging let format = if self.json { Format::Json } else { Format::Raw }; - let level_filter = if self.debug { - LevelFilter::from_level(Level::DEBUG) - } else { - LevelFilter::from_level(Level::INFO) - }; START.call_once(|| { let _ = common::tracing_util::init( - level_filter, format, log_dir.clone(), self.tauri_handle.clone(), - false, + true, ); tracing::info!( binary = "cli", - version = env!("VERGEN_GIT_DESCRIBE"), + version = env!("CARGO_PKG_VERSION"), os = std::env::consts::OS, arch = std::env::consts::ARCH, "Setting up context" @@ -716,7 +698,6 @@ mod builder { namespace: XmrBtcNamespace::from_is_testnet(self.is_testnet), env_config, seed: seed.clone().into(), - debug: self.debug, json: self.json, is_testnet: self.is_testnet, data_dir: data_dir.clone(), @@ -1165,7 +1146,6 @@ pub mod api_test { namespace: XmrBtcNamespace::from_is_testnet(is_testnet), env_config, seed: seed.into(), - debug, json, is_testnet, data_dir, diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs index 7cb55875bb..f3240f2a43 100644 --- a/swap/src/cli/api/request.rs +++ b/swap/src/cli/api/request.rs @@ -244,7 +244,6 @@ pub struct GetSwapInfoResponse { pub btc_refund_address: String, pub cancel_timelock: CancelTimelock, pub punish_timelock: PunishTimelock, - pub timelock: Option, pub monero_receive_pool: MoneroAddressPool, } @@ -256,6 +255,30 @@ impl Request for GetSwapInfoArgs { } } +// GetSwapTimelock +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct GetSwapTimelockArgs { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + +#[typeshare] +#[derive(Serialize)] +pub struct GetSwapTimelockResponse { + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, + pub timelock: Option, +} + +impl Request for GetSwapTimelockArgs { + type Response = GetSwapTimelockResponse; + + async fn request(self, ctx: Arc) -> Result { + get_swap_timelock(self, ctx).await + } +} + // Balance #[typeshare] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -279,6 +302,30 @@ impl Request for BalanceArgs { } } +// GetBitcoinAddress +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct GetBitcoinAddressArgs; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetBitcoinAddressResponse { + #[typeshare(serialized_as = "string")] + #[serde(with = "swap_serde::bitcoin::address_serde")] + pub address: bitcoin::Address, +} + +impl Request for GetBitcoinAddressArgs { + type Response = GetBitcoinAddressResponse; + + async fn request(self, ctx: Arc) -> Result { + let bitcoin_wallet = ctx.try_get_bitcoin_wallet().await?; + let address = bitcoin_wallet.new_address().await?; + + Ok(GetBitcoinAddressResponse { address }) + } +} + // GetHistory #[typeshare] #[derive(Serialize, Deserialize, Debug)] @@ -802,7 +849,6 @@ pub async fn get_swap_info( args: GetSwapInfoArgs, context: Arc, ) -> Result { - let bitcoin_wallet = context.try_get_bitcoin_wallet().await?; let db = context.try_get_db().await?; let state = db.get_state(args.swap_id).await?; @@ -866,14 +912,6 @@ pub async fn get_swap_info( }) .with_context(|| "Did not find SwapSetupCompleted state for swap")?; - let timelock = match swap_state.expired_timelocks(bitcoin_wallet.clone()).await { - Ok(timelock) => timelock, - Err(err) => { - error!(swap_id = %args.swap_id, error = ?err, "Failed to fetch expired timelock status"); - None - } - }; - let monero_receive_pool = db.get_monero_address_pool(args.swap_id).await?; Ok(GetSwapInfoResponse { @@ -894,11 +932,29 @@ pub async fn get_swap_info( btc_refund_address: btc_refund_address.to_string(), cancel_timelock, punish_timelock, - timelock, monero_receive_pool, }) } +#[tracing::instrument(fields(method = "get_swap_timelock"), skip(context))] +pub async fn get_swap_timelock( + args: GetSwapTimelockArgs, + context: Arc, +) -> Result { + let bitcoin_wallet = context.try_get_bitcoin_wallet().await?; + let db = context.try_get_db().await?; + + let state = db.get_state(args.swap_id).await?; + let swap_state: BobState = state.try_into()?; + + let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?; + + Ok(GetSwapTimelockResponse { + swap_id: args.swap_id, + timelock, + }) +} + #[tracing::instrument(fields(method = "buy_xmr"), skip(context))] pub async fn buy_xmr( buy_xmr: BuyXmrArgs, @@ -1050,7 +1106,6 @@ pub async fn buy_xmr( .await?; let behaviour = cli::Behaviour::new( - seller_peer_id, env_config, bitcoin_wallet.clone(), (seed.derive_libp2p_identity(), namespace), @@ -1075,9 +1130,7 @@ pub async fn buy_xmr( TauriSwapProgressEvent::ReceivedQuote(quote.clone()), ); - // Now create the event loop we use for the swap - let (event_loop, event_loop_handle) = - EventLoop::new(swap_id, swarm, seller_peer_id, db.clone())?; + let (event_loop, mut event_loop_handle) = EventLoop::new(swarm, db.clone())?; let event_loop = tokio::spawn(event_loop.run().in_current_span()); tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(quote)); @@ -1105,13 +1158,14 @@ pub async fn buy_xmr( } }, swap_result = async { + let swap_event_loop_handle = event_loop_handle.swap_handle(seller_peer_id, swap_id).await?; let swap = Swap::new( db.clone(), swap_id, bitcoin_wallet.clone(), monero_wallet, env_config, - event_loop_handle, + swap_event_loop_handle, monero_receive_pool.clone(), bitcoin_change_address_for_spawn, tx_lock_amount, @@ -1169,7 +1223,6 @@ pub async fn resume_swap( .derive_libp2p_identity(); let behaviour = cli::Behaviour::new( - seller_peer_id, config.env_config, bitcoin_wallet.clone(), (seed.clone(), config.namespace), @@ -1184,20 +1237,22 @@ pub async fn resume_swap( swarm.add_peer_address(seller_peer_id, seller_address); } - let (event_loop, event_loop_handle) = - EventLoop::new(swap_id, swarm, seller_peer_id, db.clone())?; + let (event_loop, mut event_loop_handle) = EventLoop::new(swarm, db.clone())?; let monero_receive_pool = db.get_monero_address_pool(swap_id).await?; let tauri_handle = context.tauri_handle.clone(); + let swap_event_loop_handle = event_loop_handle + .swap_handle(seller_peer_id, swap_id) + .await?; let swap = Swap::from_db( db.clone(), swap_id, bitcoin_wallet, monero_manager, config.env_config, - event_loop_handle, + swap_event_loop_handle, monero_receive_pool, ) .await? diff --git a/swap/src/cli/behaviour.rs b/swap/src/cli/behaviour.rs index 49ff1f4048..ccc207fdd2 100644 --- a/swap/src/cli/behaviour.rs +++ b/swap/src/cli/behaviour.rs @@ -1,85 +1,21 @@ -use crate::monero::{Scalar, TransferProof}; -use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason; -use crate::network::quote::BidQuote; use crate::network::rendezvous::XmrBtcNamespace; use crate::network::swap_setup::bob; use crate::network::{ cooperative_xmr_redeem_after_punish, encrypted_signature, quote, redial, transfer_proof, }; -use crate::protocol::bob::State2; -use anyhow::{anyhow, Error, Result}; +use anyhow::Result; use bitcoin_wallet::BitcoinWallet; -use libp2p::request_response::{ - InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel, -}; use libp2p::swarm::NetworkBehaviour; -use libp2p::{identify, identity, ping, PeerId}; +use libp2p::{identify, identity, ping}; use std::sync::Arc; use std::time::Duration; use swap_env::env; +pub use swap_p2p::out_event::bob::OutEvent; -#[derive(Debug)] -pub enum OutEvent { - QuoteReceived { - id: OutboundRequestId, - response: BidQuote, - }, - SwapSetupCompleted(Box>), - TransferProofReceived { - msg: Box, - channel: ResponseChannel<()>, - peer: PeerId, - }, - EncryptedSignatureAcknowledged { - id: OutboundRequestId, - }, - CooperativeXmrRedeemFulfilled { - id: OutboundRequestId, - s_a: Scalar, - swap_id: uuid::Uuid, - lock_transfer_proof: TransferProof, - }, - CooperativeXmrRedeemRejected { - id: OutboundRequestId, - reason: CooperativeXmrRedeemRejectReason, - swap_id: uuid::Uuid, - }, - Failure { - peer: PeerId, - error: Error, - }, - OutboundRequestResponseFailure { - peer: PeerId, - error: OutboundFailure, - request_id: OutboundRequestId, - protocol: String, - }, - InboundRequestResponseFailure { - peer: PeerId, - error: InboundFailure, - request_id: InboundRequestId, - protocol: String, - }, - /// "Fallback" variant that allows the event mapping code to swallow certain - /// events that we don't want the caller to deal with. - Other, -} +const PROTOCOL_VERSION: &str = "/comit/xmr/btc/1.0.0"; -impl OutEvent { - pub fn unexpected_request(peer: PeerId) -> OutEvent { - OutEvent::Failure { - peer, - error: anyhow!("Unexpected request received"), - } - } - - pub fn unexpected_response(peer: PeerId) -> OutEvent { - OutEvent::Failure { - peer, - error: anyhow!("Unexpected response received"), - } - } -} +const INITIAL_REDIAL_INTERVAL: Duration = Duration::from_secs(1); +const MAX_REDIAL_INTERVAL: Duration = Duration::from_secs(30); /// A `NetworkBehaviour` that represents an XMR/BTC swap node as Bob. #[derive(NetworkBehaviour)] @@ -102,16 +38,15 @@ pub struct Behaviour { impl Behaviour { pub fn new( - alice: PeerId, env_config: env::Config, bitcoin_wallet: Arc, identify_params: (identity::Keypair, XmrBtcNamespace), ) -> Self { let agentVersion = format!("cli/{} ({})", env!("CARGO_PKG_VERSION"), identify_params.1); - let protocolVersion = "/comit/xmr/btc/1.0.0".to_string(); - let identifyConfig = identify::Config::new(protocolVersion, identify_params.0.public()) - .with_agent_version(agentVersion); + let identifyConfig = + identify::Config::new(PROTOCOL_VERSION.to_string(), identify_params.0.public()) + .with_agent_version(agentVersion); let pingConfig = ping::Config::new().with_timeout(Duration::from_secs(60)); @@ -122,24 +57,13 @@ impl Behaviour { encrypted_signature: encrypted_signature::bob(), cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::bob(), redial: redial::Behaviour::new( - alice, - Duration::from_secs(2), - Duration::from_secs(5 * 60), + // This redial behaviour is responsible for redialing all Alice peers during swaps + "multi-alice-redialer", + INITIAL_REDIAL_INTERVAL, + MAX_REDIAL_INTERVAL, ), ping: ping::Behaviour::new(pingConfig), identify: identify::Behaviour::new(identifyConfig), } } } - -impl From for OutEvent { - fn from(_: ping::Event) -> Self { - OutEvent::Other - } -} - -impl From for OutEvent { - fn from(_: identify::Event) -> Self { - OutEvent::Other - } -} diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 2af532e2fe..aef1174488 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -54,10 +54,10 @@ where Err(e) => anyhow::bail!(e), }; - let debug = args.debug; let json = args.json; let is_testnet = args.testnet; let data = args.data; + let result: Result> = match args.cmd { CliCommand::BuyXmr { seller: Seller { seller }, @@ -85,7 +85,6 @@ where .with_bitcoin(bitcoin) .with_monero(monero) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -105,7 +104,6 @@ where let context = Arc::new(Context::new_without_tauri_handle()); ContextBuilder::new(is_testnet) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -122,7 +120,6 @@ where let context = Arc::new(Context::new_without_tauri_handle()); ContextBuilder::new(is_testnet) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -141,7 +138,6 @@ where let context = Arc::new(Context::new_without_tauri_handle()); ContextBuilder::new(is_testnet) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -155,7 +151,6 @@ where ContextBuilder::new(is_testnet) .with_bitcoin(bitcoin) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -179,7 +174,6 @@ where ContextBuilder::new(is_testnet) .with_bitcoin(bitcoin) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -202,7 +196,6 @@ where .with_bitcoin(bitcoin) .with_monero(monero) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -219,7 +212,6 @@ where ContextBuilder::new(is_testnet) .with_bitcoin(bitcoin) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -238,7 +230,6 @@ where ContextBuilder::new(is_testnet) .with_tor(tor.enable_tor) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -256,7 +247,6 @@ where ContextBuilder::new(is_testnet) .with_bitcoin(bitcoin) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -271,7 +261,6 @@ where let context = Arc::new(Context::new_without_tauri_handle()); ContextBuilder::new(is_testnet) .with_data_dir(data) - .with_debug(debug) .with_json(json) .build(context.clone()) .await?; @@ -292,7 +281,7 @@ where name = "swap", about = "CLI for swapping BTC for XMR", author, - version = env!("VERGEN_GIT_DESCRIBE") + version = env!("CARGO_PKG_VERSION") )] struct Arguments { // global is necessary to ensure that clap can match against testnet in subcommands @@ -310,9 +299,6 @@ struct Arguments { )] data: Option, - #[structopt(long, help = "Activate debug logging")] - debug: bool, - #[structopt( short, long = "json", diff --git a/swap/src/cli/event_loop.rs b/swap/src/cli/event_loop.rs index f8d2f8205d..166365c552 100644 --- a/swap/src/cli/event_loop.rs +++ b/swap/src/cli/event_loop.rs @@ -7,217 +7,269 @@ use crate::network::swap_setup::bob::NewSwap; use crate::protocol::bob::swap::has_already_processed_transfer_proof; use crate::protocol::bob::{BobState, State2}; use crate::protocol::Database; -use anyhow::{anyhow, Context, Result}; -use futures::future::{BoxFuture, OptionFuture}; +use anyhow::{anyhow, bail, Context, Result}; +use futures::future::BoxFuture; +use futures::stream::FuturesUnordered; use futures::{FutureExt, StreamExt}; use libp2p::request_response::{OutboundFailure, OutboundRequestId, ResponseChannel}; -use libp2p::swarm::dial_opts::DialOpts; use libp2p::swarm::SwarmEvent; use libp2p::{PeerId, Swarm}; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use swap_core::bitcoin::EncryptedSignature; +use swap_p2p::protocols::redial; use uuid::Uuid; -static REQUEST_RESPONSE_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(60); -static EXECUTION_SETUP_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(120); +// Timeout for the execution setup protocol within the event loop. +// If the behaviour does not respond within this time, we will consider the request failed. +// Also used to give up on retries within the EventLoopHandle. +static EXECUTION_SETUP_MAX_ELAPSED_TIME: Duration = Duration::from_secs(120); + +// Used for deciding how long to retry request-response protocol requests where we want to give up eventually. +// +// This is used for: +// - Requesting quotes +// - Requesting cooperative XMR redeem +static REQUEST_RESPONSE_PROTOCOL_RETRY_MAX_ELASPED_TIME: Duration = Duration::from_secs(60); + +// Used for deciding how long to wait at most between retries. +static RETRY_MAX_INTERVAL: Duration = Duration::from_secs(5); #[allow(missing_debug_implementations)] pub struct EventLoop { - swap_id: Uuid, swarm: libp2p::Swarm, - alice_peer_id: PeerId, db: Arc, - // These streams represents outgoing requests that we have to make - // These are essentially queues of requests that we will send to Alice once we are connected to her. - quote_requests: bmrng::RequestReceiverStream<(), Result>, - cooperative_xmr_redeem_requests: bmrng::RequestReceiverStream< + // When a new `SwapEventLoopHandle` is created: + // 1. a channel is created for the EventLoop to send transfer_proofs to SwapEventLoopHandle + // 2. the corresponding PeerId of Alice is stored + // + // The sender of the channel is sent into this queue. The receiver is stored in the `SwapEventLoopHandle`. + // + // This is polled and then moved into `registered_swap_handlers` + queued_swap_handlers: bmrng::unbounded::UnboundedRequestReceiverStream< + ( + Uuid, + PeerId, + bmrng::unbounded::UnboundedRequestSender, + ), (), + >, + registered_swap_handlers: HashMap< + Uuid, + ( + PeerId, + bmrng::unbounded::UnboundedRequestSender, + ), + >, + + // These streams represents outgoing requests that we have to make (queues) + // + // Requests are keyed by the PeerId because they do not correspond to an existing swap yet + quote_requests: + bmrng::unbounded::UnboundedRequestReceiverStream>, + // TODO: technically NewSwap.swap_id already contains the id of the swap + execution_setup_requests: + bmrng::unbounded::UnboundedRequestReceiverStream<(PeerId, NewSwap), Result>, + + // These streams represents outgoing requests that we have to make (queues) + // + // Requests are keyed by the swap_id because they correspond to a specific swap + cooperative_xmr_redeem_requests: bmrng::unbounded::UnboundedRequestReceiverStream< + (PeerId, Uuid), Result, >, - encrypted_signatures_requests: - bmrng::RequestReceiverStream>, - execution_setup_requests: bmrng::RequestReceiverStream>, + encrypted_signatures_requests: bmrng::unbounded::UnboundedRequestReceiverStream< + (PeerId, Uuid, EncryptedSignature), + Result<(), OutboundFailure>, + >, // These represents requests that are currently in-flight. // Meaning that we have sent them to Alice, but we have not yet received a response. // Once we get a response to a matching [`RequestId`], we will use the responder to relay the // response. - inflight_quote_requests: - HashMap>>, - inflight_encrypted_signature_requests: - HashMap>>, - inflight_swap_setup: Option>>, + inflight_quote_requests: HashMap< + OutboundRequestId, + bmrng::unbounded::UnboundedResponder>, + >, + inflight_encrypted_signature_requests: HashMap< + OutboundRequestId, + bmrng::unbounded::UnboundedResponder>, + >, + inflight_swap_setup: + HashMap<(PeerId, Uuid), bmrng::unbounded::UnboundedResponder>>, inflight_cooperative_xmr_redeem_requests: HashMap< OutboundRequestId, - bmrng::Responder>, + bmrng::unbounded::UnboundedResponder< + Result, + >, >, - /// The sender we will use to relay incoming transfer proofs to the EventLoopHandle - /// The corresponding receiver is stored in the EventLoopHandle - transfer_proof_sender: bmrng::RequestSender, - - /// The future representing the successful handling of an incoming transfer - /// proof. + /// The future representing the successful handling of an incoming transfer proof (by the state machine) + /// + /// Once we've sent a transfer proof to the ongoing swap, a future is inserted into this set + /// which will resolve once the state machine has "processed" the transfer proof. /// - /// Once we've sent a transfer proof to the ongoing swap, this future waits - /// until the swap took it "out" of the `EventLoopHandle`. As this future - /// resolves, we use the `ResponseChannel` returned from it to send an ACK - /// to Alice that we have successfully processed the transfer proof. - pending_transfer_proof: OptionFuture>>, + /// The future will yield the swap_id and the response channel which are used to send an acknowledgement to Alice. + pending_transfer_proof_acks: FuturesUnordered)>>, } impl EventLoop { + fn swap_peer_id(&self, swap_id: &Uuid) -> Option { + self.registered_swap_handlers + .get(swap_id) + .map(|(peer_id, _)| *peer_id) + } + pub fn new( - swap_id: Uuid, swarm: Swarm, - alice_peer_id: PeerId, db: Arc, ) -> Result<(Self, EventLoopHandle)> { - // We still use a timeout here, because this protocol does not dial Alice itself - // and we want to fail if we cannot reach Alice + // We still use a timeout here because we trust our own implementation of the swap setup protocol less than the libp2p library let (execution_setup_sender, execution_setup_receiver) = - bmrng::channel_with_timeout(1, EXECUTION_SETUP_PROTOCOL_TIMEOUT); + bmrng::unbounded::channel_with_timeout(EXECUTION_SETUP_MAX_ELAPSED_TIME); // It is okay to not have a timeout here, as timeouts are enforced by the request-response protocol - let (transfer_proof_sender, transfer_proof_receiver) = bmrng::channel(1); - let (encrypted_signature_sender, encrypted_signature_receiver) = bmrng::channel(1); - let (quote_sender, quote_receiver) = bmrng::channel(1); - let (cooperative_xmr_redeem_sender, cooperative_xmr_redeem_receiver) = bmrng::channel(1); + let (encrypted_signature_sender, encrypted_signature_receiver) = + bmrng::unbounded::channel(); + let (quote_sender, quote_receiver) = bmrng::unbounded::channel(); + let (cooperative_xmr_redeem_sender, cooperative_xmr_redeem_receiver) = + bmrng::unbounded::channel(); + let (queued_transfer_proof_sender, queued_transfer_proof_receiver) = + bmrng::unbounded::channel(); let event_loop = EventLoop { - swap_id, swarm, - alice_peer_id, + db, + queued_swap_handlers: queued_transfer_proof_receiver.into(), + registered_swap_handlers: HashMap::default(), execution_setup_requests: execution_setup_receiver.into(), - transfer_proof_sender, encrypted_signatures_requests: encrypted_signature_receiver.into(), cooperative_xmr_redeem_requests: cooperative_xmr_redeem_receiver.into(), quote_requests: quote_receiver.into(), inflight_quote_requests: HashMap::default(), - inflight_swap_setup: None, + inflight_swap_setup: HashMap::default(), inflight_encrypted_signature_requests: HashMap::default(), inflight_cooperative_xmr_redeem_requests: HashMap::default(), - pending_transfer_proof: OptionFuture::from(None), - db, + pending_transfer_proof_acks: FuturesUnordered::new(), }; let handle = EventLoopHandle { execution_setup_sender, - transfer_proof_receiver, encrypted_signature_sender, cooperative_xmr_redeem_sender, quote_sender, + queued_transfer_proof_sender, }; Ok((event_loop, handle)) } pub async fn run(mut self) { - match self.swarm.dial(DialOpts::from(self.alice_peer_id)) { - Ok(()) => {} - Err(e) => { - tracing::error!("Failed to initiate dial to Alice: {:?}", e); - return; - } - } - loop { // Note: We are making very elaborate use of `select!` macro's feature here. Make sure to read the documentation thoroughly: https://docs.rs/tokio/1.4.0/tokio/macro.select.html tokio::select! { swarm_event = self.swarm.select_next_some() => { match swarm_event { SwarmEvent::Behaviour(OutEvent::QuoteReceived { id, response }) => { + tracing::trace!( + %id, + "Received quote" + ); + if let Some(responder) = self.inflight_quote_requests.remove(&id) { let _ = responder.respond(Ok(response)); } } - SwarmEvent::Behaviour(OutEvent::SwapSetupCompleted(response)) => { - if let Some(responder) = self.inflight_swap_setup.take() { - let _ = responder.respond(*response); + SwarmEvent::Behaviour(OutEvent::SwapSetupCompleted { peer, swap_id, result }) => { + tracing::trace!( + %peer, + "Processing swap setup completion" + ); + + if let Some(responder) = self.inflight_swap_setup.remove(&(peer, swap_id)) { + let _ = responder.respond(*result); } } SwarmEvent::Behaviour(OutEvent::TransferProofReceived { msg, channel, peer }) => { + tracing::trace!( + %peer, + %msg.swap_id, + "Received transfer proof" + ); + let swap_id = msg.swap_id; - if swap_id == self.swap_id { - if peer != self.alice_peer_id { + // Check if we have a registered handler for this swap + if let Some((expected_peer_id, sender)) = self.registered_swap_handlers.get(&swap_id) { + // Ensure the transfer proof is coming from the expected peer + if peer != *expected_peer_id { tracing::warn!( - %swap_id, - "Ignoring malicious transfer proof from {}, expected to receive it from {}", - peer, - self.alice_peer_id); - continue; - } - - // Immediately acknowledge if we've already processed this transfer proof - // This handles the case where Alice didn't receive our previous acknowledgment - // and is retrying sending the transfer proof - if let Ok(state) = self.db.get_state(swap_id).await { - let state: BobState = state.try_into() - .expect("Bobs database only contains Bob states"); - - if has_already_processed_transfer_proof(&state) { - tracing::warn!("Received transfer proof for swap {} but we are already in state {}. Acknowledging immediately. Alice most likely did not receive the acknowledgment when we sent it before", swap_id, state); - - // We set this to a future that will resolve immediately, and returns the channel - // This will be resolved in the next iteration of the event loop, and a response will be sent to Alice - self.pending_transfer_proof = OptionFuture::from(Some(async move { - channel - }.boxed())); - - continue; - } + %swap_id, + "Ignoring malicious transfer proof from {}, expected to receive it from {}", + peer, + expected_peer_id); + continue; } - let mut responder = match self.transfer_proof_sender.send(msg.tx_lock_proof).await { - Ok(responder) => responder, - Err(e) => { - tracing::warn!("Failed to pass on transfer proof: {:#}", e); - continue; + // Send the transfer proof to the registered handler + match sender.send(msg.tx_lock_proof) { + Ok(mut responder) => { + // Insert a future that will resolve when the handle "takes the transfer proof out" + self.pending_transfer_proof_acks.push(async move { + let _ = responder.recv().await; + (swap_id, channel) + }.boxed()); } - }; - - self.pending_transfer_proof = OptionFuture::from(Some(async move { - let _ = responder.recv().await; - - channel - }.boxed())); - }else { - // Check if the transfer proof is sent from the correct peer and if we have a record of the swap - match self.db.get_peer_id(swap_id).await { - // We have a record of the swap - Ok(buffer_swap_alice_peer_id) => { - if buffer_swap_alice_peer_id == self.alice_peer_id { - // Save transfer proof in the database such that we can process it later when we resume the swap - match self.db.insert_buffered_transfer_proof(swap_id, msg.tx_lock_proof).await { - Ok(_) => { - tracing::info!("Received transfer proof for swap {} while running swap {}. Buffering this transfer proof in the database for later retrieval", swap_id, self.swap_id); - let _ = self.swarm.behaviour_mut().transfer_proof.send_response(channel, ()); - } - Err(e) => { - tracing::error!("Failed to buffer transfer proof for swap {}: {:#}", swap_id, e); - } - }; - }else { - tracing::warn!( - %swap_id, - "Ignoring malicious transfer proof from {}, expected to receive it from {}", - self.swap_id, - buffer_swap_alice_peer_id); - } - }, - // We do not have a record of the swap or an error occurred while retrieving the peer id of Alice Err(e) => { - if let Some(sqlx::Error::RowNotFound) = e.downcast_ref::() { - tracing::warn!("Ignoring transfer proof for swap {} while running swap {}. We do not have a record of this swap", swap_id, self.swap_id); - } else { - tracing::error!("Ignoring transfer proof for swap {} while running swap {}. Failed to retrieve the peer id of Alice for the corresponding swap: {:#}", swap_id, self.swap_id, e); - } + tracing::warn!( + %swap_id, + %peer, + error = ?e, + "Failed to pass transfer proof to registered handler" + ); } } + + continue; + } + + // Immediately acknowledge if we've already processed this transfer proof + // This handles the case where Alice didn't receive our previous acknowledgment + // and is retrying sending the transfer proof + match should_acknowledge_transfer_proof(self.db.clone(), swap_id, peer).await { + Ok(true) => { + // We set this to a future that will resolve immediately, and returns the channel + // This will be resolved in the next iteration of the event loop, and a response will be sent to Alice + self.pending_transfer_proof_acks.push(async move { + (swap_id, channel) + }.boxed()); + + // Skip evaluation of whether we should buffer the transfer proof + // if we already acknowledged the transfer proof + continue; + } + // TODO: Maybe we should log here? + Ok(false) => {} + Err(error) => { + tracing::warn!( + %swap_id, + %peer, + error = ?error, + "Failed to evaluate if we should acknowledge the transfer proof, we will not respond at all" + ); + } + } + + // Check if we should buffer the transfer proof + if let Err(error) = buffer_transfer_proof_if_needed(self.db.clone(), swap_id, peer, msg.tx_lock_proof).await { + tracing::warn!( + %swap_id, + %peer, + error = ?error, + "Failed to buffer transfer proof" + ); } } SwarmEvent::Behaviour(OutEvent::EncryptedSignatureAcknowledged { id }) => { @@ -239,30 +291,21 @@ impl EventLoop { tracing::warn!(%peer, err = ?error, "Communication error"); return; } - SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } if peer_id == self.alice_peer_id => { - tracing::info!(peer_id = %endpoint.get_remote_address(), "Connected to Alice"); + SwarmEvent::ConnectionEstablished { peer_id: _, endpoint, .. } => { + tracing::info!(peer_id = %endpoint.get_remote_address(), "Connected to peer"); } - SwarmEvent::Dialing { peer_id: Some(alice_peer_id), connection_id } if alice_peer_id == self.alice_peer_id => { - tracing::debug!(%alice_peer_id, %connection_id, "Dialing Alice"); + SwarmEvent::Dialing { peer_id: Some(peer_id), connection_id } => { + tracing::debug!(%peer_id, %connection_id, "Dialing peer"); } - SwarmEvent::ConnectionClosed { peer_id, endpoint, num_established, cause: Some(error), connection_id } if peer_id == self.alice_peer_id && num_established == 0 => { - tracing::warn!(peer_id = %endpoint.get_remote_address(), cause = ?error, %connection_id, "Lost connection to Alice"); - - if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() { - tracing::info!(seconds_until_next_redial = %duration.as_secs(), "Waiting for next redial attempt"); - } + SwarmEvent::ConnectionClosed { peer_id: _, endpoint, num_established, cause: Some(error), connection_id } if num_established == 0 => { + tracing::warn!(peer_id = %endpoint.get_remote_address(), cause = ?error, %connection_id, "Lost connection to peer"); } - SwarmEvent::ConnectionClosed { peer_id, num_established, cause: None, .. } if peer_id == self.alice_peer_id && num_established == 0 => { + SwarmEvent::ConnectionClosed { peer_id, num_established, cause: None, .. } if num_established == 0 => { // no error means the disconnection was requested - tracing::info!("Successfully closed connection to Alice"); - return; + tracing::info!(%peer_id, "Successfully closed connection to peer"); } - SwarmEvent::OutgoingConnectionError { peer_id: Some(alice_peer_id), error, connection_id } if alice_peer_id == self.alice_peer_id => { - tracing::warn!(%alice_peer_id, %connection_id, ?error, "Failed to connect to Alice"); - - if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() { - tracing::info!(seconds_until_next_redial = %duration.as_secs(), "Waiting for next redial attempt"); - } + SwarmEvent::OutgoingConnectionError { peer_id: Some(peer_id), error, connection_id } => { + tracing::warn!(%peer_id, %connection_id, ?error, "Outgoing connection error to peer"); } SwarmEvent::Behaviour(OutEvent::OutboundRequestResponseFailure {peer, error, request_id, protocol}) => { tracing::error!( @@ -299,116 +342,195 @@ impl EventLoop { %request_id, ?error, %protocol, - "Failed to receive request-response request from peer"); + "Failed to receive or send response for request-response request from peer"); + } + SwarmEvent::Behaviour(OutEvent::Redial(redial::Event::ScheduledRedial { peer, next_dial_in })) => { + tracing::trace!( + %peer, + seconds_until_next_redial = %next_dial_in.as_secs(), + "Scheduled redial for peer" + ); } _ => {} } }, // Handle to-be-sent outgoing requests for all our network protocols. - Some(((), responder)) = self.quote_requests.next().fuse() => { - let id = self.swarm.behaviour_mut().quote.send_request(&self.alice_peer_id, ()); - self.inflight_quote_requests.insert(id, responder); + Some((peer_id, responder)) = self.quote_requests.next().fuse() => { + let outbound_request_id = self.swarm.behaviour_mut().quote.send_request(&peer_id, ()); + self.inflight_quote_requests.insert(outbound_request_id, responder); + + tracing::trace!( + %peer_id, + %outbound_request_id, + "Dispatching outgoing quote request" + ); }, - Some((tx_redeem_encsig, responder)) = self.encrypted_signatures_requests.next().fuse() => { + Some(((peer_id, swap_id, tx_redeem_encsig), responder)) = self.encrypted_signatures_requests.next().fuse() => { let request = encrypted_signature::Request { - swap_id: self.swap_id, + swap_id, tx_redeem_encsig }; - let id = self.swarm.behaviour_mut().encrypted_signature.send_request(&self.alice_peer_id, request); - self.inflight_encrypted_signature_requests.insert(id, responder); + let outbound_request_id = self.swarm.behaviour_mut().encrypted_signature.send_request(&peer_id, request); + self.inflight_encrypted_signature_requests.insert(outbound_request_id, responder); + + tracing::trace!( + %peer_id, + %swap_id, + %outbound_request_id, + "Dispatching outgoing encrypted signature" + ); }, - Some((_, responder)) = self.cooperative_xmr_redeem_requests.next().fuse() => { - let id = self.swarm.behaviour_mut().cooperative_xmr_redeem.send_request(&self.alice_peer_id, Request { - swap_id: self.swap_id + Some(((peer_id, swap_id), responder)) = self.cooperative_xmr_redeem_requests.next().fuse() => { + let outbound_request_id = self.swarm.behaviour_mut().cooperative_xmr_redeem.send_request(&peer_id, Request { + swap_id }); - self.inflight_cooperative_xmr_redeem_requests.insert(id, responder); + self.inflight_cooperative_xmr_redeem_requests.insert(outbound_request_id, responder); + + tracing::trace!( + %peer_id, + %swap_id, + %outbound_request_id, + "Dispatching outgoing cooperative xmr redeem request" + ); }, - // We use `self.is_connected_to_alice` as a guard to "buffer" requests until we are connected. - // because the protocol does not dial Alice itself - // (unlike request-response above) - Some((swap, responder)) = self.execution_setup_requests.next().fuse(), if self.is_connected_to_alice() => { - self.swarm.behaviour_mut().swap_setup.start(self.alice_peer_id, swap).await; - self.inflight_swap_setup = Some(responder); - }, + // Instruct the swap setup behaviour to do a swap setup request + // The behaviour will instruct the swarm to dial Alice, so we don't need to check if we are connected + Some(((alice_peer_id, swap), responder)) = self.execution_setup_requests.next().fuse() => { + let swap_id = swap.swap_id.clone(); + + self.swarm.behaviour_mut().swap_setup.start(alice_peer_id, swap).await; + self.inflight_swap_setup.insert((alice_peer_id, swap_id), responder); + tracing::trace!( + %alice_peer_id, + "Dispatching outgoing execution setup request" + ); + }, // Send an acknowledgement to Alice once the EventLoopHandle has processed a received transfer proof - // We use `self.is_connected_to_alice` as a guard to "buffer" requests until we are connected. - // - // Why do we do this here but not for the other request-response channels? - // This is the only request, we don't have a retry mechanism for. We lazily send this. - Some(response_channel) = &mut self.pending_transfer_proof, if self.is_connected_to_alice() => { + Some((swap_id, response_channel)) = self.pending_transfer_proof_acks.next() => { + tracing::trace!( + %swap_id, + "Dispatching outgoing transfer proof acknowledgment"); + + // We do not check if we are connected to Alice here because responding on a channel + // which has been dropped works even if a new connections has been established since + // will not work because because a channel is always bounded to one connection if self.swarm.behaviour_mut().transfer_proof.send_response(response_channel, ()).is_err() { tracing::warn!("Failed to send acknowledgment to Alice that we have received the transfer proof"); } else { tracing::info!("Sent acknowledgment to Alice that we have received the transfer proof"); - self.pending_transfer_proof = OptionFuture::from(None); } }, + + Some(((swap_id, peer_id, sender), responder)) = self.queued_swap_handlers.next().fuse() => { + tracing::trace!(%swap_id, %peer_id, "Registering swap handle for a swap internally inside the event loop"); + + // This registers the swap_id -> peer_id and swap_id -> transfer_proof_sender + self.registered_swap_handlers.insert(swap_id, (peer_id, sender)); + + // Instruct the swarm to contineously redial the peer + // TODO: We must remove it again once the swap is complete, otherwise we will redial indefinitely + self.swarm.behaviour_mut().redial.add_peer(peer_id); + + // Acknowledge the registration + let _ = responder.respond(()); + }, } } } - - fn is_connected_to_alice(&self) -> bool { - self.swarm.is_connected(&self.alice_peer_id) - } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EventLoopHandle { - /// When a NewSwap object is sent into this channel, the EventLoop will: - /// 1. Trigger the swap setup protocol with Alice to negotiate the swap parameters + /// When a (PeerId, NewSwap) tuple is sent into this channel, the EventLoop will: + /// 1. Trigger the swap setup protocol with the specified peer to negotiate the swap parameters /// 2. Return the resulting State2 if successful /// 3. Return an anyhow error if the request fails - execution_setup_sender: bmrng::RequestSender>, - - /// Receiver for incoming Monero transfer proofs from Alice. - /// When a proof is received, we process it and acknowledge receipt back to the EventLoop - /// The EventLoop will then send an acknowledgment back to Alice over the network - transfer_proof_receiver: bmrng::RequestReceiver, + execution_setup_sender: + bmrng::unbounded::UnboundedRequestSender<(PeerId, NewSwap), Result>, - /// When an encrypted signature is sent into this channel, the EventLoop will: - /// 1. Send the encrypted signature to Alice over the network - /// 2. Return Ok(()) if Alice acknowledges receipt, or + /// When a (PeerId, Uuid, EncryptedSignature) tuple is sent into this channel, the EventLoop will: + /// 1. Send the encrypted signature to the specified peer over the network + /// 2. Return Ok(()) if the peer acknowledges receipt, or /// 3. Return an OutboundFailure error if the request fails - encrypted_signature_sender: - bmrng::RequestSender>, + encrypted_signature_sender: bmrng::unbounded::UnboundedRequestSender< + (PeerId, Uuid, EncryptedSignature), + Result<(), OutboundFailure>, + >, - /// When a () is sent into this channel, the EventLoop will: - /// 1. Request a price quote from Alice + /// When a PeerId is sent into this channel, the EventLoop will: + /// 1. Request a price quote from the specified peer /// 2. Return the quote if successful /// 3. Return an OutboundFailure error if the request fails - quote_sender: bmrng::RequestSender<(), Result>, + quote_sender: + bmrng::unbounded::UnboundedRequestSender>, - /// When a () is sent into this channel, the EventLoop will: - /// 1. Request Alice's cooperation in redeeming the Monero - /// 2. Return the a response object (Fullfilled or Rejected), if the network request is successful + /// When a (PeerId, Uuid) tuple is sent into this channel, the EventLoop will: + /// 1. Request the specified peer's cooperation in redeeming the Monero for the given swap + /// 2. Return a response object (Fullfilled or Rejected), if the network request is successful /// The Fullfilled object contains the keys required to redeem the Monero /// 3. Return an OutboundFailure error if the network request fails - cooperative_xmr_redeem_sender: bmrng::RequestSender< - (), + cooperative_xmr_redeem_sender: bmrng::unbounded::UnboundedRequestSender< + (PeerId, Uuid), Result, >, + + queued_transfer_proof_sender: bmrng::unbounded::UnboundedRequestSender< + ( + Uuid, + PeerId, + bmrng::unbounded::UnboundedRequestSender, + ), + (), + >, } impl EventLoopHandle { - fn create_retry_config(max_elapsed_time: Duration) -> backoff::ExponentialBackoff { - backoff::ExponentialBackoffBuilder::new() - .with_max_elapsed_time(max_elapsed_time.into()) - .with_max_interval(Duration::from_secs(5)) - .build() + /// Creates a SwapEventLoopHandle for a specific swap + /// This registers the swap's transfer proof receiver with the event loop + pub async fn swap_handle( + &mut self, + peer_id: PeerId, + swap_id: Uuid, + ) -> Result { + // Create a channel for sending transfer proofs from the `EventLoop` to the `SwapEventLoopHandle` + // + // The sender is stored in the `EventLoop`. The receiver is stored in the `SwapEventLoopHandle`. + let (transfer_proof_sender, transfer_proof_receiver) = bmrng::unbounded_channel(); + + // Register this sender in the `EventLoop` + // It is put into the queue and then later moved into `registered_transfer_proof_senders` + // + // We use `send(...) instead of send_receive(...)` because the event loop needs to be running for this to respond + self.queued_transfer_proof_sender + .send((swap_id, peer_id, transfer_proof_sender)) + .context("Failed to register transfer proof sender with event loop")?; + + Ok(SwapEventLoopHandle { + handle: self.clone(), + peer_id, + swap_id, + transfer_proof_receiver: Some(transfer_proof_receiver), + }) } - pub async fn setup_swap(&mut self, swap: NewSwap) -> Result { - tracing::debug!(swap = ?swap, "Sending swap setup request"); + /// Sets up a swap with the specified peer + /// + /// This will retry until the maximum elapsed time is reached. It is therefore fallible. + pub async fn setup_swap(&mut self, peer_id: PeerId, swap: NewSwap) -> Result { + tracing::debug!(swap = ?swap, %peer_id, "Sending swap setup request"); - let backoff = Self::create_retry_config(EXECUTION_SETUP_PROTOCOL_TIMEOUT); + let backoff = + retry::give_up_eventually(RETRY_MAX_INTERVAL, EXECUTION_SETUP_MAX_ELAPSED_TIME); backoff::future::retry_notify(backoff, || async { - match self.execution_setup_sender.send_receive(swap.clone()).await { - Ok(Ok(state2)) => Ok(state2), + match self.execution_setup_sender.send_receive((peer_id, swap.clone())).await { + Ok(Ok(state2)) => { + Ok(state2) + } // These are errors thrown by the swap_setup/bob behaviour Ok(Err(err)) => { Err(backoff::Error::transient(err.context("A network error occurred while setting up the swap"))) @@ -428,33 +550,26 @@ impl EventLoopHandle { error = ?err, "Failed to setup swap. We will retry in {} seconds", wait_time.as_secs() - ) + ); }) .await .context("Failed to setup swap after retries") } - pub async fn recv_transfer_proof(&mut self) -> Result { - let (transfer_proof, responder) = self - .transfer_proof_receiver - .recv() - .await - .context("Failed to receive transfer proof")?; - - responder - .respond(()) - .context("Failed to acknowledge receipt of transfer proof")?; - - Ok(transfer_proof) - } - - pub async fn request_quote(&mut self) -> Result { - tracing::debug!("Requesting quote"); + /// Requests a quote from the specified peer + /// + /// This will retry until the maximum elapsed time is reached. It is therefore fallible. + pub async fn request_quote(&mut self, peer_id: PeerId) -> Result { + tracing::debug!(%peer_id, "Requesting quote"); - let backoff = Self::create_retry_config(REQUEST_RESPONSE_PROTOCOL_TIMEOUT); + // We want to give up eventually here + let backoff = retry::give_up_eventually( + RETRY_MAX_INTERVAL, + REQUEST_RESPONSE_PROTOCOL_RETRY_MAX_ELASPED_TIME, + ); backoff::future::retry_notify(backoff, || async { - match self.quote_sender.send_receive(()).await { + match self.quote_sender.send_receive(peer_id).await { Ok(Ok(quote)) => Ok(quote), Ok(Err(err)) => { Err(backoff::Error::transient(anyhow!(err).context("A network error occurred while requesting a quote"))) @@ -474,13 +589,24 @@ impl EventLoopHandle { .context("Failed to request quote after retries") } - pub async fn request_cooperative_xmr_redeem(&mut self) -> Result { - tracing::debug!("Requesting cooperative XMR redeem"); + /// Requests the cooperative XMR redeem from the specified peer + /// + /// This will retry until the maximum elapsed time is reached. It is therefore fallible. + pub async fn request_cooperative_xmr_redeem( + &mut self, + peer_id: PeerId, + swap_id: Uuid, + ) -> Result { + tracing::debug!(%peer_id, %swap_id, "Requesting cooperative XMR redeem"); - let backoff = Self::create_retry_config(REQUEST_RESPONSE_PROTOCOL_TIMEOUT); + // We want to give up eventually here + let backoff = retry::give_up_eventually( + RETRY_MAX_INTERVAL, + REQUEST_RESPONSE_PROTOCOL_RETRY_MAX_ELASPED_TIME, + ); backoff::future::retry_notify(backoff, || async { - match self.cooperative_xmr_redeem_sender.send_receive(()).await { + match self.cooperative_xmr_redeem_sender.send_receive((peer_id, swap_id)).await { Ok(Ok(response)) => Ok(response), Ok(Err(err)) => { Err(backoff::Error::transient(anyhow!(err).context("A network error occurred while requesting cooperative XMR redeem"))) @@ -500,20 +626,22 @@ impl EventLoopHandle { .context("Failed to request cooperative XMR redeem after retries") } + /// Sends an encrypted signature to the specified peer + /// + /// This will retry indefinitely until we succeed. It is therefore infalible. pub async fn send_encrypted_signature( &mut self, + peer_id: PeerId, + swap_id: Uuid, tx_redeem_encsig: EncryptedSignature, - ) -> Result<()> { - tracing::debug!("Sending encrypted signature"); + ) -> () { + tracing::debug!(%peer_id, %swap_id, "Sending encrypted signature"); // We will retry indefinitely until we succeed - let backoff = backoff::ExponentialBackoffBuilder::new() - .with_max_elapsed_time(None) - .with_max_interval(REQUEST_RESPONSE_PROTOCOL_TIMEOUT) - .build(); + let backoff = retry::never_give_up(RETRY_MAX_INTERVAL); backoff::future::retry_notify(backoff, || async { - match self.encrypted_signature_sender.send_receive(tx_redeem_encsig.clone()).await { + match self.encrypted_signature_sender.send_receive((peer_id, swap_id, tx_redeem_encsig.clone())).await { Ok(Ok(_)) => Ok(()), Ok(Err(err)) => { Err(backoff::Error::transient(anyhow!(err).context("A network error occurred while sending the encrypted signature"))) @@ -530,6 +658,131 @@ impl EventLoopHandle { ) }) .await - .context("Failed to send encrypted signature after retries") + .expect("we should never run out of retries when sending an encrypted signature") + } +} + +#[derive(Debug)] +pub struct SwapEventLoopHandle { + handle: EventLoopHandle, + peer_id: PeerId, + swap_id: Uuid, + transfer_proof_receiver: + Option>, +} + +impl SwapEventLoopHandle { + pub async fn recv_transfer_proof(&mut self) -> Result { + let receiver = self + .transfer_proof_receiver + .as_mut() + .context("Transfer proof receiver not available")?; + + let (transfer_proof, responder) = receiver + .recv() + .await + .context("Failed to receive transfer proof")?; + + responder + .respond(()) + .context("Failed to acknowledge receipt of transfer proof")?; + + Ok(transfer_proof) + } + + pub async fn send_encrypted_signature(&mut self, tx_redeem_encsig: EncryptedSignature) -> () { + self.handle + .send_encrypted_signature(self.peer_id, self.swap_id, tx_redeem_encsig) + .await + } + + pub async fn request_cooperative_xmr_redeem(&mut self) -> Result { + self.handle + .request_cooperative_xmr_redeem(self.peer_id, self.swap_id) + .await + } + + pub async fn setup_swap(&mut self, swap: NewSwap) -> Result { + self.handle.setup_swap(self.peer_id, swap).await + } + + pub async fn request_quote(&mut self) -> Result { + self.handle.request_quote(self.peer_id).await + } +} + +/// Returns Ok(true) if we should acknowledge the transfer proof +/// +/// - Checks if the peer id is the expected peer id +/// - Checks if the state indicates that we have already processed the transfer proof +async fn should_acknowledge_transfer_proof( + db: Arc, + swap_id: Uuid, + peer_id: PeerId, +) -> Result { + let expected_peer_id = db.get_peer_id(swap_id).await.context( + "Failed to get peer id for swap to check if we should acknowledge the transfer proof", + )?; + + // If the peer id is not the expected peer id, we should not acknowledge the transfer proof + // This is to prevent malicious requests + if expected_peer_id != peer_id { + bail!("Expected peer id {} but got {}", expected_peer_id, peer_id); + } + + let state = db.get_state(swap_id).await.context( + "Failed to get state for swap to check if we should acknowledge the transfer proof", + )?; + let state: BobState = state.try_into().context( + "Failed to convert state to BobState to check if we should acknowledge the transfer proof", + )?; + + Ok(has_already_processed_transfer_proof(&state)) +} + +/// Buffers the transfer proof in the database if its from the expected peer +async fn buffer_transfer_proof_if_needed( + db: Arc, + swap_id: Uuid, + peer_id: PeerId, + transfer_proof: monero::TransferProof, +) -> Result<()> { + let expected_peer_id = db.get_peer_id(swap_id).await.context( + "Failed to get peer id for swap to check if we should buffer the transfer proof", + )?; + + if expected_peer_id != peer_id { + bail!("Expected peer id {} but got {}", expected_peer_id, peer_id); + } + + db.insert_buffered_transfer_proof(swap_id, transfer_proof) + .await + .context("Failed to buffer transfer proof in database") +} + +mod retry { + use std::time::Duration; + + // Constructs a retry config that will retry indefinitely + pub(crate) fn never_give_up(max_interval: Duration) -> backoff::ExponentialBackoff { + create_retry_config(max_interval, None) + } + + // Constructs a retry config that will retry for a given amount of time + pub(crate) fn give_up_eventually( + max_interval: Duration, + max_elapsed_time: Duration, + ) -> backoff::ExponentialBackoff { + create_retry_config(max_interval, max_elapsed_time) + } + + fn create_retry_config( + max_interval: Duration, + max_elapsed_time: impl Into>, + ) -> backoff::ExponentialBackoff { + backoff::ExponentialBackoffBuilder::new() + .with_max_interval(max_interval) + .with_max_elapsed_time(max_elapsed_time.into()) + .build() } } diff --git a/swap/src/common/mod.rs b/swap/src/common/mod.rs index ecaae413e2..a7f333af37 100644 --- a/swap/src/common/mod.rs +++ b/swap/src/common/mod.rs @@ -53,35 +53,65 @@ pub async fn warn_if_outdated(current_version: &str) -> anyhow::Result<()> { /// } /// }, None, std::time::Duration::from_secs(60)); /// ``` -pub async fn retry( +pub async fn retry( description: &str, - function: F, + mut op: F, max_elapsed_time: impl Into>, max_interval: impl Into>, ) -> Result where - F: Fn() -> Fut, - Fut: Future>>, - E: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static, + F: FnMut() -> Fut, + Fut: Future>>, { - let max_interval = max_interval.into().unwrap_or(Duration::from_secs(15)); + static DEFAULT_MAX_INTERVAL: Duration = Duration::from_secs(15); + use backoff::backoff::Backoff; - let config = backoff::ExponentialBackoffBuilder::new() + let max_interval = max_interval.into().unwrap_or(DEFAULT_MAX_INTERVAL); + + let mut backoff = backoff::ExponentialBackoffBuilder::new() .with_max_elapsed_time(max_elapsed_time.into()) .with_max_interval(max_interval) .build(); - let result = backoff::future::retry_notify(config, function, |err, wait_time: Duration| { + loop { + let err = match op().await { + Ok(v) => return Ok(v), + Err(err) => err, + }; + + let (err, next) = match err { + backoff::Error::Permanent(err) => { + tracing::error!( + "Failed operation `{}` with permanent error: {:#?}, we will not retry", + description, + err + ); + return Err(err); + } + backoff::Error::Transient { err, retry_after } => { + match retry_after.or_else(|| backoff.next_backoff()) { + Some(next) => (err, next), + None => { + tracing::error!( + "Failed operation `{}` with error: {:#?}, no more retries left", + description, + err + ); + return Err(err); + } + } + } + }; + tracing::warn!( - error = ?err, - "Failed operation `{}`, retrying in {} seconds", + "Failed operation `{}` with error: {:#?}, retrying in {} seconds", description, - wait_time.as_secs() + err, + next.as_secs() ); - }) - .await; - result.map_err(|e| anyhow!("{}", e)) + tokio::time::sleep(next).await; + } } /// helper macro for [`redact`]... eldrich sorcery diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index b3cbf479cc..14db42362e 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -20,6 +20,14 @@ pub async fn create_tor_client( let state_dir = data_dir.join("state"); let cache_dir = data_dir.join("cache"); + // Workaround for https://gitlab.torproject.org/tpo/core/arti/-/issues/2224 + // We delete guards.json (if it exists) on startup to prevent an issue where arti will not find any guards to connect to + // This forces new guards on every startup + // + // TODO: This is not good for privacy and should be removed as soon as this is fixed in arti itself. + let guards_file = state_dir.join("state").join("guards.json"); + let _ = tokio::fs::remove_file(&guards_file).await; + // The client configuration describes how to connect to the Tor network, // and what directories to use for storing persistent state. let mut config = TorClientConfigBuilder::from_directories(state_dir, cache_dir); diff --git a/swap/src/common/tracing_util.rs b/swap/src/common/tracing_util.rs index d4ad691c58..7db59d419b 100644 --- a/swap/src/common/tracing_util.rs +++ b/swap/src/common/tracing_util.rs @@ -13,90 +13,28 @@ use tracing_subscriber::{fmt, EnvFilter, Layer}; use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriLogEvent}; -const TOR_CRATES: &[&str] = &[ - "arti", - "arti-client", - "arti-fork", - "tor-api2", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", - "tor-cell", - "tor-cert", - "tor-chanmgr", - "tor-checkable", - "tor-circmgr", - "tor-config", - "tor-config-path", - "tor-consdiff", - "tor-dirclient", - "tor-dirmgr", - "tor-error", - "tor-general-addr", - "tor-guardmgr", - "tor-hsclient", - "tor-hscrypto", - "tor-hsservice", - "tor-key-forge", - "tor-keymgr", - "tor-linkspec", - "tor-llcrypto", - "tor-log-ratelim", - "tor-memquota", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-relay-selection", - "tor-rtcompat", - "tor-rtmock", - "tor-socksproto", - "tor-units", -]; - -const LIBP2P_CRATES: &[&str] = &[ - "libp2p", - "libp2p_swarm", - "libp2p_core", - "libp2p_tcp", - "libp2p_noise", - "libp2p_tor", - "libp2p_core::transport", - "libp2p_core::transport::choice", - "libp2p_core::transport::dummy", - "libp2p_swarm::connection", - "libp2p_swarm::dial", - "libp2p_tcp::transport", - "libp2p_noise::protocol", - "libp2p_identify", - "libp2p_ping", - "libp2p_request_response", - "libp2p_kad", - "libp2p_dns", - "libp2p_yamux", - "libp2p_quic", - "libp2p_websocket", - "libp2p_relay", - "libp2p_autonat", - "libp2p_mdns", - "libp2p_gossipsub", - "libp2p_rendezvous", - "libp2p_dcutr", - "monero_cpp", -]; - -const OUR_CRATES: &[&str] = &[ - "swap", - "asb", - "monero_sys", - "unstoppableswap-gui-rs", - "seed", - "swap_env", - "swap_fs", - "swap_serde", - "monero_rpc_pool", -]; +/// Creates a tracing layer that writes to a rolling file appender. +macro_rules! json_rolling_layer { + ($dir:expr, $prefix:expr, $env_filter:expr, $max_files:expr) => {{ + let appender: RollingFileAppender = RollingFileAppender::builder() + .rotation(Rotation::HOURLY) + .filename_prefix($prefix) + .filename_suffix("log") + .max_log_files($max_files) + .build($dir) + .expect("initializing rolling file appender failed"); + + fmt::layer() + .with_writer(appender) + .with_ansi(false) + .with_timer(UtcTime::rfc_3339()) + .with_target(false) + .with_file(true) + .with_line_number(true) + .json() + .with_filter($env_filter?) + }}; +} /// Output formats for logging messages. pub enum Format { @@ -112,60 +50,66 @@ pub enum Format { /// disregarding the arguments to this function. When `trace_stdout` is `true`, /// all tracing logs are also emitted to stdout. pub fn init( - level_filter: LevelFilter, format: Format, dir: impl AsRef, tauri_handle: Option, trace_stdout: bool, ) -> Result<()> { - // General log file for non-verbose logs - let file_appender: RollingFileAppender = tracing_appender::rolling::never(&dir, "swap-all.log"); - - // Verbose log file, rotated hourly, with a maximum of 24 files - let tracing_file_appender: RollingFileAppender = RollingFileAppender::builder() - .rotation(Rotation::HOURLY) - .filename_prefix("tracing") - .filename_suffix("log") - .max_log_files(24) - .build(&dir) - .expect("initializing rolling file appender failed"); - - // Layer for writing to the general log file - // Crates: swap, asb - // Level: Passed in - let file_layer = fmt::layer() - .with_writer(file_appender) - .with_ansi(false) - .with_timer(UtcTime::rfc_3339()) - .with_target(false) - .with_file(true) - .with_line_number(true) - .json() - .with_filter(env_filter_with_all_crates(vec![( - OUR_CRATES.to_vec(), - level_filter, - )])?); - - // Layer for writing to the verbose log file - // Crates: All crates with different levels (libp2p at INFO+, others at TRACE) - // Level: TRACE for our crates, INFO for libp2p, TRACE for tor - let tracing_file_layer = fmt::layer() - .with_writer(tracing_file_appender) - .with_ansi(false) - .with_timer(UtcTime::rfc_3339()) - .with_target(false) - .with_file(true) - .with_line_number(true) - .json() - .with_filter(env_filter_with_all_crates(vec![ - (OUR_CRATES.to_vec(), LevelFilter::TRACE), - (LIBP2P_CRATES.to_vec(), LevelFilter::TRACE), - (TOR_CRATES.to_vec(), LevelFilter::TRACE), - ])?); + // Write our crates to the general log file at DEBUG level + let file_layer = { + let file_appender: RollingFileAppender = + tracing_appender::rolling::never(&dir, "swap-all.log"); + + fmt::layer() + .with_writer(file_appender) + .with_ansi(false) + .with_timer(UtcTime::rfc_3339()) + .with_target(false) + .with_file(true) + .with_line_number(true) + .json() + .with_filter(env_filter_with_all_crates(vec![( + crates::OUR_CRATES.to_vec(), + LevelFilter::DEBUG, + )])?) + }; + + // Write our crates to a verbose log file (tracing*.log) + let tracing_file_layer = json_rolling_layer!( + &dir, + "tracing", + env_filter_with_all_crates(vec![(crates::OUR_CRATES.to_vec(), LevelFilter::TRACE)]), + 24 + ); + + // Write Tor/arti to a verbose log file (tracing-tor*.log) + let tor_file_layer = json_rolling_layer!( + &dir, + "tracing-tor", + env_filter_with_all_crates(vec![(crates::TOR_CRATES.to_vec(), LevelFilter::TRACE)]), + 24 + ); + + // Write libp2p to a verbose log file (tracing-libp2p*.log) + let libp2p_file_layer = json_rolling_layer!( + &dir, + "tracing-libp2p", + env_filter_with_all_crates(vec![(crates::LIBP2P_CRATES.to_vec(), LevelFilter::TRACE)]), + 24 + ); + + // Write monero wallet crates to a verbose log file (tracing-monero-wallet*.log) + let monero_wallet_file_layer = json_rolling_layer!( + &dir, + "tracing-monero-wallet", + env_filter_with_all_crates(vec![( + crates::MONERO_WALLET_CRATES.to_vec(), + LevelFilter::TRACE + )]), + 24 + ); // Layer for writing to the terminal - // Crates: swap, asb - // Level: Passed in let is_terminal = std::io::stderr().is_terminal(); let terminal_layer = fmt::layer() .with_writer(std::io::stderr) @@ -187,20 +131,24 @@ pub fn init( .with_line_number(true) .json() .with_filter(env_filter_with_all_crates(vec![ - (OUR_CRATES.to_vec(), LevelFilter::DEBUG), - (LIBP2P_CRATES.to_vec(), LevelFilter::INFO), - (TOR_CRATES.to_vec(), LevelFilter::INFO), + (crates::OUR_CRATES.to_vec(), LevelFilter::TRACE), + (crates::MONERO_WALLET_CRATES.to_vec(), LevelFilter::INFO), + (crates::LIBP2P_CRATES.to_vec(), LevelFilter::INFO), + (crates::TOR_CRATES.to_vec(), LevelFilter::INFO), ])?); - // If trace_stdout is true, we log all messages to the terminal - // Otherwise, we only log the bare minimum + // If trace_stdout is true, we log our crates at TRACE level, others at INFO level + // Otherwise, we only log our crates at INFO level let terminal_layer_env_filter = match trace_stdout { true => env_filter_with_all_crates(vec![ - (OUR_CRATES.to_vec(), level_filter), - (TOR_CRATES.to_vec(), level_filter), - (LIBP2P_CRATES.to_vec(), LevelFilter::INFO), + (crates::OUR_CRATES.to_vec(), LevelFilter::TRACE), + (crates::MONERO_WALLET_CRATES.to_vec(), LevelFilter::INFO), + (crates::LIBP2P_CRATES.to_vec(), LevelFilter::INFO), + (crates::TOR_CRATES.to_vec(), LevelFilter::INFO), ])?, - false => env_filter_with_all_crates(vec![(OUR_CRATES.to_vec(), level_filter)])?, + false => { + env_filter_with_all_crates(vec![(crates::OUR_CRATES.to_vec(), LevelFilter::INFO)])? + } }; let final_terminal_layer = match format { @@ -216,20 +164,28 @@ pub fn init( let subscriber = tracing_subscriber::registry() .with(file_layer) .with(tracing_file_layer) + .with(tor_file_layer) + .with(libp2p_file_layer) + .with(monero_wallet_file_layer) .with(final_terminal_layer) .with(tauri_layer); subscriber.try_init()?; // Now we can use the tracing macros to log messages - tracing::info!(%level_filter, logs_dir=%dir.as_ref().display(), "Initialized tracing. General logs will be written to swap-all.log, and verbose logs to tracing*.log"); + tracing::info!( + logs_dir = %dir.as_ref().display(), + "Initialized tracing. General logs go to swap-all.log; verbose logs: tracing*.log (ours), tracing-tor*.log (tor), tracing-libp2p*.log (libp2p)" + ); Ok(()) } /// This function controls which crate's logs actually get logged and from which level, including all crate categories. fn env_filter_with_all_crates(crates: Vec<(Vec<&str>, LevelFilter)>) -> Result { - let mut filter = EnvFilter::from_default_env(); + let mut filter = EnvFilter::builder() + .with_default_directive(LevelFilter::OFF.into()) + .from_env_lossy(); // Add directives for each group of crates with their specified level filter for (crate_names, level_filter) in crates { @@ -244,6 +200,42 @@ fn env_filter_with_all_crates(crates: Vec<(Vec<&str>, LevelFilter)>) -> Result>>, - /// Tracks the current backoff state. - backoff: ExponentialBackoff, -} - -impl Behaviour { - pub fn new(peer: PeerId, interval: Duration, max_interval: Duration) -> Self { - Self { - peer, - sleep: None, - backoff: ExponentialBackoff { - initial_interval: interval, - current_interval: interval, - max_interval, - max_elapsed_time: None, // We never give up on re-dialling - ..ExponentialBackoff::default() - }, - } - } - - pub fn until_next_redial(&self) -> Option { - let until_next_redial = self - .sleep - .as_ref()? - .deadline() - .checked_duration_since(Instant::now())?; - - Some(until_next_redial) - } -} - -impl NetworkBehaviour for Behaviour { - type ConnectionHandler = libp2p::swarm::dummy::ConnectionHandler; - type ToSwarm = (); - - fn handle_established_inbound_connection( - &mut self, - _connection_id: libp2p::swarm::ConnectionId, - peer: PeerId, - _local_addr: &Multiaddr, - _remote_addr: &Multiaddr, - ) -> Result, libp2p::swarm::ConnectionDenied> { - // We establish an inbound connection to the peer we are interested in. - // We stop re-dialling. - // Reset the backoff state to start with the initial interval again once we disconnect again - if peer == self.peer { - self.backoff.reset(); - self.sleep = None; - } - Ok(Self::ConnectionHandler {}) - } - - fn handle_established_outbound_connection( - &mut self, - _connection_id: libp2p::swarm::ConnectionId, - peer: PeerId, - _addr: &Multiaddr, - _role_override: libp2p::core::Endpoint, - ) -> Result, libp2p::swarm::ConnectionDenied> { - // We establish an outbound connection to the peer we are interested in. - // We stop re-dialling. - // Reset the backoff state to start with the initial interval again once we disconnect again - if peer == self.peer { - self.backoff.reset(); - self.sleep = None; - } - Ok(Self::ConnectionHandler {}) - } - - fn on_swarm_event(&mut self, event: libp2p::swarm::FromSwarm<'_>) { - let redial = match event { - libp2p::swarm::FromSwarm::ConnectionClosed(e) if e.peer_id == self.peer => true, - libp2p::swarm::FromSwarm::DialFailure(e) if e.peer_id == Some(self.peer) => true, - _ => false, - }; - - if redial && self.sleep.is_none() { - self.sleep = Some(Box::pin(tokio::time::sleep(self.backoff.initial_interval))); - tracing::info!(seconds_until_next_redial = %self.until_next_redial().expect("We initialize the backoff without max_elapsed_time").as_secs(), "Waiting for next redial attempt"); - } - } - - fn poll(&mut self, cx: &mut Context<'_>) -> std::task::Poll> { - let sleep = match self.sleep.as_mut() { - None => return Poll::Pending, // early exit if we shouldn't be re-dialling - Some(future) => future, - }; - - futures::ready!(sleep.poll_unpin(cx)); - - let next_dial_in = match self.backoff.next_backoff() { - Some(next_dial_in) => next_dial_in, - None => { - unreachable!("The backoff should never run out of attempts"); - } - }; - - self.sleep = Some(Box::pin(tokio::time::sleep(next_dial_in))); - - Poll::Ready(ToSwarm::Dial { - opts: DialOpts::peer_id(self.peer) - .condition(PeerCondition::Disconnected) - .build(), - }) - } - - fn on_connection_handler_event( - &mut self, - _peer_id: PeerId, - _connection_id: libp2p::swarm::ConnectionId, - _event: libp2p::swarm::THandlerOutEvent, - ) { - unreachable!("The re-dial dummy connection handler does not produce any events"); - } -} - -impl From<()> for cli::OutEvent { - fn from(_: ()) -> Self { - Self::Other - } -} diff --git a/swap/src/network/rendezvous.rs b/swap/src/network/rendezvous.rs deleted file mode 100644 index bbc0276b0f..0000000000 --- a/swap/src/network/rendezvous.rs +++ /dev/null @@ -1,39 +0,0 @@ -use libp2p::rendezvous::Namespace; -use std::fmt; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum XmrBtcNamespace { - Mainnet, - Testnet, -} - -const MAINNET: &str = "xmr-btc-swap-mainnet"; -const TESTNET: &str = "xmr-btc-swap-testnet"; - -impl fmt::Display for XmrBtcNamespace { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - XmrBtcNamespace::Mainnet => write!(f, "{}", MAINNET), - XmrBtcNamespace::Testnet => write!(f, "{}", TESTNET), - } - } -} - -impl From for Namespace { - fn from(namespace: XmrBtcNamespace) -> Self { - match namespace { - XmrBtcNamespace::Mainnet => Namespace::from_static(MAINNET), - XmrBtcNamespace::Testnet => Namespace::from_static(TESTNET), - } - } -} - -impl XmrBtcNamespace { - pub fn from_is_testnet(testnet: bool) -> XmrBtcNamespace { - if testnet { - XmrBtcNamespace::Testnet - } else { - XmrBtcNamespace::Mainnet - } - } -} diff --git a/swap/src/network/swap_setup/bob.rs b/swap/src/network/swap_setup/bob.rs deleted file mode 100644 index 6530598fdc..0000000000 --- a/swap/src/network/swap_setup/bob.rs +++ /dev/null @@ -1,383 +0,0 @@ -use crate::network::swap_setup::{protocol, BlockchainNetwork, SpotPriceError, SpotPriceResponse}; -use crate::protocol::bob::{State0, State2}; -use crate::protocol::{Message1, Message3}; -use crate::{cli, monero}; -use anyhow::{Context, Result}; -use bitcoin_wallet::BitcoinWallet; -use futures::future::{BoxFuture, OptionFuture}; -use futures::AsyncWriteExt; -use futures::FutureExt; -use libp2p::core::upgrade; -use libp2p::swarm::{ - ConnectionDenied, ConnectionHandler, ConnectionHandlerEvent, ConnectionId, FromSwarm, - NetworkBehaviour, SubstreamProtocol, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, -}; -use libp2p::{Multiaddr, PeerId}; -use std::collections::VecDeque; -use std::sync::Arc; -use std::task::Poll; -use std::time::Duration; -use swap_core::bitcoin; -use swap_env::env; -use uuid::Uuid; - -use super::{read_cbor_message, write_cbor_message, SpotPriceRequest}; - -#[allow(missing_debug_implementations)] -pub struct Behaviour { - env_config: env::Config, - bitcoin_wallet: Arc, - new_swaps: VecDeque<(PeerId, NewSwap)>, - completed_swaps: VecDeque<(PeerId, Completed)>, -} - -impl Behaviour { - pub fn new(env_config: env::Config, bitcoin_wallet: Arc) -> Self { - Self { - env_config, - bitcoin_wallet, - new_swaps: VecDeque::default(), - completed_swaps: VecDeque::default(), - } - } - - pub async fn start(&mut self, alice: PeerId, swap: NewSwap) { - self.new_swaps.push_back((alice, swap)) - } -} - -impl From for cli::OutEvent { - fn from(completed: Completed) -> Self { - cli::OutEvent::SwapSetupCompleted(Box::new(completed.0)) - } -} - -impl NetworkBehaviour for Behaviour { - type ConnectionHandler = Handler; - type ToSwarm = Completed; - - fn handle_established_inbound_connection( - &mut self, - _connection_id: ConnectionId, - _peer: PeerId, - _local_addr: &Multiaddr, - _remote_addr: &Multiaddr, - ) -> Result, ConnectionDenied> { - Ok(Handler::new(self.env_config, self.bitcoin_wallet.clone())) - } - - fn handle_established_outbound_connection( - &mut self, - _connection_id: ConnectionId, - _peer: PeerId, - _addr: &Multiaddr, - _role_override: libp2p::core::Endpoint, - ) -> Result, ConnectionDenied> { - Ok(Handler::new(self.env_config, self.bitcoin_wallet.clone())) - } - - fn on_swarm_event(&mut self, _event: FromSwarm<'_>) { - // We do not need to handle swarm events - } - - fn on_connection_handler_event( - &mut self, - peer_id: PeerId, - _connection_id: libp2p::swarm::ConnectionId, - event: THandlerOutEvent, - ) { - self.completed_swaps.push_back((peer_id, event)); - } - - fn poll( - &mut self, - _cx: &mut std::task::Context<'_>, - ) -> Poll>> { - // Forward completed swaps from the connection handler to the swarm - if let Some((_peer, completed)) = self.completed_swaps.pop_front() { - return Poll::Ready(ToSwarm::GenerateEvent(completed)); - } - - // If there is a new swap to be started, send it to the connection handler - if let Some((peer, event)) = self.new_swaps.pop_front() { - return Poll::Ready(ToSwarm::NotifyHandler { - peer_id: peer, - handler: libp2p::swarm::NotifyHandler::Any, - event, - }); - } - - Poll::Pending - } -} - -type OutboundStream = BoxFuture<'static, Result>; - -pub struct Handler { - outbound_stream: OptionFuture, - env_config: env::Config, - timeout: Duration, - new_swaps: VecDeque, - bitcoin_wallet: Arc, - keep_alive: bool, -} - -impl Handler { - fn new(env_config: env::Config, bitcoin_wallet: Arc) -> Self { - Self { - env_config, - outbound_stream: OptionFuture::from(None), - timeout: Duration::from_secs(120), - new_swaps: VecDeque::default(), - bitcoin_wallet, - keep_alive: true, - } - } -} - -#[derive(Debug, Clone)] -pub struct NewSwap { - pub swap_id: Uuid, - pub btc: bitcoin::Amount, - pub tx_lock_fee: bitcoin::Amount, - pub tx_refund_fee: bitcoin::Amount, - pub tx_cancel_fee: bitcoin::Amount, - pub bitcoin_refund_address: bitcoin::Address, -} - -#[derive(Debug)] -pub struct Completed(Result); - -impl ConnectionHandler for Handler { - type FromBehaviour = NewSwap; - type ToBehaviour = Completed; - type InboundProtocol = upgrade::DeniedUpgrade; - type OutboundProtocol = protocol::SwapSetup; - type InboundOpenInfo = (); - type OutboundOpenInfo = NewSwap; - - fn listen_protocol(&self) -> SubstreamProtocol { - // Bob does not support inbound substreams - SubstreamProtocol::new(upgrade::DeniedUpgrade, ()) - } - - fn on_connection_event( - &mut self, - event: libp2p::swarm::handler::ConnectionEvent< - '_, - Self::InboundProtocol, - Self::OutboundProtocol, - Self::InboundOpenInfo, - Self::OutboundOpenInfo, - >, - ) { - match event { - libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedInbound(_) => { - unreachable!("Bob does not support inbound substreams") - } - libp2p::swarm::handler::ConnectionEvent::FullyNegotiatedOutbound(outbound) => { - let mut substream = outbound.protocol; - let new_swap_request = outbound.info; - - let bitcoin_wallet = self.bitcoin_wallet.clone(); - let env_config = self.env_config; - - let protocol = tokio::time::timeout(self.timeout, async move { - let result = async { - // Here we request the spot price from Alice - write_cbor_message( - &mut substream, - SpotPriceRequest { - btc: new_swap_request.btc, - blockchain_network: BlockchainNetwork { - bitcoin: env_config.bitcoin_network, - monero: env_config.monero_network, - }, - }, - ) - .await - .context("Failed to send spot price request to Alice")?; - - // Here we read the spot price response from Alice - // The outer ? checks if Alice responded with an error (SpotPriceError) - let xmr = Result::from( - // The inner ? is for the read_cbor_message function - // It will return an error if the deserialization fails - read_cbor_message::(&mut substream) - .await - .context("Failed to read spot price response from Alice")?, - )?; - - let state0 = State0::new( - new_swap_request.swap_id, - &mut rand::thread_rng(), - new_swap_request.btc, - xmr, - env_config.bitcoin_cancel_timelock.into(), - env_config.bitcoin_punish_timelock.into(), - new_swap_request.bitcoin_refund_address.clone(), - env_config.monero_finality_confirmations, - new_swap_request.tx_refund_fee, - new_swap_request.tx_cancel_fee, - new_swap_request.tx_lock_fee, - ); - - write_cbor_message(&mut substream, state0.next_message()) - .await - .context("Failed to send state0 message to Alice")?; - let message1 = read_cbor_message::(&mut substream) - .await - .context("Failed to read message1 from Alice")?; - let state1 = state0 - .receive(bitcoin_wallet.as_ref(), message1) - .await - .context("Failed to receive state1")?; - write_cbor_message(&mut substream, state1.next_message()) - .await - .context("Failed to send state1 message")?; - let message3 = read_cbor_message::(&mut substream) - .await - .context("Failed to read message3 from Alice")?; - let state2 = state1 - .receive(message3) - .context("Failed to receive state2")?; - - write_cbor_message(&mut substream, state2.next_message()) - .await - .context("Failed to send state2 message")?; - - substream - .flush() - .await - .context("Failed to flush substream")?; - substream - .close() - .await - .context("Failed to close substream")?; - - Ok(state2) - } - .await; - - result.map_err(|e: anyhow::Error| { - tracing::error!("Error occurred during swap setup protocol: {:#}", e); - Error::Other - }) - }); - - let max_seconds = self.timeout.as_secs(); - - self.outbound_stream = OptionFuture::from(Some(Box::pin(async move { - protocol.await.map_err(|_| Error::Timeout { - seconds: max_seconds, - })? - }) - as OutboundStream)); - - // Once the outbound stream is created, we keep the connection alive - self.keep_alive = true; - } - _ => {} - } - } - - fn on_behaviour_event(&mut self, new_swap: Self::FromBehaviour) { - self.new_swaps.push_back(new_swap); - } - - fn connection_keep_alive(&self) -> bool { - self.keep_alive - } - - fn poll( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> Poll< - ConnectionHandlerEvent, - > { - // Check if there is a new swap to be started - if let Some(new_swap) = self.new_swaps.pop_front() { - self.keep_alive = true; - - // We instruct the swarm to start a new outbound substream - return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { - protocol: SubstreamProtocol::new(protocol::new(), new_swap), - }); - } - - // Check if the outbound stream has completed - if let Poll::Ready(Some(result)) = self.outbound_stream.poll_unpin(cx) { - self.outbound_stream = None.into(); - - // Once the outbound stream is completed, we no longer keep the connection alive - self.keep_alive = false; - - // We notify the swarm that the swap setup is completed / failed - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Completed( - result.map_err(anyhow::Error::from), - ))); - } - - Poll::Pending - } -} - -impl From for Result { - fn from(response: SpotPriceResponse) -> Self { - match response { - SpotPriceResponse::Xmr(amount) => Ok(amount), - SpotPriceResponse::Error(e) => Err(e.into()), - } - } -} - -#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] -pub enum Error { - #[error("Seller currently does not accept incoming swap requests, please try again later")] - NoSwapsAccepted, - #[error("Seller refused to buy {buy} because the minimum configured buy limit is {min}")] - AmountBelowMinimum { - min: bitcoin::Amount, - buy: bitcoin::Amount, - }, - #[error("Seller refused to buy {buy} because the maximum configured buy limit is {max}")] - AmountAboveMaximum { - max: bitcoin::Amount, - buy: bitcoin::Amount, - }, - #[error("Seller's XMR balance is currently too low to fulfill the swap request to buy {buy}, please try again later")] - BalanceTooLow { buy: bitcoin::Amount }, - - #[error("Seller blockchain network {asb:?} setup did not match your blockchain network setup {cli:?}")] - BlockchainNetworkMismatch { - cli: BlockchainNetwork, - asb: BlockchainNetwork, - }, - - #[error("Failed to complete swap setup within {seconds}s")] - Timeout { seconds: u64 }, - - /// To be used for errors that cannot be explained on the CLI side (e.g. - /// rate update problems on the seller side) - #[error("Seller encountered a problem, please try again later.")] - Other, -} - -impl From for Error { - fn from(error: SpotPriceError) -> Self { - match error { - SpotPriceError::NoSwapsAccepted => Error::NoSwapsAccepted, - SpotPriceError::AmountBelowMinimum { min, buy } => { - Error::AmountBelowMinimum { min, buy } - } - SpotPriceError::AmountAboveMaximum { max, buy } => { - Error::AmountAboveMaximum { max, buy } - } - SpotPriceError::BalanceTooLow { buy } => Error::BalanceTooLow { buy }, - SpotPriceError::BlockchainNetworkMismatch { cli, asb } => { - Error::BlockchainNetworkMismatch { cli, asb } - } - SpotPriceError::Other => Error::Other, - } - } -} diff --git a/swap/src/network/swarm.rs b/swap/src/network/swarm.rs index 2e266f767c..439b155932 100644 --- a/swap/src/network/swarm.rs +++ b/swap/src/network/swarm.rs @@ -1,4 +1,4 @@ -use crate::asb::{LatestRate, RendezvousNode}; +use crate::asb::{register, LatestRate}; use crate::libp2p_ext::MultiAddrExt; use crate::network::rendezvous::XmrBtcNamespace; use crate::seed::Seed; @@ -15,6 +15,8 @@ use swap_core::bitcoin; use swap_env::env; use tor_rtcompat::tokio::TokioRustlsRuntime; +const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 2); // 2 hours + #[allow(clippy::too_many_arguments)] pub fn asb( seed: &Seed, @@ -41,7 +43,7 @@ where .extract_peer_id() .expect("Rendezvous node address must contain peer ID"); - RendezvousNode::new(addr, peer_id, namespace, None) + register::RendezvousNode::new(addr, peer_id, namespace, None) }) .collect(); @@ -66,7 +68,7 @@ where .with_tokio() .with_other_transport(|_| transport)? .with_behaviour(|_| behaviour)? - .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::MAX)) + .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT)) .build(); Ok((swarm, onion_addresses)) @@ -86,7 +88,7 @@ where .with_tokio() .with_other_transport(|_| transport)? .with_behaviour(|_| behaviour)? - .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::MAX)) + .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT)) .build(); Ok(swarm) diff --git a/swap/src/network/transport.rs b/swap/src/network/transport.rs index 7dddfc3463..0b74b22448 100644 --- a/swap/src/network/transport.rs +++ b/swap/src/network/transport.rs @@ -7,6 +7,8 @@ use libp2p::noise; use libp2p::{identity, yamux, PeerId, Transport}; use std::time::Duration; +const AUTH_AND_MULTIPLEX_TIMEOUT: Duration = Duration::from_secs(60); + /// "Completes" a transport by applying the authentication and multiplexing /// upgrades. /// @@ -27,7 +29,7 @@ where .upgrade(Version::V1) .authenticate(auth_upgrade) .multiplex(multiplex_upgrade) - .timeout(Duration::from_secs(20)) + .timeout(AUTH_AND_MULTIPLEX_TIMEOUT) .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) .boxed(); diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index b0f827082d..43adebdb3e 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -164,12 +164,12 @@ where .context("Failed to get Monero wallet block height") .map_err(backoff::Error::transient)?; - let (address, amount) = state3 + let (lock_address, amount) = state3 .lock_xmr_transfer_request() .address_and_amount(env_config.monero_network); let destinations = - build_transfer_destinations(address, amount, developer_tip.clone())?; + build_transfer_destinations(lock_address, amount, developer_tip.clone())?; // Lock the Monero let receipt = monero_wallet @@ -186,12 +186,13 @@ where ))); }; + let tx_key = receipt.tx_keys.get(&lock_address).expect("monero-sys guarantees that the address has a valid tx key or the tx isn't published"); + Ok(Some(( monero_wallet_restore_blockheight, TransferProof::new( monero::TxHash(receipt.txid), - monero::PrivateKey::from_str(&receipt.tx_key) - .expect("tx key to be valid private key"), + *tx_key, ), ))) }, @@ -365,11 +366,21 @@ where state3, } }, - // TODO: We should already listen for the encrypted signature here. - // // If we send Bob the transfer proof, but for whatever reason we do not receive an acknoledgement from him - // we would be stuck in this state forever until the timelock expires. By listening for the encrypted signature here we - // can still proceed to the next state even if Bob does not respond with an acknoledgement. + // we would be stuck in this state forever until the timelock expires. + // + // By listening for the encrypted signature here we can still proceed to the next state + // even if Bob does not respond with an acknoledgement but sends us the encrypted signature immediately. + enc_sig = event_loop_handle.recv_encrypted_signature() => { + tracing::info!("Received encrypted signature"); + + AliceState::EncSigLearned { + monero_wallet_restore_blockheight, + transfer_proof, + encrypted_signature: Box::new(enc_sig?), + state3, + } + } result = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => { result?; AliceState::CancelTimelockExpired { @@ -400,20 +411,6 @@ where } } enc_sig = event_loop_handle.recv_encrypted_signature() => { - // Fetch the status as early as possible to update the internal cache of our Electurm client - // Prevents redundant network requests later on when we redeem the Bitcoin - let tx_lock_status = bitcoin_wallet.status_of_script(&state3.tx_lock.clone()).await?; - - if tx_lock_status.is_confirmed_with(state3.cancel_timelock.half()) { - tx_lock_status_subscription.wait_until_confirmed_with(state3.cancel_timelock).await?; - - return Ok(AliceState::CancelTimelockExpired { - monero_wallet_restore_blockheight, - transfer_proof, - state3, - }) - } - tracing::info!("Received encrypted signature"); AliceState::EncSigLearned { @@ -431,25 +428,15 @@ where encrypted_signature, state3, } => { - // Try to sign the redeem transaction, otherwise wait for the cancel timelock to expire + // Try to sign the Bitcoin redeem transactions let tx_redeem = match state3.signed_redeem_transaction(*encrypted_signature) { Ok(tx_redeem) => tx_redeem, + // If we cannot sign the transaction there must be something wrong + // We just wait for the cancel timelock to expire and then refund Err(error) => { - tracing::error!("Failed to construct redeem transaction: {:#}", error); - tracing::info!( - timelock = %state3.cancel_timelock, - "Waiting for cancellation timelock to expire", - ); - - let tx_lock_status = bitcoin_wallet - .subscribe_to(Box::new(state3.tx_lock.clone())) - .await; - - tx_lock_status - .wait_until_confirmed_with(state3.cancel_timelock) - .await?; + tracing::error!("Failed to construct redeem transaction: {:#}, we will wait for the cancel timelock expiration to refund", error); - return Ok(AliceState::CancelTimelockExpired { + return Ok(AliceState::WaitingForCancelTimelockExpiration { monero_wallet_restore_blockheight, transfer_proof, state3, @@ -465,11 +452,25 @@ where .build(); match backoff::future::retry_notify(backoff.clone(), || async { - // If the cancel timelock is expired, there is no need to try to publish the redeem transaction anymore - if !matches!( - state3.expired_timelocks(&*bitcoin_wallet).await?, - ExpiredTimelocks::None { .. } - ) { + let tx_lock_status = bitcoin_wallet + .status_of_script(&state3.tx_lock.clone()) + .await?; + + // If the cancel timelock is expired, it it not safe to publish the Bitcoin redeem transaction anymore + // + // TODO: In practice this should be redundant because the logic above will trigger for a superset of the cases where this is true + if tx_lock_status.is_confirmed_with(state3.cancel_timelock) { + return Ok(None); + } + + // We can only redeem the Bitcoin if we are fairly sure that our Bitcoin redeem transaction + // will be confirmed before the cancel timelock expires + // + // We make an assumption that it will take at most `env_config.bitcoin_blocks_till_confirmed_upper_bound_assumption` blocks + // until our transaction is included in a block. If this assumption is not satisfied, we will not publish the transaction. + // + // We will instead wait for the cancel timelock to expire and then refund. + if tx_lock_status.blocks_left_until(state3.cancel_timelock) < env_config.bitcoin_blocks_till_confirmed_upper_bound_assumption { return Ok(None); } @@ -493,6 +494,7 @@ where // We wait until we see the transaction in the mempool before transitioning to the next state Some((txid, subscription)) => match subscription.wait_until_seen().await { Ok(_) => AliceState::BtcRedeemTransactionPublished { state3, transfer_proof }, + // TODO: No need to bail here, we should just retry? Err(e) => { // We extract the txid and the hex representation of the transaction // this'll allow the user to manually re-publish the transaction @@ -502,11 +504,12 @@ where } }, - // Cancel timelock expired before we could publish the redeem transaction + // It is not safe to publish the Bitcoin redeem transaction anymore + // We wait for the cancel timelock to expire and then refund None => { - tracing::error!("We were unable to publish the redeem transaction before the timelock expired."); + tracing::error!("We were unable to publish the Bitcoin redeem transaction before the timelock expired."); - AliceState::CancelTimelockExpired { + AliceState::WaitingForCancelTimelockExpiration { monero_wallet_restore_blockheight, transfer_proof, state3, @@ -526,32 +529,69 @@ where } } } + AliceState::WaitingForCancelTimelockExpiration { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } => { + let tx_lock_status_subscription = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_lock.clone())) + .await; + + // TODO: Retry here + tx_lock_status_subscription + .wait_until_confirmed_with(state3.cancel_timelock) + .await?; + + AliceState::CancelTimelockExpired { + monero_wallet_restore_blockheight, + transfer_proof, + state3, + } + } AliceState::CancelTimelockExpired { monero_wallet_restore_blockheight, transfer_proof, state3, } => { - if state3 - .check_for_tx_cancel(&*bitcoin_wallet) - .await? - .is_none() - { - // If Bob hasn't yet broadcasted the cancel transaction, Alice has to publish it - // to be able to eventually punish. Since the punish timelock is - // relative to the publication of the cancel transaction we have to ensure it - // gets published once the cancel timelock expires. + let backoff = backoff::ExponentialBackoffBuilder::new() + .with_max_elapsed_time(None) + // No need to be super agressive here + .with_max_interval(Duration::from_secs(60 * 10)) + .build(); + + backoff::future::retry_notify::<_, anyhow::Error, _, _, _, _>( + backoff, + || async { + if state3 + .check_for_tx_cancel(&*bitcoin_wallet) + .await + .context("Failed to check for existence of Bitcoin cancel transaction on chain") + .map_err(backoff::Error::transient)? + .is_some() + { + return Ok(()); + } - if let Err(e) = state3.submit_tx_cancel(&*bitcoin_wallet).await { - // TODO: Actually ensure the transaction is published - // What about a wrapper function ensure_tx_published that repeats the tx submission until - // our subscription sees it in the mempool? + state3 + .submit_tx_cancel(&*bitcoin_wallet) + .await + .context("Failed to submit cancel transaction") + .map_err(backoff::Error::transient)?; - tracing::debug!( - "Assuming cancel transaction is already broadcasted because we failed to publish: {:#}", - e + Ok(()) + }, + |e: anyhow::Error, wait_time: Duration| { + tracing::warn!( + swap_id = %swap_id, + error = ?e, + "Failed to ensure cancel transaction is published. We will retry in {} seconds", + wait_time.as_secs() ) - } - } + }, + ) + .await + .expect("We should never run out of retries while ensuring the cancel transaction is published"); AliceState::BtcCancelled { monero_wallet_restore_blockheight, @@ -622,43 +662,35 @@ where transfer_proof, state3, } => { - // TODO: We should retry indefinitely here until we find the refund transaction - // TODO: If we crash while we are waiting for the punish_tx to be confirmed (punish_btc waits until confirmation), we will remain in this state forever because we will attempt to re-publish the punish transaction - let punish = state3.punish_btc(&*bitcoin_wallet).await; - - match punish { - Ok(_) => AliceState::BtcPunished { - state3, - transfer_proof, - }, - Err(error) => { - tracing::warn!("Failed to publish punish transaction: {:#}", error); - - // Upon punish failure we assume that the refund tx was included but we - // missed seeing it. In case we fail to fetch the refund tx we fail - // with no state update because it is unclear what state we should transition - // to. It does not help to race punish and refund inclusion, - // because a punish tx failure is not recoverable (besides re-trying) if the - // refund tx was not included. - - tracing::info!("Falling back to refund"); + retry( + "Punish Bitcoin", + || async { + // Before punishing, we explicitly check for the refund transaction as we prefer refunds over punishments + let spend_key_from_btc_refund = state3.refund_btc(&*bitcoin_wallet).await.context("Failed to check for existence of Bitcoin refund transaction before punishing").map_err(backoff::Error::transient)?; - let published_refund_tx = bitcoin_wallet - .get_raw_transaction(state3.tx_refund().txid()) - .await - .context("Failed to fetch refund transaction after assuming it was included because the punish transaction failed")? - .context("Bitcoin refund transaction not found")?; + // If we find the Bitcoin refund transaction, we go ahead and refund the Monero + if let Some(spend_key_from_btc_refund) = spend_key_from_btc_refund { + return Ok::>(AliceState::BtcRefunded { + monero_wallet_restore_blockheight, + transfer_proof: transfer_proof.clone(), + spend_key: spend_key_from_btc_refund, + state3: state3.clone(), + }); + } - let spend_key = state3.extract_monero_private_key(published_refund_tx)?; + state3.punish_btc(&*bitcoin_wallet).await.context("Failed to construct and publish Bitcoin punish transaction").map_err(backoff::Error::transient)?; - AliceState::BtcRefunded { - monero_wallet_restore_blockheight, - transfer_proof, - spend_key, - state3, - } - } - } + Ok::>(AliceState::BtcPunished { + state3: state3.clone(), + transfer_proof: transfer_proof.clone(), + }) + }, + None, + // We can take our time when punishing + Duration::from_secs(60 * 5), + ) + .await + .expect("We should never run out of retries while publishing the punish transaction") } AliceState::XmrRefunded => AliceState::XmrRefunded, AliceState::BtcRedeemed => AliceState::BtcRedeemed, diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index d19abaca15..5a6ba065e4 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -20,7 +20,7 @@ pub mod swap; pub struct Swap { pub state: BobState, - pub event_loop_handle: cli::EventLoopHandle, + pub event_loop_handle: cli::SwapEventLoopHandle, pub db: Arc, pub bitcoin_wallet: Arc, pub monero_wallet: Arc, @@ -38,7 +38,7 @@ impl Swap { bitcoin_wallet: Arc, monero_wallet: Arc, env_config: env::Config, - event_loop_handle: cli::EventLoopHandle, + event_loop_handle: cli::SwapEventLoopHandle, monero_receive_pool: MoneroAddressPool, bitcoin_change_address: bitcoin::Address, btc_amount: bitcoin::Amount, @@ -68,7 +68,7 @@ impl Swap { bitcoin_wallet: Arc, monero_wallet: Arc, env_config: env::Config, - event_loop_handle: cli::EventLoopHandle, + event_loop_handle: cli::SwapEventLoopHandle, monero_receive_pool: MoneroAddressPool, ) -> Result { let state = db.get_state(id).await?.try_into()?; diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index e3278c9a13..46c26314f4 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,6 +1,6 @@ use crate::cli::api::tauri_bindings::LockBitcoinDetails; use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent}; -use crate::cli::EventLoopHandle; +use crate::cli::SwapEventLoopHandle; use crate::common::retry; use crate::monero; use crate::monero::MoneroAddressPool; @@ -8,7 +8,7 @@ use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, use crate::network::swap_setup::bob::NewSwap; use crate::protocol::bob::*; use crate::protocol::{bob, Database}; -use anyhow::{bail, Context as AnyContext, Result}; +use anyhow::{Context as AnyContext, Result}; use std::sync::Arc; use std::time::Duration; use swap_core::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; @@ -90,7 +90,7 @@ pub async fn run_until( async fn next_state( swap_id: Uuid, state: BobState, - event_loop_handle: &mut EventLoopHandle, + event_loop_handle: &mut SwapEventLoopHandle, db: Arc, bitcoin_wallet: Arc, monero_wallet: Arc, @@ -480,19 +480,52 @@ async fn next_state( BobState::XmrLocked(state) => { event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::XmrLocked); - // In case we send the encrypted signature to Alice, but she doesn't give us a confirmation - // We need to check if she still published the Bitcoin redeem transaction - // Otherwise we risk staying stuck in "XmrLocked" - if let Some(state5) = state.check_for_tx_redeem(&*bitcoin_wallet).await? { + let bitcoin_wallet_for_retry = bitcoin_wallet.clone(); + + let (redeem_state, expired_timelocks) = retry( + "Checking Bitcoin redeem transaction and cancel timelock status before sending encrypted signature", + || { + let bitcoin_wallet = bitcoin_wallet_for_retry.clone(); + let state_for_attempt = state.clone(); + + async move { + // In case we send the encrypted signature to Alice, but she doesn't give us a confirmation + // We need to check if she still published the Bitcoin redeem transaction + // Otherwise we risk staying stuck in "XmrLocked" + let redeem_state = state_for_attempt + .check_for_tx_redeem(&*bitcoin_wallet) + .await + .context("Failed to check for existence of tx_redeem before sending encrypted signature") + .map_err(backoff::Error::transient)?; + + // We do not want to race tx_refund against tx_redeem + // we therefore never send the encrypted signature if the cancel timelock has expired + let expired_timelocks = state_for_attempt + .expired_timelock(&*bitcoin_wallet) + .await + .context("Failed to check for expired timelocks before sending encrypted signature") + .map_err(backoff::Error::transient)?; + + Ok::<_, backoff::Error>(( + redeem_state, + expired_timelocks, + )) + } + }, + None, + None, + ) + .await?; + + // It is important that we check for tx_redeem BEFORE checking for the timelock + // because do not want to race tx_refund against tx_redeem and we prefer + // successful redeem over a refund (obviously) + if let Some(state5) = redeem_state { return Ok(BobState::BtcRedeemed(state5)); } // Check whether we can cancel the swap and do so if possible. - if state - .expired_timelock(&*bitcoin_wallet) - .await? - .cancel_timelock_expired() - { + if expired_timelocks.cancel_timelock_expired() { return Ok(BobState::CancelTimelockExpired(state.cancel())); } @@ -508,14 +541,8 @@ async fn next_state( // Bob sends Alice the encrypted signature which allows her to sign and broadcast the Bitcoin redeem transaction select! { // Wait for the confirmation from Alice that she has received the encrypted signature - result = event_loop_handle.send_encrypted_signature(state.tx_redeem_encsig()) => { - match result { - Ok(_) => BobState::EncSigSent(state), - Err(err) => { - tracing::error!(%err, "Failed to send encrypted signature to Alice"); - bail!("Failed to send encrypted signature to Alice"); - } - } + _ = event_loop_handle.send_encrypted_signature(state.tx_redeem_encsig()) => { + BobState::EncSigSent(state) }, // Wait for the cancel timelock to expire result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => { @@ -536,18 +563,51 @@ async fn next_state( event_emitter .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::EncryptedSignatureSent); - // We need to make sure that Alice did not publish the redeem transaction while we were offline - // Even if the cancel timelock expired, if Alice published the redeem transaction while we were away we cannot miss it - // If we do we cannot refund and will never be able to leave the "CancelTimelockExpired" state - if let Some(state5) = state.check_for_tx_redeem(&*bitcoin_wallet).await? { + let bitcoin_wallet_for_retry = bitcoin_wallet.clone(); + + let (redeem_state, expired_timelocks) = retry( + "Checking Bitcoin redeem transaction and cancel timelock status after sending encrypted signature", + || { + let bitcoin_wallet = bitcoin_wallet_for_retry.clone(); + let state_for_attempt = state.clone(); + + async move { + // We need to make sure that Alice did not publish the redeem transaction while we were offline + // Even if the cancel timelock expired, if Alice published the redeem transaction while we were away we cannot miss it + // If we do we cannot refund and will never be able to leave the "CancelTimelockExpired" state + let redeem_state = state_for_attempt + .check_for_tx_redeem(&*bitcoin_wallet) + .await + .context("Failed to check for existence of tx_redeem after sending encrypted signature") + .map_err(backoff::Error::transient)?; + + // Then, check timelock status + let expired_timelocks = state_for_attempt + .expired_timelock(&*bitcoin_wallet) + .await + .context("Failed to check for expired timelocks after sending encrypted signature") + .map_err(backoff::Error::transient)?; + + Ok::<_, backoff::Error>(( + redeem_state, + expired_timelocks, + )) + } + }, + None, + None, + ) + .await?; + + // It is important that we check for tx_redeem BEFORE checking for the timelock + // because we do not want to race tx_refund against tx_redeem and we prefer + // successful redeem over a refund + if let Some(state5) = redeem_state { return Ok(BobState::BtcRedeemed(state5)); } - if state - .expired_timelock(&*bitcoin_wallet) - .await? - .cancel_timelock_expired() - { + // Check if the cancel timelock has expired AFTER checking for tx_redeem + if expired_timelocks.cancel_timelock_expired() { return Ok(BobState::CancelTimelockExpired(state.cancel())); } @@ -644,33 +704,52 @@ async fn next_state( event_emitter .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::CancelTimelockExpired); - if state6 - .check_for_tx_cancel(&*bitcoin_wallet) - .await? - .is_none() - { - tracing::debug!("Couldn't find tx_cancel yet, publishing ourselves"); - - if let Err(tx_cancel_err) = state6.submit_tx_cancel(&*bitcoin_wallet).await { - tracing::warn!(err = %tx_cancel_err, "Failed to publish tx_cancel even though it is not present in the chain. Did Alice already refund us our Bitcoin early?"); + let bitcoin_wallet_for_retry = bitcoin_wallet.clone(); + let state6_for_retry = state6.clone(); + retry( + "Check for tx_redeem, tx_early_refund and tx_cancel then publish tx_cancel if necessary", + || { + let bitcoin_wallet = bitcoin_wallet_for_retry.clone(); + let state6 = state6_for_retry.clone(); + async move { + + // TODO: Uncomment this once we have the required data in State6 + // First we check if tx_redeem is present on the chain + // + // We may have sent the enc sig close to the timelock expiration, + // never received the confirmation and now the cancel timelock has expired. + // + // Alice may still have received the enc sig even if we are in this state + // if state6.check_for_tx_redeem(&*bitcoin_wallet).await.map_err(backoff::Error::transient)?.is_some() { + // return Ok(BobState::BtcRedeemed(state6)); + // } + + // TODO: Do these in parallel to speed up + + // Check if tx_early_refund is present on the chain, if it is then there + if state6.check_for_tx_early_refund(&*bitcoin_wallet).await.context("Failed to check for existence of tx_early_refund before cancelling").map_err(backoff::Error::transient)?.is_some() { + return Ok(BobState::BtcEarlyRefundPublished(state6.clone())); + } - // If tx_cancel is not present in the chain and we fail to publish it. There's only one logical conclusion: - // The tx_lock UTXO has been spent by the tx_early_refund transaction - // Therefore we check for the early refund transaction - match state6.check_for_tx_early_refund(&*bitcoin_wallet).await? { - Some(_) => { - return Ok(BobState::BtcEarlyRefundPublished(state6)); - } - None => { - bail!("Failed to publish tx_cancel even though it is not present. We also did not find tx_early_refund in the chain. This is unexpected. Could be an issue with the Electrum server? tx_cancel_err: {:?}", tx_cancel_err); - } + // Then we check if tx_cancel is present on the chain + if state6.check_for_tx_cancel(&*bitcoin_wallet).await.context("Failed to check for existence of tx_cancel before cancelling").map_err(backoff::Error::transient)?.is_some() { + return Ok(BobState::BtcCancelled(state6.clone())); } - } - } - BobState::BtcCancelled(state6) + // If none of the above are present, we publish tx_cancel + state6.submit_tx_cancel(&*bitcoin_wallet).await.context("Failed to submit tx_cancel after ensuring both tx_early_refund and tx_cancel are not present").map_err(backoff::Error::transient)?; + + Ok(BobState::BtcCancelled(state6)) + } + }, + None, + None, + ) + .await + .expect("we never stop retrying to check for tx_redeem, tx_early_refund and tx_cancel then publishing tx_cancel if necessary") } BobState::BtcCancelled(state) => { + // TODO: We should differentiate between BtcCancelPublished and BtcCancelled (confirmed) let btc_cancel_txid = state.construct_tx_cancel()?.txid(); event_emitter.emit_swap_progress_event( @@ -678,25 +757,42 @@ async fn next_state( TauriSwapProgressEvent::BtcCancelled { btc_cancel_txid }, ); - // Bob has cancelled the swap - match state.expired_timelock(&*bitcoin_wallet).await? { - ExpiredTimelocks::None { .. } => { - bail!( - "Internal error: canceled state reached before cancel timelock was expired" - ); - } - ExpiredTimelocks::Cancel { .. } => { - let btc_refund_txid = state.publish_refund_btc(&*bitcoin_wallet).await?; + let bitcoin_wallet_for_retry = bitcoin_wallet.clone(); + let state_for_retry = state.clone(); + retry( + "Check timelocks and try to refund", + || { + let bitcoin_wallet = bitcoin_wallet_for_retry.clone(); + let state = state_for_retry.clone(); + async move { + match state.expired_timelock(&*bitcoin_wallet).await.map_err(backoff::Error::transient)? { + ExpiredTimelocks::None { .. } => { + Err(backoff::Error::Permanent(anyhow::anyhow!( + "Internal error: canceled state reached before cancel timelock was expired" + ))) + } + ExpiredTimelocks::Cancel { .. } => { + let btc_refund_txid = state.publish_refund_btc(&*bitcoin_wallet).await.context("Failed to publish refund transaction after ensuring cancel timelock has expired and refund timelock has not expired").map_err(backoff::Error::transient)?; - tracing::info!(%btc_refund_txid, "Refunded our Bitcoin"); + tracing::info!(%btc_refund_txid, "Refunded our Bitcoin"); - BobState::BtcRefundPublished(state) - } - ExpiredTimelocks::Punish => BobState::BtcPunished { - tx_lock_id: state.tx_lock_id(), - state, + Ok(BobState::BtcRefundPublished(state.clone())) + } + ExpiredTimelocks::Punish => { + let tx_lock_id = state.tx_lock_id(); + Ok(BobState::BtcPunished { + tx_lock_id, + state, + }) + } + } + } }, - } + None, + None, + ) + .await + .expect("we never stop retrying to refund") } BobState::BtcRefundPublished(state) => { // Emit a Tauri event @@ -725,6 +821,7 @@ async fn next_state( // BtcRefunded state with the txid of the confirmed transaction select! { // Wait for the refund transaction to be confirmed + // TODO: Publish the tx_refund transaction anyway _ = tx_refund_status.wait_until_final() => { let tx_refund_txid = tx_refund.txid(); diff --git a/swap/tests/happy_path_alice_developer_tip.rs b/swap/tests/happy_path_alice_developer_tip.rs index 4009cd6741..eba9df567e 100644 --- a/swap/tests/happy_path_alice_developer_tip.rs +++ b/swap/tests/happy_path_alice_developer_tip.rs @@ -10,7 +10,7 @@ use tokio::join; async fn happy_path_alice_developer_tip() { harness::setup_test( SlowCancelConfig, - Some(Decimal::from_f32_retain(0.1).unwrap()), + Some((Decimal::from_f32_retain(0.1).unwrap(), false)), |mut ctx| async move { let (bob_swap, _) = ctx.bob_swap().await; let bob_swap = tokio::spawn(bob::run(bob_swap)); diff --git a/swap/tests/happy_path_alice_developer_tip_subaddress.rs b/swap/tests/happy_path_alice_developer_tip_subaddress.rs new file mode 100644 index 0000000000..df34d3c6d5 --- /dev/null +++ b/swap/tests/happy_path_alice_developer_tip_subaddress.rs @@ -0,0 +1,31 @@ +pub mod harness; + +use harness::SlowCancelConfig; +use rust_decimal::Decimal; +use swap::asb::FixedRate; +use swap::protocol::{alice, bob}; +use tokio::join; + +#[tokio::test] +async fn happy_path_alice_developer_tip_subaddress() { + harness::setup_test( + SlowCancelConfig, + Some((Decimal::from_f32_retain(0.1).unwrap(), true)), + |mut ctx| async move { + let (bob_swap, _) = ctx.bob_swap().await; + let bob_swap = tokio::spawn(bob::run(bob_swap)); + + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run(alice_swap, FixedRate::default())); + + let (bob_state, alice_state) = join!(bob_swap, alice_swap); + + ctx.assert_alice_redeemed(alice_state??).await; + ctx.assert_bob_redeemed(bob_state??).await; + ctx.assert_alice_developer_tip_received().await; + + Ok(()) + }, + ) + .await; +} diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index a45262b81a..dd387e9f19 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -44,8 +44,15 @@ use tokio::time::{interval, timeout}; use url::Url; use uuid::Uuid; -pub async fn setup_test(_config: C, developer_tip_ratio: Option, testfn: T) -where +/// developer_tip_ratio is a tuple of (ratio, use_subaddress) +/// +/// If use_subaddress is true, we will use a subaddress for the developer tip. We do this +/// because using a subaddress changes things about the tx keys involved +pub async fn setup_test( + _config: C, + developer_tip_ratio: Option<(Decimal, bool)>, + testfn: T, +) where T: Fn(TestContext) -> F, F: Future>, C: GetConfig, @@ -57,7 +64,7 @@ where let cli = Cli::default(); tracing_subscriber::fmt() - .with_env_filter("info,swap=debug,monero_harness=debug,monero_rpc=debug,bitcoin_harness=info,testcontainers=info,monero_cpp=info,monero_sys=debug") // add `reqwest::connect::verbose=trace` if you want to logs of the RPC clients + .with_env_filter("info,swap=trace,swap_p2p=trace,monero_harness=debug,monero_rpc=debug,bitcoin_harness=info,testcontainers=info,monero_cpp=info,monero_sys=debug") // add `reqwest::connect::verbose=trace` if you want to logs of the RPC clients .with_test_writer() .init(); @@ -77,6 +84,7 @@ where .unwrap() .path() .join("developer_tip-monero-wallets"); + let (_, developer_tip_monero_wallet) = init_test_wallets( "developer_tip", containers.bitcoind_url.clone(), @@ -89,16 +97,29 @@ where env_config, ) .await; - let developer_tip_monero_wallet_address = developer_tip_monero_wallet + + let developer_tip_monero_wallet_main_address = developer_tip_monero_wallet .main_wallet() .await .main_address() .await .into(); + let developer_tip_monero_wallet_subaddress = developer_tip_monero_wallet + .main_wallet() + .await + // explicitly use a suabddress here to test the addtional tx key logic + .address(0, 2) + .await + .into(); + let developer_tip = TipConfig { - ratio: developer_tip_ratio.unwrap_or(Decimal::ZERO), - address: developer_tip_monero_wallet_address, + ratio: developer_tip_ratio.unwrap_or((Decimal::ZERO, false)).0, + address: match developer_tip_ratio { + Some((_, true)) => developer_tip_monero_wallet_subaddress, + Some((_, false)) => developer_tip_monero_wallet_main_address, + None => developer_tip_monero_wallet_main_address, + }, }; let alice_starting_balances = @@ -525,7 +546,9 @@ impl BobParams { } let db = Arc::new(SqliteDatabase::open(&self.db_path, AccessMode::ReadWrite).await?); - let (event_loop, handle) = self.new_eventloop(swap_id, db.clone()).await?; + let (event_loop, mut handle) = self.new_eventloop(db.clone()).await?; + + let swap_handle = handle.swap_handle(self.alice_peer_id, swap_id).await?; let swap = bob::Swap::from_db( db.clone(), @@ -533,7 +556,7 @@ impl BobParams { self.bitcoin_wallet.clone(), self.monero_wallet.clone(), self.env_config, - handle, + swap_handle, self.monero_wallet .main_wallet() .await @@ -560,17 +583,19 @@ impl BobParams { } let db = Arc::new(SqliteDatabase::open(&self.db_path, AccessMode::ReadWrite).await?); - let (event_loop, handle) = self.new_eventloop(swap_id, db.clone()).await?; + let (event_loop, mut handle) = self.new_eventloop(db.clone()).await?; db.insert_peer_id(swap_id, self.alice_peer_id).await?; + let swap_handle = handle.swap_handle(self.alice_peer_id, swap_id).await?; + let swap = bob::Swap::new( db, swap_id, self.bitcoin_wallet.clone(), self.monero_wallet.clone(), self.env_config, - handle, + swap_handle, self.monero_wallet .main_wallet() .await @@ -587,13 +612,11 @@ impl BobParams { pub async fn new_eventloop( &self, - swap_id: Uuid, db: Arc, ) -> Result<(cli::EventLoop, cli::EventLoopHandle)> { let identity = self.seed.derive_libp2p_identity(); let behaviour = cli::Behaviour::new( - self.alice_peer_id, self.env_config, self.bitcoin_wallet.clone(), (identity.clone(), XmrBtcNamespace::Testnet), @@ -601,7 +624,7 @@ impl BobParams { let mut swarm = swarm::cli(identity.clone(), None, behaviour).await?; swarm.add_peer_address(self.alice_peer_id, self.alice_address.clone()); - cli::EventLoop::new(swap_id, swarm, self.alice_peer_id, db.clone()) + cli::EventLoop::new(swarm, db.clone()) } } diff --git a/throttle/src/throttle.rs b/throttle/src/throttle.rs index 45f6351a93..26171281c3 100644 --- a/throttle/src/throttle.rs +++ b/throttle/src/throttle.rs @@ -9,7 +9,7 @@ use std::time::{self, Duration}; /// while ensuring the most recent argument is eventually processed. pub fn throttle(closure: F, delay: Duration) -> Throttle where - F: Fn(T) -> () + Send + Sync + 'static, + F: Fn(T) + Send + Sync + 'static, T: Send + Sync + 'static, { let (sender, receiver) = mpsc::channel(); @@ -22,7 +22,8 @@ where })); let dup_throttle_config = throttle_config.clone(); - let throttle = Throttle { + + Throttle { sender: Some(sender), thread: Some(std::thread::spawn(move || { let throttle_config = dup_throttle_config; @@ -58,7 +59,7 @@ where } } else { // There is pending work; wait for either a timeout or a new message. - let message = receiver.recv_timeout((*throttle_config.lock().unwrap()).delay); + let message = receiver.recv_timeout(throttle_config.lock().unwrap().delay); let now = time::Instant::now(); match message { Ok(param) => { @@ -98,12 +99,11 @@ where } })), throttle_config, - }; - throttle + } } struct ThrottleConfig { - closure: Pin () + Send + Sync + 'static>>, + closure: Pin>, delay: Duration, } impl Drop for ThrottleConfig { diff --git a/utils/gpg_keys/einliterflasche.asc b/utils/gpg_keys/einliterflasche.asc new file mode 100644 index 0000000000..026f1c98c9 --- /dev/null +++ b/utils/gpg_keys/einliterflasche.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaQtSYRYJKwYBBAHaRw8BAQdAwb3Kby98GpsM6wt7m2Wxx/EGrs/FOuS2++48 +aHlqXMy0D2VpbmxpdGVyZmxhc2NoZYiTBBMWCgA7FiEEvEGkAG7bUe3mONS0AKvI +CIZr8m8FAmkLUmECGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQAKvI +CIZr8m9JWgD8CZZzeP1S4pktWMPjxVI2ZA3bWMJ5PwaldKQWwJMGGOIA/A6v+Mmn +h+Uv4dR8mhrP8Y2+4ne0zpQx8zpz0dJFK8QJuDgEaQtSYRIKKwYBBAGXVQEFAQEH +QH7Fe09RQuM194oEiMLdcKV/Zpo0vcrO1/e4O3uvh5pQAwEIB4h4BBgWCgAgFiEE +vEGkAG7bUe3mONS0AKvICIZr8m8FAmkLUmECGwwACgkQAKvICIZr8m9h/wEAuKf6 +yvoYxzBufeZBExusC+BBpMSgN+BRMxb0mvfZitkBAOFzgzrVi3EyxFBUITniPI34 +hv8vURdB6gIhHbpcYA0D +=aK60 +-----END PGP PUBLIC KEY BLOCK----- + From f8a887f66412f777b7363cc34ec33e4bf2a709bb Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 26 Nov 2025 13:17:37 +0100 Subject: [PATCH 14/16] add controller command wrapper, reimplement command confirmation prompt --- swap-orchestrator/src/command.rs | 3 ++ swap-orchestrator/src/command/build.rs | 14 ++++---- swap-orchestrator/src/command/start.rs | 11 +++--- swap-orchestrator/src/docker/compose.rs | 46 +++++++++++++++++++++++++ swap-orchestrator/src/main.rs | 1 + swap-orchestrator/src/util.rs | 21 ++--------- 6 files changed, 64 insertions(+), 32 deletions(-) diff --git a/swap-orchestrator/src/command.rs b/swap-orchestrator/src/command.rs index 8a5001234d..7f62c4556e 100644 --- a/swap-orchestrator/src/command.rs +++ b/swap-orchestrator/src/command.rs @@ -1,10 +1,12 @@ mod build; +mod controller; mod export; mod init; pub mod prompt; mod start; pub use build::build; +pub use controller::controller; pub use export::export; pub use init::init; pub use start::start; @@ -30,4 +32,5 @@ pub enum Command { Start, Build, Export, + Controller } diff --git a/swap-orchestrator/src/command/build.rs b/swap-orchestrator/src/command/build.rs index c85535c49a..2b014d3441 100644 --- a/swap-orchestrator/src/command/build.rs +++ b/swap-orchestrator/src/command/build.rs @@ -1,20 +1,20 @@ -use crate::util::CommandExt; use crate::{command, flag}; pub async fn build() -> anyhow::Result<()> { println!("Pulling the latest Docker images..."); - let mut command = command!("docker", flag!("compose"), flag!("pull")).to_tokio_command()?; - command.exec_piped().await?; + command!("docker", flag!("compose"), flag!("pull")) + .exec_piped(false) + .await?; - println!("Building the Docker images... (this might take a while)"); - let mut command = command!( + println!("Building the Docker images... (this might take a while - up to a few hours depending on your machine)"); + command!( "docker", flag!("compose"), flag!("build"), flag!("--no-cache") ) - .to_tokio_command()?; - command.exec_piped().await?; + .exec_piped(true) + .await?; println!("Done!"); diff --git a/swap-orchestrator/src/command/start.rs b/swap-orchestrator/src/command/start.rs index 8a55a41736..bef3c57bdc 100644 --- a/swap-orchestrator/src/command/start.rs +++ b/swap-orchestrator/src/command/start.rs @@ -2,7 +2,7 @@ use anyhow::bail; use crate::{ command, flag, - util::{CommandExt, probe_docker, probe_maker_config}, + util::{probe_docker, probe_maker_config}, }; pub async fn start() -> anyhow::Result<()> { @@ -12,9 +12,8 @@ pub async fn start() -> anyhow::Result<()> { probe_docker().await?; - let mut command = command!("docker", flag!("compose"), flag!("up"), flag!("-d")) - .to_tokio_command() - .expect("non-empty command"); - - command.exec_piped().await.map(|_| ()) + command!("docker", flag!("compose"), flag!("up"), flag!("-d")) + .exec_piped(true) + .await + .map(|_| ()) } diff --git a/swap-orchestrator/src/docker/compose.rs b/swap-orchestrator/src/docker/compose.rs index 525c1e4858..8741313742 100644 --- a/swap-orchestrator/src/docker/compose.rs +++ b/swap-orchestrator/src/docker/compose.rs @@ -1,10 +1,12 @@ use std::{ fmt::{Display, Write}, path::PathBuf, + process::Stdio, sync::Arc, }; use anyhow::Context; +use dialoguer::theme::ColorfulTheme; use url::Url; use crate::writer::IndentedWriter; @@ -567,6 +569,50 @@ impl Command { Ok(command) } + + /// Returns the command as a string for display purposes. + pub fn to_display_string(&self) -> String { + self.flags + .iter() + .map(|flag| flag.0.clone()) + .collect::>() + .join(" ") + } + + /// Prompt the user for confirmation before executing the command. + /// Exits with code 0 if the user declines. + pub fn confirm(&self) -> anyhow::Result<()> { + let consent = dialoguer::Confirm::with_theme(&ColorfulTheme::default()) + .default(true) + .with_prompt(format!( + "orchestrator will execute this command: \"{}\" - continue?", + self.to_display_string() + )) + .interact()?; + + if !consent { + std::process::exit(0); + } + Ok(()) + } + + /// Execute the command with stdin/stdout/stderr piped to the parent process. + /// + /// If `confirm` is true, a confirmation prompt will be shown before executing. + pub async fn exec_piped(&self, confirm: bool) -> anyhow::Result { + if confirm { + self.confirm()?; + } + + self.to_tokio_command()? + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()? + .wait() + .await + .context("Failed to execute command") + } } impl WriteConfig for Command { diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index 1f37b7516c..71f47f559b 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -17,6 +17,7 @@ async fn main() { Some(Command::Start) => command::start().await, Some(Command::Build) => command::build().await, Some(Command::Export) => command::export().await, + Some(Command::Controller) => command::controller().await, }; if let Err(err) = result { diff --git a/swap-orchestrator/src/util.rs b/swap-orchestrator/src/util.rs index f6905a1135..6e06450cf9 100644 --- a/swap-orchestrator/src/util.rs +++ b/swap-orchestrator/src/util.rs @@ -1,6 +1,6 @@ -use std::{path::PathBuf, process::Stdio}; +use std::path::PathBuf; -use anyhow::{Context, Result}; +use anyhow::Result; use swap_env::config::Config; use crate::{command, flag}; @@ -84,20 +84,3 @@ pub fn compose_name( "{bitcoin_network_str}_monero_{monero_network_str}_bitcoin" )) } - -#[allow(async_fn_in_trait)] -pub trait CommandExt { - async fn exec_piped(&mut self) -> anyhow::Result; -} - -impl CommandExt for tokio::process::Command { - async fn exec_piped(&mut self) -> anyhow::Result { - self.stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn()? - .wait() - .await - .context("Failed to execute command") - } -} From 76dead44a0e87834460403b6248a3d84386c6738 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 26 Nov 2025 13:21:00 +0100 Subject: [PATCH 15/16] fix vscode lsp build issue --- .gitignore | 1 + .vscode/settings.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dd2013c042..92421f7f40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target/ target-check/ +analyzer-target/ .vscode .claude/settings.local.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 104cee660d..8ef190d7ae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -78,7 +78,7 @@ "*.ipp": "cpp" }, "rust-analyzer.cargo.extraEnv": { - "CARGO_TARGET_DIR": "target-check" + "CARGO_TARGET_DIR": "analyzer-target" }, "[cpp]": { "editor.formatOnSave": false From 4a290f5b332c5d44d6bcd370f0bfffe1f3e399e6 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Mon, 1 Dec 2025 15:54:13 +0100 Subject: [PATCH 16/16] add controller command --- swap-orchestrator/src/command/controller.rs | 34 +++++++++++++++++++++ swap-orchestrator/src/util.rs | 23 ++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 swap-orchestrator/src/command/controller.rs diff --git a/swap-orchestrator/src/command/controller.rs b/swap-orchestrator/src/command/controller.rs new file mode 100644 index 0000000000..8325ffa511 --- /dev/null +++ b/swap-orchestrator/src/command/controller.rs @@ -0,0 +1,34 @@ +use std::{io::{Write, stdout}, process::exit}; + +use crate::{command, flag, util::{is_compose_container_running, is_docker_compose_running}}; + +pub async fn controller() -> anyhow::Result<()> { + if !is_docker_compose_running().await { + println!("Docker Compose is not running. Start it first and try again."); + exit(1); + } + + if !is_compose_container_running("asb-controller").await { + println!("ASB controller is not running. Start it by running `orchestrator start`."); + exit(1); + } + + let cmd = command!( + "docker", + flag!("compose"), + flag!("attach"), + flag!("asb-controller") + ); + + // Prompt for confirmation + cmd.confirm()?; + + // Print fake prompt before attaching (the real prompt won't appear immediately) + print!("Entering ASB controller. Type \"help\" for a list of commands\nasb> "); + stdout().flush()?; + + // Execute without confirmation (we already confirmed above) + cmd.exec_piped(false).await?; + + Ok(()) +} diff --git a/swap-orchestrator/src/util.rs b/swap-orchestrator/src/util.rs index 6e06450cf9..c03e0ed004 100644 --- a/swap-orchestrator/src/util.rs +++ b/swap-orchestrator/src/util.rs @@ -34,6 +34,29 @@ pub async fn probe_docker() -> Result<()> { } } +pub async fn is_docker_compose_running() -> bool { + probe_docker().await.is_ok() +} + +pub async fn is_compose_container_running(container_name: &str) -> bool { + let output = command!( + "docker", + flag!("compose"), + flag!("ps"), + flag!("{}", container_name), + flag!("--format"), + flag!("{{{{.State}}}}") + ) + .to_tokio_command() + .expect("non-empty command") + .output() + .await + .unwrap(); + + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.trim() == "running" +} + /// Check whether there's a valid maker config.toml file in the current directory. /// `None` if there isn't, `Some(Err(err))` if there is but it's invalid, `Some(Ok(config))` if there is and it's valid. pub async fn probe_maker_config() -> Option> {