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