1use chia_protocol::{Bytes32, Coin};
2use chia_puzzle_types::{
3 nft::{NftOwnershipLayerSolution, NftStateLayerSolution},
4 offer::{NotarizedPayment, SettlementPaymentsSolution},
5 singleton::{SingletonArgs, SingletonSolution},
6 LineageProof, Proof,
7};
8use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
9use chia_sdk_types::{
10 conditions::{TradePrice, TransferNft},
11 Conditions,
12};
13use chia_sha2::Sha256;
14use clvm_traits::{clvm_list, FromClvm, ToClvm};
15use clvm_utils::{tree_hash, ToTreeHash};
16use clvmr::{Allocator, NodePtr};
17
18use crate::{
19 DriverError, Layer, Puzzle, SettlementLayer, Spend, SpendContext, SpendWithConditions,
20};
21
22mod metadata_update;
23mod nft_info;
24mod nft_launcher;
25mod nft_mint;
26
27pub use metadata_update::*;
28pub use nft_info::*;
29pub use nft_mint::*;
30
31#[must_use]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Nft<M> {
43 pub coin: Coin,
45
46 pub proof: Proof,
54
55 pub info: NftInfo<M>,
57}
58
59impl<M> Nft<M> {
60 pub fn new(coin: Coin, proof: Proof, info: NftInfo<M>) -> Self {
61 Nft { coin, proof, info }
62 }
63
64 pub fn with_metadata<N>(self, metadata: N) -> Nft<N> {
65 Nft {
66 coin: self.coin,
67 proof: self.proof,
68 info: self.info.with_metadata(metadata),
69 }
70 }
71}
72
73impl<M> Nft<M>
74where
75 M: ToTreeHash,
76{
77 pub fn child_lineage_proof(&self) -> LineageProof {
79 LineageProof {
80 parent_parent_coin_info: self.coin.parent_coin_info,
81 parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
82 parent_amount: self.coin.amount,
83 }
84 }
85
86 pub fn child<N>(
88 &self,
89 p2_puzzle_hash: Bytes32,
90 current_owner: Option<Bytes32>,
91 metadata: N,
92 amount: u64,
93 ) -> Nft<N>
94 where
95 M: Clone,
96 N: ToTreeHash,
97 {
98 self.child_with(
99 NftInfo {
100 current_owner,
101 p2_puzzle_hash,
102 ..self.info.clone().with_metadata(metadata)
103 },
104 amount,
105 )
106 }
107
108 pub fn child_with<N>(&self, info: NftInfo<N>, amount: u64) -> Nft<N>
116 where
117 N: ToTreeHash,
118 {
119 Nft::new(
120 Coin::new(
121 self.coin.coin_id(),
122 SingletonArgs::curry_tree_hash(info.launcher_id, info.inner_puzzle_hash()).into(),
123 amount,
124 ),
125 Proof::Lineage(self.child_lineage_proof()),
126 info,
127 )
128 }
129}
130
131impl<M> Nft<M>
132where
133 M: ToClvm<Allocator> + FromClvm<Allocator> + ToTreeHash + Clone,
134{
135 pub fn spend(&self, ctx: &mut SpendContext, inner_spend: Spend) -> Result<Self, DriverError> {
138 let layers = self.info.clone().into_layers(inner_spend.puzzle);
139
140 let spend = layers.construct_spend(
141 ctx,
142 SingletonSolution {
143 lineage_proof: self.proof,
144 amount: self.coin.amount,
145 inner_solution: NftStateLayerSolution {
146 inner_solution: NftOwnershipLayerSolution {
147 inner_solution: inner_spend.solution,
148 },
149 },
150 },
151 )?;
152
153 ctx.spend(self.coin, spend)?;
154
155 let (info, create_coin) = self.info.child_from_p2_spend(ctx, inner_spend)?;
156
157 Ok(self.child_with(info, create_coin.amount))
158 }
159
160 pub fn spend_with<I>(
166 &self,
167 ctx: &mut SpendContext,
168 inner: &I,
169 conditions: Conditions,
170 ) -> Result<Self, DriverError>
171 where
172 I: SpendWithConditions,
173 {
174 let inner_spend = inner.spend_with_conditions(ctx, conditions)?;
175 self.spend(ctx, inner_spend)
176 }
177
178 pub fn transfer_with_metadata<I>(
184 self,
185 ctx: &mut SpendContext,
186 inner: &I,
187 p2_puzzle_hash: Bytes32,
188 metadata_update: Spend,
189 extra_conditions: Conditions,
190 ) -> Result<Nft<M>, DriverError>
191 where
192 I: SpendWithConditions,
193 {
194 let memos = ctx.hint(p2_puzzle_hash)?;
195
196 self.spend_with(
197 ctx,
198 inner,
199 extra_conditions
200 .create_coin(p2_puzzle_hash, self.coin.amount, memos)
201 .update_nft_metadata(metadata_update.puzzle, metadata_update.solution),
202 )
203 }
204
205 pub fn transfer<I>(
210 self,
211 ctx: &mut SpendContext,
212 inner: &I,
213 p2_puzzle_hash: Bytes32,
214 extra_conditions: Conditions,
215 ) -> Result<Nft<M>, DriverError>
216 where
217 I: SpendWithConditions,
218 {
219 let memos = ctx.hint(p2_puzzle_hash)?;
220
221 self.spend_with(
222 ctx,
223 inner,
224 extra_conditions.create_coin(p2_puzzle_hash, self.coin.amount, memos),
225 )
226 }
227
228 pub fn lock_settlement<I>(
234 self,
235 ctx: &mut SpendContext,
236 inner: &I,
237 trade_prices: Vec<TradePrice>,
238 extra_conditions: Conditions,
239 ) -> Result<Nft<M>, DriverError>
240 where
241 I: SpendWithConditions,
242 {
243 let transfer_condition = TransferNft::new(None, trade_prices, None);
244
245 let (conditions, nft) = self.assign_owner(
246 ctx,
247 inner,
248 SETTLEMENT_PAYMENT_HASH.into(),
249 transfer_condition,
250 extra_conditions,
251 )?;
252
253 assert_eq!(conditions.len(), 0);
254
255 Ok(nft)
256 }
257
258 pub fn unlock_settlement(
261 self,
262 ctx: &mut SpendContext,
263 notarized_payments: Vec<NotarizedPayment>,
264 ) -> Result<Nft<M>, DriverError> {
265 let inner_spend = SettlementLayer
266 .construct_spend(ctx, SettlementPaymentsSolution { notarized_payments })?;
267
268 self.spend(ctx, inner_spend)
269 }
270
271 pub fn assign_owner<I>(
281 self,
282 ctx: &mut SpendContext,
283 inner: &I,
284 p2_puzzle_hash: Bytes32,
285 transfer_condition: TransferNft,
286 extra_conditions: Conditions,
287 ) -> Result<(Conditions, Nft<M>), DriverError>
288 where
289 I: SpendWithConditions,
290 {
291 let launcher_id = transfer_condition.launcher_id;
292
293 let assignment_conditions = if launcher_id.is_some() {
294 Conditions::new()
295 .assert_puzzle_announcement(assignment_puzzle_announcement_id(
296 self.coin.puzzle_hash,
297 &transfer_condition,
298 ))
299 .create_puzzle_announcement(self.info.launcher_id.into())
300 } else {
301 Conditions::new()
302 };
303
304 let memos = ctx.hint(p2_puzzle_hash)?;
305
306 let child = self.spend_with(
307 ctx,
308 inner,
309 extra_conditions
310 .create_coin(p2_puzzle_hash, self.coin.amount, memos)
311 .with(transfer_condition),
312 )?;
313
314 Ok((assignment_conditions, child))
315 }
316}
317
318impl<M> Nft<M>
319where
320 M: ToClvm<Allocator> + FromClvm<Allocator> + ToTreeHash,
321{
322 pub fn parse_child(
332 allocator: &mut Allocator,
333 parent_coin: Coin,
334 parent_puzzle: Puzzle,
335 parent_solution: NodePtr,
336 ) -> Result<Option<Self>, DriverError>
337 where
338 Self: Sized,
339 M: Clone,
340 {
341 let Some((parent_info, p2_puzzle)) = NftInfo::<M>::parse(allocator, parent_puzzle)? else {
342 return Ok(None);
343 };
344
345 let p2_solution =
346 StandardNftLayers::<M, Puzzle>::parse_solution(allocator, parent_solution)?
347 .inner_solution
348 .inner_solution
349 .inner_solution;
350
351 let (info, create_coin) =
352 parent_info.child_from_p2_spend(allocator, Spend::new(p2_puzzle.ptr(), p2_solution))?;
353
354 Ok(Some(Self {
355 coin: Coin::new(
356 parent_coin.coin_id(),
357 info.puzzle_hash().into(),
358 create_coin.amount,
359 ),
360 proof: Proof::Lineage(LineageProof {
361 parent_parent_coin_info: parent_coin.parent_coin_info,
362 parent_inner_puzzle_hash: parent_info.inner_puzzle_hash().into(),
363 parent_amount: parent_coin.amount,
364 }),
365 info,
366 }))
367 }
368}
369
370pub fn assignment_puzzle_announcement_id(
371 nft_full_puzzle_hash: Bytes32,
372 new_nft_owner: &TransferNft,
373) -> Bytes32 {
374 let mut allocator = Allocator::new();
375
376 let new_nft_owner_args = clvm_list!(
377 new_nft_owner.launcher_id,
378 &new_nft_owner.trade_prices,
379 new_nft_owner.singleton_inner_puzzle_hash
380 )
381 .to_clvm(&mut allocator)
382 .unwrap();
383
384 let mut hasher = Sha256::new();
385 hasher.update(nft_full_puzzle_hash);
386 hasher.update([0xad, 0x4c]);
387 hasher.update(tree_hash(&allocator, new_nft_owner_args));
388
389 Bytes32::new(hasher.finalize())
390}
391
392#[cfg(test)]
393mod tests {
394 use crate::{IntermediateLauncher, Launcher, NftMint, StandardLayer};
395
396 use super::*;
397
398 use chia_puzzle_types::nft::NftMetadata;
399 use chia_sdk_test::Simulator;
400
401 #[test]
402 fn test_nft_transfer() -> anyhow::Result<()> {
403 let mut sim = Simulator::new();
404 let ctx = &mut SpendContext::new();
405
406 let alice = sim.bls(2);
407 let alice_p2 = StandardLayer::new(alice.pk);
408
409 let (create_did, did) =
410 Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
411 alice_p2.spend(ctx, alice.coin, create_did)?;
412
413 let mint = NftMint::new(
414 NftMetadata::default(),
415 alice.puzzle_hash,
416 300,
417 Some(TransferNft::new(
418 Some(did.info.launcher_id),
419 Vec::new(),
420 Some(did.info.inner_puzzle_hash().into()),
421 )),
422 );
423
424 let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
425 .create(ctx)?
426 .mint_nft(ctx, mint)?;
427 let _did = did.update(ctx, &alice_p2, mint_nft)?;
428 let _nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
429
430 sim.spend_coins(ctx.take(), &[alice.sk])?;
431
432 Ok(())
433 }
434
435 #[test]
436 fn test_nft_lineage() -> anyhow::Result<()> {
437 let mut sim = Simulator::new();
438 let ctx = &mut SpendContext::new();
439
440 let alice = sim.bls(2);
441 let alice_p2 = StandardLayer::new(alice.pk);
442
443 let (create_did, did) =
444 Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
445 alice_p2.spend(ctx, alice.coin, create_did)?;
446
447 let mint = NftMint::new(
448 NftMetadata::default(),
449 alice.puzzle_hash,
450 300,
451 Some(TransferNft::new(
452 Some(did.info.launcher_id),
453 Vec::new(),
454 Some(did.info.inner_puzzle_hash().into()),
455 )),
456 );
457
458 let (mint_nft, mut nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
459 .create(ctx)?
460 .mint_nft(ctx, mint)?;
461
462 let mut did = did.update(ctx, &alice_p2, mint_nft)?;
463
464 sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
465
466 for i in 0..5 {
467 let transfer_condition = TransferNft::new(
468 Some(did.info.launcher_id),
469 Vec::new(),
470 Some(did.info.inner_puzzle_hash().into()),
471 );
472
473 let (spend_nft, new_nft) = nft.assign_owner(
474 ctx,
475 &alice_p2,
476 alice.puzzle_hash,
477 if i % 2 == 0 {
478 transfer_condition
479 } else {
480 TransferNft::new(None, Vec::new(), None)
481 },
482 Conditions::new(),
483 )?;
484
485 nft = new_nft;
486 did = did.update(ctx, &alice_p2, spend_nft)?;
487 }
488
489 sim.spend_coins(ctx.take(), &[alice.sk])?;
490
491 Ok(())
492 }
493
494 #[test]
495 fn test_nft_metadata_update() -> anyhow::Result<()> {
496 let mut sim = Simulator::new();
497 let ctx = &mut SpendContext::new();
498
499 let alice = sim.bls(2);
500 let alice_p2 = StandardLayer::new(alice.pk);
501
502 let (create_did, did) =
503 Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
504 alice_p2.spend(ctx, alice.coin, create_did)?;
505
506 let mint = NftMint::new(
507 NftMetadata {
508 data_uris: vec!["example.com".to_string()],
509 data_hash: Some(Bytes32::default()),
510 ..Default::default()
511 },
512 alice.puzzle_hash,
513 300,
514 Some(TransferNft::new(
515 Some(did.info.launcher_id),
516 Vec::new(),
517 Some(did.info.inner_puzzle_hash().into()),
518 )),
519 );
520
521 let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
522 .create(ctx)?
523 .mint_nft(ctx, mint)?;
524 let _did = did.update(ctx, &alice_p2, mint_nft)?;
525
526 let metadata_update = MetadataUpdate::NewDataUri("another.com".to_string()).spend(ctx)?;
527 let parent_nft = nft.clone();
528 let nft: Nft<NftMetadata> = nft.transfer_with_metadata(
529 ctx,
530 &alice_p2,
531 alice.puzzle_hash,
532 metadata_update,
533 Conditions::new(),
534 )?;
535
536 assert_eq!(
537 nft.info.metadata,
538 NftMetadata {
539 data_uris: vec!["another.com".to_string(), "example.com".to_string()],
540 data_hash: Some(Bytes32::default()),
541 ..Default::default()
542 }
543 );
544
545 let child_nft = nft.clone();
546 let _nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
547
548 sim.spend_coins(ctx.take(), &[alice.sk])?;
549
550 let parent_puzzle = sim
552 .puzzle_reveal(parent_nft.coin.coin_id())
553 .expect("missing puzzle");
554
555 let parent_solution = sim
556 .solution(parent_nft.coin.coin_id())
557 .expect("missing solution");
558
559 let parent_puzzle = parent_puzzle.to_clvm(ctx)?;
560 let parent_puzzle = Puzzle::parse(ctx, parent_puzzle);
561 let parent_solution = parent_solution.to_clvm(ctx)?;
562
563 let new_child_nft =
564 Nft::<NftMetadata>::parse_child(ctx, parent_nft.coin, parent_puzzle, parent_solution)?
565 .expect("child is not an NFT");
566
567 assert_eq!(new_child_nft, child_nft);
568
569 Ok(())
570 }
571
572 #[test]
573 fn test_parse_nft() -> anyhow::Result<()> {
574 let mut sim = Simulator::new();
575 let ctx = &mut SpendContext::new();
576
577 let alice = sim.bls(2);
578 let alice_p2 = StandardLayer::new(alice.pk);
579
580 let (create_did, did) =
581 Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
582 alice_p2.spend(ctx, alice.coin, create_did)?;
583
584 let mut metadata = NftMetadata::default();
585 metadata.data_uris.push("example.com".to_string());
586
587 let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
588 .create(ctx)?
589 .mint_nft(
590 ctx,
591 NftMint::new(
592 metadata,
593 alice.puzzle_hash,
594 300,
595 Some(TransferNft::new(
596 Some(did.info.launcher_id),
597 Vec::new(),
598 Some(did.info.inner_puzzle_hash().into()),
599 )),
600 ),
601 )?;
602 let _did = did.update(ctx, &alice_p2, mint_nft)?;
603
604 let parent_coin = nft.coin;
605 let expected_nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
606
607 sim.spend_coins(ctx.take(), &[alice.sk])?;
608
609 let mut allocator = Allocator::new();
610
611 let puzzle_reveal = sim
612 .puzzle_reveal(parent_coin.coin_id())
613 .expect("missing puzzle")
614 .to_clvm(&mut allocator)?;
615
616 let solution = sim
617 .solution(parent_coin.coin_id())
618 .expect("missing solution")
619 .to_clvm(&mut allocator)?;
620
621 let puzzle = Puzzle::parse(&allocator, puzzle_reveal);
622
623 let nft = Nft::<NftMetadata>::parse_child(&mut allocator, parent_coin, puzzle, solution)?
624 .expect("could not parse nft");
625
626 assert_eq!(nft, expected_nft);
627
628 Ok(())
629 }
630}