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 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 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 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 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 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 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 if i < claim_intervals.len() - 1 {
416 sim.pass_time(claim_intervals[i + 1]);
417 }
418
419 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 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 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}