use chia_protocol::Bytes32;
use chia_puzzles::{
NFT_INTERMEDIATE_LAUNCHER_HASH, NFT_METADATA_UPDATER_DEFAULT_HASH, NFT_OWNERSHIP_LAYER_HASH,
NFT_OWNERSHIP_TRANSFER_PROGRAM_ONE_WAY_CLAIM_WITH_ROYALTIES_HASH, NFT_STATE_LAYER_HASH,
SINGLETON_LAUNCHER_HASH,
};
use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, Raw, ToClvm, ToClvmError};
use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash};
use crate::singleton::SingletonStruct;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[clvm(curry)]
pub struct NftIntermediateLauncherArgs {
pub launcher_puzzle_hash: Bytes32,
pub mint_number: usize,
pub mint_total: usize,
}
impl NftIntermediateLauncherArgs {
pub fn new(mint_number: usize, mint_total: usize) -> Self {
Self {
launcher_puzzle_hash: SINGLETON_LAUNCHER_HASH.into(),
mint_number,
mint_total,
}
}
pub fn curry_tree_hash(mint_number: usize, mint_total: usize) -> TreeHash {
CurriedProgram {
program: TreeHash::new(NFT_INTERMEDIATE_LAUNCHER_HASH),
args: NftIntermediateLauncherArgs {
launcher_puzzle_hash: SINGLETON_LAUNCHER_HASH.into(),
mint_number,
mint_total,
},
}
.tree_hash()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[clvm(curry)]
pub struct NftStateLayerArgs<I, M> {
pub mod_hash: Bytes32,
pub metadata: M,
pub metadata_updater_puzzle_hash: Bytes32,
pub inner_puzzle: I,
}
impl<I, M> NftStateLayerArgs<I, M> {
pub fn new(metadata: M, inner_puzzle: I) -> Self {
Self {
mod_hash: NFT_STATE_LAYER_HASH.into(),
metadata,
metadata_updater_puzzle_hash: NFT_METADATA_UPDATER_DEFAULT_HASH.into(),
inner_puzzle,
}
}
}
impl NftStateLayerArgs<TreeHash, TreeHash> {
pub fn curry_tree_hash(metadata: TreeHash, inner_puzzle: TreeHash) -> TreeHash {
CurriedProgram {
program: TreeHash::new(NFT_STATE_LAYER_HASH),
args: NftStateLayerArgs {
mod_hash: NFT_STATE_LAYER_HASH.into(),
metadata,
metadata_updater_puzzle_hash: NFT_METADATA_UPDATER_DEFAULT_HASH.into(),
inner_puzzle,
},
}
.tree_hash()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[clvm(list)]
pub struct NftStateLayerSolution<I> {
pub inner_solution: I,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[clvm(curry)]
pub struct NftOwnershipLayerArgs<I, P> {
pub mod_hash: Bytes32,
pub current_owner: Option<Bytes32>,
pub transfer_program: P,
pub inner_puzzle: I,
}
impl<I, P> NftOwnershipLayerArgs<I, P> {
pub fn new(current_owner: Option<Bytes32>, transfer_program: P, inner_puzzle: I) -> Self {
Self {
mod_hash: NFT_OWNERSHIP_LAYER_HASH.into(),
current_owner,
transfer_program,
inner_puzzle,
}
}
}
impl NftOwnershipLayerArgs<TreeHash, TreeHash> {
pub fn curry_tree_hash(
current_owner: Option<Bytes32>,
transfer_program: TreeHash,
inner_puzzle: TreeHash,
) -> TreeHash {
CurriedProgram {
program: TreeHash::new(NFT_OWNERSHIP_LAYER_HASH),
args: NftOwnershipLayerArgs {
mod_hash: NFT_OWNERSHIP_LAYER_HASH.into(),
current_owner,
transfer_program,
inner_puzzle,
},
}
.tree_hash()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[clvm(list)]
pub struct NftOwnershipLayerSolution<I> {
pub inner_solution: I,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[clvm(curry)]
pub struct NftRoyaltyTransferPuzzleArgs {
pub singleton_struct: SingletonStruct,
pub royalty_puzzle_hash: Bytes32,
pub royalty_ten_thousandths: u16,
}
impl NftRoyaltyTransferPuzzleArgs {
pub fn new(
launcher_id: Bytes32,
royalty_puzzle_hash: Bytes32,
royalty_ten_thousandths: u16,
) -> Self {
Self {
singleton_struct: SingletonStruct::new(launcher_id),
royalty_puzzle_hash,
royalty_ten_thousandths,
}
}
pub fn curry_tree_hash(
launcher_id: Bytes32,
royalty_puzzle_hash: Bytes32,
royalty_ten_thousandths: u16,
) -> TreeHash {
CurriedProgram {
program: TreeHash::new(
NFT_OWNERSHIP_TRANSFER_PROGRAM_ONE_WAY_CLAIM_WITH_ROYALTIES_HASH,
),
args: NftRoyaltyTransferPuzzleArgs {
singleton_struct: SingletonStruct::new(launcher_id),
royalty_puzzle_hash,
royalty_ten_thousandths,
},
}
.tree_hash()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct NftMetadata {
pub edition_number: u64,
pub edition_total: u64,
pub data_uris: Vec<String>,
pub data_hash: Option<Bytes32>,
pub metadata_uris: Vec<String>,
pub metadata_hash: Option<Bytes32>,
pub license_uris: Vec<String>,
pub license_hash: Option<Bytes32>,
}
impl Default for NftMetadata {
fn default() -> Self {
Self {
edition_number: 1,
edition_total: 1,
data_uris: Vec::new(),
data_hash: None,
metadata_uris: Vec::new(),
metadata_hash: None,
license_uris: Vec::new(),
license_hash: None,
}
}
}
impl<N, D: ClvmDecoder<Node = N>> FromClvm<D> for NftMetadata {
fn from_clvm(decoder: &D, node: N) -> Result<Self, FromClvmError> {
let items: Vec<(String, Raw<N>)> = FromClvm::from_clvm(decoder, node)?;
let mut metadata = Self::default();
for (key, value_ptr) in items {
match key.as_str() {
"sn" => {
metadata.edition_number =
FromClvm::from_clvm(decoder, value_ptr.0).unwrap_or(1);
}
"st" => {
metadata.edition_total = FromClvm::from_clvm(decoder, value_ptr.0).unwrap_or(1);
}
"u" => {
metadata.data_uris =
FromClvm::from_clvm(decoder, value_ptr.0).unwrap_or_default();
}
"h" => {
metadata.data_hash =
FromClvm::from_clvm(decoder, value_ptr.0).unwrap_or_default();
}
"mu" => {
metadata.metadata_uris =
FromClvm::from_clvm(decoder, value_ptr.0).unwrap_or_default();
}
"mh" => {
metadata.metadata_hash =
FromClvm::from_clvm(decoder, value_ptr.0).unwrap_or_default();
}
"lu" => {
metadata.license_uris =
FromClvm::from_clvm(decoder, value_ptr.0).unwrap_or_default();
}
"lh" => {
metadata.license_hash =
FromClvm::from_clvm(decoder, value_ptr.0).unwrap_or_default();
}
_ => (),
}
}
Ok(metadata)
}
}
impl<N, E: ClvmEncoder<Node = N>> ToClvm<E> for NftMetadata {
fn to_clvm(&self, encoder: &mut E) -> Result<N, ToClvmError> {
let mut items: Vec<(&str, Raw<N>)> = Vec::new();
if !self.data_uris.is_empty() {
items.push(("u", Raw(self.data_uris.to_clvm(encoder)?)));
}
if let Some(hash) = self.data_hash {
items.push(("h", Raw(hash.to_clvm(encoder)?)));
}
if !self.metadata_uris.is_empty() {
items.push(("mu", Raw(self.metadata_uris.to_clvm(encoder)?)));
}
if let Some(hash) = self.metadata_hash {
items.push(("mh", Raw(hash.to_clvm(encoder)?)));
}
if !self.license_uris.is_empty() {
items.push(("lu", Raw(self.license_uris.to_clvm(encoder)?)));
}
if let Some(hash) = self.license_hash {
items.push(("lh", Raw(hash.to_clvm(encoder)?)));
}
items.extend(vec![
("sn", Raw(self.edition_number.to_clvm(encoder)?)),
("st", Raw(self.edition_total.to_clvm(encoder)?)),
]);
items.to_clvm(encoder)
}
}
#[cfg(test)]
mod tests {
use clvm_traits::clvm_list;
use clvmr::Allocator;
use super::*;
#[test]
fn test_nft_metadata_roundtrip() {
let mut allocator = Allocator::new();
let metadata = NftMetadata {
edition_number: 1,
edition_total: 1,
data_uris: vec!["https://example.com/data.json".to_string()],
data_hash: Some(Bytes32::default()),
metadata_uris: vec!["https://example.com/metadata.json".to_string()],
metadata_hash: Some(Bytes32::default()),
license_uris: vec!["https://example.com/license.txt".to_string()],
license_hash: Some(Bytes32::default()),
};
let encoded = metadata.to_clvm(&mut allocator).unwrap();
let decoded = NftMetadata::from_clvm(&allocator, encoded).unwrap();
assert_eq!(metadata, decoded);
}
#[test]
fn test_malformed_nft_metadata() {
let mut allocator = Allocator::new();
let metadata = clvm_list!(
("sn", (1, 2)),
("st", (1, 2)),
("u", 1),
("h", 2),
("mu", 3),
("mh", 4),
("lu", 5),
("lh", 6),
);
let encoded = metadata.to_clvm(&mut allocator).unwrap();
let decoded = NftMetadata::from_clvm(&allocator, encoded).unwrap();
assert_eq!(NftMetadata::default(), decoded);
}
}