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

1use chia_protocol::Bytes32;
2use chia_puzzle_types::{nft::NftRoyaltyTransferPuzzleArgs, singleton::SingletonStruct};
3use chia_puzzles::{
4    NFT_OWNERSHIP_LAYER_HASH, NFT_STATE_LAYER_HASH, SINGLETON_LAUNCHER_HASH,
5    SINGLETON_TOP_LAYER_V1_1_HASH,
6};
7use chia_sdk_types::{
8    Conditions, Mod,
9    puzzles::{
10        NONCE_WRAPPER_PUZZLE_HASH, NonceWrapperArgs, P2DelegatedBySingletonLayerArgs,
11        P2DelegatedBySingletonLayerSolution, RewardDistributorEntrySlotValue,
12        RewardDistributorSlotNonce, RewardDistributorUnstakeActionArgs,
13        RewardDistributorUnstakeActionSolution,
14    },
15};
16use clvm_traits::clvm_quote;
17use clvm_utils::{ToTreeHash, TreeHash};
18use clvmr::NodePtr;
19
20use crate::{
21    DriverError, Layer, Nft, P2DelegatedBySingletonLayer, RewardDistributor,
22    RewardDistributorConstants, SingletonAction, Slot, Spend, SpendContext,
23};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct RewardDistributorUnstakeAction {
27    pub launcher_id: Bytes32,
28    pub max_second_offset: u64,
29}
30
31impl ToTreeHash for RewardDistributorUnstakeAction {
32    fn tree_hash(&self) -> TreeHash {
33        Self::new_args(self.launcher_id, self.max_second_offset).curry_tree_hash()
34    }
35}
36
37impl SingletonAction<RewardDistributor> for RewardDistributorUnstakeAction {
38    fn from_constants(constants: &RewardDistributorConstants) -> Self {
39        Self {
40            launcher_id: constants.launcher_id,
41            max_second_offset: constants.max_seconds_offset,
42        }
43    }
44}
45
46impl RewardDistributorUnstakeAction {
47    pub fn new_args(
48        launcher_id: Bytes32,
49        max_second_offset: u64,
50    ) -> RewardDistributorUnstakeActionArgs {
51        RewardDistributorUnstakeActionArgs {
52            singleton_mod_hash: SINGLETON_TOP_LAYER_V1_1_HASH.into(),
53            singleton_launcher_hash: SINGLETON_LAUNCHER_HASH.into(),
54            nft_state_layer_mod_hash: NFT_STATE_LAYER_HASH.into(),
55            nft_ownership_layer_mod_hash: NFT_OWNERSHIP_LAYER_HASH.into(),
56            nonce_mod_hash: NONCE_WRAPPER_PUZZLE_HASH.into(),
57            my_p2_puzzle_hash: Self::my_p2_puzzle_hash(launcher_id),
58            entry_slot_1st_curry_hash: Slot::<()>::first_curry_hash(
59                launcher_id,
60                RewardDistributorSlotNonce::ENTRY.to_u64(),
61            )
62            .into(),
63            max_second_offset,
64        }
65    }
66
67    pub fn my_p2_puzzle_hash(launcher_id: Bytes32) -> Bytes32 {
68        P2DelegatedBySingletonLayerArgs::curry_tree_hash(
69            SingletonStruct::new(launcher_id).tree_hash().into(),
70            1,
71        )
72        .into()
73    }
74
75    fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
76        ctx.curry(Self::new_args(self.launcher_id, self.max_second_offset))
77    }
78
79    pub fn spent_slot_value(
80        ctx: &SpendContext,
81        solution: NodePtr,
82    ) -> Result<RewardDistributorEntrySlotValue, DriverError> {
83        let solution = ctx.extract::<RewardDistributorUnstakeActionSolution>(solution)?;
84
85        Ok(RewardDistributorEntrySlotValue {
86            payout_puzzle_hash: solution.entry_custody_puzzle_hash,
87            initial_cumulative_payout: solution.entry_initial_cumulative_payout,
88            shares: 1,
89        })
90    }
91
92    pub fn spend(
93        self,
94        ctx: &mut SpendContext,
95        distributor: &mut RewardDistributor,
96        entry_slot: Slot<RewardDistributorEntrySlotValue>,
97        locked_nft: Nft,
98    ) -> Result<(Conditions, u64), DriverError> {
99        // u64 = last payment amount
100        let my_state = distributor.pending_spend.latest_state.1;
101        let entry_slot = distributor.actual_entry_slot_value(entry_slot);
102
103        // compute message that the custody puzzle needs to send
104        let unstake_message = locked_nft.info.launcher_id.to_vec();
105
106        let remove_entry_conditions = Conditions::new()
107            .send_message(
108                18,
109                unstake_message.into(),
110                vec![ctx.alloc(&distributor.coin.puzzle_hash)?],
111            )
112            .assert_concurrent_puzzle(entry_slot.coin.puzzle_hash);
113
114        // spend self
115        let entry_payout_amount = entry_slot.info.value.shares
116            * (my_state.round_reward_info.cumulative_payout
117                - entry_slot.info.value.initial_cumulative_payout);
118        let action_solution = ctx.alloc(&RewardDistributorUnstakeActionSolution {
119            nft_launcher_id: locked_nft.info.launcher_id,
120            nft_parent_id: locked_nft.coin.parent_coin_info,
121            nft_metadata_hash: locked_nft.info.metadata.tree_hash().into(),
122            nft_metadata_updater_hash_hash: locked_nft
123                .info
124                .metadata_updater_puzzle_hash
125                .tree_hash()
126                .into(),
127            nft_transfer_porgram_hash: NftRoyaltyTransferPuzzleArgs::curry_tree_hash(
128                locked_nft.info.launcher_id,
129                locked_nft.info.royalty_puzzle_hash,
130                locked_nft.info.royalty_basis_points,
131            )
132            .into(),
133            entry_initial_cumulative_payout: entry_slot.info.value.initial_cumulative_payout,
134            entry_custody_puzzle_hash: entry_slot.info.value.payout_puzzle_hash,
135        })?;
136        let action_puzzle = self.construct_puzzle(ctx)?;
137
138        let registry_inner_puzzle_hash = distributor.info.inner_puzzle_hash();
139        distributor.insert_action_spend(ctx, Spend::new(action_puzzle, action_solution))?;
140
141        // spend NFT
142        let my_p2 = P2DelegatedBySingletonLayer::new(
143            SingletonStruct::new(self.launcher_id).tree_hash().into(),
144            1,
145        );
146        let nft_inner_puzzle = my_p2.construct_puzzle(ctx)?;
147        // don't forget about the nonce wrapper!
148        let nft_inner_puzzle = ctx.curry(NonceWrapperArgs::<Bytes32, NodePtr> {
149            nonce: entry_slot.info.value.payout_puzzle_hash,
150            inner_puzzle: nft_inner_puzzle,
151        })?;
152
153        let hint = ctx.hint(entry_slot.info.value.payout_puzzle_hash)?;
154        let delegated_puzzle = ctx.alloc(&clvm_quote!(Conditions::new().create_coin(
155            entry_slot.info.value.payout_puzzle_hash,
156            1,
157            hint,
158        )))?;
159        let nft_inner_solution = my_p2.construct_solution(
160            ctx,
161            P2DelegatedBySingletonLayerSolution::<NodePtr, NodePtr> {
162                singleton_inner_puzzle_hash: registry_inner_puzzle_hash.into(),
163                delegated_puzzle,
164                delegated_solution: NodePtr::NIL,
165            },
166        )?;
167
168        let _new_nft = locked_nft.spend(ctx, Spend::new(nft_inner_puzzle, nft_inner_solution))?;
169
170        // spend entry slot
171        entry_slot.spend(ctx, distributor.info.inner_puzzle_hash().into())?;
172
173        Ok((remove_entry_conditions, entry_payout_amount))
174    }
175}