Skip to main content

chia_sdk_driver/clear_signing/
children.rs

1use std::collections::VecDeque;
2
3use chia_protocol::{Bytes32, Coin};
4use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
5use chia_sdk_types::{Condition, conditions::CreateCoin};
6
7use crate::{
8    BURN_PUZZLE_HASH, Cat, DriverError, Facts, Nft, ParsedAsset, ParsedMemos, RevealedCoinSpend,
9    RevealedP2Puzzle, Reveals, SpendContext, parse_memos,
10};
11
12#[derive(Debug, Clone)]
13pub struct ParsedChild {
14    pub asset: ParsedAsset,
15    pub memos: ParsedMemos,
16    pub transfer_type: TransferType,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum TransferType {
21    Sent,
22    Burned,
23    Offered,
24    OfferPreSplit(OfferPreSplitInfo),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct OfferPreSplitInfo {
29    pub launcher_id: Bytes32,
30    pub nonce: usize,
31    pub fixed_conditions: Vec<Condition>,
32    pub settlement_amount: u64,
33}
34
35pub fn parse_children(
36    reveals: &mut Reveals,
37    facts: &mut Facts,
38    ctx: &mut SpendContext,
39    asset: &ParsedAsset,
40    spend: RevealedCoinSpend,
41    conditions: &[Condition],
42    is_claw_back: bool,
43) -> Result<Vec<ParsedChild>, DriverError> {
44    // Now, we should be able to assume that the conditions will be output if the transaction is valid.
45    let mut children = Vec::new();
46
47    // We should parse CAT children up front, so that we can hydrate their details when adding children later.
48    let mut cats = if matches!(asset, ParsedAsset::Cat(_)) {
49        if let Some(cats) = Cat::parse_children(ctx, spend.coin, spend.puzzle, spend.solution)? {
50            VecDeque::from(cats)
51        } else {
52            return Err(DriverError::MissingChild);
53        }
54    } else {
55        VecDeque::new()
56    };
57
58    for condition in conditions {
59        match condition {
60            Condition::AssertPuzzleAnnouncement(condition) => {
61                facts.assert_puzzle_announcement(condition.announcement_id);
62            }
63            Condition::AssertConcurrentSpend(condition) => {
64                facts.assert_spend(condition.coin_id);
65            }
66            // We shouldn't allow a claw back spend to say when the transaction will expire.
67            // Otherwise, it could pretend that it's impossible for the clawback to expire
68            // before the transaction expires, which would be a security vulnerability.
69            Condition::AssertBeforeSecondsAbsolute(condition) if !is_claw_back => {
70                facts.update_actual_expiration_time(condition.seconds);
71            }
72            Condition::ReserveFee(condition) => {
73                facts.add_reserved_fees(condition.amount);
74            }
75            Condition::CreateCoin(condition) => {
76                match &asset {
77                    // All XCH and bulletin children are considered to be XCH.
78                    // However, ephemeral bulletin children are hydrated later based on the spend.
79                    ParsedAsset::Xch(_) | ParsedAsset::Bulletin(_) => {
80                        let memos = parse_memos(reveals, ctx, *condition, false);
81                        let transfer_type =
82                            calculate_transfer_type(reveals, &memos, condition.amount);
83
84                        children.push(ParsedChild {
85                            asset: ParsedAsset::Xch(Coin::new(
86                                spend.coin.coin_id(),
87                                condition.puzzle_hash,
88                                condition.amount,
89                            )),
90                            memos,
91                            transfer_type,
92                        });
93                    }
94                    // For NFTs, even amount children are XCH coins. If the amount is odd, it's the
95                    // next NFT singleton coin in the lineage.
96                    ParsedAsset::Nft(_) => {
97                        if condition.amount % 2 == 1 {
98                            let Some(nft) =
99                                Nft::parse_child(ctx, spend.coin, spend.puzzle, spend.solution)?
100                            else {
101                                return Err(DriverError::MissingChild);
102                            };
103
104                            let memos = parse_memos(reveals, ctx, *condition, true);
105                            let transfer_type =
106                                calculate_transfer_type(reveals, &memos, condition.amount);
107
108                            children.push(ParsedChild {
109                                asset: ParsedAsset::Nft(nft),
110                                memos,
111                                transfer_type,
112                            });
113                        } else {
114                            let memos = parse_memos(reveals, ctx, *condition, false);
115                            let transfer_type =
116                                calculate_transfer_type(reveals, &memos, condition.amount);
117
118                            children.push(ParsedChild {
119                                asset: ParsedAsset::Xch(Coin::new(
120                                    spend.coin.coin_id(),
121                                    condition.puzzle_hash,
122                                    condition.amount,
123                                )),
124                                memos,
125                                transfer_type,
126                            });
127                        }
128                    }
129                    // CATs never output anything other than CAT children.
130                    ParsedAsset::Cat(parent) => {
131                        let cat = cats.pop_front().ok_or(DriverError::MissingChild)?;
132
133                        // This prevents an attack where someone tricks you into spending a CAT, sending it
134                        // back to you, and wrapping it in a revocation layer that they control.
135                        if cat.info.hidden_puzzle_hash != parent.info.hidden_puzzle_hash {
136                            return Err(DriverError::RevocableChild);
137                        }
138
139                        let memos = parse_memos(
140                            reveals,
141                            ctx,
142                            CreateCoin::new(
143                                cat.info.p2_puzzle_hash,
144                                condition.amount,
145                                condition.memos,
146                            ),
147                            true,
148                        );
149                        let transfer_type =
150                            calculate_transfer_type(reveals, &memos, condition.amount);
151
152                        children.push(ParsedChild {
153                            asset: ParsedAsset::Cat(cat),
154                            memos,
155                            transfer_type,
156                        });
157                    }
158                }
159            }
160            _ => {}
161        }
162    }
163
164    if !cats.is_empty() {
165        return Err(DriverError::MissingChild);
166    }
167
168    Ok(children)
169}
170
171fn calculate_transfer_type(
172    reveals: &Reveals,
173    memos: &ParsedMemos,
174    input_amount: u64,
175) -> TransferType {
176    if memos.p2_puzzle_hash == BURN_PUZZLE_HASH {
177        TransferType::Burned
178    } else if memos.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
179        TransferType::Offered
180    } else if memos.clawback.is_none()
181        && let Some(RevealedP2Puzzle::P2ConditionsOrSingleton(reveal)) =
182            reveals.p2_puzzle(memos.p2_puzzle_hash.into())
183        && let Some(fixed_conditions) = &memos.fixed_conditions
184    {
185        let mut reserved_fee = 0;
186
187        for condition in fixed_conditions {
188            if let Condition::ReserveFee(condition) = condition {
189                reserved_fee += condition.amount;
190            }
191        }
192
193        TransferType::OfferPreSplit(OfferPreSplitInfo {
194            launcher_id: reveal.launcher_id,
195            nonce: reveal.nonce,
196            fixed_conditions: fixed_conditions.clone(),
197            settlement_amount: input_amount.saturating_sub(reserved_fee),
198        })
199    } else {
200        TransferType::Sent
201    }
202}