use std::{path::PathBuf, vec};
use borsh::BorshDeserialize;
use nifty_asset_types::constraints::{
Account, ConstraintBuilder, NotBuilder, OwnedByBuilder, PubkeyMatchBuilder,
};
use solana_program::{instruction::Instruction, pubkey::Pubkey};
use thiserror::Error;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
instructions::{
Allocate, AllocateInstructionArgs, Create, CreateInstructionArgs, Write,
WriteInstructionArgs,
},
types::{ExtensionInput, ExtensionType, Standard},
};
pub struct MintIxArgs {
pub accounts: MintAccounts,
pub asset_args: AssetArgs,
pub extension_args: Vec<ExtensionArgs>,
}
pub struct MintAccounts {
pub asset: Pubkey,
pub owner: Pubkey,
pub payer: Option<Pubkey>,
}
pub struct AssetArgs {
pub name: String,
pub standard: Standard,
pub mutable: bool,
}
pub struct ExtensionArgs {
pub extension_type: ExtensionType,
pub data: Vec<u8>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssetFile {
pub name: String,
pub standard: Standard,
pub mutable: bool,
pub extensions: Vec<JsonExtension>,
#[cfg_attr(
feature = "serde",
serde(with = "serde_with::As::<serde_with::DisplayFromStr>")
)]
pub owner: Pubkey,
pub asset_keypair_path: Option<PathBuf>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonExtension {
pub extension_type: ExtensionType,
pub value: ExtensionValue,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtensionValue {
JsonCreator(Vec<JsonCreator>),
JsonAttribute(Vec<JsonAttribute>),
JsonLink(Vec<JsonLink>),
JsonBlob(JsonBlob),
JsonMetadata(JsonMetadata),
JsonRoyalities(JsonRoyalties),
}
impl ExtensionValue {
pub fn into_data(self) -> Vec<u8> {
match self {
Self::JsonCreator(value) => value.into_iter().fold(vec![], |mut acc, creator| {
acc.extend(creator.into_data());
acc
}),
Self::JsonAttribute(value) => value.into_iter().fold(vec![], |mut acc, attribute| {
acc.extend(attribute.into_data());
acc
}),
Self::JsonLink(value) => value.into_iter().fold(vec![], |mut acc, link| {
acc.extend(link.into_data());
acc
}),
Self::JsonBlob(value) => value.into_data(),
Self::JsonMetadata(value) => value.into_data(),
Self::JsonRoyalities(value) => value.into_data(),
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonCreator {
#[cfg_attr(
feature = "serde",
serde(with = "serde_with::As::<serde_with::DisplayFromStr>")
)]
pub address: Pubkey,
pub verified: bool,
pub share: u8,
}
impl JsonCreator {
pub const LEN: usize = std::mem::size_of::<Self>();
pub fn into_data(self) -> Vec<u8> {
let mut data = vec![];
data.extend(self.address.to_bytes());
data.extend([self.verified as u8, self.share]);
data
}
pub fn from_data(data: &[u8]) -> Self {
Self {
address: Pubkey::try_from_slice(&data[0..32]).unwrap(),
verified: data[32] != 0,
share: data[33],
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonAttribute {
pub name: String,
pub value: String,
}
impl JsonAttribute {
pub fn into_data(self) -> Vec<u8> {
let mut data = vec![];
let name_bytes = self.name.into_bytes();
data.push(name_bytes.len() as u8);
data.extend(name_bytes);
let value_bytes = self.value.into_bytes();
data.push(value_bytes.len() as u8);
data.extend(value_bytes);
data
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonLink {
pub name: String,
pub uri: String,
}
impl JsonLink {
pub fn into_data(self) -> Vec<u8> {
let mut data = vec![];
let name_bytes = self.name.into_bytes();
data.push(name_bytes.len() as u8);
data.extend(name_bytes);
let uri_bytes = self.uri.into_bytes();
data.push(uri_bytes.len() as u8);
data.extend(uri_bytes);
data
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonBlob {
pub content_type: String,
pub path: String,
}
impl JsonBlob {
pub fn into_data(self) -> Vec<u8> {
let mut data = vec![];
let content_type_bytes = self.content_type.into_bytes();
data.push(content_type_bytes.len() as u8);
data.extend(content_type_bytes);
let path = PathBuf::from(self.path);
let blob_data = std::fs::read(path).expect("failed to read blob file");
data.extend(blob_data);
data
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonMetadata {
pub symbol: String,
pub description: String,
pub uri: String,
}
impl JsonMetadata {
pub fn into_data(self) -> Vec<u8> {
let mut data = vec![];
let symbol_bytes = self.symbol.into_bytes();
data.push(symbol_bytes.len() as u8);
data.extend(symbol_bytes);
let description_bytes = self.description.into_bytes();
data.push(description_bytes.len() as u8);
data.extend(description_bytes);
let uri_bytes = self.uri.into_bytes();
data.push(uri_bytes.len() as u8);
data.extend(uri_bytes);
data
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonRoyalties {
pub kind: RoyaltiesKind,
pub basis_points: u64,
#[cfg_attr(
feature = "serde",
serde(with = "serde_with::As::<Vec<serde_with::DisplayFromStr>>")
)]
pub items: Vec<Pubkey>,
}
impl JsonRoyalties {
pub fn into_data(self) -> Vec<u8> {
let mut data = vec![];
data.extend(self.basis_points.to_le_bytes());
match self.kind {
RoyaltiesKind::Allowlist => {
let mut builder = PubkeyMatchBuilder::default();
builder.set(Account::Asset, &self.items);
let bytes = builder.build();
data.extend(bytes);
}
RoyaltiesKind::Denylist => {
let mut owned_by_builder = OwnedByBuilder::default();
owned_by_builder.set(Account::Asset, &self.items);
let mut builder = NotBuilder::default();
builder.set(&mut owned_by_builder);
let bytes = builder.build();
data.extend(bytes);
}
}
data
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoyaltiesKind {
Allowlist,
Denylist,
}
impl RoyaltiesKind {
pub fn into_data(self) -> u8 {
match self {
Self::Allowlist => 0,
Self::Denylist => 1,
}
}
}
#[derive(Debug, Error)]
pub enum MintError {
#[error("Invalid extension type: {0}")]
InvalidExtensionType(String),
#[error("Invalid extension data: {0}")]
InvalidExtensionData(String),
}
pub const MAX_TX_SIZE: usize = 1232;
pub const ALLOCATE_TX_OVERHEAD: usize = 312;
pub const WRITE_TX_OVERHEAD: usize = ALLOCATE_TX_OVERHEAD - 5;
pub const MAX_ALLOCATE_DATA_SIZE: usize = MAX_TX_SIZE - ALLOCATE_TX_OVERHEAD;
pub const MAX_WRITE_DATA_SIZE: usize = MAX_TX_SIZE - WRITE_TX_OVERHEAD;
pub fn mint(args: MintIxArgs) -> Result<Vec<Instruction>, MintError> {
let mut instructions = vec![];
let payer = args.accounts.payer.unwrap_or(args.accounts.owner);
for extension in args.extension_args.iter() {
let extension_data_len = extension.data.len();
let ix_args = AllocateInstructionArgs {
extension: ExtensionInput {
extension_type: extension.extension_type.clone(),
length: extension.data.len() as u32,
data: Some(
extension.data[..std::cmp::min(extension_data_len, MAX_ALLOCATE_DATA_SIZE)]
.to_vec(),
),
},
};
instructions.push(
Allocate {
asset: args.accounts.asset,
payer: Some(payer),
system_program: Some(solana_program::system_program::id()),
}
.instruction(ix_args),
);
if extension_data_len > MAX_ALLOCATE_DATA_SIZE {
for chunk in extension.data[MAX_ALLOCATE_DATA_SIZE..].chunks(MAX_WRITE_DATA_SIZE) {
let ix_args = WriteInstructionArgs {
overwrite: false,
bytes: chunk.to_vec(),
};
instructions.push(
Write {
asset: args.accounts.asset,
payer,
system_program: solana_program::system_program::id(),
}
.instruction(ix_args),
);
}
}
}
let ix_args = CreateInstructionArgs {
name: args.asset_args.name,
standard: args.asset_args.standard,
mutable: args.asset_args.mutable,
extensions: None,
};
instructions.push(
Create {
asset: args.accounts.asset,
authority: (args.accounts.owner, false),
owner: args.accounts.owner,
payer: Some(payer),
group: None,
group_authority: None,
system_program: Some(solana_program::system_program::id()),
}
.instruction(ix_args),
);
Ok(instructions)
}