use {
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
solana_program::{
borsh::{get_instance_packed_len, try_from_slice_unchecked},
program_error::ProgramError,
pubkey::Pubkey,
},
spl_discriminator::{ArrayDiscriminator, SplDiscriminate},
spl_pod::optional_keys::OptionalNonZeroPubkey,
spl_type_length_value::{
state::{TlvState, TlvStateBorrowed},
variable_len_pack::VariableLenPack,
},
};
#[cfg(feature = "serde-traits")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct TokenMetadata {
pub update_authority: OptionalNonZeroPubkey,
pub mint: Pubkey,
pub name: String,
pub symbol: String,
pub uri: String,
pub additional_metadata: Vec<(String, String)>,
}
impl SplDiscriminate for TokenMetadata {
const SPL_DISCRIMINATOR: ArrayDiscriminator =
ArrayDiscriminator::new([112, 132, 90, 90, 11, 88, 157, 87]);
}
impl TokenMetadata {
pub fn tlv_size_of(&self) -> Result<usize, ProgramError> {
TlvStateBorrowed::get_base_len()
.checked_add(get_instance_packed_len(self)?)
.ok_or(ProgramError::InvalidAccountData)
}
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),
}
}
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));
}
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
}
pub fn get_slice(data: &[u8], start: Option<u64>, end: Option<u64>) -> 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<Self, ProgramError> {
try_from_slice_unchecked(src).map_err(Into::into)
}
fn get_packed_len(&self) -> Result<usize, ProgramError> {
get_instance_packed_len(self).map_err(Into::into)
}
}
#[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 {
Name,
Symbol,
Uri,
Key(String),
}
#[cfg(test)]
mod tests {
use {super::*, crate::NAMESPACE, solana_program::hash};
#[test]
fn discriminator() {
let preimage = hash::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()
};
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);
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())
);
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()
};
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));
assert!(token_metadata.remove_key(&key));
assert_eq!(token_metadata.additional_metadata.len(), 0);
assert!(!token_metadata.remove_key(&key));
assert_eq!(token_metadata.additional_metadata.len(), 0);
}
}