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
13use crate::{
16 instructions::{
17 Allocate, AllocateInstructionArgs, Create, CreateInstructionArgs, Write,
18 WriteInstructionArgs,
19 },
20 types::{ExtensionInput, ExtensionType, Standard},
21};
22
23pub struct MintIxArgs {
25 pub accounts: MintAccounts,
26 pub asset_args: AssetArgs,
27 pub extension_args: Vec<ExtensionArgs>,
28}
29
30pub struct MintAccounts {
32 pub asset: Pubkey,
33 pub owner: Pubkey,
34 pub payer: Option<Pubkey>,
36}
37
38pub struct AssetArgs {
40 pub name: String,
41 pub standard: Standard,
42 pub mutable: bool,
43}
44
45pub struct ExtensionArgs {
47 pub extension_type: ExtensionType,
48 pub data: Vec<u8>,
49}
50
51#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
310pub 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
316pub 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 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 if extension_data_len > MAX_ALLOCATE_DATA_SIZE {
349 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 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}