11use {
2- super :: utils:: STAKE_RENT_EXEMPTION , solana_account:: AccountSharedData ,
3- solana_stake_interface:: state:: StakeStateV2 , solana_stake_program:: id,
2+ super :: utils:: { add_sysvars, create_vote_account, STAKE_RENT_EXEMPTION } ,
3+ mollusk_svm:: Mollusk ,
4+ solana_account:: { Account , AccountSharedData , WritableAccount } ,
5+ solana_pubkey:: Pubkey ,
6+ solana_stake_interface:: {
7+ instruction as ixn,
8+ state:: { Authorized , Lockup , StakeStateV2 } ,
9+ } ,
10+ solana_stake_program:: id,
411} ;
512
613/// Lifecycle states for stake accounts in tests
@@ -16,14 +23,134 @@ pub enum StakeLifecycle {
1623}
1724
1825impl StakeLifecycle {
19- /// Create an uninitialized stake account
20- pub fn create_uninitialized_account ( self ) -> AccountSharedData {
21- AccountSharedData :: new_data_with_space (
22- STAKE_RENT_EXEMPTION ,
23- & StakeStateV2 :: Uninitialized ,
24- StakeStateV2 :: size_of ( ) ,
25- & id ( ) ,
26+ /// Create a stake account with full specification of authorities and lockup
27+ #[ allow( clippy:: too_many_arguments) ]
28+ pub fn create_stake_account_fully_specified (
29+ self ,
30+ mollusk : & mut Mollusk ,
31+ // tracker: &mut StakeTracker, // added in subsequent PR
32+ stake_pubkey : & Pubkey ,
33+ vote_account : & Pubkey ,
34+ staked_amount : u64 ,
35+ staker : & Pubkey ,
36+ withdrawer : & Pubkey ,
37+ lockup : & Lockup ,
38+ ) -> AccountSharedData {
39+ let is_closed = self == StakeLifecycle :: Closed ;
40+
41+ // Create base account
42+ let mut stake_account = if is_closed {
43+ let mut account = Account :: create ( STAKE_RENT_EXEMPTION , vec ! [ ] , id ( ) , false , u64:: MAX ) ;
44+ // Add staked_amount even for closed accounts (matches program-test behavior)
45+ if staked_amount > 0 {
46+ account. lamports += staked_amount;
47+ }
48+ account. into ( )
49+ } else {
50+ Account :: create (
51+ STAKE_RENT_EXEMPTION + staked_amount,
52+ vec ! [ 0 ; StakeStateV2 :: size_of( ) ] ,
53+ id ( ) ,
54+ false ,
55+ u64:: MAX ,
56+ )
57+ . into ( )
58+ } ;
59+
60+ if is_closed {
61+ return stake_account;
62+ }
63+
64+ let authorized = Authorized {
65+ staker : * staker,
66+ withdrawer : * withdrawer,
67+ } ;
68+
69+ // Initialize if needed
70+ if self >= StakeLifecycle :: Initialized {
71+ let stake_state = StakeStateV2 :: Initialized ( solana_stake_interface:: state:: Meta {
72+ rent_exempt_reserve : STAKE_RENT_EXEMPTION ,
73+ authorized,
74+ lockup : * lockup,
75+ } ) ;
76+ bincode:: serialize_into ( stake_account. data_as_mut_slice ( ) , & stake_state) . unwrap ( ) ;
77+ }
78+
79+ // Delegate if needed
80+ if self >= StakeLifecycle :: Activating {
81+ let instruction = ixn:: delegate_stake ( stake_pubkey, staker, vote_account) ;
82+
83+ let accounts = vec ! [
84+ ( * stake_pubkey, stake_account. clone( ) ) ,
85+ ( * vote_account, create_vote_account( ) ) ,
86+ ] ;
87+
88+ // Use add_sysvars to provide clock, stake history, and config accounts
89+ let accounts_with_sysvars = add_sysvars ( mollusk, & instruction, accounts) ;
90+ let result = mollusk. process_instruction ( & instruction, & accounts_with_sysvars) ;
91+ stake_account = result. resulting_accounts [ 0 ] . 1 . clone ( ) . into ( ) ;
92+
93+ // Track delegation in the tracker
94+ // let activation_epoch = mollusk.sysvars.clock.epoch;
95+ // TODO: uncomment in subsequent PR (add `tracker.track_delegation` here)
96+ // tracker.track_delegation(stake_pubkey, staked_amount, activation_epoch, vote_account);
97+ }
98+
99+ // Advance epoch to activate if needed (Active and beyond)
100+ if self >= StakeLifecycle :: Active {
101+ // With background stake in tracker, just warp 1 epoch
102+ // The background stake provides baseline for instant partial activation
103+ let slots_per_epoch = mollusk. sysvars . epoch_schedule . slots_per_epoch ;
104+ let current_slot = mollusk. sysvars . clock . slot ;
105+ let target_slot = current_slot + slots_per_epoch;
106+
107+ // TODO: use `warp_to_slot_with_stake_tracking` here (in subsequent PR)
108+ mollusk. warp_to_slot ( target_slot) ;
109+ }
110+
111+ // Deactivate if needed
112+ if self >= StakeLifecycle :: Deactivating {
113+ let instruction = ixn:: deactivate_stake ( stake_pubkey, staker) ;
114+
115+ let accounts = vec ! [ ( * stake_pubkey, stake_account. clone( ) ) ] ;
116+
117+ // Use add_sysvars to provide clock account
118+ let accounts_with_sysvars = add_sysvars ( mollusk, & instruction, accounts) ;
119+ let result = mollusk. process_instruction ( & instruction, & accounts_with_sysvars) ;
120+ stake_account = result. resulting_accounts [ 0 ] . 1 . clone ( ) . into ( ) ;
121+
122+ // Track deactivation in the tracker
123+ // let deactivation_epoch = mollusk.sysvars.clock.epoch;
124+ // TODO: uncomment in subsequent PR
125+ // tracker.track_deactivation(stake_pubkey, deactivation_epoch);
126+ }
127+
128+ // Advance epoch to fully deactivate if needed (Deactive lifecycle)
129+ // Matches program_test.rs line 978-983: advance_epoch once to fully deactivate
130+ if self == StakeLifecycle :: Deactive {
131+ // With background stake, advance 1 epoch for deactivation
132+ // Background provides the baseline for instant partial deactivation
133+ let slots_per_epoch = mollusk. sysvars . epoch_schedule . slots_per_epoch ;
134+ let current_slot = mollusk. sysvars . clock . slot ;
135+ let target_slot = current_slot + slots_per_epoch;
136+
137+ // TODO: use `warp_to_slot_with_stake_tracking` here (in subsequent PR)
138+ mollusk. warp_to_slot ( target_slot) ;
139+ }
140+
141+ stake_account
142+ }
143+
144+ /// Whether this lifecycle stage enforces minimum delegation for split
145+ pub fn split_minimum_enforced ( & self ) -> bool {
146+ matches ! (
147+ self ,
148+ Self :: Activating | Self :: Active | Self :: Deactivating | Self :: Deactive
26149 )
27- . unwrap ( )
150+ }
151+
152+ /// Whether this lifecycle stage enforces minimum delegation for withdraw
153+ pub fn withdraw_minimum_enforced ( & self ) -> bool {
154+ matches ! ( self , Self :: Activating | Self :: Active | Self :: Deactivating )
28155 }
29156}
0 commit comments