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 #[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 #[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 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 let my_inner_puzzle_hash = registry.info.inner_puzzle_hash().into();
310 precommit_coin.spend(ctx, PrecommitSpendMode::REGISTER, my_inner_puzzle_hash)?;
311
312 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 left_slot.spend(ctx, my_inner_puzzle_hash)?;
356 right_slot.spend(ctx, my_inner_puzzle_hash)?;
357
358 let message_destination = ctx.alloc(®istry.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 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; let registration_period = 366 * 24 * 60 * 60; 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, 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 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 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 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}