|
4 | 4 | //! |
5 | 5 | //! Tests the first pass of two-phase transaction application, covering: |
6 | 6 | //! - Successful payment transactions |
| 7 | +//! - Payment creating receiver account |
7 | 8 | //! - Insufficient balance errors |
8 | 9 | //! - Invalid nonce errors |
9 | 10 | //! - Nonexistent fee payer errors |
@@ -439,3 +440,126 @@ fn test_apply_payment_nonexistent_fee_payer() { |
439 | 440 | "Alice's account should still not exist after transaction error" |
440 | 441 | ); |
441 | 442 | } |
| 443 | + |
| 444 | +/// Test payment that creates a new receiver account. |
| 445 | +/// |
| 446 | +/// When the receiver account doesn't exist, a new account is created |
| 447 | +/// automatically. The account creation fee is deducted from the payment amount, |
| 448 | +/// not from the sender's balance. |
| 449 | +/// |
| 450 | +/// Ledger state: Sender's balance decreased by amount + fee, receiver account |
| 451 | +/// created with balance = amount - account_creation_fee. |
| 452 | +#[test] |
| 453 | +fn test_apply_payment_creates_receiver_account() { |
| 454 | + let db = Database::create(15); |
| 455 | + let mut ledger = Mask::new_root(db); |
| 456 | + |
| 457 | + let alice_pk = mina_signer::PubKey::from_address( |
| 458 | + "B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja", |
| 459 | + ) |
| 460 | + .unwrap() |
| 461 | + .into_compressed(); |
| 462 | + let bob_pk = mina_signer::PubKey::from_address( |
| 463 | + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS", |
| 464 | + ) |
| 465 | + .unwrap() |
| 466 | + .into_compressed(); |
| 467 | + |
| 468 | + // Create only Alice's account |
| 469 | + let alice_id = AccountId::new(alice_pk.clone(), Default::default()); |
| 470 | + let alice_account = Account::create_with(alice_id.clone(), Balance::from_u64(5_000_000_000)); |
| 471 | + ledger |
| 472 | + .get_or_create_account(alice_id.clone(), alice_account) |
| 473 | + .unwrap(); |
| 474 | + |
| 475 | + let bob_id = AccountId::new(bob_pk.clone(), Default::default()); |
| 476 | + |
| 477 | + // Verify Bob's account does not exist before the transaction |
| 478 | + assert!( |
| 479 | + ledger.location_of_account(&bob_id).is_none(), |
| 480 | + "Bob's account should not exist before transaction" |
| 481 | + ); |
| 482 | + |
| 483 | + // Record initial state |
| 484 | + let alice_location = ledger.location_of_account(&alice_id).unwrap(); |
| 485 | + let alice_before = ledger.get(alice_location).unwrap(); |
| 486 | + let initial_alice_balance = alice_before.balance; |
| 487 | + let initial_alice_nonce = alice_before.nonce; |
| 488 | + let initial_alice_receipt_hash = alice_before.receipt_chain_hash; |
| 489 | + |
| 490 | + let amount = 2_000_000_000; // 2 MINA |
| 491 | + let fee = 10_000_000; // 0.01 MINA |
| 492 | + let nonce = 0; |
| 493 | + let payment = create_payment(&alice_pk, &bob_pk, amount, fee, nonce); |
| 494 | + |
| 495 | + let constraint_constants = &test_constraint_constants(); |
| 496 | + let account_creation_fee = constraint_constants.account_creation_fee; // 1 MINA |
| 497 | + |
| 498 | + let state_view = ProtocolStateView { |
| 499 | + snarked_ledger_hash: Fp::zero(), |
| 500 | + blockchain_length: Length::from_u32(0), |
| 501 | + min_window_density: Length::from_u32(0), |
| 502 | + total_currency: Amount::zero(), |
| 503 | + global_slot_since_genesis: Slot::from_u32(0), |
| 504 | + staking_epoch_data: dummy_epoch_data(), |
| 505 | + next_epoch_data: dummy_epoch_data(), |
| 506 | + }; |
| 507 | + let result = apply_transaction_first_pass( |
| 508 | + constraint_constants, |
| 509 | + Slot::from_u32(0), |
| 510 | + &state_view, |
| 511 | + &mut ledger, |
| 512 | + &Transaction::Command(UserCommand::SignedCommand(Box::new(payment))), |
| 513 | + ); |
| 514 | + |
| 515 | + assert!(result.is_ok()); |
| 516 | + |
| 517 | + // Verify Alice's balance decreased by fee + payment amount |
| 518 | + let alice_location = ledger.location_of_account(&alice_id).unwrap(); |
| 519 | + let alice_after = ledger.get(alice_location).unwrap(); |
| 520 | + let expected_alice_balance = initial_alice_balance |
| 521 | + .sub_amount(Amount::from_u64(fee)) |
| 522 | + .unwrap() |
| 523 | + .sub_amount(Amount::from_u64(amount)) |
| 524 | + .unwrap(); |
| 525 | + assert_eq!( |
| 526 | + alice_after.balance, expected_alice_balance, |
| 527 | + "Alice's balance should decrease by fee + payment amount" |
| 528 | + ); |
| 529 | + |
| 530 | + // Verify Alice's nonce incremented |
| 531 | + assert_eq!( |
| 532 | + alice_after.nonce, |
| 533 | + initial_alice_nonce.incr(), |
| 534 | + "Alice's nonce should be incremented" |
| 535 | + ); |
| 536 | + |
| 537 | + // Verify Alice's receipt chain hash updated |
| 538 | + assert_ne!( |
| 539 | + alice_after.receipt_chain_hash, initial_alice_receipt_hash, |
| 540 | + "Alice's receipt chain hash should be updated" |
| 541 | + ); |
| 542 | + |
| 543 | + // Verify Bob's account was created |
| 544 | + let bob_location = ledger.location_of_account(&bob_id); |
| 545 | + assert!( |
| 546 | + bob_location.is_some(), |
| 547 | + "Bob's account should now exist after transaction" |
| 548 | + ); |
| 549 | + |
| 550 | + // Verify Bob's balance is payment amount minus account creation fee |
| 551 | + let bob_location = bob_location.unwrap(); |
| 552 | + let bob_after = ledger.get(bob_location).unwrap(); |
| 553 | + let expected_bob_balance = Balance::from_u64(amount - account_creation_fee); |
| 554 | + assert_eq!( |
| 555 | + bob_after.balance, expected_bob_balance, |
| 556 | + "Bob's balance should be payment amount minus account creation fee" |
| 557 | + ); |
| 558 | + |
| 559 | + // Verify Bob's nonce is 0 (new account) |
| 560 | + assert_eq!( |
| 561 | + bob_after.nonce, |
| 562 | + Nonce::zero(), |
| 563 | + "Bob's nonce should be 0 for new account" |
| 564 | + ); |
| 565 | +} |
0 commit comments