1use std::collections::{HashMap, HashSet};
2
3use chia_protocol::Bytes32;
4use chia_puzzle_types::cat::CatSolution;
5use chia_sdk_types::{Condition, Mod, puzzles::SingletonMember};
6use clvm_traits::{FromClvm, ToClvm, clvm_quote};
7use clvm_utils::tree_hash;
8use clvmr::NodePtr;
9use indexmap::{IndexMap, IndexSet};
10
11use crate::{
12 AssertedNotarizedPayment, AssertedPayment, ClawbackInfo, ClearSigningAsset, CustodyInfo,
13 DriverError, DropCoin, Facts, Issuance, IssuanceKind, LinkedOffer, P2ConditionsOrSingletonInfo,
14 P2SingletonInfo, ParsedAsset, ParsedChild, ParsedSpend, RevealedCoinSpend, Reveals, Spend,
15 SpendContext, TransferType, VaultMessage, VaultOutput, build_linked_offer,
16 get_extra_delta_message, mips_puzzle_hash, parse_asserted_requested_payments, parse_children,
17 parse_run_cat_tail, parse_spend, parse_vault_delegated_spend, split_asserted_payments,
18};
19
20#[derive(Debug, Clone)]
24pub struct VaultTransaction {
25 pub vault_child: Option<VaultOutput>,
30 pub drop_coins: Vec<DropCoin>,
32 pub spends: Vec<VerifiedSpend>,
34 pub issuances: Vec<Issuance>,
38 pub asserted_payments: Vec<AssertedNotarizedPayment>,
40 pub received_payments: Vec<AssertedPayment>,
42 pub external_payments: Vec<AssertedPayment>,
45 pub asset_flows: Vec<AssetFlow>,
47 pub linked_offer: Option<LinkedOffer>,
53 pub reserved_fee: u64,
55 pub p2_puzzle_hashes: Vec<Bytes32>,
57 pub delegated_puzzle_hash: Bytes32,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct AssetFlow {
63 pub asset: ClearSigningAsset,
64 pub input_amount: u64,
65 pub output_amount: u64,
66 pub issued_amount: u64,
67 pub melted_amount: u64,
68 pub received_amount: u64,
69 pub paid_amount: u64,
70 pub unaccounted_amount: u64,
71}
72
73#[derive(Debug, Clone)]
74pub struct VerifiedSpend {
75 pub asset: ParsedAsset,
76 pub clawback: Option<ClawbackInfo>,
77 pub custody: CustodyInfo,
78 pub children: Vec<ParsedChild>,
79 pub revoked: bool,
80}
81
82pub fn parse_vault_transaction(
83 mut reveals: Reveals,
84 ctx: &mut SpendContext,
85 launcher_id: Bytes32,
86 delegated_spend: Spend,
87) -> Result<VaultTransaction, DriverError> {
88 let mut facts = Facts::default();
89
90 let vault_spend = parse_vault_delegated_spend(&mut facts, ctx, delegated_spend)?;
91
92 let mut parsed_spends = HashMap::new();
93
94 for spend in reveals.coin_spends().copied().collect::<Vec<_>>() {
95 let parsed_spend = parse_spend(&mut reveals, ctx, &spend)?;
96
97 if let Some(
98 CustodyInfo::P2Singleton(P2SingletonInfo {
99 launcher_id: spend_launcher_id,
100 ..
101 })
102 | CustodyInfo::P2ConditionsOrSingleton(P2ConditionsOrSingletonInfo {
103 launcher_id: spend_launcher_id,
104 ..
105 }),
106 ) = &parsed_spend.custody
107 && spend_launcher_id != &launcher_id
108 {
109 continue;
110 }
111
112 parsed_spends.insert(spend.coin.coin_id(), parsed_spend);
113 }
114
115 let mut messages_by_coin: IndexMap<Bytes32, Vec<VaultMessage>> = IndexMap::new();
116 for message in vault_spend.messages {
117 messages_by_coin
118 .entry(message.spent_coin_id)
119 .or_default()
120 .push(message);
121 }
122
123 let mut verified_spends = Vec::new();
124 let mut issuances: Vec<Issuance> = Vec::new();
125
126 for (coin_id, messages) in messages_by_coin {
127 let spend = *reveals
128 .coin_spend(coin_id)
129 .ok_or(DriverError::MissingSpend)?;
130 let parsed_spend = parsed_spends
131 .remove(&coin_id)
132 .ok_or(DriverError::MissingSpend)?;
133
134 let Some(verified_spend) = verify_spend(
135 &mut reveals,
136 &mut facts,
137 ctx,
138 spend,
139 parsed_spend,
140 &messages,
141 &mut issuances,
142 )?
143 else {
144 return Err(DriverError::InvalidLinkedCustody);
145 };
146
147 verified_spends.push(verified_spend);
148 }
149
150 let mut stack: IndexSet<Bytes32> = verified_spends
151 .iter()
152 .flat_map(|spend| {
153 spend
154 .children
155 .iter()
156 .map(|child| child.asset.coin().coin_id())
157 })
158 .collect();
159
160 while let Some(coin_id) = stack.pop() {
161 if !facts.is_spend_asserted(coin_id) {
162 continue;
163 }
164
165 let Some(parsed_spend) = parsed_spends.remove(&coin_id) else {
166 continue;
167 };
168
169 let spend = *reveals
170 .coin_spend(coin_id)
171 .ok_or(DriverError::MissingSpend)?;
172
173 let Some(verified_spend) = verify_spend(
174 &mut reveals,
175 &mut facts,
176 ctx,
177 spend,
178 parsed_spend,
179 &[],
180 &mut issuances,
181 )?
182 else {
183 continue;
184 };
185
186 for child in &verified_spend.children {
187 stack.insert(child.asset.coin().coin_id());
188 }
189
190 verified_spends.push(verified_spend);
191 }
192
193 if facts.required_expiration_time().is_some_and(|required| {
197 facts
198 .actual_expiration_time()
199 .is_none_or(|expiration| expiration > required)
200 }) {
201 return Err(DriverError::UnguaranteedClawBack);
202 }
203
204 let delegated_puzzle_hash = tree_hash(ctx, delegated_spend.puzzle).into();
205
206 let p2_puzzle_hashes = calculate_p2_puzzle_hashes(&reveals, launcher_id);
207 let p2_puzzle_hash_set = p2_puzzle_hashes.iter().copied().collect();
208 let reserved_fee = facts.reserved_fees().try_into()?;
209 let asserted_payments = parse_asserted_requested_payments(&reveals, &facts, ctx)?;
210 let split_payments = split_asserted_payments(
211 &asserted_payments,
212 &p2_puzzle_hash_set,
213 reveals.asset_info(),
214 );
215 let linked_offer = build_linked_offer(
216 &reveals,
217 ctx,
218 &verified_spends,
219 launcher_id,
220 &p2_puzzle_hash_set,
221 )?;
222
223 let mut bulletins = HashMap::new();
225
226 for spend in &verified_spends {
227 if let ParsedAsset::Bulletin(bulletin) = &spend.asset {
228 bulletins.insert(bulletin.coin.coin_id(), bulletin.clone());
229 }
230 }
231
232 for spend in &mut verified_spends {
233 for child in &mut spend.children {
234 if matches!(child.asset, ParsedAsset::Xch(_))
235 && let Some(bulletin) = bulletins.remove(&child.asset.coin().coin_id())
236 {
237 child.asset = ParsedAsset::Bulletin(bulletin);
238 }
239 }
240 }
241
242 let asset_flows = build_asset_flows(
243 &verified_spends,
244 &split_payments.received_payments,
245 &split_payments.external_payments,
246 &vault_spend.drop_coins,
247 &issuances,
248 reserved_fee,
249 );
250
251 Ok(VaultTransaction {
252 vault_child: vault_spend.child,
253 drop_coins: vault_spend.drop_coins,
254 spends: verified_spends,
255 issuances,
256 asserted_payments,
257 received_payments: split_payments.received_payments,
258 external_payments: split_payments.external_payments,
259 asset_flows,
260 linked_offer,
261 reserved_fee,
262 p2_puzzle_hashes,
263 delegated_puzzle_hash,
264 })
265}
266
267fn verify_spend(
268 reveals: &mut Reveals,
269 facts: &mut Facts,
270 allocator: &mut SpendContext,
271 spend: RevealedCoinSpend,
272 parsed_spend: ParsedSpend,
273 messages: &[VaultMessage],
274 issuances: &mut Vec<Issuance>,
275) -> Result<Option<VerifiedSpend>, DriverError> {
276 let Some(custody) = parsed_spend.custody else {
277 return Ok(None);
278 };
279
280 let conditions: &[Condition] = match &custody {
281 CustodyInfo::P2Singleton(P2SingletonInfo { conditions, .. })
282 | CustodyInfo::P2ConditionsOrSingleton(P2ConditionsOrSingletonInfo {
283 conditions, ..
284 })
285 | CustodyInfo::DelegatedConditions(conditions) => conditions,
286 };
287
288 if matches!(&custody, CustodyInfo::P2ConditionsOrSingleton(_))
289 && !conditions.contains(&Condition::assert_my_coin_id(spend.coin.coin_id()))
290 {
291 return Err(DriverError::MissingP2ConditionsOrSingletonAssertion);
292 }
293
294 if messages.is_empty() && custody.receives_message() {
295 return Err(DriverError::MissingVaultMessage);
296 }
297
298 let conditions_hash = if custody.receives_message() {
299 let delegated_puzzle = clvm_quote!(conditions).to_clvm(allocator)?;
300 Some(tree_hash(allocator, delegated_puzzle))
301 } else {
302 None
303 };
304
305 let run_cat_tail = if matches!(parsed_spend.asset, ParsedAsset::Cat(_)) {
306 parse_run_cat_tail(allocator, conditions)?
307 } else {
308 None
309 };
310
311 let issuance = if let Some(run_cat_tail) = run_cat_tail {
312 let cat_solution = CatSolution::<NodePtr>::from_clvm(allocator, spend.solution)?;
313
314 Some(Issuance {
315 coin_id: spend.coin.coin_id(),
316 asset_id: run_cat_tail.asset_id,
317 extra_delta: cat_solution.extra_delta,
318 kind: run_cat_tail.kind,
319 })
320 } else {
321 None
322 };
323
324 let mut tail_matched = false;
325 let mut custody_matched = false;
326
327 for message in messages {
328 if let Some(hash) = conditions_hash
329 && message.data.as_ref() == hash.as_ref()
330 {
331 if custody_matched {
332 return Err(DriverError::DuplicateVaultMessage);
333 }
334
335 custody_matched = true;
336 } else if let Some(issuance) = &issuance
337 && matches!(issuance.kind, IssuanceKind::EverythingWithSingleton { .. })
338 && message.data == get_extra_delta_message(issuance.extra_delta)
339 {
340 if tail_matched {
341 return Err(DriverError::DuplicateVaultMessage);
342 }
343
344 tail_matched = true;
345 } else {
346 return Err(DriverError::UnmatchedVaultMessage);
347 }
348 }
349
350 if custody.receives_message() && !custody_matched {
351 return Err(DriverError::WrongConditions);
352 }
353
354 if let Some(time) = parsed_spend.required_expiration_time {
355 facts.update_required_expiration_time(time);
356 }
357
358 let children = parse_children(
359 reveals,
360 facts,
361 allocator,
362 &parsed_spend.asset,
363 spend,
364 conditions,
365 parsed_spend.required_expiration_time.is_some(),
366 )?;
367
368 if let Some(issuance) = issuance {
372 issuances.push(issuance);
373 }
374
375 Ok(Some(VerifiedSpend {
376 asset: parsed_spend.asset,
377 clawback: parsed_spend.clawback,
378 custody,
379 children,
380 revoked: parsed_spend.revoked,
381 }))
382}
383
384#[derive(Debug, Clone)]
385struct AssetFlowTotals {
386 asset: ClearSigningAsset,
387 input_amount: u64,
388 output_amount: u64,
389 issued_amount: u64,
390 melted_amount: u64,
391 received_amount: u64,
392 paid_amount: u64,
393}
394
395fn build_asset_flows(
396 spends: &[VerifiedSpend],
397 received_payments: &[AssertedPayment],
398 external_payments: &[AssertedPayment],
399 drop_coins: &[DropCoin],
400 issuances: &[Issuance],
401 reserved_fee: u64,
402) -> Vec<AssetFlow> {
403 let child_coin_ids: HashSet<Bytes32> = spends
404 .iter()
405 .flat_map(|spend| spend.children.iter())
406 .map(|child| child.asset.coin().coin_id())
407 .collect();
408 let spend_by_coin_id: HashMap<Bytes32, &VerifiedSpend> = spends
409 .iter()
410 .map(|spend| (spend.asset.coin().coin_id(), spend))
411 .collect();
412 let xch_child_coin_ids: HashSet<Bytes32> = spends
413 .iter()
414 .filter(|spend| matches!(spend.asset, ParsedAsset::Xch(_) | ParsedAsset::Bulletin(_)))
415 .flat_map(|spend| spend.children.iter())
416 .map(|child| child.asset.coin().coin_id())
417 .collect();
418
419 let mut flows = IndexMap::<Option<Bytes32>, AssetFlowTotals>::new();
420
421 for spend in spends {
422 if !child_coin_ids.contains(&spend.asset.coin().coin_id()) {
423 asset_flow_mut(&mut flows, asset_from_parsed(&spend.asset)).input_amount +=
424 spend.asset.coin().amount;
425 }
426
427 for child in &spend.children {
428 if !spend_by_coin_id.contains_key(&child.asset.coin().coin_id())
429 && child.transfer_type != TransferType::Offered
430 {
431 asset_flow_mut(&mut flows, asset_from_parsed(&child.asset)).output_amount +=
432 child.asset.coin().amount;
433 }
434 }
435 }
436
437 for asserted_payment in received_payments {
438 asset_flow_mut(&mut flows, asserted_payment.asset).received_amount +=
439 asserted_payment.payment.amount;
440 }
441
442 for asserted_payment in external_payments {
443 asset_flow_mut(&mut flows, asserted_payment.asset).paid_amount +=
444 asserted_payment.payment.amount;
445 }
446
447 for drop_coin in drop_coins {
448 asset_flow_mut(&mut flows, ClearSigningAsset::Xch).output_amount += drop_coin.amount;
449 }
450
451 for spend in spends {
452 if matches!(spend.asset, ParsedAsset::Nft(_))
453 && xch_child_coin_ids.contains(&spend.asset.coin().parent_coin_info)
454 {
455 asset_flow_mut(&mut flows, ClearSigningAsset::Xch).melted_amount +=
456 spend.asset.coin().amount;
457 }
458 }
459
460 for issuance in issuances {
461 let Some(spend) = spend_by_coin_id.get(&issuance.coin_id) else {
462 continue;
463 };
464
465 let ParsedAsset::Cat(cat) = &spend.asset else {
466 continue;
467 };
468
469 if cat.info.asset_id != issuance.asset_id {
470 continue;
471 }
472
473 let cat_asset = asset_from_parsed(&spend.asset);
474
475 if xch_child_coin_ids.contains(&issuance.coin_id) {
476 let amount = spend.asset.coin().amount;
477 asset_flow_mut(&mut flows, cat_asset).issued_amount += amount;
478 asset_flow_mut(&mut flows, ClearSigningAsset::Xch).melted_amount += amount;
479 }
480
481 if issuance.extra_delta > 0 {
482 let amount = u64::try_from(issuance.extra_delta).unwrap();
483 asset_flow_mut(&mut flows, cat_asset).issued_amount += amount;
484 asset_flow_mut(&mut flows, ClearSigningAsset::Xch).melted_amount += amount;
485 } else if issuance.extra_delta < 0 {
486 let amount = u64::try_from(-issuance.extra_delta).unwrap();
487 asset_flow_mut(&mut flows, cat_asset).melted_amount += amount;
488 asset_flow_mut(&mut flows, ClearSigningAsset::Xch).issued_amount += amount;
489 }
490 }
491
492 flows
493 .into_values()
494 .filter_map(|flow| {
495 let unaccounted_amount = flow
496 .input_amount
497 .saturating_add(flow.issued_amount)
498 .saturating_sub(flow.output_amount)
499 .saturating_sub(flow.melted_amount)
500 .saturating_sub(flow.paid_amount)
501 .saturating_sub(if matches!(flow.asset, ClearSigningAsset::Xch) {
502 reserved_fee
503 } else {
504 0
505 });
506
507 let include = flow.input_amount > 0
508 || flow.output_amount > 0
509 || flow.received_amount > 0
510 || flow.paid_amount > 0
511 || flow.issued_amount > 0
512 || flow.melted_amount > 0;
513
514 include.then_some((flow, unaccounted_amount))
515 })
516 .map(|(flow, unaccounted_amount)| AssetFlow {
517 asset: flow.asset,
518 input_amount: flow.input_amount,
519 output_amount: flow.output_amount,
520 issued_amount: flow.issued_amount,
521 melted_amount: flow.melted_amount,
522 received_amount: flow.received_amount,
523 paid_amount: flow.paid_amount,
524 unaccounted_amount,
525 })
526 .collect()
527}
528
529fn asset_flow_mut(
530 flows: &mut IndexMap<Option<Bytes32>, AssetFlowTotals>,
531 asset: ClearSigningAsset,
532) -> &mut AssetFlowTotals {
533 flows
534 .entry(asset_flow_key(asset))
535 .or_insert_with(|| AssetFlowTotals {
536 asset,
537 input_amount: 0,
538 output_amount: 0,
539 issued_amount: 0,
540 melted_amount: 0,
541 received_amount: 0,
542 paid_amount: 0,
543 })
544}
545
546fn asset_flow_key(asset: ClearSigningAsset) -> Option<Bytes32> {
547 match asset {
548 ClearSigningAsset::Xch => None,
549 ClearSigningAsset::Cat { asset_id, .. }
550 | ClearSigningAsset::Nft {
551 launcher_id: asset_id,
552 ..
553 } => Some(asset_id),
554 }
555}
556
557fn asset_from_parsed(asset: &ParsedAsset) -> ClearSigningAsset {
558 match asset {
559 ParsedAsset::Xch(_) | ParsedAsset::Bulletin(_) => ClearSigningAsset::Xch,
560 ParsedAsset::Cat(cat) => ClearSigningAsset::Cat {
561 asset_id: cat.info.asset_id,
562 hidden_puzzle_hash: cat.info.hidden_puzzle_hash,
563 },
564 ParsedAsset::Nft(nft) => ClearSigningAsset::Nft {
565 launcher_id: nft.info.launcher_id,
566 metadata: nft.info.metadata,
567 metadata_updater_puzzle_hash: nft.info.metadata_updater_puzzle_hash,
568 royalty_puzzle_hash: nft.info.royalty_puzzle_hash,
569 royalty_basis_points: nft.info.royalty_basis_points,
570 },
571 }
572}
573
574fn calculate_p2_puzzle_hashes(reveals: &Reveals, launcher_id: Bytes32) -> Vec<Bytes32> {
575 let mut p2_puzzle_hashes = Vec::new();
576
577 for nonce in reveals.vault_nonces() {
578 p2_puzzle_hashes.push(
579 mips_puzzle_hash(
580 nonce,
581 vec![],
582 SingletonMember::new(launcher_id).curry_tree_hash(),
583 true,
584 )
585 .into(),
586 );
587 }
588
589 p2_puzzle_hashes
590}
591
592pub fn iter_final_children(spends: &[VerifiedSpend]) -> impl Iterator<Item = &ParsedChild> {
593 let spent_coin_ids: HashSet<Bytes32> = spends
594 .iter()
595 .map(|spend| spend.asset.coin().coin_id())
596 .collect();
597
598 spends
599 .iter()
600 .flat_map(|spend| spend.children.iter())
601 .filter(move |child| !spent_coin_ids.contains(&child.asset.coin().coin_id()))
602}