pub mod harness; #[cfg(test)] mod test { use anyhow::Result; use jsonrpsee::ws_client::WsClientBuilder; use jsonrpsee_core::client::{Client, ClientT}; use jsonrpsee_core::params::ObjectParams; use serial_test::serial; use serde_json::Value; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use swap::api::request::{Method, Request}; use swap::api::Context; use crate::harness::alice_run_until::is_xmr_lock_transaction_sent; use crate::harness::bob_run_until::is_btc_locked; use crate::harness::{setup_test, SlowCancelConfig, TestContext}; use swap::asb::FixedRate; use swap::protocol::{alice, bob}; use swap::tracing_ext::{capture_logs, MakeCapturingWriter}; use tracing_subscriber::filter::LevelFilter; use uuid::Uuid; const SERVER_ADDRESS: &str = "127.0.0.1:1234"; const SERVER_START_TIMEOUT_SECS: u64 = 50; const BITCOIN_ADDR: &str = "bcrt1qahvhjfc7vx5857zf8knxs8yp5lkm26jgyt0k76"; const MONERO_ADDR: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a"; const SELLER: &str = "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b"; pub async fn setup_daemon( harness_ctx: TestContext, ) -> (Client, MakeCapturingWriter, Arc) { let writer = capture_logs(LevelFilter::DEBUG); let server_address: SocketAddr = SERVER_ADDRESS.parse().unwrap(); let request = Request::new(Method::StartDaemon { server_address: Some(server_address), }); let context = Arc::new(harness_ctx.get_bob_context().await); let context_clone = context.clone(); tokio::spawn(async move { if let Err(err) = request.call(context_clone).await { println!("Failed to initialize daemon for testing: {}", err); } }); for _ in 0..SERVER_START_TIMEOUT_SECS { if writer.captured().contains("Started RPC server") { let url = format!("ws://{}", SERVER_ADDRESS); let client = WsClientBuilder::default().build(&url).await.unwrap(); return (client, writer, context); } tokio::time::sleep(Duration::from_secs(1)).await; } panic!( "Failed to start RPC server after {} seconds", SERVER_START_TIMEOUT_SECS ); } fn assert_has_keys_serde(map: &serde_json::Map, keys: &[&str]) { for &key in keys { assert!(map.contains_key(key), "Key {} is missing", key); } } // Helper function for HashMap fn assert_has_keys_hashmap(map: &HashMap, keys: &[&str]) { for &key in keys { assert!(map.contains_key(key), "Key {} is missing", key); } } #[tokio::test] #[serial] pub async fn get_swap_info() { setup_test(SlowCancelConfig, |mut harness_ctx| async move { // Start a swap and wait for xmr lock transaction to be published (XmrLockTransactionSent) let (bob_swap, _) = harness_ctx.bob_swap().await; let bob_swap_id = bob_swap.id; tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let alice_swap = harness_ctx.alice_next_swap().await; alice::run_until( alice_swap, is_xmr_lock_transaction_sent, FixedRate::default(), ) .await?; let (client, _, _) = setup_daemon(harness_ctx).await; let response: HashMap> = client .request("get_history", ObjectParams::new()) .await .unwrap(); let swaps = response.get("swaps").unwrap(); assert_eq!(swaps.len(), 1); assert_has_keys_serde( swaps[0].as_object().unwrap(), &[ "swapId", "startDate", "state", "btcAmount", "xmrAmount", "exchangeRate", "tradingPartnerPeerId", ], ); let response: HashMap>> = client .request("get_raw_states", ObjectParams::new()) .await .unwrap(); let response_raw_states = response.get("raw_states").unwrap(); assert!(response_raw_states.contains_key(&bob_swap_id)); assert_eq!(response_raw_states.get(&bob_swap_id).unwrap().len(), 2); let mut params = ObjectParams::new(); params.insert("swap_id", bob_swap_id).unwrap(); let response: HashMap = client.request("get_swap_info", params).await.unwrap(); // Check primary keys in response assert_has_keys_hashmap( &response, &[ "txRefundFee", "swapId", "cancelTimelock", "timelock", "punishTimelock", "stateName", "btcAmount", "startDate", "btcRefundAddress", "txCancelFee", "xmrAmount", "completed", "txLockId", "seller", ], ); // Assert specific fields assert_eq!(response.get("swapId").unwrap(), &bob_swap_id.to_string()); assert_eq!( response.get("stateName").unwrap(), &"btc is locked".to_string() ); assert_eq!(response.get("completed").unwrap(), &Value::Bool(false)); // Check seller object and its keys let seller = response .get("seller") .expect("Field 'seller' is missing from response") .as_object() .expect("'seller' is not an object"); assert_has_keys_serde(seller, &["peerId"]); // Check timelock object, nested 'None' object, and blocks_left let timelock = response .get("timelock") .expect("Field 'timelock' is missing from response") .as_object() .expect("'timelock' is not an object"); let none_obj = timelock .get("None") .expect("Field 'None' is missing from 'timelock'") .as_object() .expect("'None' is not an object in 'timelock'"); let blocks_left = none_obj .get("blocks_left") .expect("Field 'blocks_left' is missing from 'None'") .as_i64() .expect("'blocks_left' is not an integer"); // Validate blocks_left assert!( blocks_left > 0 && blocks_left <= 180, "Field 'blocks_left' should be > 0 and <= 180 but got {}", blocks_left ); Ok(()) }) .await; } #[tokio::test] #[serial] pub async fn test_rpc_calls() { setup_test(SlowCancelConfig, |harness_ctx| async move { let alice_addr = harness_ctx.bob_params.get_concentenated_alice_address(); let (change_address, receive_address) = harness_ctx.bob_params.get_change_receive_addresses().await; let (client, writer, _) = setup_daemon(harness_ctx).await; assert!(client.is_connected()); let mut params = ObjectParams::new(); params.insert("force_refresh", false).unwrap(); let response: HashMap = client .request("get_bitcoin_balance", params) .await .unwrap(); assert_eq!(response, HashMap::from([("balance".to_string(), 10000000)])); let mut params = ObjectParams::new(); params.insert("log_reference_id", "test_ref_id").unwrap(); params.insert("force_refresh", false).unwrap(); let _: HashMap = client.request("get_bitcoin_balance", params).await.unwrap(); assert!(writer.captured().contains( r#"method{method_name="Balance" log_reference_id="\"test_ref_id\""}: swap::api::request: Current Bitcoin balance as of last sync balance=0.1 BTC"# )); for method in ["get_swap_info", "resume_swap", "cancel_refund_swap"].iter() { let mut params = ObjectParams::new(); params.insert("swap_id", "invalid_swap").unwrap(); let response: Result, _> = client.request(method, params).await; response.expect_err(&format!( "Expected an error when swap_id is invalid for method {}", method )); let params = ObjectParams::new(); let response: Result, _> = client.request(method, params).await; response.expect_err(&format!( "Expected an error when swap_id is missing for method {}", method )); } let params = ObjectParams::new(); let result: Result, _> = client.request("list_sellers", params).await; result.expect_err("Expected an error when rendezvous_point is missing"); let params = ObjectParams::new(); let result: Result, _> = client.request("list_sellers", params).await; result.expect_err("Expected an error when rendezvous_point is missing"); let params = ObjectParams::new(); let response: Result, _> = client.request("withdraw_btc", params).await; response.expect_err("Expected an error when withdraw_address is missing"); let mut params = ObjectParams::new(); params.insert("address", "invalid_address").unwrap(); let response: Result, _> = client.request("withdraw_btc", params).await; response.expect_err("Expected an error when withdraw_address is malformed"); let mut params = ObjectParams::new(); params.insert("address", BITCOIN_ADDR).unwrap(); params.insert("amount", "0").unwrap(); let response: Result, _> = client.request("withdraw_btc", params).await; response.expect_err("Expected an error when amount is 0"); let mut params = ObjectParams::new(); params .insert("address", BITCOIN_ADDR) .unwrap(); params.insert("amount", "0.01").unwrap(); let response: HashMap = client .request("withdraw_btc", params) .await .expect("Expected a valid response"); assert_has_keys_hashmap(&response, &["signed_tx", "amount", "txid"]); assert_eq!( response.get("amount").unwrap().as_u64().unwrap(), 1_000_000 ); let params = ObjectParams::new(); let response: Result, _> = client.request("buy_xmr", params).await; response.expect_err("Expected an error when no params are given"); let mut params = ObjectParams::new(); params .insert("bitcoin_change_address", BITCOIN_ADDR) .unwrap(); params .insert("monero_receive_address", MONERO_ADDR) .unwrap(); let response: Result, _> = client.request("buy_xmr", params).await; response.expect_err("Expected an error when seller is missing"); let mut params = ObjectParams::new(); params .insert("bitcoin_change_address", BITCOIN_ADDR) .unwrap(); params.insert("seller", SELLER).unwrap(); let response: Result, _> = client.request("buy_xmr", params).await; response.expect_err("Expected an error when monero_receive_address is missing"); let mut params = ObjectParams::new(); params .insert("monero_receive_address", MONERO_ADDR) .unwrap(); params.insert("seller", SELLER).unwrap(); let response: Result, _> = client.request("buy_xmr", params).await; response.expect_err("Expected an error when bitcoin_change_address is missing"); let mut params = ObjectParams::new(); params .insert("bitcoin_change_address", "invalid_address") .unwrap(); params .insert("monero_receive_address", MONERO_ADDR) .unwrap(); params.insert("seller", SELLER).unwrap(); let response: Result, _> = client.request("buy_xmr", params).await; response.expect_err("Expected an error when bitcoin_change_address is malformed"); let mut params = ObjectParams::new(); params .insert("bitcoin_change_address", BITCOIN_ADDR) .unwrap(); params .insert("monero_receive_address", "invalid_address") .unwrap(); params.insert("seller", SELLER).unwrap(); let response: Result, _> = client.request("buy_xmr", params).await; response.expect_err("Expected an error when monero_receive_address is malformed"); let mut params = ObjectParams::new(); params .insert("bitcoin_change_address", BITCOIN_ADDR) .unwrap(); params .insert("monero_receive_address", MONERO_ADDR) .unwrap(); params.insert("seller", "invalid_seller").unwrap(); let response: Result, _> = client.request("buy_xmr", params).await; response.expect_err("Expected an error when seller is malformed"); let response: Result, _> = client .request("suspend_current_swap", ObjectParams::new()) .await; response.expect_err("Expected an error when no swap is running"); let mut params = ObjectParams::new(); params .insert("bitcoin_change_address", change_address) .unwrap(); params .insert("monero_receive_address", receive_address) .unwrap(); params.insert("seller", alice_addr).unwrap(); let response: HashMap = client .request("buy_xmr", params) .await .expect("Expected a HashMap, got an error"); assert_has_keys_hashmap(&response, &["swapId"]); Ok(()) }) .await; } #[tokio::test] #[serial] pub async fn suspend_current_swap_swap_running() { setup_test(SlowCancelConfig, |harness_ctx| async move { let (client, _, ctx) = setup_daemon(harness_ctx).await; ctx.swap_lock .acquire_swap_lock(Uuid::parse_str(SWAP_ID).unwrap()) .await .unwrap(); let cloned_ctx = ctx.clone(); tokio::spawn(async move { // Immediately release lock when suspend signal is received. Mocks a running swap that is then cancelled. ctx.swap_lock .listen_for_swap_force_suspension() .await .unwrap(); ctx.swap_lock.release_swap_lock().await.unwrap(); }); let response: HashMap = client .request("suspend_current_swap", ObjectParams::new()) .await .unwrap(); assert_eq!( response, HashMap::from([("swapId".to_string(), SWAP_ID.to_string())]) ); cloned_ctx .swap_lock .acquire_swap_lock(Uuid::parse_str(SWAP_ID).unwrap()) .await .unwrap(); let response: Result, _> = client .request("suspend_current_swap", ObjectParams::new()) .await; response.expect_err("Expected an error when suspend signal times out"); Ok(()) }) .await; } }