Skip to main content

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

1use chia_protocol::Bytes32;
2use chia_puzzle_types::singleton::{SingletonArgs, SingletonStruct};
3use chia_puzzles::{SINGLETON_LAUNCHER_HASH, SINGLETON_TOP_LAYER_V1_1_HASH};
4use chia_sdk_types::{
5    Conditions, Mod, announcement_id,
6    puzzles::{
7        DefaultCatMakerArgs, PrecommitSpendMode, PuzzleAndSolution, SlotNeigborsInfo,
8        XchandlesFactorPricingPuzzleArgs, XchandlesHandleSlotValue, XchandlesNewDataPuzzleHashes,
9        XchandlesOtherPrecommitData, XchandlesPricingSolution, XchandlesRegisterActionArgs,
10        XchandlesRegisterActionSolution, XchandlesRestOfSlot, XchandlesSlotNonce,
11    },
12};
13use clvm_traits::ToClvm;
14use clvm_utils::{ToTreeHash, TreeHash};
15use clvmr::NodePtr;
16
17use crate::{
18    Asset, DriverError, PrecommitCoin, PrecommitLayer, SingletonAction, Slot, Spend, SpendContext,
19    XchandlesConstants, XchandlesPrecommitValue, XchandlesRegistry,
20    XchandlesRegistryCreatedAnnouncementPrefix, XchandlesRegistryReceivedMessagePrefix,
21};
22
23use super::{
24    XchandlesExpirePricingPuzzle, XchandlesPrecommitValueLog, XchandlesRegisterActionLog,
25    run_pricing_output,
26};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct XchandlesRegisterAction {
30    pub launcher_id: Bytes32,
31    pub relative_block_height: u32,
32    pub payout_puzzle_hash: Bytes32,
33}
34
35impl ToTreeHash for XchandlesRegisterAction {
36    fn tree_hash(&self) -> TreeHash {
37        Self::new_args(
38            self.launcher_id,
39            self.relative_block_height,
40            self.payout_puzzle_hash,
41        )
42        .curry_tree_hash()
43    }
44}
45
46impl SingletonAction<XchandlesRegistry> for XchandlesRegisterAction {
47    fn from_constants(constants: &XchandlesConstants) -> Self {
48        Self {
49            launcher_id: constants.launcher_id,
50            relative_block_height: constants.relative_block_height,
51            payout_puzzle_hash: constants.precommit_payout_puzzle_hash,
52        }
53    }
54}
55
56impl XchandlesRegisterAction {
57    pub fn new_args(
58        launcher_id: Bytes32,
59        relative_block_height: u32,
60        payout_puzzle_hash: Bytes32,
61    ) -> XchandlesRegisterActionArgs {
62        XchandlesRegisterActionArgs {
63            singleton_mod_hash: SINGLETON_TOP_LAYER_V1_1_HASH.into(),
64            singleton_launcher_puzzle_hash: SINGLETON_LAUNCHER_HASH.into(),
65            precommit_1st_curry_hash: PrecommitLayer::<()>::first_curry_hash(
66                SingletonStruct::new(launcher_id).tree_hash().into(),
67                relative_block_height,
68                payout_puzzle_hash,
69            )
70            .into(),
71            handle_slot_1st_curry_hash: Slot::<()>::first_curry_hash(
72                launcher_id,
73                XchandlesSlotNonce::HANDLE.to_u64(),
74            )
75            .into(),
76        }
77    }
78
79    fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
80        ctx.curry(Self::new_args(
81            self.launcher_id,
82            self.relative_block_height,
83            self.payout_puzzle_hash,
84        ))
85    }
86
87    pub fn get_log(
88        ctx: &mut SpendContext,
89        solution: NodePtr,
90    ) -> Result<XchandlesRegisterActionLog, DriverError> {
91        let solution = ctx.extract::<XchandlesRegisterActionSolution<
92            NodePtr,
93            NodePtr,
94            NodePtr,
95            NodePtr,
96            Bytes32,
97        >>(solution)?;
98
99        let spent_left_slot = XchandlesHandleSlotValue::new(
100            solution.left_rest_of_slot.this_counter,
101            solution.neighbors.left_value,
102            solution.left_rest_of_slot.this_this_value,
103            solution.neighbors.right_value,
104            solution.left_rest_of_slot.this_expiration,
105            solution.left_rest_of_slot.this_data.owner_launcher_id,
106            solution.left_rest_of_slot.this_data.resolved_launcher_id,
107        );
108        let spent_right_slot = XchandlesHandleSlotValue::new(
109            solution.right_rest_of_slot.this_counter,
110            solution.neighbors.right_value,
111            solution.neighbors.left_value,
112            solution.right_rest_of_slot.this_this_value,
113            solution.right_rest_of_slot.this_expiration,
114            solution.right_rest_of_slot.this_data.owner_launcher_id,
115            solution.right_rest_of_slot.this_data.resolved_launcher_id,
116        );
117
118        let pricing_solution =
119            ctx.extract::<XchandlesPricingSolution>(solution.pricing_puzzle_and_solution.solution)?;
120
121        let (total_price, registered_time) = run_pricing_output(
122            ctx,
123            solution.pricing_puzzle_and_solution.puzzle,
124            solution.pricing_puzzle_and_solution.solution,
125        )?;
126
127        let created_left_slot = XchandlesHandleSlotValue::new(
128            solution.left_rest_of_slot.this_counter + 1,
129            solution.neighbors.left_value,
130            solution.left_rest_of_slot.this_this_value,
131            solution.handle_hash,
132            solution.left_rest_of_slot.this_expiration,
133            solution.left_rest_of_slot.this_data.owner_launcher_id,
134            solution.left_rest_of_slot.this_data.resolved_launcher_id,
135        );
136        let created_handle_slot = XchandlesHandleSlotValue::new(
137            0,
138            solution.handle_hash,
139            solution.neighbors.left_value,
140            solution.neighbors.right_value,
141            pricing_solution.buy_time + registered_time,
142            solution.other_precommit_data.launcher_ids.owner_launcher_id,
143            solution
144                .other_precommit_data
145                .launcher_ids
146                .resolved_launcher_id,
147        );
148        let created_right_slot = XchandlesHandleSlotValue::new(
149            solution.right_rest_of_slot.this_counter + 1,
150            solution.neighbors.right_value,
151            solution.handle_hash,
152            solution.right_rest_of_slot.this_this_value,
153            solution.right_rest_of_slot.this_expiration,
154            solution.right_rest_of_slot.this_data.owner_launcher_id,
155            solution.right_rest_of_slot.this_data.resolved_launcher_id,
156        );
157
158        let handle = pricing_solution.handle.clone();
159        let cat_maker_puzzle_hash: Bytes32 = ctx
160            .tree_hash(solution.cat_maker_puzzle_and_solution.puzzle)
161            .into();
162        let pricing_puzzle_hash: Bytes32 = ctx
163            .tree_hash(solution.pricing_puzzle_and_solution.puzzle)
164            .into();
165        let precommit_value = XchandlesPrecommitValueLog::new(
166            cat_maker_puzzle_hash,
167            pricing_puzzle_hash,
168            pricing_solution,
169            handle,
170            solution.other_precommit_data.refund_and_secret.secret,
171            solution.other_precommit_data.launcher_ids.owner_launcher_id,
172            solution
173                .other_precommit_data
174                .launcher_ids
175                .resolved_launcher_id,
176        );
177
178        let owner_full_puzzle_hash = SingletonArgs::curry_tree_hash(
179            solution.other_precommit_data.launcher_ids.owner_launcher_id,
180            solution
181                .data_puzzle_hashes
182                .new_owner_inner_puzzle_hash
183                .into(),
184        )
185        .into();
186
187        let resolved_full_puzzle_hash =
188            if solution.other_precommit_data.launcher_ids.owner_launcher_id
189                == solution
190                    .other_precommit_data
191                    .launcher_ids
192                    .resolved_launcher_id
193            {
194                None
195            } else {
196                Some(
197                    SingletonArgs::curry_tree_hash(
198                        solution
199                            .other_precommit_data
200                            .launcher_ids
201                            .resolved_launcher_id,
202                        solution
203                            .data_puzzle_hashes
204                            .new_resolved_inner_puzzle_hash
205                            .into(),
206                    )
207                    .into(),
208                )
209            };
210
211        Ok(XchandlesRegisterActionLog {
212            spent_left_slot,
213            spent_right_slot,
214            created_left_slot,
215            created_handle_slot,
216            created_right_slot,
217            precommit_value,
218            total_price,
219            registered_time,
220            owner_full_puzzle_hash,
221            resolved_full_puzzle_hash,
222            owner_inner_puzzle_hash: solution.data_puzzle_hashes.new_owner_inner_puzzle_hash,
223            resolved_inner_puzzle_hash: solution.data_puzzle_hashes.new_resolved_inner_puzzle_hash,
224        })
225    }
226
227    /// Historical factor-pricing register path. Prefer
228    /// [`Self::spend_with_pricing`] when the precommit committed the deployed
229    /// expiry-pricing puzzle.
230    #[allow(clippy::too_many_arguments)]
231    pub fn spend(
232        self,
233        ctx: &mut SpendContext,
234        registry: &mut XchandlesRegistry,
235        left_slot: Slot<XchandlesHandleSlotValue>,
236        right_slot: Slot<XchandlesHandleSlotValue>,
237        precommit_coin: &PrecommitCoin<XchandlesPrecommitValue>,
238        base_handle_price: u64,
239        registration_period: u64,
240        start_time: u64,
241        owner_inner_puzzle_hash: Bytes32,
242        resolved_inner_puzzle_hash: Bytes32,
243    ) -> Result<(Conditions, Conditions, Option<Conditions>), DriverError> {
244        let handle = precommit_coin.value.handle.clone();
245        let num_periods = precommit_coin.coin.amount()
246            / XchandlesFactorPricingPuzzleArgs::get_price(base_handle_price, &handle, 1);
247        let pricing_puzzle = ctx.curry(XchandlesFactorPricingPuzzleArgs {
248            base_price: base_handle_price,
249            registration_period,
250        })?;
251        let pricing_solution = XchandlesPricingSolution {
252            buy_time: start_time,
253            current_expiration: 0,
254            handle,
255            num_periods,
256        };
257        self.spend_with_pricing(
258            ctx,
259            registry,
260            left_slot,
261            right_slot,
262            precommit_coin,
263            pricing_puzzle,
264            pricing_solution,
265            owner_inner_puzzle_hash,
266            resolved_inner_puzzle_hash,
267        )
268    }
269
270    /// Register using an explicit pricing puzzle reveal and solution.
271    ///
272    /// Used for the deployed expiry-pricing precommit (ordinary
273    /// `current_expiration = 0`) so the reveal matches the commitment instead
274    /// of silently substituting factor pricing.
275    #[allow(clippy::too_many_arguments)]
276    pub fn spend_with_pricing(
277        self,
278        ctx: &mut SpendContext,
279        registry: &mut XchandlesRegistry,
280        left_slot: Slot<XchandlesHandleSlotValue>,
281        right_slot: Slot<XchandlesHandleSlotValue>,
282        precommit_coin: &PrecommitCoin<XchandlesPrecommitValue>,
283        pricing_puzzle: NodePtr,
284        pricing_solution: XchandlesPricingSolution,
285        owner_inner_puzzle_hash: Bytes32,
286        resolved_inner_puzzle_hash: Bytes32,
287    ) -> Result<(Conditions, Conditions, Option<Conditions>), DriverError> {
288        let handle_hash = pricing_solution.handle.tree_hash().into();
289        let (left_slot, right_slot) = registry.actual_neigbors(handle_hash, left_slot, right_slot);
290
291        let secret = precommit_coin.value.secret;
292
293        // calculate announcement
294        let register_announcement =
295            XchandlesRegistryCreatedAnnouncementPrefix::register(precommit_coin.coin.puzzle_hash);
296        let new_owner_message =
297            XchandlesRegistryReceivedMessagePrefix::register_owner(precommit_coin.coin.puzzle_hash);
298        let new_resolved_message = if precommit_coin.value.resolved_launcher_id
299            == precommit_coin.value.owner_launcher_id
300        {
301            None
302        } else {
303            Some(XchandlesRegistryReceivedMessagePrefix::register_resolved(
304                precommit_coin.coin.puzzle_hash,
305            ))
306        };
307
308        // spend precommit coin
309        let my_inner_puzzle_hash = registry.info.inner_puzzle_hash().into();
310        precommit_coin.spend(ctx, PrecommitSpendMode::REGISTER, my_inner_puzzle_hash)?;
311
312        // spend self
313        let action_solution = XchandlesRegisterActionSolution {
314            handle_hash,
315            neighbors: SlotNeigborsInfo {
316                left_value: left_slot.info.value.handle_hash,
317                right_value: right_slot.info.value.handle_hash,
318            },
319            cat_maker_puzzle_and_solution: PuzzleAndSolution::new(
320                ctx.curry(DefaultCatMakerArgs::new(
321                    precommit_coin.asset_id.tree_hash().into(),
322                ))?,
323                (),
324            ),
325            pricing_puzzle_and_solution: PuzzleAndSolution::new(pricing_puzzle, pricing_solution),
326            left_rest_of_slot: XchandlesRestOfSlot::new(
327                left_slot.info.value.counter,
328                left_slot.info.value.neighbors.left_value,
329                left_slot.info.value.expiration,
330                left_slot.info.value.rest_data(),
331            ),
332            right_rest_of_slot: XchandlesRestOfSlot::new(
333                right_slot.info.value.counter,
334                right_slot.info.value.neighbors.right_value,
335                right_slot.info.value.expiration,
336                right_slot.info.value.rest_data(),
337            ),
338            data_puzzle_hashes: XchandlesNewDataPuzzleHashes::new(
339                owner_inner_puzzle_hash,
340                resolved_inner_puzzle_hash,
341            ),
342            other_precommit_data: XchandlesOtherPrecommitData::new(
343                precommit_coin.value.owner_launcher_id,
344                precommit_coin.value.resolved_launcher_id,
345                precommit_coin.refund_puzzle_hash.tree_hash().into(),
346                secret,
347            ),
348        }
349        .to_clvm(ctx)?;
350        let action_puzzle = self.construct_puzzle(ctx)?;
351
352        registry.insert_action_spend(ctx, Spend::new(action_puzzle, action_solution))?;
353
354        // spend slots
355        left_slot.spend(ctx, my_inner_puzzle_hash)?;
356        right_slot.spend(ctx, my_inner_puzzle_hash)?;
357
358        let message_destination = ctx.alloc(&registry.coin.puzzle_hash)?;
359        Ok((
360            Conditions::new().assert_puzzle_announcement(announcement_id(
361                registry.coin.puzzle_hash,
362                register_announcement,
363            )),
364            Conditions::new().send_message(18, new_owner_message.into(), vec![message_destination]),
365            new_resolved_message.map(|message| {
366                Conditions::new().send_message(18, message.into(), vec![message_destination])
367            }),
368        ))
369    }
370
371    /// Curry the deployed expiry-pricing puzzle for an ordinary-register reveal.
372    pub fn expiry_pricing_puzzle(
373        ctx: &mut SpendContext,
374        base_handle_price: u64,
375        registration_period: u64,
376    ) -> Result<NodePtr, DriverError> {
377        let args =
378            XchandlesExpirePricingPuzzle::from_info(ctx, base_handle_price, registration_period)?;
379        ctx.curry(args)
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use clvm_traits::FromClvm;
386    use clvmr::error::EvalErr;
387
388    use super::*;
389
390    #[derive(FromClvm, ToClvm, Debug, Clone, PartialEq, Eq)]
391    #[clvm(list)]
392    struct XchandlesFactorPricingOutput {
393        pub price: u64,
394        #[clvm(rest)]
395        pub registered_time: u64,
396    }
397
398    #[test]
399    fn test_factor_pricing_puzzle() -> Result<(), DriverError> {
400        const LONG_PREMINE_HANDLES: &[&str] = &[
401            "ashorttermmindgetsinthewayofalongtermgrind",
402            "bigbouncingthicctwerkingthunderclappingbadonkabooty",
403            "bigfathonkingjigglymommymilkerboobies",
404            "rolexislandpermutoplatinumlamboempirexrp404inu",
405            "thankstopawketforprovidingthebestchiasdk",
406        ];
407
408        let mut ctx = SpendContext::new();
409        let base_price = 1; // puzzle will only spit out factors
410        let registration_period = 366 * 24 * 60 * 60; // one year
411
412        let puzzle = ctx.curry(XchandlesFactorPricingPuzzleArgs {
413            base_price,
414            registration_period,
415        })?;
416
417        for handle_length in 3..=63 {
418            for num_periods in 1..=3 {
419                for has_number in [false, true] {
420                    let handle = if has_number {
421                        "a".repeat(handle_length - 1) + "1"
422                    } else {
423                        "a".repeat(handle_length)
424                    };
425
426                    let solution = ctx.alloc(&XchandlesPricingSolution {
427                        buy_time: 0,
428                        current_expiration: (handle_length - 3) as u64, // shouldn't matter
429                        handle: handle.clone(),
430                        num_periods,
431                    })?;
432
433                    let output = ctx.run(puzzle, solution)?;
434                    let output = ctx.extract::<XchandlesFactorPricingOutput>(output)?;
435
436                    let expected_price = XchandlesFactorPricingPuzzleArgs::get_price(
437                        base_price,
438                        &handle,
439                        num_periods,
440                    );
441
442                    assert_eq!(output.price, expected_price);
443                    assert_eq!(output.registered_time, num_periods * registration_period);
444                }
445            }
446        }
447
448        // Reject lengths 0, 1, 2, and 64+.
449        for handle in ["", "a", "aa", &*"a".repeat(64)] {
450            let solution = ctx.alloc(&XchandlesPricingSolution {
451                buy_time: 0,
452                current_expiration: 0,
453                handle: handle.to_string(),
454                num_periods: 1,
455            })?;
456
457            let Err(DriverError::Eval(EvalErr::Raise(_))) = ctx.run(puzzle, solution) else {
458                panic!("Expected clvm raise for handle {handle:?}");
459            };
460        }
461
462        // Reject invalid characters (uppercase, punctuation, whitespace, non-ASCII).
463        for handle in ["ABC", "yak@test", "foo bar", "café", "a.b", "a-b"] {
464            let solution = ctx.alloc(&XchandlesPricingSolution {
465                buy_time: 0,
466                current_expiration: 0,
467                handle: handle.to_string(),
468                num_periods: 1,
469            })?;
470
471            let Err(DriverError::Eval(EvalErr::Raise(_))) = ctx.run(puzzle, solution) else {
472                panic!("Expected clvm raise for handle {handle:?}");
473            };
474        }
475
476        // Published Premine handles longer than 31 must price through the executable path.
477        for handle in LONG_PREMINE_HANDLES {
478            assert!(
479                XchandlesFactorPricingPuzzleArgs::is_valid_handle(handle),
480                "premine handle failed launch validation: {handle}"
481            );
482            let solution = ctx.alloc(&XchandlesPricingSolution {
483                buy_time: 0,
484                current_expiration: 0,
485                handle: (*handle).to_string(),
486                num_periods: 1,
487            })?;
488            let output = ctx.run(puzzle, solution)?;
489            let output = ctx.extract::<XchandlesFactorPricingOutput>(output)?;
490            assert_eq!(
491                output.price,
492                XchandlesFactorPricingPuzzleArgs::get_price(base_price, handle, 1)
493            );
494            assert_eq!(output.registered_time, registration_period);
495        }
496
497        Ok(())
498    }
499
500    #[test]
501    fn expiry_pricing_register_reveal_matches_deployed_expire_hash() -> Result<(), DriverError> {
502        let mut ctx = SpendContext::new();
503        let base_price = 5_000;
504        let registration_period = 31_557_600;
505        let puzzle = XchandlesRegisterAction::expiry_pricing_puzzle(
506            &mut ctx,
507            base_price,
508            registration_period,
509        )?;
510        assert_eq!(
511            ctx.tree_hash(puzzle),
512            XchandlesExpirePricingPuzzle::curry_tree_hash(base_price, registration_period)
513        );
514        let factor = XchandlesFactorPricingPuzzleArgs {
515            base_price,
516            registration_period,
517        }
518        .curry_tree_hash();
519        assert_ne!(ctx.tree_hash(puzzle), factor);
520        Ok(())
521    }
522}