From 0bc568af81a63dc1bf18656e12a564d1b9d33705 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Thu, 13 Aug 2026 19:14:54 +0000 Subject: [PATCH] test(pinata_cooldown): pay the prize via an authenticated_transfer PDA --- lez/sequencer/core/src/tests.rs | 33 +++++++++-- .../guest/src/bin/pinata_cooldown.rs | 55 +++++++++++-------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs index ae0a6c31b..2c97fa268 100644 --- a/lez/sequencer/core/src/tests.rs +++ b/lez/sequencer/core/src/tests.rs @@ -1713,13 +1713,14 @@ fn pinata_cooldown_data(prize: u128, cooldown_ms: u64, last_claim_timestamp: u64 fn pinata_cooldown_transaction( pinata_id: AccountId, + prize_pda_id: AccountId, winner_id: AccountId, clock_account_id: AccountId, ) -> PublicTransaction { let program_id = test_programs::pinata_cooldown().id(); let message = lee::public_transaction::Message::try_new( program_id, - vec![pinata_id, winner_id, clock_account_id], + vec![pinata_id, prize_pda_id, winner_id, clock_account_id], vec![], (), ) @@ -1743,6 +1744,9 @@ fn pinata_cooldown_claim_succeeds_after_cooldown() { let block_timestamp = genesis_timestamp + cooldown_ms; let mut state = state_with_clock_and_program(test_programs::pinata_cooldown(), block_timestamp); + let prize_pda_id = + AccountId::for_public_pda(&test_programs::pinata_cooldown().id(), &PdaSeed::new([0; 32])); + // The winner must be a non-default account so the program may credit it without claiming. state.force_insert_account( winner_id, @@ -1751,11 +1755,18 @@ fn pinata_cooldown_claim_succeeds_after_cooldown() { ..Account::default() }, ); + state.force_insert_account( + prize_pda_id, + Account { + program_owner: programs::authenticated_transfer().id(), + balance: 1000, + ..Account::default() + }, + ); state.force_insert_account( pinata_id, Account { program_owner: test_programs::pinata_cooldown().id(), - balance: 1000, data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) .try_into() .unwrap(), @@ -1765,6 +1776,7 @@ fn pinata_cooldown_claim_succeeds_after_cooldown() { let tx = pinata_cooldown_transaction( pinata_id, + prize_pda_id, winner_id, system_accounts::clock_account_ids()[0], ); @@ -1773,7 +1785,7 @@ fn pinata_cooldown_claim_succeeds_after_cooldown() { .transition_from_public_transaction(&tx, 2, block_timestamp) .unwrap(); - assert_eq!(state.get_account_by_id(pinata_id).balance, 1000 - prize); + assert_eq!(state.get_account_by_id(prize_pda_id).balance, 1000 - prize); assert_eq!(state.get_account_by_id(winner_id).balance, prize); } @@ -1791,6 +1803,9 @@ fn pinata_cooldown_claim_fails_during_cooldown() { let block_timestamp = genesis_timestamp + 100; let mut state = state_with_clock_and_program(test_programs::pinata_cooldown(), block_timestamp); + let prize_pda_id = + AccountId::for_public_pda(&test_programs::pinata_cooldown().id(), &PdaSeed::new([0; 32])); + state.force_insert_account( winner_id, Account { @@ -1798,11 +1813,18 @@ fn pinata_cooldown_claim_fails_during_cooldown() { ..Account::default() }, ); + state.force_insert_account( + prize_pda_id, + Account { + program_owner: programs::authenticated_transfer().id(), + balance: 1000, + ..Account::default() + }, + ); state.force_insert_account( pinata_id, Account { program_owner: test_programs::pinata_cooldown().id(), - balance: 1000, data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) .try_into() .unwrap(), @@ -1812,6 +1834,7 @@ fn pinata_cooldown_claim_fails_during_cooldown() { let tx = pinata_cooldown_transaction( pinata_id, + prize_pda_id, winner_id, system_accounts::clock_account_ids()[0], ); @@ -1819,7 +1842,7 @@ fn pinata_cooldown_claim_fails_during_cooldown() { let result = state.transition_from_public_transaction(&tx, 2, block_timestamp); assert!(result.is_err(), "Claim should fail during cooldown period"); - assert_eq!(state.get_account_by_id(pinata_id).balance, 1000); + assert_eq!(state.get_account_by_id(prize_pda_id).balance, 1000); assert_eq!(state.get_account_by_id(winner_id).balance, 0); } diff --git a/test_programs/guest/src/bin/pinata_cooldown.rs b/test_programs/guest/src/bin/pinata_cooldown.rs index 160fba21a..41c154639 100644 --- a/test_programs/guest/src/bin/pinata_cooldown.rs +++ b/test_programs/guest/src/bin/pinata_cooldown.rs @@ -4,16 +4,18 @@ //! After each prize claim the program records the current timestamp; the next claim is only //! allowed once a configurable cooldown period has elapsed. //! -//! Expected pre-states (in order): -//! 0 - pinata account (authorized, owned by this program) -//! 1 - winner account -//! 2 - clock account `CLOCK_01`. -//! //! Pinata account data layout (24 bytes): //! [prize: u64 LE | `cooldown_ms`: u64 LE | `last_claim_timestamp`: u64 LE]. use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData}; -use lee_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs}; +use lee_core::{ + account::AccountId, + program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramInput, ProgramOutput, read_lee_inputs, + }, +}; + +const PRIZE_SEED: PdaSeed = PdaSeed::new([0; 32]); type Instruction = (); @@ -56,12 +58,17 @@ fn main() { instruction_words, ) = read_lee_inputs::(); - let Ok([pinata, winner, clock_pre]) = <[_; 3]>::try_from(pre_states) else { - panic!("Expected exactly 3 input accounts: pinata, winner, clock"); + let Ok([pinata, prize_pda, winner, clock_pre]) = <[_; 4]>::try_from(pre_states) else { + panic!("Expected exactly 4 input accounts: pinata, prize_pda, winner, clock"); }; // Check the clock account is the system clock account assert_eq!(clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID); + assert_eq!( + prize_pda.account_id, + AccountId::for_public_pda(&self_program_id, &PRIZE_SEED), + "Second account must be the prize-pool PDA" + ); let clock_data = ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner()); let current_timestamp = clock_data.timestamp; @@ -77,16 +84,21 @@ fn main() { ); let mut pinata_post = pinata.account.clone(); - let mut winner_post = winner.account.clone(); + let prize_pda_post = prize_pda.account.clone(); + let winner_post = winner.account.clone(); + let clock_post = clock_pre.account.clone(); - pinata_post.balance = pinata_post - .balance - .checked_sub(pinata_state.prize) - .expect("Not enough balance in the pinata"); - winner_post.balance = winner_post - .balance - .checked_add(pinata_state.prize) - .expect("Overflow when adding prize to winner"); + let mut prize_authorized = prize_pda.clone(); + prize_authorized.is_authorized = true; + + let chained_call = ChainedCall::new( + prize_authorized.account.program_owner, + vec![prize_authorized, winner.clone()], + &authenticated_transfer_core::Instruction::Transfer { + amount: pinata_state.prize, + }, + ) + .with_pda_seeds(vec![PRIZE_SEED]); // Update the last claim timestamp. let updated_state = PinataState { @@ -98,19 +110,18 @@ fn main() { .try_into() .expect("Pinata state should fit in account data"); - // Clock account is read-only. - let clock_post = clock_pre.account.clone(); - ProgramOutput::new( self_program_id, caller_program_id, instruction_words, - vec![pinata, winner, clock_pre], + vec![pinata, prize_pda, winner, clock_pre], vec![ - AccountPostState::new_claimed_if_default(pinata_post, Claim::Authorized), + AccountPostState::new(pinata_post), + AccountPostState::new(prize_pda_post), AccountPostState::new(winner_post), AccountPostState::new(clock_post), ], ) + .with_chained_calls(vec![chained_call]) .write(); }