//! M8: optional monero-wallet-rpc JSON client. Mock TCP only — no live monerod. use onionwire::wallet::{self, Wallet}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; fn json_rpc_ok(result: &str) -> String { let body = format!(r#"{{"jsonrpc":"2.0","id":"0","result":{result}}}"#); format!( "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ) } async fn serve_once(listener: TcpListener, response: String) { let (mut sock, _) = listener.accept().await.expect("accept"); let mut buf = vec![0u8; 4096]; let _ = sock.read(&mut buf).await; sock.write_all(response.as_bytes()).await.expect("write"); } #[test] fn disabled_create_address_is_not_configured() { let w = Wallet::disabled(); let err = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap() .block_on(w.create_address()) .unwrap_err(); assert!(err.to_string().contains("not configured"), "got {err}"); } #[test] fn refuse_non_loopback_non_onion_host() { let err = Wallet::from_url("http://example.com:18083").unwrap_err(); assert!( err.to_string().to_ascii_lowercase().contains("host") || err.to_string().contains("loopback") || err.to_string().contains("onion"), "got {err}" ); } #[tokio::test] async fn mock_get_address_parses_string() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let canned = json_rpc_ok( r#"{"address":"4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}"#, ); tokio::spawn(serve_once(listener, canned)); let w = Wallet::from_url(&format!("http://127.0.0.1:{}", addr.port())).unwrap(); let got = w.get_address().await.expect("get_address"); assert!(got.starts_with('4'), "got {got}"); } #[tokio::test] async fn mock_create_address_and_transfer() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let canned = json_rpc_ok( r#"{"address":"8BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","tx_hash":"abc123"}"#, ); tokio::spawn(serve_once(listener, canned.clone())); let w = Wallet::from_url(&format!("http://127.0.0.1:{}", addr.port())).unwrap(); let created = w.create_address().await.expect("create_address"); assert!(created.starts_with('8')); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(serve_once(listener, canned)); let w = Wallet::from_url(&format!("http://127.0.0.1:{}", addr.port())).unwrap(); let txid = w .transfer("8BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", 1) .await .expect("transfer"); assert_eq!(txid, "abc123"); } #[tokio::test] async fn mock_get_transfers_matches_txid() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let canned = json_rpc_ok( r#"{"in":[{"txid":"deadbeef","amount":5,"address":"8BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"}],"pending":[]}"#, ); tokio::spawn(serve_once(listener, canned)); let w = Wallet::from_url(&format!("http://127.0.0.1:{}", addr.port())).unwrap(); let rows = w.get_transfers().await.expect("get_transfers"); assert!(wallet::transfers_match( &rows, "deadbeef", "5", "8BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" )); assert!(!wallet::transfers_match(&rows, "nope", "1", "nope")); }