chia_sdk_driver/layers/action_layer/actions/reward_distributor/
unstake.rs1use 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 let my_state = distributor.pending_spend.latest_state.1;
101 let entry_slot = distributor.actual_entry_slot_value(entry_slot);
102
103 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 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 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 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 entry_slot.spend(ctx, distributor.info.inner_puzzle_hash().into())?;
172
173 Ok((remove_entry_conditions, entry_payout_amount))
174 }
175}