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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}