1use crate::{
2 CatLayer, CatMaker, DriverError, HashedPtr, Layer, P2ParentLayer, Puzzle, Spend, SpendContext,
3};
4use chia_protocol::{Bytes32, Coin};
5use chia_puzzle_types::{
6 cat::{CatArgs, CatSolution},
7 CoinProof, LineageProof, Memos,
8};
9use chia_puzzles::CAT_PUZZLE_HASH;
10use chia_sdk_types::{
11 puzzles::{P2ParentArgs, P2ParentSolution},
12 run_puzzle, Conditions, Mod,
13};
14use clvm_traits::{FromClvm, ToClvm};
15use clvm_utils::{ToTreeHash, TreeHash};
16use clvmr::{Allocator, NodePtr};
17
18#[derive(Debug, Clone, PartialEq, Eq, Copy)]
19#[must_use]
20pub struct P2ParentCoin {
21 pub coin: Coin,
22 pub asset_id: Option<Bytes32>,
23 pub proof: LineageProof,
24}
25
26impl P2ParentCoin {
27 pub fn new(coin: Coin, asset_id: Option<Bytes32>, proof: LineageProof) -> Self {
28 Self {
29 coin,
30 asset_id,
31 proof,
32 }
33 }
34
35 pub fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
36 if let Some(asset_id) = self.asset_id {
37 CatLayer::new(asset_id, P2ParentLayer::cat(asset_id.tree_hash())).construct_puzzle(ctx)
38 } else {
39 P2ParentLayer::xch().construct_puzzle(ctx)
40 }
41 }
42
43 pub fn inner_puzzle_hash(asset_id: Option<Bytes32>) -> TreeHash {
44 P2ParentArgs {
45 cat_maker: if let Some(asset_id) = asset_id {
46 CatMaker::Default {
47 tail_hash_hash: asset_id.tree_hash(),
48 }
49 .curry_tree_hash()
50 } else {
51 CatMaker::Xch.curry_tree_hash()
52 },
53 }
54 .curry_tree_hash()
55 }
56
57 pub fn puzzle_hash(asset_id: Option<Bytes32>) -> TreeHash {
58 let inner_puzzle_hash = Self::inner_puzzle_hash(asset_id);
59
60 if let Some(asset_id) = asset_id {
61 CatArgs::curry_tree_hash(asset_id, inner_puzzle_hash)
62 } else {
63 inner_puzzle_hash
64 }
65 }
66
67 pub fn construct_solution<CMS>(
68 &self,
69 ctx: &mut SpendContext,
70 delegated_spend: Spend,
71 cat_maker_solution: CMS,
72 ) -> Result<NodePtr, DriverError>
73 where
74 CMS: ToClvm<Allocator>,
75 {
76 let inner_solution = P2ParentSolution {
77 parent_parent_id: self.proof.parent_parent_coin_info,
78 parent_amount: self.proof.parent_amount,
79 parent_inner_puzzle: delegated_spend.puzzle,
80 parent_solution: delegated_spend.solution,
81 cat_maker_solution: cat_maker_solution.to_clvm(ctx)?,
82 };
83
84 if let Some(asset_id) = self.asset_id {
85 let inner_layer = P2ParentLayer::cat(asset_id.tree_hash());
86
87 CatLayer::new(asset_id, inner_layer).construct_solution(
88 ctx,
89 CatSolution {
90 inner_puzzle_solution: inner_solution,
91 lineage_proof: Some(self.proof),
92 prev_coin_id: self.coin.coin_id(),
93 this_coin_info: self.coin,
94 next_coin_proof: CoinProof {
95 parent_coin_info: self.coin.parent_coin_info,
96 inner_puzzle_hash: Self::inner_puzzle_hash(self.asset_id).into(),
97 amount: self.coin.amount,
98 },
99 prev_subtotal: 0,
100 extra_delta: 0,
101 },
102 )
103 } else {
104 P2ParentLayer::xch().construct_solution(ctx, inner_solution)
105 }
106 }
107
108 pub fn spend<CMS>(
109 &self,
110 ctx: &mut SpendContext,
111 delegated_spend: Spend,
112 cat_maker_solution: CMS,
113 ) -> Result<(), DriverError>
114 where
115 CMS: ToClvm<Allocator>,
116 {
117 let puzzle = self.construct_puzzle(ctx)?;
118 let solution = self.construct_solution(ctx, delegated_spend, cat_maker_solution)?;
119
120 ctx.spend(self.coin, Spend::new(puzzle, solution))
121 }
122
123 pub fn parse_child(
125 allocator: &mut Allocator,
126 parent_coin: Coin,
127 parent_puzzle: Puzzle,
128 parent_solution: NodePtr,
129 ) -> Result<Option<(Self, Memos)>, DriverError> {
130 let (parent_inner_puzzle_hash, asset_id) =
131 if parent_puzzle.mod_hash() == CAT_PUZZLE_HASH.into() {
132 let Some(parent_puzzle) = parent_puzzle.as_curried() else {
133 return Err(DriverError::Custom(
134 "Expected parent puzzle to be curried but it's not.".to_string(),
135 ));
136 };
137
138 let args = CatArgs::<HashedPtr>::from_clvm(allocator, parent_puzzle.args)?;
139 (args.inner_puzzle.tree_hash().into(), Some(args.asset_id))
140 } else {
141 (parent_coin.puzzle_hash, None)
142 };
143
144 let proof = LineageProof {
145 parent_parent_coin_info: parent_coin.parent_coin_info,
146 parent_inner_puzzle_hash,
147 parent_amount: parent_coin.amount,
148 };
149
150 let expected_puzzle_hash: Bytes32 = Self::puzzle_hash(asset_id).into();
151
152 let parent_output = run_puzzle(allocator, parent_puzzle.ptr(), parent_solution)?;
153 let parent_conditions = Conditions::<NodePtr>::from_clvm(allocator, parent_output)?;
154 let Some(create_coin) = parent_conditions.iter().find_map(|c| {
155 c.as_create_coin()
156 .filter(|&create_coin| create_coin.puzzle_hash == expected_puzzle_hash)
157 }) else {
158 return Ok(None);
159 };
160
161 Ok(Some((
162 Self {
163 coin: Coin::new(
164 parent_coin.coin_id(),
165 expected_puzzle_hash,
166 create_coin.amount,
167 ),
168 asset_id,
169 proof,
170 },
171 create_coin.memos,
172 )))
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use std::slice;
179
180 use chia_protocol::Bytes;
181 use chia_sdk_test::{Benchmark, Simulator};
182 use chia_sdk_types::puzzles::{P2_PARENT_PUZZLE, P2_PARENT_PUZZLE_HASH};
183 use clvm_utils::tree_hash;
184 use clvmr::serde::node_from_bytes;
185 use rstest::rstest;
186
187 use crate::{Cat, CatSpend, FungibleAsset, SpendWithConditions, StandardLayer};
188
189 use super::*;
190
191 #[test]
192 fn test_puzzle_hash() {
193 let mut allocator = Allocator::new();
194
195 let ptr = node_from_bytes(&mut allocator, &P2_PARENT_PUZZLE).unwrap();
196 assert_eq!(tree_hash(&allocator, ptr), P2_PARENT_PUZZLE_HASH);
197 }
198
199 #[rstest]
200 #[case::xch(false)]
201 #[case::cat(true)]
202 fn test_p2_parent(#[case] cat_mode: bool) -> anyhow::Result<()> {
203 let mut ctx = SpendContext::new();
204 let mut sim = Simulator::new();
205 let mut benchmark = Benchmark::new(format!(
206 "P2 Parent Coin ({})",
207 if cat_mode { "CAT" } else { "XCH" }
208 ));
209
210 let parent_bls = sim.bls(1337);
211
212 let server_list = vec![
214 Bytes::new(b"yak1".to_vec()),
215 Bytes::new(b"yak2".to_vec()),
216 Bytes::new(b"yak3".to_vec()),
217 ];
218
219 let (expected_coin, expected_asset_id, expected_lp) = if cat_mode {
220 let (issue_cat, cats) = Cat::issue_with_coin(
221 &mut ctx,
222 parent_bls.coin.coin_id(),
223 parent_bls.coin.amount,
224 Conditions::new().create_coin(
225 parent_bls.puzzle_hash,
226 parent_bls.coin.amount,
227 Memos::None,
228 ),
229 )?;
230 StandardLayer::new(parent_bls.pk).spend(&mut ctx, parent_bls.coin, issue_cat)?;
231 sim.spend_coins(ctx.take(), slice::from_ref(&parent_bls.sk))?;
232
233 let parent_conds = Conditions::new().create_coin(
234 P2ParentCoin::inner_puzzle_hash(Some(cats[0].info.asset_id)).into(),
235 1337,
236 ctx.memos(&server_list)?,
237 );
238 let parent_cat_inner_spend =
239 StandardLayer::new(parent_bls.pk).spend_with_conditions(&mut ctx, parent_conds)?;
240
241 let cats = Cat::spend_all(&mut ctx, &[CatSpend::new(cats[0], parent_cat_inner_spend)])?;
242
243 (
244 cats[0].coin,
245 Some(cats[0].info.asset_id),
246 cats[0].lineage_proof.unwrap(),
247 )
248 } else {
249 let parent_conds = Conditions::new().create_coin(
250 P2ParentCoin::puzzle_hash(None).into(),
251 1337,
252 ctx.memos(&server_list)?,
253 );
254 let parent_inner_spend =
255 StandardLayer::new(parent_bls.pk).spend_with_conditions(&mut ctx, parent_conds)?;
256
257 ctx.spend(parent_bls.coin, parent_inner_spend)?;
258
259 (
260 parent_bls.coin.make_child(
261 P2ParentCoin::puzzle_hash(None).into(),
262 parent_bls.coin.amount,
263 ),
264 None,
265 LineageProof {
266 parent_parent_coin_info: parent_bls.coin.parent_coin_info,
267 parent_inner_puzzle_hash: parent_bls.coin.puzzle_hash,
268 parent_amount: parent_bls.coin.amount,
269 },
270 )
271 };
272
273 let spends = ctx.take();
274 let launch_spend = spends.last().unwrap().clone();
275 benchmark.add_spends(
276 &mut ctx,
277 &mut sim,
278 spends,
279 "create",
280 slice::from_ref(&parent_bls.sk),
281 )?;
282
283 let parent_puzzle = ctx.alloc(&launch_spend.puzzle_reveal)?;
285 let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle);
286 let parent_solution = ctx.alloc(&launch_spend.solution)?;
287 let (p2_parent_coin, memos) =
288 P2ParentCoin::parse_child(&mut ctx, launch_spend.coin, parent_puzzle, parent_solution)?
289 .unwrap();
290
291 assert_eq!(
292 p2_parent_coin,
293 P2ParentCoin {
294 coin: expected_coin,
295 asset_id: expected_asset_id,
296 proof: expected_lp,
297 },
298 );
299 let Memos::Some(memos) = memos else {
300 panic!("Expected memos");
301 };
302 let memos = ctx.extract::<Vec<Bytes>>(memos)?;
303 assert_eq!(memos, server_list);
304
305 let new_coin_inner_puzzle_hash = Bytes32::new([0; 32]);
307 let new_coin = Coin::new(
308 p2_parent_coin.coin.coin_id(),
309 if cat_mode {
310 CatArgs::curry_tree_hash(
311 p2_parent_coin.asset_id.unwrap(),
312 new_coin_inner_puzzle_hash.into(),
313 )
314 .into()
315 } else {
316 new_coin_inner_puzzle_hash
317 },
318 p2_parent_coin.coin.amount,
319 );
320
321 let delegated_spend = StandardLayer::new(parent_bls.pk).spend_with_conditions(
322 &mut ctx,
323 Conditions::new().create_coin(new_coin_inner_puzzle_hash, new_coin.amount, Memos::None),
324 )?;
325 p2_parent_coin.spend(&mut ctx, delegated_spend, ())?;
326
327 let spends = ctx.take();
328 benchmark.add_spends(
329 &mut ctx,
330 &mut sim,
331 spends,
332 "spend",
333 slice::from_ref(&parent_bls.sk),
334 )?;
335
336 assert!(sim.coin_state(new_coin.coin_id()).is_some());
337
338 benchmark.print_summary(Some(&format!(
339 "p2-parent-coin-{}.costs",
340 if cat_mode { "cat" } else { "xch" }
341 )));
342
343 Ok(())
344 }
345}