nifty_asset/
mint.rs

1use std::{path::PathBuf, vec};
2
3use borsh::BorshDeserialize;
4use nifty_asset_types::constraints::{
5    Account, ConstraintBuilder, NotBuilder, OwnedByBuilder, PubkeyMatchBuilder,
6};
7use solana_program::{instruction::Instruction, pubkey::Pubkey};
8use thiserror::Error;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// Creates a full asset by allocating any extensions, writing the data to them, and then calling
14/// `create` to finalize the asset.
15use crate::{
16    instructions::{
17        Allocate, AllocateInstructionArgs, Create, CreateInstructionArgs, Write,
18        WriteInstructionArgs,
19    },
20    types::{ExtensionInput, ExtensionType, Standard},
21};
22
23/// Mint instruction args.
24pub struct MintIxArgs {
25    pub accounts: MintAccounts,
26    pub asset_args: AssetArgs,
27    pub extension_args: Vec<ExtensionArgs>,
28}
29
30/// Mint instruction accounts.
31pub struct MintAccounts {
32    pub asset: Pubkey,
33    pub owner: Pubkey,
34    /// If not specified, the owner is used as the payer.
35    pub payer: Option<Pubkey>,
36}
37
38/// Mint instruction asset sub-args.
39pub struct AssetArgs {
40    pub name: String,
41    pub standard: Standard,
42    pub mutable: bool,
43}
44
45/// Mint instruction extension sub-args.
46pub struct ExtensionArgs {
47    pub extension_type: ExtensionType,
48    pub data: Vec<u8>,
49}
50
51/// A type suitable for JSON serde de/serialization.
52#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct AssetFile {
55    pub name: String,
56    pub standard: Standard,
57    pub mutable: bool,
58    pub extensions: Vec<JsonExtension>,
59    #[cfg_attr(
60        feature = "serde",
61        serde(with = "serde_with::As::<serde_with::DisplayFromStr>")
62    )]
63    pub owner: Pubkey,
64    pub asset_keypair_path: Option<PathBuf>,
65}
66
67/// A type suitable for JSON serde de/serialization.
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct JsonExtension {
71    pub extension_type: ExtensionType,
72    pub value: ExtensionValue,
73}
74
75/// A type suitable for JSON serde de/serialization.
76#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
77#[cfg_attr(feature = "serde", serde(untagged))]
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum ExtensionValue {
80    JsonCreator(Vec<JsonCreator>),
81    JsonAttribute(Vec<JsonAttribute>),
82    JsonLink(Vec<JsonLink>),
83    JsonBlob(JsonBlob),
84    JsonMetadata(JsonMetadata),
85    JsonRoyalities(JsonRoyalties),
86}
87
88impl ExtensionValue {
89    pub fn into_data(self) -> Vec<u8> {
90        match self {
91            Self::JsonCreator(value) => value.into_iter().fold(vec![], |mut acc, creator| {
92                acc.extend(creator.into_data());
93                acc
94            }),
95            Self::JsonAttribute(value) => value.into_iter().fold(vec![], |mut acc, attribute| {
96                acc.extend(attribute.into_data());
97                acc
98            }),
99            Self::JsonLink(value) => value.into_iter().fold(vec![], |mut acc, link| {
100                acc.extend(link.into_data());
101                acc
102            }),
103            Self::JsonBlob(value) => value.into_data(),
104            Self::JsonMetadata(value) => value.into_data(),
105            Self::JsonRoyalities(value) => value.into_data(),
106        }
107    }
108}
109
110/// A type suitable for JSON serde de/serialization.
111#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct JsonCreator {
114    #[cfg_attr(
115        feature = "serde",
116        serde(with = "serde_with::As::<serde_with::DisplayFromStr>")
117    )]
118    pub address: Pubkey,
119    pub verified: bool,
120    pub share: u8,
121}
122
123impl JsonCreator {
124    pub const LEN: usize = std::mem::size_of::<Self>();
125
126    pub fn into_data(self) -> Vec<u8> {
127        let mut data = vec![];
128        data.extend(self.address.to_bytes());
129        data.extend([self.verified as u8, self.share]);
130        data
131    }
132
133    pub fn from_data(data: &[u8]) -> Self {
134        Self {
135            address: Pubkey::try_from_slice(&data[0..32]).unwrap(),
136            verified: data[32] != 0,
137            share: data[33],
138        }
139    }
140}
141
142/// A type suitable for JSON serde de/serialization.
143#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct JsonAttribute {
146    pub name: String,
147    pub value: String,
148}
149
150impl JsonAttribute {
151    pub fn into_data(self) -> Vec<u8> {
152        let mut data = vec![];
153
154        let name_bytes = self.name.into_bytes();
155        data.push(name_bytes.len() as u8);
156        data.extend(name_bytes);
157
158        let value_bytes = self.value.into_bytes();
159        data.push(value_bytes.len() as u8);
160        data.extend(value_bytes);
161
162        data
163    }
164}
165
166/// A type suitable for JSON serde de/serialization.
167#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct JsonLink {
170    pub name: String,
171    pub uri: String,
172}
173
174impl JsonLink {
175    pub fn into_data(self) -> Vec<u8> {
176        let mut data = vec![];
177
178        let name_bytes = self.name.into_bytes();
179        data.push(name_bytes.len() as u8);
180        data.extend(name_bytes);
181
182        let uri_bytes = self.uri.into_bytes();
183        data.push(uri_bytes.len() as u8);
184        data.extend(uri_bytes);
185
186        data
187    }
188}
189
190/// A type suitable for JSON serde de/serialization.
191#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct JsonBlob {
194    pub content_type: String,
195    pub path: String,
196}
197
198impl JsonBlob {
199    pub fn into_data(self) -> Vec<u8> {
200        let mut data = vec![];
201
202        let content_type_bytes = self.content_type.into_bytes();
203        data.push(content_type_bytes.len() as u8);
204        data.extend(content_type_bytes);
205
206        let path = PathBuf::from(self.path);
207        let blob_data = std::fs::read(path).expect("failed to read blob file");
208        data.extend(blob_data);
209
210        data
211    }
212}
213
214/// A type suitable for JSON serde de/serialization.
215#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct JsonMetadata {
218    pub symbol: String,
219    pub description: String,
220    pub uri: String,
221}
222
223impl JsonMetadata {
224    pub fn into_data(self) -> Vec<u8> {
225        let mut data = vec![];
226
227        let symbol_bytes = self.symbol.into_bytes();
228        data.push(symbol_bytes.len() as u8);
229        data.extend(symbol_bytes);
230
231        let description_bytes = self.description.into_bytes();
232        data.push(description_bytes.len() as u8);
233        data.extend(description_bytes);
234
235        let uri_bytes = self.uri.into_bytes();
236        data.push(uri_bytes.len() as u8);
237        data.extend(uri_bytes);
238
239        data
240    }
241}
242
243/// A type suitable for JSON serde de/serialization.
244#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct JsonRoyalties {
247    pub kind: RoyaltiesKind,
248    pub basis_points: u64,
249    #[cfg_attr(
250        feature = "serde",
251        serde(with = "serde_with::As::<Vec<serde_with::DisplayFromStr>>")
252    )]
253    pub items: Vec<Pubkey>,
254}
255
256impl JsonRoyalties {
257    pub fn into_data(self) -> Vec<u8> {
258        let mut data = vec![];
259        data.extend(self.basis_points.to_le_bytes());
260
261        match self.kind {
262            RoyaltiesKind::Allowlist => {
263                let mut builder = PubkeyMatchBuilder::default();
264                builder.set(Account::Asset, &self.items);
265                let bytes = builder.build();
266                data.extend(bytes);
267            }
268            RoyaltiesKind::Denylist => {
269                let mut owned_by_builder = OwnedByBuilder::default();
270                owned_by_builder.set(Account::Asset, &self.items);
271                let mut builder = NotBuilder::default();
272                builder.set(&mut owned_by_builder);
273                let bytes = builder.build();
274                data.extend(bytes);
275            }
276        }
277        data
278    }
279}
280
281#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
282#[derive(Debug, Clone, PartialEq, Eq)]
283pub enum RoyaltiesKind {
284    Allowlist,
285    Denylist,
286}
287
288impl RoyaltiesKind {
289    pub fn into_data(self) -> u8 {
290        match self {
291            Self::Allowlist => 0,
292            Self::Denylist => 1,
293        }
294    }
295}
296
297/// Errors returned from the mint command.
298#[derive(Debug, Error)]
299pub enum MintError {
300    #[error("Invalid extension type: {0}")]
301    InvalidExtensionType(String),
302    #[error("Invalid extension data: {0}")]
303    InvalidExtensionData(String),
304}
305
306pub const MAX_TX_SIZE: usize = 1232;
307
308pub const ALLOCATE_TX_OVERHEAD: usize = 312;
309
310// Write only has 1 byte in the data input, instead of 6
311pub const WRITE_TX_OVERHEAD: usize = ALLOCATE_TX_OVERHEAD - 5;
312
313pub const MAX_ALLOCATE_DATA_SIZE: usize = MAX_TX_SIZE - ALLOCATE_TX_OVERHEAD;
314pub const MAX_WRITE_DATA_SIZE: usize = MAX_TX_SIZE - WRITE_TX_OVERHEAD;
315
316/// Returns a vector of instructions to fully mint an asset, including with extensions.
317/// The instructions are returned in the order they should be executed.
318pub fn mint(args: MintIxArgs) -> Result<Vec<Instruction>, MintError> {
319    let mut instructions = vec![];
320
321    let payer = args.accounts.payer.unwrap_or(args.accounts.owner);
322
323    // Extension allocation instructions.
324    for extension in args.extension_args.iter() {
325        let extension_data_len = extension.data.len();
326
327        let ix_args = AllocateInstructionArgs {
328            extension: ExtensionInput {
329                extension_type: extension.extension_type,
330                length: extension.data.len() as u32,
331                data: Some(
332                    extension.data[..std::cmp::min(extension_data_len, MAX_ALLOCATE_DATA_SIZE)]
333                        .to_vec(),
334                ),
335            },
336        };
337
338        instructions.push(
339            Allocate {
340                asset: args.accounts.asset,
341                payer: Some(payer),
342                system_program: Some(solana_program::system_program::id()),
343            }
344            .instruction(ix_args),
345        );
346
347        // Write data instructions if the data is larger than the max allocate data size.
348        if extension_data_len > MAX_ALLOCATE_DATA_SIZE {
349            // Start at the max allocate data size and write the rest of the data in chunks.
350            for chunk in extension.data[MAX_ALLOCATE_DATA_SIZE..].chunks(MAX_WRITE_DATA_SIZE) {
351                let ix_args = WriteInstructionArgs {
352                    overwrite: false,
353                    bytes: chunk.to_vec(),
354                };
355
356                instructions.push(
357                    Write {
358                        asset: args.accounts.asset,
359                        payer,
360                        system_program: solana_program::system_program::id(),
361                    }
362                    .instruction(ix_args),
363                );
364            }
365        }
366    }
367
368    // Finalize the asset by creating it.
369    let ix_args = CreateInstructionArgs {
370        name: args.asset_args.name,
371        standard: args.asset_args.standard,
372        mutable: args.asset_args.mutable,
373        extensions: None,
374    };
375
376    instructions.push(
377        Create {
378            asset: args.accounts.asset,
379            authority: (args.accounts.owner, false),
380            owner: args.accounts.owner,
381            payer: Some(payer),
382            group: None,
383            group_authority: None,
384            system_program: Some(solana_program::system_program::id()),
385        }
386        .instruction(ix_args),
387    );
388
389    Ok(instructions)
390}