mpl_token_metadata/processor/
mod.rs

1mod bubblegum;
2mod burn;
3mod collection;
4mod delegate;
5mod edition;
6pub(crate) mod escrow;
7mod freeze;
8mod metadata;
9mod state;
10mod uses;
11mod verification;
12
13use borsh::{BorshDeserialize, BorshSerialize};
14pub use bubblegum::*;
15pub use burn::*;
16pub use collection::*;
17pub use delegate::*;
18pub use edition::*;
19pub use escrow::*;
20pub use freeze::*;
21pub use metadata::*;
22use mpl_token_auth_rules::payload::Payload;
23use mpl_utils::cmp_pubkeys;
24#[cfg(feature = "serde-feature")]
25use serde::{Deserialize, Serialize};
26use solana_program::{
27    account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
28    pubkey::Pubkey,
29};
30pub use state::*;
31pub use uses::*;
32pub use verification::*;
33
34use crate::{
35    error::MetadataError,
36    instruction::{
37        MetadataInstruction, CREATE_METADATA_ACCOUNT, CREATE_METADATA_ACCOUNT_V2,
38        DEPRECATED_CREATE_MASTER_EDITION, DEPRECATED_CREATE_RESERVATION_LIST,
39        DEPRECATED_MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_PRINTING_TOKEN,
40        DEPRECATED_MINT_PRINTING_TOKENS, DEPRECATED_MINT_PRINTING_TOKENS_VIA_TOKEN,
41        DEPRECATED_SET_RESERVATION_LIST, UPDATE_METADATA_ACCOUNT,
42    },
43    processor::{
44        edition::{
45            process_convert_master_edition_v1_to_v2, process_create_master_edition,
46            process_mint_new_edition_from_master_edition_via_token,
47        },
48        escrow::process_transfer_out_of_escrow,
49    },
50    state::{
51        Key, Metadata, TokenMetadataAccount, TokenStandard, TokenState, DISCRIMINATOR_INDEX,
52        TOKEN_STATE_INDEX,
53    },
54};
55
56#[repr(C)]
57#[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))]
58#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)]
59pub struct AuthorizationData {
60    pub payload: Payload,
61}
62
63impl AuthorizationData {
64    pub fn new(payload: Payload) -> Self {
65        Self { payload }
66    }
67    pub fn new_empty() -> Self {
68        Self {
69            payload: Payload::new(),
70        }
71    }
72}
73
74/// Process Token Metadata instructions.
75///
76/// The processor is divided into two parts:
77/// * It first tries to match the instruction into the new API;
78/// * If it is not one of the new instructions, it checks that any metadata
79///   account is not a pNFT before forwarding the transaction processing to
80///   the "legacy" processor.
81pub fn process_instruction<'a>(
82    program_id: &'a Pubkey,
83    accounts: &'a [AccountInfo<'a>],
84    input: &[u8],
85) -> ProgramResult {
86    let (variant, _args) = input
87        .split_first()
88        .ok_or(MetadataError::InvalidInstruction)?;
89
90    let instruction = match MetadataInstruction::try_from_slice(input) {
91        Ok(instruction) => Ok(instruction),
92        // Check if the instruction is a deprecated instruction.
93        Err(_) => match *variant {
94            CREATE_METADATA_ACCOUNT
95            | UPDATE_METADATA_ACCOUNT
96            | DEPRECATED_CREATE_MASTER_EDITION
97            | DEPRECATED_MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_PRINTING_TOKEN
98            | DEPRECATED_SET_RESERVATION_LIST
99            | DEPRECATED_CREATE_RESERVATION_LIST
100            | DEPRECATED_MINT_PRINTING_TOKENS_VIA_TOKEN
101            | DEPRECATED_MINT_PRINTING_TOKENS
102            | CREATE_METADATA_ACCOUNT_V2 => Err(MetadataError::Removed.into()),
103            _ => Err(ProgramError::InvalidInstructionData),
104        },
105    }?;
106
107    // checks if there is a locked token; this will block any instruction that
108    // requires the token record account when the token is locked – 'Update' is
109    // an example of an instruction that does not require the token record, so
110    // it can be executed even when a token is locked
111    if is_locked(program_id, accounts) && !matches!(instruction, MetadataInstruction::Unlock(_)) {
112        return Err(MetadataError::LockedToken.into());
113    }
114
115    // match on the new instruction set
116    match instruction {
117        MetadataInstruction::Burn(args) => {
118            msg!("IX: Burn");
119            burn::burn(program_id, accounts, args)
120        }
121        MetadataInstruction::Create(args) => {
122            msg!("IX: Create");
123            metadata::create(program_id, accounts, args)
124        }
125        MetadataInstruction::Mint(args) => {
126            msg!("IX: Mint");
127            metadata::mint(program_id, accounts, args)
128        }
129        MetadataInstruction::Delegate(args) => {
130            msg!("IX: Delegate");
131            delegate::delegate(program_id, accounts, args)
132        }
133        MetadataInstruction::Revoke(args) => {
134            msg!("IX: Revoke");
135            delegate::revoke(program_id, accounts, args)
136        }
137        MetadataInstruction::Lock(args) => {
138            msg!("IX: Lock");
139            state::lock(program_id, accounts, args)
140        }
141        MetadataInstruction::Unlock(args) => {
142            msg!("IX: Unlock");
143            state::unlock(program_id, accounts, args)
144        }
145        MetadataInstruction::Migrate(args) => {
146            msg!("IX: Migrate");
147            metadata::migrate(program_id, accounts, args)
148        }
149        MetadataInstruction::Transfer(args) => {
150            msg!("IX: Transfer");
151            metadata::transfer(program_id, accounts, args)
152        }
153        MetadataInstruction::Update(args) => {
154            msg!("IX: Update");
155            metadata::update(program_id, accounts, args)
156        }
157        MetadataInstruction::Verify(args) => {
158            msg!("IX: Verify");
159            verification::verify(program_id, accounts, args)
160        }
161        MetadataInstruction::Unverify(args) => {
162            msg!("IX: Unverify");
163            verification::unverify(program_id, accounts, args)
164        }
165        _ => {
166            // pNFT accounts can only be used by the "new" API; before forwarding
167            // the transaction to the "legacy" processor we determine whether we are
168            // dealing with a pNFT or not
169            if !has_programmable_metadata(program_id, accounts)? {
170                process_legacy_instruction(program_id, accounts, instruction)
171            } else {
172                Err(MetadataError::InstructionNotSupported.into())
173            }
174        }
175    }
176}
177
178/// Matches "legacy" (pre-pNFT) instructions.
179fn process_legacy_instruction<'a>(
180    program_id: &'a Pubkey,
181    accounts: &'a [AccountInfo<'a>],
182    instruction: MetadataInstruction,
183) -> ProgramResult {
184    match instruction {
185        MetadataInstruction::CreateMetadataAccount => Err(MetadataError::Removed.into()),
186        MetadataInstruction::UpdateMetadataAccount => Err(MetadataError::Removed.into()),
187        MetadataInstruction::CreateMetadataAccountV2 => Err(MetadataError::Removed.into()),
188        MetadataInstruction::CreateMetadataAccountV3(args) => {
189            msg!("IX: Create Metadata Accounts v3");
190            process_create_metadata_accounts_v3(
191                program_id,
192                accounts,
193                args.data,
194                args.is_mutable,
195                args.collection_details,
196            )
197        }
198        MetadataInstruction::UpdateMetadataAccountV2(args) => {
199            msg!("IX: Update Metadata Accounts v2");
200            process_update_metadata_accounts_v2(
201                program_id,
202                accounts,
203                args.data,
204                args.update_authority,
205                args.primary_sale_happened,
206                args.is_mutable,
207            )
208        }
209        MetadataInstruction::DeprecatedCreateMasterEdition => Err(MetadataError::Removed.into()),
210        MetadataInstruction::DeprecatedMintNewEditionFromMasterEditionViaPrintingToken => {
211            Err(MetadataError::Removed.into())
212        }
213        MetadataInstruction::UpdatePrimarySaleHappenedViaToken => {
214            msg!("IX: Update primary sale via token");
215            process_update_primary_sale_happened_via_token(program_id, accounts)
216        }
217        MetadataInstruction::DeprecatedSetReservationList => Err(MetadataError::Removed.into()),
218        MetadataInstruction::DeprecatedCreateReservationList => Err(MetadataError::Removed.into()),
219        MetadataInstruction::SignMetadata => {
220            msg!("IX: Sign Metadata");
221            process_sign_metadata(program_id, accounts)
222        }
223        MetadataInstruction::RemoveCreatorVerification => {
224            msg!("IX: Remove Creator Verification");
225            process_remove_creator_verification(program_id, accounts)
226        }
227        MetadataInstruction::DeprecatedMintPrintingTokensViaToken => {
228            Err(MetadataError::Removed.into())
229        }
230        MetadataInstruction::DeprecatedMintPrintingTokens => Err(MetadataError::Removed.into()),
231        MetadataInstruction::CreateMasterEdition => Err(MetadataError::Removed.into()),
232        MetadataInstruction::CreateMasterEditionV3(args) => {
233            msg!("V3 Create Master Edition");
234            process_create_master_edition(program_id, accounts, args.max_supply)
235        }
236        MetadataInstruction::MintNewEditionFromMasterEditionViaToken(args) => {
237            msg!("IX: Mint New Edition from Master Edition Via Token");
238            process_mint_new_edition_from_master_edition_via_token(
239                program_id,
240                accounts,
241                args.edition,
242                false,
243            )
244        }
245        MetadataInstruction::ConvertMasterEditionV1ToV2 => {
246            msg!("IX: Convert Master Edition V1 to V2");
247            process_convert_master_edition_v1_to_v2(program_id, accounts)
248        }
249        MetadataInstruction::MintNewEditionFromMasterEditionViaVaultProxy(_args) => {
250            Err(MetadataError::Removed.into())
251        }
252        MetadataInstruction::PuffMetadata => {
253            msg!("IX: Puff Metadata");
254            process_puff_metadata_account(program_id, accounts)
255        }
256        MetadataInstruction::VerifyCollection => {
257            msg!("IX: Verify Collection");
258            verify_collection(program_id, accounts)
259        }
260        MetadataInstruction::SetAndVerifyCollection => {
261            msg!("IX: Set and Verify Collection");
262            set_and_verify_collection(program_id, accounts)
263        }
264        MetadataInstruction::UnverifyCollection => {
265            msg!("IX: Unverify Collection");
266            unverify_collection(program_id, accounts)
267        }
268        MetadataInstruction::Utilize(args) => {
269            msg!("IX: Use/Utilize Token");
270            process_utilize(program_id, accounts, args.number_of_uses)
271        }
272        MetadataInstruction::ApproveUseAuthority(args) => {
273            msg!("IX: Approve Use Authority");
274            process_approve_use_authority(program_id, accounts, args.number_of_uses)
275        }
276        MetadataInstruction::RevokeUseAuthority => {
277            msg!("IX: Revoke Use Authority");
278            process_revoke_use_authority(program_id, accounts)
279        }
280        MetadataInstruction::ApproveCollectionAuthority => {
281            msg!("IX: Approve Collection Authority");
282            process_approve_collection_authority(program_id, accounts)
283        }
284        MetadataInstruction::RevokeCollectionAuthority => {
285            msg!("IX: Revoke Collection Authority");
286            process_revoke_collection_authority(program_id, accounts)
287        }
288        MetadataInstruction::FreezeDelegatedAccount => {
289            msg!("IX: Freeze Delegated Account");
290            process_freeze_delegated_account(program_id, accounts)
291        }
292        MetadataInstruction::ThawDelegatedAccount => {
293            msg!("IX: Thaw Delegated Account");
294            process_thaw_delegated_account(program_id, accounts)
295        }
296        MetadataInstruction::BurnNft => {
297            msg!("IX: Burn NFT");
298            process_burn_nft(program_id, accounts)
299        }
300        MetadataInstruction::BurnEditionNft => {
301            msg!("IX: Burn Edition NFT");
302            process_burn_edition_nft(program_id, accounts)
303        }
304        MetadataInstruction::VerifySizedCollectionItem => {
305            msg!("IX: Verify Collection V2");
306            verify_sized_collection_item(program_id, accounts)
307        }
308        MetadataInstruction::SetAndVerifySizedCollectionItem => {
309            msg!("IX: Set and Verify Collection");
310            set_and_verify_sized_collection_item(program_id, accounts)
311        }
312        MetadataInstruction::UnverifySizedCollectionItem => {
313            msg!("IX: Unverify Sized Collection");
314            unverify_sized_collection_item(program_id, accounts)
315        }
316        MetadataInstruction::SetCollectionSize(args) => {
317            msg!("IX: Set Collection Size");
318            set_collection_size(program_id, accounts, args)
319        }
320        MetadataInstruction::SetTokenStandard => {
321            msg!("IX: Set Token Standard");
322            process_set_token_standard(program_id, accounts)
323        }
324        MetadataInstruction::BubblegumSetCollectionSize(args) => {
325            msg!("IX: Bubblegum Program Set Collection Size");
326            bubblegum_set_collection_size(program_id, accounts, args)
327        }
328        MetadataInstruction::CreateEscrowAccount => {
329            msg!("IX: Create Escrow Account");
330            process_create_escrow_account(program_id, accounts)
331        }
332        MetadataInstruction::CloseEscrowAccount => {
333            msg!("IX: Close Escrow Account");
334            process_close_escrow_account(program_id, accounts)
335        }
336        MetadataInstruction::TransferOutOfEscrow(args) => {
337            msg!("IX: Transfer Out Of Escrow");
338            process_transfer_out_of_escrow(program_id, accounts, args)
339        }
340        _ => Err(ProgramError::InvalidInstructionData),
341    }
342}
343
344/// Convenience function for accessing the next item in an [`AccountInfo`]
345/// iterator and validating whether the account is present or not.
346///
347/// This relies on the client setting the `crate::ID` as the pubkey for
348/// accounts that are not set, which effectively allows us to use positional
349/// optional accounts.
350pub fn next_optional_account_info<'a, 'b, I: Iterator<Item = &'a AccountInfo<'b>>>(
351    iter: &mut I,
352) -> Result<Option<I::Item>, ProgramError> {
353    let account_info = iter.next().ok_or(ProgramError::NotEnoughAccountKeys)?;
354
355    Ok(if cmp_pubkeys(account_info.key, &crate::ID) {
356        None
357    } else {
358        Some(account_info)
359    })
360}
361
362/// Convenience function for accessing an [`AccountInfo`] by index
363/// and validating whether the account is present or not.
364///
365/// This relies on the client setting the `crate::ID` as the pubkey for
366/// accounts that are not set, which effectively allows us to use positional
367/// optional accounts.
368pub fn try_get_account_info<'a>(
369    accounts: &'a [AccountInfo<'a>],
370    index: usize,
371) -> Result<&'a AccountInfo<'a>, ProgramError> {
372    let account_info = try_get_optional_account_info(accounts, index)?;
373    // validates that we got an account info
374    if let Some(account_info) = account_info {
375        Ok(account_info)
376    } else {
377        Err(ProgramError::NotEnoughAccountKeys)
378    }
379}
380
381/// Convenience function for accessing an [`AccountInfo`] by index
382/// and validating whether the account is present or not.
383///
384/// This relies on the client setting the `crate::ID` as the pubkey for
385/// accounts that are not set, which effectively allows us to use positional
386/// optional accounts.
387pub fn try_get_optional_account_info<'a>(
388    accounts: &'a [AccountInfo<'a>],
389    index: usize,
390) -> Result<Option<&'a AccountInfo<'a>>, ProgramError> {
391    if index < accounts.len() {
392        Ok(if cmp_pubkeys(accounts[index].key, &crate::ID) {
393            None
394        } else {
395            Some(&accounts[index])
396        })
397    } else {
398        Err(ProgramError::NotEnoughAccountKeys)
399    }
400}
401
402/// Checks if the instruction's accounts contain a pNFT metadata.
403///
404/// We need to determine if we are dealing with a pNFT metadata or not
405/// so we can restrict the available instructions.
406fn has_programmable_metadata(
407    program_id: &Pubkey,
408    accounts: &[AccountInfo],
409) -> Result<bool, ProgramError> {
410    for account_info in accounts {
411        // checks the account is owned by Token Metadata and it has data
412        if account_info.owner == program_id && !account_info.data_is_empty() {
413            let discriminator = account_info.data.borrow()[DISCRIMINATOR_INDEX];
414            // checks if the account is a Metadata account
415            if discriminator == Key::MetadataV1 as u8 {
416                let metadata = Metadata::from_account_info(account_info)?;
417
418                if matches!(
419                    metadata.token_standard,
420                    Some(TokenStandard::ProgrammableNonFungible)
421                ) {
422                    return Ok(true);
423                }
424            }
425        }
426    }
427
428    Ok(false)
429}
430
431/// Checks if the instruction's accounts contain a locked pNFT.
432fn is_locked(program_id: &Pubkey, accounts: &[AccountInfo]) -> bool {
433    for account_info in accounts {
434        // checks the account is owned by Token Metadata and it has data
435        if account_info.owner == program_id && !account_info.data_is_empty() {
436            let data = account_info.data.borrow();
437            // checks if the account is a Metadata account
438            if (data[DISCRIMINATOR_INDEX] == Key::TokenRecord as u8)
439                && (data[TOKEN_STATE_INDEX] == TokenState::Locked as u8)
440            {
441                return true;
442            }
443        }
444    }
445
446    false
447}