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