1use crate::{CatLayer, DriverError, HashedPtr, Layer, Puzzle, Spend, SpendContext};
2use chia_consensus::make_aggsig_final_message::u64_to_bytes;
3use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend};
4use chia_puzzle_types::{
5 CoinProof, LineageProof, Memos,
6 cat::{CatArgs, CatSolution},
7};
8use chia_sdk_types::{Condition, Conditions};
9use chia_sha2::Sha256;
10use clvm_traits::FromClvm;
11use clvm_utils::TreeHash;
12use clvmr::{Allocator, NodePtr, op_utils::u64_from_bytes};
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, PartialEq, Eq)]
166#[must_use]
167pub struct StreamedAsset {
168 pub coin: Coin,
169 pub asset_id: Option<Bytes32>,
170 pub proof: Option<LineageProof>,
171 pub info: StreamingPuzzleInfo,
172}
173
174impl StreamedAsset {
175 pub fn cat(
176 coin: Coin,
177 asset_id: Bytes32,
178 proof: LineageProof,
179 info: StreamingPuzzleInfo,
180 ) -> Self {
181 Self {
182 coin,
183 asset_id: Some(asset_id),
184 proof: Some(proof),
185 info,
186 }
187 }
188
189 pub fn xch(coin: Coin, info: StreamingPuzzleInfo) -> Self {
190 Self {
191 coin,
192 asset_id: None,
193 proof: None,
194 info,
195 }
196 }
197
198 pub fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
199 let inner_layer = self.info.into_layer();
200 if let Some(asset_id) = self.asset_id {
201 CatLayer::new(asset_id, inner_layer).construct_puzzle(ctx)
202 } else {
203 inner_layer.construct_puzzle(ctx)
204 }
205 }
206
207 pub fn construct_solution(
208 &self,
209 ctx: &mut SpendContext,
210 payment_time: u64,
211 clawback: bool,
212 ) -> Result<NodePtr, DriverError> {
213 let inner_layer = self.info.into_layer();
214 let inner_solution = StreamPuzzleSolution {
215 my_amount: self.coin.amount,
216 payment_time,
217 to_pay: self.info.amount_to_be_paid(self.coin.amount, payment_time),
218 clawback,
219 };
220
221 if let Some(asset_id) = self.asset_id {
222 CatLayer::new(asset_id, inner_layer).construct_solution(
223 ctx,
224 CatSolution {
225 inner_puzzle_solution: inner_solution,
226 lineage_proof: Some(self.proof.ok_or(DriverError::Custom(
227 "Missing lineage proof for CAT steam".to_string(),
228 ))?),
229 prev_coin_id: self.coin.coin_id(),
230 this_coin_info: self.coin,
231 next_coin_proof: CoinProof {
232 parent_coin_info: self.coin.parent_coin_info,
233 inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
234 amount: self.coin.amount,
235 },
236 prev_subtotal: 0,
237 extra_delta: 0,
238 },
239 )
240 } else {
241 inner_layer.construct_solution(ctx, inner_solution)
242 }
243 }
244
245 pub fn spend(
246 &self,
247 ctx: &mut SpendContext,
248 payment_time: u64,
249 clawback: bool,
250 ) -> Result<(), DriverError> {
251 let puzzle = self.construct_puzzle(ctx)?;
252 let solution = self.construct_solution(ctx, payment_time, clawback)?;
253
254 ctx.spend(self.coin, Spend::new(puzzle, solution))
255 }
256
257 pub fn from_parent_spend(
259 ctx: &mut SpendContext,
260 coin_spend: &CoinSpend,
261 ) -> Result<(Option<Self>, bool, u64), DriverError> {
262 let parent_coin = coin_spend.coin;
263 let parent_puzzle_ptr = ctx.alloc(&coin_spend.puzzle_reveal)?;
264 let parent_puzzle = Puzzle::from_clvm(ctx, parent_puzzle_ptr)?;
265 let parent_solution = ctx.alloc(&coin_spend.solution)?;
266
267 if let Some((asset_id, proof, streaming_layer, streaming_solution)) =
268 if let Ok(Some(layers)) = CatLayer::<StreamLayer>::parse_puzzle(ctx, parent_puzzle) {
269 Some((
272 Some(layers.asset_id),
273 Some(LineageProof {
274 parent_parent_coin_info: parent_coin.parent_coin_info,
275 parent_inner_puzzle_hash: layers.inner_puzzle.puzzle_hash().into(),
276 parent_amount: parent_coin.amount,
277 }),
278 layers.inner_puzzle,
279 ctx.extract::<CatSolution<StreamPuzzleSolution>>(parent_solution)?
280 .inner_puzzle_solution,
281 ))
282 } else if let Ok(Some(layer)) = StreamLayer::parse_puzzle(ctx, parent_puzzle) {
283 Some((
284 None,
285 None,
286 layer,
287 ctx.extract::<StreamPuzzleSolution>(parent_solution)?,
288 ))
289 } else {
290 None
291 }
292 {
293 if streaming_solution.clawback {
294 return Ok((None, true, streaming_solution.to_pay));
295 }
296
297 let new_amount = parent_coin.amount - streaming_solution.to_pay;
298
299 let new_inner_layer = StreamLayer::new(
300 streaming_layer.recipient,
301 streaming_layer.clawback_ph,
302 streaming_layer.end_time,
303 streaming_solution.payment_time,
304 );
305 let new_puzzle_hash = if let Some(asset_id) = asset_id {
306 CatArgs::curry_tree_hash(asset_id, new_inner_layer.puzzle_hash())
307 } else {
308 new_inner_layer.puzzle_hash()
309 };
310
311 return Ok((
312 Some(Self {
313 coin: Coin::new(parent_coin.coin_id(), new_puzzle_hash.into(), new_amount),
314 asset_id,
315 proof,
316 info: StreamingPuzzleInfo::from_layer(streaming_layer)
318 .with_last_payment_time(streaming_solution.payment_time),
319 }),
320 false,
321 0,
322 ));
323 }
324
325 let parent_puzzle_ptr = parent_puzzle.ptr();
328 let output = ctx.run(parent_puzzle_ptr, parent_solution)?;
329 let conds = ctx.extract::<Conditions<NodePtr>>(output)?;
330
331 let (asset_id, proof) = if let Ok(Some(parent_layer)) =
332 CatLayer::<HashedPtr>::parse_puzzle(ctx, parent_puzzle)
333 {
334 (
335 Some(parent_layer.asset_id),
336 Some(LineageProof {
337 parent_parent_coin_info: parent_coin.parent_coin_info,
338 parent_inner_puzzle_hash: parent_layer.inner_puzzle.tree_hash().into(),
339 parent_amount: parent_coin.amount,
340 }),
341 )
342 } else {
343 (None, None)
344 };
345
346 for cond in conds {
347 let Condition::CreateCoin(cc) = cond else {
348 continue;
349 };
350
351 let Memos::Some(memos) = cc.memos else {
352 continue;
353 };
354
355 let memos = ctx.extract::<Vec<Bytes>>(memos)?;
356 let Some(candidate_info) = StreamingPuzzleInfo::from_memos(&memos)? else {
357 continue;
358 };
359 let candidate_inner_puzzle_hash = candidate_info.inner_puzzle_hash();
360 let candidate_puzzle_hash = if let Some(asset_id) = asset_id {
361 CatArgs::curry_tree_hash(asset_id, candidate_inner_puzzle_hash)
362 } else {
363 candidate_inner_puzzle_hash
364 };
365
366 if cc.puzzle_hash != candidate_puzzle_hash.into() {
367 continue;
368 }
369
370 return Ok((
371 Some(Self {
372 coin: Coin::new(
373 parent_coin.coin_id(),
374 candidate_puzzle_hash.into(),
375 cc.amount,
376 ),
377 asset_id,
378 proof,
379 info: candidate_info,
380 }),
381 false,
382 0,
383 ));
384 }
385
386 Ok((None, false, 0))
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use std::slice;
393
394 use chia_protocol::Bytes;
395 use chia_sdk_test::{Benchmark, Simulator};
396 use clvm_utils::tree_hash;
397 use clvmr::serde::node_from_bytes;
398 use rstest::rstest;
399
400 use crate::{
401 Cat, CatSpend, FungibleAsset, STREAM_PUZZLE, STREAM_PUZZLE_HASH, SpendWithConditions,
402 StandardLayer,
403 };
404
405 use super::*;
406
407 #[test]
408 fn test_puzzle_hash() {
409 let mut allocator = Allocator::new();
410
411 let ptr = node_from_bytes(&mut allocator, &STREAM_PUZZLE).unwrap();
412 assert_eq!(tree_hash(&allocator, ptr), STREAM_PUZZLE_HASH);
413 }
414
415 #[rstest]
416 fn test_streamed_asset(#[values(true, false)] xch_stream: bool) -> anyhow::Result<()> {
417 let mut ctx = SpendContext::new();
418 let mut sim = Simulator::new();
419 let mut benchmark = Benchmark::new(format!(
420 "Streamed {}",
421 if xch_stream { "XCH" } else { "CAT" }
422 ));
423
424 let claim_intervals = [1000, 2000, 500, 1000, 10];
425 let clawback_offset = 1234;
426 let total_claim_time = claim_intervals.iter().sum::<u64>() + clawback_offset;
427
428 let user_bls = sim.bls(0);
430 let minter_bls = sim.bls(1000);
431
432 let clawback_puzzle_ptr = ctx.alloc(&1)?;
433 let clawback_ph = ctx.tree_hash(clawback_puzzle_ptr);
434 let streaming_inner_puzzle = StreamLayer::new(
435 user_bls.puzzle_hash,
436 Some(clawback_ph.into()),
437 total_claim_time + 1000,
438 1000,
439 );
440 let streaming_inner_puzzle_hash: Bytes32 = streaming_inner_puzzle.puzzle_hash().into();
441
442 let launch_hints =
443 ctx.alloc(&StreamingPuzzleInfo::from_layer(streaming_inner_puzzle).get_launch_hints())?;
444 let create_inner_spend = StandardLayer::new(minter_bls.pk).spend_with_conditions(
445 &mut ctx,
446 Conditions::new().create_coin(
447 streaming_inner_puzzle_hash,
448 minter_bls.coin.amount,
449 Memos::Some(launch_hints),
450 ),
451 )?;
452
453 let (expected_coin, expected_asset_id, expected_lp) = if xch_stream {
454 ctx.spend(minter_bls.coin, create_inner_spend)?;
455
456 (
457 minter_bls
458 .coin
459 .make_child(streaming_inner_puzzle_hash, minter_bls.coin.amount),
460 None,
461 None,
462 )
463 } else {
464 let (issue_cat, cats) = Cat::issue_with_coin(
465 &mut ctx,
466 minter_bls.coin.coin_id(),
467 minter_bls.coin.amount,
468 Conditions::new().create_coin(
469 minter_bls.puzzle_hash,
470 minter_bls.coin.amount,
471 Memos::None,
472 ),
473 )?;
474 StandardLayer::new(minter_bls.pk).spend(&mut ctx, minter_bls.coin, issue_cat)?;
475 sim.spend_coins(ctx.take(), slice::from_ref(&minter_bls.sk))?;
476
477 let cats = Cat::spend_all(&mut ctx, &[CatSpend::new(cats[0], create_inner_spend)])?;
478
479 (
480 cats[0].coin,
481 Some(cats[0].info.asset_id),
482 cats[0].lineage_proof,
483 )
484 };
485
486 let spends = ctx.take();
487 let launch_spend = spends.last().unwrap().clone();
488 benchmark.add_spends(
489 &mut ctx,
490 &mut sim,
491 spends,
492 "create",
493 slice::from_ref(&minter_bls.sk),
494 )?;
495 sim.set_next_timestamp(1000 + claim_intervals[0])?;
496
497 let mut streamed_asset = StreamedAsset::from_parent_spend(&mut ctx, &launch_spend)?
499 .0
500 .unwrap();
501 assert_eq!(
502 streamed_asset,
503 StreamedAsset {
504 coin: expected_coin,
505 asset_id: expected_asset_id,
506 proof: expected_lp,
507 info: StreamingPuzzleInfo::new(
508 user_bls.puzzle_hash,
509 Some(clawback_ph.into()),
510 total_claim_time + 1000,
511 1000,
512 ),
513 },
514 );
515
516 let mut claim_time = sim.next_timestamp();
517 for (i, _interval) in claim_intervals.iter().enumerate() {
518 if i < claim_intervals.len() - 1 {
520 sim.pass_time(claim_intervals[i + 1]);
521 }
522
523 let user_coin = sim.new_coin(user_bls.puzzle_hash, 0);
525 let message_to_send: Bytes = Bytes::new(u64_to_bytes(claim_time));
526 let coin_id_ptr = ctx.alloc(&streamed_asset.coin.coin_id())?;
527 StandardLayer::new(user_bls.pk).spend(
528 &mut ctx,
529 user_coin,
530 Conditions::new().send_message(23, message_to_send, vec![coin_id_ptr]),
531 )?;
532
533 streamed_asset.spend(&mut ctx, claim_time, false)?;
534
535 let spends = ctx.take();
536 let streamed_asset_spend = spends.last().unwrap().clone();
537 benchmark.add_spends(
538 &mut ctx,
539 &mut sim,
540 spends,
541 "claim",
542 slice::from_ref(&user_bls.sk),
543 )?;
544
545 if i < claim_intervals.len() - 1 {
547 claim_time += claim_intervals[i + 1];
548 }
549 let (Some(new_streamed_asset), clawback, _) =
550 StreamedAsset::from_parent_spend(&mut ctx, &streamed_asset_spend)?
551 else {
552 panic!("Failed to parse new streamed asset");
553 };
554
555 assert!(!clawback);
556 streamed_asset = new_streamed_asset;
557 }
558
559 assert!(streamed_asset.coin.amount > 0);
561 let clawback_msg_coin = sim.new_coin(clawback_ph.into(), 0);
562 let claim_time = sim.next_timestamp() + 1;
563 let message_to_send: Bytes = Bytes::new(u64_to_bytes(claim_time));
564 let coin_id_ptr = ctx.alloc(&streamed_asset.coin.coin_id())?;
565 let solution =
566 ctx.alloc(&Conditions::new().send_message(23, message_to_send, vec![coin_id_ptr]))?;
567 ctx.spend(clawback_msg_coin, Spend::new(clawback_puzzle_ptr, solution))?;
568
569 streamed_asset.spend(&mut ctx, claim_time, true)?;
570
571 let spends = ctx.take();
572 let streamed_asset_spend = spends.last().unwrap().clone();
573 benchmark.add_spends(
574 &mut ctx,
575 &mut sim,
576 spends,
577 "clawback",
578 slice::from_ref(&user_bls.sk),
579 )?;
580
581 let (new_streamed_asset, clawback, _paid_amount_if_clawback) =
582 StreamedAsset::from_parent_spend(&mut ctx, &streamed_asset_spend)?;
583
584 assert!(clawback);
585 assert!(new_streamed_asset.is_none());
586
587 benchmark.print_summary(Some(&format!(
588 "streamed-{}.costs",
589 if xch_stream { "xch" } else { "cat" }
590 )));
591
592 Ok(())
593 }
594}