use {
crate::state::Field,
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
},
spl_discriminator::{discriminator::ArrayDiscriminator, SplDiscriminate},
spl_pod::optional_keys::OptionalNonZeroPubkey,
};
#[cfg(feature = "serde-traits")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
#[discriminator_hash_input("spl_token_metadata_interface:initialize_account")]
pub struct Initialize {
pub name: String,
pub symbol: String,
pub uri: String,
}
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
#[discriminator_hash_input("spl_token_metadata_interface:updating_field")]
pub struct UpdateField {
pub field: Field,
pub value: String,
}
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
#[discriminator_hash_input("spl_token_metadata_interface:remove_key_ix")]
pub struct RemoveKey {
pub idempotent: bool,
pub key: String,
}
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
#[discriminator_hash_input("spl_token_metadata_interface:update_the_authority")]
pub struct UpdateAuthority {
pub new_authority: OptionalNonZeroPubkey,
}
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)]
#[discriminator_hash_input("spl_token_metadata_interface:emitter")]
pub struct Emit {
pub start: Option<u64>,
pub end: Option<u64>,
}
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq)]
pub enum TokenMetadataInstruction {
Initialize(Initialize),
UpdateField(UpdateField),
RemoveKey(RemoveKey),
UpdateAuthority(UpdateAuthority),
Emit(Emit),
}
impl TokenMetadataInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
if input.len() < ArrayDiscriminator::LENGTH {
return Err(ProgramError::InvalidInstructionData);
}
let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH);
Ok(match discriminator {
Initialize::SPL_DISCRIMINATOR_SLICE => {
let data = Initialize::try_from_slice(rest)?;
Self::Initialize(data)
}
UpdateField::SPL_DISCRIMINATOR_SLICE => {
let data = UpdateField::try_from_slice(rest)?;
Self::UpdateField(data)
}
RemoveKey::SPL_DISCRIMINATOR_SLICE => {
let data = RemoveKey::try_from_slice(rest)?;
Self::RemoveKey(data)
}
UpdateAuthority::SPL_DISCRIMINATOR_SLICE => {
let data = UpdateAuthority::try_from_slice(rest)?;
Self::UpdateAuthority(data)
}
Emit::SPL_DISCRIMINATOR_SLICE => {
let data = Emit::try_from_slice(rest)?;
Self::Emit(data)
}
_ => return Err(ProgramError::InvalidInstructionData),
})
}
pub fn pack(&self) -> Vec<u8> {
let mut buf = vec![];
match self {
Self::Initialize(data) => {
buf.extend_from_slice(Initialize::SPL_DISCRIMINATOR_SLICE);
buf.append(&mut data.try_to_vec().unwrap());
}
Self::UpdateField(data) => {
buf.extend_from_slice(UpdateField::SPL_DISCRIMINATOR_SLICE);
buf.append(&mut data.try_to_vec().unwrap());
}
Self::RemoveKey(data) => {
buf.extend_from_slice(RemoveKey::SPL_DISCRIMINATOR_SLICE);
buf.append(&mut data.try_to_vec().unwrap());
}
Self::UpdateAuthority(data) => {
buf.extend_from_slice(UpdateAuthority::SPL_DISCRIMINATOR_SLICE);
buf.append(&mut data.try_to_vec().unwrap());
}
Self::Emit(data) => {
buf.extend_from_slice(Emit::SPL_DISCRIMINATOR_SLICE);
buf.append(&mut data.try_to_vec().unwrap());
}
};
buf
}
}
#[allow(clippy::too_many_arguments)]
pub fn initialize(
program_id: &Pubkey,
metadata: &Pubkey,
update_authority: &Pubkey,
mint: &Pubkey,
mint_authority: &Pubkey,
name: String,
symbol: String,
uri: String,
) -> Instruction {
let data = TokenMetadataInstruction::Initialize(Initialize { name, symbol, uri });
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(*metadata, false),
AccountMeta::new_readonly(*update_authority, false),
AccountMeta::new_readonly(*mint, false),
AccountMeta::new_readonly(*mint_authority, true),
],
data: data.pack(),
}
}
pub fn update_field(
program_id: &Pubkey,
metadata: &Pubkey,
update_authority: &Pubkey,
field: Field,
value: String,
) -> Instruction {
let data = TokenMetadataInstruction::UpdateField(UpdateField { field, value });
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(*metadata, false),
AccountMeta::new_readonly(*update_authority, true),
],
data: data.pack(),
}
}
pub fn remove_key(
program_id: &Pubkey,
metadata: &Pubkey,
update_authority: &Pubkey,
key: String,
idempotent: bool,
) -> Instruction {
let data = TokenMetadataInstruction::RemoveKey(RemoveKey { key, idempotent });
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(*metadata, false),
AccountMeta::new_readonly(*update_authority, true),
],
data: data.pack(),
}
}
pub fn update_authority(
program_id: &Pubkey,
metadata: &Pubkey,
current_authority: &Pubkey,
new_authority: OptionalNonZeroPubkey,
) -> Instruction {
let data = TokenMetadataInstruction::UpdateAuthority(UpdateAuthority { new_authority });
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(*metadata, false),
AccountMeta::new_readonly(*current_authority, true),
],
data: data.pack(),
}
}
pub fn emit(
program_id: &Pubkey,
metadata: &Pubkey,
start: Option<u64>,
end: Option<u64>,
) -> Instruction {
let data = TokenMetadataInstruction::Emit(Emit { start, end });
Instruction {
program_id: *program_id,
accounts: vec![AccountMeta::new_readonly(*metadata, false)],
data: data.pack(),
}
}
#[cfg(test)]
mod test {
use {super::*, crate::NAMESPACE, solana_program::hash};
#[cfg(feature = "serde-traits")]
use std::str::FromStr;
fn check_pack_unpack<T: BorshSerialize>(
instruction: TokenMetadataInstruction,
discriminator: &[u8],
data: T,
) {
let mut expect = vec![];
expect.extend_from_slice(discriminator.as_ref());
expect.append(&mut data.try_to_vec().unwrap());
let packed = instruction.pack();
assert_eq!(packed, expect);
let unpacked = TokenMetadataInstruction::unpack(&expect).unwrap();
assert_eq!(unpacked, instruction);
}
#[test]
fn initialize_pack() {
let name = "My test token";
let symbol = "TEST";
let uri = "http://test.test";
let data = Initialize {
name: name.to_string(),
symbol: symbol.to_string(),
uri: uri.to_string(),
};
let check = TokenMetadataInstruction::Initialize(data.clone());
let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_account").as_bytes()]);
let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
check_pack_unpack(check, discriminator, data);
}
#[test]
fn update_field_pack() {
let field = "MyTestField";
let value = "http://test.uri";
let data = UpdateField {
field: Field::Key(field.to_string()),
value: value.to_string(),
};
let check = TokenMetadataInstruction::UpdateField(data.clone());
let preimage = hash::hashv(&[format!("{NAMESPACE}:updating_field").as_bytes()]);
let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
check_pack_unpack(check, discriminator, data);
}
#[test]
fn remove_key_pack() {
let data = RemoveKey {
key: "MyTestField".to_string(),
idempotent: true,
};
let check = TokenMetadataInstruction::RemoveKey(data.clone());
let preimage = hash::hashv(&[format!("{NAMESPACE}:remove_key_ix").as_bytes()]);
let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
check_pack_unpack(check, discriminator, data);
}
#[test]
fn update_authority_pack() {
let data = UpdateAuthority {
new_authority: OptionalNonZeroPubkey::default(),
};
let check = TokenMetadataInstruction::UpdateAuthority(data.clone());
let preimage = hash::hashv(&[format!("{NAMESPACE}:update_the_authority").as_bytes()]);
let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
check_pack_unpack(check, discriminator, data);
}
#[test]
fn emit_pack() {
let data = Emit {
start: None,
end: Some(10),
};
let check = TokenMetadataInstruction::Emit(data.clone());
let preimage = hash::hashv(&[format!("{NAMESPACE}:emitter").as_bytes()]);
let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH];
check_pack_unpack(check, discriminator, data);
}
#[cfg(feature = "serde-traits")]
#[test]
fn initialize_serde() {
let data = Initialize {
name: "Token Name".to_string(),
symbol: "TST".to_string(),
uri: "uri.test".to_string(),
};
let ix = TokenMetadataInstruction::Initialize(data);
let serialized = serde_json::to_string(&ix).unwrap();
let serialized_expected =
"{\"initialize\":{\"name\":\"Token Name\",\"symbol\":\"TST\",\"uri\":\"uri.test\"}}";
assert_eq!(&serialized, serialized_expected);
let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
assert_eq!(ix, deserialized);
}
#[cfg(feature = "serde-traits")]
#[test]
fn update_field_serde() {
let data = UpdateField {
field: Field::Key("MyField".to_string()),
value: "my field value".to_string(),
};
let ix = TokenMetadataInstruction::UpdateField(data);
let serialized = serde_json::to_string(&ix).unwrap();
let serialized_expected =
"{\"updateField\":{\"field\":{\"key\":\"MyField\"},\"value\":\"my field value\"}}";
assert_eq!(&serialized, serialized_expected);
let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
assert_eq!(ix, deserialized);
}
#[cfg(feature = "serde-traits")]
#[test]
fn remove_key_serde() {
let data = RemoveKey {
key: "MyTestField".to_string(),
idempotent: true,
};
let ix = TokenMetadataInstruction::RemoveKey(data);
let serialized = serde_json::to_string(&ix).unwrap();
let serialized_expected = "{\"removeKey\":{\"idempotent\":true,\"key\":\"MyTestField\"}}";
assert_eq!(&serialized, serialized_expected);
let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
assert_eq!(ix, deserialized);
}
#[cfg(feature = "serde-traits")]
#[test]
fn update_authority_serde() {
let update_authority_option: Option<Pubkey> =
Some(Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap());
let update_authority: OptionalNonZeroPubkey = update_authority_option.try_into().unwrap();
let data = UpdateAuthority {
new_authority: update_authority,
};
let ix = TokenMetadataInstruction::UpdateAuthority(data);
let serialized = serde_json::to_string(&ix).unwrap();
let serialized_expected = "{\"updateAuthority\":{\"newAuthority\":\"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM\"}}";
assert_eq!(&serialized, serialized_expected);
let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
assert_eq!(ix, deserialized);
}
#[cfg(feature = "serde-traits")]
#[test]
fn update_authority_serde_with_none() {
let data = UpdateAuthority {
new_authority: OptionalNonZeroPubkey::default(),
};
let ix = TokenMetadataInstruction::UpdateAuthority(data);
let serialized = serde_json::to_string(&ix).unwrap();
let serialized_expected = "{\"updateAuthority\":{\"newAuthority\":null}}";
assert_eq!(&serialized, serialized_expected);
let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
assert_eq!(ix, deserialized);
}
#[cfg(feature = "serde-traits")]
#[test]
fn emit_serde() {
let data = Emit {
start: None,
end: Some(10),
};
let ix = TokenMetadataInstruction::Emit(data);
let serialized = serde_json::to_string(&ix).unwrap();
let serialized_expected = "{\"emit\":{\"start\":null,\"end\":10}}";
assert_eq!(&serialized, serialized_expected);
let deserialized = serde_json::from_str::<TokenMetadataInstruction>(&serialized).unwrap();
assert_eq!(ix, deserialized);
}
}