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