mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-12-25 07:29:32 -05:00
Merge #442
442: Minor cleanups towards implementing a Monero wallet for local signing r=thomaseizinger a=thomaseizinger Extracted out of #434. Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
commit
e262345b4f
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -130,6 +130,4 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v1.2.0
|
- uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
- name: Run test ${{ matrix.test_name }}
|
- name: Run test ${{ matrix.test_name }}
|
||||||
run: cargo test --package swap --all-features --test ${{ matrix.test_name }} ""
|
run: cargo test --package swap --all-features --test ${{ matrix.test_name }} -- --nocapture
|
||||||
env:
|
|
||||||
MONERO_ADDITIONAL_SLEEP_PERIOD: 60000
|
|
||||||
|
31
Cargo.lock
generated
31
Cargo.lock
generated
@ -356,7 +356,7 @@ dependencies = [
|
|||||||
"bitcoincore-rpc-json",
|
"bitcoincore-rpc-json",
|
||||||
"futures",
|
"futures",
|
||||||
"hex 0.4.3",
|
"hex 0.4.3",
|
||||||
"jsonrpc_client",
|
"jsonrpc_client 0.5.1",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1629,7 +1629,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fc8515639023bf4260cf89475355fa77301685418f655c680528c380759e7782"
|
checksum = "fc8515639023bf4260cf89475355fa77301685418f655c680528c380759e7782"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"jsonrpc_client_macro",
|
"jsonrpc_client_macro 0.2.0",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url 2.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonrpc_client"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a85cf2c5ce158eabf30b2ac4f535463d7b09ce7905502e11238b7d6048ef7d02"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"jsonrpc_client_macro 0.3.0",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1646,6 +1660,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonrpc_client_macro"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97c11e429f0eaa41fe659013680b459d2368d8f0a3e69dccfb7a35800b0dc27b"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keccak-hash"
|
name = "keccak-hash"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -2143,7 +2167,6 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"futures",
|
"futures",
|
||||||
"monero-rpc",
|
"monero-rpc",
|
||||||
"port_check",
|
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
"spectral",
|
"spectral",
|
||||||
"testcontainers 0.12.0",
|
"testcontainers 0.12.0",
|
||||||
@ -2157,6 +2180,8 @@ name = "monero-rpc"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"jsonrpc_client 0.6.0",
|
||||||
|
"monero",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -8,7 +8,6 @@ edition = "2018"
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
monero-rpc = { path = "../monero-rpc" }
|
monero-rpc = { path = "../monero-rpc" }
|
||||||
port_check = "0.1"
|
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
spectral = "0.6"
|
spectral = "0.6"
|
||||||
testcontainers = "0.12"
|
testcontainers = "0.12"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashMap, env::var, thread::sleep, time::Duration};
|
use std::collections::HashMap;
|
||||||
use testcontainers::{
|
use testcontainers::{
|
||||||
core::{Container, Docker, WaitForMessage},
|
core::{Container, Docker, WaitForMessage},
|
||||||
Image,
|
Image,
|
||||||
@ -6,42 +6,36 @@ use testcontainers::{
|
|||||||
|
|
||||||
pub const MONEROD_DAEMON_CONTAINER_NAME: &str = "monerod";
|
pub const MONEROD_DAEMON_CONTAINER_NAME: &str = "monerod";
|
||||||
pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network";
|
pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network";
|
||||||
pub const MONEROD_RPC_PORT: u16 = 48081;
|
|
||||||
pub const WALLET_RPC_PORT: u16 = 48083;
|
/// The port we use for all RPC communication.
|
||||||
|
///
|
||||||
|
/// This is the default when running monerod.
|
||||||
|
/// For `monero-wallet-rpc` we always need to specify a port. To make things
|
||||||
|
/// simpler, we just specify the same one. They are in different containers so
|
||||||
|
/// this doesn't matter.
|
||||||
|
pub const RPC_PORT: u16 = 18081;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Monero {
|
pub struct Monerod {
|
||||||
tag: String,
|
args: MonerodArgs,
|
||||||
args: Args,
|
|
||||||
entrypoint: Option<String>,
|
|
||||||
wait_for_message: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image for Monero {
|
impl Image for Monerod {
|
||||||
type Args = Args;
|
type Args = MonerodArgs;
|
||||||
type EnvVars = HashMap<String, String>;
|
type EnvVars = HashMap<String, String>;
|
||||||
type Volumes = HashMap<String, String>;
|
type Volumes = HashMap<String, String>;
|
||||||
type EntryPoint = str;
|
type EntryPoint = str;
|
||||||
|
|
||||||
fn descriptor(&self) -> String {
|
fn descriptor(&self) -> String {
|
||||||
format!("xmrto/monero:{}", self.tag)
|
"xmrto/monero:v0.17.2.0".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
|
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
|
||||||
container
|
container
|
||||||
.logs()
|
.logs()
|
||||||
.stdout
|
.stdout
|
||||||
.wait_for_message(&self.wait_for_message)
|
.wait_for_message("JOINING all threads")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let additional_sleep_period =
|
|
||||||
var("MONERO_ADDITIONAL_SLEEP_PERIOD").map(|value| value.parse());
|
|
||||||
|
|
||||||
if let Ok(Ok(sleep_period)) = additional_sleep_period {
|
|
||||||
let sleep_period = Duration::from_millis(sleep_period);
|
|
||||||
|
|
||||||
sleep(sleep_period)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn args(&self) -> <Self as Image>::Args {
|
fn args(&self) -> <Self as Image>::Args {
|
||||||
@ -57,77 +51,80 @@ impl Image for Monero {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn with_args(self, args: <Self as Image>::Args) -> Self {
|
fn with_args(self, args: <Self as Image>::Args) -> Self {
|
||||||
Monero { args, ..self }
|
Self { args }
|
||||||
}
|
|
||||||
|
|
||||||
fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self {
|
|
||||||
Self {
|
|
||||||
entrypoint: Some(entrypoint.to_string()),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entrypoint(&self) -> Option<String> {
|
fn entrypoint(&self) -> Option<String> {
|
||||||
self.entrypoint.to_owned()
|
Some("".to_owned()) // an empty entrypoint disables the entrypoint
|
||||||
|
// script and gives us full control
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Monero {
|
impl Default for Monerod {
|
||||||
fn default() -> Self {
|
|
||||||
Monero {
|
|
||||||
tag: "v0.17.2.0".into(),
|
|
||||||
args: Args::default(),
|
|
||||||
entrypoint: Some("".into()),
|
|
||||||
wait_for_message: "core RPC server started ok".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Monero {
|
|
||||||
pub fn with_tag(self, tag_str: &str) -> Self {
|
|
||||||
Monero {
|
|
||||||
tag: tag_str.to_string(),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wallet(name: &str, daemon_address: String) -> Self {
|
|
||||||
let wallet = WalletArgs::new(name, daemon_address, WALLET_RPC_PORT);
|
|
||||||
let default = Monero::default();
|
|
||||||
Self {
|
|
||||||
args: Args {
|
|
||||||
image_args: ImageArgs::WalletArgs(wallet),
|
|
||||||
},
|
|
||||||
wait_for_message: "Run server thread name: RPC".to_string(),
|
|
||||||
..default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Args {
|
|
||||||
image_args: ImageArgs,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Args {
|
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
image_args: ImageArgs::MonerodArgs(MonerodArgs::default()),
|
args: MonerodArgs::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ImageArgs {
|
pub struct MoneroWalletRpc {
|
||||||
MonerodArgs(MonerodArgs),
|
args: MoneroWalletRpcArgs,
|
||||||
WalletArgs(WalletArgs),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageArgs {
|
impl Image for MoneroWalletRpc {
|
||||||
fn args(&self) -> String {
|
type Args = MoneroWalletRpcArgs;
|
||||||
match self {
|
type EnvVars = HashMap<String, String>;
|
||||||
ImageArgs::MonerodArgs(monerod_args) => monerod_args.args(),
|
type Volumes = HashMap<String, String>;
|
||||||
ImageArgs::WalletArgs(wallet_args) => wallet_args.args(),
|
type EntryPoint = str;
|
||||||
|
|
||||||
|
fn descriptor(&self) -> String {
|
||||||
|
"xmrto/monero:v0.17.2.0".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
|
||||||
|
container
|
||||||
|
.logs()
|
||||||
|
.stdout
|
||||||
|
.wait_for_message("JOINING all threads")
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn args(&self) -> <Self as Image>::Args {
|
||||||
|
self.args.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn volumes(&self) -> Self::Volumes {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_vars(&self) -> Self::EnvVars {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_args(self, args: <Self as Image>::Args) -> Self {
|
||||||
|
Self { args }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entrypoint(&self) -> Option<String> {
|
||||||
|
Some("".to_owned()) // an empty entrypoint disables the entrypoint
|
||||||
|
// script and gives us full control
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MoneroWalletRpc {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
args: MoneroWalletRpcArgs::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MoneroWalletRpc {
|
||||||
|
pub fn new(name: &str, daemon_address: String) -> Self {
|
||||||
|
Self {
|
||||||
|
args: MoneroWalletRpcArgs::new(name, daemon_address),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,51 +135,39 @@ pub struct MonerodArgs {
|
|||||||
pub offline: bool,
|
pub offline: bool,
|
||||||
pub rpc_payment_allow_free_loopback: bool,
|
pub rpc_payment_allow_free_loopback: bool,
|
||||||
pub confirm_external_bind: bool,
|
pub confirm_external_bind: bool,
|
||||||
pub non_interactive: bool,
|
|
||||||
pub no_igd: bool,
|
pub no_igd: bool,
|
||||||
pub hide_my_port: bool,
|
pub hide_my_port: bool,
|
||||||
pub rpc_bind_ip: String,
|
pub rpc_bind_ip: String,
|
||||||
pub rpc_bind_port: u16,
|
|
||||||
pub fixed_difficulty: u32,
|
pub fixed_difficulty: u32,
|
||||||
pub data_dir: String,
|
pub data_dir: String,
|
||||||
pub log_level: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct WalletArgs {
|
|
||||||
pub disable_rpc_login: bool,
|
|
||||||
pub confirm_external_bind: bool,
|
|
||||||
pub wallet_dir: String,
|
|
||||||
pub rpc_bind_ip: String,
|
|
||||||
pub rpc_bind_port: u16,
|
|
||||||
pub daemon_address: String,
|
|
||||||
pub log_level: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sane defaults for a mainnet regtest instance.
|
|
||||||
impl Default for MonerodArgs {
|
impl Default for MonerodArgs {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
MonerodArgs {
|
Self {
|
||||||
regtest: true,
|
regtest: true,
|
||||||
offline: true,
|
offline: true,
|
||||||
rpc_payment_allow_free_loopback: true,
|
rpc_payment_allow_free_loopback: true,
|
||||||
confirm_external_bind: true,
|
confirm_external_bind: true,
|
||||||
non_interactive: true,
|
|
||||||
no_igd: true,
|
no_igd: true,
|
||||||
hide_my_port: true,
|
hide_my_port: true,
|
||||||
rpc_bind_ip: "0.0.0.0".to_string(),
|
rpc_bind_ip: "0.0.0.0".to_string(),
|
||||||
rpc_bind_port: MONEROD_RPC_PORT,
|
|
||||||
fixed_difficulty: 1,
|
fixed_difficulty: 1,
|
||||||
data_dir: "/monero".to_string(),
|
data_dir: "/monero".to_string(),
|
||||||
log_level: 2,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MonerodArgs {
|
impl IntoIterator for MonerodArgs {
|
||||||
// Return monerod args as is single string so we can pass it to bash.
|
type Item = String;
|
||||||
fn args(&self) -> String {
|
type IntoIter = ::std::vec::IntoIter<String>;
|
||||||
let mut args = vec!["monerod".to_string()];
|
|
||||||
|
fn into_iter(self) -> <Self as IntoIterator>::IntoIter {
|
||||||
|
let mut args = vec![
|
||||||
|
"monerod".to_string(),
|
||||||
|
"--log-level=4".to_string(),
|
||||||
|
"--non-interactive".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
if self.regtest {
|
if self.regtest {
|
||||||
args.push("--regtest".to_string())
|
args.push("--regtest".to_string())
|
||||||
@ -200,10 +185,6 @@ impl MonerodArgs {
|
|||||||
args.push("--confirm-external-bind".to_string())
|
args.push("--confirm-external-bind".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.non_interactive {
|
|
||||||
args.push("--non-interactive".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.no_igd {
|
if self.no_igd {
|
||||||
args.push("--no-igd".to_string())
|
args.push("--no-igd".to_string())
|
||||||
}
|
}
|
||||||
@ -213,45 +194,60 @@ impl MonerodArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !self.rpc_bind_ip.is_empty() {
|
if !self.rpc_bind_ip.is_empty() {
|
||||||
args.push(format!("--rpc-bind-ip {}", self.rpc_bind_ip));
|
args.push(format!("--rpc-bind-ip={}", self.rpc_bind_ip));
|
||||||
}
|
|
||||||
|
|
||||||
if self.rpc_bind_port != 0 {
|
|
||||||
args.push(format!("--rpc-bind-port {}", self.rpc_bind_port));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.data_dir.is_empty() {
|
if !self.data_dir.is_empty() {
|
||||||
args.push(format!("--data-dir {}", self.data_dir));
|
args.push(format!("--data-dir={}", self.data_dir));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.fixed_difficulty != 0 {
|
if self.fixed_difficulty != 0 {
|
||||||
args.push(format!("--fixed-difficulty {}", self.fixed_difficulty));
|
args.push(format!("--fixed-difficulty={}", self.fixed_difficulty));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.log_level != 0 {
|
args.into_iter()
|
||||||
args.push(format!("--log-level {}", self.log_level));
|
|
||||||
}
|
|
||||||
|
|
||||||
args.join(" ")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WalletArgs {
|
#[derive(Debug, Clone)]
|
||||||
pub fn new(wallet_name: &str, daemon_address: String, rpc_port: u16) -> Self {
|
pub struct MoneroWalletRpcArgs {
|
||||||
WalletArgs {
|
pub disable_rpc_login: bool,
|
||||||
|
pub confirm_external_bind: bool,
|
||||||
|
pub wallet_dir: String,
|
||||||
|
pub rpc_bind_ip: String,
|
||||||
|
pub daemon_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MoneroWalletRpcArgs {
|
||||||
|
fn default() -> Self {
|
||||||
|
unimplemented!("A default instance for `MoneroWalletRpc` doesn't make sense because we always need to connect to a node.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MoneroWalletRpcArgs {
|
||||||
|
pub fn new(wallet_name: &str, daemon_address: String) -> Self {
|
||||||
|
Self {
|
||||||
disable_rpc_login: true,
|
disable_rpc_login: true,
|
||||||
confirm_external_bind: true,
|
confirm_external_bind: true,
|
||||||
wallet_dir: wallet_name.into(),
|
wallet_dir: wallet_name.into(),
|
||||||
rpc_bind_ip: "0.0.0.0".into(),
|
rpc_bind_ip: "0.0.0.0".into(),
|
||||||
rpc_bind_port: rpc_port,
|
|
||||||
daemon_address,
|
daemon_address,
|
||||||
log_level: 4,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return monero-wallet-rpc args as is single string so we can pass it to bash.
|
impl IntoIterator for MoneroWalletRpcArgs {
|
||||||
fn args(&self) -> String {
|
type Item = String;
|
||||||
let mut args = vec!["monero-wallet-rpc".to_string()];
|
type IntoIter = ::std::vec::IntoIter<String>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> <Self as IntoIterator>::IntoIter {
|
||||||
|
let mut args = vec![
|
||||||
|
"monero-wallet-rpc".to_string(),
|
||||||
|
format!("--wallet-dir={}", self.wallet_dir),
|
||||||
|
format!("--daemon-address={}", self.daemon_address),
|
||||||
|
format!("--rpc-bind-port={}", RPC_PORT),
|
||||||
|
"--log-level=4".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
if self.disable_rpc_login {
|
if self.disable_rpc_login {
|
||||||
args.push("--disable-rpc-login".to_string())
|
args.push("--disable-rpc-login".to_string())
|
||||||
@ -261,40 +257,10 @@ impl WalletArgs {
|
|||||||
args.push("--confirm-external-bind".to_string())
|
args.push("--confirm-external-bind".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.wallet_dir.is_empty() {
|
|
||||||
args.push(format!("--wallet-dir {}", self.wallet_dir));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.rpc_bind_ip.is_empty() {
|
if !self.rpc_bind_ip.is_empty() {
|
||||||
args.push(format!("--rpc-bind-ip {}", self.rpc_bind_ip));
|
args.push(format!("--rpc-bind-ip={}", self.rpc_bind_ip));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.rpc_bind_port != 0 {
|
args.into_iter()
|
||||||
args.push(format!("--rpc-bind-port {}", self.rpc_bind_port));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.daemon_address.is_empty() {
|
|
||||||
args.push(format!("--daemon-address {}", self.daemon_address));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.log_level != 0 {
|
|
||||||
args.push(format!("--log-level {}", self.log_level));
|
|
||||||
}
|
|
||||||
|
|
||||||
args.join(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for Args {
|
|
||||||
type Item = String;
|
|
||||||
type IntoIter = ::std::vec::IntoIter<String>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> <Self as IntoIterator>::IntoIter {
|
|
||||||
vec![
|
|
||||||
"/bin/bash".to_string(),
|
|
||||||
"-c".to_string(),
|
|
||||||
format!("{} ", self.image_args.args()),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,16 +22,15 @@
|
|||||||
//! Also provides standalone JSON RPC clients for monerod and monero-wallet-rpc.
|
//! Also provides standalone JSON RPC clients for monerod and monero-wallet-rpc.
|
||||||
pub mod image;
|
pub mod image;
|
||||||
|
|
||||||
use crate::image::{
|
use crate::image::{MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, RPC_PORT};
|
||||||
MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, MONEROD_RPC_PORT, WALLET_RPC_PORT,
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
};
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
|
||||||
use monero_rpc::{
|
use monero_rpc::{
|
||||||
monerod,
|
monerod,
|
||||||
wallet::{self, GetAddress, Refreshed, Transfer},
|
monerod::MonerodRpc as _,
|
||||||
|
wallet::{self, GetAddress, MoneroWalletRpc as _, Refreshed, Transfer},
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use testcontainers::{clients::Cli, core::Port, Container, Docker, RunArgs};
|
use testcontainers::{clients::Cli, Container, Docker, RunArgs};
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
/// How often we mine a block.
|
/// How often we mine a block.
|
||||||
@ -55,15 +54,19 @@ impl<'c> Monero {
|
|||||||
/// miner wallet container name is: `miner`
|
/// miner wallet container name is: `miner`
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
cli: &'c Cli,
|
cli: &'c Cli,
|
||||||
additional_wallets: Vec<String>,
|
additional_wallets: Vec<&'static str>,
|
||||||
) -> Result<(Self, Vec<Container<'c, Cli, image::Monero>>)> {
|
) -> Result<(
|
||||||
|
Self,
|
||||||
|
Container<'c, Cli, image::Monerod>,
|
||||||
|
Vec<Container<'c, Cli, image::MoneroWalletRpc>>,
|
||||||
|
)> {
|
||||||
let prefix = format!("{}_", random_prefix());
|
let prefix = format!("{}_", random_prefix());
|
||||||
let monerod_name = format!("{}{}", prefix, MONEROD_DAEMON_CONTAINER_NAME);
|
let monerod_name = format!("{}{}", prefix, MONEROD_DAEMON_CONTAINER_NAME);
|
||||||
let network = format!("{}{}", prefix, MONEROD_DEFAULT_NETWORK);
|
let network = format!("{}{}", prefix, MONEROD_DEFAULT_NETWORK);
|
||||||
|
|
||||||
tracing::info!("Starting monerod: {}", monerod_name);
|
tracing::info!("Starting monerod: {}", monerod_name);
|
||||||
let (monerod, monerod_container) = Monerod::new(cli, monerod_name, network)?;
|
let (monerod, monerod_container) = Monerod::new(cli, monerod_name, network)?;
|
||||||
let mut containers = vec![monerod_container];
|
let mut containers = vec![];
|
||||||
let mut wallets = vec![];
|
let mut wallets = vec![];
|
||||||
|
|
||||||
let miner = "miner";
|
let miner = "miner";
|
||||||
@ -81,7 +84,7 @@ impl<'c> Monero {
|
|||||||
containers.push(container);
|
containers.push(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((Self { monerod, wallets }, containers))
|
Ok((Self { monerod, wallets }, monerod_container, containers))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn monerod(&self) -> &Monerod {
|
pub fn monerod(&self) -> &Monerod {
|
||||||
@ -104,7 +107,10 @@ impl<'c> Monero {
|
|||||||
|
|
||||||
// generate the first 70 as bulk
|
// generate the first 70 as bulk
|
||||||
let monerod = &self.monerod;
|
let monerod = &self.monerod;
|
||||||
let res = monerod.client().generate_blocks(70, &miner_address).await?;
|
let res = monerod
|
||||||
|
.client()
|
||||||
|
.generateblocks(70, miner_address.clone())
|
||||||
|
.await?;
|
||||||
tracing::info!("Generated {:?} blocks", res.blocks.len());
|
tracing::info!("Generated {:?} blocks", res.blocks.len());
|
||||||
miner_wallet.refresh().await?;
|
miner_wallet.refresh().await?;
|
||||||
|
|
||||||
@ -123,7 +129,10 @@ impl<'c> Monero {
|
|||||||
if amount > 0 {
|
if amount > 0 {
|
||||||
miner_wallet.transfer(&address, amount).await?;
|
miner_wallet.transfer(&address, amount).await?;
|
||||||
tracing::info!("Funded {} wallet with {}", wallet.name, amount);
|
tracing::info!("Funded {} wallet with {}", wallet.name, amount);
|
||||||
monerod.client().generate_blocks(10, &miner_address).await?;
|
monerod
|
||||||
|
.client()
|
||||||
|
.generateblocks(10, miner_address.clone())
|
||||||
|
.await?;
|
||||||
wallet.refresh().await?;
|
wallet.refresh().await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,7 +148,7 @@ impl<'c> Monero {
|
|||||||
monerod.start_miner(&miner_address).await?;
|
monerod.start_miner(&miner_address).await?;
|
||||||
|
|
||||||
tracing::info!("Waiting for miner wallet to catch up...");
|
tracing::info!("Waiting for miner wallet to catch up...");
|
||||||
let block_height = monerod.client().get_block_count().await?;
|
let block_height = monerod.client().get_block_count().await?.count;
|
||||||
miner_wallet
|
miner_wallet
|
||||||
.wait_for_wallet_height(block_height)
|
.wait_for_wallet_height(block_height)
|
||||||
.await
|
.await
|
||||||
@ -158,17 +167,11 @@ impl<'c> Monero {
|
|||||||
|
|
||||||
fn random_prefix() -> String {
|
fn random_prefix() -> String {
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
|
|
||||||
const LEN: usize = 4;
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
|
|
||||||
let prefix: String = (0..LEN)
|
rand::thread_rng()
|
||||||
.map(|_| {
|
.sample_iter(rand::distributions::Alphanumeric)
|
||||||
let idx = rng.gen_range(0, CHARSET.len());
|
.take(4)
|
||||||
CHARSET[idx] as char
|
.collect()
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
prefix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -176,6 +179,7 @@ pub struct Monerod {
|
|||||||
rpc_port: u16,
|
rpc_port: u16,
|
||||||
name: String,
|
name: String,
|
||||||
network: String,
|
network: String,
|
||||||
|
client: monerod::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -183,6 +187,7 @@ pub struct MoneroWalletRpc {
|
|||||||
rpc_port: u16,
|
rpc_port: u16,
|
||||||
name: String,
|
name: String,
|
||||||
network: String,
|
network: String,
|
||||||
|
client: wallet::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'c> Monerod {
|
impl<'c> Monerod {
|
||||||
@ -191,38 +196,35 @@ impl<'c> Monerod {
|
|||||||
cli: &'c Cli,
|
cli: &'c Cli,
|
||||||
name: String,
|
name: String,
|
||||||
network: String,
|
network: String,
|
||||||
) -> Result<(Self, Container<'c, Cli, image::Monero>)> {
|
) -> Result<(Self, Container<'c, Cli, image::Monerod>)> {
|
||||||
let monerod_rpc_port: u16 =
|
let image = image::Monerod::default();
|
||||||
port_check::free_local_port().ok_or_else(|| anyhow!("Could not retrieve free port"))?;
|
|
||||||
|
|
||||||
let image = image::Monero::default();
|
|
||||||
let run_args = RunArgs::default()
|
let run_args = RunArgs::default()
|
||||||
.with_name(name.clone())
|
.with_name(name.clone())
|
||||||
.with_network(network.clone())
|
.with_network(network.clone());
|
||||||
.with_mapped_port(Port {
|
let container = cli.run_with_args(image, run_args);
|
||||||
local: monerod_rpc_port,
|
let monerod_rpc_port = container
|
||||||
internal: MONEROD_RPC_PORT,
|
.get_host_port(RPC_PORT)
|
||||||
});
|
.context("port not exposed")?;
|
||||||
let docker = cli.run_with_args(image, run_args);
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Self {
|
Self {
|
||||||
rpc_port: monerod_rpc_port,
|
rpc_port: monerod_rpc_port,
|
||||||
name,
|
name,
|
||||||
network,
|
network,
|
||||||
|
client: monerod::Client::localhost(monerod_rpc_port)?,
|
||||||
},
|
},
|
||||||
docker,
|
container,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client(&self) -> monerod::Client {
|
pub fn client(&self) -> &monerod::Client {
|
||||||
monerod::Client::localhost(self.rpc_port)
|
&self.client
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns a task to mine blocks in a regular interval to the provided
|
/// Spawns a task to mine blocks in a regular interval to the provided
|
||||||
/// address
|
/// address
|
||||||
pub async fn start_miner(&self, miner_wallet_address: &str) -> Result<()> {
|
pub async fn start_miner(&self, miner_wallet_address: &str) -> Result<()> {
|
||||||
let monerod = self.client();
|
let monerod = self.client().clone();
|
||||||
let _ = tokio::spawn(mine(monerod, miner_wallet_address.to_string()));
|
let _ = tokio::spawn(mine(monerod, miner_wallet_address.to_string()));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -236,48 +238,46 @@ impl<'c> MoneroWalletRpc {
|
|||||||
name: &str,
|
name: &str,
|
||||||
monerod: &Monerod,
|
monerod: &Monerod,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
) -> Result<(Self, Container<'c, Cli, image::Monero>)> {
|
) -> Result<(Self, Container<'c, Cli, image::MoneroWalletRpc>)> {
|
||||||
let wallet_rpc_port: u16 =
|
let daemon_address = format!("{}:{}", monerod.name, RPC_PORT);
|
||||||
port_check::free_local_port().ok_or_else(|| anyhow!("Could not retrieve free port"))?;
|
let image = image::MoneroWalletRpc::new(&name, daemon_address);
|
||||||
|
|
||||||
let daemon_address = format!("{}:{}", monerod.name, MONEROD_RPC_PORT);
|
|
||||||
let image = image::Monero::wallet(&name, daemon_address);
|
|
||||||
|
|
||||||
let network = monerod.network.clone();
|
let network = monerod.network.clone();
|
||||||
let run_args = RunArgs::default()
|
let run_args = RunArgs::default()
|
||||||
// prefix the container name so we can run multiple tests
|
// prefix the container name so we can run multiple tests
|
||||||
.with_name(format!("{}{}", prefix, name))
|
.with_name(format!("{}{}", prefix, name))
|
||||||
.with_network(network.clone())
|
.with_network(network.clone());
|
||||||
.with_mapped_port(Port {
|
let container = cli.run_with_args(image, run_args);
|
||||||
local: wallet_rpc_port,
|
let wallet_rpc_port = container
|
||||||
internal: WALLET_RPC_PORT,
|
.get_host_port(RPC_PORT)
|
||||||
});
|
.context("port not exposed")?;
|
||||||
let docker = cli.run_with_args(image, run_args);
|
|
||||||
|
|
||||||
// create new wallet
|
// create new wallet
|
||||||
wallet::Client::localhost(wallet_rpc_port)
|
let client = wallet::Client::localhost(wallet_rpc_port)?;
|
||||||
.create_wallet(name)
|
|
||||||
.await
|
client
|
||||||
.unwrap();
|
.create_wallet(name.to_owned(), "English".to_owned())
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Self {
|
Self {
|
||||||
rpc_port: wallet_rpc_port,
|
rpc_port: wallet_rpc_port,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
network,
|
network,
|
||||||
|
client,
|
||||||
},
|
},
|
||||||
docker,
|
container,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client(&self) -> wallet::Client {
|
pub fn client(&self) -> &wallet::Client {
|
||||||
wallet::Client::localhost(self.rpc_port)
|
&self.client
|
||||||
}
|
}
|
||||||
|
|
||||||
// It takes a little while for the wallet to sync with monerod.
|
// It takes a little while for the wallet to sync with monerod.
|
||||||
pub async fn wait_for_wallet_height(&self, height: u32) -> Result<()> {
|
pub async fn wait_for_wallet_height(&self, height: u32) -> Result<()> {
|
||||||
let mut retry: u8 = 0;
|
let mut retry: u8 = 0;
|
||||||
while self.client().block_height().await?.height < height {
|
while self.client().get_height().await?.height < height {
|
||||||
if retry >= 30 {
|
if retry >= 30 {
|
||||||
// ~30 seconds
|
// ~30 seconds
|
||||||
bail!("Wallet could not catch up with monerod after 30 retries.")
|
bail!("Wallet could not catch up with monerod after 30 retries.")
|
||||||
@ -290,26 +290,28 @@ impl<'c> MoneroWalletRpc {
|
|||||||
|
|
||||||
/// Sends amount to address
|
/// Sends amount to address
|
||||||
pub async fn transfer(&self, address: &str, amount: u64) -> Result<Transfer> {
|
pub async fn transfer(&self, address: &str, amount: u64) -> Result<Transfer> {
|
||||||
self.client().transfer(0, amount, address).await
|
Ok(self.client().transfer_single(0, amount, address).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn address(&self) -> Result<GetAddress> {
|
pub async fn address(&self) -> Result<GetAddress> {
|
||||||
self.client().get_address(0).await
|
Ok(self.client().get_address(0).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn balance(&self) -> Result<u64> {
|
pub async fn balance(&self) -> Result<u64> {
|
||||||
self.client().refresh().await?;
|
self.client().refresh().await?;
|
||||||
self.client().get_balance(0).await
|
let balance = self.client().get_balance(0).await?.balance;
|
||||||
|
|
||||||
|
Ok(balance)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(&self) -> Result<Refreshed> {
|
pub async fn refresh(&self) -> Result<Refreshed> {
|
||||||
self.client().refresh().await
|
Ok(self.client().refresh().await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Mine a block ever BLOCK_TIME_SECS seconds.
|
/// Mine a block ever BLOCK_TIME_SECS seconds.
|
||||||
async fn mine(monerod: monerod::Client, reward_address: String) -> Result<()> {
|
async fn mine(monerod: monerod::Client, reward_address: String) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
time::sleep(Duration::from_secs(BLOCK_TIME_SECS)).await;
|
time::sleep(Duration::from_secs(BLOCK_TIME_SECS)).await;
|
||||||
monerod.generate_blocks(1, &reward_address).await?;
|
monerod.generateblocks(1, reward_address.clone()).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use monero_harness::Monero;
|
use monero_harness::Monero;
|
||||||
|
use monero_rpc::monerod::MonerodRpc as _;
|
||||||
use spectral::prelude::*;
|
use spectral::prelude::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
@ -12,7 +13,7 @@ async fn init_miner_and_mine_to_miner_address() {
|
|||||||
.set_default();
|
.set_default();
|
||||||
|
|
||||||
let tc = Cli::default();
|
let tc = Cli::default();
|
||||||
let (monero, _monerod_container) = Monero::new(&tc, vec![]).await.unwrap();
|
let (monero, _monerod_container, _wallet_containers) = Monero::new(&tc, vec![]).await.unwrap();
|
||||||
|
|
||||||
monero.init_and_start_miner().await.unwrap();
|
monero.init_and_start_miner().await.unwrap();
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ async fn init_miner_and_mine_to_miner_address() {
|
|||||||
time::sleep(Duration::from_millis(1010)).await;
|
time::sleep(Duration::from_millis(1010)).await;
|
||||||
|
|
||||||
// after a bit more than 1 sec another block should have been mined
|
// after a bit more than 1 sec another block should have been mined
|
||||||
let block_height = monerod.client().get_block_count().await.unwrap();
|
let block_height = monerod.client().get_block_count().await.unwrap().count;
|
||||||
|
|
||||||
assert_that(&block_height).is_greater_than(70);
|
assert_that(&block_height).is_greater_than(70);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use monero_harness::{Monero, MoneroWalletRpc};
|
use monero_harness::{Monero, MoneroWalletRpc};
|
||||||
|
use monero_rpc::wallet::MoneroWalletRpc as _;
|
||||||
use spectral::prelude::*;
|
use spectral::prelude::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
@ -16,9 +17,8 @@ async fn fund_transfer_and_check_tx_key() {
|
|||||||
let send_to_bob = 5_000_000_000;
|
let send_to_bob = 5_000_000_000;
|
||||||
|
|
||||||
let tc = Cli::default();
|
let tc = Cli::default();
|
||||||
let (monero, _containers) = Monero::new(&tc, vec!["alice".to_string(), "bob".to_string()])
|
let (monero, _monerod_container, _wallet_containers) =
|
||||||
.await
|
Monero::new(&tc, vec!["alice", "bob"]).await.unwrap();
|
||||||
.unwrap();
|
|
||||||
let alice_wallet = monero.wallet("alice").unwrap();
|
let alice_wallet = monero.wallet("alice").unwrap();
|
||||||
let bob_wallet = monero.wallet("bob").unwrap();
|
let bob_wallet = monero.wallet("bob").unwrap();
|
||||||
|
|
||||||
@ -45,10 +45,10 @@ async fn fund_transfer_and_check_tx_key() {
|
|||||||
|
|
||||||
// check if tx was actually seen
|
// check if tx was actually seen
|
||||||
let tx_id = transfer.tx_hash;
|
let tx_id = transfer.tx_hash;
|
||||||
let tx_key = transfer.tx_key;
|
let tx_key = transfer.tx_key.unwrap().to_string();
|
||||||
let res = bob_wallet
|
let res = bob_wallet
|
||||||
.client()
|
.client()
|
||||||
.check_tx_key(&tx_id, &tx_key, &bob_address)
|
.check_tx_key(tx_id, tx_key, bob_address)
|
||||||
.await
|
.await
|
||||||
.expect("failed to check tx by key");
|
.expect("failed to check tx by key");
|
||||||
|
|
||||||
|
@ -10,3 +10,5 @@ reqwest = { version = "0.11", default-features = false, features = ["json"] }
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
jsonrpc_client = { version = "0.6", features = ["reqwest"] }
|
||||||
|
monero = "0.11"
|
||||||
|
@ -12,6 +12,5 @@
|
|||||||
)]
|
)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
mod rpc;
|
pub mod monerod;
|
||||||
|
pub mod wallet;
|
||||||
pub use self::rpc::*;
|
|
||||||
|
63
monero-rpc/src/monerod.rs
Normal file
63
monero-rpc/src/monerod.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[jsonrpc_client::api(version = "2.0")]
|
||||||
|
pub trait MonerodRpc {
|
||||||
|
async fn generateblocks(&self, amount_of_blocks: u32, wallet_address: String)
|
||||||
|
-> GenerateBlocks;
|
||||||
|
async fn get_block_header_by_height(&self, height: u32) -> BlockHeader;
|
||||||
|
async fn get_block_count(&self) -> BlockCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[jsonrpc_client::implement(MonerodRpc)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
inner: reqwest::Client,
|
||||||
|
base_url: reqwest::Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
/// New local host monerod RPC client.
|
||||||
|
pub fn localhost(port: u16) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: reqwest::ClientBuilder::new()
|
||||||
|
.connection_verbose(true)
|
||||||
|
.build()?,
|
||||||
|
base_url: format!("http://127.0.0.1:{}/json_rpc", port)
|
||||||
|
.parse()
|
||||||
|
.context("url is well formed")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct GenerateBlocks {
|
||||||
|
pub blocks: Vec<String>,
|
||||||
|
pub height: u32,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct BlockCount {
|
||||||
|
pub count: u32,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should be able to use monero-rs for this but it does not include all
|
||||||
|
// the fields.
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct BlockHeader {
|
||||||
|
pub block_size: u32,
|
||||||
|
pub depth: u32,
|
||||||
|
pub difficulty: u32,
|
||||||
|
pub hash: String,
|
||||||
|
pub height: u32,
|
||||||
|
pub major_version: u32,
|
||||||
|
pub minor_version: u32,
|
||||||
|
pub nonce: u32,
|
||||||
|
pub num_txes: u32,
|
||||||
|
pub orphan_status: bool,
|
||||||
|
pub prev_hash: String,
|
||||||
|
pub reward: u64,
|
||||||
|
pub timestamp: u32,
|
||||||
|
}
|
@ -1,63 +0,0 @@
|
|||||||
//! JSON RPC clients for `monerd` and `monero-wallet-rpc`.
|
|
||||||
pub mod monerod;
|
|
||||||
pub mod wallet;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
pub struct Request<T> {
|
|
||||||
/// JSON RPC version, we hard cod this to 2.0.
|
|
||||||
jsonrpc: String,
|
|
||||||
/// Client controlled identifier, we hard code this to 1.
|
|
||||||
id: String,
|
|
||||||
/// The method to call.
|
|
||||||
method: String,
|
|
||||||
/// The method parameters.
|
|
||||||
params: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JSON RPC request.
|
|
||||||
impl<T> Request<T> {
|
|
||||||
pub fn new(method: &str, params: T) -> Self {
|
|
||||||
Self {
|
|
||||||
jsonrpc: "2.0".to_owned(),
|
|
||||||
id: "1".to_owned(),
|
|
||||||
method: method.to_owned(),
|
|
||||||
params,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JSON RPC response.
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
struct Response<T> {
|
|
||||||
pub id: String,
|
|
||||||
pub jsonrpc: String,
|
|
||||||
pub result: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct Params {
|
|
||||||
val: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_serialize_request_with_params() {
|
|
||||||
// Dummy method and parameters.
|
|
||||||
let params = Params { val: 0 };
|
|
||||||
let method = "get_block";
|
|
||||||
|
|
||||||
let r = Request::new(method, ¶ms);
|
|
||||||
let got = serde_json::to_string(&r).expect("failed to serialize request");
|
|
||||||
|
|
||||||
let want =
|
|
||||||
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"get_block\",\"params\":{\"val\":0}}"
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
assert_eq!(got, want);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
use crate::rpc::{Request, Response};
|
|
||||||
use anyhow::Result;
|
|
||||||
use reqwest::Url;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
/// RPC client for monerod and monero-wallet-rpc.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Client {
|
|
||||||
pub inner: reqwest::Client,
|
|
||||||
pub url: Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
/// New local host monerod RPC client.
|
|
||||||
pub fn localhost(port: u16) -> Self {
|
|
||||||
let url = format!("http://127.0.0.1:{}/json_rpc", port);
|
|
||||||
let url = Url::parse(&url).expect("url is well formed");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
inner: reqwest::Client::new(),
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn generate_blocks(
|
|
||||||
&self,
|
|
||||||
amount_of_blocks: u32,
|
|
||||||
wallet_address: &str,
|
|
||||||
) -> Result<GenerateBlocks> {
|
|
||||||
let params = GenerateBlocksParams {
|
|
||||||
amount_of_blocks,
|
|
||||||
wallet_address: wallet_address.to_owned(),
|
|
||||||
};
|
|
||||||
let url = self.url.clone();
|
|
||||||
// // Step 1: Get the auth header
|
|
||||||
// let res = self.inner.get(url.clone()).send().await?;
|
|
||||||
// let headers = res.headers();
|
|
||||||
// let wwwauth = headers["www-authenticate"].to_str()?;
|
|
||||||
//
|
|
||||||
// // Step 2: Given the auth header, sign the digest for the real req.
|
|
||||||
// let tmp_url = url.clone();
|
|
||||||
// let context = AuthContext::new("username", "password", tmp_url.path());
|
|
||||||
// let mut prompt = digest_auth::parse(wwwauth)?;
|
|
||||||
// let answer = prompt.respond(&context)?.to_header_string();
|
|
||||||
|
|
||||||
let request = Request::new("generateblocks", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(url)
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("generate blocks response: {}", response);
|
|
||||||
|
|
||||||
let res: Response<GenerateBlocks> = serde_json::from_str(&response)?;
|
|
||||||
|
|
||||||
Ok(res.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// $ curl http://127.0.0.1:18081/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"get_block_header_by_height","params":{"height":1}}' -H 'Content-Type: application/json'
|
|
||||||
pub async fn get_block_header_by_height(&self, height: u32) -> Result<BlockHeader> {
|
|
||||||
let params = GetBlockHeaderByHeightParams { height };
|
|
||||||
let request = Request::new("get_block_header_by_height", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("get block header by height response: {}", response);
|
|
||||||
|
|
||||||
let res: Response<GetBlockHeaderByHeight> = serde_json::from_str(&response)?;
|
|
||||||
|
|
||||||
Ok(res.result.block_header)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_block_count(&self) -> Result<u32> {
|
|
||||||
let request = Request::new("get_block_count", "");
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("get block count response: {}", response);
|
|
||||||
|
|
||||||
let res: Response<BlockCount> = serde_json::from_str(&response)?;
|
|
||||||
|
|
||||||
Ok(res.result.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
struct GenerateBlocksParams {
|
|
||||||
amount_of_blocks: u32,
|
|
||||||
wallet_address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct GenerateBlocks {
|
|
||||||
pub blocks: Vec<String>,
|
|
||||||
pub height: u32,
|
|
||||||
pub status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
struct GetBlockHeaderByHeightParams {
|
|
||||||
height: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
struct GetBlockHeaderByHeight {
|
|
||||||
block_header: BlockHeader,
|
|
||||||
status: String,
|
|
||||||
untrusted: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
struct BlockCount {
|
|
||||||
count: u32,
|
|
||||||
status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should be able to use monero-rs for this but it does not include all
|
|
||||||
// the fields.
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct BlockHeader {
|
|
||||||
pub block_size: u32,
|
|
||||||
pub depth: u32,
|
|
||||||
pub difficulty: u32,
|
|
||||||
pub hash: String,
|
|
||||||
pub height: u32,
|
|
||||||
pub major_version: u32,
|
|
||||||
pub minor_version: u32,
|
|
||||||
pub nonce: u32,
|
|
||||||
pub num_txes: u32,
|
|
||||||
pub orphan_status: bool,
|
|
||||||
pub prev_hash: String,
|
|
||||||
pub reward: u64,
|
|
||||||
pub timestamp: u32,
|
|
||||||
}
|
|
@ -1,569 +0,0 @@
|
|||||||
use crate::rpc::{Request, Response};
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use reqwest::Url;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
/// JSON RPC client for monero-wallet-rpc.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Client {
|
|
||||||
pub inner: reqwest::Client,
|
|
||||||
pub url: Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
/// Constructs a monero-wallet-rpc client with localhost endpoint.
|
|
||||||
pub fn localhost(port: u16) -> Self {
|
|
||||||
let url = format!("http://127.0.0.1:{}/json_rpc", port);
|
|
||||||
let url = Url::parse(&url).expect("url is well formed");
|
|
||||||
|
|
||||||
Client::new(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a monero-wallet-rpc client with `url` endpoint.
|
|
||||||
pub fn new(url: Url) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: reqwest::Client::new(),
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get addresses for account by index.
|
|
||||||
pub async fn get_address(&self, account_index: u32) -> Result<GetAddress> {
|
|
||||||
let params = GetAddressParams { account_index };
|
|
||||||
let request = Request::new("get_address", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("get address RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<GetAddress>>(&response)?;
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the balance of account by index.
|
|
||||||
pub async fn get_balance(&self, index: u32) -> Result<u64> {
|
|
||||||
let params = GetBalanceParams {
|
|
||||||
account_index: index,
|
|
||||||
};
|
|
||||||
let request = Request::new("get_balance", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"get balance of account index {} RPC response: {}",
|
|
||||||
index, response
|
|
||||||
);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<GetBalance>>(&response)?;
|
|
||||||
|
|
||||||
let balance = r.result.balance;
|
|
||||||
|
|
||||||
Ok(balance)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_account(&self, label: &str) -> Result<CreateAccount> {
|
|
||||||
let params = LabelParams {
|
|
||||||
label: label.to_owned(),
|
|
||||||
};
|
|
||||||
let request = Request::new("create_account", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("create account RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<CreateAccount>>(&response)?;
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get accounts, filtered by tag ("" for no filtering).
|
|
||||||
pub async fn get_accounts(&self, tag: &str) -> Result<GetAccounts> {
|
|
||||||
let params = TagParams {
|
|
||||||
tag: tag.to_owned(),
|
|
||||||
};
|
|
||||||
let request = Request::new("get_accounts", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("get accounts RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<GetAccounts>>(&response)?;
|
|
||||||
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens a wallet using `filename`.
|
|
||||||
pub async fn open_wallet(&self, filename: &str) -> Result<()> {
|
|
||||||
let params = OpenWalletParams {
|
|
||||||
filename: filename.to_owned(),
|
|
||||||
};
|
|
||||||
let request = Request::new("open_wallet", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("open wallet RPC response: {}", response);
|
|
||||||
|
|
||||||
// TODO: Proper error handling once switching to https://github.com/thomaseizinger/rust-jsonrpc-client/
|
|
||||||
// Currently blocked by https://github.com/thomaseizinger/rust-jsonrpc-client/issues/20
|
|
||||||
if response.contains("error") {
|
|
||||||
bail!("Failed to open wallet")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close the currently opened wallet, after trying to save it.
|
|
||||||
pub async fn close_wallet(&self) -> Result<()> {
|
|
||||||
let request = Request::new("close_wallet", "");
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("close wallet RPC response: {}", response);
|
|
||||||
|
|
||||||
if response.contains("error") {
|
|
||||||
bail!("Failed to close wallet")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a wallet using `filename`.
|
|
||||||
pub async fn create_wallet(&self, filename: &str) -> Result<()> {
|
|
||||||
let params = CreateWalletParams {
|
|
||||||
filename: filename.to_owned(),
|
|
||||||
language: "English".to_owned(),
|
|
||||||
};
|
|
||||||
let request = Request::new("create_wallet", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("create wallet RPC response: {}", response);
|
|
||||||
|
|
||||||
if response.contains("error") {
|
|
||||||
bail!("Failed to create wallet")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transfers `amount` moneroj from `account_index` to `address`.
|
|
||||||
pub async fn transfer(
|
|
||||||
&self,
|
|
||||||
account_index: u32,
|
|
||||||
amount: u64,
|
|
||||||
address: &str,
|
|
||||||
) -> Result<Transfer> {
|
|
||||||
let dest = vec![Destination {
|
|
||||||
amount,
|
|
||||||
address: address.to_owned(),
|
|
||||||
}];
|
|
||||||
self.multi_transfer(account_index, dest).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transfers moneroj from `account_index` to `destinations`.
|
|
||||||
pub async fn multi_transfer(
|
|
||||||
&self,
|
|
||||||
account_index: u32,
|
|
||||||
destinations: Vec<Destination>,
|
|
||||||
) -> Result<Transfer> {
|
|
||||||
let params = TransferParams {
|
|
||||||
account_index,
|
|
||||||
destinations,
|
|
||||||
get_tx_key: true,
|
|
||||||
};
|
|
||||||
let request = Request::new("transfer", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("transfer RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<Transfer>>(&response)?;
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get wallet block height, this might be behind monerod height.
|
|
||||||
pub async fn block_height(&self) -> Result<BlockHeight> {
|
|
||||||
let request = Request::new("get_height", "");
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("wallet height RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<BlockHeight>>(&response)?;
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check a transaction in the blockchain with its secret key.
|
|
||||||
pub async fn check_tx_key(
|
|
||||||
&self,
|
|
||||||
tx_id: &str,
|
|
||||||
tx_key: &str,
|
|
||||||
address: &str,
|
|
||||||
) -> Result<CheckTxKey> {
|
|
||||||
let params = CheckTxKeyParams {
|
|
||||||
tx_id: tx_id.to_owned(),
|
|
||||||
tx_key: tx_key.to_owned(),
|
|
||||||
address: address.to_owned(),
|
|
||||||
};
|
|
||||||
let request = Request::new("check_tx_key", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("check_tx_key RPC response: {}", response);
|
|
||||||
|
|
||||||
let check_tx_key = serde_json::from_str::<Response<CheckTxKey>>(&response)?;
|
|
||||||
let mut check_tx_key = check_tx_key.result;
|
|
||||||
|
|
||||||
// Due to a bug in monerod that causes check_tx_key confirmations
|
|
||||||
// to overflow we safeguard the confirmations to avoid unwanted
|
|
||||||
// side effects.
|
|
||||||
if check_tx_key.confirmations > u64::MAX - 1000 {
|
|
||||||
check_tx_key.confirmations = 0u64;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(check_tx_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn generate_from_keys(
|
|
||||||
&self,
|
|
||||||
filename: &str,
|
|
||||||
address: &str,
|
|
||||||
spend_key: &str,
|
|
||||||
view_key: &str,
|
|
||||||
restore_height: u32,
|
|
||||||
) -> Result<GenerateFromKeys> {
|
|
||||||
let params = GenerateFromKeysParams {
|
|
||||||
restore_height,
|
|
||||||
filename: filename.into(),
|
|
||||||
address: address.into(),
|
|
||||||
spendkey: spend_key.into(),
|
|
||||||
viewkey: view_key.into(),
|
|
||||||
password: "".into(),
|
|
||||||
autosave_current: true,
|
|
||||||
};
|
|
||||||
let request = Request::new("generate_from_keys", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("generate_from_keys RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<GenerateFromKeys>>(&response)?;
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh(&self) -> Result<Refreshed> {
|
|
||||||
let request = Request::new("refresh", "");
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("refresh RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<Refreshed>>(&response)?;
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transfers the complete balance of the account to `address`.
|
|
||||||
pub async fn sweep_all(&self, address: &str) -> Result<SweepAll> {
|
|
||||||
let params = SweepAllParams {
|
|
||||||
address: address.into(),
|
|
||||||
};
|
|
||||||
let request = Request::new("sweep_all", params);
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("sweep_all RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<SweepAll>>(&response)?;
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_version(&self) -> Result<Version> {
|
|
||||||
let request = Request::new("get_version", "");
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(self.url.clone())
|
|
||||||
.json(&request)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("get_version RPC response: {}", response);
|
|
||||||
|
|
||||||
let r = serde_json::from_str::<Response<Version>>(&response)?;
|
|
||||||
Ok(r.result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct GetAddressParams {
|
|
||||||
account_index: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct GetAddress {
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct GetBalanceParams {
|
|
||||||
account_index: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
struct GetBalance {
|
|
||||||
balance: u64,
|
|
||||||
blocks_to_unlock: u32,
|
|
||||||
multisig_import_needed: bool,
|
|
||||||
time_to_unlock: u32,
|
|
||||||
unlocked_balance: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct LabelParams {
|
|
||||||
label: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct CreateAccount {
|
|
||||||
pub account_index: u32,
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct TagParams {
|
|
||||||
tag: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct GetAccounts {
|
|
||||||
pub subaddress_accounts: Vec<SubAddressAccount>,
|
|
||||||
pub total_balance: u64,
|
|
||||||
pub total_unlocked_balance: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct SubAddressAccount {
|
|
||||||
pub account_index: u32,
|
|
||||||
pub balance: u32,
|
|
||||||
pub base_address: String,
|
|
||||||
pub label: String,
|
|
||||||
pub tag: String,
|
|
||||||
pub unlocked_balance: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct OpenWalletParams {
|
|
||||||
filename: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct CreateWalletParams {
|
|
||||||
filename: String,
|
|
||||||
language: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct TransferParams {
|
|
||||||
// Transfer from this account.
|
|
||||||
account_index: u32,
|
|
||||||
// Destinations to receive XMR:
|
|
||||||
destinations: Vec<Destination>,
|
|
||||||
// Return the transaction key after sending.
|
|
||||||
get_tx_key: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
pub struct Destination {
|
|
||||||
amount: u64,
|
|
||||||
address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct Transfer {
|
|
||||||
pub amount: u64,
|
|
||||||
pub fee: u64,
|
|
||||||
pub multisig_txset: String,
|
|
||||||
pub tx_blob: String,
|
|
||||||
pub tx_hash: String,
|
|
||||||
pub tx_key: String,
|
|
||||||
pub tx_metadata: String,
|
|
||||||
pub unsigned_txset: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
|
||||||
pub struct BlockHeight {
|
|
||||||
pub height: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
|
||||||
struct CheckTxKeyParams {
|
|
||||||
#[serde(rename = "txid")]
|
|
||||||
tx_id: String,
|
|
||||||
tx_key: String,
|
|
||||||
address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
|
||||||
pub struct CheckTxKey {
|
|
||||||
pub confirmations: u64,
|
|
||||||
pub received: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub struct GenerateFromKeysParams {
|
|
||||||
pub restore_height: u32,
|
|
||||||
pub filename: String,
|
|
||||||
pub address: String,
|
|
||||||
pub spendkey: String,
|
|
||||||
pub viewkey: String,
|
|
||||||
pub password: String,
|
|
||||||
pub autosave_current: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct GenerateFromKeys {
|
|
||||||
pub address: String,
|
|
||||||
pub info: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
|
||||||
pub struct Refreshed {
|
|
||||||
pub blocks_fetched: u32,
|
|
||||||
pub received_money: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct SweepAllParams {
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct SweepAll {
|
|
||||||
amount_list: Vec<u64>,
|
|
||||||
fee_list: Vec<u64>,
|
|
||||||
multisig_txset: String,
|
|
||||||
pub tx_hash_list: Vec<String>,
|
|
||||||
unsigned_txset: String,
|
|
||||||
weight_list: Vec<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Deserialize)]
|
|
||||||
pub struct Version {
|
|
||||||
version: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_deserialize_sweep_all_response() {
|
|
||||||
let response = r#"{
|
|
||||||
"id": "0",
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"result": {
|
|
||||||
"amount_list": [29921410000],
|
|
||||||
"fee_list": [78590000],
|
|
||||||
"multisig_txset": "",
|
|
||||||
"tx_hash_list": ["c1d8cfa87d445c1915a59d67be3e93ba8a29018640cf69b465f07b1840a8f8c8"],
|
|
||||||
"unsigned_txset": "",
|
|
||||||
"weight_list": [1448]
|
|
||||||
}
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
let _: Response<SweepAll> = serde_json::from_str(&response).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
260
monero-rpc/src/wallet.rs
Normal file
260
monero-rpc/src/wallet.rs
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::{de::Error, Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
|
#[jsonrpc_client::api(version = "2.0")]
|
||||||
|
pub trait MoneroWalletRpc {
|
||||||
|
async fn get_address(&self, account_index: u32) -> GetAddress;
|
||||||
|
async fn get_balance(&self, account_index: u32) -> GetBalance;
|
||||||
|
async fn create_account(&self, label: String) -> CreateAccount;
|
||||||
|
async fn get_accounts(&self, tag: String) -> GetAccounts;
|
||||||
|
async fn open_wallet(&self, filename: String) -> WalletOpened;
|
||||||
|
async fn close_wallet(&self) -> WalletClosed;
|
||||||
|
async fn create_wallet(&self, filename: String, language: String) -> WalletCreated;
|
||||||
|
async fn transfer(
|
||||||
|
&self,
|
||||||
|
account_index: u32,
|
||||||
|
destinations: Vec<Destination>,
|
||||||
|
get_tx_key: bool,
|
||||||
|
) -> Transfer;
|
||||||
|
async fn get_height(&self) -> BlockHeight;
|
||||||
|
async fn check_tx_key(&self, txid: String, tx_key: String, address: String) -> CheckTxKey;
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn generate_from_keys(
|
||||||
|
&self,
|
||||||
|
filename: String,
|
||||||
|
address: String,
|
||||||
|
spendkey: String,
|
||||||
|
viewkey: String,
|
||||||
|
restore_height: u32,
|
||||||
|
password: String,
|
||||||
|
autosave_current: bool,
|
||||||
|
) -> GenerateFromKeys;
|
||||||
|
async fn refresh(&self) -> Refreshed;
|
||||||
|
async fn sweep_all(&self, address: String) -> SweepAll;
|
||||||
|
async fn get_version(&self) -> Version;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[jsonrpc_client::implement(MoneroWalletRpc)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
inner: reqwest::Client,
|
||||||
|
base_url: reqwest::Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
/// Constructs a monero-wallet-rpc client with localhost endpoint.
|
||||||
|
pub fn localhost(port: u16) -> Result<Self> {
|
||||||
|
Client::new(
|
||||||
|
format!("http://127.0.0.1:{}/json_rpc", port)
|
||||||
|
.parse()
|
||||||
|
.context("url is well formed")?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a monero-wallet-rpc client with `url` endpoint.
|
||||||
|
pub fn new(url: reqwest::Url) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: reqwest::ClientBuilder::new()
|
||||||
|
.connection_verbose(true)
|
||||||
|
.build()?,
|
||||||
|
base_url: url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transfers `amount` monero from `account_index` to `address`.
|
||||||
|
pub async fn transfer_single(
|
||||||
|
&self,
|
||||||
|
account_index: u32,
|
||||||
|
amount: u64,
|
||||||
|
address: &str,
|
||||||
|
) -> Result<Transfer> {
|
||||||
|
let dest = vec![Destination {
|
||||||
|
amount,
|
||||||
|
address: address.to_owned(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
Ok(self.transfer(account_index, dest, true).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct GetAddress {
|
||||||
|
pub address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone, Copy)]
|
||||||
|
pub struct GetBalance {
|
||||||
|
pub balance: u64,
|
||||||
|
pub blocks_to_unlock: u32,
|
||||||
|
pub multisig_import_needed: bool,
|
||||||
|
pub time_to_unlock: u32,
|
||||||
|
pub unlocked_balance: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct CreateAccount {
|
||||||
|
pub account_index: u32,
|
||||||
|
pub address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct GetAccounts {
|
||||||
|
pub subaddress_accounts: Vec<SubAddressAccount>,
|
||||||
|
pub total_balance: u64,
|
||||||
|
pub total_unlocked_balance: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct SubAddressAccount {
|
||||||
|
pub account_index: u32,
|
||||||
|
pub balance: u32,
|
||||||
|
pub base_address: String,
|
||||||
|
pub label: String,
|
||||||
|
pub tag: String,
|
||||||
|
pub unlocked_balance: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
pub struct Destination {
|
||||||
|
pub amount: u64,
|
||||||
|
pub address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct Transfer {
|
||||||
|
pub amount: u64,
|
||||||
|
pub fee: u64,
|
||||||
|
pub multisig_txset: String,
|
||||||
|
pub tx_blob: String,
|
||||||
|
pub tx_hash: String,
|
||||||
|
#[serde(deserialize_with = "opt_key_from_blank")]
|
||||||
|
pub tx_key: Option<monero::PrivateKey>,
|
||||||
|
pub tx_metadata: String,
|
||||||
|
pub unsigned_txset: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub struct BlockHeight {
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||||
|
#[serde(from = "CheckTxKeyResponse")]
|
||||||
|
pub struct CheckTxKey {
|
||||||
|
pub confirmations: u64,
|
||||||
|
pub received: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||||
|
struct CheckTxKeyResponse {
|
||||||
|
pub confirmations: u64,
|
||||||
|
pub received: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CheckTxKeyResponse> for CheckTxKey {
|
||||||
|
fn from(response: CheckTxKeyResponse) -> Self {
|
||||||
|
// Due to a bug in monerod that causes check_tx_key confirmations
|
||||||
|
// to overflow we safeguard the confirmations to avoid unwanted
|
||||||
|
// side effects.
|
||||||
|
let confirmations = if response.confirmations > u64::MAX - 1000 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
response.confirmations
|
||||||
|
};
|
||||||
|
|
||||||
|
CheckTxKey {
|
||||||
|
confirmations,
|
||||||
|
received: response.received,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct GenerateFromKeys {
|
||||||
|
pub address: String,
|
||||||
|
pub info: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||||
|
pub struct Refreshed {
|
||||||
|
pub blocks_fetched: u32,
|
||||||
|
pub received_money: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct SweepAll {
|
||||||
|
amount_list: Vec<u64>,
|
||||||
|
fee_list: Vec<u64>,
|
||||||
|
multisig_txset: String,
|
||||||
|
pub tx_hash_list: Vec<String>,
|
||||||
|
unsigned_txset: String,
|
||||||
|
weight_list: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||||
|
pub struct Version {
|
||||||
|
pub version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type WalletCreated = Empty;
|
||||||
|
pub type WalletClosed = Empty;
|
||||||
|
pub type WalletOpened = Empty;
|
||||||
|
|
||||||
|
/// Zero-sized struct to allow serde to deserialize an empty JSON object.
|
||||||
|
///
|
||||||
|
/// With `serde`, an empty JSON object (`{ }`) does not deserialize into Rust's
|
||||||
|
/// `()`. With the adoption of `jsonrpc_client`, we need to be explicit about
|
||||||
|
/// what the response of every RPC call is. Unfortunately, monerod likes to
|
||||||
|
/// return empty objects instead of `null`s in certain cases. We use this struct
|
||||||
|
/// to all the "deserialization" to happily continue.
|
||||||
|
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||||
|
pub struct Empty {}
|
||||||
|
|
||||||
|
fn opt_key_from_blank<'de, D>(deserializer: D) -> Result<Option<monero::PrivateKey>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let string = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
if string.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(string.parse().map_err(D::Error::custom)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use jsonrpc_client::Response;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_deserialize_sweep_all_response() {
|
||||||
|
let response = r#"{
|
||||||
|
"id": "0",
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"amount_list": [29921410000],
|
||||||
|
"fee_list": [78590000],
|
||||||
|
"multisig_txset": "",
|
||||||
|
"tx_hash_list": ["c1d8cfa87d445c1915a59d67be3e93ba8a29018640cf69b465f07b1840a8f8c8"],
|
||||||
|
"unsigned_txset": "",
|
||||||
|
"weight_list": [1448]
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let _: Response<SweepAll> = serde_json::from_str(&response).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_deserialize_create_wallet() {
|
||||||
|
let response = r#"{
|
||||||
|
"id": 0,
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let _: Response<WalletCreated> = serde_json::from_str(&response).unwrap();
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ use swap::env::{Config, GetConfig};
|
|||||||
use swap::network::quote::BidQuote;
|
use swap::network::quote::BidQuote;
|
||||||
use swap::network::swarm;
|
use swap::network::swarm;
|
||||||
use swap::protocol::bob;
|
use swap::protocol::bob;
|
||||||
use swap::protocol::bob::{Builder, EventLoop};
|
use swap::protocol::bob::{EventLoop, Swap};
|
||||||
use swap::seed::Seed;
|
use swap::seed::Seed;
|
||||||
use swap::{bitcoin, cli, env, monero};
|
use swap::{bitcoin, cli, env, monero};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
@ -105,17 +105,16 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
db.insert_peer_id(swap_id, alice_peer_id).await?;
|
db.insert_peer_id(swap_id, alice_peer_id).await?;
|
||||||
|
|
||||||
let swap = Builder::new(
|
let swap = Swap::new(
|
||||||
db,
|
db,
|
||||||
swap_id,
|
swap_id,
|
||||||
bitcoin_wallet.clone(),
|
bitcoin_wallet,
|
||||||
Arc::new(monero_wallet),
|
Arc::new(monero_wallet),
|
||||||
env_config,
|
env_config,
|
||||||
event_loop_handle,
|
event_loop_handle,
|
||||||
receive_monero_address,
|
receive_monero_address,
|
||||||
)
|
send_bitcoin,
|
||||||
.with_init_params(send_bitcoin)
|
);
|
||||||
.build()?;
|
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
result = event_loop => {
|
result = event_loop => {
|
||||||
@ -184,16 +183,15 @@ async fn main() -> Result<()> {
|
|||||||
EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?;
|
EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?;
|
||||||
let handle = tokio::spawn(event_loop.run());
|
let handle = tokio::spawn(event_loop.run());
|
||||||
|
|
||||||
let swap = Builder::new(
|
let swap = Swap::from_db(
|
||||||
db,
|
db,
|
||||||
swap_id,
|
swap_id,
|
||||||
bitcoin_wallet.clone(),
|
bitcoin_wallet,
|
||||||
Arc::new(monero_wallet),
|
Arc::new(monero_wallet),
|
||||||
env_config,
|
env_config,
|
||||||
event_loop_handle,
|
event_loop_handle,
|
||||||
receive_monero_address,
|
receive_monero_address,
|
||||||
)
|
)?;
|
||||||
.build()?;
|
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
event_loop_result = handle => {
|
event_loop_result = handle => {
|
||||||
|
@ -5,7 +5,7 @@ use crate::monero::{
|
|||||||
use ::monero::{Address, Network, PrivateKey, PublicKey};
|
use ::monero::{Address, Network, PrivateKey, PublicKey};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use monero_rpc::wallet;
|
use monero_rpc::wallet;
|
||||||
use monero_rpc::wallet::{BlockHeight, CheckTxKey, Refreshed};
|
use monero_rpc::wallet::{BlockHeight, CheckTxKey, MoneroWalletRpc as _, Refreshed};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@ -25,11 +25,11 @@ pub struct Wallet {
|
|||||||
impl Wallet {
|
impl Wallet {
|
||||||
/// Connect to a wallet RPC and load the given wallet by name.
|
/// Connect to a wallet RPC and load the given wallet by name.
|
||||||
pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result<Self> {
|
pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result<Self> {
|
||||||
let client = wallet::Client::new(url);
|
let client = wallet::Client::new(url)?;
|
||||||
|
|
||||||
let open_wallet_response = client.open_wallet(name.as_str()).await;
|
let open_wallet_response = client.open_wallet(name.clone()).await;
|
||||||
if open_wallet_response.is_err() {
|
if open_wallet_response.is_err() {
|
||||||
client.create_wallet(name.as_str()).await.context(
|
client.create_wallet(name.clone(), "English".to_owned()).await.context(
|
||||||
"Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available",
|
"Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@ -59,12 +59,12 @@ impl Wallet {
|
|||||||
self.inner
|
self.inner
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.open_wallet(self.name.as_str())
|
.open_wallet(self.name.clone())
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open(&self, filename: &str) -> Result<()> {
|
pub async fn open(&self, filename: String) -> Result<()> {
|
||||||
self.inner.lock().await.open_wallet(filename).await?;
|
self.inner.lock().await.open_wallet(filename).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@ impl Wallet {
|
|||||||
/// keys. The generated wallet will remain loaded.
|
/// keys. The generated wallet will remain loaded.
|
||||||
pub async fn create_from_and_load(
|
pub async fn create_from_and_load(
|
||||||
&self,
|
&self,
|
||||||
file_name: &str,
|
file_name: String,
|
||||||
private_spend_key: PrivateKey,
|
private_spend_key: PrivateKey,
|
||||||
private_view_key: PrivateViewKey,
|
private_view_key: PrivateViewKey,
|
||||||
restore_height: BlockHeight,
|
restore_height: BlockHeight,
|
||||||
@ -87,17 +87,23 @@ impl Wallet {
|
|||||||
|
|
||||||
// Properly close the wallet before generating the other wallet to ensure that
|
// Properly close the wallet before generating the other wallet to ensure that
|
||||||
// it saves its state correctly
|
// it saves its state correctly
|
||||||
let _ = wallet.close_wallet().await?;
|
let _ = wallet
|
||||||
|
.close_wallet()
|
||||||
|
.await
|
||||||
|
.context("Failed to close wallet")?;
|
||||||
|
|
||||||
let _ = wallet
|
let _ = wallet
|
||||||
.generate_from_keys(
|
.generate_from_keys(
|
||||||
file_name,
|
file_name,
|
||||||
&address.to_string(),
|
address.to_string(),
|
||||||
&private_spend_key.to_string(),
|
private_spend_key.to_string(),
|
||||||
&PrivateKey::from(private_view_key).to_string(),
|
PrivateKey::from(private_view_key).to_string(),
|
||||||
restore_height.height,
|
restore_height.height,
|
||||||
|
String::from(""),
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.context("Failed to generate new wallet from keys")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -108,7 +114,7 @@ impl Wallet {
|
|||||||
/// stored name.
|
/// stored name.
|
||||||
pub async fn create_from(
|
pub async fn create_from(
|
||||||
&self,
|
&self,
|
||||||
file_name: &str,
|
file_name: String,
|
||||||
private_spend_key: PrivateKey,
|
private_spend_key: PrivateKey,
|
||||||
private_view_key: PrivateViewKey,
|
private_view_key: PrivateViewKey,
|
||||||
restore_height: BlockHeight,
|
restore_height: BlockHeight,
|
||||||
@ -128,19 +134,18 @@ impl Wallet {
|
|||||||
let _ = wallet
|
let _ = wallet
|
||||||
.generate_from_keys(
|
.generate_from_keys(
|
||||||
file_name,
|
file_name,
|
||||||
&temp_wallet_address.to_string(),
|
temp_wallet_address.to_string(),
|
||||||
&private_spend_key.to_string(),
|
private_spend_key.to_string(),
|
||||||
&PrivateKey::from(private_view_key).to_string(),
|
PrivateKey::from(private_view_key).to_string(),
|
||||||
restore_height.height,
|
restore_height.height,
|
||||||
|
String::from(""),
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Try to send all the funds from the generated wallet to the default wallet
|
// Try to send all the funds from the generated wallet to the default wallet
|
||||||
match wallet.refresh().await {
|
match wallet.refresh().await {
|
||||||
Ok(_) => match wallet
|
Ok(_) => match wallet.sweep_all(self.main_address.to_string()).await {
|
||||||
.sweep_all(self.main_address.to_string().as_str())
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(sweep_all) => {
|
Ok(sweep_all) => {
|
||||||
for tx in sweep_all.tx_hash_list {
|
for tx in sweep_all.tx_hash_list {
|
||||||
tracing::info!(%tx, "Monero transferred back to default wallet {}", self.main_address);
|
tracing::info!(%tx, "Monero transferred back to default wallet {}", self.main_address);
|
||||||
@ -159,7 +164,7 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = wallet.open_wallet(self.name.as_str()).await?;
|
let _ = wallet.open_wallet(self.name.clone()).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -178,7 +183,7 @@ impl Wallet {
|
|||||||
.inner
|
.inner
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.transfer(0, amount.as_piconero(), &destination_address.to_string())
|
.transfer_single(0, amount.as_piconero(), &destination_address.to_string())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@ -190,7 +195,8 @@ impl Wallet {
|
|||||||
|
|
||||||
Ok(TransferProof::new(
|
Ok(TransferProof::new(
|
||||||
TxHash(res.tx_hash),
|
TxHash(res.tx_hash),
|
||||||
PrivateKey::from_str(&res.tx_key)?,
|
res.tx_key
|
||||||
|
.context("Missing tx_key in `transfer` response")?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,16 +216,20 @@ impl Wallet {
|
|||||||
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||||
|
|
||||||
let check_interval = tokio::time::interval(self.sync_interval);
|
let check_interval = tokio::time::interval(self.sync_interval);
|
||||||
let key = &transfer_proof.tx_key().to_string();
|
let key = transfer_proof.tx_key().to_string();
|
||||||
|
|
||||||
wait_for_confirmations(
|
wait_for_confirmations(
|
||||||
txid.0,
|
txid.0,
|
||||||
|txid| async move {
|
move |txid| {
|
||||||
self.inner
|
let key = key.clone();
|
||||||
.lock()
|
async move {
|
||||||
.await
|
Ok(self
|
||||||
.check_tx_key(&txid, &key, &address.to_string())
|
.inner
|
||||||
.await
|
.lock()
|
||||||
|
.await
|
||||||
|
.check_tx_key(txid, key, address.to_string())
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
check_interval,
|
check_interval,
|
||||||
expected,
|
expected,
|
||||||
@ -235,7 +245,7 @@ impl Wallet {
|
|||||||
.inner
|
.inner
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.sweep_all(address.to_string().as_str())
|
.sweep_all(address.to_string())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let tx_hashes = sweep_all.tx_hash_list.into_iter().map(TxHash).collect();
|
let tx_hashes = sweep_all.tx_hash_list.into_iter().map(TxHash).collect();
|
||||||
@ -244,13 +254,13 @@ impl Wallet {
|
|||||||
|
|
||||||
/// Get the balance of the primary account.
|
/// Get the balance of the primary account.
|
||||||
pub async fn get_balance(&self) -> Result<Amount> {
|
pub async fn get_balance(&self) -> Result<Amount> {
|
||||||
let amount = self.inner.lock().await.get_balance(0).await?;
|
let amount = self.inner.lock().await.get_balance(0).await?.balance;
|
||||||
|
|
||||||
Ok(Amount::from_piconero(amount))
|
Ok(Amount::from_piconero(amount))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn block_height(&self) -> Result<BlockHeight> {
|
pub async fn block_height(&self) -> Result<BlockHeight> {
|
||||||
self.inner.lock().await.block_height().await
|
Ok(self.inner.lock().await.get_height().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_main_address(&self) -> Address {
|
pub fn get_main_address(&self) -> Address {
|
||||||
@ -258,7 +268,7 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(&self) -> Result<Refreshed> {
|
pub async fn refresh(&self) -> Result<Refreshed> {
|
||||||
self.inner.lock().await.refresh().await
|
Ok(self.inner.lock().await.refresh().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn static_tx_fee_estimate(&self) -> Amount {
|
pub fn static_tx_fee_estimate(&self) -> Amount {
|
||||||
|
@ -2,7 +2,7 @@ use ::monero::Network;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use big_bytes::BigByte;
|
use big_bytes::BigByte;
|
||||||
use futures::{StreamExt, TryStreamExt};
|
use futures::{StreamExt, TryStreamExt};
|
||||||
use monero_rpc::wallet::Client;
|
use monero_rpc::wallet::{Client, MoneroWalletRpc as _};
|
||||||
use reqwest::header::CONTENT_LENGTH;
|
use reqwest::header::CONTENT_LENGTH;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
@ -165,7 +165,7 @@ impl WalletRpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send a json rpc request to make sure monero_wallet_rpc is ready
|
// Send a json rpc request to make sure monero_wallet_rpc is ready
|
||||||
Client::localhost(port).get_version().await?;
|
Client::localhost(port)?.get_version().await?;
|
||||||
|
|
||||||
Ok(WalletRpcProcess {
|
Ok(WalletRpcProcess {
|
||||||
_child: child,
|
_child: child,
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
//! Alice holds XMR and wishes receive BTC.
|
//! Alice holds XMR and wishes receive BTC.
|
||||||
use crate::bitcoin::ExpiredTimelocks;
|
use crate::bitcoin::ExpiredTimelocks;
|
||||||
use crate::env::Config;
|
use crate::env::Config;
|
||||||
use crate::protocol::alice;
|
|
||||||
use crate::protocol::alice::event_loop::EventLoopHandle;
|
use crate::protocol::alice::event_loop::EventLoopHandle;
|
||||||
use crate::protocol::alice::AliceState;
|
use crate::protocol::alice::{AliceState, Swap};
|
||||||
use crate::{bitcoin, database, monero};
|
use crate::{bitcoin, database, monero};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
@ -12,15 +11,12 @@ use tokio::time::timeout;
|
|||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub async fn run(swap: alice::Swap) -> Result<AliceState> {
|
pub async fn run(swap: Swap) -> Result<AliceState> {
|
||||||
run_until(swap, |_| false).await
|
run_until(swap, |_| false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(name = "swap", skip(swap,exit_early), fields(id = %swap.swap_id), err)]
|
#[tracing::instrument(name = "swap", skip(swap,exit_early), fields(id = %swap.swap_id), err)]
|
||||||
pub async fn run_until(
|
pub async fn run_until(mut swap: Swap, exit_early: fn(&AliceState) -> bool) -> Result<AliceState> {
|
||||||
mut swap: alice::Swap,
|
|
||||||
exit_early: fn(&AliceState) -> bool,
|
|
||||||
) -> Result<AliceState> {
|
|
||||||
let mut current_state = swap.state;
|
let mut current_state = swap.state;
|
||||||
|
|
||||||
while !is_complete(¤t_state) && !exit_early(¤t_state) {
|
while !is_complete(¤t_state) && !exit_early(¤t_state) {
|
||||||
@ -104,7 +100,13 @@ async fn next_state(
|
|||||||
ExpiredTimelocks::None => {
|
ExpiredTimelocks::None => {
|
||||||
monero_wallet
|
monero_wallet
|
||||||
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1))
|
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1))
|
||||||
.await?;
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to watch for transfer of XMR in transaction {}",
|
||||||
|
transfer_proof.tx_hash()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
AliceState::XmrLocked {
|
AliceState::XmrLocked {
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
@ -299,7 +301,7 @@ async fn next_state(
|
|||||||
|
|
||||||
monero_wallet
|
monero_wallet
|
||||||
.create_from(
|
.create_from(
|
||||||
&swap_id.to_string(),
|
swap_id.to_string(),
|
||||||
spend_key,
|
spend_key,
|
||||||
view_key,
|
view_key,
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::env::Config;
|
use crate::{bitcoin, env, monero};
|
||||||
use crate::network::quote::BidQuote;
|
use anyhow::Result;
|
||||||
use crate::network::{encrypted_signature, quote, redial, spot_price, transfer_proof};
|
|
||||||
use crate::protocol::bob;
|
|
||||||
use crate::{bitcoin, monero};
|
|
||||||
use anyhow::{anyhow, Error, Result};
|
|
||||||
use libp2p::core::Multiaddr;
|
|
||||||
use libp2p::request_response::{RequestId, ResponseChannel};
|
|
||||||
use libp2p::{NetworkBehaviour, PeerId};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub use self::behaviour::{Behaviour, OutEvent};
|
||||||
pub use self::cancel::cancel;
|
pub use self::cancel::cancel;
|
||||||
pub use self::event_loop::{EventLoop, EventLoopHandle};
|
pub use self::event_loop::{EventLoop, EventLoopHandle};
|
||||||
pub use self::refund::refund;
|
pub use self::refund::refund;
|
||||||
pub use self::state::*;
|
pub use self::state::*;
|
||||||
pub use self::swap::{run, run_until};
|
pub use self::swap::{run, run_until};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
|
mod behaviour;
|
||||||
pub mod cancel;
|
pub mod cancel;
|
||||||
pub mod event_loop;
|
pub mod event_loop;
|
||||||
mod execution_setup;
|
mod execution_setup;
|
||||||
@ -27,160 +21,59 @@ pub mod swap;
|
|||||||
|
|
||||||
pub struct Swap {
|
pub struct Swap {
|
||||||
pub state: BobState,
|
pub state: BobState,
|
||||||
pub event_loop_handle: bob::EventLoopHandle,
|
pub event_loop_handle: EventLoopHandle,
|
||||||
pub db: Database,
|
pub db: Database,
|
||||||
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
|
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
pub monero_wallet: Arc<monero::Wallet>,
|
pub monero_wallet: Arc<monero::Wallet>,
|
||||||
pub env_config: Config,
|
pub env_config: env::Config,
|
||||||
pub swap_id: Uuid,
|
pub id: Uuid,
|
||||||
pub receive_monero_address: ::monero::Address,
|
pub receive_monero_address: monero::Address,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Builder {
|
impl Swap {
|
||||||
swap_id: Uuid,
|
|
||||||
db: Database,
|
|
||||||
|
|
||||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
|
||||||
monero_wallet: Arc<monero::Wallet>,
|
|
||||||
|
|
||||||
init_params: InitParams,
|
|
||||||
env_config: Config,
|
|
||||||
|
|
||||||
event_loop_handle: EventLoopHandle,
|
|
||||||
|
|
||||||
receive_monero_address: ::monero::Address,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum InitParams {
|
|
||||||
None,
|
|
||||||
New { btc_amount: bitcoin::Amount },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Builder {
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
db: Database,
|
db: Database,
|
||||||
swap_id: Uuid,
|
id: Uuid,
|
||||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
monero_wallet: Arc<monero::Wallet>,
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
env_config: Config,
|
env_config: env::Config,
|
||||||
event_loop_handle: EventLoopHandle,
|
event_loop_handle: EventLoopHandle,
|
||||||
receive_monero_address: ::monero::Address,
|
receive_monero_address: monero::Address,
|
||||||
|
btc_amount: bitcoin::Amount,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
swap_id,
|
state: BobState::Started { btc_amount },
|
||||||
|
event_loop_handle,
|
||||||
db,
|
db,
|
||||||
bitcoin_wallet,
|
bitcoin_wallet,
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
init_params: InitParams::None,
|
|
||||||
env_config,
|
env_config,
|
||||||
event_loop_handle,
|
id,
|
||||||
receive_monero_address,
|
receive_monero_address,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_init_params(self, btc_amount: bitcoin::Amount) -> Self {
|
pub fn from_db(
|
||||||
Self {
|
db: Database,
|
||||||
init_params: InitParams::New { btc_amount },
|
id: Uuid,
|
||||||
..self
|
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
}
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
}
|
env_config: env::Config,
|
||||||
|
event_loop_handle: EventLoopHandle,
|
||||||
|
receive_monero_address: monero::Address,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let state = db.get_state(id)?.try_into_bob()?.into();
|
||||||
|
|
||||||
pub fn build(self) -> Result<bob::Swap> {
|
Ok(Self {
|
||||||
let state = match self.init_params {
|
|
||||||
InitParams::New { btc_amount } => BobState::Started { btc_amount },
|
|
||||||
InitParams::None => self.db.get_state(self.swap_id)?.try_into_bob()?.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Swap {
|
|
||||||
state,
|
state,
|
||||||
event_loop_handle: self.event_loop_handle,
|
event_loop_handle,
|
||||||
db: self.db,
|
db,
|
||||||
bitcoin_wallet: self.bitcoin_wallet.clone(),
|
bitcoin_wallet,
|
||||||
monero_wallet: self.monero_wallet.clone(),
|
monero_wallet,
|
||||||
swap_id: self.swap_id,
|
env_config,
|
||||||
env_config: self.env_config,
|
id,
|
||||||
receive_monero_address: self.receive_monero_address,
|
receive_monero_address,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum OutEvent {
|
|
||||||
QuoteReceived {
|
|
||||||
id: RequestId,
|
|
||||||
response: BidQuote,
|
|
||||||
},
|
|
||||||
SpotPriceReceived {
|
|
||||||
id: RequestId,
|
|
||||||
response: spot_price::Response,
|
|
||||||
},
|
|
||||||
ExecutionSetupDone(Box<Result<State2>>),
|
|
||||||
TransferProofReceived {
|
|
||||||
msg: Box<transfer_proof::Request>,
|
|
||||||
channel: ResponseChannel<()>,
|
|
||||||
},
|
|
||||||
EncryptedSignatureAcknowledged {
|
|
||||||
id: RequestId,
|
|
||||||
},
|
|
||||||
AllRedialAttemptsExhausted {
|
|
||||||
peer: PeerId,
|
|
||||||
},
|
|
||||||
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 Bob.
|
|
||||||
#[derive(NetworkBehaviour)]
|
|
||||||
#[behaviour(out_event = "OutEvent", event_process = false)]
|
|
||||||
#[allow(missing_debug_implementations)]
|
|
||||||
pub struct Behaviour {
|
|
||||||
pub quote: quote::Behaviour,
|
|
||||||
pub spot_price: spot_price::Behaviour,
|
|
||||||
pub execution_setup: execution_setup::Behaviour,
|
|
||||||
pub transfer_proof: transfer_proof::Behaviour,
|
|
||||||
pub encrypted_signature: encrypted_signature::Behaviour,
|
|
||||||
pub redial: redial::Behaviour,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Behaviour {
|
|
||||||
pub fn new(alice: PeerId) -> Self {
|
|
||||||
Self {
|
|
||||||
quote: quote::bob(),
|
|
||||||
spot_price: spot_price::bob(),
|
|
||||||
execution_setup: Default::default(),
|
|
||||||
transfer_proof: transfer_proof::bob(),
|
|
||||||
encrypted_signature: encrypted_signature::bob(),
|
|
||||||
redial: redial::Behaviour::new(alice, Duration::from_secs(2)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a known address for the given peer
|
|
||||||
pub fn add_address(&mut self, peer_id: PeerId, address: Multiaddr) {
|
|
||||||
self.quote.add_address(&peer_id, address.clone());
|
|
||||||
self.spot_price.add_address(&peer_id, address.clone());
|
|
||||||
self.transfer_proof.add_address(&peer_id, address.clone());
|
|
||||||
self.encrypted_signature.add_address(&peer_id, address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
88
swap/src/protocol/bob/behaviour.rs
Normal file
88
swap/src/protocol/bob/behaviour.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use crate::network::quote::BidQuote;
|
||||||
|
use crate::network::{encrypted_signature, quote, redial, spot_price, transfer_proof};
|
||||||
|
use crate::protocol::bob::{execution_setup, State2};
|
||||||
|
use anyhow::{anyhow, Error, Result};
|
||||||
|
use libp2p::core::Multiaddr;
|
||||||
|
use libp2p::request_response::{RequestId, ResponseChannel};
|
||||||
|
use libp2p::{NetworkBehaviour, PeerId};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OutEvent {
|
||||||
|
QuoteReceived {
|
||||||
|
id: RequestId,
|
||||||
|
response: BidQuote,
|
||||||
|
},
|
||||||
|
SpotPriceReceived {
|
||||||
|
id: RequestId,
|
||||||
|
response: spot_price::Response,
|
||||||
|
},
|
||||||
|
ExecutionSetupDone(Box<Result<State2>>),
|
||||||
|
TransferProofReceived {
|
||||||
|
msg: Box<transfer_proof::Request>,
|
||||||
|
channel: ResponseChannel<()>,
|
||||||
|
},
|
||||||
|
EncryptedSignatureAcknowledged {
|
||||||
|
id: RequestId,
|
||||||
|
},
|
||||||
|
AllRedialAttemptsExhausted {
|
||||||
|
peer: PeerId,
|
||||||
|
},
|
||||||
|
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 Bob.
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
#[behaviour(out_event = "OutEvent", event_process = false)]
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct Behaviour {
|
||||||
|
pub quote: quote::Behaviour,
|
||||||
|
pub spot_price: spot_price::Behaviour,
|
||||||
|
pub execution_setup: execution_setup::Behaviour,
|
||||||
|
pub transfer_proof: transfer_proof::Behaviour,
|
||||||
|
pub encrypted_signature: encrypted_signature::Behaviour,
|
||||||
|
pub redial: redial::Behaviour,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Behaviour {
|
||||||
|
pub fn new(alice: PeerId) -> Self {
|
||||||
|
Self {
|
||||||
|
quote: quote::bob(),
|
||||||
|
spot_price: spot_price::bob(),
|
||||||
|
execution_setup: Default::default(),
|
||||||
|
transfer_proof: transfer_proof::bob(),
|
||||||
|
encrypted_signature: encrypted_signature::bob(),
|
||||||
|
redial: redial::Behaviour::new(alice, Duration::from_secs(2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a known address for the given peer
|
||||||
|
pub fn add_address(&mut self, peer_id: PeerId, address: Multiaddr) {
|
||||||
|
self.quote.add_address(&peer_id, address.clone());
|
||||||
|
self.spot_price.add_address(&peer_id, address.clone());
|
||||||
|
self.transfer_proof.add_address(&peer_id, address.clone());
|
||||||
|
self.encrypted_signature.add_address(&peer_id, address);
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,7 @@ pub async fn run_until(
|
|||||||
|
|
||||||
while !is_target_state(¤t_state) {
|
while !is_target_state(¤t_state) {
|
||||||
current_state = next_state(
|
current_state = next_state(
|
||||||
swap.swap_id,
|
swap.id,
|
||||||
current_state,
|
current_state,
|
||||||
&mut swap.event_loop_handle,
|
&mut swap.event_loop_handle,
|
||||||
swap.bitcoin_wallet.as_ref(),
|
swap.bitcoin_wallet.as_ref(),
|
||||||
@ -45,7 +45,7 @@ pub async fn run_until(
|
|||||||
|
|
||||||
let db_state = current_state.clone().into();
|
let db_state = current_state.clone().into();
|
||||||
swap.db
|
swap.db
|
||||||
.insert_latest_state(swap.swap_id, Swap::Bob(db_state))
|
.insert_latest_state(swap.id, Swap::Bob(db_state))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,21 +197,24 @@ async fn next_state(
|
|||||||
BobState::BtcRedeemed(state) => {
|
BobState::BtcRedeemed(state) => {
|
||||||
let (spend_key, view_key) = state.xmr_keys();
|
let (spend_key, view_key) = state.xmr_keys();
|
||||||
|
|
||||||
let generated_wallet_file_name = &swap_id.to_string();
|
let generated_wallet_file_name = swap_id.to_string();
|
||||||
if monero_wallet
|
if let Err(e) = monero_wallet
|
||||||
.create_from_and_load(
|
.create_from_and_load(
|
||||||
generated_wallet_file_name,
|
generated_wallet_file_name.clone(),
|
||||||
spend_key,
|
spend_key,
|
||||||
view_key,
|
view_key,
|
||||||
state.monero_wallet_restore_blockheight,
|
state.monero_wallet_restore_blockheight,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
|
||||||
{
|
{
|
||||||
// In case we failed to refresh/sweep, when resuming the wallet might already
|
// In case we failed to refresh/sweep, when resuming the wallet might already
|
||||||
// exist! This is a very unlikely scenario, but if we don't take care of it we
|
// exist! This is a very unlikely scenario, but if we don't take care of it we
|
||||||
// might not be able to ever transfer the Monero.
|
// might not be able to ever transfer the Monero.
|
||||||
tracing::warn!("Failed to generate monero wallet from keys, falling back to trying to open the the wallet if it already exists: {}", swap_id);
|
tracing::warn!("Failed to generate monero wallet from keys: {:#}", e);
|
||||||
|
tracing::info!(
|
||||||
|
"Falling back to trying to open the the wallet if it already exists: {}",
|
||||||
|
swap_id
|
||||||
|
);
|
||||||
monero_wallet.open(generated_wallet_file_name).await?;
|
monero_wallet.open(generated_wallet_file_name).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ use swap::protocol::{alice, bob};
|
|||||||
async fn alice_punishes_after_restart_if_punish_timelock_expired() {
|
async fn alice_punishes_after_restart_if_punish_timelock_expired() {
|
||||||
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.swap_id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
@ -9,7 +9,7 @@ use swap::protocol::{alice, bob};
|
|||||||
async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
|
async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
|
||||||
harness::setup_test(FastCancelConfig, |mut ctx| async move {
|
harness::setup_test(FastCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.swap_id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
@ -37,7 +37,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
|
|||||||
// Bob manually cancels
|
// Bob manually cancels
|
||||||
bob_join_handle.abort();
|
bob_join_handle.abort();
|
||||||
let (_, state) = bob::cancel(
|
let (_, state) = bob::cancel(
|
||||||
bob_swap.swap_id,
|
bob_swap.id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
@ -54,7 +54,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
|
|||||||
// Bob manually refunds
|
// Bob manually refunds
|
||||||
bob_join_handle.abort();
|
bob_join_handle.abort();
|
||||||
let bob_state = bob::refund(
|
let bob_state = bob::refund(
|
||||||
bob_swap.swap_id,
|
bob_swap.id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
|
@ -10,7 +10,7 @@ use swap::protocol::{alice, bob};
|
|||||||
async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
||||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.swap_id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
@ -26,7 +26,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
|||||||
|
|
||||||
// Bob tries but fails to manually cancel
|
// Bob tries but fails to manually cancel
|
||||||
let result = bob::cancel(
|
let result = bob::cancel(
|
||||||
bob_swap.swap_id,
|
bob_swap.id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
@ -45,7 +45,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
|||||||
|
|
||||||
// Bob tries but fails to manually refund
|
// Bob tries but fails to manually refund
|
||||||
bob::refund(
|
bob::refund(
|
||||||
bob_swap.swap_id,
|
bob_swap.id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
|
@ -9,7 +9,7 @@ use swap::protocol::{alice, bob};
|
|||||||
async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
|
async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
|
||||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.swap_id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
@ -25,7 +25,7 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
|
|||||||
|
|
||||||
// Bob forces a cancel that will fail
|
// Bob forces a cancel that will fail
|
||||||
let is_error = bob::cancel(
|
let is_error = bob::cancel(
|
||||||
bob_swap.swap_id,
|
bob_swap.id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
@ -43,7 +43,7 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
|
|||||||
|
|
||||||
// Bob forces a refund that will fail
|
// Bob forces a refund that will fail
|
||||||
let is_error = bob::refund(
|
let is_error = bob::refund(
|
||||||
bob_swap.swap_id,
|
bob_swap.id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
|
@ -11,7 +11,7 @@ async fn concurrent_bobs_after_xmr_lock_proof_sent() {
|
|||||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap_1, bob_join_handle_1) = ctx.bob_swap().await;
|
let (bob_swap_1, bob_join_handle_1) = ctx.bob_swap().await;
|
||||||
|
|
||||||
let swap_id = bob_swap_1.swap_id;
|
let swap_id = bob_swap_1.id;
|
||||||
|
|
||||||
let bob_swap_1 = tokio::spawn(bob::run_until(bob_swap_1, is_xmr_locked));
|
let bob_swap_1 = tokio::spawn(bob::run_until(bob_swap_1, is_xmr_locked));
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ async fn concurrent_bobs_before_xmr_lock_proof_sent() {
|
|||||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap_1, bob_join_handle_1) = ctx.bob_swap().await;
|
let (bob_swap_1, bob_join_handle_1) = ctx.bob_swap().await;
|
||||||
|
|
||||||
let swap_id = bob_swap_1.swap_id;
|
let swap_id = bob_swap_1.id;
|
||||||
|
|
||||||
let bob_swap_1 = tokio::spawn(bob::run_until(bob_swap_1, is_btc_locked));
|
let bob_swap_1 = tokio::spawn(bob::run_until(bob_swap_1, is_btc_locked));
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ use swap::protocol::bob;
|
|||||||
async fn ensure_same_swap_id_for_alice_and_bob() {
|
async fn ensure_same_swap_id_for_alice_and_bob() {
|
||||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap, _) = ctx.bob_swap().await;
|
let (bob_swap, _) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.swap_id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let _ = tokio::spawn(bob::run(bob_swap));
|
let _ = tokio::spawn(bob::run(bob_swap));
|
||||||
|
|
||||||
// once Bob's swap is spawned we can retrieve Alice's swap and assert on the
|
// once Bob's swap is spawned we can retrieve Alice's swap and assert on the
|
||||||
|
@ -4,8 +4,6 @@ use harness::SlowCancelConfig;
|
|||||||
use swap::protocol::{alice, bob};
|
use swap::protocol::{alice, bob};
|
||||||
use tokio::join;
|
use tokio::join;
|
||||||
|
|
||||||
/// Run the following tests with RUST_MIN_STACK=10000000
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn happy_path() {
|
async fn happy_path() {
|
||||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
|
@ -9,7 +9,7 @@ use swap::protocol::{alice, bob};
|
|||||||
async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
|
async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
|
||||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.swap_id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked));
|
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked));
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
@ -9,7 +9,7 @@ use swap::protocol::{alice, bob};
|
|||||||
async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
|
async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
|
||||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.swap_id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked));
|
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked));
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
mod bitcoind;
|
mod bitcoind;
|
||||||
mod electrs;
|
mod electrs;
|
||||||
|
|
||||||
use crate::harness;
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bitcoin_harness::{BitcoindRpcApi, Client};
|
use bitcoin_harness::{BitcoindRpcApi, Client};
|
||||||
@ -36,6 +35,293 @@ use tracing_subscriber::util::SubscriberInitExt;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub async fn setup_test<T, F, C>(_config: C, testfn: T)
|
||||||
|
where
|
||||||
|
T: Fn(TestContext) -> F,
|
||||||
|
F: Future<Output = Result<()>>,
|
||||||
|
C: GetConfig,
|
||||||
|
{
|
||||||
|
let cli = Cli::default();
|
||||||
|
|
||||||
|
let _guard = tracing_subscriber::fmt()
|
||||||
|
.with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=debug,bitcoin_harness=info,testcontainers=info") // add `reqwest::connect::verbose=trace` if you want to logs of the RPC clients
|
||||||
|
.with_test_writer()
|
||||||
|
.set_default();
|
||||||
|
|
||||||
|
let env_config = C::get_config();
|
||||||
|
|
||||||
|
let (monero, containers) = init_containers(&cli).await;
|
||||||
|
monero.init_miner().await.unwrap();
|
||||||
|
|
||||||
|
let btc_amount = bitcoin::Amount::from_sat(1_000_000);
|
||||||
|
let xmr_amount = monero::Amount::from_monero(btc_amount.as_btc() / FixedRate::RATE).unwrap();
|
||||||
|
|
||||||
|
let alice_starting_balances =
|
||||||
|
StartingBalances::new(bitcoin::Amount::ZERO, xmr_amount, Some(10));
|
||||||
|
|
||||||
|
let electrs_rpc_port = containers
|
||||||
|
.electrs
|
||||||
|
.get_host_port(electrs::RPC_PORT)
|
||||||
|
.expect("Could not map electrs rpc port");
|
||||||
|
|
||||||
|
let alice_seed = Seed::random().unwrap();
|
||||||
|
let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets(
|
||||||
|
MONERO_WALLET_NAME_ALICE,
|
||||||
|
containers.bitcoind_url.clone(),
|
||||||
|
&monero,
|
||||||
|
alice_starting_balances.clone(),
|
||||||
|
tempdir().unwrap().path(),
|
||||||
|
electrs_rpc_port,
|
||||||
|
&alice_seed,
|
||||||
|
env_config,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let alice_listen_port = get_port().expect("Failed to find a free port");
|
||||||
|
let alice_listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", alice_listen_port)
|
||||||
|
.parse()
|
||||||
|
.expect("failed to parse Alice's address");
|
||||||
|
|
||||||
|
let alice_db_path = tempdir().unwrap().into_path();
|
||||||
|
let (alice_handle, alice_swap_handle) = start_alice(
|
||||||
|
&alice_seed,
|
||||||
|
alice_db_path.clone(),
|
||||||
|
alice_listen_address.clone(),
|
||||||
|
env_config,
|
||||||
|
alice_bitcoin_wallet.clone(),
|
||||||
|
alice_monero_wallet.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let bob_seed = Seed::random().unwrap();
|
||||||
|
let bob_starting_balances = StartingBalances::new(btc_amount * 10, monero::Amount::ZERO, None);
|
||||||
|
|
||||||
|
let (bob_bitcoin_wallet, bob_monero_wallet) = init_test_wallets(
|
||||||
|
MONERO_WALLET_NAME_BOB,
|
||||||
|
containers.bitcoind_url,
|
||||||
|
&monero,
|
||||||
|
bob_starting_balances.clone(),
|
||||||
|
tempdir().unwrap().path(),
|
||||||
|
electrs_rpc_port,
|
||||||
|
&bob_seed,
|
||||||
|
env_config,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let bob_params = BobParams {
|
||||||
|
seed: Seed::random().unwrap(),
|
||||||
|
db_path: tempdir().unwrap().path().to_path_buf(),
|
||||||
|
bitcoin_wallet: bob_bitcoin_wallet.clone(),
|
||||||
|
monero_wallet: bob_monero_wallet.clone(),
|
||||||
|
alice_address: alice_listen_address.clone(),
|
||||||
|
alice_peer_id: alice_handle.peer_id,
|
||||||
|
env_config,
|
||||||
|
};
|
||||||
|
|
||||||
|
monero.start_miner().await.unwrap();
|
||||||
|
|
||||||
|
let test = TestContext {
|
||||||
|
env_config,
|
||||||
|
btc_amount,
|
||||||
|
xmr_amount,
|
||||||
|
alice_seed,
|
||||||
|
alice_db_path,
|
||||||
|
alice_listen_address,
|
||||||
|
alice_starting_balances,
|
||||||
|
alice_bitcoin_wallet,
|
||||||
|
alice_monero_wallet,
|
||||||
|
alice_swap_handle,
|
||||||
|
alice_handle,
|
||||||
|
bob_params,
|
||||||
|
bob_starting_balances,
|
||||||
|
bob_bitcoin_wallet,
|
||||||
|
bob_monero_wallet,
|
||||||
|
};
|
||||||
|
|
||||||
|
testfn(test).await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
|
||||||
|
let prefix = random_prefix();
|
||||||
|
let bitcoind_name = format!("{}_{}", prefix, "bitcoind");
|
||||||
|
let (bitcoind, bitcoind_url) =
|
||||||
|
init_bitcoind_container(&cli, prefix.clone(), bitcoind_name.clone(), prefix.clone())
|
||||||
|
.await
|
||||||
|
.expect("could not init bitcoind");
|
||||||
|
let electrs = init_electrs_container(&cli, prefix.clone(), bitcoind_name, prefix)
|
||||||
|
.await
|
||||||
|
.expect("could not init electrs");
|
||||||
|
let (monero, monerod_container, monero_wallet_rpc_containers) =
|
||||||
|
Monero::new(&cli, vec![MONERO_WALLET_NAME_ALICE, MONERO_WALLET_NAME_BOB])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(monero, Containers {
|
||||||
|
bitcoind_url,
|
||||||
|
bitcoind,
|
||||||
|
monerod_container,
|
||||||
|
monero_wallet_rpc_containers,
|
||||||
|
electrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_bitcoind_container(
|
||||||
|
cli: &Cli,
|
||||||
|
volume: String,
|
||||||
|
name: String,
|
||||||
|
network: String,
|
||||||
|
) -> Result<(Container<'_, Cli, bitcoind::Bitcoind>, Url)> {
|
||||||
|
let image = bitcoind::Bitcoind::default().with_volume(volume);
|
||||||
|
|
||||||
|
let run_args = RunArgs::default().with_name(name).with_network(network);
|
||||||
|
|
||||||
|
let docker = cli.run_with_args(image, run_args);
|
||||||
|
let a = docker
|
||||||
|
.get_host_port(bitcoind::RPC_PORT)
|
||||||
|
.context("Could not map bitcoind rpc port")?;
|
||||||
|
|
||||||
|
let bitcoind_url = {
|
||||||
|
let input = format!(
|
||||||
|
"http://{}:{}@localhost:{}",
|
||||||
|
bitcoind::RPC_USER,
|
||||||
|
bitcoind::RPC_PASSWORD,
|
||||||
|
a
|
||||||
|
);
|
||||||
|
Url::parse(&input).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
init_bitcoind(bitcoind_url.clone(), 5).await?;
|
||||||
|
|
||||||
|
Ok((docker, bitcoind_url.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_electrs_container(
|
||||||
|
cli: &Cli,
|
||||||
|
volume: String,
|
||||||
|
bitcoind_container_name: String,
|
||||||
|
network: String,
|
||||||
|
) -> Result<Container<'_, Cli, electrs::Electrs>> {
|
||||||
|
let bitcoind_rpc_addr = format!("{}:{}", bitcoind_container_name, bitcoind::RPC_PORT);
|
||||||
|
let image = electrs::Electrs::default()
|
||||||
|
.with_volume(volume)
|
||||||
|
.with_daemon_rpc_addr(bitcoind_rpc_addr)
|
||||||
|
.with_tag("latest");
|
||||||
|
|
||||||
|
let run_args = RunArgs::default().with_network(network);
|
||||||
|
|
||||||
|
let docker = cli.run_with_args(image, run_args);
|
||||||
|
|
||||||
|
Ok(docker)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_alice(
|
||||||
|
seed: &Seed,
|
||||||
|
db_path: PathBuf,
|
||||||
|
listen_address: Multiaddr,
|
||||||
|
env_config: Config,
|
||||||
|
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||||
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
|
) -> (AliceApplicationHandle, Receiver<alice::Swap>) {
|
||||||
|
let db = Arc::new(Database::open(db_path.as_path()).unwrap());
|
||||||
|
|
||||||
|
let mut swarm = swarm::alice(&seed).unwrap();
|
||||||
|
swarm.listen_on(listen_address).unwrap();
|
||||||
|
|
||||||
|
let (event_loop, swap_handle) = alice::EventLoop::new(
|
||||||
|
swarm,
|
||||||
|
env_config,
|
||||||
|
bitcoin_wallet,
|
||||||
|
monero_wallet,
|
||||||
|
db,
|
||||||
|
FixedRate::default(),
|
||||||
|
bitcoin::Amount::ONE_BTC,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let peer_id = event_loop.peer_id();
|
||||||
|
let handle = tokio::spawn(event_loop.run());
|
||||||
|
|
||||||
|
(AliceApplicationHandle { handle, peer_id }, swap_handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn init_test_wallets(
|
||||||
|
name: &str,
|
||||||
|
bitcoind_url: Url,
|
||||||
|
monero: &Monero,
|
||||||
|
starting_balances: StartingBalances,
|
||||||
|
datadir: &Path,
|
||||||
|
electrum_rpc_port: u16,
|
||||||
|
seed: &Seed,
|
||||||
|
env_config: Config,
|
||||||
|
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
||||||
|
monero
|
||||||
|
.init_wallet(
|
||||||
|
name,
|
||||||
|
starting_balances
|
||||||
|
.xmr_outputs
|
||||||
|
.into_iter()
|
||||||
|
.map(|amount| amount.as_piconero())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let xmr_wallet = swap::monero::Wallet::connect(
|
||||||
|
monero.wallet(name).unwrap().client().clone(),
|
||||||
|
name.to_string(),
|
||||||
|
env_config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let electrum_rpc_url = {
|
||||||
|
let input = format!("tcp://@localhost:{}", electrum_rpc_port);
|
||||||
|
Url::parse(&input).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let btc_wallet = swap::bitcoin::Wallet::new(
|
||||||
|
electrum_rpc_url,
|
||||||
|
datadir,
|
||||||
|
seed.derive_extended_private_key(env_config.bitcoin_network)
|
||||||
|
.expect("Could not create extended private key from seed"),
|
||||||
|
env_config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("could not init btc wallet");
|
||||||
|
|
||||||
|
if starting_balances.btc != bitcoin::Amount::ZERO {
|
||||||
|
mint(
|
||||||
|
bitcoind_url,
|
||||||
|
btc_wallet.new_address().await.unwrap(),
|
||||||
|
starting_balances.btc,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("could not mint btc starting balance");
|
||||||
|
|
||||||
|
let mut interval = interval(Duration::from_secs(1u64));
|
||||||
|
let mut retries = 0u8;
|
||||||
|
let max_retries = 30u8;
|
||||||
|
loop {
|
||||||
|
retries += 1;
|
||||||
|
btc_wallet.sync().await.unwrap();
|
||||||
|
|
||||||
|
let btc_balance = btc_wallet.balance().await.unwrap();
|
||||||
|
|
||||||
|
if btc_balance == starting_balances.btc {
|
||||||
|
break;
|
||||||
|
} else if retries == max_retries {
|
||||||
|
panic!(
|
||||||
|
"Bitcoin wallet initialization failed, reached max retries upon balance sync"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interval.tick().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(Arc::new(btc_wallet), Arc::new(xmr_wallet))
|
||||||
|
}
|
||||||
|
|
||||||
const MONERO_WALLET_NAME_BOB: &str = "bob";
|
const MONERO_WALLET_NAME_BOB: &str = "bob";
|
||||||
const MONERO_WALLET_NAME_ALICE: &str = "alice";
|
const MONERO_WALLET_NAME_ALICE: &str = "alice";
|
||||||
const BITCOIN_TEST_WALLET_NAME: &str = "testwallet";
|
const BITCOIN_TEST_WALLET_NAME: &str = "testwallet";
|
||||||
@ -97,22 +383,41 @@ struct BobParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BobParams {
|
impl BobParams {
|
||||||
pub async fn builder(
|
pub fn new_swap_from_db(&self, swap_id: Uuid) -> Result<(bob::Swap, bob::EventLoop)> {
|
||||||
&self,
|
let (event_loop, handle) = self.new_eventloop(swap_id)?;
|
||||||
event_loop_handle: bob::EventLoopHandle,
|
let db = Database::open(&self.db_path)?;
|
||||||
swap_id: Uuid,
|
|
||||||
) -> Result<bob::Builder> {
|
|
||||||
let receive_address = self.monero_wallet.get_main_address();
|
|
||||||
|
|
||||||
Ok(bob::Builder::new(
|
let swap = bob::Swap::from_db(
|
||||||
Database::open(&self.db_path.clone().as_path()).unwrap(),
|
db,
|
||||||
swap_id,
|
swap_id,
|
||||||
self.bitcoin_wallet.clone(),
|
self.bitcoin_wallet.clone(),
|
||||||
self.monero_wallet.clone(),
|
self.monero_wallet.clone(),
|
||||||
self.env_config,
|
self.env_config,
|
||||||
event_loop_handle,
|
handle,
|
||||||
receive_address,
|
self.monero_wallet.get_main_address(),
|
||||||
))
|
)?;
|
||||||
|
|
||||||
|
Ok((swap, event_loop))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_swap(&self, btc_amount: bitcoin::Amount) -> Result<(bob::Swap, bob::EventLoop)> {
|
||||||
|
let swap_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
let (event_loop, handle) = self.new_eventloop(swap_id)?;
|
||||||
|
let db = Database::open(&self.db_path)?;
|
||||||
|
|
||||||
|
let swap = bob::Swap::new(
|
||||||
|
db,
|
||||||
|
swap_id,
|
||||||
|
self.bitcoin_wallet.clone(),
|
||||||
|
self.monero_wallet.clone(),
|
||||||
|
self.env_config,
|
||||||
|
handle,
|
||||||
|
self.monero_wallet.get_main_address(),
|
||||||
|
btc_amount,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((swap, event_loop))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_eventloop(&self, swap_id: Uuid) -> Result<(bob::EventLoop, bob::EventLoopHandle)> {
|
pub fn new_eventloop(&self, swap_id: Uuid) -> Result<(bob::EventLoop, bob::EventLoopHandle)> {
|
||||||
@ -196,17 +501,7 @@ impl TestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn bob_swap(&mut self) -> (bob::Swap, BobApplicationHandle) {
|
pub async fn bob_swap(&mut self) -> (bob::Swap, BobApplicationHandle) {
|
||||||
let swap_id = Uuid::new_v4();
|
let (swap, event_loop) = self.bob_params.new_swap(self.btc_amount).unwrap();
|
||||||
let (event_loop, event_loop_handle) = self.bob_params.new_eventloop(swap_id).unwrap();
|
|
||||||
|
|
||||||
let swap = self
|
|
||||||
.bob_params
|
|
||||||
.builder(event_loop_handle, swap_id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.with_init_params(self.btc_amount)
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// ensure the wallet is up to date for concurrent swap tests
|
// ensure the wallet is up to date for concurrent swap tests
|
||||||
swap.bitcoin_wallet.sync().await.unwrap();
|
swap.bitcoin_wallet.sync().await.unwrap();
|
||||||
@ -223,15 +518,7 @@ impl TestContext {
|
|||||||
) -> (bob::Swap, BobApplicationHandle) {
|
) -> (bob::Swap, BobApplicationHandle) {
|
||||||
join_handle.abort();
|
join_handle.abort();
|
||||||
|
|
||||||
let (event_loop, event_loop_handle) = self.bob_params.new_eventloop(swap_id).unwrap();
|
let (swap, event_loop) = self.bob_params.new_swap_from_db(swap_id).unwrap();
|
||||||
|
|
||||||
let swap = self
|
|
||||||
.bob_params
|
|
||||||
.builder(event_loop_handle, swap_id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let join_handle = tokio::spawn(event_loop.run());
|
let join_handle = tokio::spawn(event_loop.run());
|
||||||
|
|
||||||
@ -528,141 +815,6 @@ impl Wallet for bitcoin::Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn setup_test<T, F, C>(_config: C, testfn: T)
|
|
||||||
where
|
|
||||||
T: Fn(TestContext) -> F,
|
|
||||||
F: Future<Output = Result<()>>,
|
|
||||||
C: GetConfig,
|
|
||||||
{
|
|
||||||
let cli = Cli::default();
|
|
||||||
|
|
||||||
let _guard = tracing_subscriber::fmt()
|
|
||||||
.with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=debug,bitcoin_harness=info,testcontainers=info")
|
|
||||||
.with_test_writer()
|
|
||||||
.set_default();
|
|
||||||
|
|
||||||
let env_config = C::get_config();
|
|
||||||
|
|
||||||
let (monero, containers) = harness::init_containers(&cli).await;
|
|
||||||
monero.init_miner().await.unwrap();
|
|
||||||
|
|
||||||
let btc_amount = bitcoin::Amount::from_sat(1_000_000);
|
|
||||||
let xmr_amount = monero::Amount::from_monero(btc_amount.as_btc() / FixedRate::RATE).unwrap();
|
|
||||||
|
|
||||||
let alice_starting_balances =
|
|
||||||
StartingBalances::new(bitcoin::Amount::ZERO, xmr_amount, Some(10));
|
|
||||||
|
|
||||||
let electrs_rpc_port = containers
|
|
||||||
.electrs
|
|
||||||
.get_host_port(harness::electrs::RPC_PORT)
|
|
||||||
.expect("Could not map electrs rpc port");
|
|
||||||
|
|
||||||
let alice_seed = Seed::random().unwrap();
|
|
||||||
let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets(
|
|
||||||
MONERO_WALLET_NAME_ALICE,
|
|
||||||
containers.bitcoind_url.clone(),
|
|
||||||
&monero,
|
|
||||||
alice_starting_balances.clone(),
|
|
||||||
tempdir().unwrap().path(),
|
|
||||||
electrs_rpc_port,
|
|
||||||
&alice_seed,
|
|
||||||
env_config,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let alice_listen_port = get_port().expect("Failed to find a free port");
|
|
||||||
let alice_listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", alice_listen_port)
|
|
||||||
.parse()
|
|
||||||
.expect("failed to parse Alice's address");
|
|
||||||
|
|
||||||
let alice_db_path = tempdir().unwrap().into_path();
|
|
||||||
let (alice_handle, alice_swap_handle) = start_alice(
|
|
||||||
&alice_seed,
|
|
||||||
alice_db_path.clone(),
|
|
||||||
alice_listen_address.clone(),
|
|
||||||
env_config,
|
|
||||||
alice_bitcoin_wallet.clone(),
|
|
||||||
alice_monero_wallet.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let bob_seed = Seed::random().unwrap();
|
|
||||||
let bob_starting_balances = StartingBalances::new(btc_amount * 10, monero::Amount::ZERO, None);
|
|
||||||
|
|
||||||
let (bob_bitcoin_wallet, bob_monero_wallet) = init_test_wallets(
|
|
||||||
MONERO_WALLET_NAME_BOB,
|
|
||||||
containers.bitcoind_url,
|
|
||||||
&monero,
|
|
||||||
bob_starting_balances.clone(),
|
|
||||||
tempdir().unwrap().path(),
|
|
||||||
electrs_rpc_port,
|
|
||||||
&bob_seed,
|
|
||||||
env_config,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let bob_params = BobParams {
|
|
||||||
seed: Seed::random().unwrap(),
|
|
||||||
db_path: tempdir().unwrap().path().to_path_buf(),
|
|
||||||
bitcoin_wallet: bob_bitcoin_wallet.clone(),
|
|
||||||
monero_wallet: bob_monero_wallet.clone(),
|
|
||||||
alice_address: alice_listen_address.clone(),
|
|
||||||
alice_peer_id: alice_handle.peer_id,
|
|
||||||
env_config,
|
|
||||||
};
|
|
||||||
|
|
||||||
monero.start_miner().await.unwrap();
|
|
||||||
|
|
||||||
let test = TestContext {
|
|
||||||
env_config,
|
|
||||||
btc_amount,
|
|
||||||
xmr_amount,
|
|
||||||
alice_seed,
|
|
||||||
alice_db_path,
|
|
||||||
alice_listen_address,
|
|
||||||
alice_starting_balances,
|
|
||||||
alice_bitcoin_wallet,
|
|
||||||
alice_monero_wallet,
|
|
||||||
alice_swap_handle,
|
|
||||||
alice_handle,
|
|
||||||
bob_params,
|
|
||||||
bob_starting_balances,
|
|
||||||
bob_bitcoin_wallet,
|
|
||||||
bob_monero_wallet,
|
|
||||||
};
|
|
||||||
|
|
||||||
testfn(test).await.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_alice(
|
|
||||||
seed: &Seed,
|
|
||||||
db_path: PathBuf,
|
|
||||||
listen_address: Multiaddr,
|
|
||||||
env_config: Config,
|
|
||||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
|
||||||
monero_wallet: Arc<monero::Wallet>,
|
|
||||||
) -> (AliceApplicationHandle, Receiver<alice::Swap>) {
|
|
||||||
let db = Arc::new(Database::open(db_path.as_path()).unwrap());
|
|
||||||
|
|
||||||
let mut swarm = swarm::alice(&seed).unwrap();
|
|
||||||
swarm.listen_on(listen_address).unwrap();
|
|
||||||
|
|
||||||
let (event_loop, swap_handle) = alice::EventLoop::new(
|
|
||||||
swarm,
|
|
||||||
env_config,
|
|
||||||
bitcoin_wallet,
|
|
||||||
monero_wallet,
|
|
||||||
db,
|
|
||||||
FixedRate::default(),
|
|
||||||
bitcoin::Amount::ONE_BTC,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let peer_id = event_loop.peer_id();
|
|
||||||
let handle = tokio::spawn(event_loop.run());
|
|
||||||
|
|
||||||
(AliceApplicationHandle { handle, peer_id }, swap_handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_prefix() -> String {
|
fn random_prefix() -> String {
|
||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
@ -677,78 +829,6 @@ fn random_prefix() -> String {
|
|||||||
chars
|
chars
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) {
|
|
||||||
let prefix = random_prefix();
|
|
||||||
let bitcoind_name = format!("{}_{}", prefix, "bitcoind");
|
|
||||||
let (bitcoind, bitcoind_url) =
|
|
||||||
init_bitcoind_container(&cli, prefix.clone(), bitcoind_name.clone(), prefix.clone())
|
|
||||||
.await
|
|
||||||
.expect("could not init bitcoind");
|
|
||||||
let electrs = init_electrs_container(&cli, prefix.clone(), bitcoind_name, prefix)
|
|
||||||
.await
|
|
||||||
.expect("could not init electrs");
|
|
||||||
let (monero, monerods) = init_monero_container(&cli).await;
|
|
||||||
(monero, Containers {
|
|
||||||
bitcoind_url,
|
|
||||||
bitcoind,
|
|
||||||
monerods,
|
|
||||||
electrs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn init_bitcoind_container(
|
|
||||||
cli: &Cli,
|
|
||||||
volume: String,
|
|
||||||
name: String,
|
|
||||||
network: String,
|
|
||||||
) -> Result<(Container<'_, Cli, bitcoind::Bitcoind>, Url)> {
|
|
||||||
let image = bitcoind::Bitcoind::default().with_volume(volume);
|
|
||||||
|
|
||||||
let run_args = RunArgs::default().with_name(name).with_network(network);
|
|
||||||
|
|
||||||
let docker = cli.run_with_args(image, run_args);
|
|
||||||
let a = docker
|
|
||||||
.get_host_port(harness::bitcoind::RPC_PORT)
|
|
||||||
.context("Could not map bitcoind rpc port")?;
|
|
||||||
|
|
||||||
let bitcoind_url = {
|
|
||||||
let input = format!(
|
|
||||||
"http://{}:{}@localhost:{}",
|
|
||||||
bitcoind::RPC_USER,
|
|
||||||
bitcoind::RPC_PASSWORD,
|
|
||||||
a
|
|
||||||
);
|
|
||||||
Url::parse(&input).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
init_bitcoind(bitcoind_url.clone(), 5).await?;
|
|
||||||
|
|
||||||
Ok((docker, bitcoind_url.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init_electrs_container(
|
|
||||||
cli: &Cli,
|
|
||||||
volume: String,
|
|
||||||
bitcoind_container_name: String,
|
|
||||||
network: String,
|
|
||||||
) -> Result<Container<'_, Cli, electrs::Electrs>> {
|
|
||||||
let bitcoind_rpc_addr = format!(
|
|
||||||
"{}:{}",
|
|
||||||
bitcoind_container_name,
|
|
||||||
harness::bitcoind::RPC_PORT
|
|
||||||
);
|
|
||||||
let image = electrs::Electrs::default()
|
|
||||||
.with_volume(volume)
|
|
||||||
.with_daemon_rpc_addr(bitcoind_rpc_addr)
|
|
||||||
.with_tag("latest");
|
|
||||||
|
|
||||||
let run_args = RunArgs::default().with_network(network);
|
|
||||||
|
|
||||||
let docker = cli.run_with_args(image, run_args);
|
|
||||||
|
|
||||||
Ok(docker)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mine(bitcoind_client: Client, reward_address: bitcoin::Address) -> Result<()> {
|
async fn mine(bitcoind_client: Client, reward_address: bitcoin::Address) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
@ -798,107 +878,13 @@ pub async fn mint(node_url: Url, address: bitcoin::Address, amount: bitcoin::Amo
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_monero_container(
|
|
||||||
cli: &Cli,
|
|
||||||
) -> (
|
|
||||||
Monero,
|
|
||||||
Vec<Container<'_, Cli, monero_harness::image::Monero>>,
|
|
||||||
) {
|
|
||||||
let (monero, monerods) = Monero::new(&cli, vec![
|
|
||||||
MONERO_WALLET_NAME_ALICE.to_string(),
|
|
||||||
MONERO_WALLET_NAME_BOB.to_string(),
|
|
||||||
])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
(monero, monerods)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn init_test_wallets(
|
|
||||||
name: &str,
|
|
||||||
bitcoind_url: Url,
|
|
||||||
monero: &Monero,
|
|
||||||
starting_balances: StartingBalances,
|
|
||||||
datadir: &Path,
|
|
||||||
electrum_rpc_port: u16,
|
|
||||||
seed: &Seed,
|
|
||||||
env_config: Config,
|
|
||||||
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
|
||||||
monero
|
|
||||||
.init_wallet(
|
|
||||||
name,
|
|
||||||
starting_balances
|
|
||||||
.xmr_outputs
|
|
||||||
.into_iter()
|
|
||||||
.map(|amount| amount.as_piconero())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let xmr_wallet = swap::monero::Wallet::connect(
|
|
||||||
monero.wallet(name).unwrap().client(),
|
|
||||||
name.to_string(),
|
|
||||||
env_config,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let electrum_rpc_url = {
|
|
||||||
let input = format!("tcp://@localhost:{}", electrum_rpc_port);
|
|
||||||
Url::parse(&input).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let btc_wallet = swap::bitcoin::Wallet::new(
|
|
||||||
electrum_rpc_url,
|
|
||||||
datadir,
|
|
||||||
seed.derive_extended_private_key(env_config.bitcoin_network)
|
|
||||||
.expect("Could not create extended private key from seed"),
|
|
||||||
env_config,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("could not init btc wallet");
|
|
||||||
|
|
||||||
if starting_balances.btc != bitcoin::Amount::ZERO {
|
|
||||||
mint(
|
|
||||||
bitcoind_url,
|
|
||||||
btc_wallet.new_address().await.unwrap(),
|
|
||||||
starting_balances.btc,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("could not mint btc starting balance");
|
|
||||||
|
|
||||||
let mut interval = interval(Duration::from_secs(1u64));
|
|
||||||
let mut retries = 0u8;
|
|
||||||
let max_retries = 30u8;
|
|
||||||
loop {
|
|
||||||
retries += 1;
|
|
||||||
btc_wallet.sync().await.unwrap();
|
|
||||||
|
|
||||||
let btc_balance = btc_wallet.balance().await.unwrap();
|
|
||||||
|
|
||||||
if btc_balance == starting_balances.btc {
|
|
||||||
break;
|
|
||||||
} else if retries == max_retries {
|
|
||||||
panic!(
|
|
||||||
"Bitcoin wallet initialization failed, reached max retries upon balance sync"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interval.tick().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(Arc::new(btc_wallet), Arc::new(xmr_wallet))
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is just to keep the containers alive
|
// This is just to keep the containers alive
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct Containers<'a> {
|
struct Containers<'a> {
|
||||||
bitcoind_url: Url,
|
bitcoind_url: Url,
|
||||||
bitcoind: Container<'a, Cli, bitcoind::Bitcoind>,
|
bitcoind: Container<'a, Cli, bitcoind::Bitcoind>,
|
||||||
monerods: Vec<Container<'a, Cli, image::Monero>>,
|
monerod_container: Container<'a, Cli, image::Monerod>,
|
||||||
|
monero_wallet_rpc_containers: Vec<Container<'a, Cli, image::MoneroWalletRpc>>,
|
||||||
electrs: Container<'a, Cli, electrs::Electrs>,
|
electrs: Container<'a, Cli, electrs::Electrs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ use swap::protocol::{alice, bob};
|
|||||||
async fn alice_punishes_if_bob_never_acts_after_fund() {
|
async fn alice_punishes_if_bob_never_acts_after_fund() {
|
||||||
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||||
let bob_swap_id = bob_swap.swap_id;
|
let bob_swap_id = bob_swap.id;
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
|
Loading…
Reference in New Issue
Block a user