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