Skip to main content

chia_sdk_driver/layers/action_layer/actions/reward_distributor/
stake.rs

1use chia_protocol::{Bytes32, Coin};
2use chia_puzzle_types::{
3    nft::NftRoyaltyTransferPuzzleArgs,
4    offer::{NotarizedPayment, Payment},
5    singleton::{SingletonArgs, SingletonStruct},
6};
7use chia_sdk_types::{
8    Conditions, MerkleProof, Mod, announcement_id,
9    puzzles::{
10        NONCE_WRAPPER_PUZZLE_HASH, NftLauncherProof, NonceWrapperArgs,
11        P2DelegatedBySingletonLayerArgs, RewardDistributorCatLockingPuzzleArgs,
12        RewardDistributorCatLockingPuzzleSolution, RewardDistributorEntrySlotValue,
13        RewardDistributorNftsFromDidLockingPuzzleArgs,
14        RewardDistributorNftsFromDidLockingPuzzleSolution,
15        RewardDistributorNftsFromDlLockingPuzzleArgs,
16        RewardDistributorNftsFromDlLockingPuzzleSolution, RewardDistributorSlotNonce,
17        RewardDistributorStakeActionArgs, RewardDistributorStakeActionSolution,
18        StakeNftFromDidInfo, StakeNftFromDlInfo,
19    },
20};
21use clvm_traits::{ToClvm, clvm_tuple};
22use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash};
23use clvmr::{Allocator, NodePtr};
24
25use crate::{
26    Asset, Cat, CatMaker, DriverError, HashedPtr, Nft, RewardDistributor,
27    RewardDistributorConstants, RewardDistributorCreatedAnnouncementPrefix,
28    RewardDistributorNftStakeEntry, RewardDistributorReceivedMessagePrefix,
29    RewardDistributorStakeActionLog, RewardDistributorState, RewardDistributorStateTransition,
30    RewardDistributorType, SingletonAction, Slot, Spend, SpendContext,
31};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct RewardDistributorStakeAction {
35    pub launcher_id: Bytes32,
36    pub max_second_offset: u64,
37    pub distributor_type: RewardDistributorType,
38}
39
40impl ToTreeHash for RewardDistributorStakeAction {
41    fn tree_hash(&self) -> TreeHash {
42        Self::new_args_treehash(
43            self.launcher_id,
44            self.max_second_offset,
45            self.distributor_type,
46        )
47        .curry_tree_hash()
48    }
49}
50
51impl SingletonAction<RewardDistributor> for RewardDistributorStakeAction {
52    fn from_constants(constants: &RewardDistributorConstants) -> Self {
53        Self {
54            launcher_id: constants.launcher_id,
55            max_second_offset: constants.max_seconds_offset,
56            distributor_type: constants.reward_distributor_type,
57        }
58    }
59}
60
61impl RewardDistributorStakeAction {
62    pub fn nft_launcher_id_from_proof(
63        did_launcher_id: Bytes32,
64        proof: &NftLauncherProof,
65    ) -> Bytes32 {
66        let mut coin_id = Coin::new(
67            proof.did_proof.parent_parent_coin_info,
68            SingletonArgs::curry_tree_hash(
69                did_launcher_id,
70                proof.did_proof.parent_inner_puzzle_hash.into(),
71            )
72            .into(),
73            proof.did_proof.parent_amount,
74        )
75        .coin_id();
76
77        for intermediary in proof.intermediary_coin_proofs.iter().rev() {
78            coin_id =
79                Coin::new(coin_id, intermediary.full_puzzle_hash, intermediary.amount).coin_id();
80        }
81
82        coin_id
83    }
84
85    fn nft_entries_from_stake_lock_solution(
86        ctx: &SpendContext,
87        lock_puzzle_solution: NodePtr,
88        distributor_type: RewardDistributorType,
89    ) -> Result<Option<Vec<RewardDistributorNftStakeEntry>>, DriverError> {
90        match distributor_type {
91            RewardDistributorType::NftCollection {
92                collection_did_launcher_id,
93            } => {
94                let lock_solution = ctx
95                    .extract::<RewardDistributorNftsFromDidLockingPuzzleSolution>(
96                        lock_puzzle_solution,
97                    )?;
98                let entries = lock_solution
99                    .nft_infos
100                    .iter()
101                    .map(|info| RewardDistributorNftStakeEntry {
102                        launcher_id: Self::nft_launcher_id_from_proof(
103                            collection_did_launcher_id,
104                            &info.nft_launcher_proof,
105                        ),
106                        shares: 1,
107                    })
108                    .collect();
109                Ok(Some(entries))
110            }
111            RewardDistributorType::CuratedNft { .. } => {
112                let lock_solution = ctx
113                    .extract::<RewardDistributorNftsFromDlLockingPuzzleSolution>(
114                        lock_puzzle_solution,
115                    )?;
116                Ok(Some(
117                    lock_solution
118                        .nft_infos
119                        .iter()
120                        .map(|info: &StakeNftFromDlInfo| RewardDistributorNftStakeEntry {
121                            launcher_id: info.nft_launcher_id,
122                            shares: info.nft_shares,
123                        })
124                        .collect(),
125                ))
126            }
127            _ => Ok(None),
128        }
129    }
130
131    fn cat_amount_from_stake_lock_solution(
132        ctx: &SpendContext,
133        lock_puzzle_solution: NodePtr,
134        distributor_type: RewardDistributorType,
135    ) -> Result<Option<u64>, DriverError> {
136        match distributor_type {
137            RewardDistributorType::Cat { .. } => {
138                let lock_solution = ctx
139                    .extract::<RewardDistributorCatLockingPuzzleSolution<NodePtr>>(
140                        lock_puzzle_solution,
141                    )?;
142                Ok(Some(lock_solution.cat_amount))
143            }
144            _ => Ok(None),
145        }
146    }
147
148    fn stake_cat_and_nft_from_solution(
149        ctx: &SpendContext,
150        solution: NodePtr,
151        distributor_type: RewardDistributorType,
152    ) -> Result<(Option<u64>, Option<Vec<RewardDistributorNftStakeEntry>>), DriverError> {
153        let solution = ctx.extract::<RewardDistributorStakeActionSolution<NodePtr>>(solution)?;
154        let cat_amount = Self::cat_amount_from_stake_lock_solution(
155            ctx,
156            solution.lock_puzzle_solution,
157            distributor_type,
158        )?;
159        let nft_entries = Self::nft_entries_from_stake_lock_solution(
160            ctx,
161            solution.lock_puzzle_solution,
162            distributor_type,
163        )?;
164        Ok((cat_amount, nft_entries))
165    }
166
167    pub fn new_args(
168        ctx: &mut SpendContext,
169        launcher_id: Bytes32,
170        max_second_offset: u64,
171        distributor_type: RewardDistributorType,
172    ) -> Result<RewardDistributorStakeActionArgs<NodePtr>, DriverError> {
173        let lock_puzzle = match distributor_type {
174            RewardDistributorType::Managed {
175                manager_singleton_launcher_id: _,
176            } => Err(DriverError::Custom(
177                "Stake action not available in managed mode".to_string(),
178            )),
179            RewardDistributorType::NftCollection {
180                collection_did_launcher_id,
181            } => ctx.curry(RewardDistributorNftsFromDidLockingPuzzleArgs::new(
182                collection_did_launcher_id,
183                Self::my_p2_puzzle_hash(launcher_id),
184            )),
185            RewardDistributorType::CuratedNft {
186                store_launcher_id,
187                refreshable: _,
188            } => ctx.curry(RewardDistributorNftsFromDlLockingPuzzleArgs::new(
189                store_launcher_id,
190                Self::my_p2_puzzle_hash(launcher_id),
191            )),
192            RewardDistributorType::Cat {
193                asset_id,
194                hidden_puzzle_hash,
195            } => {
196                let cat_maker = if let Some(hidden_puzzle_hash) = hidden_puzzle_hash {
197                    CatMaker::Revocable {
198                        tail_hash_hash: asset_id.tree_hash(),
199                        hidden_puzzle_hash_hash: hidden_puzzle_hash.tree_hash(),
200                    }
201                } else {
202                    CatMaker::Default {
203                        tail_hash_hash: asset_id.tree_hash(),
204                    }
205                };
206                let cat_maker_puzzle = cat_maker.get_puzzle(ctx)?;
207
208                ctx.curry(RewardDistributorCatLockingPuzzleArgs::new(
209                    cat_maker_puzzle,
210                    Self::my_p2_puzzle_hash(launcher_id),
211                ))
212            }
213        }?;
214
215        Ok(RewardDistributorStakeActionArgs {
216            entry_slot_1st_curry_hash: Slot::<()>::first_curry_hash(
217                launcher_id,
218                RewardDistributorSlotNonce::ENTRY.to_u64(),
219            )
220            .into(),
221            max_second_offset,
222            lock_puzzle,
223        })
224    }
225
226    pub fn new_args_treehash(
227        launcher_id: Bytes32,
228        max_second_offset: u64,
229        distributor_type: RewardDistributorType,
230    ) -> RewardDistributorStakeActionArgs<TreeHash> {
231        let lock_puzzle_hash = match distributor_type {
232            RewardDistributorType::Managed {
233                manager_singleton_launcher_id: _,
234            } => TreeHash::new([0; 32]),
235            RewardDistributorType::NftCollection {
236                collection_did_launcher_id,
237            } => RewardDistributorNftsFromDidLockingPuzzleArgs::new(
238                collection_did_launcher_id,
239                Self::my_p2_puzzle_hash(launcher_id),
240            )
241            .curry_tree_hash(),
242            RewardDistributorType::CuratedNft {
243                store_launcher_id,
244                refreshable: _,
245            } => RewardDistributorNftsFromDlLockingPuzzleArgs::new(
246                store_launcher_id,
247                Self::my_p2_puzzle_hash(launcher_id),
248            )
249            .curry_tree_hash(),
250            RewardDistributorType::Cat {
251                asset_id,
252                hidden_puzzle_hash,
253            } => {
254                let cat_maker = if let Some(hidden_puzzle_hash) = hidden_puzzle_hash {
255                    CatMaker::Revocable {
256                        tail_hash_hash: asset_id.tree_hash(),
257                        hidden_puzzle_hash_hash: hidden_puzzle_hash.tree_hash(),
258                    }
259                } else {
260                    CatMaker::Default {
261                        tail_hash_hash: asset_id.tree_hash(),
262                    }
263                };
264                let cat_maker_puzzle_hash = cat_maker.curry_tree_hash();
265
266                RewardDistributorCatLockingPuzzleArgs::new(
267                    cat_maker_puzzle_hash,
268                    Self::my_p2_puzzle_hash(launcher_id),
269                )
270                .curry_tree_hash()
271            }
272        };
273
274        RewardDistributorStakeActionArgs {
275            entry_slot_1st_curry_hash: Slot::<()>::first_curry_hash(
276                launcher_id,
277                RewardDistributorSlotNonce::ENTRY.to_u64(),
278            )
279            .into(),
280            max_second_offset,
281            lock_puzzle: lock_puzzle_hash,
282        }
283    }
284
285    pub fn my_p2_puzzle_hash(launcher_id: Bytes32) -> Bytes32 {
286        P2DelegatedBySingletonLayerArgs::curry_tree_hash(
287            SingletonStruct::new(launcher_id).tree_hash().into(),
288            1,
289        )
290        .into()
291    }
292
293    fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
294        let args = Self::new_args(
295            ctx,
296            self.launcher_id,
297            self.max_second_offset,
298            self.distributor_type,
299        )?;
300
301        ctx.curry(args)
302    }
303
304    pub fn created_slot_value<LPS>(
305        ctx: &mut SpendContext,
306        state: &RewardDistributorState,
307        distributor_type: RewardDistributorType,
308        solution: &RewardDistributorStakeActionSolution<LPS>,
309    ) -> Result<RewardDistributorEntrySlotValue, DriverError>
310    where
311        LPS: ToClvm<Allocator> + Clone,
312    {
313        let lock_puzzle = Self::new_args(ctx, Bytes32::default(), 1, distributor_type)?.lock_puzzle;
314        let actual_lock_solution = ctx.alloc(&(
315            1,
316            (
317                solution.entry_custody_puzzle_hash,
318                solution.lock_puzzle_solution.clone(),
319            ),
320        ))?;
321
322        let lock_puzzle_output = ctx.run(lock_puzzle, actual_lock_solution)?;
323        let (new_shares, _conds): (u64, NodePtr) = ctx.extract(lock_puzzle_output)?;
324
325        Ok(RewardDistributorEntrySlotValue {
326            counter: u64::try_from(solution.existing_slot_counter + 1)?,
327            payout_puzzle_hash: solution.entry_custody_puzzle_hash,
328            initial_cumulative_payout: state.round_reward_info.cumulative_payout,
329            shares: solution.existing_slot_shares + new_shares,
330        })
331    }
332
333    pub fn get_log(
334        ctx: &mut SpendContext,
335        solution: NodePtr,
336        changes: RewardDistributorStateTransition,
337        distributor_type: RewardDistributorType,
338    ) -> Result<RewardDistributorStakeActionLog, DriverError> {
339        let stake_solution =
340            ctx.extract::<RewardDistributorStakeActionSolution<NodePtr>>(solution)?;
341
342        let spent_entry_slot = if stake_solution.existing_slot_counter == -1i128 {
343            None
344        } else {
345            Some(RewardDistributorEntrySlotValue {
346                counter: u64::try_from(stake_solution.existing_slot_counter)?,
347                payout_puzzle_hash: stake_solution.entry_custody_puzzle_hash,
348                initial_cumulative_payout: stake_solution.existing_slot_cumulative_payout,
349                shares: stake_solution.existing_slot_shares,
350            })
351        };
352
353        let created_entry_slot =
354            Self::created_slot_value(ctx, &changes.old_state, distributor_type, &stake_solution)?;
355
356        let (cat_amount, nft_entries) =
357            Self::stake_cat_and_nft_from_solution(ctx, solution, distributor_type)?;
358
359        Ok(RewardDistributorStakeActionLog {
360            spent_entry_slot,
361            created_entry_slot,
362            cat_amount,
363            nft_entries,
364            changes,
365        })
366    }
367
368    #[allow(clippy::cast_possible_wrap)]
369    pub fn spend_for_collection_nft_mode(
370        self,
371        ctx: &mut SpendContext,
372        distributor: &mut RewardDistributor,
373        offered_nfts: &[Nft],
374        nft_launcher_proofs: &[NftLauncherProof],
375        entry_custody_puzzle_hash: Bytes32,
376        existing_slot: Option<Slot<RewardDistributorEntrySlotValue>>,
377    ) -> Result<(Conditions, Vec<NotarizedPayment>, Vec<Nft>), DriverError> {
378        let ephemeral_counter =
379            ctx.extract::<HashedPtr>(distributor.pending_spend.latest_state.0)?;
380        let my_id = distributor.coin.coin_id();
381
382        // calculate notarized payments; spend said nfts
383        let my_p2 = Self::my_p2_puzzle_hash(self.launcher_id);
384        let my_p2_treehash: TreeHash = my_p2.into();
385        let payment_puzzle_hash: Bytes32 = CurriedProgram {
386            program: NONCE_WRAPPER_PUZZLE_HASH,
387            args: NonceWrapperArgs::<(Bytes32, u64), TreeHash> {
388                nonce: clvm_tuple!(entry_custody_puzzle_hash, 1),
389                inner_puzzle: my_p2_treehash,
390            },
391        }
392        .tree_hash()
393        .into();
394
395        let mut notarized_payments = Vec::with_capacity(offered_nfts.len());
396        let mut created_nfts = Vec::with_capacity(offered_nfts.len());
397        let mut nft_infos = Vec::with_capacity(offered_nfts.len());
398        let mut security_conditions = Conditions::new();
399        for i in 0..offered_nfts.len() {
400            let nonce: Bytes32 = clvm_tuple!(i, clvm_tuple!(ephemeral_counter.tree_hash(), my_id))
401                .tree_hash()
402                .into();
403            let np = NotarizedPayment {
404                // i = cumulative shares until now since each NFT has a weight of 1 in the Collection NFT mode
405                nonce,
406                payments: vec![Payment::new(
407                    payment_puzzle_hash,
408                    1,
409                    ctx.hint(
410                        clvm_tuple!(entry_custody_puzzle_hash, my_p2)
411                            .tree_hash()
412                            .into(),
413                    )?,
414                )],
415            };
416            let notarized_payment_ptr = ctx.alloc(&np)?;
417            notarized_payments.push(np);
418
419            created_nfts.push(offered_nfts[i].child(
420                payment_puzzle_hash,
421                offered_nfts[i].info.current_owner,
422                offered_nfts[i].info.metadata,
423                offered_nfts[i].coin.amount,
424            ));
425
426            nft_infos.push(StakeNftFromDidInfo {
427                nft_metadata_hash: offered_nfts[i].info.metadata.tree_hash().into(),
428                nft_metadata_updater_hash_hash: offered_nfts[i]
429                    .info
430                    .metadata_updater_puzzle_hash
431                    .tree_hash()
432                    .into(),
433                nft_owner: offered_nfts[i].info.current_owner,
434                nft_transfer_porgram_hash: NftRoyaltyTransferPuzzleArgs::curry_tree_hash(
435                    offered_nfts[i].info.launcher_id,
436                    offered_nfts[i].info.royalty_puzzle_hash,
437                    offered_nfts[i].info.royalty_basis_points,
438                )
439                .into(),
440                nft_launcher_proof: nft_launcher_proofs[i].clone(),
441            });
442
443            let msg: Bytes32 = ctx.tree_hash(notarized_payment_ptr).into();
444            security_conditions = security_conditions.assert_puzzle_announcement(announcement_id(
445                distributor.coin.puzzle_hash,
446                RewardDistributorCreatedAnnouncementPrefix::stake_lock(announcement_id(
447                    offered_nfts[i].coin.puzzle_hash,
448                    msg,
449                )),
450            ));
451        }
452
453        let existing_slot = existing_slot.map(|s| distributor.actual_entry_slot_value(s));
454
455        // spend self
456        let lock_puzzle_solution = RewardDistributorNftsFromDidLockingPuzzleSolution {
457            my_id: distributor.coin.coin_id(),
458            nft_infos,
459        };
460        let action_solution = &RewardDistributorStakeActionSolution {
461            lock_puzzle_solution,
462            existing_slot_counter: existing_slot
463                .as_ref()
464                .map_or(-1i128, |s| i128::from(s.info.value.counter)),
465            entry_custody_puzzle_hash,
466            existing_slot_cumulative_payout: existing_slot
467                .as_ref()
468                .map_or(0, |s| s.info.value.initial_cumulative_payout),
469            existing_slot_shares: existing_slot.as_ref().map_or(0, |s| s.info.value.shares),
470        };
471        let action_puzzle = self.construct_puzzle(ctx)?;
472
473        // if needed, spend existing slot
474        if let Some(existing_slot) = existing_slot {
475            let rewards_to_give_up = u128::from(existing_slot.info.value.shares)
476                * (distributor
477                    .pending_spend
478                    .latest_state
479                    .1
480                    .round_reward_info
481                    .cumulative_payout
482                    - existing_slot.info.value.initial_cumulative_payout);
483            security_conditions = security_conditions.send_message(
484                18,
485                RewardDistributorReceivedMessagePrefix::stake(rewards_to_give_up).into(),
486                vec![ctx.alloc(&distributor.coin.puzzle_hash)?],
487            );
488            existing_slot.spend(ctx, distributor.info.inner_puzzle_hash().into())?;
489        }
490
491        // ensure new slot is properly created
492        let new_slot_value = Self::created_slot_value(
493            ctx,
494            &distributor.pending_spend.latest_state.1,
495            self.distributor_type,
496            action_solution,
497        )?;
498        security_conditions = security_conditions.assert_puzzle_announcement(announcement_id(
499            distributor.coin.puzzle_hash,
500            RewardDistributorCreatedAnnouncementPrefix::stake_slot(new_slot_value.tree_hash()),
501        ));
502        let action_solution = ctx.alloc(&action_solution)?;
503        distributor.insert_action_spend(ctx, Spend::new(action_puzzle, action_solution))?;
504
505        Ok((security_conditions, notarized_payments, created_nfts))
506    }
507
508    #[allow(clippy::too_many_arguments)]
509    #[allow(clippy::cast_possible_wrap)]
510    pub fn spend_for_curated_nft_mode(
511        self,
512        ctx: &mut SpendContext,
513        distributor: &mut RewardDistributor,
514        offered_nfts: &[Nft],
515        nft_shares: &[u64],
516        inclusion_proofs: &[MerkleProof],
517        entry_custody_puzzle_hash: Bytes32,
518        existing_slot: Option<Slot<RewardDistributorEntrySlotValue>>,
519        dl_root_hash: Bytes32,
520        dl_metadata_rest_hash: Option<Bytes32>,
521        dl_metadata_updater_hash_hash: Bytes32,
522        dl_inner_puzzle_hash: Bytes32,
523    ) -> Result<(Conditions, Vec<NotarizedPayment>, Vec<Nft>), DriverError> {
524        let ephemeral_counter =
525            ctx.extract::<HashedPtr>(distributor.pending_spend.latest_state.0)?;
526        let my_id = distributor.coin.coin_id();
527
528        // calculate notarized payments; spend said nfts
529        let my_p2 = Self::my_p2_puzzle_hash(self.launcher_id);
530        let my_p2_treehash: TreeHash = my_p2.into();
531
532        let mut notarized_payments = Vec::with_capacity(offered_nfts.len());
533        let mut created_nfts = Vec::with_capacity(offered_nfts.len());
534        let mut nft_infos = Vec::with_capacity(offered_nfts.len());
535        let mut security_conditions = Conditions::new();
536        let mut total_shares_until_now = 0;
537        for i in 0..offered_nfts.len() {
538            let payment_puzzle_hash: Bytes32 = CurriedProgram {
539                program: NONCE_WRAPPER_PUZZLE_HASH,
540                args: NonceWrapperArgs::<(Bytes32, u64), TreeHash> {
541                    nonce: clvm_tuple!(entry_custody_puzzle_hash, nft_shares[i]),
542                    inner_puzzle: my_p2_treehash,
543                },
544            }
545            .tree_hash()
546            .into();
547
548            let np = NotarizedPayment {
549                // NFTs may have different weights in curated NFT mode
550                nonce: clvm_tuple!(
551                    total_shares_until_now,
552                    clvm_tuple!(ephemeral_counter.tree_hash(), my_id)
553                )
554                .tree_hash()
555                .into(),
556                payments: vec![Payment::new(
557                    payment_puzzle_hash,
558                    1,
559                    ctx.hint(
560                        clvm_tuple!(entry_custody_puzzle_hash, my_p2)
561                            .tree_hash()
562                            .into(),
563                    )?,
564                )],
565            };
566            let notarized_payment_ptr = ctx.alloc(&np)?;
567            notarized_payments.push(np);
568            total_shares_until_now += nft_shares[i];
569
570            created_nfts.push(offered_nfts[i].child(
571                payment_puzzle_hash,
572                offered_nfts[i].info.current_owner,
573                offered_nfts[i].info.metadata,
574                offered_nfts[i].coin.amount,
575            ));
576
577            nft_infos.push(StakeNftFromDlInfo {
578                nft_launcher_id: offered_nfts[i].info.launcher_id,
579                nft_metadata_hash: offered_nfts[i].info.metadata.tree_hash().into(),
580                nft_metadata_updater_hash_hash: offered_nfts[i]
581                    .info
582                    .metadata_updater_puzzle_hash
583                    .tree_hash()
584                    .into(),
585                nft_owner: offered_nfts[i].info.current_owner,
586                nft_transfer_porgram_hash: NftRoyaltyTransferPuzzleArgs::curry_tree_hash(
587                    offered_nfts[i].info.launcher_id,
588                    offered_nfts[i].info.royalty_puzzle_hash,
589                    offered_nfts[i].info.royalty_basis_points,
590                )
591                .into(),
592                nft_shares: nft_shares[i],
593                nft_inclusion_proof: inclusion_proofs[i].clone(),
594            });
595
596            let msg: Bytes32 = ctx.tree_hash(notarized_payment_ptr).into();
597            security_conditions = security_conditions.assert_puzzle_announcement(announcement_id(
598                distributor.coin.puzzle_hash,
599                RewardDistributorCreatedAnnouncementPrefix::stake_lock(announcement_id(
600                    offered_nfts[i].coin.puzzle_hash,
601                    msg,
602                )),
603            ));
604        }
605
606        let existing_slot = existing_slot.map(|s| distributor.actual_entry_slot_value(s));
607
608        // spend self
609        let lock_puzzle_solution = RewardDistributorNftsFromDlLockingPuzzleSolution {
610            my_id: distributor.coin.coin_id(),
611            nft_infos,
612            dl_root_hash,
613            dl_metadata_rest_hash,
614            dl_metadata_updater_hash_hash,
615            dl_inner_puzzle_hash,
616        };
617        let action_solution = RewardDistributorStakeActionSolution {
618            lock_puzzle_solution,
619            existing_slot_counter: existing_slot
620                .as_ref()
621                .map_or(-1i128, |s| i128::from(s.info.value.counter)),
622            entry_custody_puzzle_hash,
623            existing_slot_cumulative_payout: existing_slot
624                .as_ref()
625                .map_or(0, |s| s.info.value.initial_cumulative_payout),
626            existing_slot_shares: existing_slot.as_ref().map_or(0, |s| s.info.value.shares),
627        };
628        let action_puzzle = self.construct_puzzle(ctx)?;
629
630        // if needed, spend existing slot
631        if let Some(existing_slot) = existing_slot {
632            let rewards_to_give_up = u128::from(existing_slot.info.value.shares)
633                * (distributor
634                    .pending_spend
635                    .latest_state
636                    .1
637                    .round_reward_info
638                    .cumulative_payout
639                    - existing_slot.info.value.initial_cumulative_payout);
640            security_conditions = security_conditions.send_message(
641                18,
642                RewardDistributorReceivedMessagePrefix::stake(rewards_to_give_up).into(),
643                vec![ctx.alloc(&distributor.coin.puzzle_hash)?],
644            );
645            existing_slot.spend(ctx, distributor.info.inner_puzzle_hash().into())?;
646        }
647
648        // ensure new slot is properly created
649        let new_slot_value = Self::created_slot_value(
650            ctx,
651            &distributor.pending_spend.latest_state.1,
652            self.distributor_type,
653            &action_solution,
654        )?;
655        security_conditions = security_conditions.assert_puzzle_announcement(announcement_id(
656            distributor.coin.puzzle_hash,
657            RewardDistributorCreatedAnnouncementPrefix::stake_slot(new_slot_value.tree_hash()),
658        ));
659        let action_solution = ctx.alloc(&action_solution)?;
660        distributor.insert_action_spend(ctx, Spend::new(action_puzzle, action_solution))?;
661
662        Ok((security_conditions, notarized_payments, created_nfts))
663    }
664
665    #[allow(clippy::cast_possible_wrap)]
666    pub fn spend_for_cat_mode(
667        self,
668        ctx: &mut SpendContext,
669        distributor: &mut RewardDistributor,
670        offered_cat: Cat,
671        entry_custody_puzzle_hash: Bytes32,
672        existing_slot: Option<Slot<RewardDistributorEntrySlotValue>>,
673    ) -> Result<(Conditions, NotarizedPayment, Cat), DriverError> {
674        let ephemeral_counter =
675            ctx.extract::<HashedPtr>(distributor.pending_spend.latest_state.0)?;
676        let my_id = distributor.coin.coin_id();
677
678        // calculate notarized payments; spend said nfts
679        let my_p2 = Self::my_p2_puzzle_hash(self.launcher_id);
680        let my_p2_treehash: TreeHash = my_p2.into();
681        let payment_puzzle_hash: Bytes32 = CurriedProgram {
682            program: NONCE_WRAPPER_PUZZLE_HASH,
683            args: NonceWrapperArgs::<(Bytes32, u64), TreeHash> {
684                nonce: clvm_tuple!(entry_custody_puzzle_hash, offered_cat.amount()),
685                inner_puzzle: my_p2_treehash,
686            },
687        }
688        .tree_hash()
689        .into();
690
691        let np = NotarizedPayment {
692            nonce: clvm_tuple!(ephemeral_counter.tree_hash(), my_id)
693                .tree_hash()
694                .into(),
695            payments: vec![Payment::new(
696                payment_puzzle_hash,
697                offered_cat.amount(),
698                ctx.hint(
699                    clvm_tuple!(entry_custody_puzzle_hash, my_p2)
700                        .tree_hash()
701                        .into(),
702                )?,
703            )],
704        };
705        let notarized_payment_ptr = ctx.alloc(&np)?;
706
707        let msg: Bytes32 = ctx.tree_hash(notarized_payment_ptr).into();
708        let mut security_conditions =
709            Conditions::new().assert_puzzle_announcement(announcement_id(
710                distributor.coin.puzzle_hash,
711                RewardDistributorCreatedAnnouncementPrefix::stake_lock(announcement_id(
712                    offered_cat.coin.puzzle_hash,
713                    msg,
714                )),
715            ));
716
717        let existing_slot = existing_slot.map(|s| distributor.actual_entry_slot_value(s));
718
719        // spend self
720        let lock_puzzle_solution = RewardDistributorCatLockingPuzzleSolution {
721            my_id: distributor.coin.coin_id(),
722            cat_amount: offered_cat.amount(),
723            cat_maker_solution_rest: (),
724        };
725        let action_solution = RewardDistributorStakeActionSolution {
726            lock_puzzle_solution,
727            existing_slot_counter: existing_slot
728                .as_ref()
729                .map_or(-1i128, |s| i128::from(s.info.value.counter)),
730            entry_custody_puzzle_hash,
731            existing_slot_cumulative_payout: existing_slot
732                .as_ref()
733                .map_or(0, |s| s.info.value.initial_cumulative_payout),
734            existing_slot_shares: existing_slot.as_ref().map_or(0, |s| s.info.value.shares),
735        };
736        let action_puzzle = self.construct_puzzle(ctx)?;
737
738        // if needed, spend existing slot
739        if let Some(existing_slot) = existing_slot {
740            let rewards_to_give_up = u128::from(existing_slot.info.value.shares)
741                * (distributor
742                    .pending_spend
743                    .latest_state
744                    .1
745                    .round_reward_info
746                    .cumulative_payout
747                    - existing_slot.info.value.initial_cumulative_payout);
748            security_conditions = security_conditions.send_message(
749                18,
750                RewardDistributorReceivedMessagePrefix::stake(rewards_to_give_up).into(),
751                vec![ctx.alloc(&distributor.coin.puzzle_hash)?],
752            );
753            existing_slot.spend(ctx, distributor.info.inner_puzzle_hash().into())?;
754        }
755
756        // ensure new slot is properly created
757        let new_slot_value = Self::created_slot_value(
758            ctx,
759            &distributor.pending_spend.latest_state.1,
760            self.distributor_type,
761            &action_solution,
762        )?;
763        security_conditions = security_conditions.assert_puzzle_announcement(announcement_id(
764            distributor.coin.puzzle_hash,
765            RewardDistributorCreatedAnnouncementPrefix::stake_slot(new_slot_value.tree_hash()),
766        ));
767        let action_solution = ctx.alloc(&action_solution)?;
768        distributor.insert_action_spend(ctx, Spend::new(action_puzzle, action_solution))?;
769
770        Ok((
771            security_conditions,
772            np,
773            offered_cat.child(payment_puzzle_hash, offered_cat.amount()),
774        ))
775    }
776}