chia_sdk_driver/primitives/nft/
nft_info.rs1use chia_protocol::Bytes32;
2use chia_puzzle_types::nft::{NftOwnershipLayerArgs, NftStateLayerArgs};
3use chia_puzzles::NFT_STATE_LAYER_HASH;
4use chia_sdk_types::{
5 Condition, Mod,
6 conditions::{CreateCoin, NewMetadataOutput},
7 run_puzzle,
8};
9use clvm_traits::{FromClvm, ToClvm, clvm_list};
10use clvm_utils::{ToTreeHash, TreeHash};
11use clvmr::{Allocator, NodePtr};
12
13use crate::{
14 DriverError, HashedPtr, Layer, NftOwnershipLayer, NftStateLayer, Puzzle, RoyaltyTransferLayer,
15 SingletonInfo, SingletonLayer, Spend,
16};
17
18pub type StandardNftLayers<M, I> =
19 SingletonLayer<NftStateLayer<M, NftOwnershipLayer<RoyaltyTransferLayer, I>>>;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct NftInfo {
27 pub launcher_id: Bytes32,
29
30 pub metadata: HashedPtr,
33
34 pub metadata_updater_puzzle_hash: Bytes32,
42
43 pub current_owner: Option<Bytes32>,
49
50 pub royalty_puzzle_hash: Bytes32,
54
55 pub royalty_basis_points: u16,
58
59 pub p2_puzzle_hash: Bytes32,
62}
63
64impl NftInfo {
65 pub fn new(
66 launcher_id: Bytes32,
67 metadata: HashedPtr,
68 metadata_updater_puzzle_hash: Bytes32,
69 current_owner: Option<Bytes32>,
70 royalty_puzzle_hash: Bytes32,
71 royalty_basis_points: u16,
72 p2_puzzle_hash: Bytes32,
73 ) -> Self {
74 Self {
75 launcher_id,
76 metadata,
77 metadata_updater_puzzle_hash,
78 current_owner,
79 royalty_puzzle_hash,
80 royalty_basis_points,
81 p2_puzzle_hash,
82 }
83 }
84
85 pub fn parse(
92 allocator: &Allocator,
93 puzzle: Puzzle,
94 ) -> Result<Option<(Self, Puzzle)>, DriverError> {
95 let Some(layers) = StandardNftLayers::<HashedPtr, Puzzle>::parse_puzzle(allocator, puzzle)?
96 else {
97 return Ok(None);
98 };
99
100 let p2_puzzle = layers.inner_puzzle.inner_puzzle.inner_puzzle;
101
102 Ok(Some((Self::from_layers(&layers), p2_puzzle)))
103 }
104
105 pub fn from_layers<I>(layers: &StandardNftLayers<HashedPtr, I>) -> Self
106 where
107 I: ToTreeHash,
108 {
109 Self {
110 launcher_id: layers.launcher_id,
111 metadata: layers.inner_puzzle.metadata,
112 metadata_updater_puzzle_hash: layers.inner_puzzle.metadata_updater_puzzle_hash,
113 current_owner: layers.inner_puzzle.inner_puzzle.current_owner,
114 royalty_puzzle_hash: layers
115 .inner_puzzle
116 .inner_puzzle
117 .transfer_layer
118 .royalty_puzzle_hash,
119 royalty_basis_points: layers
120 .inner_puzzle
121 .inner_puzzle
122 .transfer_layer
123 .royalty_basis_points,
124 p2_puzzle_hash: layers
125 .inner_puzzle
126 .inner_puzzle
127 .inner_puzzle
128 .tree_hash()
129 .into(),
130 }
131 }
132
133 #[must_use]
134 pub fn into_layers<I>(self, p2_puzzle: I) -> StandardNftLayers<HashedPtr, I> {
135 SingletonLayer::new(
136 self.launcher_id,
137 NftStateLayer::new(
138 self.metadata,
139 self.metadata_updater_puzzle_hash,
140 NftOwnershipLayer::new(
141 self.current_owner,
142 RoyaltyTransferLayer::new(
143 self.launcher_id,
144 self.royalty_puzzle_hash,
145 self.royalty_basis_points,
146 ),
147 p2_puzzle,
148 ),
149 ),
150 )
151 }
152
153 pub fn child_from_p2_spend(
159 &self,
160 allocator: &mut Allocator,
161 spend: Spend,
162 ) -> Result<(Self, CreateCoin<NodePtr>), DriverError> {
163 let output = run_puzzle(allocator, spend.puzzle, spend.solution)?;
164 let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
165
166 let mut create_coin = None;
167 let mut new_owner = None;
168 let mut new_metadata = None;
169
170 for condition in conditions {
171 match condition {
172 Condition::CreateCoin(condition) if condition.amount % 2 == 1 => {
173 create_coin = Some(condition);
174 }
175 Condition::TransferNft(condition) => {
176 new_owner = Some(condition);
177 }
178 Condition::UpdateNftMetadata(condition) => {
179 new_metadata = Some(condition);
180 }
181 _ => {}
182 }
183 }
184
185 let Some(create_coin) = create_coin else {
186 return Err(DriverError::MissingChild);
187 };
188
189 let mut info = *self;
190
191 if let Some(new_owner) = new_owner {
192 info.current_owner = new_owner.launcher_id;
193 }
194
195 if let Some(new_metadata) = new_metadata {
196 let metadata_updater_solution = clvm_list!(
197 &self.metadata,
198 self.metadata_updater_puzzle_hash,
199 new_metadata.updater_solution
200 )
201 .to_clvm(allocator)?;
202
203 let output = run_puzzle(
204 allocator,
205 new_metadata.updater_puzzle_reveal,
206 metadata_updater_solution,
207 )?;
208
209 let output = NewMetadataOutput::<HashedPtr, NodePtr>::from_clvm(allocator, output)?
210 .metadata_info;
211 info.metadata = output.new_metadata;
212 info.metadata_updater_puzzle_hash = output.new_updater_puzzle_hash;
213 }
214
215 info.p2_puzzle_hash = create_coin.puzzle_hash;
216
217 Ok((info, create_coin))
218 }
219}
220
221impl SingletonInfo for NftInfo {
222 fn launcher_id(&self) -> Bytes32 {
223 self.launcher_id
224 }
225
226 fn inner_puzzle_hash(&self) -> TreeHash {
227 NftStateLayerArgs {
228 mod_hash: NFT_STATE_LAYER_HASH.into(),
229 metadata: self.metadata.tree_hash(),
230 metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash,
231 inner_puzzle: NftOwnershipLayerArgs::curry_tree_hash(
232 self.current_owner,
233 RoyaltyTransferLayer::new(
234 self.launcher_id,
235 self.royalty_puzzle_hash,
236 self.royalty_basis_points,
237 )
238 .tree_hash(),
239 self.p2_puzzle_hash.into(),
240 ),
241 }
242 .curry_tree_hash()
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use chia_puzzle_types::nft::NftMetadata;
249 use chia_sdk_test::Simulator;
250 use chia_sdk_types::{Conditions, conditions::TransferNft};
251
252 use crate::{
253 IntermediateLauncher, Launcher, NftMint, SingletonInfo, SpendContext, StandardLayer,
254 };
255
256 use super::*;
257
258 #[test]
259 fn test_parse_nft_info() -> anyhow::Result<()> {
260 let mut sim = Simulator::new();
261 let ctx = &mut SpendContext::new();
262
263 let alice = sim.bls(2);
264 let alice_p2 = StandardLayer::new(alice.pk);
265
266 let (create_did, did) =
267 Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
268 alice_p2.spend(ctx, alice.coin, create_did)?;
269
270 let mut metadata = NftMetadata::default();
271 metadata.data_uris.push("example.com".to_string());
272
273 let metadata = ctx.alloc_hashed(&metadata)?;
274
275 let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
276 .create(ctx)?
277 .mint_nft(
278 ctx,
279 &NftMint::new(
280 metadata,
281 alice.puzzle_hash,
282 300,
283 Some(TransferNft::new(
284 Some(did.info.launcher_id),
285 Vec::new(),
286 Some(did.info.inner_puzzle_hash().into()),
287 )),
288 ),
289 )?;
290
291 let _did = did.update(ctx, &alice_p2, mint_nft)?;
292 let original_nft = nft;
293 let _nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
294
295 sim.spend_coins(ctx.take(), &[alice.sk])?;
296
297 let puzzle_reveal = sim
298 .puzzle_reveal(original_nft.coin.coin_id())
299 .expect("missing nft puzzle");
300
301 let mut allocator = Allocator::new();
302 let ptr = puzzle_reveal.to_clvm(&mut allocator)?;
303 let puzzle = Puzzle::parse(&allocator, ptr);
304 let (nft_info, p2_puzzle) = NftInfo::parse(&allocator, puzzle)?.expect("not an nft");
305
306 assert_eq!(nft_info, original_nft.info);
307 assert_eq!(p2_puzzle.curried_puzzle_hash(), alice.puzzle_hash.into());
308
309 Ok(())
310 }
311}