Skip to main content

apl_token_metadata/
processor.rs

1//! Program state processor
2
3use {
4    crate::{
5        error::MetadataError,
6        find_attributes_pda_with_program, find_metadata_pda_with_program,
7        instruction::MetadataInstruction,
8        state::{
9            TokenMetadata, TokenMetadataAttributes, DESCRIPTION_MAX_LEN, IMAGE_MAX_LEN,
10            MAX_ATTRIBUTES, MAX_KEY_LENGTH, MAX_VALUE_LENGTH, NAME_MAX_LEN, SYMBOL_MAX_LEN,
11        },
12        ATTRIBUTES_SEED, METADATA_SEED,
13    },
14    apl_token::{self, state::Mint},
15    arch_program::{
16        account::{next_account_info, AccountInfo},
17        bitcoin::{self as arch_bitcoin, hashes::Hash},
18        entrypoint::ProgramResult,
19        input_to_sign::InputToSign,
20        msg,
21        program::{get_transaction_to_sign, invoke_signed, set_input_to_sign},
22        program_error::ProgramError,
23        program_option::COption,
24        program_pack::{IsInitialized, Pack},
25        pubkey::Pubkey,
26        rent::minimum_rent,
27        system_instruction::create_account,
28    },
29};
30
31/// Program state handler.
32pub struct Processor {}
33
34impl Processor {
35    /// Process a single instruction
36    pub fn process(
37        program_id: &Pubkey,
38        accounts: &[AccountInfo],
39        instruction_data: &[u8],
40    ) -> ProgramResult {
41        let instruction = MetadataInstruction::unpack(instruction_data)?;
42
43        match instruction {
44            MetadataInstruction::CreateMetadata {
45                name,
46                symbol,
47                image,
48                description,
49                immutable,
50            } => Self::process_create_metadata(
51                program_id,
52                accounts,
53                name,
54                symbol,
55                image,
56                description,
57                immutable,
58            ),
59            MetadataInstruction::UpdateMetadata {
60                name,
61                symbol,
62                image,
63                description,
64            } => Self::process_update_metadata(accounts, name, symbol, image, description),
65            MetadataInstruction::CreateAttributes { data } => {
66                Self::process_create_attributes(program_id, accounts, data)
67            }
68            MetadataInstruction::ReplaceAttributes { data } => {
69                Self::process_replace_attributes(program_id, accounts, data)
70            }
71
72            MetadataInstruction::TransferAuthority { new_authority } => {
73                Self::process_transfer_authority(accounts, new_authority)
74            }
75
76            MetadataInstruction::MakeImmutable => Self::process_make_immutable(accounts),
77            MetadataInstruction::Anchor {
78                input_index,
79                input_signer,
80            } => {
81                msg!("Instruction: Anchor");
82                Self::process_anchor(program_id, accounts, input_index, input_signer)
83            }
84        }
85    }
86
87    fn process_create_metadata(
88        program_id: &Pubkey,
89        accounts: &[AccountInfo],
90        name: String,
91        symbol: String,
92        image: String,
93        description: String,
94        immutable: bool,
95    ) -> ProgramResult {
96        let account_info_iter = &mut accounts.iter();
97        let payer_info = next_account_info(account_info_iter)?; // [writable, signer]
98        let system_program_info = next_account_info(account_info_iter)?; // []
99        let mint_info = next_account_info(account_info_iter)?; // []
100        let metadata_info = next_account_info(account_info_iter)?; // [writable]
101        let mint_authority_info = next_account_info(account_info_iter)?; // [signer]
102
103        // Token mint must be owned by the token program
104        if mint_info.owner != &apl_token::id() {
105            msg!(
106                "Mint is not owned by the token program expected {:?}, got {:?}",
107                apl_token::id(),
108                mint_info.owner
109            );
110            return Err(ProgramError::IncorrectProgramId);
111        }
112
113        // Deserialize mint and enforce authority policy
114        let mint = Mint::unpack(&mint_info.data.borrow()) //
115            .map_err(|_| ProgramError::InvalidAccountData)?;
116
117        if !mint.is_initialized() {
118            msg!("Mint is not initialized");
119            return Err(ProgramError::UninitializedAccount);
120        }
121
122        // Check signer against mint authority, with freeze authority fallback
123        let matched_signer: Option<Pubkey> = match mint.mint_authority {
124            COption::Some(mint_auth) => {
125                if cmp_pubkeys(&mint_auth, mint_authority_info.key) {
126                    Some(mint_auth)
127                } else {
128                    msg!("Mint authority does not match expected authority");
129                    return Err(MetadataError::InvalidAuthority.into());
130                }
131            }
132            COption::None => match mint.freeze_authority {
133                COption::Some(freeze_auth) => {
134                    if cmp_pubkeys(&freeze_auth, mint_authority_info.key) {
135                        Some(freeze_auth)
136                    } else {
137                        msg!("Freeze authority does not match expected authority");
138                        return Err(MetadataError::InvalidAuthority.into());
139                    }
140                }
141                COption::None => {
142                    msg!("Mint has no mint or freeze authority");
143                    return Err(MetadataError::InvalidAuthority.into());
144                }
145            },
146        };
147
148        // Validate mint authority is signer
149        if !mint_authority_info.is_signer {
150            msg!("Mint authority is not a signer");
151            return Err(MetadataError::InvalidAuthority.into());
152        }
153
154        // Validate the metadata PDA address
155        let (expected_md_pda, md_bump) = find_metadata_pda_with_program(program_id, mint_info.key);
156        if !cmp_pubkeys(&expected_md_pda, metadata_info.key) {
157            msg!("Metadata PDA does not match expected PDA");
158            return Err(ProgramError::InvalidSeeds);
159        }
160
161        // Validate field sizes
162        if name.len() > NAME_MAX_LEN
163            || symbol.len() > SYMBOL_MAX_LEN
164            || image.len() > IMAGE_MAX_LEN
165            || description.len() > DESCRIPTION_MAX_LEN
166        {
167            msg!(
168                "Metadata field size is too long: name={}/{}, symbol={}/{}, image={}/{}, description={}/{}",
169                name.len(),
170                NAME_MAX_LEN,
171                symbol.len(),
172                SYMBOL_MAX_LEN,
173                image.len(),
174                IMAGE_MAX_LEN,
175                description.len(),
176                DESCRIPTION_MAX_LEN,
177            );
178            return Err(MetadataError::StringTooLong.into());
179        }
180
181        // If not owned by this program, create metadata PDA via CPI using PDA seeds
182        if metadata_info.owner != program_id {
183            // Require correct system program id
184            if *system_program_info.key != Pubkey::system_program() {
185                msg!("System program id does not match expected system program id");
186                return Err(ProgramError::IncorrectProgramId);
187            }
188
189            if !payer_info.is_signer {
190                msg!("Payer is not a signer");
191                return Err(ProgramError::MissingRequiredSignature);
192            }
193
194            let space = TokenMetadata::LEN as u64;
195            let lamports = minimum_rent(TokenMetadata::LEN);
196
197            invoke_signed(
198                &create_account(
199                    payer_info.key,
200                    metadata_info.key,
201                    lamports,
202                    space,
203                    program_id,
204                ),
205                &[
206                    payer_info.clone(),
207                    metadata_info.clone(),
208                    system_program_info.clone(),
209                ],
210                &[&[
211                    METADATA_SEED, //
212                    mint_info.key.as_ref(),
213                    &[md_bump],
214                ]],
215            )?;
216        }
217
218        // Ensure not already initialized (zero-initialized account will have first byte == 0)
219        {
220            let data_ref = metadata_info.data.borrow();
221            if !data_ref.is_empty() && data_ref[0] != 0 {
222                return Err(MetadataError::MetadataAlreadyExists.into());
223            }
224        }
225
226        // Write metadata
227        let metadata = TokenMetadata {
228            is_initialized: true,
229            mint: *mint_info.key,
230            name,
231            symbol,
232            image,
233            description,
234            update_authority: if immutable { None } else { matched_signer },
235        };
236
237        metadata.pack_into_slice(&mut metadata_info.data.borrow_mut());
238
239        Ok(())
240    }
241
242    fn process_update_metadata(
243        accounts: &[AccountInfo],
244        name: Option<String>,
245        symbol: Option<String>,
246        image: Option<String>,
247        description: Option<String>,
248    ) -> ProgramResult {
249        let account_info_iter = &mut accounts.iter();
250        let metadata_info = next_account_info(account_info_iter)?; // [writable]
251        let update_authority_info = next_account_info(account_info_iter)?; // [signer]
252
253        if !update_authority_info.is_signer {
254            msg!("Update authority is not a signer");
255            return Err(ProgramError::MissingRequiredSignature);
256        }
257
258        // Load existing metadata
259        let mut metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
260            .map_err(|_| ProgramError::InvalidAccountData)?;
261
262        if !metadata.is_initialized() {
263            msg!("Metadata not initialized");
264            return Err(ProgramError::UninitializedAccount);
265        }
266
267        // Enforce update authority (immutable if None)
268        match metadata.update_authority {
269            Some(current_auth) => {
270                if !cmp_pubkeys(&current_auth, update_authority_info.key) {
271                    msg!("Update authority does not match");
272                    return Err(MetadataError::InvalidAuthority.into());
273                }
274            }
275            None => {
276                msg!("Metadata is immutable");
277                return Err(MetadataError::InvalidAuthority.into());
278            }
279        }
280
281        // Validate and apply optional fields
282        if let Some(ref n) = name {
283            if n.len() > NAME_MAX_LEN {
284                return Err(MetadataError::StringTooLong.into());
285            }
286        }
287        if let Some(ref s) = symbol {
288            if s.len() > SYMBOL_MAX_LEN {
289                return Err(MetadataError::StringTooLong.into());
290            }
291        }
292        if let Some(ref i) = image {
293            if i.len() > IMAGE_MAX_LEN {
294                return Err(MetadataError::StringTooLong.into());
295            }
296        }
297        if let Some(ref d) = description {
298            if d.len() > DESCRIPTION_MAX_LEN {
299                return Err(MetadataError::StringTooLong.into());
300            }
301        }
302
303        if let Some(n) = name {
304            metadata.name = n;
305        }
306        if let Some(s) = symbol {
307            metadata.symbol = s;
308        }
309        if let Some(i) = image {
310            metadata.image = i;
311        }
312        if let Some(d) = description {
313            metadata.description = d;
314        }
315
316        metadata.pack_into_slice(&mut metadata_info.data.borrow_mut());
317        Ok(())
318    }
319
320    fn process_create_attributes(
321        program_id: &Pubkey,
322        accounts: &[AccountInfo],
323        data: Vec<(String, String)>,
324    ) -> ProgramResult {
325        let account_info_iter = &mut accounts.iter();
326        let payer_info = next_account_info(account_info_iter)?; // [writable, signer]
327        let system_program_info = next_account_info(account_info_iter)?; // []
328        let mint_info = next_account_info(account_info_iter)?; // []
329        let attributes_info = next_account_info(account_info_iter)?; // [writable]
330        let update_authority_info = next_account_info(account_info_iter)?; // [signer]
331        let metadata_info = next_account_info(account_info_iter)?; // [] (readonly)
332
333        if !payer_info.is_signer || !update_authority_info.is_signer {
334            return Err(ProgramError::MissingRequiredSignature);
335        }
336
337        // Validate attribute PDA address using this program_id
338        let (expected_attrs_pda, attrs_bump) =
339            find_attributes_pda_with_program(program_id, mint_info.key);
340        if !cmp_pubkeys(&expected_attrs_pda, attributes_info.key) {
341            msg!("Attributes PDA does not match expected PDA");
342            return Err(ProgramError::InvalidSeeds);
343        }
344
345        // Ensure metadata exists and authority matches
346        let metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
347            .map_err(|_| ProgramError::InvalidAccountData)?;
348        if !metadata.is_initialized() || !cmp_pubkeys(&metadata.mint, mint_info.key) {
349            msg!("Metadata not initialized or mint mismatch");
350            return Err(ProgramError::InvalidAccountData);
351        }
352
353        match metadata.update_authority {
354            Some(current_auth) => {
355                if !cmp_pubkeys(&current_auth, update_authority_info.key) {
356                    msg!("Update authority does not match");
357                    return Err(MetadataError::InvalidAuthority.into());
358                }
359            }
360            None => {
361                msg!("Metadata is immutable");
362                return Err(MetadataError::InvalidAuthority.into());
363            }
364        }
365
366        // Validate vector sizes and elements
367        if data.len() > MAX_ATTRIBUTES {
368            msg!("Too many attributes: {} > {}", data.len(), MAX_ATTRIBUTES);
369            return Err(MetadataError::TooManyAttributes.into());
370        }
371        for (k, v) in &data {
372            if k.is_empty() || v.is_empty() {
373                msg!("Attribute key and value must be non-empty");
374                return Err(MetadataError::InvalidInstructionData.into());
375            }
376            if k.len() > MAX_KEY_LENGTH || v.len() > MAX_VALUE_LENGTH {
377                msg!(
378                    "Attribute key or value is too long: key={}/{}, value={}/{}",
379                    k.len(),
380                    MAX_KEY_LENGTH,
381                    v.len(),
382                    MAX_VALUE_LENGTH,
383                );
384                return Err(MetadataError::StringTooLong.into());
385            }
386        }
387
388        // Allocate full max size so future replacements never need reallocation
389        let required_space: u64 = TokenMetadataAttributes::LEN as u64;
390
391        if attributes_info.owner != program_id {
392            if *system_program_info.key != Pubkey::system_program() {
393                msg!("System program id does not match expected system program id");
394                return Err(ProgramError::IncorrectProgramId);
395            }
396            if !payer_info.is_signer {
397                msg!("Payer is not a signer");
398                return Err(ProgramError::MissingRequiredSignature);
399            }
400            let lamports = minimum_rent(TokenMetadataAttributes::LEN);
401
402            invoke_signed(
403                &create_account(
404                    payer_info.key,
405                    attributes_info.key,
406                    lamports,
407                    required_space,
408                    program_id,
409                ),
410                &[
411                    payer_info.clone(),
412                    attributes_info.clone(),
413                    system_program_info.clone(),
414                ],
415                &[&[
416                    ATTRIBUTES_SEED, //
417                    mint_info.key.as_ref(),
418                    &[attrs_bump],
419                ]],
420            )?;
421        } else {
422            let curr_len = attributes_info.data.borrow().len() as u64;
423            if curr_len != required_space {
424                msg!(
425                    "Attributes account size mismatch: curr={} required={}",
426                    curr_len,
427                    required_space
428                );
429                return Err(ProgramError::InvalidAccountData);
430            }
431        }
432
433        // Ensure not already initialized
434        {
435            let data_ref = attributes_info.data.borrow();
436            if !data_ref.is_empty() && data_ref[0] != 0 {
437                return Err(MetadataError::MetadataAlreadyExists.into());
438            }
439        }
440
441        let attrs = TokenMetadataAttributes {
442            is_initialized: true,
443            mint: *mint_info.key,
444            data,
445        };
446        attrs.pack_into_slice(&mut attributes_info.data.borrow_mut());
447        Ok(())
448    }
449
450    fn process_replace_attributes(
451        program_id: &Pubkey,
452        accounts: &[AccountInfo],
453        data: Vec<(String, String)>,
454    ) -> ProgramResult {
455        let account_info_iter = &mut accounts.iter();
456        let attributes_info = next_account_info(account_info_iter)?; // [writable]
457        let update_authority_info = next_account_info(account_info_iter)?; // [signer]
458        let metadata_info = next_account_info(account_info_iter)?; // [] (readonly)
459
460        if !update_authority_info.is_signer {
461            return Err(ProgramError::MissingRequiredSignature);
462        }
463
464        // Validate metadata and authority
465        let metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
466            .map_err(|_| ProgramError::InvalidAccountData)?;
467        if !metadata.is_initialized() {
468            return Err(ProgramError::UninitializedAccount);
469        }
470        match metadata.update_authority {
471            Some(current_auth) => {
472                if !cmp_pubkeys(&current_auth, update_authority_info.key) {
473                    return Err(MetadataError::InvalidAuthority.into());
474                }
475            }
476            None => return Err(MetadataError::InvalidAuthority.into()),
477        }
478
479        // Validate PDA
480        let (expected_attrs_pda, _bump) =
481            find_attributes_pda_with_program(program_id, &metadata.mint);
482        if !cmp_pubkeys(&expected_attrs_pda, attributes_info.key) {
483            return Err(ProgramError::InvalidSeeds);
484        }
485
486        // Ensure attributes exist
487        let mut attrs = TokenMetadataAttributes::unpack(&attributes_info.data.borrow())
488            .map_err(|_| ProgramError::InvalidAccountData)?;
489        if !attrs.is_initialized() {
490            return Err(ProgramError::UninitializedAccount);
491        }
492
493        // Validate sizes
494        if data.len() > MAX_ATTRIBUTES {
495            return Err(MetadataError::TooManyAttributes.into());
496        }
497        for (k, v) in &data {
498            if k.is_empty() || v.is_empty() {
499                return Err(MetadataError::InvalidInstructionData.into());
500            }
501            if k.len() > MAX_KEY_LENGTH || v.len() > MAX_VALUE_LENGTH {
502                return Err(MetadataError::StringTooLong.into());
503            }
504        }
505
506        // Replace vector
507        attrs.data = data;
508        attrs.pack_into_slice(&mut attributes_info.data.borrow_mut());
509        Ok(())
510    }
511
512    fn process_transfer_authority(
513        accounts: &[AccountInfo],
514        new_authority: Pubkey,
515    ) -> ProgramResult {
516        let account_info_iter = &mut accounts.iter();
517        let metadata_info = next_account_info(account_info_iter)?; // [writable]
518        let current_authority_info = next_account_info(account_info_iter)?; // [signer]
519
520        if !current_authority_info.is_signer {
521            msg!("Current authority is not a signer");
522            return Err(ProgramError::MissingRequiredSignature);
523        }
524
525        let mut metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
526            .map_err(|_| ProgramError::InvalidAccountData)?;
527        if !metadata.is_initialized() {
528            msg!("Metadata not initialized");
529            return Err(ProgramError::UninitializedAccount);
530        }
531
532        match metadata.update_authority {
533            Some(current_auth) => {
534                if !cmp_pubkeys(&current_auth, current_authority_info.key) {
535                    msg!("Signer is not current update authority");
536                    return Err(MetadataError::InvalidAuthority.into());
537                }
538            }
539            None => {
540                msg!("Metadata is immutable; cannot transfer authority");
541                return Err(MetadataError::InvalidAuthority.into());
542            }
543        }
544
545        metadata.update_authority = Some(new_authority);
546        metadata.pack_into_slice(&mut metadata_info.data.borrow_mut());
547        Ok(())
548    }
549
550    /// Signs a Bitcoin transaction input for a metadata-program-owned account.
551    /// Validates the update_authority from the metadata account.
552    fn process_anchor(
553        program_id: &Pubkey,
554        accounts: &[AccountInfo],
555        input_index: u32,
556        input_signer: Pubkey,
557    ) -> ProgramResult {
558        let account_info_iter = &mut accounts.iter();
559        let account_info = next_account_info(account_info_iter)?;
560        let authority_info = next_account_info(account_info_iter)?;
561
562        if !cmp_pubkeys(program_id, account_info.owner) {
563            msg!("Anchor: account not owned by this program");
564            return Err(ProgramError::IncorrectProgramId);
565        }
566
567        let metadata = TokenMetadata::unpack(&account_info.data.borrow())
568            .map_err(|_| ProgramError::InvalidAccountData)?;
569
570        if !metadata.is_initialized() {
571            msg!("Anchor: metadata not initialized");
572            return Err(ProgramError::UninitializedAccount);
573        }
574
575        let expected_authority = metadata
576            .update_authority
577            .ok_or(MetadataError::InvalidAuthority)?;
578
579        if !cmp_pubkeys(&expected_authority, authority_info.key) {
580            msg!("Anchor: authority does not match");
581            return Err(MetadataError::InvalidAuthority.into());
582        }
583
584        if !authority_info.is_signer {
585            msg!("Anchor: authority must be a signer");
586            return Err(ProgramError::MissingRequiredSignature);
587        }
588
589        let buf = get_transaction_to_sign();
590        let arr: [u8; 4] = buf
591            .get(..4)
592            .and_then(|s| s.try_into().ok())
593            .ok_or(ProgramError::InvalidInstructionData)?;
594        let tx_len = u32::from_le_bytes(arr) as usize;
595        let tx_data = buf
596            .get(4..4 + tx_len)
597            .ok_or(ProgramError::InvalidInstructionData)?;
598        let tx: arch_bitcoin::Transaction = arch_bitcoin::consensus::deserialize(tx_data)
599            .map_err(|_| ProgramError::InvalidInstructionData)?;
600
601        let txid = tx.compute_txid();
602        let mut txid_bytes: [u8; 32] = txid.as_raw_hash().to_byte_array();
603        txid_bytes.reverse();
604
605        let input_to_sign = InputToSign {
606            index: input_index,
607            signer: input_signer,
608        };
609
610        set_input_to_sign(accounts, txid_bytes, &[input_to_sign])?;
611
612        Ok(())
613    }
614
615    fn process_make_immutable(accounts: &[AccountInfo]) -> ProgramResult {
616        let account_info_iter = &mut accounts.iter();
617        let metadata_info = next_account_info(account_info_iter)?; // [writable]
618        let current_authority_info = next_account_info(account_info_iter)?; // [signer]
619
620        if !current_authority_info.is_signer {
621            msg!("Current authority is not a signer");
622            return Err(ProgramError::MissingRequiredSignature);
623        }
624
625        let mut metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
626            .map_err(|_| ProgramError::InvalidAccountData)?;
627        if !metadata.is_initialized() {
628            msg!("Metadata not initialized");
629            return Err(ProgramError::UninitializedAccount);
630        }
631
632        match metadata.update_authority {
633            Some(current_auth) => {
634                if !cmp_pubkeys(&current_auth, current_authority_info.key) {
635                    msg!("Signer is not current update authority");
636                    return Err(MetadataError::InvalidAuthority.into());
637                }
638            }
639            None => {
640                msg!("Metadata already immutable");
641                return Err(MetadataError::InvalidAuthority.into());
642            }
643        }
644
645        metadata.update_authority = None;
646        metadata.pack_into_slice(&mut metadata_info.data.borrow_mut());
647        Ok(())
648    }
649}
650
651/// Checks two pubkeys for equality using a cheap memcmp
652fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool {
653    arch_program::program_memory::sol_memcmp(a.as_ref(), b.as_ref(), 32) == 0
654}