|
1 | 1 | use dash_spv::sync::ProgressPercentage; |
2 | | -use dashcore::Amount; |
3 | | - |
4 | | -use super::helpers::wait_for_sync; |
5 | | -use super::setup::TestContext; |
| 2 | +use dashcore::{Address, Amount, Network}; |
| 3 | +use std::sync::Arc; |
| 4 | +use std::time::Duration; |
| 5 | +use tokio::sync::RwLock; |
| 6 | + |
| 7 | +use super::helpers::{ |
| 8 | + wait_for_mempool_tx, wait_for_sync, wait_for_wallet_synced, EMPTY_MNEMONIC, SECONDARY_MNEMONIC, |
| 9 | +}; |
| 10 | +use super::setup::{create_and_start_client, create_test_wallet, TestContext}; |
6 | 11 | use dash_spv::test_utils::TestChain; |
| 12 | +use dashcore::address::NetworkUnchecked; |
| 13 | +use key_wallet::account::ManagedAccountTrait; |
| 14 | +use key_wallet::wallet::managed_wallet_info::transaction_builder::{ |
| 15 | + BuilderError, TransactionBuilder, |
| 16 | +}; |
| 17 | +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; |
| 18 | +use key_wallet::wallet::ManagedWalletInfo; |
| 19 | +use key_wallet::ManagedAccountType; |
| 20 | +use key_wallet_manager::{WalletId, WalletManager}; |
7 | 21 |
|
8 | 22 | /// Verify incremental sync works by generating blocks after initial sync. |
9 | 23 | /// |
@@ -230,3 +244,144 @@ async fn test_multiple_transactions_across_blocks() { |
230 | 244 | fees_paid |
231 | 245 | ); |
232 | 246 | } |
| 247 | + |
| 248 | +const MEMPOOL_TIMEOUT: Duration = Duration::from_secs(30); |
| 249 | + |
| 250 | +async fn reserve_first_address(mnemonic: &str) -> Address { |
| 251 | + let (temp_mgr, temp_id) = create_test_wallet(mnemonic, Network::Regtest); |
| 252 | + |
| 253 | + let reader = temp_mgr.read().await; |
| 254 | + let info = reader.get_wallet_info(&temp_id).expect("wallet info"); |
| 255 | + let account = info.accounts().standard_bip44_accounts.get(&0).expect("BIP44 account 0"); |
| 256 | + |
| 257 | + let ManagedAccountType::Standard { |
| 258 | + external_addresses, |
| 259 | + .. |
| 260 | + } = &account.managed_account_type() |
| 261 | + else { |
| 262 | + panic!("not a Standard account"); |
| 263 | + }; |
| 264 | + |
| 265 | + external_addresses.unused_addresses().into_iter().next().expect("unused address") |
| 266 | +} |
| 267 | + |
| 268 | +async fn build_and_sign( |
| 269 | + wallet: &Arc<RwLock<WalletManager<ManagedWalletInfo>>>, |
| 270 | + wallet_id: &WalletId, |
| 271 | + destination: &Address, |
| 272 | + amount: u64, |
| 273 | +) -> Result<(dashcore::Transaction, u64), BuilderError> { |
| 274 | + let dest_unchecked: Address<NetworkUnchecked> = |
| 275 | + destination.to_string().parse().expect("destination address"); |
| 276 | + |
| 277 | + let mut wallet_lock = wallet.write().await; |
| 278 | + let (w, info) = wallet_lock.get_wallet_and_info_mut(wallet_id).expect("wallet present"); |
| 279 | + |
| 280 | + let height = info.last_processed_height(); |
| 281 | + let network = w.network; |
| 282 | + let account = w.get_bip44_account(0).expect("account 0").clone(); |
| 283 | + let funds_account = info.accounts.standard_bip44_accounts.get_mut(&0).expect("account 0"); |
| 284 | + let dest = dest_unchecked.require_network(network).expect("destination network"); |
| 285 | + |
| 286 | + TransactionBuilder::new() |
| 287 | + .set_current_height(height) |
| 288 | + .set_funding(funds_account, &account) |
| 289 | + .add_output(&dest, amount) |
| 290 | + .build_signed(w, |a| funds_account.address_derivation_path(&a)) |
| 291 | + .await |
| 292 | +} |
| 293 | + |
| 294 | +/// Build, sign and broadcast a tx via `TransactionBuilder`, then re-spend |
| 295 | +/// the resulting mempool change UTXO before its parent confirms. |
| 296 | +#[tokio::test] |
| 297 | +async fn test_spend_change_balance() { |
| 298 | + let Some(ctx) = TestContext::new(TestChain::Minimal).await else { |
| 299 | + return; |
| 300 | + }; |
| 301 | + if !ctx.dashd.supports_mining { |
| 302 | + eprintln!("Skipping test (dashd RPC miner not available)"); |
| 303 | + return; |
| 304 | + } |
| 305 | + |
| 306 | + let (wallet, wallet_id) = create_test_wallet(EMPTY_MNEMONIC, Network::Regtest); |
| 307 | + let mut client_handle = create_and_start_client(&ctx.client_config, Arc::clone(&wallet)).await; |
| 308 | + wait_for_sync(&mut client_handle.progress_receiver, ctx.dashd.initial_height).await; |
| 309 | + |
| 310 | + let receive_address = reserve_first_address(EMPTY_MNEMONIC).await; |
| 311 | + let funding_amount = Amount::from_sat(500_000_000); |
| 312 | + ctx.dashd.node.send_to_address(&receive_address, funding_amount); |
| 313 | + |
| 314 | + let miner_address = ctx.dashd.node.get_new_address_from_wallet("default"); |
| 315 | + ctx.dashd.node.generate_blocks(1, &miner_address); |
| 316 | + let funded_height = ctx.dashd.initial_height + 1; |
| 317 | + wait_for_sync(&mut client_handle.progress_receiver, funded_height).await; |
| 318 | + wait_for_wallet_synced(&wallet, &wallet_id, funded_height).await; |
| 319 | + |
| 320 | + let dest_a = Address::dummy(Network::Regtest, 1); |
| 321 | + let (tx_a, _) = |
| 322 | + build_and_sign(&wallet, &wallet_id, &dest_a, 100_000_000).await.expect("build tx_a"); |
| 323 | + |
| 324 | + client_handle.client.broadcast_transaction(&tx_a).await.expect("broadcast tx_a"); |
| 325 | + wait_for_mempool_tx(&mut client_handle.wallet_event_receiver, MEMPOOL_TIMEOUT) |
| 326 | + .await |
| 327 | + .expect("detect tx_a"); |
| 328 | + |
| 329 | + // The wallet's only UTXO now is the mempool change from tx_a, so a |
| 330 | + // successful build proves coin selection used it. |
| 331 | + let dest_b = Address::dummy(Network::Regtest, 2); |
| 332 | + let (tx_b, _) = build_and_sign(&wallet, &wallet_id, &dest_b, 50_000_000) |
| 333 | + .await |
| 334 | + .expect("spend mempool change"); |
| 335 | + assert!( |
| 336 | + tx_b.input.iter().any(|i| i.previous_output.txid == tx_a.txid()), |
| 337 | + "tx_b must spend tx_a's mempool change UTXO", |
| 338 | + ); |
| 339 | + |
| 340 | + client_handle.client.broadcast_transaction(&tx_b).await.expect("broadcast tx_b"); |
| 341 | + wait_for_mempool_tx(&mut client_handle.wallet_event_receiver, MEMPOOL_TIMEOUT) |
| 342 | + .await |
| 343 | + .expect("detect tx_b"); |
| 344 | + |
| 345 | + client_handle.stop().await; |
| 346 | +} |
| 347 | + |
| 348 | +/// Spend an incoming mempool UTXO (we own the output, none of the inputs) |
| 349 | +/// before it confirms. |
| 350 | +#[tokio::test] |
| 351 | +async fn test_spend_incoming_balance() { |
| 352 | + let Some(ctx) = TestContext::new(TestChain::Minimal).await else { |
| 353 | + return; |
| 354 | + }; |
| 355 | + if !ctx.dashd.supports_mining { |
| 356 | + eprintln!("Skipping test (dashd RPC miner not available)"); |
| 357 | + return; |
| 358 | + } |
| 359 | + |
| 360 | + let (wallet, wallet_id) = create_test_wallet(SECONDARY_MNEMONIC, Network::Regtest); |
| 361 | + let mut client_handle = create_and_start_client(&ctx.client_config, Arc::clone(&wallet)).await; |
| 362 | + wait_for_sync(&mut client_handle.progress_receiver, ctx.dashd.initial_height).await; |
| 363 | + |
| 364 | + let receive_address = reserve_first_address(SECONDARY_MNEMONIC).await; |
| 365 | + let incoming_amount = Amount::from_sat(300_000_000); |
| 366 | + let incoming_txid = ctx.dashd.node.send_to_address(&receive_address, incoming_amount); |
| 367 | + |
| 368 | + wait_for_mempool_tx(&mut client_handle.wallet_event_receiver, MEMPOOL_TIMEOUT) |
| 369 | + .await |
| 370 | + .expect("detect incoming"); |
| 371 | + |
| 372 | + let dest = Address::dummy(Network::Regtest, 3); |
| 373 | + let (tx, _) = build_and_sign(&wallet, &wallet_id, &dest, 150_000_000) |
| 374 | + .await |
| 375 | + .expect("spend unconfirmed incoming"); |
| 376 | + assert!( |
| 377 | + tx.input.iter().any(|i| i.previous_output.txid == incoming_txid), |
| 378 | + "spend must reference the unconfirmed incoming txid", |
| 379 | + ); |
| 380 | + |
| 381 | + client_handle.client.broadcast_transaction(&tx).await.expect("broadcast spend"); |
| 382 | + wait_for_mempool_tx(&mut client_handle.wallet_event_receiver, MEMPOOL_TIMEOUT) |
| 383 | + .await |
| 384 | + .expect("detect spend"); |
| 385 | + |
| 386 | + client_handle.stop().await; |
| 387 | +} |
0 commit comments