spl_token_metadata_interface/
instruction.rs

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