use chia_protocol::Bytes32;
use chia_puzzle_types::nft::{NftOwnershipLayerArgs, NftStateLayerArgs};
use chia_puzzles::NFT_STATE_LAYER_HASH;
use chia_sdk_types::{
Condition, Mod,
conditions::{CreateCoin, NewMetadataOutput},
run_puzzle,
};
use clvm_traits::{FromClvm, ToClvm, clvm_list};
use clvm_utils::{ToTreeHash, TreeHash};
use clvmr::{Allocator, NodePtr};
use crate::{
DriverError, HashedPtr, Layer, NftOwnershipLayer, NftStateLayer, Puzzle, RoyaltyTransferLayer,
SingletonInfo, SingletonLayer, Spend,
};
pub type StandardNftLayers<M, I> =
SingletonLayer<NftStateLayer<M, NftOwnershipLayer<RoyaltyTransferLayer, I>>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NftInfo {
pub launcher_id: Bytes32,
pub metadata: HashedPtr,
pub metadata_updater_puzzle_hash: Bytes32,
pub current_owner: Option<Bytes32>,
pub royalty_puzzle_hash: Bytes32,
pub royalty_basis_points: u16,
pub p2_puzzle_hash: Bytes32,
}
impl NftInfo {
pub fn new(
launcher_id: Bytes32,
metadata: HashedPtr,
metadata_updater_puzzle_hash: Bytes32,
current_owner: Option<Bytes32>,
royalty_puzzle_hash: Bytes32,
royalty_basis_points: u16,
p2_puzzle_hash: Bytes32,
) -> Self {
Self {
launcher_id,
metadata,
metadata_updater_puzzle_hash,
current_owner,
royalty_puzzle_hash,
royalty_basis_points,
p2_puzzle_hash,
}
}
pub fn parse(
allocator: &Allocator,
puzzle: Puzzle,
) -> Result<Option<(Self, Puzzle)>, DriverError> {
let Some(layers) = StandardNftLayers::<HashedPtr, Puzzle>::parse_puzzle(allocator, puzzle)?
else {
return Ok(None);
};
let p2_puzzle = layers.inner_puzzle.inner_puzzle.inner_puzzle;
Ok(Some((Self::from_layers(&layers), p2_puzzle)))
}
pub fn from_layers<I>(layers: &StandardNftLayers<HashedPtr, I>) -> Self
where
I: ToTreeHash,
{
Self {
launcher_id: layers.launcher_id,
metadata: layers.inner_puzzle.metadata,
metadata_updater_puzzle_hash: layers.inner_puzzle.metadata_updater_puzzle_hash,
current_owner: layers.inner_puzzle.inner_puzzle.current_owner,
royalty_puzzle_hash: layers
.inner_puzzle
.inner_puzzle
.transfer_layer
.royalty_puzzle_hash,
royalty_basis_points: layers
.inner_puzzle
.inner_puzzle
.transfer_layer
.royalty_basis_points,
p2_puzzle_hash: layers
.inner_puzzle
.inner_puzzle
.inner_puzzle
.tree_hash()
.into(),
}
}
#[must_use]
pub fn into_layers<I>(self, p2_puzzle: I) -> StandardNftLayers<HashedPtr, I> {
SingletonLayer::new(
self.launcher_id,
NftStateLayer::new(
self.metadata,
self.metadata_updater_puzzle_hash,
NftOwnershipLayer::new(
self.current_owner,
RoyaltyTransferLayer::new(
self.launcher_id,
self.royalty_puzzle_hash,
self.royalty_basis_points,
),
p2_puzzle,
),
),
)
}
pub fn child_from_p2_spend(
&self,
allocator: &mut Allocator,
spend: Spend,
) -> Result<(Self, CreateCoin<NodePtr>), DriverError> {
let output = run_puzzle(allocator, spend.puzzle, spend.solution)?;
let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
let mut create_coin = None;
let mut new_owner = None;
let mut new_metadata = None;
for condition in conditions {
match condition {
Condition::CreateCoin(condition) if condition.amount % 2 == 1 => {
create_coin = Some(condition);
}
Condition::TransferNft(condition) => {
new_owner = Some(condition);
}
Condition::UpdateNftMetadata(condition) => {
new_metadata = Some(condition);
}
_ => {}
}
}
let Some(create_coin) = create_coin else {
return Err(DriverError::MissingChild);
};
let mut info = *self;
if let Some(new_owner) = new_owner {
info.current_owner = new_owner.launcher_id;
}
if let Some(new_metadata) = new_metadata {
let metadata_updater_solution = clvm_list!(
&self.metadata,
self.metadata_updater_puzzle_hash,
new_metadata.updater_solution
)
.to_clvm(allocator)?;
let output = run_puzzle(
allocator,
new_metadata.updater_puzzle_reveal,
metadata_updater_solution,
)?;
let output = NewMetadataOutput::<HashedPtr, NodePtr>::from_clvm(allocator, output)?
.metadata_info;
info.metadata = output.new_metadata;
info.metadata_updater_puzzle_hash = output.new_updater_puzzle_hash;
}
info.p2_puzzle_hash = create_coin.puzzle_hash;
Ok((info, create_coin))
}
}
impl SingletonInfo for NftInfo {
fn launcher_id(&self) -> Bytes32 {
self.launcher_id
}
fn inner_puzzle_hash(&self) -> TreeHash {
NftStateLayerArgs {
mod_hash: NFT_STATE_LAYER_HASH.into(),
metadata: self.metadata.tree_hash(),
metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash,
inner_puzzle: NftOwnershipLayerArgs::curry_tree_hash(
self.current_owner,
RoyaltyTransferLayer::new(
self.launcher_id,
self.royalty_puzzle_hash,
self.royalty_basis_points,
)
.tree_hash(),
self.p2_puzzle_hash.into(),
),
}
.curry_tree_hash()
}
}
#[cfg(test)]
mod tests {
use chia_puzzle_types::nft::NftMetadata;
use chia_sdk_test::Simulator;
use chia_sdk_types::{Conditions, conditions::TransferNft};
use crate::{
IntermediateLauncher, Launcher, NftMint, SingletonInfo, SpendContext, StandardLayer,
};
use super::*;
#[test]
fn test_parse_nft_info() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let alice = sim.bls(2);
let alice_p2 = StandardLayer::new(alice.pk);
let (create_did, did) =
Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
alice_p2.spend(ctx, alice.coin, create_did)?;
let mut metadata = NftMetadata::default();
metadata.data_uris.push("example.com".to_string());
let metadata = ctx.alloc_hashed(&metadata)?;
let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
.create(ctx)?
.mint_nft(
ctx,
&NftMint::new(
metadata,
alice.puzzle_hash,
300,
Some(TransferNft::new(
Some(did.info.launcher_id),
Vec::new(),
Some(did.info.inner_puzzle_hash().into()),
)),
),
)?;
let _did = did.update(ctx, &alice_p2, mint_nft)?;
let original_nft = nft;
let _nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
sim.spend_coins(ctx.take(), &[alice.sk])?;
let puzzle_reveal = sim
.puzzle_reveal(original_nft.coin.coin_id())
.expect("missing nft puzzle");
let mut allocator = Allocator::new();
let ptr = puzzle_reveal.to_clvm(&mut allocator)?;
let puzzle = Puzzle::parse(&allocator, ptr);
let (nft_info, p2_puzzle) = NftInfo::parse(&allocator, puzzle)?.expect("not an nft");
assert_eq!(nft_info, original_nft.info);
assert_eq!(p2_puzzle.curried_puzzle_hash(), alice.puzzle_hash.into());
Ok(())
}
}