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, ¬arized_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}