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