1use chia_protocol::Bytes32;
2use chia_puzzle_types::singleton::SingletonStruct;
3use chia_sdk_types::{
4 Conditions, Mod, announcement_id,
5 puzzles::{
6 DefaultCatMakerArgs, PREMIUM_BITS_LIST, PREMIUM_PRECISION, PrecommitSpendMode,
7 XchandlesDataValue, XchandlesExpireActionArgs, XchandlesExpireActionSolution,
8 XchandlesExponentialPremiumRenewPuzzleArgs, XchandlesFactorPricingPuzzleArgs,
9 XchandlesPricingSolution, XchandlesSlotValue,
10 },
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 XchandlesExpireAction {
23 pub launcher_id: Bytes32,
24 pub relative_block_height: u32,
25 pub payout_puzzle_hash: Bytes32,
26}
27
28impl ToTreeHash for XchandlesExpireAction {
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 XchandlesExpireAction {
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 XchandlesExpireAction {
50 pub fn new_args(
51 launcher_id: Bytes32,
52 relative_block_height: u32,
53 payout_puzzle_hash: Bytes32,
54 ) -> XchandlesExpireActionArgs {
55 XchandlesExpireActionArgs {
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_value(
75 ctx: &SpendContext,
76 solution: NodePtr,
77 ) -> Result<XchandlesSlotValue, DriverError> {
78 let solution = XchandlesExpireActionSolution::<
80 NodePtr,
81 NodePtr,
82 NodePtr,
83 (NodePtr, (u64, (String, NodePtr))),
84 NodePtr,
85 >::from_clvm(ctx, solution)?;
86
87 let handle = solution.expired_handle_pricing_puzzle_solution.1.1.0;
88 let current_expiration = solution.expired_handle_pricing_puzzle_solution.1.0;
89
90 Ok(XchandlesSlotValue::new(
91 handle.tree_hash().into(),
92 solution.neighbors.left_value,
93 solution.neighbors.right_value,
94 current_expiration,
95 solution.old_rest.owner_launcher_id,
96 solution.old_rest.resolved_data,
97 ))
98 }
99
100 pub fn created_slot_value(
101 ctx: &mut SpendContext,
102 solution: NodePtr,
103 ) -> Result<XchandlesSlotValue, DriverError> {
104 let solution = ctx.extract::<XchandlesExpireActionSolution<
105 NodePtr,
106 NodePtr,
107 NodePtr,
108 NodePtr,
109 NodePtr,
110 >>(solution)?;
111
112 let pricing_output = ctx.run(
113 solution.expired_handle_pricing_puzzle_reveal,
114 solution.expired_handle_pricing_puzzle_solution,
115 )?;
116 let registration_time_delta = <(NodePtr, u64)>::from_clvm(ctx, pricing_output)?.1;
117
118 let (buy_time, (_, (handle, _))) = ctx.extract::<(u64, (NodePtr, (String, NodePtr)))>(
120 solution.expired_handle_pricing_puzzle_solution,
121 )?;
122
123 Ok(XchandlesSlotValue::new(
124 handle.tree_hash().into(),
125 solution.neighbors.left_value,
126 solution.neighbors.right_value,
127 buy_time + registration_time_delta,
128 solution.new_rest.owner_launcher_id,
129 solution.new_rest.resolved_data,
130 ))
131 }
132
133 #[allow(clippy::too_many_arguments)]
134 pub fn spend(
135 self,
136 ctx: &mut SpendContext,
137 registry: &mut XchandlesRegistry,
138 slot: Slot<XchandlesSlotValue>,
139 num_periods: u64,
140 base_handle_price: u64,
141 registration_period: u64,
142 precommit_coin: PrecommitCoin<XchandlesPrecommitValue>,
143 start_time: u64,
144 ) -> Result<Conditions, DriverError> {
145 let my_inner_puzzle_hash = registry.info.inner_puzzle_hash().into();
146
147 let expire_ann = precommit_coin.coin.puzzle_hash;
149
150 precommit_coin.spend(ctx, PrecommitSpendMode::REGISTER, my_inner_puzzle_hash)?;
152
153 let slot = registry.actual_slot(slot);
155 let expire_args =
156 XchandlesExpirePricingPuzzle::from_info(ctx, base_handle_price, registration_period)?;
157 let action_solution = XchandlesExpireActionSolution {
158 cat_maker_puzzle_reveal: ctx.curry(DefaultCatMakerArgs::new(
159 precommit_coin.asset_id.tree_hash().into(),
160 ))?,
161 cat_maker_puzzle_solution: (),
162 expired_handle_pricing_puzzle_reveal: ctx.curry(expire_args)?,
163 expired_handle_pricing_puzzle_solution: XchandlesPricingSolution {
164 buy_time: start_time,
165 current_expiration: slot.info.value.expiration,
166 handle: precommit_coin.value.handle.clone(),
167 num_periods,
168 },
169 refund_puzzle_hash_hash: precommit_coin.refund_puzzle_hash.tree_hash().into(),
170 secret: precommit_coin.value.secret,
171 neighbors: slot.info.value.neighbors,
172 old_rest: slot.info.value.rest_data(),
173 new_rest: XchandlesDataValue {
174 owner_launcher_id: precommit_coin.value.owner_launcher_id,
175 resolved_data: precommit_coin.value.resolved_data,
176 },
177 }
178 .to_clvm(ctx)?;
179 let action_puzzle = self.construct_puzzle(ctx)?;
180
181 registry.insert_action_spend(ctx, Spend::new(action_puzzle, action_solution))?;
182
183 slot.spend(ctx, my_inner_puzzle_hash)?;
185
186 let mut expire_ann = expire_ann.to_vec();
187 expire_ann.insert(0, b'x');
188 Ok(Conditions::new()
189 .assert_puzzle_announcement(announcement_id(registry.coin.puzzle_hash, expire_ann)))
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct XchandlesExpirePricingPuzzle {}
195
196impl XchandlesExpirePricingPuzzle {
197 pub fn from_info(
200 ctx: &mut SpendContext,
201 base_price: u64,
202 registration_period: u64,
203 ) -> Result<XchandlesExponentialPremiumRenewPuzzleArgs<NodePtr>, DriverError> {
204 Ok(XchandlesExponentialPremiumRenewPuzzleArgs {
205 base_program: ctx.curry(XchandlesFactorPricingPuzzleArgs {
206 base_price,
207 registration_period,
208 })?,
209 halving_period: 86400, start_premium: XchandlesExponentialPremiumRenewPuzzleArgs::<()>::get_start_premium(
211 1000,
212 ),
213 end_value: XchandlesExponentialPremiumRenewPuzzleArgs::<()>::get_end_value(1000),
214 precision: PREMIUM_PRECISION,
215 bits_list: PREMIUM_BITS_LIST.to_vec(),
216 })
217 }
218
219 pub fn curry_tree_hash(base_price: u64, registration_period: u64) -> TreeHash {
220 XchandlesExponentialPremiumRenewPuzzleArgs::<TreeHash> {
221 base_program: XchandlesFactorPricingPuzzleArgs {
222 base_price,
223 registration_period,
224 }
225 .curry_tree_hash(),
226 halving_period: 86400, start_premium: XchandlesExponentialPremiumRenewPuzzleArgs::<()>::get_start_premium(
228 1000,
229 ),
230 end_value: XchandlesExponentialPremiumRenewPuzzleArgs::<()>::get_end_value(1000),
231 precision: PREMIUM_PRECISION,
232 bits_list: PREMIUM_BITS_LIST.to_vec(),
233 }
234 .curry_tree_hash()
235 }
236
237 pub fn get_price(
238 ctx: &mut SpendContext,
239 args: XchandlesExponentialPremiumRenewPuzzleArgs<NodePtr>,
240 handle: String,
241 expiration: u64,
242 buy_time: u64,
243 num_periods: u64,
244 ) -> Result<u128, DriverError> {
245 let puzzle = ctx.curry(args)?;
246 let solution = ctx.alloc(&XchandlesPricingSolution {
247 buy_time,
248 current_expiration: expiration,
249 handle,
250 num_periods,
251 })?;
252 let output = ctx.run(puzzle, solution)?;
253
254 Ok(ctx.extract::<(u128, u64)>(output)?.0)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[derive(FromClvm, ToClvm, Debug, Copy, Clone, PartialEq, Eq)]
263 #[clvm(list)]
264 struct XchandlesPricingOutput {
265 pub price: u128,
266 #[clvm(rest)]
267 pub registered_time: u64,
268 }
269
270 #[test]
271 fn test_exponential_premium_puzzle() -> Result<(), DriverError> {
272 let mut ctx = SpendContext::new();
273
274 let registration_period = 366 * 24 * 60 * 60;
275 let exponential_args =
276 XchandlesExpirePricingPuzzle::from_info(&mut ctx, 0, registration_period)?;
277 let puzzle = ctx.curry(exponential_args.clone())?;
278
279 let mut last_price = 100_000_000_000;
280 for day in 0..28 {
281 for hour in 0..24 {
282 let buy_time = day * 24 * 60 * 60 + hour * 60 * 60;
283 let solution = ctx.alloc(&XchandlesPricingSolution {
284 buy_time,
285 current_expiration: 0,
286 handle: "yakuhito".to_string(),
287 num_periods: 1,
288 })?;
289
290 let output = ctx.run(puzzle, solution)?;
291 let output = ctx.extract::<XchandlesPricingOutput>(output)?;
292
293 assert_eq!(output.registered_time, 366 * 24 * 60 * 60);
294
295 if hour == 0 {
296 let scale_factor =
297 372_529_029_846_191_406_u128 * 1000_u128 / 1_000_000_000_000_000_000_u128;
298 assert_eq!(
299 output.price,
300 (100_000_000 * 1000) / (1 << day) - scale_factor
301 );
302 }
303
304 assert!(output.price < last_price);
305 last_price = output.price;
306
307 assert_eq!(
308 XchandlesExpirePricingPuzzle::get_price(
309 &mut ctx,
310 exponential_args.clone(),
311 "yakuhito".to_string(),
312 0,
313 buy_time,
314 1
315 )?,
316 output.price
317 );
318 }
319 }
320
321 let solution = ctx.alloc(&XchandlesPricingSolution {
323 buy_time: 28 * 24 * 60 * 60,
324 current_expiration: 0,
325 handle: "yakuhito".to_string(),
326 num_periods: 1,
327 })?;
328
329 let output = ctx.run(puzzle, solution)?;
330 let output = ctx.extract::<XchandlesPricingOutput>(output)?;
331
332 assert_eq!(output.registered_time, 366 * 24 * 60 * 60);
333 assert_eq!(output.price, 0);
334
335 Ok(())
336 }
337}