use mpl_token_metadata::accounts::Metadata;
use mpl_token_metadata::types::{Key, TokenStandard};
use super::decoder::{AccountDecoder, DecoderError};
use super::types::{
DasAsset, DasAuthority, DasContent, DasCreator, DasFile, DasGrouping, DasLinks, DasMetadata,
DasOwnership,
};
pub struct TokenMetadataDecoder;
impl TokenMetadataDecoder {
pub const PROGRAM_ID: &'static str = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
pub const NAME: &'static str = "V1_NFT";
}
impl AccountDecoder for TokenMetadataDecoder {
fn program_id(&self) -> &str {
Self::PROGRAM_ID
}
fn name(&self) -> &str {
Self::NAME
}
fn decode(&self, pubkey: &str, data: &[u8]) -> Result<Option<DasAsset>, DecoderError> {
if data.is_empty() {
return Ok(None);
}
if data[0] != Key::MetadataV1 as u8 {
return Ok(None);
}
let metadata = Metadata::from_bytes(data).map_err(|e| DecoderError::DecodeFailed {
decoder: Self::NAME,
context: format!("pubkey {pubkey}"),
source: e,
})?;
Ok(Some(to_das_asset(pubkey, &metadata)))
}
}
fn interface_for_standard(std: Option<&TokenStandard>) -> &'static str {
match std {
Some(
TokenStandard::ProgrammableNonFungible | TokenStandard::ProgrammableNonFungibleEdition,
) => "ProgrammableNFT",
Some(TokenStandard::Fungible) => "FungibleToken",
Some(TokenStandard::FungibleAsset) => "FungibleAsset",
Some(TokenStandard::NonFungible | TokenStandard::NonFungibleEdition) | None => "V1_NFT",
}
}
fn token_standard_name(std: Option<&TokenStandard>) -> Option<String> {
let name = match std? {
TokenStandard::NonFungible => "NonFungible",
TokenStandard::FungibleAsset => "FungibleAsset",
TokenStandard::Fungible => "Fungible",
TokenStandard::NonFungibleEdition => "NonFungibleEdition",
TokenStandard::ProgrammableNonFungible => "ProgrammableNonFungible",
TokenStandard::ProgrammableNonFungibleEdition => "ProgrammableNonFungibleEdition",
};
Some(name.into())
}
fn to_das_asset(pubkey: &str, m: &Metadata) -> DasAsset {
let creators = m
.creators
.as_ref()
.map(|list| {
list.iter()
.map(|c| DasCreator {
address: c.address.to_string(),
share: c.share,
verified: c.verified,
})
.collect()
})
.unwrap_or_default();
let grouping = m
.collection
.as_ref()
.filter(|c| c.verified)
.map(|c| {
vec![DasGrouping {
group_key: "collection".into(),
group_value: c.key.to_string(),
}]
})
.unwrap_or_default();
DasAsset {
id: pubkey.to_string(),
interface: interface_for_standard(m.token_standard.as_ref()).into(),
content: DasContent {
schema: "https://schema.metaplex.com/nft1.0.json".into(),
json_uri: m.uri.trim_end_matches('\0').to_string(),
metadata: DasMetadata {
name: m.name.trim_end_matches('\0').to_string(),
symbol: m.symbol.trim_end_matches('\0').to_string(),
description: String::new(),
token_standard: token_standard_name(m.token_standard.as_ref()),
..Default::default()
},
links: DasLinks::default(),
files: Vec::<DasFile>::new(),
category: None,
},
authorities: vec![DasAuthority {
address: m.update_authority.to_string(),
scopes: vec!["full".into()],
}],
creators,
ownership: DasOwnership {
ownership_model: "single".into(),
owner: String::new(),
..Default::default()
},
grouping,
mutable: m.is_mutable,
burnt: false,
compression: None,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interface_maps_programmable_nfts() {
assert_eq!(
interface_for_standard(Some(&TokenStandard::ProgrammableNonFungible)),
"ProgrammableNFT"
);
assert_eq!(
interface_for_standard(Some(&TokenStandard::ProgrammableNonFungibleEdition)),
"ProgrammableNFT"
);
}
#[test]
fn interface_maps_plain_nfts_to_v1_nft() {
assert_eq!(
interface_for_standard(Some(&TokenStandard::NonFungible)),
"V1_NFT"
);
assert_eq!(
interface_for_standard(Some(&TokenStandard::NonFungibleEdition)),
"V1_NFT"
);
}
#[test]
fn interface_maps_fungibles() {
assert_eq!(
interface_for_standard(Some(&TokenStandard::Fungible)),
"FungibleToken"
);
assert_eq!(
interface_for_standard(Some(&TokenStandard::FungibleAsset)),
"FungibleAsset"
);
}
#[test]
fn interface_missing_standard_defaults_to_v1_nft() {
assert_eq!(interface_for_standard(None), "V1_NFT");
}
#[test]
fn token_standard_name_round_trips_all_variants() {
let cases = [
(TokenStandard::NonFungible, "NonFungible"),
(TokenStandard::FungibleAsset, "FungibleAsset"),
(TokenStandard::Fungible, "Fungible"),
(TokenStandard::NonFungibleEdition, "NonFungibleEdition"),
(
TokenStandard::ProgrammableNonFungible,
"ProgrammableNonFungible",
),
(
TokenStandard::ProgrammableNonFungibleEdition,
"ProgrammableNonFungibleEdition",
),
];
for (std, expected) in cases {
assert_eq!(token_standard_name(Some(&std)).as_deref(), Some(expected));
}
assert!(token_standard_name(None).is_none());
}
}