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