spl_token_metadata_interface/
state.rs

1//! Token-metadata interface state types
2
3#[cfg(feature = "serde-traits")]
4use serde::{Deserialize, Serialize};
5use {
6    borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
7    miraland_program::{
8        borsh1::{get_instance_packed_len, try_from_slice_unchecked},
9        program_error::ProgramError,
10        pubkey::Pubkey,
11    },
12    spl_discriminator::{ArrayDiscriminator, SplDiscriminate},
13    spl_pod::optional_keys::OptionalNonZeroPubkey,
14    spl_type_length_value::{
15        state::{TlvState, TlvStateBorrowed},
16        variable_len_pack::VariableLenPack,
17    },
18};
19
20/// Data struct for all token-metadata, stored in a TLV entry
21///
22/// The type and length parts must be handled by the TLV library, and not stored
23/// as part of this struct.
24#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
25pub struct TokenMetadata {
26    /// The authority that can sign to update the metadata
27    pub update_authority: OptionalNonZeroPubkey,
28    /// The associated mint, used to counter spoofing to be sure that metadata
29    /// belongs to a particular mint
30    pub mint: Pubkey,
31    /// The longer name of the token
32    pub name: String,
33    /// The shortened symbol for the token
34    pub symbol: String,
35    /// The URI pointing to richer metadata
36    pub uri: String,
37    /// Any additional metadata about the token as key-value pairs. The program
38    /// must avoid storing the same key twice.
39    pub additional_metadata: Vec<(String, String)>,
40}
41impl SplDiscriminate for TokenMetadata {
42    /// Please use this discriminator in your program when matching
43    const SPL_DISCRIMINATOR: ArrayDiscriminator =
44        ArrayDiscriminator::new([112, 132, 90, 90, 11, 88, 157, 87]);
45}
46impl TokenMetadata {
47    /// Gives the total size of this struct as a TLV entry in an account
48    pub fn tlv_size_of(&self) -> Result<usize, ProgramError> {
49        TlvStateBorrowed::get_base_len()
50            .checked_add(get_instance_packed_len(self)?)
51            .ok_or(ProgramError::InvalidAccountData)
52    }
53
54    /// Updates a field in the metadata struct
55    pub fn update(&mut self, field: Field, value: String) {
56        match field {
57            Field::Name => self.name = value,
58            Field::Symbol => self.symbol = value,
59            Field::Uri => self.uri = value,
60            Field::Key(key) => self.set_key_value(key, value),
61        }
62    }
63
64    /// Sets a key-value pair in the additional metadata
65    ///
66    /// If the key is already present, overwrites the existing entry. Otherwise,
67    /// adds it to the end.
68    pub fn set_key_value(&mut self, new_key: String, new_value: String) {
69        for (key, value) in self.additional_metadata.iter_mut() {
70            if *key == new_key {
71                value.replace_range(.., &new_value);
72                return;
73            }
74        }
75        self.additional_metadata.push((new_key, new_value));
76    }
77
78    /// Removes the key-value pair given by the provided key. Returns true if
79    /// the key was found.
80    pub fn remove_key(&mut self, key: &str) -> bool {
81        let mut found_key = false;
82        self.additional_metadata.retain(|x| {
83            let should_retain = x.0 != key;
84            if !should_retain {
85                found_key = true;
86            }
87            should_retain
88        });
89        found_key
90    }
91
92    /// Get the slice corresponding to the given start and end range
93    pub fn get_slice(data: &[u8], start: Option<u64>, end: Option<u64>) -> Option<&[u8]> {
94        let start = start.unwrap_or(0) as usize;
95        let end = end.map(|x| x as usize).unwrap_or(data.len());
96        data.get(start..end)
97    }
98}
99impl VariableLenPack for TokenMetadata {
100    fn pack_into_slice(&self, dst: &mut [u8]) -> Result<(), ProgramError> {
101        borsh::to_writer(&mut dst[..], self).map_err(Into::into)
102    }
103    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
104        try_from_slice_unchecked(src).map_err(Into::into)
105    }
106    fn get_packed_len(&self) -> Result<usize, ProgramError> {
107        get_instance_packed_len(self).map_err(Into::into)
108    }
109}
110
111/// Fields in the metadata account, used for updating
112#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
113#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
114#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
115pub enum Field {
116    /// The name field, corresponding to `TokenMetadata.name`
117    Name,
118    /// The symbol field, corresponding to `TokenMetadata.symbol`
119    Symbol,
120    /// The uri field, corresponding to `TokenMetadata.uri`
121    Uri,
122    /// A user field, whose key is given by the associated string
123    Key(String),
124}
125
126#[cfg(test)]
127mod tests {
128    use {super::*, crate::NAMESPACE, miraland_program::hash};
129
130    #[test]
131    fn discriminator() {
132        let preimage = hash::hashv(&[format!("{NAMESPACE}:token_metadata").as_bytes()]);
133        let discriminator =
134            ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap();
135        assert_eq!(TokenMetadata::SPL_DISCRIMINATOR, discriminator);
136    }
137
138    #[test]
139    fn update() {
140        let name = "name".to_string();
141        let symbol = "symbol".to_string();
142        let uri = "uri".to_string();
143        let mut token_metadata = TokenMetadata {
144            name,
145            symbol,
146            uri,
147            ..Default::default()
148        };
149
150        // updating base fields
151        let new_name = "new_name".to_string();
152        token_metadata.update(Field::Name, new_name.clone());
153        assert_eq!(token_metadata.name, new_name);
154
155        let new_symbol = "new_symbol".to_string();
156        token_metadata.update(Field::Symbol, new_symbol.clone());
157        assert_eq!(token_metadata.symbol, new_symbol);
158
159        let new_uri = "new_uri".to_string();
160        token_metadata.update(Field::Uri, new_uri.clone());
161        assert_eq!(token_metadata.uri, new_uri);
162
163        // add new key-value pairs
164        let key1 = "key1".to_string();
165        let value1 = "value1".to_string();
166        token_metadata.update(Field::Key(key1.clone()), value1.clone());
167        assert_eq!(token_metadata.additional_metadata.len(), 1);
168        assert_eq!(
169            token_metadata.additional_metadata[0],
170            (key1.clone(), value1.clone())
171        );
172
173        let key2 = "key2".to_string();
174        let value2 = "value2".to_string();
175        token_metadata.update(Field::Key(key2.clone()), value2.clone());
176        assert_eq!(token_metadata.additional_metadata.len(), 2);
177        assert_eq!(
178            token_metadata.additional_metadata[0],
179            (key1.clone(), value1)
180        );
181        assert_eq!(
182            token_metadata.additional_metadata[1],
183            (key2.clone(), value2.clone())
184        );
185
186        // update first key, see that order is preserved
187        let new_value1 = "new_value1".to_string();
188        token_metadata.update(Field::Key(key1.clone()), new_value1.clone());
189        assert_eq!(token_metadata.additional_metadata.len(), 2);
190        assert_eq!(token_metadata.additional_metadata[0], (key1, new_value1));
191        assert_eq!(token_metadata.additional_metadata[1], (key2, value2));
192    }
193
194    #[test]
195    fn remove_key() {
196        let name = "name".to_string();
197        let symbol = "symbol".to_string();
198        let uri = "uri".to_string();
199        let mut token_metadata = TokenMetadata {
200            name,
201            symbol,
202            uri,
203            ..Default::default()
204        };
205
206        // add new key-value pair
207        let key = "key".to_string();
208        let value = "value".to_string();
209        token_metadata.update(Field::Key(key.clone()), value.clone());
210        assert_eq!(token_metadata.additional_metadata.len(), 1);
211        assert_eq!(token_metadata.additional_metadata[0], (key.clone(), value));
212
213        // remove it
214        assert!(token_metadata.remove_key(&key));
215        assert_eq!(token_metadata.additional_metadata.len(), 0);
216
217        // remove it again, returns false
218        assert!(!token_metadata.remove_key(&key));
219        assert_eq!(token_metadata.additional_metadata.len(), 0);
220    }
221}