Skip to main content

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

1use chia_protocol::Bytes32;
2use chia_puzzle_types::singleton::SingletonArgs;
3use chia_puzzle_types::{nft::NftRoyaltyTransferPuzzleArgs, singleton::SingletonStruct};
4use chia_sdk_types::{
5    Conditions, MerkleProof, Mod, announcement_id,
6    puzzles::{
7        NONCE_WRAPPER_PUZZLE_HASH, NonceWrapperArgs, P2DelegatedBySingletonLayerArgs,
8        P2DelegatedBySingletonLayerSolution, RefreshNftInfo, RewardDistributorDlInfo,
9        RewardDistributorEntryPayoutInfo, RewardDistributorEntrySlotValue,
10        RewardDistributorRefreshNftsFromDlActionArgs,
11        RewardDistributorRefreshNftsFromDlActionSolution, RewardDistributorRefreshNftsTotals,
12        RewardDistributorSlotNonce, SlotAndNfts,
13    },
14};
15use clvm_traits::{clvm_quote, clvm_tuple};
16use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash};
17use clvmr::NodePtr;
18
19use crate::{
20    DriverError, Layer, Nft, P2DelegatedBySingletonLayer, RewardDistributor,
21    RewardDistributorConstants, RewardDistributorCreatedAnnouncementPrefix,
22    RewardDistributorNftStakeEntry, RewardDistributorRefreshNftsFromDlActionLog,
23    RewardDistributorStateTransition, RewardDistributorType, SingletonAction, Slot, Spend,
24    SpendContext,
25};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct RewardDistributorRefreshAction {
29    pub launcher_id: Bytes32,
30    pub max_second_offset: u64,
31    pub distributor_type: RewardDistributorType,
32    pub precision: u64,
33}
34
35impl ToTreeHash for RewardDistributorRefreshAction {
36    fn tree_hash(&self) -> TreeHash {
37        if let Ok(args) = Self::new_args(
38            self.launcher_id,
39            self.max_second_offset,
40            self.distributor_type,
41            self.precision,
42        ) {
43            args.curry_tree_hash()
44        } else {
45            TreeHash::new([0; 32])
46        }
47    }
48}
49
50impl SingletonAction<RewardDistributor> for RewardDistributorRefreshAction {
51    fn from_constants(constants: &RewardDistributorConstants) -> Self {
52        Self {
53            launcher_id: constants.launcher_id,
54            max_second_offset: constants.max_seconds_offset,
55            distributor_type: constants.reward_distributor_type,
56            precision: constants.precision,
57        }
58    }
59}
60
61impl RewardDistributorRefreshAction {
62    pub fn new_args(
63        launcher_id: Bytes32,
64        max_second_offset: u64,
65        distributor_type: RewardDistributorType,
66        precision: u64,
67    ) -> Result<RewardDistributorRefreshNftsFromDlActionArgs, DriverError> {
68        match distributor_type {
69            RewardDistributorType::CuratedNft {
70                store_launcher_id,
71                refreshable,
72            } => {
73                if !refreshable {
74                    return Err(DriverError::Custom(
75                        "Refresh action is only available in *refreshable* curated NFT mode"
76                            .to_string(),
77                    ));
78                }
79
80                Ok(RewardDistributorRefreshNftsFromDlActionArgs::new(
81                    store_launcher_id,
82                    Self::my_p2_puzzle_hash(launcher_id),
83                    Slot::<()>::first_curry_hash(
84                        launcher_id,
85                        RewardDistributorSlotNonce::ENTRY.to_u64(),
86                    )
87                    .into(),
88                    max_second_offset,
89                    precision,
90                ))
91            }
92            _ => Err(DriverError::Custom(
93                "Refresh action is only available in curated NFT mode".to_string(),
94            )),
95        }
96    }
97
98    pub fn my_p2_puzzle_hash(launcher_id: Bytes32) -> Bytes32 {
99        P2DelegatedBySingletonLayerArgs::curry_tree_hash(
100            SingletonStruct::new(launcher_id).tree_hash().into(),
101            1,
102        )
103        .into()
104    }
105
106    fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
107        let args = Self::new_args(
108            self.launcher_id,
109            self.max_second_offset,
110            self.distributor_type,
111            self.precision,
112        )?;
113
114        ctx.curry(args)
115    }
116
117    #[allow(clippy::cast_sign_loss)]
118    pub fn get_log(
119        ctx: &mut SpendContext,
120        solution: NodePtr,
121        changes: RewardDistributorStateTransition,
122        store_launcher_id: Bytes32,
123    ) -> Result<RewardDistributorRefreshNftsFromDlActionLog, DriverError> {
124        let params = ctx.extract::<RewardDistributorRefreshNftsFromDlActionSolution>(solution)?;
125
126        let spent_entry_slots = params
127            .slots_and_nfts
128            .iter()
129            .map(|e| e.existing_slot_value)
130            .collect();
131        let created_entry_slots = params
132            .slots_and_nfts
133            .iter()
134            .map(|e| {
135                Ok(RewardDistributorEntrySlotValue {
136                    counter: e.existing_slot_value.counter + 1,
137                    payout_puzzle_hash: e.existing_slot_value.payout_puzzle_hash,
138                    initial_cumulative_payout: changes
139                        .old_state
140                        .round_reward_info
141                        .cumulative_payout,
142                    shares: u64::try_from(
143                        i128::from(e.existing_slot_value.shares)
144                            + i128::from(e.nfts_total_shares_delta),
145                    )?,
146                })
147            })
148            .collect::<Result<Vec<_>, DriverError>>()?;
149        let nft_entries = params
150            .slots_and_nfts
151            .iter()
152            .flat_map(|slot| &slot.nfts)
153            .map(|nft| RewardDistributorNftStakeEntry {
154                launcher_id: nft.nft_launcher_id,
155                shares: nft.new_nft_shares,
156            })
157            .collect();
158
159        Ok(RewardDistributorRefreshNftsFromDlActionLog {
160            spent_entry_slots,
161            created_entry_slots,
162            nft_entries,
163            dl_root_hash: params.dl_root_hash,
164            dl_inner_puzzle_hash: params.dl_info.dl_inner_puzzle_hash,
165            dl_full_puzzle_hash: SingletonArgs::curry_tree_hash(
166                store_launcher_id,
167                params.dl_info.dl_inner_puzzle_hash.into(),
168            )
169            .into(),
170            changes,
171        })
172    }
173
174    #[allow(clippy::too_many_arguments)]
175    #[allow(clippy::cast_sign_loss)]
176    pub fn spend(
177        self,
178        ctx: &mut SpendContext,
179        distributor: &mut RewardDistributor,
180        slots: Vec<Slot<RewardDistributorEntrySlotValue>>,
181        nfts: &[&[Nft]],
182        nft_shares_delta: &[&[i64]],
183        nft_new_shares: &[&[u64]],
184        nft_inclusion_proofs: &[&[MerkleProof]],
185        dl_root_hash: Bytes32,
186        dl_metadata_rest_hash: Option<Bytes32>,
187        dl_metadata_updater_hash_hash: Bytes32,
188        dl_inner_puzzle_hash: Bytes32,
189    ) -> Result<(Conditions, Vec<Nft>), DriverError> {
190        // spend existing slots, build security conds, compute NFT children
191        let mut security_conditions = Conditions::new();
192        let mut slots_and_nfts = Vec::<SlotAndNfts>::new();
193        let mut created_nfts = Vec::<Nft>::new();
194
195        let my_inner_puzzle_hash: Bytes32 = distributor.info.inner_puzzle_hash().into();
196        let my_p2_puzzle_hash = Self::my_p2_puzzle_hash(self.launcher_id);
197        let my_p2_treehash: TreeHash = my_p2_puzzle_hash.into();
198        let my_singleton_struct_hash = SingletonStruct::new(self.launcher_id).tree_hash().into();
199
200        for (i, slot) in slots.into_iter().enumerate() {
201            let slot = distributor.actual_entry_slot_value(slot);
202            let mut nft_infos = Vec::<RefreshNftInfo>::new();
203            for (j, nft) in nfts[i].iter().enumerate() {
204                // add NFT data to solution
205                nft_infos.push(RefreshNftInfo {
206                    nft_shares_delta: nft_shares_delta[i][j],
207                    new_nft_shares: nft_new_shares[i][j],
208                    nft_parent_id: nft.coin.parent_coin_info,
209                    nft_launcher_id: nft.info.launcher_id,
210                    nft_metadata_hash: nft.info.metadata.tree_hash().into(),
211                    nft_metadata_updater_hash_hash: nft
212                        .info
213                        .metadata_updater_puzzle_hash
214                        .tree_hash()
215                        .into(),
216                    nft_transfer_porgram_hash: NftRoyaltyTransferPuzzleArgs::curry_tree_hash(
217                        nft.info.launcher_id,
218                        nft.info.royalty_puzzle_hash,
219                        nft.info.royalty_basis_points,
220                    )
221                    .into(),
222                    nft_owner: nft.info.current_owner,
223                    nft_inclusion_proof: nft_inclusion_proofs[i][j].clone(),
224                });
225
226                // spend NFT
227                let new_nft_inner_puzzle_hash = CurriedProgram {
228                    program: NONCE_WRAPPER_PUZZLE_HASH,
229                    args: NonceWrapperArgs::<(Bytes32, u64), TreeHash> {
230                        nonce: clvm_tuple!(
231                            slot.info.value.payout_puzzle_hash,
232                            nft_new_shares[i][j]
233                        ),
234                        inner_puzzle: my_p2_treehash,
235                    },
236                }
237                .tree_hash()
238                .into();
239                let nft_p2 = P2DelegatedBySingletonLayer::new(my_singleton_struct_hash, 1);
240                let nft_inner_puzzle = nft_p2.construct_puzzle(ctx)?;
241                let old_nft_shares = u64::try_from(
242                    i128::from(nft_new_shares[i][j]) - i128::from(nft_shares_delta[i][j]),
243                )?;
244                let nft_nonce: (Bytes32, u64) =
245                    clvm_tuple!(slot.info.value.payout_puzzle_hash, old_nft_shares);
246                let nft_inner_puzzle = ctx.curry(NonceWrapperArgs::<(Bytes32, u64), NodePtr> {
247                    nonce: nft_nonce,
248                    inner_puzzle: nft_inner_puzzle,
249                })?;
250
251                let hint = ctx.hint(
252                    (slot.info.value.payout_puzzle_hash, my_p2_puzzle_hash)
253                        .tree_hash()
254                        .into(),
255                )?;
256                let delegated_puzzle = ctx.alloc(&clvm_quote!(Conditions::new().create_coin(
257                    new_nft_inner_puzzle_hash,
258                    1,
259                    hint,
260                )))?;
261                let nft_inner_solution = nft_p2.construct_solution(
262                    ctx,
263                    P2DelegatedBySingletonLayerSolution::<NodePtr, NodePtr> {
264                        singleton_inner_puzzle_hash: my_inner_puzzle_hash,
265                        delegated_puzzle,
266                        delegated_solution: NodePtr::NIL,
267                    },
268                )?;
269
270                created_nfts
271                    .push(nft.spend(ctx, Spend::new(nft_inner_puzzle, nft_inner_solution))?);
272
273                // compute security condition for this NFT
274                security_conditions =
275                    security_conditions.assert_puzzle_announcement(announcement_id(
276                        distributor.coin.puzzle_hash,
277                        RewardDistributorCreatedAnnouncementPrefix::refresh(nft.info.launcher_id),
278                    ));
279            }
280
281            let payout_amount_precision = u128::from(slot.info.value.shares)
282                * (distributor
283                    .pending_spend
284                    .latest_state
285                    .1
286                    .round_reward_info
287                    .cumulative_payout
288                    - slot.info.value.initial_cumulative_payout);
289            let entry_payout_amount =
290                u64::try_from(payout_amount_precision / u128::from(self.precision))?;
291            let payout_rounding_error = payout_amount_precision % u128::from(self.precision);
292            slots_and_nfts.push(SlotAndNfts {
293                existing_slot_value: slot.info.value,
294                entry_payout_info: RewardDistributorEntryPayoutInfo {
295                    payout_amount: entry_payout_amount,
296                    payout_rounding_error,
297                },
298                nfts_total_shares_delta: nft_infos.iter().map(|e| e.nft_shares_delta).sum(),
299                nfts: nft_infos,
300            });
301            slot.spend(ctx, my_inner_puzzle_hash)?;
302        }
303
304        // spend self
305        let action_solution = ctx.alloc(&RewardDistributorRefreshNftsFromDlActionSolution {
306            dl_root_hash,
307            dl_info: RewardDistributorDlInfo {
308                dl_metadata_rest_hash,
309                dl_metadata_updater_hash_hash,
310                dl_inner_puzzle_hash,
311            },
312            totals: RewardDistributorRefreshNftsTotals {
313                total_entry_payout_amount: slots_and_nfts
314                    .iter()
315                    .map(|e| e.entry_payout_info.payout_amount)
316                    .sum(),
317                total_shares_delta: i128::from(
318                    slots_and_nfts
319                        .iter()
320                        .map(|e| e.nfts_total_shares_delta)
321                        .sum::<i64>(),
322                ),
323                total_payout_rounding_error: slots_and_nfts
324                    .iter()
325                    .map(|e| e.entry_payout_info.payout_rounding_error)
326                    .sum(),
327            },
328            slots_and_nfts,
329        })?;
330        let action_puzzle = self.construct_puzzle(ctx)?;
331
332        distributor.insert_action_spend(ctx, Spend::new(action_puzzle, action_solution))?;
333
334        Ok((security_conditions, created_nfts))
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use chia_sdk_types::puzzles::{
341        RewardDistributorDlInfo, RewardDistributorEntryPayoutInfo,
342        RewardDistributorRefreshNftsFromDlActionSolution, RewardDistributorRefreshNftsTotals,
343    };
344
345    use super::*;
346    use crate::{
347        RewardDistributorState, RewardDistributorStateTransition, RoundRewardInfo, RoundTimeInfo,
348    };
349
350    fn id(byte: u8) -> Bytes32 {
351        Bytes32::new([byte; 32])
352    }
353
354    fn nft(launcher_id: Bytes32, shares_delta: i64, new_shares: u64) -> RefreshNftInfo {
355        RefreshNftInfo {
356            nft_shares_delta: shares_delta,
357            new_nft_shares: new_shares,
358            nft_parent_id: Bytes32::default(),
359            nft_launcher_id: launcher_id,
360            nft_metadata_hash: Bytes32::default(),
361            nft_metadata_updater_hash_hash: Bytes32::default(),
362            nft_transfer_porgram_hash: Bytes32::default(),
363            nft_owner: None,
364            nft_inclusion_proof: MerkleProof::new(0, vec![]),
365        }
366    }
367
368    fn slot(shares: u64, shares_delta: i64, nfts: Vec<RefreshNftInfo>) -> SlotAndNfts {
369        SlotAndNfts {
370            existing_slot_value: RewardDistributorEntrySlotValue {
371                counter: 0,
372                payout_puzzle_hash: Bytes32::default(),
373                initial_cumulative_payout: 0,
374                shares,
375            },
376            entry_payout_info: RewardDistributorEntryPayoutInfo {
377                payout_amount: 0,
378                payout_rounding_error: 0,
379            },
380            nfts_total_shares_delta: shares_delta,
381            nfts,
382        }
383    }
384
385    #[test]
386    fn refresh_log_maps_each_nft_to_its_new_shares_across_slot_groups() {
387        let first = id(1);
388        let second = id(2);
389        let third = id(3);
390        let mut ctx = SpendContext::new();
391        let solution = ctx
392            .alloc(&RewardDistributorRefreshNftsFromDlActionSolution {
393                dl_root_hash: id(4),
394                dl_info: RewardDistributorDlInfo {
395                    dl_metadata_rest_hash: None,
396                    dl_metadata_updater_hash_hash: id(5),
397                    dl_inner_puzzle_hash: id(6),
398                },
399                totals: RewardDistributorRefreshNftsTotals {
400                    total_entry_payout_amount: 0,
401                    total_shares_delta: 1,
402                    total_payout_rounding_error: 0,
403                },
404                slots_and_nfts: vec![
405                    slot(12, -2, vec![nft(first, -4, 0), nft(second, 2, 2)]),
406                    slot(7, 3, vec![nft(third, 3, 10)]),
407                ],
408            })
409            .unwrap();
410        let state = RewardDistributorState {
411            total_reserves: 0,
412            active_shares: 19,
413            round_reward_info: RoundRewardInfo {
414                cumulative_payout: 100,
415                remaining_rewards: 0,
416            },
417            round_time_info: RoundTimeInfo {
418                last_update: 0,
419                epoch_end: 0,
420            },
421        };
422
423        let log = RewardDistributorRefreshAction::get_log(
424            &mut ctx,
425            solution,
426            RewardDistributorStateTransition {
427                old_state: state,
428                new_state: RewardDistributorState {
429                    active_shares: 20,
430                    ..state
431                },
432            },
433            id(9),
434        )
435        .unwrap();
436
437        assert_eq!(
438            log.nft_entries,
439            vec![
440                RewardDistributorNftStakeEntry {
441                    launcher_id: first,
442                    shares: 0,
443                },
444                RewardDistributorNftStakeEntry {
445                    launcher_id: second,
446                    shares: 2,
447                },
448                RewardDistributorNftStakeEntry {
449                    launcher_id: third,
450                    shares: 10,
451                },
452            ]
453        );
454    }
455}