chia_sdk_driver/primitives/
streamed_cat.rs

1use crate::{CatLayer, DriverError, Layer, Puzzle, Spend, SpendContext};
2use chia_consensus::make_aggsig_final_message::u64_to_bytes;
3use chia_protocol::{Bytes, Bytes32, Coin};
4use chia_puzzle_types::{
5    cat::{CatArgs, CatSolution},
6    CoinProof, LineageProof, Memos,
7};
8use chia_sdk_types::{run_puzzle, Condition, Conditions};
9use chia_sha2::Sha256;
10use clvm_traits::FromClvm;
11use clvm_utils::{tree_hash, TreeHash};
12use clvmr::{op_utils::u64_from_bytes, Allocator, NodePtr};
13
14use crate::{StreamLayer, StreamPuzzleSolution};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct StreamingPuzzleInfo {
18    pub recipient: Bytes32,
19    pub clawback_ph: Option<Bytes32>,
20    pub end_time: u64,
21    pub last_payment_time: u64,
22}
23
24impl StreamingPuzzleInfo {
25    pub fn new(
26        recipient: Bytes32,
27        clawback_ph: Option<Bytes32>,
28        end_time: u64,
29        last_payment_time: u64,
30    ) -> Self {
31        Self {
32            recipient,
33            clawback_ph,
34            end_time,
35            last_payment_time,
36        }
37    }
38
39    pub fn amount_to_be_paid(&self, my_coin_amount: u64, payment_time: u64) -> u64 {
40        // LAST_PAYMENT_TIME + (to_pay * (END_TIME - LAST_PAYMENT_TIME) / my_amount) = payment_time
41        // to_pay = my_amount * (payment_time - LAST_PAYMENT_TIME) / (END_TIME - LAST_PAYMENT_TIME)
42        my_coin_amount * (payment_time - self.last_payment_time)
43            / (self.end_time - self.last_payment_time)
44    }
45
46    pub fn get_hint(recipient: Bytes32) -> Bytes32 {
47        let mut s = Sha256::new();
48        s.update(b"s");
49        s.update(recipient.as_slice());
50        s.finalize().into()
51    }
52
53    pub fn get_launch_hints(&self) -> Vec<Bytes> {
54        let hint: Bytes = self.recipient.into();
55        let clawback_ph: Bytes = if let Some(clawback_ph) = self.clawback_ph {
56            clawback_ph.into()
57        } else {
58            Bytes::new(vec![])
59        };
60        let second_memo = u64_to_bytes(self.last_payment_time);
61        let third_memo = u64_to_bytes(self.end_time);
62
63        vec![hint, clawback_ph, second_memo.into(), third_memo.into()]
64    }
65
66    #[must_use]
67    pub fn with_last_payment_time(self, last_payment_time: u64) -> Self {
68        Self {
69            last_payment_time,
70            ..self
71        }
72    }
73
74    pub fn parse(allocator: &Allocator, puzzle: Puzzle) -> Result<Option<Self>, DriverError> {
75        let Some(layer) = StreamLayer::parse_puzzle(allocator, puzzle)? else {
76            return Ok(None);
77        };
78
79        Ok(Some(Self::from_layer(layer)))
80    }
81
82    pub fn into_layer(self) -> StreamLayer {
83        StreamLayer::new(
84            self.recipient,
85            self.clawback_ph,
86            self.end_time,
87            self.last_payment_time,
88        )
89    }
90
91    pub fn from_layer(layer: StreamLayer) -> Self {
92        Self {
93            recipient: layer.recipient,
94            clawback_ph: layer.clawback_ph,
95            end_time: layer.end_time,
96            last_payment_time: layer.last_payment_time,
97        }
98    }
99
100    pub fn inner_puzzle_hash(&self) -> TreeHash {
101        self.into_layer().puzzle_hash()
102    }
103
104    pub fn from_memos(memos: &[Bytes]) -> Result<Option<Self>, DriverError> {
105        if memos.len() < 4 || memos.len() > 5 {
106            return Ok(None);
107        }
108
109        let (recipient, clawback_ph, last_payment_time, end_time): (
110            Bytes32,
111            Option<Bytes32>,
112            u64,
113            u64,
114        ) = if memos.len() == 4 {
115            let Ok(recipient_b64): Result<Bytes32, _> = memos[0].clone().try_into() else {
116                return Ok(None);
117            };
118            let clawback_ph_b64: Option<Bytes32> = if memos[1].is_empty() {
119                None
120            } else {
121                let b32: Result<Bytes32, _> = memos[1].clone().try_into();
122                if let Ok(b32) = b32 {
123                    Some(b32)
124                } else {
125                    return Ok(None);
126                }
127            };
128            (
129                recipient_b64,
130                clawback_ph_b64,
131                u64_from_bytes(&memos[2]),
132                u64_from_bytes(&memos[3]),
133            )
134        } else {
135            let Ok(recipient_b64): Result<Bytes32, _> = memos[1].clone().try_into() else {
136                return Ok(None);
137            };
138            let clawback_ph_b64: Option<Bytes32> = if memos[2].is_empty() {
139                None
140            } else {
141                let b32: Result<Bytes32, _> = memos[2].clone().try_into();
142                if let Ok(b32) = b32 {
143                    Some(b32)
144                } else {
145                    return Ok(None);
146                }
147            };
148            (
149                recipient_b64,
150                clawback_ph_b64,
151                u64_from_bytes(&memos[3]),
152                u64_from_bytes(&memos[4]),
153            )
154        };
155
156        Ok(Some(Self::new(
157            recipient,
158            clawback_ph,
159            end_time,
160            last_payment_time,
161        )))
162    }
163}
164
165#[derive(Debug, Clone)]
166#[must_use]
167pub struct StreamedCat {
168    pub coin: Coin,
169    pub asset_id: Bytes32,
170    pub proof: LineageProof,
171    pub info: StreamingPuzzleInfo,
172}
173
174impl StreamedCat {
175    pub fn new(
176        coin: Coin,
177        asset_id: Bytes32,
178        proof: LineageProof,
179        info: StreamingPuzzleInfo,
180    ) -> Self {
181        Self {
182            coin,
183            asset_id,
184            proof,
185            info,
186        }
187    }
188
189    pub fn layers(&self) -> CatLayer<StreamLayer> {
190        CatLayer::<StreamLayer>::new(self.asset_id, self.info.into_layer())
191    }
192
193    pub fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
194        self.layers().construct_puzzle(ctx)
195    }
196
197    pub fn construct_solution(
198        &self,
199        ctx: &mut SpendContext,
200        payment_time: u64,
201        clawback: bool,
202    ) -> Result<NodePtr, DriverError> {
203        self.layers().construct_solution(
204            ctx,
205            CatSolution {
206                inner_puzzle_solution: StreamPuzzleSolution {
207                    my_amount: self.coin.amount,
208                    payment_time,
209                    to_pay: self.info.amount_to_be_paid(self.coin.amount, payment_time),
210                    clawback,
211                },
212                lineage_proof: Some(self.proof),
213                prev_coin_id: self.coin.coin_id(),
214                this_coin_info: self.coin,
215                next_coin_proof: CoinProof {
216                    parent_coin_info: self.coin.parent_coin_info,
217                    inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
218                    amount: self.coin.amount,
219                },
220                prev_subtotal: 0,
221                extra_delta: 0,
222            },
223        )
224    }
225
226    pub fn spend(
227        &self,
228        ctx: &mut SpendContext,
229        payment_time: u64,
230        clawback: bool,
231    ) -> Result<(), DriverError> {
232        let puzzle = self.construct_puzzle(ctx)?;
233        let solution = self.construct_solution(ctx, payment_time, clawback)?;
234
235        ctx.spend(self.coin, Spend::new(puzzle, solution))
236    }
237
238    // if clawback, 3rd arg = las
239    pub fn from_parent_spend(
240        allocator: &mut Allocator,
241        parent_coin: Coin,
242        parent_puzzle: Puzzle,
243        parent_solution: NodePtr,
244    ) -> Result<(Option<Self>, bool, u64), DriverError> {
245        let Some(layers) = CatLayer::<StreamLayer>::parse_puzzle(allocator, parent_puzzle)? else {
246            // check if parent created streaming CAT
247            let parent_puzzle_ptr = parent_puzzle.ptr();
248            let output = run_puzzle(allocator, parent_puzzle_ptr, parent_solution)?;
249            let conds: Conditions<NodePtr> = Conditions::from_clvm(allocator, output)?;
250
251            let Some(parent_layer) = CatLayer::<NodePtr>::parse_puzzle(allocator, parent_puzzle)?
252            else {
253                return Ok((None, false, 0));
254            };
255
256            let mut found_stream_layer: Option<Self> = None;
257            for cond in conds {
258                let Condition::CreateCoin(cc) = cond else {
259                    continue;
260                };
261
262                let Memos::Some(memos) = cc.memos else {
263                    continue;
264                };
265
266                let memos = Vec::<Bytes>::from_clvm(allocator, memos)?;
267                let Some(candidate_info) = StreamingPuzzleInfo::from_memos(&memos)? else {
268                    continue;
269                };
270                let candidate_inner_puzzle_hash = candidate_info.inner_puzzle_hash();
271                let candidate_puzzle_hash =
272                    CatArgs::curry_tree_hash(parent_layer.asset_id, candidate_inner_puzzle_hash);
273
274                if cc.puzzle_hash != candidate_puzzle_hash.into() {
275                    continue;
276                }
277
278                found_stream_layer = Some(Self::new(
279                    Coin::new(
280                        parent_coin.coin_id(),
281                        candidate_puzzle_hash.into(),
282                        cc.amount,
283                    ),
284                    parent_layer.asset_id,
285                    LineageProof {
286                        parent_parent_coin_info: parent_coin.parent_coin_info,
287                        parent_inner_puzzle_hash: tree_hash(allocator, parent_layer.inner_puzzle)
288                            .into(),
289                        parent_amount: parent_coin.amount,
290                    },
291                    candidate_info,
292                ));
293            }
294
295            return Ok((found_stream_layer, false, 0));
296        };
297
298        let proof = LineageProof {
299            parent_parent_coin_info: parent_coin.parent_coin_info,
300            parent_inner_puzzle_hash: layers.inner_puzzle.puzzle_hash().into(),
301            parent_amount: parent_coin.amount,
302        };
303
304        let parent_solution =
305            CatSolution::<StreamPuzzleSolution>::from_clvm(allocator, parent_solution)?;
306        if parent_solution.inner_puzzle_solution.clawback {
307            return Ok((None, true, parent_solution.inner_puzzle_solution.to_pay));
308        }
309
310        let new_amount = parent_coin.amount - parent_solution.inner_puzzle_solution.to_pay;
311
312        let new_inner_layer = StreamLayer::new(
313            layers.inner_puzzle.recipient,
314            layers.inner_puzzle.clawback_ph,
315            layers.inner_puzzle.end_time,
316            parent_solution.inner_puzzle_solution.payment_time,
317        );
318        let new_puzzle_hash =
319            CatArgs::curry_tree_hash(layers.asset_id, new_inner_layer.puzzle_hash());
320
321        Ok((
322            Some(Self::new(
323                Coin::new(parent_coin.coin_id(), new_puzzle_hash.into(), new_amount),
324                layers.asset_id,
325                proof,
326                // last payment time should've been updated by the spend
327                StreamingPuzzleInfo::from_layer(layers.inner_puzzle)
328                    .with_last_payment_time(parent_solution.inner_puzzle_solution.payment_time),
329            )),
330            false,
331            0,
332        ))
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use chia_protocol::Bytes;
339    use chia_sdk_test::{BlsPair, Simulator};
340    use clvm_utils::tree_hash;
341    use clvmr::serde::node_from_bytes;
342
343    use crate::{Cat, StandardLayer, STREAM_PUZZLE, STREAM_PUZZLE_HASH};
344
345    use super::*;
346
347    #[test]
348    fn test_puzzle_hash() {
349        let mut allocator = Allocator::new();
350
351        let ptr = node_from_bytes(&mut allocator, &STREAM_PUZZLE).unwrap();
352        assert_eq!(tree_hash(&allocator, ptr), STREAM_PUZZLE_HASH);
353    }
354
355    #[test]
356    fn test_streamed_cat() -> anyhow::Result<()> {
357        let mut ctx = SpendContext::new();
358        let mut sim = Simulator::new();
359
360        let claim_intervals = [1000, 2000, 500, 1000, 10];
361        let clawback_offset = 1234;
362        let total_claim_time = claim_intervals.iter().sum::<u64>() + clawback_offset;
363
364        // Create CAT & launch vesting one
365        let user_key = BlsPair::new(0);
366        let user_p2 = StandardLayer::new(user_key.pk);
367        let user_puzzle_hash: Bytes32 = user_key.puzzle_hash;
368
369        let payment_cat_amount = 1000;
370        let minter_key = BlsPair::new(1);
371        let minter_coin = sim.new_coin(minter_key.puzzle_hash, payment_cat_amount);
372        let minter_p2 = StandardLayer::new(minter_key.pk);
373
374        let clawback_puzzle_ptr = ctx.alloc(&1)?;
375        let clawback_ph = ctx.tree_hash(clawback_puzzle_ptr);
376        let streaming_inner_puzzle = StreamLayer::new(
377            user_puzzle_hash,
378            Some(clawback_ph.into()),
379            total_claim_time + 1000,
380            1000,
381        );
382        let streaming_inner_puzzle_hash: Bytes32 = streaming_inner_puzzle.puzzle_hash().into();
383        let (issue_cat, cats) = Cat::issue_with_coin(
384            &mut ctx,
385            minter_coin.coin_id(),
386            payment_cat_amount,
387            Conditions::new().create_coin(
388                streaming_inner_puzzle_hash,
389                payment_cat_amount,
390                Memos::None,
391            ),
392        )?;
393        minter_p2.spend(&mut ctx, minter_coin, issue_cat)?;
394
395        let initial_vesting_cat = cats[0];
396        sim.spend_coins(ctx.take(), &[minter_key.sk.clone()])?;
397        sim.set_next_timestamp(1000 + claim_intervals[0])?;
398
399        // spend streaming CAT
400        let mut streamed_cat = StreamedCat::new(
401            initial_vesting_cat.coin,
402            initial_vesting_cat.info.asset_id,
403            initial_vesting_cat.lineage_proof.unwrap(),
404            StreamingPuzzleInfo::new(
405                user_puzzle_hash,
406                Some(clawback_ph.into()),
407                total_claim_time + 1000,
408                1000,
409            ),
410        );
411
412        let mut claim_time = sim.next_timestamp();
413        for (i, _interval) in claim_intervals.iter().enumerate() {
414            /* Payment is always based on last block's timestamp */
415            if i < claim_intervals.len() - 1 {
416                sim.pass_time(claim_intervals[i + 1]);
417            }
418
419            // to claim the payment, user needs to send a message to the streaming CAT
420            let user_coin = sim.new_coin(user_puzzle_hash, 0);
421            let message_to_send: Bytes = Bytes::new(u64_to_bytes(claim_time));
422            let coin_id_ptr = ctx.alloc(&streamed_cat.coin.coin_id())?;
423            user_p2.spend(
424                &mut ctx,
425                user_coin,
426                Conditions::new().send_message(23, message_to_send, vec![coin_id_ptr]),
427            )?;
428
429            streamed_cat.spend(&mut ctx, claim_time, false)?;
430
431            let spends = ctx.take();
432            let streamed_cat_spend = spends.last().unwrap().clone();
433            sim.spend_coins(spends, &[user_key.sk.clone()])?;
434
435            // set up for next iteration
436            if i < claim_intervals.len() - 1 {
437                claim_time += claim_intervals[i + 1];
438            }
439            let parent_puzzle = ctx.alloc(&streamed_cat_spend.puzzle_reveal)?;
440            let parent_puzzle = Puzzle::from_clvm(&ctx, parent_puzzle)?;
441            let parent_solution = ctx.alloc(&streamed_cat_spend.solution)?;
442            let (Some(new_streamed_cat), clawback, _) = StreamedCat::from_parent_spend(
443                &mut ctx,
444                streamed_cat.coin,
445                parent_puzzle,
446                parent_solution,
447            )?
448            else {
449                panic!("Failed to parse new streamed cat");
450            };
451
452            assert!(!clawback);
453            streamed_cat = new_streamed_cat;
454        }
455
456        // Test clawback
457        assert!(streamed_cat.coin.amount > 0);
458        let clawback_msg_coin = sim.new_coin(clawback_ph.into(), 0);
459        let claim_time = sim.next_timestamp() + 1;
460        let message_to_send: Bytes = Bytes::new(u64_to_bytes(claim_time));
461        let coin_id_ptr = ctx.alloc(&streamed_cat.coin.coin_id())?;
462        let solution =
463            ctx.alloc(&Conditions::new().send_message(23, message_to_send, vec![coin_id_ptr]))?;
464        ctx.spend(clawback_msg_coin, Spend::new(clawback_puzzle_ptr, solution))?;
465
466        streamed_cat.spend(&mut ctx, claim_time, true)?;
467
468        let spends = ctx.take();
469        let streamed_cat_spend = spends.last().unwrap().clone();
470        sim.spend_coins(spends, &[user_key.sk.clone()])?;
471
472        let parent_puzzle = ctx.alloc(&streamed_cat_spend.puzzle_reveal)?;
473        let parent_puzzle = Puzzle::from_clvm(&ctx, parent_puzzle)?;
474        let parent_solution = ctx.alloc(&streamed_cat_spend.solution)?;
475        let (new_streamed_cat, clawback, _paid_amount_if_clawback) =
476            StreamedCat::from_parent_spend(
477                &mut ctx,
478                streamed_cat.coin,
479                parent_puzzle,
480                parent_solution,
481            )?;
482
483        assert!(clawback);
484        assert!(new_streamed_cat.is_none());
485
486        Ok(())
487    }
488}