1use std::collections::HashSet;
2
3use chia_protocol::{Bytes32, Coin, CoinSpend};
4use chia_puzzle_types::{
5 Memos,
6 nft::NftMetadata,
7 offer::{NotarizedPayment, SettlementPaymentsSolution},
8};
9use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
10use chia_sdk_types::{
11 Condition, MessageFlags, MessageSide, Mod, announcement_id, conditions::CreateCoin,
12 puzzles::SingletonMember, run_puzzle, tree_hash_notarized_payment,
13};
14use clvm_traits::{FromClvm, ToClvm};
15use clvm_utils::TreeHash;
16use clvmr::{Allocator, NodePtr};
17
18use crate::{
19 BURN_PUZZLE_HASH, Cat, ClawbackV2, DriverError, MetadataUpdate, Nft, Puzzle, Spend, UriKind,
20 mips_puzzle_hash,
21};
22
23#[derive(Debug, Clone, Copy)]
25pub struct VaultSpendReveal {
26 pub launcher_id: Bytes32,
29 pub custody_hash: TreeHash,
32 pub delegated_spend: Spend,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct VaultTransaction {
42 pub new_custody_hash: Option<TreeHash>,
46 pub payments: Vec<ParsedPayment>,
48 pub nfts: Vec<ParsedNftTransfer>,
50 pub drop_coins: Vec<DropCoin>,
52 pub fee_paid: u64,
56 pub total_fee: u64,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ParsedPayment {
63 pub transfer_type: TransferType,
65 pub asset_id: Option<Bytes32>,
67 pub hidden_puzzle_hash: Option<Bytes32>,
69 pub p2_puzzle_hash: Bytes32,
71 pub coin: Coin,
74 pub clawback: Option<ClawbackV2>,
76 pub memos: Vec<String>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ParsedNftTransfer {
82 pub transfer_type: TransferType,
84 pub launcher_id: Bytes32,
86 pub p2_puzzle_hash: Bytes32,
88 pub coin: Coin,
91 pub clawback: Option<ClawbackV2>,
93 pub memos: Vec<String>,
95 pub new_uris: Vec<MetadataUpdate>,
97 pub latest_owner: Option<Bytes32>,
99 pub includes_unverifiable_updates: bool,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum TransferType {
105 Sent,
109 Burned,
111 Offered,
113 Received,
117 Updated,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct DropCoin {
124 pub puzzle_hash: Bytes32,
125 pub amount: u64,
126}
127
128impl VaultTransaction {
129 pub fn parse(
130 allocator: &mut Allocator,
131 vault: &VaultSpendReveal,
132 coin_spends: Vec<CoinSpend>,
133 ) -> Result<Self, DriverError> {
134 let our_p2_puzzle_hash = vault_p2_puzzle_hash(vault.launcher_id);
135
136 let all_spent_coin_ids = coin_spends.iter().map(|cs| cs.coin.coin_id()).collect();
137
138 let ParsedDelegatedSpend {
139 new_custody_hash,
140 our_spent_coin_ids,
141 puzzle_assertion_ids,
142 drop_coins,
143 } = parse_delegated_spend(allocator, vault.delegated_spend, &all_spent_coin_ids)?;
144
145 let ParsedConditions {
146 puzzle_assertion_ids,
147 all_created_coin_ids,
148 } = parse_our_conditions(
149 allocator,
150 &coin_spends,
151 &our_spent_coin_ids,
152 puzzle_assertion_ids,
153 )?;
154
155 let coin_spends = reorder_coin_spends(coin_spends);
156
157 let mut payments = Vec::new();
158 let mut nfts = Vec::new();
159 let mut our_input = 0;
160 let mut our_output = 0;
161 let mut total_input = 0;
162 let mut total_output = 0;
163
164 for coin_spend in coin_spends {
165 let coin_id = coin_spend.coin.coin_id();
166 let is_parent_ours = our_spent_coin_ids.contains(&coin_id);
167 let is_parent_ephemeral = all_created_coin_ids.contains(&coin_id);
168
169 total_input += coin_spend.coin.amount;
170
171 if is_parent_ours && !is_parent_ephemeral {
172 our_input += coin_spend.coin.amount;
173 }
174
175 let puzzle = coin_spend.puzzle_reveal.to_clvm(allocator)?;
176 let puzzle = Puzzle::parse(allocator, puzzle);
177 let solution = coin_spend.solution.to_clvm(allocator)?;
178
179 let output = run_puzzle(allocator, puzzle.ptr(), solution)?;
180 let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
181
182 if let Some((cat, p2_puzzle, p2_solution)) =
183 Cat::parse(allocator, coin_spend.coin, puzzle, solution)?
184 {
185 let p2_output = run_puzzle(allocator, p2_puzzle.ptr(), p2_solution)?;
186
187 let mut p2_create_coins = Vec::<Condition>::from_clvm(allocator, p2_output)?
188 .into_iter()
189 .filter_map(Condition::into_create_coin)
190 .collect::<Vec<_>>();
191
192 let children = Cat::parse_children(allocator, coin_spend.coin, puzzle, solution)?
193 .unwrap_or_default();
194
195 let notarized_payments =
196 if cat.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
197 SettlementPaymentsSolution::from_clvm(allocator, p2_solution)?
198 .notarized_payments
199 } else {
200 Vec::new()
201 };
202
203 for child in children {
204 let child_coin_id = child.coin.coin_id();
205 let create_coin = p2_create_coins.remove(0);
206 let parsed_memos = parse_memos(allocator, create_coin, true);
207 let is_child_ours = parsed_memos.p2_puzzle_hash == our_p2_puzzle_hash;
208 let is_child_ephemeral = all_spent_coin_ids.contains(&child_coin_id);
209
210 total_output += child.coin.amount;
211
212 if is_parent_ours && !is_child_ephemeral {
213 our_output += child.coin.amount;
214 }
215
216 if our_spent_coin_ids.contains(&child_coin_id) {
218 continue;
219 }
220
221 if let Some(transfer_type) = calculate_transfer_type(
222 allocator,
223 TransferTypeContext {
224 puzzle_assertion_ids: &puzzle_assertion_ids,
225 notarized_payments: ¬arized_payments,
226 create_coin: &create_coin,
227 p2_puzzle_hash: parsed_memos.p2_puzzle_hash,
228 full_puzzle_hash: cat.coin.puzzle_hash,
229 is_parent_ours,
230 is_child_ours,
231 },
232 ) {
233 payments.push(ParsedPayment {
234 transfer_type,
235 asset_id: Some(child.info.asset_id),
236 hidden_puzzle_hash: child.info.hidden_puzzle_hash,
237 p2_puzzle_hash: parsed_memos.p2_puzzle_hash,
238 coin: child.coin,
239 clawback: parsed_memos.clawback,
240 memos: parsed_memos.memos,
241 });
242 }
243 }
244
245 continue;
246 }
247
248 let mut is_singleton = false;
249
250 if let Some((nft, p2_puzzle, p2_solution)) =
251 Nft::parse(allocator, coin_spend.coin, puzzle, solution)?
252 {
253 is_singleton = true;
254
255 let p2_output = run_puzzle(allocator, p2_puzzle.ptr(), p2_solution)?;
256
257 let mut p2_create_coins = Vec::<Condition>::from_clvm(allocator, p2_output)?
258 .into_iter()
259 .filter_map(Condition::into_create_coin)
260 .filter(|cc| cc.amount % 2 == 1)
261 .collect::<Vec<_>>();
262
263 let child = Nft::parse_child(allocator, coin_spend.coin, puzzle, solution)?
264 .ok_or(DriverError::MissingChild)?;
265
266 let notarized_payments =
267 if nft.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
268 SettlementPaymentsSolution::from_clvm(allocator, p2_solution)?
269 .notarized_payments
270 } else {
271 Vec::new()
272 };
273
274 let child_coin_id = child.coin.coin_id();
275 let is_child_ephemeral = all_spent_coin_ids.contains(&child_coin_id);
276 let create_coin = p2_create_coins.remove(0);
277 let parsed_memos = parse_memos(allocator, create_coin, true);
278 let is_child_ours = parsed_memos.p2_puzzle_hash == our_p2_puzzle_hash;
279
280 total_output += child.coin.amount;
281
282 if is_parent_ours && !is_child_ephemeral {
283 our_output += child.coin.amount;
284 }
285
286 if our_spent_coin_ids.contains(&child.coin.coin_id()) {
288 continue;
289 }
290
291 if let Some(transfer_type) = calculate_transfer_type(
292 allocator,
293 TransferTypeContext {
294 puzzle_assertion_ids: &puzzle_assertion_ids,
295 notarized_payments: ¬arized_payments,
296 create_coin: &create_coin,
297 p2_puzzle_hash: parsed_memos.p2_puzzle_hash,
298 full_puzzle_hash: nft.coin.puzzle_hash,
299 is_parent_ours,
300 is_child_ours,
301 },
302 ) {
303 let mut includes_unverifiable_updates = false;
304
305 let new_uris = if let Ok(old_metadata) =
306 NftMetadata::from_clvm(allocator, nft.info.metadata.ptr())
307 && let Ok(new_metadata) =
308 NftMetadata::from_clvm(allocator, child.info.metadata.ptr())
309 {
310 let mut new_uris = Vec::new();
311
312 for uri in new_metadata.data_uris {
313 if !old_metadata.data_uris.contains(&uri) {
314 new_uris.push(MetadataUpdate {
315 kind: UriKind::Data,
316 uri,
317 });
318 }
319 }
320
321 for uri in new_metadata.metadata_uris {
322 if !old_metadata.metadata_uris.contains(&uri) {
323 new_uris.push(MetadataUpdate {
324 kind: UriKind::Metadata,
325 uri,
326 });
327 }
328 }
329
330 for uri in new_metadata.license_uris {
331 if !old_metadata.license_uris.contains(&uri) {
332 new_uris.push(MetadataUpdate {
333 kind: UriKind::License,
334 uri,
335 });
336 }
337 }
338
339 new_uris
340 } else {
341 includes_unverifiable_updates |= nft.info.metadata != child.info.metadata;
342
343 vec![]
344 };
345
346 nfts.push(ParsedNftTransfer {
347 transfer_type,
348 launcher_id: child.info.launcher_id,
349 p2_puzzle_hash: parsed_memos.p2_puzzle_hash,
350 coin: child.coin,
351 clawback: parsed_memos.clawback,
352 memos: parsed_memos.memos,
353 new_uris,
354 latest_owner: child.info.current_owner,
355 includes_unverifiable_updates,
356 });
357 }
358 }
359
360 let create_coins = conditions
361 .into_iter()
362 .filter_map(Condition::into_create_coin)
363 .collect::<Vec<_>>();
364
365 let notarized_payments =
366 if coin_spend.coin.puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
367 SettlementPaymentsSolution::from_clvm(allocator, solution)?.notarized_payments
368 } else {
369 Vec::new()
370 };
371
372 for create_coin in create_coins {
373 let child_coin = Coin::new(
374 coin_spend.coin.coin_id(),
375 create_coin.puzzle_hash,
376 create_coin.amount,
377 );
378
379 let child_coin_id = child_coin.coin_id();
380 let is_child_ephemeral = all_spent_coin_ids.contains(&child_coin_id);
381
382 if is_singleton && child_coin.amount % 2 == 1 {
384 continue;
385 }
386
387 let parsed_memos = parse_memos(allocator, create_coin, false);
388 let is_child_ours = parsed_memos.p2_puzzle_hash == our_p2_puzzle_hash;
389
390 total_output += child_coin.amount;
391
392 if is_parent_ours && !is_child_ephemeral {
393 our_output += child_coin.amount;
394 }
395
396 if our_spent_coin_ids.contains(&child_coin_id) {
398 continue;
399 }
400
401 if let Some(transfer_type) = calculate_transfer_type(
402 allocator,
403 TransferTypeContext {
404 puzzle_assertion_ids: &puzzle_assertion_ids,
405 notarized_payments: ¬arized_payments,
406 create_coin: &create_coin,
407 p2_puzzle_hash: parsed_memos.p2_puzzle_hash,
408 full_puzzle_hash: coin_spend.coin.puzzle_hash,
409 is_parent_ours,
410 is_child_ours,
411 },
412 ) {
413 payments.push(ParsedPayment {
414 transfer_type,
415 asset_id: None,
416 hidden_puzzle_hash: None,
417 p2_puzzle_hash: parsed_memos.p2_puzzle_hash,
418 coin: child_coin,
419 clawback: parsed_memos.clawback,
420 memos: parsed_memos.memos,
421 });
422 }
423 }
424 }
425
426 Ok(Self {
427 new_custody_hash,
428 payments,
429 nfts,
430 drop_coins,
431 fee_paid: our_input.saturating_sub(our_output),
432 total_fee: total_input.saturating_sub(total_output),
433 })
434 }
435}
436
437fn vault_p2_puzzle_hash(launcher_id: Bytes32) -> Bytes32 {
438 mips_puzzle_hash(
439 0,
440 vec![],
441 SingletonMember::new(launcher_id).curry_tree_hash(),
442 true,
443 )
444 .into()
445}
446
447#[derive(Debug, Clone)]
448struct ParsedDelegatedSpend {
449 new_custody_hash: Option<TreeHash>,
450 our_spent_coin_ids: HashSet<Bytes32>,
451 puzzle_assertion_ids: HashSet<Bytes32>,
452 drop_coins: Vec<DropCoin>,
453}
454
455fn parse_delegated_spend(
456 allocator: &mut Allocator,
457 delegated_spend: Spend,
458 spent_coin_ids: &HashSet<Bytes32>,
459) -> Result<ParsedDelegatedSpend, DriverError> {
460 let vault_output = run_puzzle(allocator, delegated_spend.puzzle, delegated_spend.solution)?;
461 let vault_conditions = Vec::<Condition>::from_clvm(allocator, vault_output)?;
462
463 let mut new_custody_hash = None;
464 let mut our_spent_coin_ids = HashSet::new();
465 let mut puzzle_assertion_ids = HashSet::new();
466 let mut drop_coins = Vec::new();
467
468 for condition in vault_conditions {
469 match condition {
470 Condition::CreateCoin(condition) => {
471 if condition.amount % 2 == 1 {
472 new_custody_hash = Some(condition.puzzle_hash.into());
474 } else {
475 drop_coins.push(DropCoin {
476 puzzle_hash: condition.puzzle_hash,
477 amount: condition.amount,
478 });
479 }
480 }
481 Condition::SendMessage(condition) => {
482 let sender = MessageFlags::decode(condition.mode, MessageSide::Sender);
484 let receiver = MessageFlags::decode(condition.mode, MessageSide::Receiver);
485
486 if sender != MessageFlags::PUZZLE
487 || receiver != MessageFlags::COIN
488 || condition.data.len() != 1
489 {
490 return Err(DriverError::MissingSpend);
492 }
493
494 let coin_id = Bytes32::from_clvm(allocator, condition.data[0])?;
497
498 if !spent_coin_ids.contains(&coin_id) || !our_spent_coin_ids.insert(coin_id) {
499 return Err(DriverError::MissingSpend);
500 }
501 }
502 Condition::AssertPuzzleAnnouncement(condition) => {
503 puzzle_assertion_ids.insert(condition.announcement_id);
504 }
505 _ => {}
506 }
507 }
508
509 Ok(ParsedDelegatedSpend {
510 new_custody_hash,
511 our_spent_coin_ids,
512 puzzle_assertion_ids,
513 drop_coins,
514 })
515}
516
517#[derive(Debug, Clone)]
518struct ParsedConditions {
519 puzzle_assertion_ids: HashSet<Bytes32>,
520 all_created_coin_ids: HashSet<Bytes32>,
521}
522
523fn parse_our_conditions(
524 allocator: &mut Allocator,
525 coin_spends: &[CoinSpend],
526 our_coin_ids: &HashSet<Bytes32>,
527 mut puzzle_assertion_ids: HashSet<Bytes32>,
528) -> Result<ParsedConditions, DriverError> {
529 let mut all_created_coin_ids = HashSet::new();
530
531 for coin_spend in coin_spends {
532 let coin_id = coin_spend.coin.coin_id();
533 let puzzle = coin_spend.puzzle_reveal.to_clvm(allocator)?;
534 let solution = coin_spend.solution.to_clvm(allocator)?;
535 let output = run_puzzle(allocator, puzzle, solution)?;
536 let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
537
538 for condition in conditions {
539 match condition {
540 Condition::AssertPuzzleAnnouncement(condition) => {
541 if our_coin_ids.contains(&coin_id) {
542 puzzle_assertion_ids.insert(condition.announcement_id);
543 }
544 }
545 Condition::CreateCoin(condition) => {
546 all_created_coin_ids.insert(
547 Coin::new(coin_id, condition.puzzle_hash, condition.amount).coin_id(),
548 );
549 }
550 _ => {}
551 }
552 }
553 }
554
555 Ok(ParsedConditions {
556 puzzle_assertion_ids,
557 all_created_coin_ids,
558 })
559}
560
561fn reorder_coin_spends(mut coin_spends: Vec<CoinSpend>) -> Vec<CoinSpend> {
565 let mut reordered_coin_spends = Vec::new();
566 let mut remaining_spent_coin_ids: HashSet<Bytes32> =
567 coin_spends.iter().map(|cs| cs.coin.coin_id()).collect();
568
569 while !coin_spends.is_empty() {
570 coin_spends.retain(|cs| {
571 if remaining_spent_coin_ids.contains(&cs.coin.parent_coin_info) {
572 true
573 } else {
574 remaining_spent_coin_ids.remove(&cs.coin.coin_id());
575 reordered_coin_spends.push(cs.clone());
576 false
577 }
578 });
579 }
580
581 reordered_coin_spends
582}
583
584#[derive(Debug, Clone)]
585struct ParsedMemos {
586 p2_puzzle_hash: Bytes32,
587 clawback: Option<ClawbackV2>,
588 memos: Vec<String>,
589}
590
591fn parse_memos(
592 allocator: &Allocator,
593 p2_create_coin: CreateCoin<NodePtr>,
594 requires_hint: bool,
595) -> ParsedMemos {
596 let Memos::Some(memos) = p2_create_coin.memos else {
598 return ParsedMemos {
599 p2_puzzle_hash: p2_create_coin.puzzle_hash,
600 clawback: None,
601 memos: Vec::new(),
602 };
603 };
604
605 if let Ok((hint, (clawback_memo, rest))) =
608 <(Bytes32, (NodePtr, NodePtr))>::from_clvm(allocator, memos)
609 && let Some(clawback) = ClawbackV2::from_memo(
610 allocator,
611 clawback_memo,
612 hint,
613 p2_create_coin.amount,
614 requires_hint,
615 p2_create_coin.puzzle_hash,
616 )
617 {
618 return ParsedMemos {
619 p2_puzzle_hash: clawback.receiver_puzzle_hash,
620 clawback: Some(clawback),
621 memos: parse_memo_list(allocator, rest),
622 };
623 }
624
625 if requires_hint && let Ok((_hint, rest)) = <(Bytes32, NodePtr)>::from_clvm(allocator, memos) {
627 return ParsedMemos {
628 p2_puzzle_hash: p2_create_coin.puzzle_hash,
629 clawback: None,
630 memos: parse_memo_list(allocator, rest),
631 };
632 }
633
634 ParsedMemos {
636 p2_puzzle_hash: p2_create_coin.puzzle_hash,
637 clawback: None,
638 memos: parse_memo_list(allocator, memos),
639 }
640}
641
642fn parse_memo_list(allocator: &Allocator, memos: NodePtr) -> Vec<String> {
643 let memos = Vec::<NodePtr>::from_clvm(allocator, memos).unwrap_or_default();
644
645 let mut result = Vec::new();
646
647 for memo in memos {
648 let Ok(memo) = String::from_clvm(allocator, memo) else {
649 continue;
650 };
651 result.push(memo);
652 }
653
654 result
655}
656
657#[derive(Debug, Clone, Copy)]
658struct TransferTypeContext<'a> {
659 puzzle_assertion_ids: &'a HashSet<Bytes32>,
660 notarized_payments: &'a Vec<NotarizedPayment>,
661 create_coin: &'a CreateCoin<NodePtr>,
662 p2_puzzle_hash: Bytes32,
663 full_puzzle_hash: Bytes32,
664 is_parent_ours: bool,
665 is_child_ours: bool,
666}
667
668fn calculate_transfer_type(
669 allocator: &Allocator,
670 context: TransferTypeContext<'_>,
671) -> Option<TransferType> {
672 let TransferTypeContext {
673 puzzle_assertion_ids,
674 notarized_payments,
675 create_coin,
676 p2_puzzle_hash,
677 full_puzzle_hash,
678 is_parent_ours,
679 is_child_ours,
680 } = context;
681
682 if is_parent_ours && !is_child_ours {
683 if p2_puzzle_hash == BURN_PUZZLE_HASH {
686 Some(TransferType::Burned)
687 } else if p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
688 Some(TransferType::Offered)
689 } else {
690 Some(TransferType::Sent)
691 }
692 } else if !is_parent_ours
693 && is_child_ours
694 && let Some(notarized_payment) = notarized_payments.iter().find(|np| {
695 np.payments
696 .iter()
697 .any(|p| p.puzzle_hash == create_coin.puzzle_hash && p.amount == create_coin.amount)
698 })
699 {
700 let notarized_payment_hash = tree_hash_notarized_payment(allocator, notarized_payment);
701 let settlement_announcement_id = announcement_id(full_puzzle_hash, notarized_payment_hash);
702
703 if puzzle_assertion_ids.contains(&settlement_announcement_id) {
706 Some(TransferType::Received)
707 } else {
708 None
709 }
710 } else if is_parent_ours && is_child_ours {
711 Some(TransferType::Updated)
713 } else {
714 None
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721
722 use anyhow::Result;
723 use chia_puzzles::SINGLETON_LAUNCHER_HASH;
724 use chia_sdk_test::Simulator;
725 use chia_sdk_types::Conditions;
726 use rstest::rstest;
727
728 use crate::{Action, Id, SpendContext, Spends, TestVault};
729
730 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
731 enum AssetKind {
732 Xch,
733 Cat,
734 RevocableCat,
735 }
736
737 #[rstest]
738 fn test_clear_signing_sent(
739 #[values(AssetKind::Xch, AssetKind::Cat, AssetKind::RevocableCat)] asset_kind: AssetKind,
740 #[values(0, 100)] fee: u64,
741 ) -> Result<()> {
742 let mut sim = Simulator::new();
743 let mut ctx = SpendContext::new();
744
745 let alice = TestVault::mint(&mut sim, &mut ctx, 1000 + fee)?;
746 let bob = TestVault::mint(&mut sim, &mut ctx, 0)?;
747
748 let (id, asset_id) = if let AssetKind::Cat | AssetKind::RevocableCat = asset_kind {
749 let result = alice.spend(
750 &mut sim,
751 &mut ctx,
752 &[Action::single_issue_cat(
753 if let AssetKind::RevocableCat = asset_kind {
754 Some(Bytes32::default())
755 } else {
756 None
757 },
758 1000,
759 )],
760 )?;
761
762 let asset_id = result.outputs.cats[0][0].info.asset_id;
763 let id = Id::Existing(asset_id);
764 (id, Some(asset_id))
765 } else {
766 (Id::Xch, None)
767 };
768
769 let result = alice.spend(
770 &mut sim,
771 &mut ctx,
772 &[
773 Action::send(id, bob.puzzle_hash(), 1000, Memos::None),
774 Action::fee(fee),
775 ],
776 )?;
777
778 let reveal = VaultSpendReveal {
779 launcher_id: alice.launcher_id(),
780 custody_hash: alice.custody_hash(),
781 delegated_spend: result.delegated_spend,
782 };
783
784 let tx = VaultTransaction::parse(&mut ctx, &reveal, result.coin_spends)?;
785 assert_eq!(tx.new_custody_hash, Some(alice.custody_hash()));
786 assert_eq!(tx.payments.len(), 1);
787 assert_eq!(tx.fee_paid, fee);
788 assert_eq!(tx.total_fee, fee);
789
790 let payment = &tx.payments[0];
791 assert_eq!(payment.transfer_type, TransferType::Sent);
792 assert_eq!(payment.asset_id, asset_id);
793 assert_eq!(payment.p2_puzzle_hash, bob.puzzle_hash());
794 assert_eq!(payment.coin.amount, 1000);
795
796 Ok(())
797 }
798
799 #[rstest]
800 fn test_clear_signing_received(
801 #[values(AssetKind::Xch, AssetKind::Cat, AssetKind::RevocableCat)] asset_kind: AssetKind,
802 #[values(true, false)] disable_settlement_assertions: bool,
803 #[values(0, 100)] alice_fee: u64,
804 #[values(0, 100)] bob_fee: u64,
805 ) -> Result<()> {
806 let mut sim = Simulator::new();
807 let mut ctx = SpendContext::new();
808
809 let alice = TestVault::mint(&mut sim, &mut ctx, 1000 + alice_fee)?;
810 let bob = TestVault::mint(&mut sim, &mut ctx, bob_fee)?;
811
812 let (id, asset_id) = if let AssetKind::Cat | AssetKind::RevocableCat = asset_kind {
813 let result = alice.spend(
814 &mut sim,
815 &mut ctx,
816 &[Action::single_issue_cat(
817 if let AssetKind::RevocableCat = asset_kind {
818 Some(Bytes32::default())
819 } else {
820 None
821 },
822 1000,
823 )],
824 )?;
825
826 let asset_id = result.outputs.cats[0][0].info.asset_id;
827 let id = Id::Existing(asset_id);
828 (id, Some(asset_id))
829 } else {
830 (Id::Xch, None)
831 };
832
833 let result = alice.spend(
834 &mut sim,
835 &mut ctx,
836 &[
837 Action::send(id, SETTLEMENT_PAYMENT_HASH.into(), 1000, Memos::None),
838 Action::fee(alice_fee),
839 ],
840 )?;
841
842 let reveal = VaultSpendReveal {
843 launcher_id: bob.launcher_id(),
844 custody_hash: bob.custody_hash(),
845 delegated_spend: result.delegated_spend,
846 };
847
848 let tx = VaultTransaction::parse(&mut ctx, &reveal, result.coin_spends)?;
849
850 assert_eq!(tx.payments.len(), 1);
851 assert_eq!(tx.fee_paid, alice_fee);
852 assert_eq!(tx.total_fee, alice_fee);
853
854 let payment = &tx.payments[0];
855 assert_eq!(payment.transfer_type, TransferType::Offered);
856 assert_eq!(payment.asset_id, asset_id);
857 assert_eq!(payment.p2_puzzle_hash, SETTLEMENT_PAYMENT_HASH.into());
858 assert_eq!(payment.coin.amount, 1000);
859
860 let mut spends = Spends::new(bob.puzzle_hash());
861 if id == Id::Xch {
862 spends.add(result.outputs.xch[0]);
863 } else {
864 spends.add(result.outputs.cats[&id][0]);
865 }
866 spends.conditions.disable_settlement_assertions = disable_settlement_assertions;
867
868 let result = bob.custom_spend(
869 &mut sim,
870 &mut ctx,
871 &[Action::fee(bob_fee)],
872 spends,
873 Conditions::new(),
874 )?;
875
876 let reveal = VaultSpendReveal {
877 launcher_id: bob.launcher_id(),
878 custody_hash: bob.custody_hash(),
879 delegated_spend: result.delegated_spend,
880 };
881
882 let tx = VaultTransaction::parse(&mut ctx, &reveal, result.coin_spends)?;
883
884 if disable_settlement_assertions {
885 assert_eq!(tx.payments.len(), 0);
886 assert_eq!(tx.fee_paid, bob_fee);
887 assert_eq!(tx.total_fee, bob_fee);
888 } else {
889 assert_eq!(tx.payments.len(), 1);
890 assert_eq!(tx.fee_paid, bob_fee);
891 assert_eq!(tx.total_fee, bob_fee);
892
893 let payment = &tx.payments[0];
894 assert_eq!(payment.transfer_type, TransferType::Received);
895 assert_eq!(payment.asset_id, asset_id);
896 assert_eq!(payment.p2_puzzle_hash, bob.puzzle_hash());
897 assert_eq!(payment.coin.amount, 1000);
898 }
899
900 Ok(())
901 }
902
903 #[rstest]
904 fn test_clear_signing_nft_lifecycle() -> Result<()> {
905 let mut sim = Simulator::new();
906 let mut ctx = SpendContext::new();
907
908 let alice = TestVault::mint(&mut sim, &mut ctx, 1)?;
909 let bob = TestVault::mint(&mut sim, &mut ctx, 0)?;
910
911 let result = alice.spend(&mut sim, &mut ctx, &[Action::mint_empty_nft()])?;
912
913 let reveal = VaultSpendReveal {
914 launcher_id: alice.launcher_id(),
915 custody_hash: alice.custody_hash(),
916 delegated_spend: result.delegated_spend,
917 };
918
919 let tx = VaultTransaction::parse(&mut ctx, &reveal, result.coin_spends)?;
920
921 assert_eq!(tx.payments.len(), 1);
922 assert_eq!(tx.nfts.len(), 1);
923 assert_eq!(tx.fee_paid, 0);
924 assert_eq!(tx.total_fee, 0);
925
926 let payment = &tx.payments[0];
928 assert_eq!(payment.transfer_type, TransferType::Sent);
929 assert_eq!(payment.p2_puzzle_hash, SINGLETON_LAUNCHER_HASH.into());
930 assert_eq!(payment.coin.amount, 0);
931
932 let nft = &tx.nfts[0];
934 assert_eq!(nft.transfer_type, TransferType::Updated);
935 assert_eq!(nft.p2_puzzle_hash, alice.puzzle_hash());
936 assert!(!nft.includes_unverifiable_updates);
937
938 let nft_id = Id::Existing(nft.launcher_id);
940 let bob_hint = ctx.hint(bob.puzzle_hash())?;
941
942 let result = alice.spend(
943 &mut sim,
944 &mut ctx,
945 &[Action::send(nft_id, bob.puzzle_hash(), 1, bob_hint)],
946 )?;
947
948 let reveal = VaultSpendReveal {
949 launcher_id: alice.launcher_id(),
950 custody_hash: alice.custody_hash(),
951 delegated_spend: result.delegated_spend,
952 };
953
954 let tx = VaultTransaction::parse(&mut ctx, &reveal, result.coin_spends)?;
955
956 assert_eq!(tx.payments.len(), 0);
957 assert_eq!(tx.nfts.len(), 1);
958 assert_eq!(tx.fee_paid, 0);
959 assert_eq!(tx.total_fee, 0);
960
961 let nft = &tx.nfts[0];
962 assert_eq!(nft.transfer_type, TransferType::Sent);
963 assert_eq!(nft.p2_puzzle_hash, bob.puzzle_hash());
964 assert!(!nft.includes_unverifiable_updates);
965
966 Ok(())
967 }
968
969 #[rstest]
970 fn test_clear_signing_split(
971 #[values(AssetKind::Xch, AssetKind::Cat, AssetKind::RevocableCat)] asset_kind: AssetKind,
972 #[values(0, 100)] fee: u64,
973 ) -> Result<()> {
974 let mut sim = Simulator::new();
975 let mut ctx = SpendContext::new();
976
977 let alice = TestVault::mint(&mut sim, &mut ctx, 1000 + fee)?;
978
979 let (id, asset_id) = if let AssetKind::Cat | AssetKind::RevocableCat = asset_kind {
980 let result = alice.spend(
981 &mut sim,
982 &mut ctx,
983 &[Action::single_issue_cat(
984 if let AssetKind::RevocableCat = asset_kind {
985 Some(Bytes32::default())
986 } else {
987 None
988 },
989 1000,
990 )],
991 )?;
992
993 let asset_id = result.outputs.cats[0][0].info.asset_id;
994 let id = Id::Existing(asset_id);
995 (id, Some(asset_id))
996 } else {
997 (Id::Xch, None)
998 };
999
1000 let result = alice.spend(
1001 &mut sim,
1002 &mut ctx,
1003 &[
1004 Action::send(id, alice.puzzle_hash(), 250, Memos::None),
1005 Action::send(id, alice.puzzle_hash(), 250, Memos::None),
1006 Action::send(id, alice.puzzle_hash(), 250, Memos::None),
1007 Action::send(id, alice.puzzle_hash(), 250, Memos::None),
1008 Action::fee(fee),
1009 ],
1010 )?;
1011
1012 let reveal = VaultSpendReveal {
1013 launcher_id: alice.launcher_id(),
1014 custody_hash: alice.custody_hash(),
1015 delegated_spend: result.delegated_spend,
1016 };
1017
1018 let tx = VaultTransaction::parse(&mut ctx, &reveal, result.coin_spends)?;
1019 assert_eq!(tx.new_custody_hash, Some(alice.custody_hash()));
1020 assert_eq!(tx.payments.len(), 4);
1021 assert_eq!(tx.fee_paid, fee);
1022 assert_eq!(tx.total_fee, fee);
1023
1024 for payment in &tx.payments {
1025 assert_eq!(payment.transfer_type, TransferType::Updated);
1026 assert_eq!(payment.asset_id, asset_id);
1027 assert_eq!(payment.p2_puzzle_hash, alice.puzzle_hash());
1028 assert_eq!(payment.coin.amount, 250);
1029 }
1030
1031 let result = alice.spend(
1032 &mut sim,
1033 &mut ctx,
1034 &[Action::send(id, alice.puzzle_hash(), 1000, Memos::None)],
1035 )?;
1036
1037 let reveal = VaultSpendReveal {
1038 launcher_id: alice.launcher_id(),
1039 custody_hash: alice.custody_hash(),
1040 delegated_spend: result.delegated_spend,
1041 };
1042
1043 let tx = VaultTransaction::parse(&mut ctx, &reveal, result.coin_spends)?;
1044 assert_eq!(tx.new_custody_hash, Some(alice.custody_hash()));
1045 assert_eq!(tx.payments.len(), 1);
1046 assert_eq!(tx.fee_paid, 0);
1047 assert_eq!(tx.total_fee, 0);
1048
1049 let payment = &tx.payments[0];
1050 assert_eq!(payment.transfer_type, TransferType::Updated);
1051 assert_eq!(payment.asset_id, asset_id);
1052 assert_eq!(payment.p2_puzzle_hash, alice.puzzle_hash());
1053 assert_eq!(payment.coin.amount, 1000);
1054
1055 Ok(())
1056 }
1057}