Skip to main content

spl_token_metadata_interface/
instruction.rs

1//! Instruction types
2
3use {
4    crate::state::Field,
5    alloc::{string::String, vec, vec::Vec},
6    borsh::{BorshDeserialize, BorshSerialize},
7    solana_address::Address,
8    solana_instruction::{account_meta::AccountMeta, Instruction},
9    solana_nullable::MaybeNull,
10    solana_program_error::ProgramError,
11    spl_discriminator::{discriminator::ArrayDiscriminator, SplDiscriminate},
12};
13
14#[cfg(feature = "serde-traits")]
15use {
16    serde::{Deserialize, Serialize},
17    serde_with::DisplayFromStr,
18};
19
20/// Initialization instruction data
21#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
22#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
23#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
24#[discriminator_hash_input("spl_token_metadata_interface:initialize_account")]
25pub struct Initialize {
26    /// Longer name of the token
27    pub name: String,
28    /// Shortened symbol of the token
29    pub symbol: String,
30    /// URI pointing to more metadata (image, video, etc.)
31    pub uri: String,
32}
33
34/// Update field instruction data
35#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
36#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
37#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
38#[discriminator_hash_input("spl_token_metadata_interface:updating_field")]
39pub struct UpdateField {
40    /// Field to update in the metadata
41    pub field: Field,
42    /// Value to write for the field
43    pub value: String,
44}
45
46/// Remove key instruction data
47#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
48#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
49#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
50#[discriminator_hash_input("spl_token_metadata_interface:remove_key_ix")]
51pub struct RemoveKey {
52    /// If the idempotent flag is set to true, then the instruction will not
53    /// error if the key does not exist
54    pub idempotent: bool,
55    /// Key to remove in the additional metadata portion
56    pub key: String,
57}
58
59/// Update authority instruction data
60#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
61#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
62#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
63#[discriminator_hash_input("spl_token_metadata_interface:update_the_authority")]
64pub struct UpdateAuthority {
65    /// New authority for the token metadata, or unset if `None`
66    #[cfg_attr(
67        feature = "serde-traits",
68        serde(with = "serde_with::As::<Option<DisplayFromStr>>")
69    )]
70    pub new_authority: MaybeNull<Address>,
71}
72
73/// Instruction data for Emit
74#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
75#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
76#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
77#[discriminator_hash_input("spl_token_metadata_interface:emitter")]
78pub struct Emit {
79    /// Start of range of data to emit
80    pub start: Option<u64>,
81    /// End of range of data to emit
82    pub end: Option<u64>,
83}
84
85/// All instructions that must be implemented in the token-metadata interface
86#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
87#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
88#[derive(Clone, Debug, PartialEq)]
89pub enum TokenMetadataInstruction {
90    /// Initializes a TLV entry with the basic token-metadata fields.
91    ///
92    /// Assumes that the provided mint is an SPL token mint, that the metadata
93    /// account is allocated and assigned to the program, and that the metadata
94    /// account has enough lamports to cover the rent-exempt reserve.
95    ///
96    /// Accounts expected by this instruction:
97    ///
98    ///   0. `[w]` Metadata
99    ///   1. `[]` Update authority
100    ///   2. `[]` Mint
101    ///   3. `[s]` Mint authority
102    ///
103    /// Data: `Initialize` data, name / symbol / uri strings
104    Initialize(Initialize),
105
106    /// Updates a field in a token-metadata account.
107    ///
108    /// The field can be one of the required fields (name, symbol, URI), or a
109    /// totally new field denoted by a "key" string.
110    ///
111    /// By the end of the instruction, the metadata account must be properly
112    /// re-sized based on the new size of the TLV entry.
113    ///   * If the new size is larger, the program must first reallocate to
114    ///     avoid overwriting other TLV entries.
115    ///   * If the new size is smaller, the program must reallocate at the end
116    ///     so that it's possible to iterate over TLV entries
117    ///
118    /// Accounts expected by this instruction:
119    ///
120    ///   0. `[w]` Metadata account
121    ///   1. `[s]` Update authority
122    ///
123    /// Data: `UpdateField` data, specifying the new field and value. If the
124    /// field does not exist on the account, it will be created. If the
125    /// field does exist, it will be overwritten.
126    UpdateField(UpdateField),
127
128    /// Removes a key-value pair in a token-metadata account.
129    ///
130    /// This only applies to additional fields, and not the base name / symbol /
131    /// URI fields.
132    ///
133    /// By the end of the instruction, the metadata account must be properly
134    /// re-sized at the end based on the new size of the TLV entry.
135    ///
136    /// Accounts expected by this instruction:
137    ///
138    ///   0. `[w]` Metadata account
139    ///   1. `[s]` Update authority
140    ///
141    /// Data: the string key to remove. If the idempotent flag is set to false,
142    /// returns an error if the key is not present
143    RemoveKey(RemoveKey),
144
145    /// Updates the token-metadata authority
146    ///
147    /// Accounts expected by this instruction:
148    ///
149    ///   0. `[w]` Metadata account
150    ///   1. `[s]` Current update authority
151    ///
152    /// Data: the new authority. Can be unset using a `None` value
153    UpdateAuthority(UpdateAuthority),
154
155    /// Emits the token-metadata as return data
156    ///
157    /// The format of the data emitted follows exactly the `TokenMetadata`
158    /// struct, but it's possible that the account data is stored in another
159    /// format by the program.
160    ///
161    /// With this instruction, a program that implements the token-metadata
162    /// interface can return `TokenMetadata` without adhering to the specific
163    /// byte layout of the `TokenMetadata` struct in any accounts.
164    ///
165    /// Accounts expected by this instruction:
166    ///
167    ///   0. `[]` Metadata account
168    Emit(Emit),
169}
170impl TokenMetadataInstruction {
171    /// Unpacks a byte buffer into a
172    /// [`TokenMetadataInstruction`](enum.TokenMetadataInstruction.html).
173    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
174        if input.len() < ArrayDiscriminator::LENGTH {
175            return Err(ProgramError::InvalidInstructionData);
176        }
177        let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH);
178        Ok(match discriminator {
179            Initialize::SPL_DISCRIMINATOR_SLICE => {
180                let data = Initialize::try_from_slice(rest)?;
181                Self::Initialize(data)
182            }
183            UpdateField::SPL_DISCRIMINATOR_SLICE => {
184                let data = UpdateField::try_from_slice(rest)?;
185                Self::UpdateField(data)
186            }
187            RemoveKey::SPL_DISCRIMINATOR_SLICE => {
188                let data = RemoveKey::try_from_slice(rest)?;
189                Self::RemoveKey(data)
190            }
191            UpdateAuthority::SPL_DISCRIMINATOR_SLICE => {
192                let data = UpdateAuthority::try_from_slice(rest)?;
193                Self::UpdateAuthority(data)
194            }
195            Emit::SPL_DISCRIMINATOR_SLICE => {
196                let data = Emit::try_from_slice(rest)?;
197                Self::Emit(data)
198            }
199            _ => return Err(ProgramError::InvalidInstructionData),
200        })
201    }
202
203    /// Packs a [`TokenMetadataInstruction`](enum.TokenMetadataInstruction.html)
204    /// into a byte buffer.
205    pub fn pack(&self) -> Vec<u8> {
206        let mut buf = vec![];
207        match self {
208            Self::Initialize(data) => {
209                buf.extend_from_slice(Initialize::SPL_DISCRIMINATOR_SLICE);
210                buf.append(&mut borsh::to_vec(data).unwrap());
211            }
212            Self::UpdateField(data) => {
213                buf.extend_from_slice(UpdateField::SPL_DISCRIMINATOR_SLICE);
214                buf.append(&mut borsh::to_vec(data).unwrap());
215            }
216            Self::RemoveKey(data) => {
217                buf.extend_from_slice(RemoveKey::SPL_DISCRIMINATOR_SLICE);
218                buf.append(&mut borsh::to_vec(data).unwrap());
219            }
220            Self::UpdateAuthority(data) => {
221                buf.extend_from_slice(UpdateAuthority::SPL_DISCRIMINATOR_SLICE);
222                buf.append(&mut borsh::to_vec(data).unwrap());
223            }
224            Self::Emit(data) => {
225                buf.extend_from_slice(Emit::SPL_DISCRIMINATOR_SLICE);
226                buf.append(&mut borsh::to_vec(data).unwrap());
227            }
228        };
229        buf
230    }
231}
232
233/// Creates an `Initialize` instruction
234#[allow(clippy::too_many_arguments)]
235pub fn initialize(
236    program_id: &Address,
237    metadata: &Address,
238    update_authority: &Address,
239    mint: &Address,
240    mint_authority: &Address,
241    name: String,
242    symbol: String,
243    uri: String,
244) -> Instruction {
245    let data = TokenMetadataInstruction::Initialize(Initialize { name, symbol, uri });
246    Instruction {
247        program_id: *program_id,
248        accounts: vec![
249            AccountMeta::new(*metadata, false),
250            AccountMeta::new_readonly(*update_authority, false),
251            AccountMeta::new_readonly(*mint, false),
252            AccountMeta::new_readonly(*mint_authority, true),
253        ],
254        data: data.pack(),
255    }
256}
257
258/// Creates an `UpdateField` instruction
259pub fn update_field(
260    program_id: &Address,
261    metadata: &Address,
262    update_authority: &Address,
263    field: Field,
264    value: String,
265) -> Instruction {
266    let data = TokenMetadataInstruction::UpdateField(UpdateField { field, value });
267    Instruction {
268        program_id: *program_id,
269        accounts: vec![
270            AccountMeta::new(*metadata, false),
271            AccountMeta::new_readonly(*update_authority, true),
272        ],
273        data: data.pack(),
274    }
275}
276
277/// Creates a `RemoveKey` instruction
278pub fn remove_key(
279    program_id: &Address,
280    metadata: &Address,
281    update_authority: &Address,
282    key: String,
283    idempotent: bool,
284) -> Instruction {
285    let data = TokenMetadataInstruction::RemoveKey(RemoveKey { key, idempotent });
286    Instruction {
287        program_id: *program_id,
288        accounts: vec![
289            AccountMeta::new(*metadata, false),
290            AccountMeta::new_readonly(*update_authority, true),
291        ],
292        data: data.pack(),
293    }
294}
295
296/// Creates an `UpdateAuthority` instruction
297pub fn update_authority(
298    program_id: &Address,
299    metadata: &Address,
300    current_authority: &Address,
301    new_authority: MaybeNull<Address>,
302) -> Instruction {
303    let data = TokenMetadataInstruction::UpdateAuthority(UpdateAuthority { new_authority });
304    Instruction {
305        program_id: *program_id,
306        accounts: vec![
307            AccountMeta::new(*metadata, false),
308            AccountMeta::new_readonly(*current_authority, true),
309        ],
310        data: data.pack(),
311    }
312}
313
314/// Creates an `Emit` instruction
315pub fn emit(
316    program_id: &Address,
317    metadata: &Address,
318    start: Option<u64>,
319    end: Option<u64>,
320) -> Instruction {
321    let data = TokenMetadataInstruction::Emit(Emit { start, end });
322    Instruction {
323        program_id: *program_id,
324        accounts: vec![AccountMeta::new_readonly(*metadata, false)],
325        data: data.pack(),
326    }
327}
328
329#[cfg(test)]
330mod test {
331    #[cfg(feature = "serde-traits")]
332    use core::str::FromStr;
333    use {
334        super::*,
335        crate::NAMESPACE,
336        alloc::{format, string::ToString, vec},
337        solana_sha256_hasher::hashv,
338    };
339
340    fn check_pack_unpack<T: BorshSerialize>(
341        instruction: TokenMetadataInstruction,
342        discriminator: &[u8],
343        data: T,
344    ) {
345        let mut expect = vec![];
346        expect.extend_from_slice(discriminator.as_ref());
347        expect.append(&mut borsh::to_vec(&data).unwrap());
348        let packed = instruction.pack();
349        assert_eq!(packed, expect);
350        let unpacked = TokenMetadataInstruction::unpack(&expect).unwrap();
351        assert_eq!(unpacked, instruction);
352    }
353
354    #[test]
355    fn initialize_pack() {
356        let name = "My test token";
357        let symbol = "TEST";
358        let uri = "http://test.test";
359        let data = Initialize {
360            name: name.to_string(),
361            symbol: symbol.to_string(),
362            uri: uri.to_string(),
363        };
364        let check = TokenMetadataInstruction::Initialize(data.clone());
365        let preimage = hashv(&[format!("{NAMESPACE}:initialize_account").as_bytes()]);
366        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
367        check_pack_unpack(check, discriminator, data);
368    }
369
370    #[test]
371    fn update_field_pack() {
372        let field = "MyTestField";
373        let value = "http://test.uri";
374        let data = UpdateField {
375            field: Field::Key(field.to_string()),
376            value: value.to_string(),
377        };
378        let check = TokenMetadataInstruction::UpdateField(data.clone());
379        let preimage = hashv(&[format!("{NAMESPACE}:updating_field").as_bytes()]);
380        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
381        check_pack_unpack(check, discriminator, data);
382    }
383
384    #[test]
385    fn remove_key_pack() {
386        let data = RemoveKey {
387            key: "MyTestField".to_string(),
388            idempotent: true,
389        };
390        let check = TokenMetadataInstruction::RemoveKey(data.clone());
391        let preimage = hashv(&[format!("{NAMESPACE}:remove_key_ix").as_bytes()]);
392        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
393        check_pack_unpack(check, discriminator, data);
394    }
395
396    #[test]
397    fn update_authority_pack() {
398        let data = UpdateAuthority {
399            new_authority: MaybeNull::default(),
400        };
401        let check = TokenMetadataInstruction::UpdateAuthority(data.clone());
402        let preimage = hashv(&[format!("{NAMESPACE}:update_the_authority").as_bytes()]);
403        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
404        check_pack_unpack(check, discriminator, data);
405    }
406
407    #[test]
408    fn emit_pack() {
409        let data = Emit {
410            start: None,
411            end: Some(10),
412        };
413        let check = TokenMetadataInstruction::Emit(data.clone());
414        let preimage = hashv(&[format!("{NAMESPACE}:emitter").as_bytes()]);
415        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
416        check_pack_unpack(check, discriminator, data);
417    }
418
419    #[cfg(feature = "serde-traits")]
420    #[test]
421    fn initialize_serde() {
422        let data = Initialize {
423            name: "Token Name".to_string(),
424            symbol: "TST".to_string(),
425            uri: "uri.test".to_string(),
426        };
427        let ix = TokenMetadataInstruction::Initialize(data);
428        let serialized = serde_json::to_string(&ix).unwrap();
429        let serialized_expected =
430            "{\"initialize\":{\"name\":\"Token Name\",\"symbol\":\"TST\",\"uri\":\"uri.test\"}}";
431        assert_eq!(&serialized, serialized_expected);
432
433        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
434        assert_eq!(ix, deserialized);
435    }
436
437    #[cfg(feature = "serde-traits")]
438    #[test]
439    fn update_field_serde() {
440        let data = UpdateField {
441            field: Field::Key("MyField".to_string()),
442            value: "my field value".to_string(),
443        };
444        let ix = TokenMetadataInstruction::UpdateField(data);
445        let serialized = serde_json::to_string(&ix).unwrap();
446        let serialized_expected =
447            "{\"updateField\":{\"field\":{\"key\":\"MyField\"},\"value\":\"my field value\"}}";
448        assert_eq!(&serialized, serialized_expected);
449
450        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
451        assert_eq!(ix, deserialized);
452    }
453
454    #[cfg(feature = "serde-traits")]
455    #[test]
456    fn remove_key_serde() {
457        let data = RemoveKey {
458            key: "MyTestField".to_string(),
459            idempotent: true,
460        };
461        let ix = TokenMetadataInstruction::RemoveKey(data);
462        let serialized = serde_json::to_string(&ix).unwrap();
463        let serialized_expected = "{\"removeKey\":{\"idempotent\":true,\"key\":\"MyTestField\"}}";
464        assert_eq!(&serialized, serialized_expected);
465
466        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
467        assert_eq!(ix, deserialized);
468    }
469
470    #[cfg(feature = "serde-traits")]
471    #[test]
472    fn update_authority_serde() {
473        let update_authority_option: Option<Address> =
474            Some(Address::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap());
475        let update_authority: MaybeNull<Address> = update_authority_option.try_into().unwrap();
476        let data = UpdateAuthority {
477            new_authority: update_authority,
478        };
479        let ix = TokenMetadataInstruction::UpdateAuthority(data);
480        let serialized = serde_json::to_string(&ix).unwrap();
481        let serialized_expected = "{\"updateAuthority\":{\"newAuthority\":\"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM\"}}";
482        assert_eq!(&serialized, serialized_expected);
483
484        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
485        assert_eq!(ix, deserialized);
486    }
487
488    #[cfg(feature = "serde-traits")]
489    #[test]
490    fn update_authority_serde_with_none() {
491        let data = UpdateAuthority {
492            new_authority: MaybeNull::default(),
493        };
494        let ix = TokenMetadataInstruction::UpdateAuthority(data);
495        let serialized = serde_json::to_string(&ix).unwrap();
496        let serialized_expected = "{\"updateAuthority\":{\"newAuthority\":null}}";
497        assert_eq!(&serialized, serialized_expected);
498
499        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
500        assert_eq!(ix, deserialized);
501    }
502
503    #[cfg(feature = "serde-traits")]
504    #[test]
505    fn emit_serde() {
506        let data = Emit {
507            start: None,
508            end: Some(10),
509        };
510        let ix = TokenMetadataInstruction::Emit(data);
511        let serialized = serde_json::to_string(&ix).unwrap();
512        let serialized_expected = "{\"emit\":{\"start\":null,\"end\":10}}";
513        assert_eq!(&serialized, serialized_expected);
514
515        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
516        assert_eq!(ix, deserialized);
517    }
518}