From a4c8beef8a2d97c38be9239e81a3c3f67f7381c4 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Thu, 11 Sep 2025 19:19:46 +0200 Subject: [PATCH 01/24] add cooperative redeem key export to asb-controller, add XmrCooperativelyRedeemable state to Bob state machine --- Cargo.lock | 6 ++ Cargo.toml | 8 ++ monero-seed/Cargo.toml | 2 +- swap-controller-api/Cargo.toml | 4 + swap-controller-api/src/lib.rs | 19 ++++ swap-controller/Cargo.toml | 1 + swap-controller/src/cli.rs | 3 + swap-controller/src/main.rs | 17 +++- swap/Cargo.toml | 25 +++-- swap/src/asb/rpc/server.rs | 39 +++++++- swap/src/cli/cancel_and_refund.rs | 2 + swap/src/database/bob.rs | 21 ++++ swap/src/monero.rs | 13 +++ swap/src/protocol/bob/state.rs | 28 +++++- swap/src/protocol/bob/swap.rs | 157 +++++++++++++++--------------- 15 files changed, 249 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 228ceb8f5..fc2179f34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13447,6 +13447,7 @@ dependencies = [ "moka", "monero", "monero-harness", + "monero-oxide", "monero-rpc 0.1.0", "monero-rpc 0.1.0 (git+https://github.com/monero-oxide/monero-oxide)", "monero-rpc-pool", @@ -13546,6 +13547,7 @@ dependencies = [ "shell-words", "swap-controller-api", "tokio", + "uuid", ] [[package]] @@ -13553,10 +13555,14 @@ name = "swap-controller-api" version = "0.1.0" dependencies = [ "bitcoin 0.32.7", + "curve25519-dalek-ng", "jsonrpsee 0.25.1", + "monero", "serde", "serde_json", + "swap-serde", "tokio", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4e7b53755..9e2ba2296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,14 @@ bdk_electrum = { version = "0.23.0", default-features = false } bdk_wallet = "2.0.0" bitcoin = { version = "0.32", features = ["rand", "serde"] } +# Crypto +curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" } + +# Monero +monero-oxide = { git = "https://github.com/monero-oxide/monero-oxide", default-features = false, features = ["std"] } + +swap-serde = { path = "./swap-serde" } + anyhow = "1" backoff = { version = "0.4", features = ["futures", "tokio"] } futures = { version = "0.3", default-features = false, features = ["std"] } diff --git a/monero-seed/Cargo.toml b/monero-seed/Cargo.toml index 04b6466b4..4cf612160 100644 --- a/monero-seed/Cargo.toml +++ b/monero-seed/Cargo.toml @@ -24,7 +24,7 @@ curve25519-dalek = { version = "4", default-features = false, features = ["alloc [dev-dependencies] hex = { version = "0.4", default-features = false, features = ["std"] } -monero-oxide = { git = "https://github.com/monero-oxide/monero-oxide", default-features = false, features = ["std"] } +monero-oxide = { workspace = true } [features] std = [ diff --git a/swap-controller-api/Cargo.toml b/swap-controller-api/Cargo.toml index c6bc7e764..5520623e6 100644 --- a/swap-controller-api/Cargo.toml +++ b/swap-controller-api/Cargo.toml @@ -8,6 +8,10 @@ bitcoin = { workspace = true } jsonrpsee = { workspace = true, features = ["macros", "server", "client-core", "http-client"] } serde = { workspace = true } serde_json = { workspace = true } +curve25519-dalek = { workspace = true } +monero = { workspace = true } +swap-serde = { workspace = true } +uuid = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["test-util"] } diff --git a/swap-controller-api/src/lib.rs b/swap-controller-api/src/lib.rs index 2e4523d6f..ad2a4fc6d 100644 --- a/swap-controller-api/src/lib.rs +++ b/swap-controller-api/src/lib.rs @@ -1,6 +1,8 @@ use jsonrpsee::proc_macros::rpc; use jsonrpsee::types::ErrorObjectOwned; +use monero::PrivateKey; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BitcoinBalanceResponse { @@ -45,6 +47,18 @@ pub struct MoneroSeedResponse { pub restore_height: u64, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CooperativeRedeemResponse { + /// Actual secret key + #[serde(with = "swap_serde::monero::private_key")] + pub inner: PrivateKey, + /// Monero lock tx id + pub lock_tx_id: String, + /// Monero lock tx key -> combined with tx id is the transfer proof + #[serde(with = "swap_serde::monero::private_key")] + pub lock_tx_key: PrivateKey, +} + #[rpc(client, server)] pub trait AsbApi { #[method(name = "check_connection")] @@ -65,4 +79,9 @@ pub trait AsbApi { async fn active_connections(&self) -> Result; #[method(name = "get_swaps")] async fn get_swaps(&self) -> Result, ErrorObjectOwned>; + #[method(name = "get_coop_redeem_key")] + async fn get_coop_redeem_info( + &self, + swap_id: Uuid, + ) -> Result, ErrorObjectOwned>; } diff --git a/swap-controller/Cargo.toml b/swap-controller/Cargo.toml index 5439f10d0..e97564468 100644 --- a/swap-controller/Cargo.toml +++ b/swap-controller/Cargo.toml @@ -17,6 +17,7 @@ serde_json = { workspace = true } shell-words = "1.1" swap-controller-api = { path = "../swap-controller-api" } tokio = { workspace = true } +uuid = { workspace = true, features = ["serde"] } [lints] workspace = true diff --git a/swap-controller/src/cli.rs b/swap-controller/src/cli.rs index 5aca11289..a447b995e 100644 --- a/swap-controller/src/cli.rs +++ b/swap-controller/src/cli.rs @@ -1,4 +1,5 @@ use clap::{Parser, Subcommand}; +use uuid::Uuid; #[derive(Parser)] #[command(name = "asb-controller")] @@ -33,4 +34,6 @@ pub enum Cmd { ActiveConnections, /// Get list of swaps GetSwaps, + /// Get the secret key needed for manual cooperative redeem + CooperativeRedeemKey { swap_id: Uuid }, } diff --git a/swap-controller/src/main.rs b/swap-controller/src/main.rs index 9124ddc1d..75d8e0522 100644 --- a/swap-controller/src/main.rs +++ b/swap-controller/src/main.rs @@ -82,7 +82,22 @@ async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> { } Cmd::BitcoinSeed => { let response = client.bitcoin_seed().await?; - println!("Descriptor (BIP-0382) containing the private keys of the internal Bitcoin wallet: \n{}", response.descriptor); + println!("Descriptor (BIP-0382) containing the private keys of the internal Bitcoin wallet:\n{}", response.descriptor); + } + Cmd::CooperativeRedeemKey { swap_id } => { + let response = client.get_coop_redeem_info(swap_id.clone()).await?; + + let Some(response) = response else { + println!("Couldn't find any swap with id {swap_id} in the database"); + return Ok(()); + }; + + println!("Cooperative redeem key:"); + println!("{}", response.inner); + println!("Monero lock transaction id:"); + println!("{}", response.lock_tx_id); + println!("Monero lock transaction ley:"); + println!("{}", response.lock_tx_key); } } Ok(()) diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 40871cf5c..dd672c5bf 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -19,6 +19,19 @@ bdk_core = { workspace = true } bdk_electrum = { workspace = true, features = ["use-rustls-ring"] } bdk_wallet = { workspace = true, features = ["rusqlite", "test-utils"] } +# Monero +monero-oxide = { workspace = true } +monero = { workspace = true } +monero-rpc = { path = "../monero-rpc" } +monero-rpc-pool = { path = "../monero-rpc-pool" } +monero-seed = { version = "0.1.0", path = "../monero-seed" } +monero-sys = { path = "../monero-sys" } + +# monero-oxide +monero-oxide-rpc = { git = "https://github.com/monero-oxide/monero-oxide.git", package = "monero-rpc" } +monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide.git" } + + anyhow = { workspace = true } arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls", "onion-service-service"] } async-compression = { version = "0.3", features = ["bzip2", "tokio"] } @@ -33,7 +46,7 @@ bmrng = "0.5.2" comfy-table = "7.1" config = { version = "0.14", default-features = false, features = ["toml"] } conquer-once = "0.4" -curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" } +curve25519-dalek = { workspace = true } data-encoding = "2.6" derive_builder = "0.20.2" dfx-swiss-sdk = { git = "https://github.com/eigenwallet/dfx-swiss-rs", optional = true } @@ -48,11 +61,6 @@ jsonrpsee = { workspace = true, features = ["server"] } libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] } libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } moka = { version = "0.12", features = ["sync", "future"] } -monero = { workspace = true } -monero-rpc = { path = "../monero-rpc" } -monero-rpc-pool = { path = "../monero-rpc-pool" } -monero-seed = { version = "0.1.0", path = "../monero-seed" } -monero-sys = { path = "../monero-sys" } once_cell = { workspace = true } pem = "3.0" proptest = "1" @@ -77,7 +85,7 @@ swap-controller-api = { path = "../swap-controller-api" } swap-env = { path = "../swap-env" } swap-feed = { path = "../swap-feed" } swap-fs = { path = "../swap-fs" } -swap-serde = { path = "../swap-serde" } +swap-serde = { workspace = true } tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false } thiserror = { workspace = true } throttle = { path = "../throttle" } @@ -98,9 +106,6 @@ uuid = { workspace = true, features = ["serde"] } void = "1" zeroize = "1.8.1" -# monero-oxide -monero-oxide-rpc = { git = "https://github.com/monero-oxide/monero-oxide.git", package = "monero-rpc" } -monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide.git" } [target.'cfg(not(windows))'.dependencies] tokio-tar = "0.3" diff --git a/swap/src/asb/rpc/server.rs b/swap/src/asb/rpc/server.rs index 2b48d000e..63594c15d 100644 --- a/swap/src/asb/rpc/server.rs +++ b/swap/src/asb/rpc/server.rs @@ -1,16 +1,20 @@ use crate::asb::event_loop::EventLoopService; -use crate::protocol::Database; +use crate::protocol::alice::AliceState; +use crate::protocol::{Database, State}; use crate::{bitcoin, monero}; -use anyhow::{Context, Result}; +use ::monero::PrivateKey; +use anyhow::{anyhow, Context, Result}; use jsonrpsee::server::{ServerBuilder, ServerHandle}; use jsonrpsee::types::error::ErrorCode; use jsonrpsee::types::ErrorObjectOwned; use std::sync::Arc; use swap_controller_api::{ ActiveConnectionsResponse, AsbApiServer, BitcoinBalanceResponse, BitcoinSeedResponse, - MoneroAddressResponse, MoneroBalanceResponse, MoneroSeedResponse, MultiaddressesResponse, Swap, + CooperativeRedeemResponse, MoneroAddressResponse, MoneroBalanceResponse, MoneroSeedResponse, + MultiaddressesResponse, Swap, }; use tokio_util::task::AbortOnDropHandle; +use uuid::Uuid; pub struct RpcServer { handle: ServerHandle, @@ -158,6 +162,35 @@ impl AsbApiServer for RpcImpl { Ok(swaps) } + + async fn get_coop_redeem_info( + &self, + swap_id: Uuid, + ) -> Result, ErrorObjectOwned> { + let states = self.db.get_states(swap_id).await.into_json_rpc_result()?; + + if states.is_empty() { + return Ok(None); + } + + states + .into_iter() + .find_map(|state| match state { + // Todo: maybe also allow XmrLockTransactionSent + State::Alice(AliceState::XmrLocked { + transfer_proof, + state3, + .. + }) => Some(Some(CooperativeRedeemResponse { + inner: PrivateKey::from_scalar(state3.s_a), + lock_tx_id: transfer_proof.tx_hash().to_string(), + lock_tx_key: transfer_proof.tx_key(), + })), + _ => None, + }) + .context("swap not cooperatively redeemable because we didn't lock the Monero") + .into_json_rpc_result() + } } trait IntoJsonRpcResult { diff --git a/swap/src/cli/cancel_and_refund.rs b/swap/src/cli/cancel_and_refund.rs index 00ab99253..e1c1bc3d0 100644 --- a/swap/src/cli/cancel_and_refund.rs +++ b/swap/src/cli/cancel_and_refund.rs @@ -71,6 +71,7 @@ pub async fn cancel( | BobState::BtcRedeemed(_) | BobState::XmrRedeemed { .. } | BobState::BtcPunished { .. } + | BobState::XmrCooperativelyRedeemable { .. } | BobState::BtcEarlyRefunded { .. } | BobState::SafelyAborted => bail!( "Cannot cancel swap {} because it is in state {} which is not cancellable.", @@ -175,6 +176,7 @@ pub async fn refund( | BobState::BtcEarlyRefunded { .. } | BobState::XmrRedeemed { .. } | BobState::BtcPunished { .. } + | BobState::XmrCooperativelyRedeemable { .. } | BobState::SafelyAborted => bail!( "Cannot refund swap {} because it is in state {} which is not refundable.", swap_id, diff --git a/swap/src/database/bob.rs b/swap/src/database/bob.rs index 9affda685..2a8fd1d6a 100644 --- a/swap/src/database/bob.rs +++ b/swap/src/database/bob.rs @@ -41,6 +41,10 @@ pub enum Bob { state: bob::State6, tx_lock_id: bitcoin::Txid, }, + XmrCooperativelyRedeemable { + state: bob::State5, + tx_lock_id: bitcoin::Txid, + }, BtcRedeemed(bob::State5), CancelTimelockExpired(bob::State6), BtcCancelled(bob::State6), @@ -110,6 +114,13 @@ impl From for Bob { BobState::BtcEarlyRefunded(state6) => { Bob::Done(BobEndState::BtcEarlyRefunded(Box::new(state6))) } + BobState::XmrCooperativelyRedeemable { + state: state5, + tx_lock_id: lock_tx_id, + } => Bob::XmrCooperativelyRedeemable { + state: state5, + tx_lock_id: lock_tx_id, + }, BobState::SafelyAborted => Bob::Done(BobEndState::SafelyAborted), } } @@ -161,6 +172,13 @@ impl From for BobState { Bob::BtcRefundPublished(state6) => BobState::BtcRefundPublished(state6), Bob::BtcEarlyRefundPublished(state6) => BobState::BtcEarlyRefundPublished(state6), Bob::BtcPunished { state, tx_lock_id } => BobState::BtcPunished { state, tx_lock_id }, + Bob::XmrCooperativelyRedeemable { + state: state5, + tx_lock_id, + } => BobState::XmrCooperativelyRedeemable { + tx_lock_id, + state: state5, + }, Bob::Done(end_state) => match end_state { BobEndState::SafelyAborted => BobState::SafelyAborted, BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, @@ -190,6 +208,9 @@ impl fmt::Display for Bob { Bob::Done(end_state) => write!(f, "Done: {}", end_state), Bob::EncSigSent { .. } => f.write_str("Encrypted signature sent"), Bob::BtcPunished { .. } => f.write_str("Bitcoin punished"), + Bob::XmrCooperativelyRedeemable { .. } => { + f.write_str("Monero cooperatively redeemable") + } } } } diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 60086a9c7..3866ee1c9 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -4,6 +4,7 @@ pub mod wallet_rpc; pub use ::monero::network::Network; pub use ::monero::{Address, PrivateKey, PublicKey}; pub use curve25519_dalek::scalar::Scalar; +use monero_oxide::primitives::keccak256; pub use wallet::{Daemon, Wallet, Wallets, WatchRequest}; use crate::bitcoin; @@ -60,6 +61,18 @@ impl PrivateViewKey { Self(private_key) } + /// Construct the private view key corresponding to a private spend key. + pub fn from_spend_key(spend_key: PrivateKey) -> Self { + let bytes = spend_key.scalar.to_bytes(); + + // See zero to monero: + // - page 34/85 annotation 1 + // - page 21/85 annotation 5 + let scalar = Scalar::from_bytes_mod_order(keccak256(bytes)); + + Self(PrivateKey::from_scalar(scalar)) + } + pub fn public(&self) -> PublicViewKey { PublicViewKey(PublicKey::from_private_key(&self.0)) } diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 36fc4dfd4..50e382b7e 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -4,10 +4,11 @@ use crate::bitcoin::{ TxLock, Txid, Wallet, }; use crate::monero::wallet::WatchRequest; -use crate::monero::TransferProof; use crate::monero::{self, MoneroAddressPool, TxHash}; +use crate::monero::{PrivateViewKey, TransferProof}; use crate::monero_ext::ScalarExt; use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM}; +use ::monero::PrivateKey; use anyhow::{anyhow, bail, Context, Result}; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::nonce::Deterministic; @@ -62,6 +63,10 @@ pub enum BobState { state: State6, tx_lock_id: bitcoin::Txid, }, + XmrCooperativelyRedeemable { + state: State5, + tx_lock_id: bitcoin::Txid, + }, SafelyAborted, } @@ -87,6 +92,9 @@ impl fmt::Display for BobState { BobState::BtcRefunded(..) => write!(f, "btc is refunded"), BobState::XmrRedeemed { .. } => write!(f, "xmr is redeemed"), BobState::BtcPunished { .. } => write!(f, "btc is punished"), + BobState::XmrCooperativelyRedeemable { .. } => { + write!(f, "xmr is cooperatively redeemable") + } BobState::BtcEarlyRefunded { .. } => write!(f, "btc is early refunded"), BobState::SafelyAborted => write!(f, "safely aborted"), } @@ -118,7 +126,9 @@ impl BobState { | BobState::BtcEarlyRefundPublished(state) => { Some(state.expired_timelock(&bitcoin_wallet).await?) } - BobState::BtcPunished { .. } => Some(ExpiredTimelocks::Punish), + BobState::BtcPunished { .. } | BobState::XmrCooperativelyRedeemable { .. } => { + Some(ExpiredTimelocks::Punish) + } BobState::BtcRefunded(_) | BobState::BtcEarlyRefunded { .. } | BobState::BtcRedeemed(_) @@ -906,10 +916,18 @@ impl State6 { &self, s_a: monero::Scalar, lock_transfer_proof: TransferProof, - ) -> State5 { + ) -> Result { let s_a = monero::PrivateKey::from_scalar(s_a); + let s_b = PrivateKey::from_scalar(self.s_b); - State5 { + // Make sure we've got the correct key by making sure the + // derived spend key is the one we expect. + let corresponding_view_key = PrivateViewKey::from_spend_key(s_a + s_b); + if corresponding_view_key != self.v { + bail!("received bogus cooperative redeem key - doesn't match view key") + } + + Ok(State5 { s_a, s_b: self.s_b, v: self.v, @@ -917,7 +935,7 @@ impl State6 { tx_lock: self.tx_lock.clone(), monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight, lock_transfer_proof, - } + }) } pub async fn check_for_tx_early_refund( diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 92bdca981..7f9b703e0 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -811,82 +811,14 @@ async fn next_state( "Alice has accepted our request to cooperatively redeem the XMR" ); - let state5 = state.attempt_cooperative_redeem(s_a, lock_transfer_proof); - - let watch_request = state5.lock_xmr_watch_request_for_sweep(); - let event_emitter_clone = event_emitter.clone(); - let state5_clone = state5.clone(); - - // Wait for XMR confirmations before redeeming - monero_wallet - .wait_until_confirmed( - watch_request, - Some( - move |( - xmr_lock_tx_confirmations, - xmr_lock_tx_target_confirmations, - )| { - let event_emitter = event_emitter_clone.clone(); - let tx_hash = state5_clone.lock_transfer_proof.tx_hash(); - - event_emitter.emit_swap_progress_event( - swap_id, - TauriSwapProgressEvent::WaitingForXmrConfirmationsBeforeRedeem { - xmr_lock_txid: tx_hash, - xmr_lock_tx_confirmations, - xmr_lock_tx_target_confirmations, - }, - ); - }, - ), - ) - .await - .map_err(|e| { - anyhow::anyhow!( - "Failed to wait for XMR confirmations during cooperative redeem: {}", - e - ) - })?; - - match retry( - "Redeeming Monero", - || async { - state5 - .redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone()) - .await - .map_err(backoff::Error::transient) - }, - Duration::from_secs(2 * 60), - None, - ) - .await - .context("Failed to redeem Monero") - { - Ok(xmr_redeem_txids) => { - event_emitter.emit_swap_progress_event( - swap_id, - TauriSwapProgressEvent::XmrRedeemInMempool { - xmr_redeem_txids, - xmr_receive_pool: monero_receive_pool.clone(), - }, - ); - - return Ok(BobState::XmrRedeemed { tx_lock_id }); - } - Err(error) => { - event_emitter.emit_swap_progress_event( - swap_id, - TauriSwapProgressEvent::CooperativeRedeemRejected { - reason: error.to_string(), - }, - ); - - let err: std::result::Result<_, anyhow::Error> = - Err(error).context("Failed to redeem XMR with revealed XMR key"); - - return err; - } - } + let state5 = state + .attempt_cooperative_redeem(s_a, lock_transfer_proof) + .context("Can't cooperatively redeem Monero")?; + + return Ok(BobState::XmrCooperativelyRedeemable { + state: state5, + tx_lock_id, + }); } Ok(Rejected { reason, .. }) => { let err = Err(reason.clone()) @@ -924,6 +856,79 @@ async fn next_state( } }; } + BobState::XmrCooperativelyRedeemable { state, tx_lock_id } => { + let watch_request = state.lock_xmr_watch_request_for_sweep(); + let event_emitter_clone = event_emitter.clone(); + let state5_clone = state.clone(); + + // Wait for XMR confirmations before redeeming + monero_wallet + .wait_until_confirmed( + watch_request, + Some( + move |(xmr_lock_tx_confirmations, xmr_lock_tx_target_confirmations)| { + let event_emitter = event_emitter_clone.clone(); + let tx_hash = state5_clone.lock_transfer_proof.tx_hash(); + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::WaitingForXmrConfirmationsBeforeRedeem { + xmr_lock_txid: tx_hash, + xmr_lock_tx_confirmations, + xmr_lock_tx_target_confirmations, + }, + ); + }, + ), + ) + .await + .map_err(|e| { + anyhow::anyhow!( + "Failed to wait for XMR confirmations during cooperative redeem: {}", + e + ) + })?; + + match retry( + "Redeeming Monero", + || async { + state + .redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone()) + .await + .map_err(backoff::Error::transient) + }, + Duration::from_secs(2 * 60), + None, + ) + .await + .context("Failed to redeem Monero") + { + Ok(xmr_redeem_txids) => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::XmrRedeemInMempool { + xmr_redeem_txids, + xmr_receive_pool: monero_receive_pool.clone(), + }, + ); + + return Ok(BobState::XmrRedeemed { tx_lock_id }); + } + Err(error) => { + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::CooperativeRedeemRejected { + reason: error.to_string(), + }, + ); + + let err: std::result::Result<_, anyhow::Error> = + Err(error).context("Failed to redeem XMR with revealed XMR key"); + + return err; + } + } + } // TODO: Emit a Tauri event here BobState::BtcEarlyRefunded(state) => BobState::BtcEarlyRefunded(state), BobState::SafelyAborted => BobState::SafelyAborted, From 7bd6659d459f771e8fc37a90b52a9b0e829e73ab Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Thu, 11 Sep 2025 19:38:27 +0200 Subject: [PATCH 02/24] remove state XmrCooperativelyRedeemable, go to BtcRedeemed instead --- swap/src/cli/cancel_and_refund.rs | 2 - swap/src/database/bob.rs | 21 ------- swap/src/protocol/bob/state.rs | 12 +--- swap/src/protocol/bob/swap.rs | 96 ++++--------------------------- 4 files changed, 13 insertions(+), 118 deletions(-) diff --git a/swap/src/cli/cancel_and_refund.rs b/swap/src/cli/cancel_and_refund.rs index e1c1bc3d0..00ab99253 100644 --- a/swap/src/cli/cancel_and_refund.rs +++ b/swap/src/cli/cancel_and_refund.rs @@ -71,7 +71,6 @@ pub async fn cancel( | BobState::BtcRedeemed(_) | BobState::XmrRedeemed { .. } | BobState::BtcPunished { .. } - | BobState::XmrCooperativelyRedeemable { .. } | BobState::BtcEarlyRefunded { .. } | BobState::SafelyAborted => bail!( "Cannot cancel swap {} because it is in state {} which is not cancellable.", @@ -176,7 +175,6 @@ pub async fn refund( | BobState::BtcEarlyRefunded { .. } | BobState::XmrRedeemed { .. } | BobState::BtcPunished { .. } - | BobState::XmrCooperativelyRedeemable { .. } | BobState::SafelyAborted => bail!( "Cannot refund swap {} because it is in state {} which is not refundable.", swap_id, diff --git a/swap/src/database/bob.rs b/swap/src/database/bob.rs index 2a8fd1d6a..9affda685 100644 --- a/swap/src/database/bob.rs +++ b/swap/src/database/bob.rs @@ -41,10 +41,6 @@ pub enum Bob { state: bob::State6, tx_lock_id: bitcoin::Txid, }, - XmrCooperativelyRedeemable { - state: bob::State5, - tx_lock_id: bitcoin::Txid, - }, BtcRedeemed(bob::State5), CancelTimelockExpired(bob::State6), BtcCancelled(bob::State6), @@ -114,13 +110,6 @@ impl From for Bob { BobState::BtcEarlyRefunded(state6) => { Bob::Done(BobEndState::BtcEarlyRefunded(Box::new(state6))) } - BobState::XmrCooperativelyRedeemable { - state: state5, - tx_lock_id: lock_tx_id, - } => Bob::XmrCooperativelyRedeemable { - state: state5, - tx_lock_id: lock_tx_id, - }, BobState::SafelyAborted => Bob::Done(BobEndState::SafelyAborted), } } @@ -172,13 +161,6 @@ impl From for BobState { Bob::BtcRefundPublished(state6) => BobState::BtcRefundPublished(state6), Bob::BtcEarlyRefundPublished(state6) => BobState::BtcEarlyRefundPublished(state6), Bob::BtcPunished { state, tx_lock_id } => BobState::BtcPunished { state, tx_lock_id }, - Bob::XmrCooperativelyRedeemable { - state: state5, - tx_lock_id, - } => BobState::XmrCooperativelyRedeemable { - tx_lock_id, - state: state5, - }, Bob::Done(end_state) => match end_state { BobEndState::SafelyAborted => BobState::SafelyAborted, BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id }, @@ -208,9 +190,6 @@ impl fmt::Display for Bob { Bob::Done(end_state) => write!(f, "Done: {}", end_state), Bob::EncSigSent { .. } => f.write_str("Encrypted signature sent"), Bob::BtcPunished { .. } => f.write_str("Bitcoin punished"), - Bob::XmrCooperativelyRedeemable { .. } => { - f.write_str("Monero cooperatively redeemable") - } } } } diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 50e382b7e..6e7b8a899 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -63,10 +63,6 @@ pub enum BobState { state: State6, tx_lock_id: bitcoin::Txid, }, - XmrCooperativelyRedeemable { - state: State5, - tx_lock_id: bitcoin::Txid, - }, SafelyAborted, } @@ -92,9 +88,7 @@ impl fmt::Display for BobState { BobState::BtcRefunded(..) => write!(f, "btc is refunded"), BobState::XmrRedeemed { .. } => write!(f, "xmr is redeemed"), BobState::BtcPunished { .. } => write!(f, "btc is punished"), - BobState::XmrCooperativelyRedeemable { .. } => { - write!(f, "xmr is cooperatively redeemable") - } + BobState::BtcEarlyRefunded { .. } => write!(f, "btc is early refunded"), BobState::SafelyAborted => write!(f, "safely aborted"), } @@ -126,9 +120,7 @@ impl BobState { | BobState::BtcEarlyRefundPublished(state) => { Some(state.expired_timelock(&bitcoin_wallet).await?) } - BobState::BtcPunished { .. } | BobState::XmrCooperativelyRedeemable { .. } => { - Some(ExpiredTimelocks::Punish) - } + BobState::BtcPunished { .. } => Some(ExpiredTimelocks::Punish), BobState::BtcRefunded(_) | BobState::BtcEarlyRefunded { .. } | BobState::BtcRedeemed(_) diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 7f9b703e0..228a1a20c 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -601,14 +601,16 @@ async fn next_state( ), ); - // Wait for the 10 confirmations to complete - watch_future + // Wait for the 10 confirmations to complete - if that fails we try to redeem anyway + let _ = watch_future .await - .map_err(|e| anyhow::anyhow!("Failed to wait for XMR confirmations: {}", e))?; + .inspect_err(|e| tracing::warn!(error=%e, "Failed to wait for XMR confirmations - attempting to redeem anyway")); event_emitter .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::RedeemingMonero); + tracing::info!("Monero lock transaction unlocked, redeeming the funds"); + let xmr_redeem_txids = retry( "Redeeming Monero", || async { @@ -790,7 +792,7 @@ async fn next_state( BobState::BtcRefunded(state) } - BobState::BtcPunished { state, tx_lock_id } => { + BobState::BtcPunished { state, .. } => { tracing::info!("You have been punished for not refunding in time"); event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcPunished); event_emitter.emit_swap_progress_event( @@ -807,18 +809,15 @@ async fn next_state( lock_transfer_proof, .. }) => { - tracing::info!( - "Alice has accepted our request to cooperatively redeem the XMR" - ); - let state5 = state .attempt_cooperative_redeem(s_a, lock_transfer_proof) .context("Can't cooperatively redeem Monero")?; - return Ok(BobState::XmrCooperativelyRedeemable { - state: state5, - tx_lock_id, - }); + tracing::info!( + "Alice has accepted our request to cooperatively redeem the XMR" + ); + + return Ok(BobState::BtcRedeemed(state5)); } Ok(Rejected { reason, .. }) => { let err = Err(reason.clone()) @@ -856,79 +855,6 @@ async fn next_state( } }; } - BobState::XmrCooperativelyRedeemable { state, tx_lock_id } => { - let watch_request = state.lock_xmr_watch_request_for_sweep(); - let event_emitter_clone = event_emitter.clone(); - let state5_clone = state.clone(); - - // Wait for XMR confirmations before redeeming - monero_wallet - .wait_until_confirmed( - watch_request, - Some( - move |(xmr_lock_tx_confirmations, xmr_lock_tx_target_confirmations)| { - let event_emitter = event_emitter_clone.clone(); - let tx_hash = state5_clone.lock_transfer_proof.tx_hash(); - - event_emitter.emit_swap_progress_event( - swap_id, - TauriSwapProgressEvent::WaitingForXmrConfirmationsBeforeRedeem { - xmr_lock_txid: tx_hash, - xmr_lock_tx_confirmations, - xmr_lock_tx_target_confirmations, - }, - ); - }, - ), - ) - .await - .map_err(|e| { - anyhow::anyhow!( - "Failed to wait for XMR confirmations during cooperative redeem: {}", - e - ) - })?; - - match retry( - "Redeeming Monero", - || async { - state - .redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone()) - .await - .map_err(backoff::Error::transient) - }, - Duration::from_secs(2 * 60), - None, - ) - .await - .context("Failed to redeem Monero") - { - Ok(xmr_redeem_txids) => { - event_emitter.emit_swap_progress_event( - swap_id, - TauriSwapProgressEvent::XmrRedeemInMempool { - xmr_redeem_txids, - xmr_receive_pool: monero_receive_pool.clone(), - }, - ); - - return Ok(BobState::XmrRedeemed { tx_lock_id }); - } - Err(error) => { - event_emitter.emit_swap_progress_event( - swap_id, - TauriSwapProgressEvent::CooperativeRedeemRejected { - reason: error.to_string(), - }, - ); - - let err: std::result::Result<_, anyhow::Error> = - Err(error).context("Failed to redeem XMR with revealed XMR key"); - - return err; - } - } - } // TODO: Emit a Tauri event here BobState::BtcEarlyRefunded(state) => BobState::BtcEarlyRefunded(state), BobState::SafelyAborted => BobState::SafelyAborted, From 748d5615da969c49712b9861ed986448a1b7e0af Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Fri, 12 Sep 2025 23:18:05 +0200 Subject: [PATCH 03/24] fix: make logs in terminal colored again --- Cargo.lock | 1 - swap/Cargo.toml | 1 - swap/src/common/tracing_util.rs | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc2179f34..9e8031b47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13413,7 +13413,6 @@ dependencies = [ "async-compression 0.3.15", "async-trait", "asynchronous-codec 0.7.0", - "atty", "backoff", "base64 0.22.1", "bdk", diff --git a/swap/Cargo.toml b/swap/Cargo.toml index dd672c5bf..3054f0104 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -37,7 +37,6 @@ arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls" async-compression = { version = "0.3", features = ["bzip2", "tokio"] } async-trait = "0.1" asynchronous-codec = "0.7.0" -atty = "0.2" backoff = { workspace = true } base64 = "0.22" big-bytes = "1" diff --git a/swap/src/common/tracing_util.rs b/swap/src/common/tracing_util.rs index eff7750f9..b03ea2ab3 100644 --- a/swap/src/common/tracing_util.rs +++ b/swap/src/common/tracing_util.rs @@ -1,4 +1,4 @@ -use std::io; +use std::io::{self, IsTerminal}; use std::path::Path; use std::str::FromStr; @@ -132,7 +132,7 @@ pub fn init( // Layer for writing to the terminal // Crates: swap, asb // Level: Passed in - let is_terminal = atty::is(atty::Stream::Stderr); + let is_terminal = std::io::stdout().is_terminal(); let terminal_layer = fmt::layer() .with_writer(std::io::stderr) .with_ansi(is_terminal) From c1513de5716307253261926652ddfbdf17333cde Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Fri, 12 Sep 2025 23:38:44 +0200 Subject: [PATCH 04/24] fix: gui won't start on testnet --- justfile | 2 +- src-gui/src/store/config.ts | 3 ++- src-tauri/capabilities/desktop.json | 17 ++++++++++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/justfile b/justfile index c684f7af8..d6f5ca98f 100644 --- a/justfile +++ b/justfile @@ -72,7 +72,7 @@ swap: cargo build -p swap-asb --bin asb && cd swap && cargo build --bin=swap # Run the asb on testnet -asb-testnet: +asb: cargo run -p swap-asb --bin asb -- --trace --testnet start --rpc-bind-port 9944 --rpc-bind-host 0.0.0.0 # Updates our submodules (currently only Monero C++ codebase) diff --git a/src-gui/src/store/config.ts b/src-gui/src/store/config.ts index 4b77152aa..1f0729d8b 100644 --- a/src-gui/src/store/config.ts +++ b/src-gui/src/store/config.ts @@ -6,7 +6,8 @@ import { Network } from "./features/settingsSlice"; let matches: CliMatches; try { matches = await getMatches(); -} catch { +} catch (e) { + console.error("couldn't get cli matches: " + e); matches = { args: {}, }; diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json index d08e1376c..d4b709613 100644 --- a/src-tauri/capabilities/desktop.json +++ b/src-tauri/capabilities/desktop.json @@ -1,5 +1,16 @@ { + "$schema": "../gen/schemas/desktop-schema.json", "identifier": "desktop-capability", - "platforms": ["macOS", "windows", "linux"], - "permissions": ["cli:default", "cli:allow-cli-matches"] -} + "windows": [ + "main" + ], + "platforms": [ + "macOS", + "windows", + "linux" + ], + "permissions": [ + "cli:default", + "cli:allow-cli-matches" + ] +} \ No newline at end of file From c5581a00afc9a568e1ff26149aff4b9a4d36884e Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sat, 13 Sep 2025 03:10:38 +0200 Subject: [PATCH 05/24] fix: gui quirks with monero seed selection --- .../other/ActionableMonospaceTextBox.tsx | 52 +++++++++-------- .../components/other/MonospaceTextBox.tsx | 1 + .../pages/monero/SeedPhraseButton.tsx | 2 +- .../pages/monero/SeedPhraseModal.tsx | 57 ++++++++++++++----- .../monero/components/WalletActionButtons.tsx | 25 +++++--- 5 files changed, 89 insertions(+), 48 deletions(-) diff --git a/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx b/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx index c321ba9b0..199a7e44d 100644 --- a/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx +++ b/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx @@ -93,24 +93,34 @@ export default function ActionableMonospaceTextBox({ - {content} - {displayCopyIcon && ( - - - - )} + + {content} + {displayCopyIcon && ( + + + + )} + + {enableQrCode && ( - - {spoilerText} - + {spoilerText} )} diff --git a/src-gui/src/renderer/components/other/MonospaceTextBox.tsx b/src-gui/src/renderer/components/other/MonospaceTextBox.tsx index 6a95c499d..5c1059f59 100644 --- a/src-gui/src/renderer/components/other/MonospaceTextBox.tsx +++ b/src-gui/src/renderer/components/other/MonospaceTextBox.tsx @@ -27,6 +27,7 @@ export default function MonospaceTextBox({ children, light = false }: Props) { lineHeight: 1.5, display: "flex", alignItems: "center", + flex: 1, }} > {children} diff --git a/src-gui/src/renderer/components/pages/monero/SeedPhraseButton.tsx b/src-gui/src/renderer/components/pages/monero/SeedPhraseButton.tsx index c5f76dc2c..0173d0cb5 100644 --- a/src-gui/src/renderer/components/pages/monero/SeedPhraseButton.tsx +++ b/src-gui/src/renderer/components/pages/monero/SeedPhraseButton.tsx @@ -26,7 +26,7 @@ export default function SeedPhraseButton({ }; return ( - + void; - seed: [GetMoneroSeedResponse, GetRestoreHeightResponse] | null; + open: boolean; +} + +interface Info { + seed: string; + restoreHeight: number; } export default function SeedPhraseModal({ onClose, - seed, + open, }: SeedPhraseModalProps) { - if (seed === null) { - return null; - } + const [info, setInfo] = useState(null); + + useEffect(() => { + getMoneroSeedAndRestoreHeight().then(([seed, height]) => { + setInfo({ seed: seed.seed, restoreHeight: height.height }); + }); + }, []); return ( - - Wallet Seed Phrase + + Export your Monero wallet's seed + + Never reveal your seed phrase to anyone. The developers will + never ask for your seed. + + + Seed phrase + + + + The seed phrase of your wallet is equivalent to the secret key. + + + Restore Block Height + + + + The restore height will help other wallets determine which parts of + the blockchain to scan for funds. + - Keep this seed phrase safe and secure. Write it down on paper and - store it in a safe place. Keep the restore height in mind when you - restore your wallet on another device. - + > + + + + + ); +} diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 4b9f9d1f9..34a36e1c3 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -46,6 +46,8 @@ import { GetRestoreHeightResponse, MoneroNodeConfig, GetMoneroSeedResponse, + ManualCooperativeRedeemArgs, + ManualCooperativeRedeemResponse, } from "models/tauriModel"; import { rpcSetBalance, @@ -255,6 +257,23 @@ export async function buyXmr( }); } +export async function manualCooperativeRedeem( + swapId: string, + s_a: string, + txId: string, + txKey: string, +) { + await invoke( + "manual_cooperative_redeem", + { + swap_id: swapId, + s_a, + lock_tx_id: txId, + lock_tx_key: txKey, + }, + ); +} + export async function resumeSwap(swapId: string) { await invoke("resume_swap", { swap_id: swapId, @@ -354,7 +373,7 @@ export async function initializeContext() { enable_monero_tor: useMoneroTor, }; - logger.info("Initializing context with settings", tauriSettings); + logger.info("Initializing context with settings" + tauriSettings); try { await invokeUnsafe("initialize_context", { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7d3e857a5..1f2c881c9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,9 +13,10 @@ use swap::cli::{ GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs, GetMoneroMainAddressArgs, GetMoneroSeedArgs, GetMoneroSyncProgressArgs, GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs, - GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs, - RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs, - SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs, + GetSwapInfosAllArgs, ListSellersArgs, ManualCooperativeRedeemArgs, MoneroRecoveryArgs, + RedactArgs, RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, + ResumeSwapArgs, SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, + WithdrawBtcArgs, }, tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings}, Context, ContextBuilder, @@ -185,6 +186,7 @@ pub fn run() { withdraw_btc, buy_xmr, resume_swap, + manual_cooperative_redeem, get_history, monero_recovery, get_logs, @@ -248,6 +250,7 @@ pub fn run() { tauri_command!(get_balance, BalanceArgs); tauri_command!(buy_xmr, BuyXmrArgs); tauri_command!(resume_swap, ResumeSwapArgs); +tauri_command!(manual_cooperative_redeem, ManualCooperativeRedeemArgs); tauri_command!(withdraw_btc, WithdrawBtcArgs); tauri_command!(monero_recovery, MoneroRecoveryArgs); tauri_command!(get_logs, GetLogsArgs); diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs index 8cb5fb012..db4d86634 100644 --- a/swap/src/cli/api/request.rs +++ b/swap/src/cli/api/request.rs @@ -10,7 +10,7 @@ use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus}; use crate::common::{get_logs, redact}; use crate::libp2p_ext::MultiAddrExt; use crate::monero::wallet_rpc::MoneroDaemon; -use crate::monero::MoneroAddressPool; +use crate::monero::{MoneroAddressPool, TransferProof, TxHash}; use crate::network::quote::BidQuote; use crate::network::rendezvous::XmrBtcNamespace; use crate::network::swarm; @@ -135,6 +135,29 @@ impl Request for CancelAndRefundArgs { } } +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ManualCooperativeRedeemArgs { + pub swap_id: Uuid, + #[serde(with = "swap_serde::monero::private_key")] + pub s_a: monero::PrivateKey, + pub lock_tx_id: String, + #[serde(with = "swap_serde::monero::private_key")] + pub lock_tx_key: monero::PrivateKey, +} + +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ManualCooperativeRedeemResponse; + +impl Request for ManualCooperativeRedeemArgs { + type Response = ManualCooperativeRedeemResponse; + + async fn request(self, ctx: Arc) -> Result { + manual_cooperative_redeem(self, ctx).await + } +} + // MoneroRecovery #[typeshare] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -1160,6 +1183,42 @@ pub async fn buy_xmr( Ok(BuyXmrResponse { swap_id, quote }) } +#[tracing::instrument(fields(method = "manual_cooperative_redeem"), skip(context))] +pub async fn manual_cooperative_redeem( + args: ManualCooperativeRedeemArgs, + context: Arc, +) -> Result { + // Get the current swap state + let swap_state = context + .db + .get_state(args.swap_id) + .await + .context("no swap with this id found")?; + + // Abort if not in BtcPunished + let State::Bob(BobState::BtcPunished { state, .. }) = swap_state else { + // Todo: maybe allow outside BtcPunished + bail!("Bitcoin wasn't punished - can't cooperatively redeem"); + }; + + // Construct and insert the new state, checking the key we received + let state5 = state + .attempt_cooperative_redeem( + args.s_a.scalar, + TransferProof::new(TxHash(args.lock_tx_id), args.lock_tx_key), + ) + .context("couldn't cooperatively redeem monero")?; + let new_state = State::Bob(BobState::BtcRedeemed(state5)); + + context + .db + .insert_latest_state(args.swap_id, new_state) + .await + .context("couldn't save new state to db")?; + + Ok(ManualCooperativeRedeemResponse) +} + #[tracing::instrument(fields(method = "resume_swap"), skip(context))] pub async fn resume_swap( resume: ResumeSwapArgs, diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 714c9f8a6..75304365f 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -915,9 +915,10 @@ impl State6 { lock_transfer_proof: TransferProof, ) -> Result { let alleged_s_a = monero::PrivateKey::from_scalar(s_a); + let alleged_S_a = PublicKey::from_private_key(&alleged_s_a); // Make sure we've got the correct key by checking that it matches the pubkey - if PublicKey::from_private_key(&alleged_s_a) != self.S_a_monero { + if alleged_S_a != self.S_a_monero { bail!("received bogus cooperative redeem key - doesn't match view key") } From 5f20b8dc7460aedaf5b0e3bddde3e693ce7097d7 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 01:51:24 +0200 Subject: [PATCH 09/24] add justfile command for running swap controller --- justfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/justfile b/justfile index d6f5ca98f..6b8f35ae5 100644 --- a/justfile +++ b/justfile @@ -75,6 +75,10 @@ swap: asb: cargo run -p swap-asb --bin asb -- --trace --testnet start --rpc-bind-port 9944 --rpc-bind-host 0.0.0.0 +# Run the asb controller connecting to your already running asb +asb-controller: + cargo run -p swap-controller --bin asb-controller -- --url http://127.0.0.1:9944 + # Updates our submodules (currently only Monero C++ codebase) update_submodules: cd monero-sys && git submodule update --init --recursive --force From 4775dec527cfaae0f11256cd8428b5106c52b730 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 01:52:04 +0200 Subject: [PATCH 10/24] fix: don't show second BTC after bitcoin balance --- swap-controller/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swap-controller/src/main.rs b/swap-controller/src/main.rs index 75d8e0522..2e656f188 100644 --- a/swap-controller/src/main.rs +++ b/swap-controller/src/main.rs @@ -31,7 +31,7 @@ async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> { } Cmd::BitcoinBalance => { let response = client.bitcoin_balance().await?; - println!("Current Bitcoin balance is {} BTC", response.balance); + println!("Current Bitcoin balance is {}", response.balance); } Cmd::MoneroBalance => { let response = client.monero_balance().await?; From 969f748474f3c37f0551a80ef1fcbe1708a71705 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 01:53:53 +0200 Subject: [PATCH 11/24] add asb rpc server to test context, integration test for manual cooperative redeem --- swap/Cargo.toml | 2 +- swap/tests/harness/mod.rs | 52 ++++++++++++++++--- swap/tests/manual_cooperative_redeem.rs | 67 +++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 swap/tests/manual_cooperative_redeem.rs diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 3054f0104..38aaa9d4e 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -56,7 +56,7 @@ electrum-pool = { path = "../electrum-pool" } fns = "0.0.7" futures = { workspace = true } hex = { workspace = true } -jsonrpsee = { workspace = true, features = ["server"] } +jsonrpsee = { workspace = true, features = ["server", "client-core", "http-client"] } libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] } libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } moka = { version = "0.12", features = ["sync", "future"] } diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index 2208bad6f..6c512b835 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -6,6 +6,9 @@ use async_trait::async_trait; use bitcoin_harness::{BitcoindRpcApi, Client}; use futures::Future; use get_port::get_port; +use jsonrpsee::core::middleware::layer::RpcLogger; +use jsonrpsee::http_client::transport::HttpBackend; +use jsonrpsee::http_client::HttpClient; use libp2p::core::Multiaddr; use libp2p::PeerId; use monero_harness::{image, Monero}; @@ -13,6 +16,8 @@ use monero_sys::Daemon; use std::cmp::Ordering; use std::fmt; use std::path::PathBuf; +use swap::asb::rpc::RpcServer; +use swap_controller_api::{AsbApiClient, AsbApiServer}; use std::str::FromStr; use std::sync::Arc; @@ -94,7 +99,7 @@ where .parse() .expect("failed to parse Alice's address"); - let (alice_handle, alice_swap_handle) = start_alice( + let (alice_handle, alice_swap_handle, rpc_client) = start_alice( &alice_seed, alice_db_path.clone(), alice_listen_address.clone(), @@ -144,6 +149,7 @@ where alice_monero_wallet, alice_swap_handle, alice_handle, + alice_rpc_client: rpc_client, bob_params, bob_starting_balances, bob_bitcoin_wallet, @@ -238,7 +244,7 @@ async fn start_alice( env_config: Config, bitcoin_wallet: Arc, monero_wallet: Arc, -) -> (AliceApplicationHandle, Receiver) { +) -> (AliceApplicationHandle, Receiver, HttpClient) { if let Some(parent_dir) = db_path.parent() { ensure_directory_exists(parent_dir).unwrap(); } @@ -272,12 +278,12 @@ async fn start_alice( .unwrap(); swarm.listen_on(listen_address).unwrap(); - let (event_loop, swap_handle, _service) = asb::EventLoop::new( + let (event_loop, swap_handle, event_loop_service) = asb::EventLoop::new( swarm, env_config, - bitcoin_wallet, - monero_wallet, - db, + bitcoin_wallet.clone(), + monero_wallet.clone(), + db.clone(), FixedRate::default(), min_buy, max_buy, @@ -285,10 +291,34 @@ async fn start_alice( ) .unwrap(); + let rpc_host = "http://127.0.0.1".to_string(); + let rpc_port = get_port().expect("port to be available"); + let rpc_server = RpcServer::start( + rpc_host.clone(), + rpc_port, + bitcoin_wallet.clone(), + monero_wallet.clone(), + event_loop_service, + db, + ) + .await + .unwrap(); + + std::mem::forget(rpc_server.spawn()); // Avoid drop or else we'll abort the process + + let rpc_url = format!("{rpc_host}:{rpc_port}"); + let rpc_client = jsonrpsee::http_client::HttpClientBuilder::default() + .build(&rpc_url) + .expect("rpc client to be built"); + let peer_id = event_loop.peer_id(); let handle = tokio::spawn(event_loop.run()); - (AliceApplicationHandle { handle, peer_id }, swap_handle) + ( + AliceApplicationHandle { handle, peer_id }, + swap_handle, + rpc_client, + ) } #[allow(clippy::too_many_arguments)] @@ -604,6 +634,7 @@ pub struct TestContext { alice_monero_wallet: Arc, alice_swap_handle: mpsc::Receiver, alice_handle: AliceApplicationHandle, + pub alice_rpc_client: HttpClient, pub bob_params: BobParams, bob_starting_balances: StartingBalances, @@ -629,7 +660,7 @@ impl TestContext { pub async fn restart_alice(&mut self) { self.alice_handle.abort(); - let (alice_handle, alice_swap_handle) = start_alice( + let (alice_handle, alice_swap_handle, rpc_client) = start_alice( &self.alice_seed, self.alice_db_path.clone(), self.alice_listen_address.clone(), @@ -639,6 +670,7 @@ impl TestContext { ) .await; + self.alice_rpc_client = rpc_client; self.alice_handle = alice_handle; self.alice_swap_handle = alice_swap_handle; } @@ -1124,6 +1156,10 @@ pub mod bob_run_until { pub fn is_encsig_sent(state: &BobState) -> bool { matches!(state, BobState::EncSigSent(..)) } + + pub fn is_btc_punished(state: &BobState) -> bool { + matches!(state, BobState::BtcPunished { .. }) + } } pub struct SlowCancelConfig; diff --git a/swap/tests/manual_cooperative_redeem.rs b/swap/tests/manual_cooperative_redeem.rs new file mode 100644 index 000000000..cf76c3cde --- /dev/null +++ b/swap/tests/manual_cooperative_redeem.rs @@ -0,0 +1,67 @@ +pub mod harness; + +use anyhow::Context; +use harness::FastPunishConfig; +use swap::asb::FixedRate; +use swap::monero::{TransferProof, TxHash}; +use swap::protocol::bob::BobState; +use swap::protocol::{alice, bob, State}; +use swap_controller_api::AsbApiClient; + +use crate::harness::bob_run_until::is_btc_punished; + +/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice +/// the encsig and fail to refund or redeem. Alice punishes. Bob then cooperates with Alice and redeems XMR with her key. +/// But this time, we use the manual export of the cooperative redeem key via the asb-controller. +#[tokio::test] +async fn alice_and_bob_manual_cooperative_redeem_after_punish() { + harness::setup_test(FastPunishConfig, |mut ctx| async move { + let (bob_swap, bob_join_handle) = ctx.bob_swap().await; + let bob_swap_id = bob_swap.id; + let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_punished)); + + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run(alice_swap, FixedRate::default())); + + let bob_state = bob_swap.await??; + assert!(matches!(bob_state, BobState::BtcPunished { .. })); + + let alice_state = alice_swap.await??; + ctx.assert_alice_punished(alice_state).await; + + // Manually do the cooperative redeem via rpc server + let manual_cooperative_redeem_info = ctx + .alice_rpc_client + .get_coop_redeem_info(bob_swap_id) + .await? + .context("swap not found")?; + let BobState::BtcPunished { state, .. } = bob_state else { + panic!("bob unexpected state") + }; + let state5 = state.attempt_cooperative_redeem( + manual_cooperative_redeem_info.inner.scalar, + TransferProof::new( + TxHash(manual_cooperative_redeem_info.lock_tx_id), + manual_cooperative_redeem_info.lock_tx_key, + ), + )?; + let new_state = State::Bob(BobState::BtcRedeemed(state5)); + // Insert new state (BtcRedeemed but we got the key manually) + ctx.bob_swap() + .await + .0 + .db + .insert_latest_state(bob_swap_id, new_state) + .await?; + + // Now try to have bob finish the swap normally + let (bob_swap, _) = ctx + .stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id) + .await; + + let bob_state = bob::run(bob_swap).await?; + ctx.assert_bob_redeemed(bob_state).await; + Ok(()) + }) + .await; +} From cf1f760420152f120e384212942046a186964911 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 02:00:10 +0200 Subject: [PATCH 12/24] add integration test for malicious cooperative redeem check --- ...manual_cooperative_redeem_malicious_key.rs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 swap/tests/manual_cooperative_redeem_malicious_key.rs diff --git a/swap/tests/manual_cooperative_redeem_malicious_key.rs b/swap/tests/manual_cooperative_redeem_malicious_key.rs new file mode 100644 index 000000000..8158d11ce --- /dev/null +++ b/swap/tests/manual_cooperative_redeem_malicious_key.rs @@ -0,0 +1,59 @@ +pub mod harness; + +use anyhow::Context; +use harness::FastPunishConfig; +use swap::asb::FixedRate; +use swap::monero::{TransferProof, TxHash}; +use swap::protocol::bob::BobState; +use swap::protocol::{alice, bob, State}; +use swap_controller_api::AsbApiClient; + +use crate::harness::bob_run_until::is_btc_punished; + +/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice +/// the encsig and fail to refund or redeem. Alice punishes. Bob then cooperates with Alice and redeems XMR with her key. +/// But this time, we use the manual export of the cooperative redeem key via the asb-controller. +/// And also, alice sends a malicious key! So we expect the cooperative redeem check to fail before changing states. +#[tokio::test] +async fn alice_and_bob_manual_cooperative_redeem_after_punish() { + harness::setup_test(FastPunishConfig, |mut ctx| async move { + let (bob_swap, bob_join_handle) = ctx.bob_swap().await; + let bob_swap_id = bob_swap.id; + let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_punished)); + + let alice_swap = ctx.alice_next_swap().await; + let alice_swap = tokio::spawn(alice::run(alice_swap, FixedRate::default())); + + let bob_state = bob_swap.await??; + assert!(matches!(bob_state, BobState::BtcPunished { .. })); + + let alice_state = alice_swap.await??; + ctx.assert_alice_punished(alice_state).await; + + // Manually do the cooperative redeem via rpc server + let mut manual_cooperative_redeem_info = ctx + .alice_rpc_client + .get_coop_redeem_info(bob_swap_id) + .await? + .context("swap not found")?; + let BobState::BtcPunished { state, .. } = bob_state else { + panic!("bob unexpected state") + }; + // Malicous: alice doesn't give the correct secret key + manual_cooperative_redeem_info.inner.scalar = + manual_cooperative_redeem_info.inner.scalar.invert(); + let state5 = state.attempt_cooperative_redeem( + manual_cooperative_redeem_info.inner.scalar, + TransferProof::new( + TxHash(manual_cooperative_redeem_info.lock_tx_id), + manual_cooperative_redeem_info.lock_tx_key, + ), + ); + assert!( + state5.is_err(), + "cooperative redeem key doesn't match actual secret key - the check should fail" + ); + Ok(()) + }) + .await; +} From 943fce33bf30ad60fdd7f0a3a66c6d6c7e3e30b0 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 02:39:32 +0200 Subject: [PATCH 13/24] fix: code smells - rename rpc method to avoid abbreviation - improve comments/naming - check Alice finalized tx_punish before revealing cooperative redeem key - gui: only resume swap if cooperative redeem worked. --- .../swap/swap/done/BitcoinPunishedPage.tsx | 11 +++++++---- swap-controller-api/src/lib.rs | 11 +++++------ swap-controller/src/main.rs | 4 ++-- swap/src/asb/rpc/server.rs | 17 ++++++++++++++--- swap/tests/manual_cooperative_redeem.rs | 4 ++-- .../manual_cooperative_redeem_malicious_key.rs | 8 ++++---- 6 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx index a54ff8ff3..1daf1918a 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx @@ -77,11 +77,14 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { setTxKey(""); } - // Wait 5 seconds before continuing + // Wait 5 seconds to give user time to read message await new Promise((res) => setTimeout(res, 5000)); - onClose(); - // Resume the swap - resumeSwap(swapId); + + // Close the modal and continue the swap normally if the cooperative redeem succeded + if (success) { + onClose(); + resumeSwap(swapId); + } }; const resultText = success diff --git a/swap-controller-api/src/lib.rs b/swap-controller-api/src/lib.rs index ad2a4fc6d..d88eba148 100644 --- a/swap-controller-api/src/lib.rs +++ b/swap-controller-api/src/lib.rs @@ -49,12 +49,11 @@ pub struct MoneroSeedResponse { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CooperativeRedeemResponse { - /// Actual secret key + /// Actual secret key needed for cooperative redeem by Bob #[serde(with = "swap_serde::monero::private_key")] - pub inner: PrivateKey, - /// Monero lock tx id + pub s_a: PrivateKey, + // Also include the tx hash and key of the Monero lock tx such that Bob can just import without scanning. pub lock_tx_id: String, - /// Monero lock tx key -> combined with tx id is the transfer proof #[serde(with = "swap_serde::monero::private_key")] pub lock_tx_key: PrivateKey, } @@ -79,8 +78,8 @@ pub trait AsbApi { async fn active_connections(&self) -> Result; #[method(name = "get_swaps")] async fn get_swaps(&self) -> Result, ErrorObjectOwned>; - #[method(name = "get_coop_redeem_key")] - async fn get_coop_redeem_info( + #[method(name = "cooperative_redeem_info")] + async fn cooperative_redeem_info( &self, swap_id: Uuid, ) -> Result, ErrorObjectOwned>; diff --git a/swap-controller/src/main.rs b/swap-controller/src/main.rs index 2e656f188..502d09d82 100644 --- a/swap-controller/src/main.rs +++ b/swap-controller/src/main.rs @@ -85,7 +85,7 @@ async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> { println!("Descriptor (BIP-0382) containing the private keys of the internal Bitcoin wallet:\n{}", response.descriptor); } Cmd::CooperativeRedeemKey { swap_id } => { - let response = client.get_coop_redeem_info(swap_id.clone()).await?; + let response = client.cooperative_redeem_info(swap_id.clone()).await?; let Some(response) = response else { println!("Couldn't find any swap with id {swap_id} in the database"); @@ -93,7 +93,7 @@ async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> { }; println!("Cooperative redeem key:"); - println!("{}", response.inner); + println!("{}", response.s_a); println!("Monero lock transaction id:"); println!("{}", response.lock_tx_id); println!("Monero lock transaction ley:"); diff --git a/swap/src/asb/rpc/server.rs b/swap/src/asb/rpc/server.rs index 63594c15d..8d6c2524e 100644 --- a/swap/src/asb/rpc/server.rs +++ b/swap/src/asb/rpc/server.rs @@ -3,7 +3,7 @@ use crate::protocol::alice::AliceState; use crate::protocol::{Database, State}; use crate::{bitcoin, monero}; use ::monero::PrivateKey; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use jsonrpsee::server::{ServerBuilder, ServerHandle}; use jsonrpsee::types::error::ErrorCode; use jsonrpsee::types::ErrorObjectOwned; @@ -163,7 +163,7 @@ impl AsbApiServer for RpcImpl { Ok(swaps) } - async fn get_coop_redeem_info( + async fn cooperative_redeem_info( &self, swap_id: Uuid, ) -> Result, ErrorObjectOwned> { @@ -173,6 +173,17 @@ impl AsbApiServer for RpcImpl { return Ok(None); } + // Check if we previously entered BtcPunished (only happens after tx_punish is confirmed) + let bob_was_punished: bool = states + .iter() + .any(|state| matches!(state, State::Alice(AliceState::BtcPunished { .. }))); + if !bob_was_punished { + anyhow!( + "Revealing cooperative redeem key would be insecure because we didn't punish yet" + ) + .into_json_rpc_error(); + } + states .into_iter() .find_map(|state| match state { @@ -182,7 +193,7 @@ impl AsbApiServer for RpcImpl { state3, .. }) => Some(Some(CooperativeRedeemResponse { - inner: PrivateKey::from_scalar(state3.s_a), + s_a: PrivateKey::from_scalar(state3.s_a), lock_tx_id: transfer_proof.tx_hash().to_string(), lock_tx_key: transfer_proof.tx_key(), })), diff --git a/swap/tests/manual_cooperative_redeem.rs b/swap/tests/manual_cooperative_redeem.rs index cf76c3cde..af5ff4e45 100644 --- a/swap/tests/manual_cooperative_redeem.rs +++ b/swap/tests/manual_cooperative_redeem.rs @@ -32,14 +32,14 @@ async fn alice_and_bob_manual_cooperative_redeem_after_punish() { // Manually do the cooperative redeem via rpc server let manual_cooperative_redeem_info = ctx .alice_rpc_client - .get_coop_redeem_info(bob_swap_id) + .cooperative_redeem_info(bob_swap_id) .await? .context("swap not found")?; let BobState::BtcPunished { state, .. } = bob_state else { panic!("bob unexpected state") }; let state5 = state.attempt_cooperative_redeem( - manual_cooperative_redeem_info.inner.scalar, + manual_cooperative_redeem_info.s_a.scalar, TransferProof::new( TxHash(manual_cooperative_redeem_info.lock_tx_id), manual_cooperative_redeem_info.lock_tx_key, diff --git a/swap/tests/manual_cooperative_redeem_malicious_key.rs b/swap/tests/manual_cooperative_redeem_malicious_key.rs index 8158d11ce..4952a5762 100644 --- a/swap/tests/manual_cooperative_redeem_malicious_key.rs +++ b/swap/tests/manual_cooperative_redeem_malicious_key.rs @@ -33,17 +33,17 @@ async fn alice_and_bob_manual_cooperative_redeem_after_punish() { // Manually do the cooperative redeem via rpc server let mut manual_cooperative_redeem_info = ctx .alice_rpc_client - .get_coop_redeem_info(bob_swap_id) + .cooperative_redeem_info(bob_swap_id) .await? .context("swap not found")?; let BobState::BtcPunished { state, .. } = bob_state else { panic!("bob unexpected state") }; // Malicous: alice doesn't give the correct secret key - manual_cooperative_redeem_info.inner.scalar = - manual_cooperative_redeem_info.inner.scalar.invert(); + manual_cooperative_redeem_info.s_a.scalar = + manual_cooperative_redeem_info.s_a.scalar.invert(); let state5 = state.attempt_cooperative_redeem( - manual_cooperative_redeem_info.inner.scalar, + manual_cooperative_redeem_info.s_a.scalar, TransferProof::new( TxHash(manual_cooperative_redeem_info.lock_tx_id), manual_cooperative_redeem_info.lock_tx_key, From 08242ec099db22dea726bc424a7e850112d5bb32 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 02:55:26 +0200 Subject: [PATCH 14/24] improve gui code, use alert to show outcome --- .../swap/swap/done/BitcoinPunishedPage.tsx | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx index 1daf1918a..173ef5ab4 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx @@ -1,4 +1,5 @@ import { + Alert, Box, Button, Dialog, @@ -15,6 +16,7 @@ import { TauriSwapProgressEventExt } from "models/tauriModelExt"; import { useState } from "react"; import { manualCooperativeRedeem, resumeSwap } from "renderer/rpc"; import { useActiveSwapId } from "store/hooks"; +import { useSnackbar } from "notistack"; export default function BitcoinPunishedPage({ state, @@ -63,41 +65,46 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { const [txKey, setTxKey] = useState(""); const swapId = useActiveSwapId(); + const { enqueueSnackbar } = useSnackbar(); + const handleAttempt = async () => { setSuccess(null); + + // Try and use the given information to cooperatively redeem try { await manualCooperativeRedeem(swapId, key, txId, txKey); setSuccess(true); + // Wait 5 seconds to give user time to read message + await new Promise((res) => setTimeout(res, 5000)); + + // Close the modal and continue the swap normally + onClose(); + resumeSwap(swapId); + setSuccess(null); } catch (e) { - console.error("Failed to cooperatively redeem: " + e); + // If we get an error, throw it to the snackbar + enqueueSnackbar<"error">(`Cooperative redeem failed: \`${e}\``); setSuccess(false); } finally { setKey(""); setTxId(""); setTxKey(""); } - - // Wait 5 seconds to give user time to read message - await new Promise((res) => setTimeout(res, 5000)); - - // Close the modal and continue the swap normally if the cooperative redeem succeded - if (success) { - onClose(); - resumeSwap(swapId); - } }; - const resultText = success - ? "Success, resuming swap" - : success === false - ? "Oops, failed (see console for error)" - : ""; + const alert = success ? ( + + Successfully verified key, attempting swap completion now. + + ) : ( + Couldn't verify the redeem key. + ); return ( Manual Cooperative Redeem - {resultText} + {success !== null ? alert : null} As a fallback to the automated cooperative redeem process, you can @@ -105,15 +112,15 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { setKey(e.target.value)} /> setTxId(e.target.value)} /> setTxKey(e.target.value)} /> @@ -122,7 +129,12 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { - From 9ed60ac091b10dc5abeae99d42fc65be1846e14f Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 02:58:58 +0200 Subject: [PATCH 15/24] disable attempt button while attempt is in progress --- .../components/pages/swap/swap/done/BitcoinPunishedPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx index 173ef5ab4..d935aa014 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx @@ -60,6 +60,7 @@ interface ManualCoopRedeemModalProps { function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { const [success, setSuccess] = useState(null); + const [inProgress, setInProgress] = useState(false); const [key, setKey] = useState(""); const [txId, setTxId] = useState(""); const [txKey, setTxKey] = useState(""); @@ -69,6 +70,7 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { const handleAttempt = async () => { setSuccess(null); + setInProgress(true); // Try and use the given information to cooperatively redeem try { @@ -86,6 +88,7 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { enqueueSnackbar<"error">(`Cooperative redeem failed: \`${e}\``); setSuccess(false); } finally { + setInProgress(false); setKey(""); setTxId(""); setTxKey(""); @@ -133,7 +136,7 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { variant="contained" color="primary" onClick={handleAttempt} - disabled={attempting} + disabled={inProgress} > Attempt From 0b31cd846bc7e224c6f030dce9518b3b0fc8de48 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 03:00:09 +0200 Subject: [PATCH 16/24] use template string --- src-gui/src/renderer/rpc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 34a36e1c3..19a4acf3c 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -373,7 +373,7 @@ export async function initializeContext() { enable_monero_tor: useMoneroTor, }; - logger.info("Initializing context with settings" + tauriSettings); + logger.info(`Initializing context with settings ${tauriSettings}`); try { await invokeUnsafe("initialize_context", { From 0e8c0c18cdc30f9458131e8ead9b9ebd4e8cc76d Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sun, 14 Sep 2025 22:51:27 +0200 Subject: [PATCH 17/24] problem: asb not registering with rendezvous points on stagenet (connection reset by peer) --- justfile | 4 ++++ swap-asb/src/main.rs | 9 +++++++++ swap/src/asb/network.rs | 37 ++++++++++++++++++++++++++++--------- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/justfile b/justfile index 6b8f35ae5..02c477490 100644 --- a/justfile +++ b/justfile @@ -75,6 +75,10 @@ swap: asb: cargo run -p swap-asb --bin asb -- --trace --testnet start --rpc-bind-port 9944 --rpc-bind-host 0.0.0.0 +# Run the asb on mainnet (only use for testing) +asb-mainnet: + cargo run -p swap-asb --bin asb -- --trace start --rpc-bind-port 9944 --rpc-bind-host 0.0.0.0 + # Run the asb controller connecting to your already running asb asb-controller: cargo run -p swap-controller --bin asb-controller -- --url http://127.0.0.1:9944 diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index f724bc587..01f568dbd 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -23,6 +23,7 @@ use std::env; use std::sync::Arc; use structopt::clap; use structopt::clap::ErrorKind; +use swap::libp2p_ext::MultiAddrExt; mod command; use command::{parse_args, Arguments, Command}; use swap::asb::rpc::RpcServer; @@ -274,6 +275,14 @@ pub async fn main() -> Result<()> { swarm.add_external_address(external_address.clone()); } + for rendezvous_point in &rendezvous_addrs { + let Some(peer_id) = rendezvous_point.extract_peer_id() else { + continue; + }; + + swarm.add_peer_address(peer_id, rendezvous_point.clone()); + } + let bitcoin_wallet = Arc::new(bitcoin_wallet); let (event_loop, mut swap_receiver, event_loop_service) = EventLoop::new( swarm, diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 16c62b42a..5199eb31d 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -222,8 +222,15 @@ pub mod behaviour { let pingConfig = ping::Config::new().with_timeout(Duration::from_secs(60)); let behaviour = if rendezvous_nodes.is_empty() { + tracing::warn!( + "No rendezvous nodes specified. ASB might not be discoverable for takers." + ); None } else { + tracing::info!( + "Registering with the following rendezvouse nodes: {:?}", + &rendezvous_nodes + ); Some(rendezvous::Behaviour::new(identity, rendezvous_nodes)) }; @@ -278,13 +285,14 @@ pub mod rendezvous { use std::pin::Pin; use std::task::Context; - #[derive(Clone, PartialEq)] + #[derive(Clone, PartialEq, Debug)] enum ConnectionStatus { Disconnected, Dialling, Connected, } + #[derive(Debug)] enum RegistrationStatus { RegisterOnNextConnection, Pending, @@ -300,6 +308,7 @@ pub mod rendezvous { } /// A node running the rendezvous server protocol. + #[derive(Debug)] pub struct RendezvousNode { pub address: Multiaddr, connection_status: ConnectionStatus, @@ -351,6 +360,7 @@ pub mod rendezvous { node.set_registration(RegistrationStatus::Pending); let (namespace, peer_id, ttl) = (node.namespace.into(), node.peer_id, node.registration_ttl); + tracing::info!("Sending rendezvous register request"); self.inner.register(namespace, peer_id, ttl) } } @@ -441,9 +451,9 @@ pub mod rendezvous { node.set_connection(ConnectionStatus::Disconnected); } } - FromSwarm::DialFailure(peer) => { + FromSwarm::DialFailure(failure) => { // Update the connection status of the rendezvous node that failed to connect. - if let Some(peer_id) = peer.peer_id { + if let Some(peer_id) = failure.peer_id { if let Some(node) = self .rendezvous_nodes .iter_mut() @@ -451,6 +461,11 @@ pub mod rendezvous { { node.set_connection(ConnectionStatus::Disconnected); } + tracing::error!( + "Rendezvous dial failure towards `{}`: {}", + peer_id, + failure.error + ); } } _ => {} @@ -473,14 +488,18 @@ pub mod rendezvous { cx: &mut Context<'_>, ) -> Poll>> { if let Some(peer_id) = self.to_dial.pop_front() { + let addr = self + .rendezvous_nodes + .iter() + .find(|node| node.peer_id == peer_id) + .map(|node| node.address.clone()) + .expect("We should have a rendezvous node for the peer id"); + + tracing::info!("Attempting to dial rendezvous peer `{peer_id}` @ `{addr}`"); + return Poll::Ready(ToSwarm::Dial { opts: DialOpts::peer_id(peer_id) - .addresses(vec![self - .rendezvous_nodes - .iter() - .find(|node| node.peer_id == peer_id) - .map(|node| node.address.clone()) - .expect("We should have a rendezvous node for the peer id")]) + .addresses(vec![addr]) .condition(PeerCondition::Disconnected) // TODO: this makes the behaviour call `NetworkBehaviour::handle_pending_outbound_connection` // but we don't implement it From 9066065cc806dc6fa21c3bad496eb628aed3d717 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Wed, 17 Sep 2025 02:32:31 +0200 Subject: [PATCH 18/24] fix integration tests, resume swap from tauri_command after manual cooperative redeem --- .../swap/swap/done/BitcoinPunishedPage.tsx | 11 ++------ src-gui/src/renderer/rpc.ts | 4 +-- swap/src/cli/api/request.rs | 28 ++++++++++++++++--- swap/tests/harness/mod.rs | 6 ++-- swap/tests/manual_cooperative_redeem.rs | 13 +++++++-- ...manual_cooperative_redeem_malicious_key.rs | 15 +++++++--- 6 files changed, 52 insertions(+), 25 deletions(-) diff --git a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx index d935aa014..a49286421 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx @@ -14,7 +14,7 @@ import { import FeedbackInfoBox from "renderer/components/pages/help/FeedbackInfoBox"; import { TauriSwapProgressEventExt } from "models/tauriModelExt"; import { useState } from "react"; -import { manualCooperativeRedeem, resumeSwap } from "renderer/rpc"; +import { resumeWithCooperativeRedeem, resumeSwap } from "renderer/rpc"; import { useActiveSwapId } from "store/hooks"; import { useSnackbar } from "notistack"; @@ -74,15 +74,8 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { // Try and use the given information to cooperatively redeem try { - await manualCooperativeRedeem(swapId, key, txId, txKey); - setSuccess(true); - // Wait 5 seconds to give user time to read message - await new Promise((res) => setTimeout(res, 5000)); - - // Close the modal and continue the swap normally + await resumeWithCooperativeRedeem(swapId, key, txId, txKey); onClose(); - resumeSwap(swapId); - setSuccess(null); } catch (e) { // If we get an error, throw it to the snackbar enqueueSnackbar<"error">(`Cooperative redeem failed: \`${e}\``); diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 19a4acf3c..3d0a3955f 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -257,14 +257,14 @@ export async function buyXmr( }); } -export async function manualCooperativeRedeem( +export async function resumeWithCooperativeRedeem( swapId: string, s_a: string, txId: string, txKey: string, ) { await invoke( - "manual_cooperative_redeem", + "resume_with_cooperative_redeem", { swap_id: swapId, s_a, diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs index db4d86634..c5db6c5c5 100644 --- a/swap/src/cli/api/request.rs +++ b/swap/src/cli/api/request.rs @@ -154,7 +154,7 @@ impl Request for ManualCooperativeRedeemArgs { type Response = ManualCooperativeRedeemResponse; async fn request(self, ctx: Arc) -> Result { - manual_cooperative_redeem(self, ctx).await + resume_with_cooperative_redeem(self, ctx).await } } @@ -1183,8 +1183,8 @@ pub async fn buy_xmr( Ok(BuyXmrResponse { swap_id, quote }) } -#[tracing::instrument(fields(method = "manual_cooperative_redeem"), skip(context))] -pub async fn manual_cooperative_redeem( +#[tracing::instrument(fields(method = "resume_with_cooperative_redeem"), skip(context))] +pub async fn resume_with_cooperative_redeem( args: ManualCooperativeRedeemArgs, context: Arc, ) -> Result { @@ -1201,7 +1201,15 @@ pub async fn manual_cooperative_redeem( bail!("Bitcoin wasn't punished - can't cooperatively redeem"); }; - // Construct and insert the new state, checking the key we received + // Attempt to acquire the swap lock -- the swap shouldn't be running if we are punished + // and already tried (and failed) the automated cooperative redeem attempt + context + .swap_lock + .acquire_swap_lock(args.swap_id) + .await + .context("Couldn't acquire swap lock")?; + + // Checking the key we received for validity, then advance and save the state let state5 = state .attempt_cooperative_redeem( args.s_a.scalar, @@ -1216,6 +1224,18 @@ pub async fn manual_cooperative_redeem( .await .context("couldn't save new state to db")?; + tracing::info!("Validated cooperative redeem key, resuming swap"); + + // Resume the swap (next step: redeem Monero) + resume_swap( + ResumeSwapArgs { + swap_id: args.swap_id, + }, + context, + ) + .await + .context("Couldn't resume the swap after manually importing cooperative redeem key")?; + Ok(ManualCooperativeRedeemResponse) } diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index 6c512b835..1fac8489a 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -291,7 +291,7 @@ async fn start_alice( ) .unwrap(); - let rpc_host = "http://127.0.0.1".to_string(); + let rpc_host = "127.0.0.1".to_string(); let rpc_port = get_port().expect("port to be available"); let rpc_server = RpcServer::start( rpc_host.clone(), @@ -304,9 +304,9 @@ async fn start_alice( .await .unwrap(); - std::mem::forget(rpc_server.spawn()); // Avoid drop or else we'll abort the process + std::mem::forget(rpc_server.spawn()); // Dropping would abort the rpc process - let rpc_url = format!("{rpc_host}:{rpc_port}"); + let rpc_url = format!("http://{rpc_host}:{rpc_port}"); let rpc_client = jsonrpsee::http_client::HttpClientBuilder::default() .build(&rpc_url) .expect("rpc client to be built"); diff --git a/swap/tests/manual_cooperative_redeem.rs b/swap/tests/manual_cooperative_redeem.rs index af5ff4e45..d5eca19a9 100644 --- a/swap/tests/manual_cooperative_redeem.rs +++ b/swap/tests/manual_cooperative_redeem.rs @@ -8,7 +8,7 @@ use swap::protocol::bob::BobState; use swap::protocol::{alice, bob, State}; use swap_controller_api::AsbApiClient; -use crate::harness::bob_run_until::is_btc_punished; +use crate::harness::bob_run_until::{is_btc_locked, is_btc_punished}; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice /// the encsig and fail to refund or redeem. Alice punishes. Bob then cooperates with Alice and redeems XMR with her key. @@ -18,17 +18,24 @@ async fn alice_and_bob_manual_cooperative_redeem_after_punish() { harness::setup_test(FastPunishConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap_id = bob_swap.id; - let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_punished)); + let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let alice_swap = ctx.alice_next_swap().await; let alice_swap = tokio::spawn(alice::run(alice_swap, FixedRate::default())); let bob_state = bob_swap.await??; - assert!(matches!(bob_state, BobState::BtcPunished { .. })); + assert!(matches!(bob_state, BobState::BtcLocked { .. })); let alice_state = alice_swap.await??; ctx.assert_alice_punished(alice_state).await; + // Let bob realize he was punished + let (bob_swap, bob_join_handle) = ctx + .stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id) + .await; + let bob_state = tokio::spawn(bob::run_until(bob_swap, is_btc_punished)).await??; + assert!(matches!(bob_state, BobState::BtcPunished { .. })); + // Manually do the cooperative redeem via rpc server let manual_cooperative_redeem_info = ctx .alice_rpc_client diff --git a/swap/tests/manual_cooperative_redeem_malicious_key.rs b/swap/tests/manual_cooperative_redeem_malicious_key.rs index 4952a5762..b8f67be54 100644 --- a/swap/tests/manual_cooperative_redeem_malicious_key.rs +++ b/swap/tests/manual_cooperative_redeem_malicious_key.rs @@ -8,28 +8,35 @@ use swap::protocol::bob::BobState; use swap::protocol::{alice, bob, State}; use swap_controller_api::AsbApiClient; -use crate::harness::bob_run_until::is_btc_punished; +use crate::harness::bob_run_until::{is_btc_locked, is_btc_punished}; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice /// the encsig and fail to refund or redeem. Alice punishes. Bob then cooperates with Alice and redeems XMR with her key. /// But this time, we use the manual export of the cooperative redeem key via the asb-controller. /// And also, alice sends a malicious key! So we expect the cooperative redeem check to fail before changing states. #[tokio::test] -async fn alice_and_bob_manual_cooperative_redeem_after_punish() { +async fn bob_rejects_malicious_cooperative_redeem_key() { harness::setup_test(FastPunishConfig, |mut ctx| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap_id = bob_swap.id; - let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_punished)); + let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let alice_swap = ctx.alice_next_swap().await; let alice_swap = tokio::spawn(alice::run(alice_swap, FixedRate::default())); let bob_state = bob_swap.await??; - assert!(matches!(bob_state, BobState::BtcPunished { .. })); + assert!(matches!(bob_state, BobState::BtcLocked { .. })); let alice_state = alice_swap.await??; ctx.assert_alice_punished(alice_state).await; + // Let bob realize he was punished + let (bob_swap, bob_join_handle) = ctx + .stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id) + .await; + let bob_state = tokio::spawn(bob::run_until(bob_swap, is_btc_punished)).await??; + assert!(matches!(bob_state, BobState::BtcPunished { .. })); + // Manually do the cooperative redeem via rpc server let mut manual_cooperative_redeem_info = ctx .alice_rpc_client From 9945bb2111c9571a113a9cec8b926ca36f256343 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Fri, 19 Sep 2025 02:48:43 +0200 Subject: [PATCH 19/24] fix tauri missing libwinpthread-1.dll --- dev_scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh | 3 ++- src-tauri/tauri.windows.conf.json | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dev_scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh b/dev_scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh index 3e9224171..0c05eff2e 100755 --- a/dev_scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh +++ b/dev_scripts/ubuntu_build_x86_86-w64-mingw32-gcc.sh @@ -293,6 +293,7 @@ build_winpthreads() { copy_dlls() { echo "Copying dll's to src-tauri/" cp -f $PREFIX/x86_64-w64-mingw32/lib/{libstdc++-6,libgcc_s_seh-1}.dll $SRC_TAURI_DIR/ + cp -f $PREFIX/x86_64-w64-mingw32/bin/libwinpthread-1.dll $SRC_TAURI_DIR/ } setup_path() { @@ -349,7 +350,7 @@ verify_installation() { # Check DLLs in src-tauri directory local missing_dlls=() - for dll in libstdc++-6.dll libgcc_s_seh-1.dll; do + for dll in libstdc++-6.dll libgcc_s_seh-1.dll libwinpthread-1.dll; do if [ ! -f "$SRC_TAURI_DIR/$dll" ]; then missing_dlls+=("$dll") fi diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index a1c9a1a3d..9ecc296e1 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,5 +1,9 @@ { "bundle": { - "resources": ["libstdc++-6.dll", "libgcc_s_seh-1.dll"] + "resources": [ + "libstdc++-6.dll", + "libgcc_s_seh-1.dll", + "libwinpthread-1.dll" + ] } } From d08cdb9f209127298d82a70b781332c20d07fe62 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Thu, 25 Sep 2025 21:15:21 +0200 Subject: [PATCH 20/24] satisfy clippy --- .vscode/settings.json | 2 +- Cargo.lock | 3 --- monero-seed/src/tests.rs | 2 +- monero-sys/build.rs | 4 ++-- swap-controller-api/Cargo.toml | 5 ----- swap-controller/src/main.rs | 2 +- swap-env/src/prompt.rs | 21 +++++++++++++-------- swap-feed/src/rate.rs | 5 ++--- throttle/src/throttle.rs | 13 ++++++------- 9 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ee485c36..8dcc918ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -78,4 +78,4 @@ "rust-analyzer.cargo.extraEnv": { "CARGO_TARGET_DIR": "target-check" } -} +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3beba3a24..6e1fbda68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13554,13 +13554,10 @@ name = "swap-controller-api" version = "0.1.0" dependencies = [ "bitcoin 0.32.7", - "curve25519-dalek-ng", "jsonrpsee 0.25.1", "monero", "serde", - "serde_json", "swap-serde", - "tokio", "uuid", ] diff --git a/monero-seed/src/tests.rs b/monero-seed/src/tests.rs index fb66dc496..152616ed5 100644 --- a/monero-seed/src/tests.rs +++ b/monero-seed/src/tests.rs @@ -3,7 +3,7 @@ use zeroize::Zeroizing; use curve25519_dalek::scalar::Scalar; -use monero_primitives::keccak256; +use monero_oxide::primitives::keccak256; use crate::*; diff --git a/monero-sys/build.rs b/monero-sys/build.rs index 62bff9442..f12841e8b 100644 --- a/monero-sys/build.rs +++ b/monero-sys/build.rs @@ -496,14 +496,14 @@ fn execute_child_with_pipe( // Spawn threads to handle stdout and stderr let stdout_handle = thread::spawn(move || { let reader = BufReader::new(stdout); - for line in reader.lines().flatten() { + for line in reader.lines().map_while(Result::ok) { println!("cargo:debug={}{}", &prefix_clone, line); } }); let stderr_handle = thread::spawn(move || { let reader = BufReader::new(stderr); - for line in reader.lines().flatten() { + for line in reader.lines().map_while(Result::ok) { println!("cargo:debug={}{}", &prefix, line); } }); diff --git a/swap-controller-api/Cargo.toml b/swap-controller-api/Cargo.toml index 5520623e6..0528805cb 100644 --- a/swap-controller-api/Cargo.toml +++ b/swap-controller-api/Cargo.toml @@ -7,14 +7,9 @@ edition = "2021" bitcoin = { workspace = true } jsonrpsee = { workspace = true, features = ["macros", "server", "client-core", "http-client"] } serde = { workspace = true } -serde_json = { workspace = true } -curve25519-dalek = { workspace = true } monero = { workspace = true } swap-serde = { workspace = true } uuid = { workspace = true } -[dev-dependencies] -tokio = { workspace = true, features = ["test-util"] } - [lints] workspace = true diff --git a/swap-controller/src/main.rs b/swap-controller/src/main.rs index 502d09d82..a6508558c 100644 --- a/swap-controller/src/main.rs +++ b/swap-controller/src/main.rs @@ -85,7 +85,7 @@ async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> { println!("Descriptor (BIP-0382) containing the private keys of the internal Bitcoin wallet:\n{}", response.descriptor); } Cmd::CooperativeRedeemKey { swap_id } => { - let response = client.cooperative_redeem_info(swap_id.clone()).await?; + let response = client.cooperative_redeem_info(swap_id).await?; let Some(response) = response else { println!("Couldn't find any swap with id {swap_id} in the database"); diff --git a/swap-env/src/prompt.rs b/swap-env/src/prompt.rs index 38b95df6f..68b8a27c0 100644 --- a/swap-env/src/prompt.rs +++ b/swap-env/src/prompt.rs @@ -1,14 +1,14 @@ use std::path::{Path, PathBuf}; use crate::defaults::{ - default_rendezvous_points, DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, + DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, default_rendezvous_points, }; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use dialoguer::Confirm; -use dialoguer::{theme::ColorfulTheme, Input, Select}; +use dialoguer::{Input, Select, theme::ColorfulTheme}; use libp2p::Multiaddr; -use rust_decimal::prelude::FromPrimitive; use rust_decimal::Decimal; +use rust_decimal::prelude::FromPrimitive; use url::Url; /// Prompt user for data directory @@ -49,7 +49,7 @@ pub fn listen_addresses(default_listen_address: &Multiaddr) -> Result) -> Result> { +pub fn electrum_rpc_urls(default_electrum_urls: &[Url]) -> Result> { println!( "You can configure multiple Electrum servers for redundancy. At least one is required." ); @@ -64,7 +64,7 @@ pub fn electrum_rpc_urls(default_electrum_urls: &Vec) -> Result> { .default(true) .interact()? { - true => default_electrum_urls.clone(), + true => default_electrum_urls.to_vec(), false => Vec::new(), }; @@ -127,7 +127,9 @@ pub fn tor_hidden_service() -> Result { "Your ASB can run a hidden service for itself. It'll be reachable at an .onion address." ); println!("You do not have to run a Tor daemon yourself. You do not have to manage anything."); - println!("This will hide your IP address and allow you to run from behind a firewall without opening ports."); + println!( + "This will hide your IP address and allow you to run from behind a firewall without opening ports." + ); println!(); let selection = Select::with_theme(&ColorfulTheme::default()) @@ -168,7 +170,10 @@ pub fn ask_spread() -> Result { .interact_text()?; if !(0.0..=1.0).contains(&ask_spread) { - bail!(format!("Invalid spread {}. For the spread value floating point number in interval [0..1] are allowed.", ask_spread)) + bail!(format!( + "Invalid spread {}. For the spread value floating point number in interval [0..1] are allowed.", + ask_spread + )) } Decimal::from_f64(ask_spread).context("Unable to parse spread") diff --git a/swap-feed/src/rate.rs b/swap-feed/src/rate.rs index cb29ad954..92011eeec 100644 --- a/swap-feed/src/rate.rs +++ b/swap-feed/src/rate.rs @@ -177,9 +177,8 @@ mod tests { .sell_quote(bitcoin::Amount::ONE_BTC) .unwrap(); - let xmr_factor = xmr_no_spread.into().as_piconero_decimal() - / xmr_with_spread.into().as_piconero_decimal() - - ONE; + let xmr_factor = + Decimal::from(xmr_no_spread.as_pico()) / Decimal::from(xmr_with_spread.as_pico()) - ONE; assert!(xmr_with_spread < xmr_no_spread); assert_eq!(xmr_factor.round_dp(8), TWO_PERCENT); // round to 8 decimal diff --git a/throttle/src/throttle.rs b/throttle/src/throttle.rs index c092d643c..f1b559577 100644 --- a/throttle/src/throttle.rs +++ b/throttle/src/throttle.rs @@ -2,12 +2,12 @@ // MIT License use std::pin::Pin; -use std::sync::{mpsc, Arc, Mutex}; +use std::sync::{Arc, Mutex, mpsc}; use std::time::{self, /* SystemTime, UNIX_EPOCH, */ Duration}; pub fn throttle(closure: F, delay: Duration) -> Throttle where - F: Fn(T) -> () + Send + Sync + 'static, + F: Fn(T) + Send + Sync + 'static, T: Send + Sync + 'static, { let (sender, receiver) = mpsc::channel(); @@ -18,7 +18,7 @@ where })); let dup_throttle_config = throttle_config.clone(); - let throttle = Throttle { + Throttle { sender: Some(sender), thread: Some(std::thread::spawn(move || { let throttle_config = dup_throttle_config; @@ -51,7 +51,7 @@ where } } } else { - let message = receiver.recv_timeout((*throttle_config.lock().unwrap()).delay); + let message = receiver.recv_timeout((throttle_config.lock().unwrap()).delay); let now = time::Instant::now(); match message { Ok(param) => { @@ -90,12 +90,11 @@ where } })), throttle_config, - }; - throttle + } } struct ThrottleConfig { - closure: Pin () + Send + Sync + 'static>>, + closure: Pin>, delay: Duration, } impl Drop for ThrottleConfig { From 1997f184af12e46964b1b7405f6a46c4e86aff0a Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sat, 27 Sep 2025 23:15:11 +0200 Subject: [PATCH 21/24] fix test compilation errors --- .github/workflows/ci.yml | 4 ++++ monero-sys/src/lib.rs | 12 ++++++++++-- monero-sys/tests/sign_message.rs | 5 +---- monero-sys/tests/simple.rs | 5 +---- monero-sys/tests/special_paths.rs | 5 +---- monero-sys/tests/wallet_closing.rs | 5 +---- swap/src/asb/event_loop.rs | 2 +- swap/src/cli/api.rs | 6 +++--- swap/src/monero.rs | 6 ++++-- swap/tests/manual_cooperative_redeem.rs | 3 ++- .../tests/manual_cooperative_redeem_malicious_key.rs | 3 ++- 11 files changed, 30 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7c961ac3..9a8dc17ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,10 @@ jobs: test_name: alice_empty_balance_after_started_btc_early_refund - package: swap test_name: alice_broken_wallet_rpc_after_started_btc_early_refund + - package: swap + test_name: manual_cooperative_redeem + - package: swap + test_name: manual_cooperative_redeem_malicious_key.rs runs-on: ubuntu-22.04 if: github.event_name == 'push' || !github.event.pull_request.draft diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs index 6d9347103..a3bd6851d 100644 --- a/monero-sys/src/lib.rs +++ b/monero-sys/src/lib.rs @@ -152,10 +152,10 @@ pub struct Daemon { pub ssl: bool, } -impl TryFrom for Daemon { +impl<'a> TryFrom<&'a str> for Daemon { type Error = anyhow::Error; - fn try_from(address: String) -> Result { + fn try_from(address: &'a str) -> Result { let url = Url::parse(&address).context("Failed to parse daemon URL")?; let hostname = url @@ -177,6 +177,14 @@ impl TryFrom for Daemon { } } +impl<'a> TryFrom<&'a String> for Daemon { + type Error = anyhow::Error; + + fn try_from(value: &'a String) -> std::result::Result { + Daemon::try_from(value.as_str()) + } +} + impl Daemon { /// Try to convert the daemon configuration to a URL pub fn to_url_string(&self) -> String { diff --git a/monero-sys/tests/sign_message.rs b/monero-sys/tests/sign_message.rs index 48c31bf17..0f97ed851 100644 --- a/monero-sys/tests/sign_message.rs +++ b/monero-sys/tests/sign_message.rs @@ -10,10 +10,7 @@ async fn test_sign_message() { .init(); let temp_dir = tempfile::tempdir().unwrap(); - let daemon = Daemon { - address: PLACEHOLDER_NODE.into(), - ssl: false, - }; + let daemon = Daemon::try_from(PLACEHOLDER_NODE).unwrap(); let wallet_name = "test_signing_wallet"; let wallet_path = temp_dir.path().join(wallet_name).display().to_string(); diff --git a/monero-sys/tests/simple.rs b/monero-sys/tests/simple.rs index 4994900fe..496d76354 100644 --- a/monero-sys/tests/simple.rs +++ b/monero-sys/tests/simple.rs @@ -15,10 +15,7 @@ async fn main() { .init(); let temp_dir = tempfile::tempdir().unwrap(); - let daemon = Daemon { - address: STAGENET_REMOTE_NODE.into(), - ssl: true, - }; + let daemon = Daemon::try_from(STAGENET_REMOTE_NODE).unwrap(); let wallet_name = "recovered_wallet"; let wallet_path = temp_dir.path().join(wallet_name).display().to_string(); diff --git a/monero-sys/tests/special_paths.rs b/monero-sys/tests/special_paths.rs index a3e6ed511..c3c3cb0da 100644 --- a/monero-sys/tests/special_paths.rs +++ b/monero-sys/tests/special_paths.rs @@ -13,10 +13,7 @@ async fn test_wallet_with_special_paths() { "path-with-hyphen", ]; - let daemon = Daemon { - address: "https://moneronode.org:18081".into(), - ssl: true, - }; + let daemon = Daemon::try_from("https://moneronode.org:18081").unwrap(); let futures = special_paths .into_iter() diff --git a/monero-sys/tests/wallet_closing.rs b/monero-sys/tests/wallet_closing.rs index 25d11f44c..af719450b 100644 --- a/monero-sys/tests/wallet_closing.rs +++ b/monero-sys/tests/wallet_closing.rs @@ -10,10 +10,7 @@ async fn main() { .init(); let temp_dir = tempfile::tempdir().unwrap(); - let daemon = Daemon { - address: STAGENET_REMOTE_NODE.into(), - ssl: true, - }; + let daemon = Daemon::try_from(STAGENET_REMOTE_NODE).unwrap(); { let wallet = WalletHandle::open_or_create( diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 89f6f7ff9..93f5508c1 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -1189,7 +1189,7 @@ mod tests { rate.clone(), || async { Ok(balance) }, || async { Ok(reserved_items) }, - None, + Decimal::ZERO, ) .await .unwrap(); diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index f94fbbb8a..c683288ba 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -441,7 +441,7 @@ impl ContextBuilder { }; // Create a daemon struct for the monero wallet based on the node address - let daemon = monero_sys::Daemon::try_from(monero_node_address)?; + let daemon = monero_sys::Daemon::try_from(&monero_node_address)?; // Prompt the user to open/create a Monero wallet let (wallet, seed) = open_monero_wallet( @@ -662,12 +662,12 @@ impl Context { let pool_url: String = server_info.clone().into(); tracing::info!("Switching to Monero RPC pool: {}", pool_url); - monero_sys::Daemon::try_from(pool_url)? + monero_sys::Daemon::try_from(&pool_url)? } MoneroNodeConfig::SingleNode { url } => { tracing::info!("Switching to single Monero node: {}", url); - monero_sys::Daemon::try_from(url.clone())? + monero_sys::Daemon::try_from(&url)? } }; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 3866ee1c9..e96f9db06 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -679,10 +679,12 @@ mod tests { use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] - pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey); + pub struct MoneroPrivateKey( + #[serde(with = "swap_serde::monero::private_key")] crate::monero::PrivateKey, + ); #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] - pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount); + pub struct MoneroAmount(crate::monero::Amount); #[test] fn serde_monero_private_key_json() { diff --git a/swap/tests/manual_cooperative_redeem.rs b/swap/tests/manual_cooperative_redeem.rs index d5eca19a9..703fcf79a 100644 --- a/swap/tests/manual_cooperative_redeem.rs +++ b/swap/tests/manual_cooperative_redeem.rs @@ -9,13 +9,14 @@ use swap::protocol::{alice, bob, State}; use swap_controller_api::AsbApiClient; use crate::harness::bob_run_until::{is_btc_locked, is_btc_punished}; +use crate::harness::TestContext; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice /// the encsig and fail to refund or redeem. Alice punishes. Bob then cooperates with Alice and redeems XMR with her key. /// But this time, we use the manual export of the cooperative redeem key via the asb-controller. #[tokio::test] async fn alice_and_bob_manual_cooperative_redeem_after_punish() { - harness::setup_test(FastPunishConfig, |mut ctx| async move { + harness::setup_test(FastPunishConfig, |mut ctx: TestContext| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); diff --git a/swap/tests/manual_cooperative_redeem_malicious_key.rs b/swap/tests/manual_cooperative_redeem_malicious_key.rs index b8f67be54..f39b303c8 100644 --- a/swap/tests/manual_cooperative_redeem_malicious_key.rs +++ b/swap/tests/manual_cooperative_redeem_malicious_key.rs @@ -9,6 +9,7 @@ use swap::protocol::{alice, bob, State}; use swap_controller_api::AsbApiClient; use crate::harness::bob_run_until::{is_btc_locked, is_btc_punished}; +use crate::harness::TestContext; /// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice /// the encsig and fail to refund or redeem. Alice punishes. Bob then cooperates with Alice and redeems XMR with her key. @@ -16,7 +17,7 @@ use crate::harness::bob_run_until::{is_btc_locked, is_btc_punished}; /// And also, alice sends a malicious key! So we expect the cooperative redeem check to fail before changing states. #[tokio::test] async fn bob_rejects_malicious_cooperative_redeem_key() { - harness::setup_test(FastPunishConfig, |mut ctx| async move { + harness::setup_test(FastPunishConfig, |mut ctx: TestContext| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); From 67e8db312720119e04a505f045a7a5dbd0a07345 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Sat, 27 Sep 2025 23:53:35 +0200 Subject: [PATCH 22/24] fix tests --- monero-rpc-pool/src/proxy.rs | 1 - monero-sys/build.rs | 18 +++++++++--------- swap-asb/src/main.rs | 7 ------- swap/tests/manual_cooperative_redeem.rs | 2 +- .../manual_cooperative_redeem_malicious_key.rs | 2 +- 5 files changed, 11 insertions(+), 19 deletions(-) diff --git a/monero-rpc-pool/src/proxy.rs b/monero-rpc-pool/src/proxy.rs index 85e37f7bf..3dba5db11 100644 --- a/monero-rpc-pool/src/proxy.rs +++ b/monero-rpc-pool/src/proxy.rs @@ -16,7 +16,6 @@ use tokio::{ }; use tokio_rustls::rustls::{ - self, client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, pki_types::{CertificateDer, ServerName, UnixTime}, DigitallySignedStruct, Error as TlsError, SignatureScheme, diff --git a/monero-sys/build.rs b/monero-sys/build.rs index f12841e8b..319586ee5 100644 --- a/monero-sys/build.rs +++ b/monero-sys/build.rs @@ -110,7 +110,7 @@ fn main() { .display() .to_string(); config.define("CMAKE_TOOLCHAIN_FILE", toolchain_file.clone()); - println!("cargo:warning=Using toolchain file: {}", toolchain_file); + println!("cargo:debug=Using toolchain file: {}", toolchain_file); let depends_lib_dir = contrib_depends_dir.join(format!("{}/lib", target)); @@ -414,7 +414,7 @@ fn compile_dependencies( "aarch64-apple-ios-sim" => "aarch64-apple-iossimulator".to_string(), _ => target, }; - println!("cargo:warning=Building for target: {}", target); + println!("cargo:debug=Building for target: {}", target); match target.as_str() { "x86_64-apple-darwin" @@ -431,7 +431,7 @@ fn compile_dependencies( } println!( - "cargo:warning=Running make HOST={} in contrib/depends", + "cargo:debug=Running make HOST={} in contrib/depends", target ); @@ -528,7 +528,7 @@ fn apply_patches() -> Result<(), Box> { for embedded in EMBEDDED_PATCHES { println!( - "cargo:warning=Processing embedded patch: {} ({})", + "cargo:debug=Processing embedded patch: {} ({})", embedded.name, embedded.description ); @@ -541,14 +541,14 @@ fn apply_patches() -> Result<(), Box> { } println!( - "cargo:warning=Found {} file(s) in patch {}", + "cargo:debug=Found {} file(s) in patch {}", file_patches.len(), embedded.name ); // Apply each file patch individually for (file_path, patch_content) in file_patches { - println!("cargo:warning=Applying patch to file: {}", file_path); + println!("cargo:debug=Applying patch to file: {}", file_path); // Parse the individual file patch let patch = diffy::Patch::from_str(&patch_content) @@ -566,7 +566,7 @@ fn apply_patches() -> Result<(), Box> { // Check if patch is already applied by trying to reverse it if diffy::apply(¤t, &patch.reverse()).is_ok() { println!( - "cargo:warning=Patch for {} already applied – skipping", + "cargo:debug=Patch for {} already applied – skipping", file_path ); continue; @@ -578,11 +578,11 @@ fn apply_patches() -> Result<(), Box> { fs::write(&target_path, patched) .map_err(|e| format!("Failed to write {}: {}", file_path, e))?; - println!("cargo:warning=Successfully applied patch to: {}", file_path); + println!("cargo:debug=Successfully applied patch to: {}", file_path); } println!( - "cargo:warning=Successfully applied all file patches for: {} ({})", + "cargo:debug=Successfully applied all file patches for: {} ({})", embedded.name, embedded.description ); } diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index 979753aa1..493cf72ac 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -271,13 +271,6 @@ pub async fn main() -> Result<()> { swarm.add_external_address(external_address.clone()); } - for rendezvous_point in &rendezvous_addrs { - let Some(peer_id) = rendezvous_point.extract_peer_id() else { - continue; - }; - - swarm.add_peer_address(peer_id, rendezvous_point.clone()); - } let tip_config = { let tip_address = monero::Address::from_str(match env_config.monero_network { monero::Network::Mainnet => { diff --git a/swap/tests/manual_cooperative_redeem.rs b/swap/tests/manual_cooperative_redeem.rs index 703fcf79a..a4323ec8a 100644 --- a/swap/tests/manual_cooperative_redeem.rs +++ b/swap/tests/manual_cooperative_redeem.rs @@ -16,7 +16,7 @@ use crate::harness::TestContext; /// But this time, we use the manual export of the cooperative redeem key via the asb-controller. #[tokio::test] async fn alice_and_bob_manual_cooperative_redeem_after_punish() { - harness::setup_test(FastPunishConfig, |mut ctx: TestContext| async move { + harness::setup_test(FastPunishConfig, None, |mut ctx: TestContext| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); diff --git a/swap/tests/manual_cooperative_redeem_malicious_key.rs b/swap/tests/manual_cooperative_redeem_malicious_key.rs index f39b303c8..b9d13fa1b 100644 --- a/swap/tests/manual_cooperative_redeem_malicious_key.rs +++ b/swap/tests/manual_cooperative_redeem_malicious_key.rs @@ -17,7 +17,7 @@ use crate::harness::TestContext; /// And also, alice sends a malicious key! So we expect the cooperative redeem check to fail before changing states. #[tokio::test] async fn bob_rejects_malicious_cooperative_redeem_key() { - harness::setup_test(FastPunishConfig, |mut ctx: TestContext| async move { + harness::setup_test(FastPunishConfig, None, |mut ctx: TestContext| async move { let (bob_swap, bob_join_handle) = ctx.bob_swap().await; let bob_swap_id = bob_swap.id; let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); From 45bee3dde3e868fbc0b9c95978b88ce793373d06 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Mon, 13 Oct 2025 16:16:47 +0200 Subject: [PATCH 23/24] add asb command manual-recovery export-cooperative-redeem-key --swap-id --- src-gui/package.json | 2 +- src-gui/yarn.lock | 8 ++++---- swap-asb/src/command.rs | 21 +++++++++++++++++++++ swap-asb/src/main.rs | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src-gui/package.json b/src-gui/package.json index c1983e336..967df85c8 100644 --- a/src-gui/package.json +++ b/src-gui/package.json @@ -27,7 +27,7 @@ "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-cli": "^2.4.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0", - "@tauri-apps/plugin-dialog": "^2.0.0", + "@tauri-apps/plugin-dialog": "2.4.0", "@tauri-apps/plugin-opener": "^2.5.0", "@tauri-apps/plugin-process": "^2.3.0", "@tauri-apps/plugin-shell": "^2.3.0", diff --git a/src-gui/yarn.lock b/src-gui/yarn.lock index db569fe86..0f2b112e0 100644 --- a/src-gui/yarn.lock +++ b/src-gui/yarn.lock @@ -1025,10 +1025,10 @@ dependencies: "@tauri-apps/api" "^2.6.0" -"@tauri-apps/plugin-dialog@^2.0.0": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.3.3.tgz#0bfd78f1f754157b39295a33826afd41f769f28f" - integrity sha512-cWXB9QJDbLIA0v7I5QY183awazBEQNPhp19iPvrMZoJRX8SbFkhWFx1/q7zy7xGpXXzxz29qtq6z21Ho7W5Iew== +"@tauri-apps/plugin-dialog@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.0.tgz#e37388bd3197f920a9975e323a105a75cb55d648" + integrity sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w== dependencies: "@tauri-apps/api" "^2.8.0" diff --git a/swap-asb/src/command.rs b/swap-asb/src/command.rs index 11ffa5587..711ce1bb5 100644 --- a/swap-asb/src/command.rs +++ b/swap-asb/src/command.rs @@ -161,6 +161,16 @@ where env_config: env_config(testnet), cmd: Command::Punish { swap_id }, }, + RawCommand::ManualRecovery(ManualRecovery::ExportCooperativeRedeemKey { swap_id }) => { + Arguments { + testnet, + json, + trace, + config_path: config_path(config, testnet)?, + env_config: env_config(testnet), + cmd: Command::ExportCooperativeRedeemKey { swap_id }, + } + } RawCommand::ManualRecovery(ManualRecovery::SafelyAbort { swap_id }) => Arguments { testnet, json, @@ -250,6 +260,9 @@ pub enum Command { SafelyAbort { swap_id: Uuid, }, + ExportCooperativeRedeemKey { + swap_id: Uuid, + }, ExportBitcoinWallet, ExportMoneroWallet, } @@ -391,6 +404,14 @@ pub enum ManualRecovery { #[structopt(flatten)] punish_params: RecoverCommandParams, }, + #[structopt(about = "Export the cooperative redeem information manually.")] + ExportCooperativeRedeemKey { + #[structopt( + long = "swap-id", + help = "The swap id for which to releave the cooperative redeem information." + )] + swap_id: Uuid, + }, #[structopt(about = "Safely Abort requires the swap to be in a state prior to locking XMR.")] SafelyAbort { #[structopt( diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index 493cf72ac..764169bc3 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -18,13 +18,13 @@ use libp2p::Swarm; use monero_sys::Daemon; use rust_decimal::prelude::FromPrimitive; use rust_decimal::Decimal; +use serde::Serialize; use std::convert::TryInto; use std::env; use std::str::FromStr; use std::sync::Arc; use structopt::clap; use structopt::clap::ErrorKind; -use swap::libp2p_ext::MultiAddrExt; mod command; use command::{parse_args, Arguments, Command}; use swap::asb::rpc::RpcServer; @@ -466,6 +466,40 @@ pub async fn main() -> Result<()> { tracing::info!("Punish transaction successfully published with id {}", txid); } + Command::ExportCooperativeRedeemKey { swap_id } => { + let db = open_db(db_file, AccessMode::ReadWrite, None).await?; + + let state = db + .get_state(swap_id) + .await + .context("Failed to query database for swap state")?; + + let (state3, transfer_proof) = match state { + State::Alice(AliceState::BtcPunished { state3, transfer_proof }) => (state3, transfer_proof), + State::Alice(otherwise) => bail!("Can't reveal cooperative redeem key because we haven't punished yet. Current state: {otherwise}"), + _ => bail!("Can't reveal cooperative redeem key because this is a Bob state") + }; + + // We need a small serialization wrapper around the Monero PrivateKeys + struct PrivateKeySerializationWrapper<'a>(&'a monero::PrivateKey); + impl<'a> Serialize for PrivateKeySerializationWrapper<'a> { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + swap_serde::monero::private_key::serialize(self.0, serializer) + } + } + + let s_a = serde_json::to_string(&PrivateKeySerializationWrapper( + &monero::PrivateKey::from_scalar(state3.s_a), + ))?; + let txid = transfer_proof.tx_hash().0; + let txkey = + serde_json::to_string(&PrivateKeySerializationWrapper(&transfer_proof.tx_key()))?; + + println!("Printing secret cooperative redeem key for swap {swap_id}.\ns_a: {s_a}\nlock transaction id: {txid}\nlock transaction key: {txkey}"); + } Command::SafelyAbort { swap_id } => { let db = open_db(db_file, AccessMode::ReadWrite, None).await?; From fa23dfade8c923dec2250210178b155d37098a2e Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Tue, 14 Oct 2025 11:27:17 +0200 Subject: [PATCH 24/24] fix logic bug --- .../pages/swap/swap/done/BitcoinPunishedPage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx index a49286421..02c9178df 100644 --- a/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx +++ b/src-gui/src/renderer/components/pages/swap/swap/done/BitcoinPunishedPage.tsx @@ -25,7 +25,9 @@ export default function BitcoinPunishedPage({ | TauriSwapProgressEventExt<"BtcPunished"> | TauriSwapProgressEventExt<"CooperativeRedeemRejected">; }) { - const [modalOpen, setModalOpen] = useState(false); + const [modalOpen, setModalOpen] = useState( + state.type === "CooperativeRedeemRejected", + ); return ( <> @@ -47,7 +49,7 @@ export default function BitcoinPunishedPage({ setModalOpen(true)} + onClose={() => setModalOpen(false)} /> ); @@ -108,7 +110,7 @@ function ManualCoopRedeemModal({ open, onClose }: ManualCoopRedeemModalProps) { setKey(e.target.value)} /> - Attempt + Attempt cooperative redeem