Skip to main content

spl_token_metadata_interface/
state.rs

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