Skip to main content

chia_sdk_driver/primitives/action_layer/
reward_distributor.rs

1use chia_bls::Signature;
2use chia_protocol::{Bytes32, Coin, CoinSpend, SpendBundle};
3use chia_puzzle_types::cat::CatSolution;
4use chia_puzzle_types::singleton::{LauncherSolution, SingletonArgs};
5use chia_puzzle_types::{
6    LineageProof, Proof,
7    singleton::{SingletonSolution, SingletonStruct},
8};
9use chia_sdk_types::puzzles::{
10    RawActionLayerSolution, ReserveFinalizerSolution, RewardDistributorCommitmentSlotValue,
11    RewardDistributorEntrySlotValue, RewardDistributorRewardSlotValue, RewardDistributorSlotNonce,
12    SlotInfo,
13};
14use chia_sdk_types::{Condition, Conditions};
15use clvm_traits::{FromClvm, clvm_tuple, match_tuple};
16use clvm_utils::{ToTreeHash, tree_hash};
17use clvmr::NodePtr;
18
19use crate::{
20    ActionLayer, ActionLayerSolution, ActionSingleton, Cat, CatSpend, DriverError, Layer, Puzzle,
21    RewardDistributorActionLog, RewardDistributorAddEntryAction,
22    RewardDistributorAddIncentivesAction, RewardDistributorCommitIncentivesAction,
23    RewardDistributorInitiatePayoutAction, RewardDistributorNewEpochAction,
24    RewardDistributorRefreshAction, RewardDistributorRemoveEntryAction,
25    RewardDistributorStakeAction, RewardDistributorStateTransition, RewardDistributorSyncAction,
26    RewardDistributorType, RewardDistributorUnstakeAction,
27    RewardDistributorWithdrawIncentivesAction, SingletonAction, SingletonLayer, Slot, Spend,
28    SpendContext,
29};
30
31use super::{Reserve, RewardDistributorConstants, RewardDistributorInfo, RewardDistributorState};
32
33#[derive(Debug, Clone)]
34pub struct RewardDistributorPendingSpendInfo {
35    pub actions: Vec<Spend>,
36
37    pub spent_reward_slots: Vec<RewardDistributorRewardSlotValue>,
38    pub spent_commitment_slots: Vec<RewardDistributorCommitmentSlotValue>,
39    pub spent_entry_slots: Vec<RewardDistributorEntrySlotValue>,
40
41    pub created_reward_slots: Vec<RewardDistributorRewardSlotValue>,
42    pub created_commitment_slots: Vec<RewardDistributorCommitmentSlotValue>,
43    pub created_entry_slots: Vec<RewardDistributorEntrySlotValue>,
44
45    pub logs: Vec<RewardDistributorActionLog>,
46
47    pub latest_state: (NodePtr, RewardDistributorState),
48
49    pub signature: Signature,
50    pub other_cats: Vec<CatSpend>,
51}
52
53impl RewardDistributorPendingSpendInfo {
54    pub fn new(latest_state: RewardDistributorState) -> Self {
55        Self {
56            actions: vec![],
57            created_reward_slots: vec![],
58            created_commitment_slots: vec![],
59            created_entry_slots: vec![],
60            spent_reward_slots: vec![],
61            spent_commitment_slots: vec![],
62            spent_entry_slots: vec![],
63            logs: vec![],
64            latest_state: (NodePtr::NIL, latest_state),
65            signature: Signature::default(),
66            other_cats: vec![],
67        }
68    }
69
70    pub fn add_delta(&mut self, delta: RewardDistributorPendingSpendInfo) {
71        self.actions.extend(delta.actions);
72
73        self.spent_reward_slots.extend(delta.spent_reward_slots);
74        self.spent_commitment_slots
75            .extend(delta.spent_commitment_slots);
76        self.spent_entry_slots.extend(delta.spent_entry_slots);
77
78        self.created_reward_slots.extend(delta.created_reward_slots);
79        self.created_commitment_slots
80            .extend(delta.created_commitment_slots);
81        self.created_entry_slots.extend(delta.created_entry_slots);
82
83        self.logs.extend(delta.logs);
84
85        self.latest_state = delta.latest_state;
86
87        // do not change pending signature
88        // or other cats
89    }
90}
91
92#[derive(Debug, Clone)]
93#[must_use]
94pub struct RewardDistributor {
95    pub coin: Coin,
96    pub proof: Proof,
97    pub info: RewardDistributorInfo,
98    pub reserve: Reserve,
99
100    pub pending_spend: RewardDistributorPendingSpendInfo,
101}
102
103impl RewardDistributor {
104    pub fn new(coin: Coin, proof: Proof, info: RewardDistributorInfo, reserve: Reserve) -> Self {
105        Self {
106            coin,
107            proof,
108            info,
109            reserve,
110            pending_spend: RewardDistributorPendingSpendInfo::new(info.state),
111        }
112    }
113}
114
115impl RewardDistributor {
116    #[allow(clippy::type_complexity)]
117    pub fn pending_info_delta_from_spend(
118        ctx: &mut SpendContext,
119        action_spend: Spend,
120        current_state_and_ephemeral: (NodePtr, RewardDistributorState),
121        constants: RewardDistributorConstants,
122    ) -> Result<RewardDistributorPendingSpendInfo, DriverError> {
123        let mut spent_reward_slots = vec![];
124        let mut spent_commitment_slots = vec![];
125        let mut spent_entry_slots = vec![];
126
127        let mut created_reward_slots = vec![];
128        let mut created_commitment_slots = vec![];
129        let mut created_entry_slots = vec![];
130
131        let new_epoch_action = RewardDistributorNewEpochAction::from_constants(&constants);
132        let new_epoch_hash = new_epoch_action.tree_hash();
133
134        let commit_incentives_action =
135            RewardDistributorCommitIncentivesAction::from_constants(&constants);
136        let commit_incentives_hash = commit_incentives_action.tree_hash();
137
138        let add_entry_action = RewardDistributorAddEntryAction::from_constants(&constants);
139        let add_entry_hash = add_entry_action.tree_hash();
140
141        let remove_entry_action = RewardDistributorRemoveEntryAction::from_constants(&constants);
142        let remove_entry_hash = remove_entry_action.tree_hash();
143
144        let stake_action = RewardDistributorStakeAction::from_constants(&constants);
145        let stake_hash = stake_action.tree_hash();
146
147        let unstake_action = RewardDistributorUnstakeAction::from_constants(&constants);
148        let unstake_hash = unstake_action.tree_hash();
149
150        let withdraw_incentives_action =
151            RewardDistributorWithdrawIncentivesAction::from_constants(&constants);
152        let withdraw_incentives_hash = withdraw_incentives_action.tree_hash();
153
154        let initiate_payout_action =
155            RewardDistributorInitiatePayoutAction::from_constants(&constants);
156        let initiate_payout_hash = initiate_payout_action.tree_hash();
157
158        let add_incentives_action =
159            RewardDistributorAddIncentivesAction::from_constants(&constants);
160        let add_incentives_hash = add_incentives_action.tree_hash();
161
162        let sync_action = RewardDistributorSyncAction::from_constants(&constants);
163        let sync_hash = sync_action.tree_hash();
164
165        let refresh_action = RewardDistributorRefreshAction::from_constants(&constants);
166        let refresh_hash = refresh_action.tree_hash();
167
168        let actual_solution = ctx.alloc(&clvm_tuple!(
169            current_state_and_ephemeral,
170            action_spend.solution
171        ))?;
172
173        let output = ctx.run(action_spend.puzzle, actual_solution)?;
174        let (new_state_and_ephemeral, _) =
175            ctx.extract::<match_tuple!((NodePtr, RewardDistributorState), NodePtr)>(output)?;
176
177        let changes = RewardDistributorStateTransition {
178            old_state: current_state_and_ephemeral.1,
179            new_state: new_state_and_ephemeral.1,
180        };
181
182        let raw_action_hash = ctx.tree_hash(action_spend.puzzle);
183
184        let log = if raw_action_hash == new_epoch_hash {
185            RewardDistributorActionLog::NewEpoch(RewardDistributorNewEpochAction::get_log(
186                ctx,
187                action_spend.solution,
188                changes,
189            )?)
190        } else if raw_action_hash == commit_incentives_hash {
191            RewardDistributorActionLog::CommitIncentives(
192                RewardDistributorCommitIncentivesAction::get_log(
193                    ctx,
194                    action_spend.solution,
195                    changes,
196                    constants.epoch_seconds,
197                )?,
198            )
199        } else if raw_action_hash == add_entry_hash {
200            RewardDistributorActionLog::AddEntry(RewardDistributorAddEntryAction::get_log(
201                ctx,
202                action_spend.solution,
203                changes,
204            )?)
205        } else if raw_action_hash == stake_hash {
206            RewardDistributorActionLog::Stake(RewardDistributorStakeAction::get_log(
207                ctx,
208                action_spend.solution,
209                changes,
210                constants.reward_distributor_type,
211            )?)
212        } else if raw_action_hash == remove_entry_hash {
213            RewardDistributorActionLog::RemoveEntry(RewardDistributorRemoveEntryAction::get_log(
214                ctx,
215                action_spend.solution,
216                changes,
217            )?)
218        } else if raw_action_hash == unstake_hash {
219            RewardDistributorActionLog::Unstake(RewardDistributorUnstakeAction::get_log(
220                ctx,
221                action_spend.solution,
222                changes,
223                constants.launcher_id,
224                constants.reward_distributor_type,
225                current_state_and_ephemeral.0,
226            )?)
227        } else if raw_action_hash == withdraw_incentives_hash {
228            RewardDistributorActionLog::WithdrawIncentives(
229                RewardDistributorWithdrawIncentivesAction::get_log(
230                    ctx,
231                    action_spend.solution,
232                    changes,
233                    constants.withdrawal_share_bps,
234                )?,
235            )
236        } else if raw_action_hash == initiate_payout_hash {
237            RewardDistributorActionLog::InitiatePayout(
238                RewardDistributorInitiatePayoutAction::get_log(
239                    ctx,
240                    action_spend.solution,
241                    changes,
242                )?,
243            )
244        } else if raw_action_hash == refresh_hash {
245            let RewardDistributorType::CuratedNft {
246                store_launcher_id, ..
247            } = constants.reward_distributor_type
248            else {
249                return Err(DriverError::InvalidMerkleProof);
250            };
251
252            RewardDistributorActionLog::RefreshNftsFromDl(RewardDistributorRefreshAction::get_log(
253                ctx,
254                action_spend.solution,
255                changes,
256                store_launcher_id,
257            )?)
258        } else if raw_action_hash == add_incentives_hash {
259            RewardDistributorActionLog::AddIncentives(
260                RewardDistributorAddIncentivesAction::get_log(ctx, action_spend.solution, changes)?,
261            )
262        } else if raw_action_hash == sync_hash {
263            RewardDistributorActionLog::Sync(RewardDistributorSyncAction::get_log(
264                ctx,
265                action_spend.solution,
266                changes,
267            )?)
268        } else {
269            return Err(DriverError::InvalidMerkleProof);
270        };
271
272        log.extend_spent_slots(
273            &mut spent_reward_slots,
274            &mut spent_commitment_slots,
275            &mut spent_entry_slots,
276        );
277        log.extend_created_slots(
278            &mut created_reward_slots,
279            &mut created_commitment_slots,
280            &mut created_entry_slots,
281        );
282
283        Ok(RewardDistributorPendingSpendInfo {
284            actions: vec![action_spend],
285            spent_reward_slots,
286            spent_commitment_slots,
287            spent_entry_slots,
288            created_reward_slots,
289            created_commitment_slots,
290            created_entry_slots,
291            logs: vec![log],
292            latest_state: new_state_and_ephemeral,
293            signature: Signature::default(),
294            other_cats: vec![],
295        })
296    }
297
298    pub fn pending_info_from_spend(
299        ctx: &mut SpendContext,
300        inner_solution: NodePtr,
301        initial_state: RewardDistributorState,
302        constants: RewardDistributorConstants,
303        signature: Signature,
304    ) -> Result<RewardDistributorPendingSpendInfo, DriverError> {
305        let mut pending_spend_info = RewardDistributorPendingSpendInfo::new(initial_state);
306
307        let inner_solution =
308            ActionLayer::<RewardDistributorState, NodePtr>::parse_solution(ctx, inner_solution)?;
309
310        for raw_action in &inner_solution.action_spends {
311            let delta = Self::pending_info_delta_from_spend(
312                ctx,
313                *raw_action,
314                pending_spend_info.latest_state,
315                constants,
316            )?;
317
318            pending_spend_info.add_delta(delta);
319        }
320
321        pending_spend_info.signature = signature;
322        Ok(pending_spend_info)
323    }
324
325    pub fn from_spend(
326        ctx: &mut SpendContext,
327        spend: &CoinSpend,
328        reserve_lineage_proof: Option<LineageProof>,
329        constants: RewardDistributorConstants,
330        signature: Signature,
331    ) -> Result<Option<Self>, DriverError> {
332        let coin = spend.coin;
333        if coin.amount != 1 {
334            return Ok(None);
335        }
336
337        let puzzle_ptr = ctx.alloc(&spend.puzzle_reveal)?;
338        let puzzle = Puzzle::parse(ctx, puzzle_ptr);
339        let solution_ptr = ctx.alloc(&spend.solution)?;
340
341        let Some(info) = RewardDistributorInfo::parse(ctx, puzzle, constants)? else {
342            return Ok(None);
343        };
344
345        let solution = ctx.extract::<SingletonSolution<NodePtr>>(solution_ptr)?;
346        let proof = solution.lineage_proof;
347
348        let pending_spend = Self::pending_info_from_spend(
349            ctx,
350            solution.inner_solution,
351            info.state,
352            constants,
353            signature,
354        )?;
355
356        let inner_solution =
357            RawActionLayerSolution::<NodePtr, NodePtr, ReserveFinalizerSolution>::from_clvm(
358                ctx,
359                solution.inner_solution,
360            )?;
361
362        let reserve = Reserve::new(
363            inner_solution.finalizer_solution.reserve_parent_id,
364            reserve_lineage_proof.unwrap_or(LineageProof {
365                parent_parent_coin_info: Bytes32::default(),
366                parent_inner_puzzle_hash: Bytes32::default(),
367                parent_amount: 0,
368            }), // dummy default value
369            constants.reserve_asset_id,
370            SingletonStruct::new(info.constants.launcher_id)
371                .tree_hash()
372                .into(),
373            0,
374            info.state.total_reserves,
375        );
376
377        Ok(Some(RewardDistributor {
378            coin,
379            proof,
380            info,
381            reserve,
382            pending_spend,
383        }))
384    }
385
386    pub fn child_lineage_proof(&self) -> LineageProof {
387        LineageProof {
388            parent_parent_coin_info: self.coin.parent_coin_info,
389            parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
390            parent_amount: self.coin.amount,
391        }
392    }
393
394    pub fn from_parent_spend(
395        ctx: &mut SpendContext,
396        parent_spend: &CoinSpend,
397        constants: RewardDistributorConstants,
398    ) -> Result<Option<Self>, DriverError>
399    where
400        Self: Sized,
401    {
402        let Some(parent_registry) =
403            Self::from_spend(ctx, parent_spend, None, constants, Signature::default())?
404        else {
405            return Ok(None);
406        };
407
408        let new_info = parent_registry
409            .info
410            .with_state(parent_registry.pending_spend.latest_state.1);
411
412        Ok(Some(RewardDistributor {
413            coin: Coin::new(
414                parent_registry.coin.coin_id(),
415                new_info.puzzle_hash().into(),
416                1,
417            ),
418            proof: Proof::Lineage(parent_registry.child_lineage_proof()),
419            info: new_info,
420            reserve: parent_registry.reserve.child(new_info.state.total_reserves),
421            pending_spend: RewardDistributorPendingSpendInfo::new(new_info.state),
422        }))
423    }
424
425    pub fn child(&self, child_state: RewardDistributorState) -> Self {
426        let new_info = self.info.with_state(child_state);
427        let new_coin = Coin::new(self.coin.coin_id(), new_info.puzzle_hash().into(), 1);
428        let new_reserve = self.reserve.child(child_state.total_reserves);
429
430        RewardDistributor {
431            coin: new_coin,
432            proof: Proof::Lineage(self.child_lineage_proof()),
433            info: new_info,
434            reserve: new_reserve,
435            pending_spend: RewardDistributorPendingSpendInfo::new(new_info.state),
436        }
437    }
438
439    #[allow(clippy::type_complexity)]
440    pub fn from_launcher_solution(
441        ctx: &mut SpendContext,
442        launcher_coin: Coin,
443        launcher_solution: NodePtr,
444    ) -> Result<Option<(RewardDistributorConstants, RewardDistributorState, Coin)>, DriverError>
445    where
446        Self: Sized,
447    {
448        let Ok(launcher_solution) =
449            ctx.extract::<LauncherSolution<(u64, RewardDistributorConstants)>>(launcher_solution)
450        else {
451            return Ok(None);
452        };
453
454        let launcher_id = launcher_coin.coin_id();
455        let (first_epoch_start, constants) = launcher_solution.key_value_list;
456
457        if constants != constants.with_launcher_id(launcher_id) {
458            return Err(DriverError::Custom(
459                "Distributor constants invalid".to_string(),
460            ));
461        }
462
463        let distributor_eve_coin =
464            Coin::new(launcher_id, launcher_solution.singleton_puzzle_hash, 1);
465
466        let initial_state = RewardDistributorState::initial(first_epoch_start);
467
468        Ok(Some((constants, initial_state, distributor_eve_coin)))
469    }
470
471    #[allow(clippy::type_complexity)]
472    pub fn from_eve_coin_spend(
473        ctx: &mut SpendContext,
474        constants: RewardDistributorConstants,
475        initial_state: RewardDistributorState,
476        eve_coin_spend: &CoinSpend,
477        reserve_parent_id: Bytes32,
478        reserve_lineage_proof: LineageProof,
479    ) -> Result<Option<(RewardDistributor, Slot<RewardDistributorRewardSlotValue>)>, DriverError>
480    where
481        Self: Sized,
482    {
483        let eve_coin_puzzle_ptr = ctx.alloc(&eve_coin_spend.puzzle_reveal)?;
484        let eve_coin_puzzle = Puzzle::parse(ctx, eve_coin_puzzle_ptr);
485        let Some(eve_coin_puzzle) = SingletonLayer::<NodePtr>::parse_puzzle(ctx, eve_coin_puzzle)?
486        else {
487            return Err(DriverError::Custom("Eve coin not a singleton".to_string()));
488        };
489
490        let eve_coin_inner_puzzle_hash = tree_hash(ctx, eve_coin_puzzle.inner_puzzle);
491
492        let eve_coin_solution_ptr = ctx.alloc(&eve_coin_spend.solution)?;
493        let eve_coin_output = ctx.run(eve_coin_puzzle_ptr, eve_coin_solution_ptr)?;
494        let eve_coin_output = ctx.extract::<Conditions<NodePtr>>(eve_coin_output)?;
495
496        let Some(Condition::CreateCoin(odd_create_coin)) = eve_coin_output.into_iter().find(|c| {
497            if let Condition::CreateCoin(create_coin) = c {
498                // singletons with amount != 1 are weird and I don't support them
499                create_coin.amount % 2 == 1
500            } else {
501                false
502            }
503        }) else {
504            return Err(DriverError::Custom(
505                "Eve coin did not create a coin".to_string(),
506            ));
507        };
508
509        let new_coin = Coin::new(
510            eve_coin_spend.coin.coin_id(),
511            odd_create_coin.puzzle_hash,
512            odd_create_coin.amount,
513        );
514        let lineage_proof = LineageProof {
515            parent_parent_coin_info: eve_coin_spend.coin.parent_coin_info,
516            parent_inner_puzzle_hash: eve_coin_inner_puzzle_hash.into(),
517            parent_amount: eve_coin_spend.coin.amount,
518        };
519        let reserve = Reserve::new(
520            reserve_parent_id,
521            reserve_lineage_proof,
522            constants.reserve_asset_id,
523            SingletonStruct::new(constants.launcher_id)
524                .tree_hash()
525                .into(),
526            0,
527            0,
528        );
529        let new_distributor = RewardDistributor::new(
530            new_coin,
531            Proof::Lineage(lineage_proof),
532            RewardDistributorInfo::new(initial_state, constants),
533            reserve,
534        );
535
536        if SingletonArgs::curry_tree_hash(
537            constants.launcher_id,
538            new_distributor.info.inner_puzzle_hash(),
539        ) != new_distributor.coin.puzzle_hash.into()
540        {
541            return Err(DriverError::Custom(
542                "Distributor singleton puzzle hash mismatch".to_string(),
543            ));
544        }
545
546        let slot_value = RewardDistributorRewardSlotValue {
547            counter: 0,
548            epoch_start: initial_state.round_time_info.epoch_end,
549            next_epoch_initialized: false,
550            rewards: 0,
551        };
552
553        let slot = Slot::new(
554            lineage_proof,
555            SlotInfo::from_value(
556                constants.launcher_id,
557                RewardDistributorSlotNonce::REWARD.to_u64(),
558                slot_value,
559            ),
560        );
561
562        Ok(Some((new_distributor, slot)))
563    }
564
565    pub fn set_pending_signature(&mut self, signature: Signature) {
566        self.pending_spend.signature = signature;
567    }
568
569    pub fn set_pending_other_cats(&mut self, other_cats: Vec<CatSpend>) {
570        self.pending_spend.other_cats = other_cats;
571    }
572
573    pub fn from_mempool_item(
574        ctx: &mut SpendContext,
575        mempool_item: SpendBundle,
576        constants: RewardDistributorConstants,
577    ) -> Result<Option<Self>, DriverError> {
578        let mut registry = None;
579
580        let mut other_cats = vec![];
581
582        for spend in &mempool_item.coin_spends {
583            if registry.is_none()
584                && let Some(parsed_registry) =
585                    Self::from_spend(ctx, spend, None, constants, Signature::default())?
586            {
587                registry = Some(parsed_registry);
588            } else {
589                // CAT spends are added to other_cats so the ring is built when the registry
590                // is spent (so it includes the reserve)
591                let puzzle_ptr = ctx.alloc(&spend.puzzle_reveal)?;
592                let puzzle = Puzzle::parse(ctx, puzzle_ptr);
593                let solution_ptr = ctx.alloc(&spend.solution)?;
594
595                if let Ok(Some(parsed_cat)) = Cat::parse(ctx, spend.coin, puzzle, solution_ptr)
596                    && parsed_cat.cat.info.asset_id == constants.reserve_asset_id
597                {
598                    other_cats.push(CatSpend::new(
599                        parsed_cat.cat,
600                        Spend::new(parsed_cat.p2_puzzle.ptr(), parsed_cat.p2_solution),
601                    ));
602                }
603            }
604        }
605
606        let Some(registry) = registry else {
607            return Ok(None);
608        };
609
610        // find & set actual reserve
611        // note that we initialized the registy with reserve_lineage_proof=None, so
612        // only the reserve parent id and amount are correct (but NOT puzzle hash)
613        // the puzzle hash can be obtained from the registry constants
614        let reserve_coin = Coin::new(
615            registry.reserve.coin.parent_coin_info,
616            registry.info.constants.reserve_full_puzzle_hash,
617            registry.reserve.coin.amount,
618        );
619        let Some(reserve_spend) = mempool_item
620            .coin_spends
621            .iter()
622            .find(|c| c.coin == reserve_coin)
623        else {
624            return Err(DriverError::Custom(
625                "Reserve spend not found in mempool item".to_string(),
626            ));
627        };
628
629        let reserve_sol_ptr = ctx.alloc(&reserve_spend.solution)?;
630        let reserve_solution = ctx.extract::<CatSolution<NodePtr>>(reserve_sol_ptr)?;
631
632        // Could theoretically be a bit more optimized, but this ensures
633        //  consistent behavior even if there are future changes in the
634        //  way attributes are calculated for the reward distributor
635        let Some(mut registry) = RewardDistributor::from_spend(
636            ctx,
637            mempool_item
638                .coin_spends
639                .iter()
640                .find(|c| c.coin == registry.coin)
641                .ok_or(DriverError::Custom(
642                    "Couldn't find distributor spend in mempool item".to_string(),
643                ))?,
644            reserve_solution.lineage_proof,
645            constants,
646            Signature::default(),
647        )?
648        else {
649            return Err(DriverError::Custom(
650                "Couldn't rebuild distributor from spend a second time - something's pretty off"
651                    .to_string(),
652            ));
653        };
654
655        while let Some(registry_spend) = mempool_item
656            .coin_spends
657            .iter()
658            .find(|c| c.coin.amount != 0 && c.coin.parent_coin_info == registry.coin.coin_id())
659        {
660            let Some(new_registry) = Self::from_spend(
661                ctx,
662                registry_spend,
663                Some(registry.reserve.child_lineage_proof()),
664                registry.info.constants,
665                Signature::default(),
666            )?
667            else {
668                break;
669            };
670
671            registry = new_registry;
672        }
673
674        // insert all other spends into the context
675        for spend in mempool_item.coin_spends {
676            if spend.coin == registry.coin || other_cats.iter().any(|c| c.cat.coin == spend.coin) {
677                continue;
678            }
679
680            ctx.insert(spend);
681        }
682
683        // filter out 'old' reserve spend from other_cats
684        // finish_spend will add the latest reserve spend
685        other_cats.retain(|c| c.cat.coin != registry.reserve.coin);
686
687        registry.set_pending_other_cats(other_cats);
688        registry.set_pending_signature(mempool_item.aggregated_signature);
689        Ok(Some(registry))
690    }
691}
692
693impl ActionSingleton for RewardDistributor {
694    type State = RewardDistributorState;
695    type Constants = RewardDistributorConstants;
696}
697
698impl RewardDistributor {
699    pub fn finish_spend(
700        self,
701        ctx: &mut SpendContext,
702        other_cat_spends: Vec<CatSpend>,
703    ) -> Result<(Self, Signature), DriverError> {
704        let layers = self.info.into_layers(ctx)?;
705
706        let puzzle = layers.construct_puzzle(ctx)?;
707
708        let action_puzzle_hashes = self
709            .pending_spend
710            .actions
711            .iter()
712            .map(|a| ctx.tree_hash(a.puzzle).into())
713            .collect::<Vec<Bytes32>>();
714
715        let finalizer_solution = ctx.alloc(&ReserveFinalizerSolution {
716            reserve_parent_id: self.reserve.coin.parent_coin_info,
717        })?;
718
719        let child = self.child(self.pending_spend.latest_state.1);
720        let solution = layers.construct_solution(
721            ctx,
722            SingletonSolution {
723                lineage_proof: self.proof,
724                amount: self.coin.amount,
725                inner_solution: ActionLayerSolution {
726                    proofs: layers
727                        .inner_puzzle
728                        .get_proofs(
729                            &RewardDistributorInfo::action_puzzle_hashes(&self.info.constants),
730                            &action_puzzle_hashes,
731                        )
732                        .ok_or(DriverError::Custom(
733                            "Couldn't build proofs for one or more actions".to_string(),
734                        ))?,
735                    action_spends: self.pending_spend.actions,
736                    finalizer_solution,
737                },
738            },
739        )?;
740
741        let my_spend = Spend::new(puzzle, solution);
742        ctx.spend(self.coin, my_spend)?;
743
744        let cat_spend = self.reserve.cat_spend_for_reserve_finalizer_controller(
745            ctx,
746            self.info.state,
747            self.info.inner_puzzle_hash().into(),
748            solution,
749        )?;
750
751        let mut cat_spends = other_cat_spends;
752        cat_spends.push(cat_spend);
753        cat_spends.extend(self.pending_spend.other_cats);
754        Cat::spend_all(ctx, &cat_spends)?;
755
756        Ok((child, self.pending_spend.signature))
757    }
758
759    pub fn new_action<A>(&self) -> A
760    where
761        A: SingletonAction<Self>,
762    {
763        A::from_constants(&self.info.constants)
764    }
765
766    pub fn created_slot_value_to_slot<SlotValue>(
767        &self,
768        slot_value: SlotValue,
769        nonce: RewardDistributorSlotNonce,
770    ) -> Slot<SlotValue>
771    where
772        SlotValue: Copy + ToTreeHash,
773    {
774        Slot::new(
775            LineageProof {
776                parent_parent_coin_info: self.coin.parent_coin_info,
777                parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
778                parent_amount: self.coin.amount,
779            },
780            SlotInfo::from_value(self.info.constants.launcher_id, nonce.to_u64(), slot_value),
781        )
782    }
783
784    pub fn insert_action_spend(
785        &mut self,
786        ctx: &mut SpendContext,
787        action_spend: Spend,
788    ) -> Result<(), DriverError> {
789        let delta = Self::pending_info_delta_from_spend(
790            ctx,
791            action_spend,
792            self.pending_spend.latest_state,
793            self.info.constants,
794        )?;
795
796        self.pending_spend.add_delta(delta);
797
798        Ok(())
799    }
800
801    pub fn actual_reward_slot_value(
802        &self,
803        slot: Slot<RewardDistributorRewardSlotValue>,
804    ) -> Slot<RewardDistributorRewardSlotValue> {
805        let mut slot = slot;
806
807        for slot_value in &self.pending_spend.created_reward_slots {
808            if slot_value.epoch_start == slot.info.value.epoch_start {
809                slot = self
810                    .created_slot_value_to_slot(*slot_value, RewardDistributorSlotNonce::REWARD);
811            }
812        }
813
814        slot
815    }
816
817    pub fn actual_entry_slot_value(
818        &self,
819        slot: Slot<RewardDistributorEntrySlotValue>,
820    ) -> Slot<RewardDistributorEntrySlotValue> {
821        let mut slot = slot;
822
823        for slot_value in &self.pending_spend.created_entry_slots {
824            if slot_value.payout_puzzle_hash == slot.info.value.payout_puzzle_hash {
825                slot =
826                    self.created_slot_value_to_slot(*slot_value, RewardDistributorSlotNonce::ENTRY);
827            }
828        }
829
830        slot
831    }
832
833    pub fn actual_commitment_slot_value(
834        &self,
835        slot: Slot<RewardDistributorCommitmentSlotValue>,
836    ) -> Slot<RewardDistributorCommitmentSlotValue> {
837        let mut slot = slot;
838
839        for slot_value in &self.pending_spend.created_commitment_slots {
840            if slot_value.epoch_start == slot.info.value.epoch_start {
841                slot = self.created_slot_value_to_slot(
842                    *slot_value,
843                    RewardDistributorSlotNonce::COMMITMENT,
844                );
845            }
846        }
847
848        slot
849    }
850}