diff --git a/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/initialize.rs b/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/initialize.rs new file mode 100644 index 00000000..dcd5592f --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/initialize.rs @@ -0,0 +1,85 @@ +use { + crate::instructions::extensions::{ + metadata_pointer::state::{ + offset_metadata_pointer_initialize as OFFSET, InstructionDiscriminatorMetadataPointer, + }, + ExtensionDiscriminator, + }, + pinocchio::{ + account_info::AccountInfo, + cpi::invoke_signed, + instruction::{AccountMeta, Instruction, Signer}, + pubkey::Pubkey, + ProgramResult, + }, +}; + +/// Initialize a new mint with a metadata pointer +/// +/// Accounts expected by this instruction: +/// +/// 0. `[writable]` The mint to initialize. +pub struct Initialize<'a, 'b> { + /// The mint to initialize with the metadata pointer extension. + pub mint: &'a AccountInfo, + /// Optional authority that can later update the metadata address. + pub authority: Option<&'b Pubkey>, + /// Optional initial metadata address. + pub metadata_address: Option<&'b Pubkey>, + /// Token program (Token-2022). + pub token_program: &'b Pubkey, +} + +impl Initialize<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + #[inline(always)] + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + let account_metas = [AccountMeta::writable(self.mint.key())]; + + let mut buffer = [0u8; OFFSET::END as usize]; + let data = initialize_instruction_data(&mut buffer, self.authority, self.metadata_address); + + let instruction = Instruction { + program_id: self.token_program, + data, + accounts: &account_metas, + }; + + invoke_signed(&instruction, &[self.mint], signers) + } +} + +#[inline(always)] +fn initialize_instruction_data<'a>( + buffer: &'a mut [u8], + authority: Option<&Pubkey>, + metadata_address: Option<&Pubkey>, +) -> &'a [u8] { + let mut offset = OFFSET::START as usize; + + // Encode discriminators (Metadata + Initialize) + buffer[..offset].copy_from_slice(&[ + ExtensionDiscriminator::MetadataPointer as u8, + InstructionDiscriminatorMetadataPointer::Initialize as u8, + ]); + + // write authority pubkey bytes + if let Some(authority) = authority { + buffer[offset..offset + OFFSET::AUTHORITY_PUBKEY as usize].copy_from_slice(authority); + } + + // shift offset past authority pubkey + offset += OFFSET::AUTHORITY_PUBKEY as usize; + + // write metadata_address pubkey bytes + if let Some(metadata_address) = metadata_address { + buffer[offset..offset + OFFSET::METADATA_ADDRESS_PUBKEY as usize] + .copy_from_slice(metadata_address); + } + + buffer +} diff --git a/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/mod.rs b/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/mod.rs new file mode 100644 index 00000000..c99d50ff --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/mod.rs @@ -0,0 +1,4 @@ +mod initialize; +mod update; + +pub use {initialize::*, update::*}; diff --git a/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/update.rs b/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/update.rs new file mode 100644 index 00000000..6e258fb6 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/metadata_pointer/instructions/update.rs @@ -0,0 +1,159 @@ +use { + crate::{ + instructions::extensions::{ + metadata_pointer::state::{ + offset_metadata_pointer_update as OFFSET, InstructionDiscriminatorMetadataPointer, + }, + ExtensionDiscriminator, + }, + instructions::MAX_MULTISIG_SIGNERS, + }, + core::{mem::MaybeUninit, slice}, + pinocchio::{ + account_info::AccountInfo, + cpi::invoke_signed_with_bounds, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, + ProgramResult, + }, +}; + +/// Update the metadata pointer address. Only supported for mints that +/// include the `MetadataPointer` extension. +/// +/// Accounts expected by this instruction: +/// +/// * Single authority +/// 0. `[writable]` The mint. +/// 1. `[signer]` The metadata pointer authority. +/// +/// * Multisignature authority +/// 0. `[writable]` The mint. +/// 1. `[]` The mint's metadata pointer authority. +/// 2. `..2+M` `[signer]` M signer accounts. +pub struct Update<'a, 'b> { + /// The mint to update. + pub mint: &'a AccountInfo, + /// The metadata pointer authority. + pub authority: &'a AccountInfo, + /// New metadata address (use `None` to clear). + pub new_metadata_address: Option<&'b Pubkey>, + /// The Signer accounts if `authority` is a multisig. + pub signers: &'a [AccountInfo], + /// Token program (Token-2022). + pub token_program: &'b Pubkey, +} + +impl Update<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + #[inline(always)] + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + let &Self { + mint, + authority, + signers: multisig_accounts, + token_program, + .. + } = self; + + if multisig_accounts.len() > MAX_MULTISIG_SIGNERS { + Err(ProgramError::InvalidArgument)?; + } + + const UNINIT_ACCOUNT_METAS: MaybeUninit = MaybeUninit::::uninit(); + let mut account_metas = [UNINIT_ACCOUNT_METAS; 2 + MAX_MULTISIG_SIGNERS]; + + unsafe { + // SAFETY: + // - `account_metas` is sized to 2 + MAX_MULTISIG_SIGNERS + + // - Index 0 is always present (Mint) + account_metas + .get_unchecked_mut(0) + .write(AccountMeta::writable(mint.key())); + + // - Index 1 is always present (Authority) + if multisig_accounts.is_empty() { + account_metas + .get_unchecked_mut(1) + .write(AccountMeta::readonly_signer(authority.key())); + } else { + account_metas + .get_unchecked_mut(1) + .write(AccountMeta::readonly(authority.key())); + } + } + + for (account_meta, signer) in account_metas[2..].iter_mut().zip(multisig_accounts.iter()) { + account_meta.write(AccountMeta::readonly_signer(signer.key())); + } + + // build instruction + let mut buffer = [0u8; OFFSET::END as usize]; + let data = update_instruction_data(&mut buffer, self.new_metadata_address); + + let num_accounts = 2 + multisig_accounts.len(); + + let instruction = Instruction { + program_id: token_program, + data: data, + accounts: unsafe { + slice::from_raw_parts(account_metas.as_ptr() as *const AccountMeta, num_accounts) + }, + }; + + // Account info array + const UNINIT_ACCOUNT_INFOS: MaybeUninit<&AccountInfo> = + MaybeUninit::<&AccountInfo>::uninit(); + let mut account_infos = [UNINIT_ACCOUNT_INFOS; 2 + MAX_MULTISIG_SIGNERS]; + + unsafe { + // SAFETY: + // - `account_infos` is sized to 2 + MAX_MULTISIG_SIGNERS + // - Index 0 is always present + account_infos.get_unchecked_mut(0).write(mint); + // - Index 1 is always present + account_infos.get_unchecked_mut(1).write(authority); + } + + // Fill signer accounts + for (account_info, signer) in account_infos[2..].iter_mut().zip(multisig_accounts.iter()) { + account_info.write(signer); + } + + invoke_signed_with_bounds::<{ 2 + MAX_MULTISIG_SIGNERS }>( + &instruction, + unsafe { + slice::from_raw_parts(account_infos.as_ptr() as *const &AccountInfo, num_accounts) + }, + signers, + ) + } +} + +#[inline(always)] +fn update_instruction_data<'a>( + buffer: &'a mut [u8], + new_metadata_address: Option<&Pubkey>, +) -> &'a [u8] { + let offset = OFFSET::START as usize; + + // Encode discriminators (Metadata + Update) + buffer[..offset].copy_from_slice(&[ + ExtensionDiscriminator::MetadataPointer as u8, + InstructionDiscriminatorMetadataPointer::Update as u8, + ]); + + // write new_metadata_address pubkey bytes + if let Some(new_metadata_address) = new_metadata_address { + buffer[offset..offset + OFFSET::METADATA_ADDRESS_PUBKEY as usize] + .copy_from_slice(new_metadata_address); + } + + buffer +} diff --git a/programs/token-2022/src/instructions/extensions/metadata_pointer/mod.rs b/programs/token-2022/src/instructions/extensions/metadata_pointer/mod.rs new file mode 100644 index 00000000..87a1fb46 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/metadata_pointer/mod.rs @@ -0,0 +1,4 @@ +pub mod instructions; +pub mod state; + +pub use {instructions::*, state::*}; diff --git a/programs/token-2022/src/instructions/extensions/metadata_pointer/state.rs b/programs/token-2022/src/instructions/extensions/metadata_pointer/state.rs new file mode 100644 index 00000000..8658d4c2 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/metadata_pointer/state.rs @@ -0,0 +1,38 @@ +use pinocchio::pubkey::Pubkey; + +#[repr(u8)] +pub enum InstructionDiscriminatorMetadataPointer { + Initialize = 0, + Update = 1, +} + +/// Instruction data layout: +/// - [0] : Extension discriminator (1 byte) +/// - [1] : Instruction discriminator (1 byte) +/// - [2..34] : authority pubkey (32 bytes) +/// - [34..66] : metadata_address pubkey (32 bytes) +pub mod offset_metadata_pointer_initialize { + pub const START: u8 = 2; + pub const AUTHORITY_PUBKEY: u8 = 32; + pub const METADATA_ADDRESS_PUBKEY: u8 = 32; + pub const END: u8 = START + AUTHORITY_PUBKEY + METADATA_ADDRESS_PUBKEY; +} + +/// Instruction data layout: +/// - [0] : Extension discriminator (1 byte) +/// - [1] : Instruction discriminator (1 byte) +/// - [2..34] : metadata_address pubkey (32 bytes) +pub mod offset_metadata_pointer_update { + pub const START: u8 = 2; + pub const METADATA_ADDRESS_PUBKEY: u8 = 32; + pub const END: u8 = START + METADATA_ADDRESS_PUBKEY; +} + +/// Metadata pointer extension data for mints. +#[repr(C)] +pub struct MetadataPointer { + /// Authority that can set the metadata address + pub authority: Pubkey, + /// Account address that holds the metadata + pub metadata_address: Pubkey, +}