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 let mut register_announcement = precommit_coin.coin.puzzle_hash.to_vec();
177 register_announcement.insert(0, b'r');
178
179 let my_inner_puzzle_hash = registry.info.inner_puzzle_hash().into();
181 precommit_coin.spend(ctx, PrecommitSpendMode::REGISTER, my_inner_puzzle_hash)?;
182
183 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 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; let registration_period = 366 * 24 * 60 * 60; 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, 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 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 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 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}