//! Token-metadata interface state types use { alloc::{ string::{String, ToString}, vec::Vec, }, borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, solana_address::Address, solana_borsh::v1::{get_instance_packed_len, try_from_slice_unchecked}, solana_nullable::MaybeNull, solana_program_error::ProgramError, spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, spl_type_length_value::{ state::{TlvState, TlvStateBorrowed}, variable_len_pack::VariableLenPack, }, }; #[cfg(feature = "serde-traits")] use serde::{Deserialize, Serialize}; /// Data struct for all token-metadata, stored in a TLV entry /// /// The type and length parts must be handled by the TLV library, and not stored /// as part of this struct. #[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct TokenMetadata { /// The authority that can sign to update the metadata pub update_authority: MaybeNull
, /// The associated mint, used to counter spoofing to be sure that metadata /// belongs to a particular mint pub mint: Address, /// The longer name of the token pub name: String, /// The shortened symbol for the token pub symbol: String, /// The URI pointing to richer metadata pub uri: String, /// Any additional metadata about the token as key-value pairs. The program /// must avoid storing the same key twice. pub additional_metadata: Vec<(String, String)>, } impl SplDiscriminate for TokenMetadata { /// Please use this discriminator in your program when matching const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([112, 132, 90, 90, 11, 88, 157, 87]); } impl TokenMetadata { /// Gives the total size of this struct as a TLV entry in an account pub fn tlv_size_of(&self) -> Result { TlvStateBorrowed::get_base_len() .checked_add(get_instance_packed_len(self)?) .ok_or(ProgramError::InvalidAccountData) } /// Updates a field in the metadata struct pub fn update(&mut self, field: Field, value: String) { match field { Field::Name => self.name = value, Field::Symbol => self.symbol = value, Field::Uri => self.uri = value, Field::Key(key) => self.set_key_value(key, value), } } /// Sets a key-value pair in the additional metadata /// /// If the key is already present, overwrites the existing entry. Otherwise, /// adds it to the end. pub fn set_key_value(&mut self, new_key: String, new_value: String) { for (key, value) in self.additional_metadata.iter_mut() { if *key == new_key { value.replace_range(.., &new_value); return; } } self.additional_metadata.push((new_key, new_value)); } /// Removes the key-value pair given by the provided key. Returns true if /// the key was found. pub fn remove_key(&mut self, key: &str) -> bool { let mut found_key = false; self.additional_metadata.retain(|x| { let should_retain = x.0 != key; if !should_retain { found_key = true; } should_retain }); found_key } /// Get the slice corresponding to the given start and end range pub fn get_slice(data: &[u8], start: Option, end: Option) -> Option<&[u8]> { let start = start.unwrap_or(0) as usize; let end = end.map(|x| x as usize).unwrap_or(data.len()); data.get(start..end) } } impl VariableLenPack for TokenMetadata { fn pack_into_slice(&self, dst: &mut [u8]) -> Result<(), ProgramError> { borsh::to_writer(&mut dst[..], self).map_err(Into::into) } fn unpack_from_slice(src: &[u8]) -> Result { try_from_slice_unchecked(src).map_err(Into::into) } fn get_packed_len(&self) -> Result { get_instance_packed_len(self).map_err(Into::into) } } /// Fields in the metadata account, used for updating #[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] #[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)] pub enum Field { /// The name field, corresponding to `TokenMetadata.name` Name, /// The symbol field, corresponding to `TokenMetadata.symbol` Symbol, /// The uri field, corresponding to `TokenMetadata.uri` Uri, /// A user field, whose key is given by the associated string Key(String), } #[cfg(test)] mod tests { use { super::*, crate::NAMESPACE, alloc::{format, string::ToString}, solana_sha256_hasher::hashv, }; #[test] fn discriminator() { let preimage = hashv(&[format!("{NAMESPACE}:token_metadata").as_bytes()]); let discriminator = ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); assert_eq!(TokenMetadata::SPL_DISCRIMINATOR, discriminator); } #[test] fn update() { let name = "name".to_string(); let symbol = "symbol".to_string(); let uri = "uri".to_string(); let mut token_metadata = TokenMetadata { name, symbol, uri, ..Default::default() }; // updating base fields let new_name = "new_name".to_string(); token_metadata.update(Field::Name, new_name.clone()); assert_eq!(token_metadata.name, new_name); let new_symbol = "new_symbol".to_string(); token_metadata.update(Field::Symbol, new_symbol.clone()); assert_eq!(token_metadata.symbol, new_symbol); let new_uri = "new_uri".to_string(); token_metadata.update(Field::Uri, new_uri.clone()); assert_eq!(token_metadata.uri, new_uri); // add new key-value pairs let key1 = "key1".to_string(); let value1 = "value1".to_string(); token_metadata.update(Field::Key(key1.clone()), value1.clone()); assert_eq!(token_metadata.additional_metadata.len(), 1); assert_eq!( token_metadata.additional_metadata[0], (key1.clone(), value1.clone()) ); let key2 = "key2".to_string(); let value2 = "value2".to_string(); token_metadata.update(Field::Key(key2.clone()), value2.clone()); assert_eq!(token_metadata.additional_metadata.len(), 2); assert_eq!( token_metadata.additional_metadata[0], (key1.clone(), value1) ); assert_eq!( token_metadata.additional_metadata[1], (key2.clone(), value2.clone()) ); // update first key, see that order is preserved let new_value1 = "new_value1".to_string(); token_metadata.update(Field::Key(key1.clone()), new_value1.clone()); assert_eq!(token_metadata.additional_metadata.len(), 2); assert_eq!(token_metadata.additional_metadata[0], (key1, new_value1)); assert_eq!(token_metadata.additional_metadata[1], (key2, value2)); } #[test] fn remove_key() { let name = "name".to_string(); let symbol = "symbol".to_string(); let uri = "uri".to_string(); let mut token_metadata = TokenMetadata { name, symbol, uri, ..Default::default() }; // add new key-value pair let key = "key".to_string(); let value = "value".to_string(); token_metadata.update(Field::Key(key.clone()), value.clone()); assert_eq!(token_metadata.additional_metadata.len(), 1); assert_eq!(token_metadata.additional_metadata[0], (key.clone(), value)); // remove it assert!(token_metadata.remove_key(&key)); assert_eq!(token_metadata.additional_metadata.len(), 0); // remove it again, returns false assert!(!token_metadata.remove_key(&key)); assert_eq!(token_metadata.additional_metadata.len(), 0); } }