chia_sdk_driver/layers/action_layer/actions/xchandles/
register.rs

1use chia_protocol::Bytes32;
2use chia_puzzle_types::singleton::SingletonStruct;
3use chia_sdk_types::{
4    Conditions, Mod, announcement_id,
5    puzzles::{
6        DefaultCatMakerArgs, PrecommitSpendMode, SlotNeigborsInfo, XchandlesDataValue,
7        XchandlesFactorPricingPuzzleArgs, XchandlesPricingSolution, XchandlesRegisterActionArgs,
8        XchandlesRegisterActionSolution, XchandlesSlotValue,
9    },
10};
11use clvm_traits::{FromClvm, ToClvm};
12use clvm_utils::{ToTreeHash, TreeHash};
13use clvmr::NodePtr;
14
15use crate::{
16    DriverError, PrecommitCoin, PrecommitLayer, SingletonAction, Slot, Spend, SpendContext,
17    XchandlesConstants, XchandlesPrecommitValue, XchandlesRegistry,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct XchandlesRegisterAction {
22    pub launcher_id: Bytes32,
23    pub relative_block_height: u32,
24    pub payout_puzzle_hash: Bytes32,
25}
26
27impl ToTreeHash for XchandlesRegisterAction {
28    fn tree_hash(&self) -> TreeHash {
29        Self::new_args(
30            self.launcher_id,
31            self.relative_block_height,
32            self.payout_puzzle_hash,
33        )
34        .curry_tree_hash()
35    }
36}
37
38impl SingletonAction<XchandlesRegistry> for XchandlesRegisterAction {
39    fn from_constants(constants: &XchandlesConstants) -> Self {
40        Self {
41            launcher_id: constants.launcher_id,
42            relative_block_height: constants.relative_block_height,
43            payout_puzzle_hash: constants.precommit_payout_puzzle_hash,
44        }
45    }
46}
47
48impl XchandlesRegisterAction {
49    pub fn new_args(
50        launcher_id: Bytes32,
51        relative_block_height: u32,
52        payout_puzzle_hash: Bytes32,
53    ) -> XchandlesRegisterActionArgs {
54        XchandlesRegisterActionArgs {
55            precommit_1st_curry_hash: PrecommitLayer::<()>::first_curry_hash(
56                SingletonStruct::new(launcher_id).tree_hash().into(),
57                relative_block_height,
58                payout_puzzle_hash,
59            )
60            .into(),
61            slot_1st_curry_hash: Slot::<()>::first_curry_hash(launcher_id, 0).into(),
62        }
63    }
64
65    fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
66        ctx.curry(Self::new_args(
67            self.launcher_id,
68            self.relative_block_height,
69            self.payout_puzzle_hash,
70        ))
71    }
72
73    pub fn spent_slot_values(
74        ctx: &SpendContext,
75        solution: NodePtr,
76    ) -> Result<[XchandlesSlotValue; 2], DriverError> {
77        let solution = XchandlesRegisterActionSolution::<
78            NodePtr,
79            NodePtr,
80            NodePtr,
81            NodePtr,
82            NodePtr,
83        >::from_clvm(ctx, solution)?;
84
85        Ok([
86            XchandlesSlotValue::new(
87                solution.neighbors.left_value,
88                solution.left_left_value,
89                solution.neighbors.right_value,
90                solution.left_expiration,
91                solution.left_data.owner_launcher_id,
92                solution.left_data.resolved_data,
93            ),
94            XchandlesSlotValue::new(
95                solution.neighbors.right_value,
96                solution.neighbors.left_value,
97                solution.right_right_value,
98                solution.right_expiration,
99                solution.right_data.owner_launcher_id,
100                solution.right_data.resolved_data,
101            ),
102        ])
103    }
104
105    pub fn created_slot_values(
106        ctx: &mut SpendContext,
107        solution: NodePtr,
108    ) -> Result<[XchandlesSlotValue; 3], DriverError> {
109        let solution = XchandlesRegisterActionSolution::<
110            NodePtr,
111            NodePtr,
112            NodePtr,
113            NodePtr,
114            NodePtr,
115        >::from_clvm(ctx, solution)?;
116
117        let pricing_output = ctx.run(
118            solution.pricing_puzzle_reveal,
119            solution.pricing_puzzle_solution,
120        )?;
121        let registration_time_delta = <(NodePtr, u64)>::from_clvm(ctx, pricing_output)?.1;
122
123        let (start_time, _) = ctx.extract::<(u64, NodePtr)>(solution.pricing_puzzle_solution)?;
124
125        Ok([
126            XchandlesSlotValue::new(
127                solution.neighbors.left_value,
128                solution.left_left_value,
129                solution.handle_hash,
130                solution.left_expiration,
131                solution.left_data.owner_launcher_id,
132                solution.left_data.resolved_data,
133            ),
134            XchandlesSlotValue::new(
135                solution.handle_hash,
136                solution.neighbors.left_value,
137                solution.neighbors.right_value,
138                start_time + registration_time_delta,
139                solution.data.owner_launcher_id,
140                solution.data.resolved_data,
141            ),
142            XchandlesSlotValue::new(
143                solution.neighbors.right_value,
144                solution.handle_hash,
145                solution.right_right_value,
146                solution.right_expiration,
147                solution.right_data.owner_launcher_id,
148                solution.right_data.resolved_data,
149            ),
150        ])
151    }
152
153    #[allow(clippy::too_many_arguments)]
154    pub fn spend(
155        self,
156        ctx: &mut SpendContext,
157        registry: &mut XchandlesRegistry,
158        left_slot: Slot<XchandlesSlotValue>,
159        right_slot: Slot<XchandlesSlotValue>,
160        precommit_coin: PrecommitCoin<XchandlesPrecommitValue>,
161        base_handle_price: u64,
162        registration_period: u64,
163        start_time: u64,
164    ) -> Result<Conditions, DriverError> {
165        let handle = precommit_coin.value.handle.clone();
166        let handle_hash = handle.tree_hash().into();
167        let (left_slot, right_slot) = registry.actual_neigbors(handle_hash, left_slot, right_slot);
168
169        let secret = precommit_coin.value.secret;
170
171        let num_periods = precommit_coin.coin.amount
172            / XchandlesFactorPricingPuzzleArgs::get_price(base_handle_price, &handle, 1);
173
174        // calculate announcement
175        let mut register_announcement = precommit_coin.coin.puzzle_hash.to_vec();
176        register_announcement.insert(0, b'r');
177
178        // spend precommit coin
179        let my_inner_puzzle_hash = registry.info.inner_puzzle_hash().into();
180        precommit_coin.spend(ctx, PrecommitSpendMode::REGISTER, my_inner_puzzle_hash)?;
181
182        // spend self
183        let action_solution = XchandlesRegisterActionSolution {
184            handle_hash,
185            pricing_puzzle_reveal: ctx.curry(XchandlesFactorPricingPuzzleArgs {
186                base_price: base_handle_price,
187                registration_period,
188            })?,
189            pricing_puzzle_solution: XchandlesPricingSolution {
190                buy_time: start_time,
191                current_expiration: 0,
192                handle: handle.clone(),
193                num_periods,
194            },
195            cat_maker_reveal: ctx.curry(DefaultCatMakerArgs::new(
196                precommit_coin.asset_id.tree_hash().into(),
197            ))?,
198            cat_maker_solution: (),
199            neighbors: SlotNeigborsInfo {
200                left_value: left_slot.info.value.handle_hash,
201                right_value: right_slot.info.value.handle_hash,
202            },
203            left_left_value: left_slot.info.value.neighbors.left_value,
204            left_expiration: left_slot.info.value.expiration,
205            left_data: left_slot.info.value.rest_data(),
206            right_right_value: right_slot.info.value.neighbors.right_value,
207            right_expiration: right_slot.info.value.expiration,
208            right_data: right_slot.info.value.rest_data(),
209            data: XchandlesDataValue {
210                owner_launcher_id: precommit_coin.value.owner_launcher_id,
211                resolved_data: precommit_coin.value.resolved_data,
212            },
213            refund_puzzle_hash_hash: precommit_coin.refund_puzzle_hash.tree_hash().into(),
214            secret,
215        }
216        .to_clvm(ctx)?;
217        let action_puzzle = self.construct_puzzle(ctx)?;
218
219        registry.insert_action_spend(ctx, Spend::new(action_puzzle, action_solution))?;
220
221        // spend slots
222        left_slot.spend(ctx, my_inner_puzzle_hash)?;
223        right_slot.spend(ctx, my_inner_puzzle_hash)?;
224
225        Ok(
226            Conditions::new().assert_puzzle_announcement(announcement_id(
227                registry.coin.puzzle_hash,
228                register_announcement,
229            )),
230        )
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use clvmr::error::EvalErr;
237
238    use super::*;
239
240    #[derive(FromClvm, ToClvm, Debug, Clone, PartialEq, Eq)]
241    #[clvm(list)]
242    struct XchandlesFactorPricingOutput {
243        pub price: u64,
244        #[clvm(rest)]
245        pub registered_time: u64,
246    }
247
248    #[test]
249    fn test_factor_pricing_puzzle() -> Result<(), DriverError> {
250        let mut ctx = SpendContext::new();
251        let base_price = 1; // puzzle will only spit out factors
252        let registration_period = 366 * 24 * 60 * 60; // one year
253
254        let puzzle = ctx.curry(XchandlesFactorPricingPuzzleArgs {
255            base_price,
256            registration_period,
257        })?;
258
259        for handle_length in 3..=31 {
260            for num_periods in 1..=3 {
261                for has_number in [false, true] {
262                    let handle = if has_number {
263                        "a".repeat(handle_length - 1) + "1"
264                    } else {
265                        "a".repeat(handle_length)
266                    };
267
268                    let solution = ctx.alloc(&XchandlesPricingSolution {
269                        buy_time: 0,
270                        current_expiration: (handle_length - 3) as u64, // shouldn't matter
271                        handle,
272                        num_periods,
273                    })?;
274
275                    let output = ctx.run(puzzle, solution)?;
276                    let output = ctx.extract::<XchandlesFactorPricingOutput>(output)?;
277
278                    let mut expected_price = if handle_length == 3 {
279                        128
280                    } else if handle_length == 4 {
281                        64
282                    } else if handle_length == 5 {
283                        16
284                    } else {
285                        2
286                    };
287                    if has_number {
288                        expected_price /= 2;
289                    }
290                    expected_price *= num_periods;
291
292                    assert_eq!(output.price, expected_price);
293                    assert_eq!(output.registered_time, num_periods * registration_period);
294                }
295            }
296        }
297
298        // make sure the puzzle won't let us register a handle of length 2
299
300        let solution = ctx.alloc(&XchandlesPricingSolution {
301            buy_time: 0,
302            current_expiration: 0,
303            handle: "aa".to_string(),
304            num_periods: 1,
305        })?;
306
307        let Err(DriverError::Eval(EvalErr::Raise(_))) = ctx.run(puzzle, solution) else {
308            panic!("Expected clvm raise");
309        };
310
311        // make sure the puzzle won't let us register a handle of length 32
312
313        let solution = ctx.alloc(&XchandlesPricingSolution {
314            buy_time: 0,
315            current_expiration: 0,
316            handle: "a".repeat(32),
317            num_periods: 1,
318        })?;
319
320        let Err(DriverError::Eval(EvalErr::Raise(_))) = ctx.run(puzzle, solution) else {
321            panic!("Expected clvm raise");
322        };
323
324        // make sure the puzzle won't let us register a handle with invalid characters
325
326        let solution = ctx.alloc(&XchandlesPricingSolution {
327            buy_time: 0,
328            current_expiration: 0,
329            handle: "yak@test".to_string(),
330            num_periods: 1,
331        })?;
332
333        let Err(DriverError::Eval(EvalErr::Raise(_))) = ctx.run(puzzle, solution) else {
334            panic!("Expected clvm raise");
335        };
336
337        Ok(())
338    }
339}