test(pinata_cooldown): pay the prize via an authenticated_transfer PDA

This commit is contained in:
Artem Gureev
2026-08-13 19:14:54 +00:00
parent 72b75d0b91
commit 0bc568af81
2 changed files with 61 additions and 27 deletions
+28 -5
View File
@@ -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);
}
+33 -22
View File
@@ -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::<Instruction>();
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();
}