chia_sdk_driver/actions/
settle.rs

1use chia_protocol::Coin;
2use chia_puzzle_types::offer::NotarizedPayment;
3use chia_sdk_types::{payment_assertion, tree_hash_notarized_payment};
4
5use crate::{Deltas, DriverError, Id, SpendAction, SpendContext, SpendKind, Spends};
6
7#[derive(Debug, Clone)]
8pub struct SettleAction {
9    pub id: Id,
10    pub notarized_payment: NotarizedPayment,
11}
12
13impl SettleAction {
14    pub fn new(id: Id, notarized_payment: NotarizedPayment) -> Self {
15        Self {
16            id,
17            notarized_payment,
18        }
19    }
20}
21
22impl SpendAction for SettleAction {
23    fn calculate_delta(&self, deltas: &mut Deltas, _index: usize) {
24        let amount: u64 = self
25            .notarized_payment
26            .payments
27            .iter()
28            .map(|p| p.amount)
29            .sum();
30
31        deltas.update(self.id).output += amount;
32        deltas.set_needed(self.id);
33    }
34
35    fn spend(
36        &self,
37        ctx: &mut SpendContext,
38        spends: &mut Spends,
39        _index: usize,
40    ) -> Result<(), DriverError> {
41        if matches!(self.id, Id::Xch) {
42            let source = spends
43                .xch
44                .notarized_payment_source(&self.notarized_payment)?;
45
46            let parent = &mut spends.xch.items[source];
47
48            if let SpendKind::Settlement(spend) = &mut parent.kind {
49                spends.xch.payment_assertions.push(payment_assertion(
50                    parent.asset.puzzle_hash,
51                    tree_hash_notarized_payment(ctx, &self.notarized_payment),
52                ));
53
54                spend.add_notarized_payment(self.notarized_payment.clone());
55            } else {
56                return Err(DriverError::CannotSettleFromSpend);
57            }
58
59            for payment in &self.notarized_payment.payments {
60                let coin = Coin::new(parent.asset.coin_id(), payment.puzzle_hash, payment.amount);
61                spends.outputs.xch.push(coin);
62            }
63        } else if let Some(cat) = spends.cats.get_mut(&self.id) {
64            let source = cat.notarized_payment_source(&self.notarized_payment)?;
65            let parent = &mut cat.items[source];
66
67            if let SpendKind::Settlement(spend) = &mut parent.kind {
68                cat.payment_assertions.push(payment_assertion(
69                    parent.asset.coin.puzzle_hash,
70                    tree_hash_notarized_payment(ctx, &self.notarized_payment),
71                ));
72
73                spend.add_notarized_payment(self.notarized_payment.clone());
74            } else {
75                return Err(DriverError::CannotSettleFromSpend);
76            }
77
78            for payment in &self.notarized_payment.payments {
79                let cat = parent.asset.child(payment.puzzle_hash, payment.amount);
80                spends.outputs.cats.entry(self.id).or_default().push(cat);
81            }
82        } else if let Some(nft) = spends.nfts.get_mut(&self.id) {
83            let index = nft.last_or_create_settlement(ctx)?;
84            let source = &mut nft.lineage[index];
85
86            if let SpendKind::Settlement(spend) = &mut source.kind {
87                source.payment_assertions.push(payment_assertion(
88                    source.asset.coin.puzzle_hash,
89                    tree_hash_notarized_payment(ctx, &self.notarized_payment),
90                ));
91
92                spend.add_notarized_payment(self.notarized_payment.clone());
93            } else {
94                return Err(DriverError::CannotSettleFromSpend);
95            }
96
97            for payment in &self.notarized_payment.payments {
98                if payment.amount % 2 == 0 {
99                    let coin = Coin::new(
100                        source.asset.coin.coin_id(),
101                        payment.puzzle_hash,
102                        payment.amount,
103                    );
104                    spends.outputs.xch.push(coin);
105                    continue;
106                }
107
108                let nft = source.asset.child(
109                    payment.puzzle_hash,
110                    source.asset.info.current_owner,
111                    source.asset.info.metadata,
112                    payment.amount,
113                );
114                spends.outputs.nfts.insert(self.id, nft);
115            }
116        } else if let Some(option) = spends.options.get_mut(&self.id) {
117            let index = option.last_or_create_settlement(ctx)?;
118            let source = &mut option.lineage[index];
119
120            if let SpendKind::Settlement(spend) = &mut source.kind {
121                source.payment_assertions.push(payment_assertion(
122                    source.asset.coin.puzzle_hash,
123                    tree_hash_notarized_payment(ctx, &self.notarized_payment),
124                ));
125
126                spend.add_notarized_payment(self.notarized_payment.clone());
127            } else {
128                return Err(DriverError::CannotSettleFromSpend);
129            }
130
131            for payment in &self.notarized_payment.payments {
132                if payment.amount % 2 == 0 {
133                    let coin = Coin::new(
134                        source.asset.coin.coin_id(),
135                        payment.puzzle_hash,
136                        payment.amount,
137                    );
138                    spends.outputs.xch.push(coin);
139                    continue;
140                }
141
142                let option = source.asset.child(payment.puzzle_hash, payment.amount);
143                spends.outputs.options.insert(self.id, option);
144            }
145        } else {
146            return Err(DriverError::InvalidAssetId);
147        }
148
149        Ok(())
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use anyhow::Result;
156    use chia_protocol::{Bytes32, Coin};
157    use chia_puzzle_types::{offer::Payment, Memos};
158    use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
159    use chia_sdk_test::Simulator;
160    use chia_sdk_types::{payment_assertion, tree_hash_notarized_payment, Conditions};
161    use indexmap::indexmap;
162
163    use crate::{Action, Relation, StandardLayer, BURN_PUZZLE_HASH};
164
165    use super::*;
166
167    #[test]
168    fn test_action_settle_xch() -> Result<()> {
169        let mut sim = Simulator::new();
170        let mut ctx = SpendContext::new();
171
172        let alice = sim.bls(2);
173
174        let notarized_payment = NotarizedPayment::new(
175            Bytes32::new([42; 32]),
176            vec![Payment::new(BURN_PUZZLE_HASH, 1, Memos::None)],
177        );
178        let hashed_notarized_payment = tree_hash_notarized_payment(&ctx, &notarized_payment);
179
180        let mut spends = Spends::new(alice.puzzle_hash);
181        spends.add(alice.coin);
182
183        let deltas = spends.apply(
184            &mut ctx,
185            &[
186                Action::send(Id::Xch, alice.puzzle_hash, 0, Memos::None),
187                Action::send(Id::Xch, SETTLEMENT_PAYMENT_HASH.into(), 1, Memos::None),
188            ],
189        )?;
190
191        let outputs = spends.finish_with_keys(
192            &mut ctx,
193            &deltas,
194            Relation::None,
195            &indexmap! { alice.puzzle_hash => alice.pk },
196        )?;
197
198        let mut spends = Spends::new(alice.puzzle_hash);
199        spends.add(
200            outputs
201                .xch
202                .iter()
203                .find(|c| c.puzzle_hash == SETTLEMENT_PAYMENT_HASH.into())
204                .copied()
205                .expect("settlement coin not found"),
206        );
207
208        let deltas = spends.apply(&mut ctx, &[Action::settle(Id::Xch, notarized_payment)])?;
209
210        let outputs = spends.finish_with_keys(
211            &mut ctx,
212            &deltas,
213            Relation::None,
214            &indexmap! { alice.puzzle_hash => alice.pk },
215        )?;
216        assert_eq!(outputs.xch.len(), 1);
217
218        StandardLayer::new(alice.pk).spend(
219            &mut ctx,
220            Coin::new(alice.coin.coin_id(), alice.puzzle_hash, 0),
221            Conditions::new().with(payment_assertion(
222                SETTLEMENT_PAYMENT_HASH.into(),
223                hashed_notarized_payment,
224            )),
225        )?;
226
227        sim.spend_coins(ctx.take(), &[alice.sk])?;
228
229        Ok(())
230    }
231}