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

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        // truths for epired solution are: Buy_Time, Current_Expiration, Handle
79        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        // truths are: Buy_Time, Current_Expiration, Handle
119        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        // announcement is simply premcommitment coin ph
148        let expire_ann = precommit_coin.coin.puzzle_hash;
149
150        // spend precommit coin
151        precommit_coin.spend(ctx, PrecommitSpendMode::REGISTER, my_inner_puzzle_hash)?;
152
153        // spend self
154        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        // spend slot
184        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    // A scale factor is how many units of the payment token equate to $1
198    // For exampe, you'd use scale_factor=1000 for wUSDC.b
199    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, // one day = 86400 = 60 * 60 * 24 seconds
210            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, // one day = 86400 = 60 * 60 * 24 seconds
227            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        // check premium after auction is 0
322        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}