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