diff --git a/coins/monero/tests/runner.rs b/coins/monero/tests/runner.rs index a40c3765..c18b9940 100644 --- a/coins/monero/tests/runner.rs +++ b/coins/monero/tests/runner.rs @@ -1,5 +1,6 @@ use core::ops::Deref; use std::sync::Mutex; +use std::collections::HashSet; use lazy_static::lazy_static; @@ -11,8 +12,9 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; use monero_serai::{ Protocol, random_scalar, wallet::{ - ViewPair, + ViewPair, Scanner, address::{Network, AddressType, AddressMeta, MoneroAddress}, + SpendableOutput, }, rpc::Rpc, }; @@ -55,6 +57,18 @@ pub async fn mine_until_unlocked(rpc: &Rpc, addr: &str, tx_hash: [u8; 32]) { rpc.generate_blocks(addr, 9).await.unwrap(); } +/// Mines 60 blocks and returns an unlocked spendable miner tx output. +pub async fn get_miner_tx_output(rpc: &Rpc, view: &ViewPair) -> SpendableOutput { + let mut scanner = Scanner::from_view(view.clone(), Network::Mainnet, Some(HashSet::new())); + + // mine 60 blocks to unlock a miner tx + let start = rpc.get_height().await.unwrap(); + rpc.generate_blocks(&scanner.address().to_string(), 60).await.unwrap(); + + let block = rpc.get_block(start).await.unwrap(); + scanner.scan(&rpc, &block).await.unwrap().swap_remove(0).ignore_timelock().swap_remove(0) +} + pub async fn rpc() -> Rpc { let rpc = Rpc::new("http://127.0.0.1:18081".to_string()).unwrap(); @@ -136,12 +150,12 @@ macro_rules! test { use monero_serai::{ random_scalar, wallet::{ - address::Network, ViewPair, Scanner, SignableTransaction, + address::{Network, AddressMeta, AddressType}, ViewPair, Scanner, SignableTransaction, SignableTransactionBuilder, }, }; - use runner::{random_address, rpc, mine_until_unlocked}; + use runner::{random_address, rpc, mine_until_unlocked, get_miner_tx_output}; type Builder = SignableTransactionBuilder; @@ -166,28 +180,12 @@ macro_rules! test { keys[&1].group_key().0 }; - let view = ViewPair::new(spend_pub, Zeroizing::new(random_scalar(&mut OsRng))); - let rpc = rpc().await; - let (addr, miner_tx) = { - let mut scanner = - Scanner::from_view(view.clone(), Network::Mainnet, Some(HashSet::new())); - let addr = scanner.address(); + let view = ViewPair::new(spend_pub, Zeroizing::new(random_scalar(&mut OsRng))); + let addr = view.address(AddressMeta::new(Network::Mainnet, AddressType::Standard)); - // mine 60 blocks to unlock a miner tx - let start = rpc.get_height().await.unwrap(); - rpc.generate_blocks(&addr.to_string(), 60).await.unwrap(); - - let block = rpc.get_block(start).await.unwrap(); - ( - addr, - scanner.scan( - &rpc, - &block - ).await.unwrap().swap_remove(0).ignore_timelock().swap_remove(0) - ) - }; + let miner_tx = get_miner_tx_output(&rpc, &view).await; let builder = SignableTransactionBuilder::new( rpc.get_protocol().await.unwrap(), diff --git a/coins/monero/tests/scan.rs b/coins/monero/tests/scan.rs new file mode 100644 index 00000000..b807401f --- /dev/null +++ b/coins/monero/tests/scan.rs @@ -0,0 +1,160 @@ +use std::collections::HashSet; +use hex_literal::hex; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; +use rand_core::OsRng; +use zeroize::Zeroizing; + +use monero_serai::{ + wallet::{ + address::{Network, MoneroAddress}, + ViewPair, SignableTransaction, Scanner, + }, +}; + +mod runner; + +const ADDR_SPEND: [u8; 32] = + hex!("bf02a79d9e30ea76872e565722517924151793c535832e4353e54dc0be698001"); +const ADDR_VIEW: [u8; 32] = + hex!("c300b798c000f3c5da4ebc5a288418fe9060d623c9830b7abc3d9e7a3152eb08"); + +pub struct AddressInfo { + address: String, + payment_id: Option<[u8; 8]>, +} + +pub async fn scan_incomings(addr_scanner: &mut Scanner, addresses: &Vec) { + // get an output to spend + let (miner_spend, miner_view, miner_addr) = runner::random_address(); + let mut miner_scanner = + Scanner::from_view(miner_view.clone(), Network::Mainnet, Some(HashSet::new())); + let rpc = runner::rpc().await; + let mut input = runner::get_miner_tx_output(&rpc, &miner_view).await; + + // chain params + let fee = rpc.get_fee().await.unwrap(); + let start = rpc.get_height().await.unwrap() - 59; // -60 input is already grabbed above + let protocol = rpc.get_protocol().await.unwrap(); + let amount = 1000000; + + for (i, addr) in addresses.iter().enumerate() { + let mut tx = SignableTransaction::new( + protocol, + vec![input], + vec![(MoneroAddress::from_str(Network::Mainnet, &addr.address).unwrap(), amount)], + Some(miner_addr), + vec![], + fee, + ) + .unwrap() + .sign(&mut OsRng, &rpc, &Zeroizing::new(miner_spend)) + .await + .unwrap(); + + // submit and unlock tx + rpc.publish_transaction(&tx).await.unwrap(); + runner::mine_until_unlocked(&rpc, &miner_addr.to_string(), tx.hash()).await; + + // get the tx and confirm receipt + tx = rpc.get_transaction(tx.hash()).await.unwrap(); + let output = addr_scanner.scan_transaction(&tx).not_locked().swap_remove(0); + assert_eq!(output.commitment().amount, amount); + if addr.payment_id.is_some() { + assert_eq!(output.metadata.payment_id, addr.payment_id.unwrap()); + } + + // pick another input to spend for the next address + let block = rpc.get_block(start + i).await.unwrap(); + input = miner_scanner + .scan(&rpc, &block) + .await + .unwrap() + .swap_remove(0) + .ignore_timelock() + .swap_remove(0); + } +} + +async_sequential!( + async fn scan_all_standard_addresses() { + // scanner + let spend_pub = &Scalar::from_bits(ADDR_SPEND) * &ED25519_BASEPOINT_TABLE; + let mut scanner = Scanner::from_view( + ViewPair::new(spend_pub, Zeroizing::new(Scalar::from_bits(ADDR_VIEW))), + Network::Mainnet, + Some(HashSet::new()), + ); + + // generate all types of addresses + let payment_ids = vec![ + [46, 48, 134, 34, 245, 148, 243, 195], + [153, 176, 98, 204, 151, 27, 197, 168], + [88, 37, 149, 111, 171, 108, 120, 181], + ]; + let mut addresses = vec![]; + // standard versions + addresses.push(AddressInfo { address: scanner.address().to_string(), payment_id: None }); + addresses + .push(AddressInfo { address: scanner.subaddress((0, 1)).to_string(), payment_id: None }); + addresses.push(AddressInfo { + address: scanner.integrated_address(payment_ids[0]).to_string(), + payment_id: Some(payment_ids[0]), + }); + // featured versions + addresses.push(AddressInfo { + address: scanner.featured_address(Some((0, 2)), Some(payment_ids[1]), false).to_string(), + payment_id: Some(payment_ids[1]), + }); + addresses.push(AddressInfo { + address: scanner.featured_address(None, None, false).to_string(), + payment_id: None, + }); + addresses.push(AddressInfo { + address: scanner.featured_address(Some((0, 3)), None, false).to_string(), + payment_id: None, + }); + addresses.push(AddressInfo { + address: scanner.featured_address(None, Some(payment_ids[2]), false).to_string(), + payment_id: Some(payment_ids[2]), + }); + + // send to & test receive from addresses + scan_incomings(&mut scanner, &addresses).await; + } +); + +async_sequential!( + async fn scan_all_guaranteed_addresses() { + // scanner + let spend_pub = &Scalar::from_bits(ADDR_SPEND) * &ED25519_BASEPOINT_TABLE; + let mut scanner = Scanner::from_view( + ViewPair::new(spend_pub, Zeroizing::new(Scalar::from_bits(ADDR_VIEW))), + Network::Mainnet, + None, + ); + + // generate all types of addresses + let payment_ids = + vec![[88, 37, 149, 111, 171, 108, 120, 181], [125, 69, 155, 152, 140, 160, 157, 186]]; + let mut addresses = vec![]; + // featured (false, none, true) + addresses.push(AddressInfo { address: scanner.address().to_string(), payment_id: None }); + // featured (false, some, true) + addresses.push(AddressInfo { + address: scanner.integrated_address(payment_ids[0]).to_string(), + payment_id: Some(payment_ids[0]), + }); + // featured (true, none, true) + addresses + .push(AddressInfo { address: scanner.subaddress((0, 1)).to_string(), payment_id: None }); + // featured (true, some, true) + addresses.push(AddressInfo { + address: scanner.featured_address(Some((0, 2)), Some(payment_ids[1]), true).to_string(), + payment_id: Some(payment_ids[1]), + }); + + // send to & test receive from addresses + scan_incomings(&mut scanner, &addresses).await; + } +);