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}
170/// Validates that a Borsh-encoded `String` starting at `*cursor` declares a
171/// length that fits within `rest`, advancing `*cursor` past it on success.
172///
173/// `TokenMetadataInstruction::unpack` calls this before `try_from_slice` so a
174/// forged length prefix can't trigger an oversized allocation that aborts on
175/// the SBF heap. See <https://github.com/solana-program/token-2022/issues/1152>.
176fn check_borsh_string(rest: &[u8], cursor: &mut usize) -> Result<(), ProgramError> {
177    let len_end = cursor
178        .checked_add(core::mem::size_of::<u32>())
179        .ok_or(ProgramError::InvalidInstructionData)?;
180    let len_bytes = rest
181        .get(*cursor..len_end)
182        .ok_or(ProgramError::InvalidInstructionData)?;
183    let len = u32::from_le_bytes(len_bytes.try_into().unwrap()) as usize;
184    let str_end = len_end
185        .checked_add(len)
186        .ok_or(ProgramError::InvalidInstructionData)?;
187    if str_end > rest.len() {
188        return Err(ProgramError::InvalidInstructionData);
189    }
190    *cursor = str_end;
191    Ok(())
192}
193
194impl TokenMetadataInstruction {
195    /// Unpacks a byte buffer into a
196    /// [`TokenMetadataInstruction`](enum.TokenMetadataInstruction.html).
197    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
198        if input.len() < ArrayDiscriminator::LENGTH {
199            return Err(ProgramError::InvalidInstructionData);
200        }
201        let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH);
202        Ok(match discriminator {
203            Initialize::SPL_DISCRIMINATOR_SLICE => {
204                let mut cursor = 0usize;
205                check_borsh_string(rest, &mut cursor)?; // name
206                check_borsh_string(rest, &mut cursor)?; // symbol
207                check_borsh_string(rest, &mut cursor)?; // uri
208                let data = Initialize::try_from_slice(rest)?;
209                Self::Initialize(data)
210            }
211            UpdateField::SPL_DISCRIMINATOR_SLICE => {
212                let mut cursor = 0usize;
213                // `Field` is a Borsh enum: 1-byte variant tag, and only the
214                // `Key` variant (tag 3) carries a nested string.
215                let tag = *rest
216                    .get(cursor)
217                    .ok_or(ProgramError::InvalidInstructionData)?;
218                cursor = cursor
219                    .checked_add(1)
220                    .ok_or(ProgramError::InvalidInstructionData)?;
221                if tag == 3 {
222                    check_borsh_string(rest, &mut cursor)?; // Field::Key(String)
223                }
224                check_borsh_string(rest, &mut cursor)?; // value
225                let data = UpdateField::try_from_slice(rest)?;
226                Self::UpdateField(data)
227            }
228            RemoveKey::SPL_DISCRIMINATOR_SLICE => {
229                let mut cursor = 1usize; // skip the 1-byte `idempotent` bool
230                check_borsh_string(rest, &mut cursor)?; // key
231                let data = RemoveKey::try_from_slice(rest)?;
232                Self::RemoveKey(data)
233            }
234            UpdateAuthority::SPL_DISCRIMINATOR_SLICE => {
235                let data = UpdateAuthority::try_from_slice(rest)?;
236                Self::UpdateAuthority(data)
237            }
238            Emit::SPL_DISCRIMINATOR_SLICE => {
239                let data = Emit::try_from_slice(rest)?;
240                Self::Emit(data)
241            }
242            _ => return Err(ProgramError::InvalidInstructionData),
243        })
244    }
245
246    /// Packs a [`TokenMetadataInstruction`](enum.TokenMetadataInstruction.html)
247    /// into a byte buffer.
248    pub fn pack(&self) -> Vec<u8> {
249        let mut buf = vec![];
250        match self {
251            Self::Initialize(data) => {
252                buf.extend_from_slice(Initialize::SPL_DISCRIMINATOR_SLICE);
253                buf.append(&mut borsh::to_vec(data).unwrap());
254            }
255            Self::UpdateField(data) => {
256                buf.extend_from_slice(UpdateField::SPL_DISCRIMINATOR_SLICE);
257                buf.append(&mut borsh::to_vec(data).unwrap());
258            }
259            Self::RemoveKey(data) => {
260                buf.extend_from_slice(RemoveKey::SPL_DISCRIMINATOR_SLICE);
261                buf.append(&mut borsh::to_vec(data).unwrap());
262            }
263            Self::UpdateAuthority(data) => {
264                buf.extend_from_slice(UpdateAuthority::SPL_DISCRIMINATOR_SLICE);
265                buf.append(&mut borsh::to_vec(data).unwrap());
266            }
267            Self::Emit(data) => {
268                buf.extend_from_slice(Emit::SPL_DISCRIMINATOR_SLICE);
269                buf.append(&mut borsh::to_vec(data).unwrap());
270            }
271        };
272        buf
273    }
274}
275
276/// Creates an `Initialize` instruction
277#[allow(clippy::too_many_arguments)]
278pub fn initialize(
279    program_id: &Address,
280    metadata: &Address,
281    update_authority: &Address,
282    mint: &Address,
283    mint_authority: &Address,
284    name: String,
285    symbol: String,
286    uri: String,
287) -> Instruction {
288    let data = TokenMetadataInstruction::Initialize(Initialize { name, symbol, uri });
289    Instruction {
290        program_id: *program_id,
291        accounts: vec![
292            AccountMeta::new(*metadata, false),
293            AccountMeta::new_readonly(*update_authority, false),
294            AccountMeta::new_readonly(*mint, false),
295            AccountMeta::new_readonly(*mint_authority, true),
296        ],
297        data: data.pack(),
298    }
299}
300
301/// Creates an `UpdateField` instruction
302pub fn update_field(
303    program_id: &Address,
304    metadata: &Address,
305    update_authority: &Address,
306    field: Field,
307    value: String,
308) -> Instruction {
309    let data = TokenMetadataInstruction::UpdateField(UpdateField { field, value });
310    Instruction {
311        program_id: *program_id,
312        accounts: vec![
313            AccountMeta::new(*metadata, false),
314            AccountMeta::new_readonly(*update_authority, true),
315        ],
316        data: data.pack(),
317    }
318}
319
320/// Creates a `RemoveKey` instruction
321pub fn remove_key(
322    program_id: &Address,
323    metadata: &Address,
324    update_authority: &Address,
325    key: String,
326    idempotent: bool,
327) -> Instruction {
328    let data = TokenMetadataInstruction::RemoveKey(RemoveKey { key, idempotent });
329    Instruction {
330        program_id: *program_id,
331        accounts: vec![
332            AccountMeta::new(*metadata, false),
333            AccountMeta::new_readonly(*update_authority, true),
334        ],
335        data: data.pack(),
336    }
337}
338
339/// Creates an `UpdateAuthority` instruction
340pub fn update_authority(
341    program_id: &Address,
342    metadata: &Address,
343    current_authority: &Address,
344    new_authority: MaybeNull<Address>,
345) -> Instruction {
346    let data = TokenMetadataInstruction::UpdateAuthority(UpdateAuthority { new_authority });
347    Instruction {
348        program_id: *program_id,
349        accounts: vec![
350            AccountMeta::new(*metadata, false),
351            AccountMeta::new_readonly(*current_authority, true),
352        ],
353        data: data.pack(),
354    }
355}
356
357/// Creates an `Emit` instruction
358pub fn emit(
359    program_id: &Address,
360    metadata: &Address,
361    start: Option<u64>,
362    end: Option<u64>,
363) -> Instruction {
364    let data = TokenMetadataInstruction::Emit(Emit { start, end });
365    Instruction {
366        program_id: *program_id,
367        accounts: vec![AccountMeta::new_readonly(*metadata, false)],
368        data: data.pack(),
369    }
370}
371
372#[cfg(test)]
373mod test {
374    #[cfg(feature = "serde-traits")]
375    use core::str::FromStr;
376    use {
377        super::*,
378        crate::NAMESPACE,
379        alloc::{format, string::ToString, vec},
380        solana_sha256_hasher::hashv,
381    };
382
383    fn check_pack_unpack<T: BorshSerialize>(
384        instruction: TokenMetadataInstruction,
385        discriminator: &[u8],
386        data: T,
387    ) {
388        let mut expect = vec![];
389        expect.extend_from_slice(discriminator.as_ref());
390        expect.append(&mut borsh::to_vec(&data).unwrap());
391        let packed = instruction.pack();
392        assert_eq!(packed, expect);
393        let unpacked = TokenMetadataInstruction::unpack(&expect).unwrap();
394        assert_eq!(unpacked, instruction);
395    }
396
397    #[test]
398    fn initialize_pack() {
399        let name = "My test token";
400        let symbol = "TEST";
401        let uri = "http://test.test";
402        let data = Initialize {
403            name: name.to_string(),
404            symbol: symbol.to_string(),
405            uri: uri.to_string(),
406        };
407        let check = TokenMetadataInstruction::Initialize(data.clone());
408        let preimage = hashv(&[format!("{NAMESPACE}:initialize_account").as_bytes()]);
409        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
410        check_pack_unpack(check, discriminator, data);
411    }
412
413    #[test]
414    fn update_field_pack() {
415        let field = "MyTestField";
416        let value = "http://test.uri";
417        let data = UpdateField {
418            field: Field::Key(field.to_string()),
419            value: value.to_string(),
420        };
421        let check = TokenMetadataInstruction::UpdateField(data.clone());
422        let preimage = hashv(&[format!("{NAMESPACE}:updating_field").as_bytes()]);
423        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
424        check_pack_unpack(check, discriminator, data);
425    }
426
427    #[test]
428    fn remove_key_pack() {
429        let data = RemoveKey {
430            key: "MyTestField".to_string(),
431            idempotent: true,
432        };
433        let check = TokenMetadataInstruction::RemoveKey(data.clone());
434        let preimage = hashv(&[format!("{NAMESPACE}:remove_key_ix").as_bytes()]);
435        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
436        check_pack_unpack(check, discriminator, data);
437    }
438
439    #[test]
440    fn update_authority_pack() {
441        let data = UpdateAuthority {
442            new_authority: MaybeNull::default(),
443        };
444        let check = TokenMetadataInstruction::UpdateAuthority(data.clone());
445        let preimage = hashv(&[format!("{NAMESPACE}:update_the_authority").as_bytes()]);
446        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
447        check_pack_unpack(check, discriminator, data);
448    }
449
450    #[test]
451    fn emit_pack() {
452        let data = Emit {
453            start: None,
454            end: Some(10),
455        };
456        let check = TokenMetadataInstruction::Emit(data.clone());
457        let preimage = hashv(&[format!("{NAMESPACE}:emitter").as_bytes()]);
458        let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
459        check_pack_unpack(check, discriminator, data);
460    }
461
462    #[test]
463    fn fail_unpack_initialize_with_forged_name_length() {
464        let mut input = vec![];
465        input.extend_from_slice(Initialize::SPL_DISCRIMINATOR_SLICE);
466        input.extend_from_slice(&u32::MAX.to_le_bytes()); // forged name length, no bytes follow
467        assert_eq!(
468            TokenMetadataInstruction::unpack(&input).unwrap_err(),
469            ProgramError::InvalidInstructionData,
470        );
471    }
472
473    #[test]
474    fn fail_unpack_update_field_with_forged_value_length() {
475        let mut input = vec![];
476        input.extend_from_slice(UpdateField::SPL_DISCRIMINATOR_SLICE);
477        input.push(0u8); // Field::Name (tag 0, no nested string)
478        input.extend_from_slice(&u32::MAX.to_le_bytes()); // forged value length
479        assert_eq!(
480            TokenMetadataInstruction::unpack(&input).unwrap_err(),
481            ProgramError::InvalidInstructionData,
482        );
483    }
484
485    #[test]
486    fn fail_unpack_remove_key_with_forged_key_length() {
487        let mut input = vec![];
488        input.extend_from_slice(RemoveKey::SPL_DISCRIMINATOR_SLICE);
489        input.push(0u8); // idempotent = false
490        input.extend_from_slice(&u32::MAX.to_le_bytes()); // forged key length
491        assert_eq!(
492            TokenMetadataInstruction::unpack(&input).unwrap_err(),
493            ProgramError::InvalidInstructionData,
494        );
495    }
496
497    #[cfg(feature = "serde-traits")]
498    #[test]
499    fn initialize_serde() {
500        let data = Initialize {
501            name: "Token Name".to_string(),
502            symbol: "TST".to_string(),
503            uri: "uri.test".to_string(),
504        };
505        let ix = TokenMetadataInstruction::Initialize(data);
506        let serialized = serde_json::to_string(&ix).unwrap();
507        let serialized_expected =
508            "{\"initialize\":{\"name\":\"Token Name\",\"symbol\":\"TST\",\"uri\":\"uri.test\"}}";
509        assert_eq!(&serialized, serialized_expected);
510
511        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
512        assert_eq!(ix, deserialized);
513    }
514
515    #[cfg(feature = "serde-traits")]
516    #[test]
517    fn update_field_serde() {
518        let data = UpdateField {
519            field: Field::Key("MyField".to_string()),
520            value: "my field value".to_string(),
521        };
522        let ix = TokenMetadataInstruction::UpdateField(data);
523        let serialized = serde_json::to_string(&ix).unwrap();
524        let serialized_expected =
525            "{\"updateField\":{\"field\":{\"key\":\"MyField\"},\"value\":\"my field value\"}}";
526        assert_eq!(&serialized, serialized_expected);
527
528        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
529        assert_eq!(ix, deserialized);
530    }
531
532    #[cfg(feature = "serde-traits")]
533    #[test]
534    fn remove_key_serde() {
535        let data = RemoveKey {
536            key: "MyTestField".to_string(),
537            idempotent: true,
538        };
539        let ix = TokenMetadataInstruction::RemoveKey(data);
540        let serialized = serde_json::to_string(&ix).unwrap();
541        let serialized_expected = "{\"removeKey\":{\"idempotent\":true,\"key\":\"MyTestField\"}}";
542        assert_eq!(&serialized, serialized_expected);
543
544        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
545        assert_eq!(ix, deserialized);
546    }
547
548    #[cfg(feature = "serde-traits")]
549    #[test]
550    fn update_authority_serde() {
551        let update_authority_option: Option<Address> =
552            Some(Address::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap());
553        let update_authority: MaybeNull<Address> = update_authority_option.try_into().unwrap();
554        let data = UpdateAuthority {
555            new_authority: update_authority,
556        };
557        let ix = TokenMetadataInstruction::UpdateAuthority(data);
558        let serialized = serde_json::to_string(&ix).unwrap();
559        let serialized_expected = "{\"updateAuthority\":{\"newAuthority\":\"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM\"}}";
560        assert_eq!(&serialized, serialized_expected);
561
562        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
563        assert_eq!(ix, deserialized);
564    }
565
566    #[cfg(feature = "serde-traits")]
567    #[test]
568    fn update_authority_serde_with_none() {
569        let data = UpdateAuthority {
570            new_authority: MaybeNull::default(),
571        };
572        let ix = TokenMetadataInstruction::UpdateAuthority(data);
573        let serialized = serde_json::to_string(&ix).unwrap();
574        let serialized_expected = "{\"updateAuthority\":{\"newAuthority\":null}}";
575        assert_eq!(&serialized, serialized_expected);
576
577        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
578        assert_eq!(ix, deserialized);
579    }
580
581    #[cfg(feature = "serde-traits")]
582    #[test]
583    fn emit_serde() {
584        let data = Emit {
585            start: None,
586            end: Some(10),
587        };
588        let ix = TokenMetadataInstruction::Emit(data);
589        let serialized = serde_json::to_string(&ix).unwrap();
590        let serialized_expected = "{\"emit\":{\"start\":null,\"end\":10}}";
591        assert_eq!(&serialized, serialized_expected);
592
593        let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
594        assert_eq!(ix, deserialized);
595    }
596}