spl_token_wrap/
metadata.rs

1//! Metadata resolution helpers for pointer-aware metadata sync
2
3use {
4    crate::{error::TokenWrapError, metaplex::metaplex_to_token_2022_metadata},
5    mpl_token_metadata::{accounts::Metadata as MetaplexMetadata, ID as MPL_TOKEN_METADATA_ID},
6    solana_account_info::AccountInfo,
7    solana_cpi::{get_return_data, invoke},
8    solana_program_error::ProgramError,
9    spl_token_2022::{
10        extension::{
11            metadata_pointer::MetadataPointer, BaseStateWithExtensions, PodStateWithExtensions,
12        },
13        id as token_2022_id,
14        pod::PodMint,
15    },
16    spl_token_metadata_interface::{instruction::emit, state::TokenMetadata},
17    spl_type_length_value::variable_len_pack::VariableLenPack,
18};
19
20/// Fetches metadata from a third-party program implementing
21/// `TokenMetadataInstruction` by invoking its `Emit` instruction and decoding
22/// the `TokenMetadata` struct from the return data.
23pub fn cpi_emit_and_decode<'a>(
24    owner_program_info: &AccountInfo<'a>,
25    metadata_info: &AccountInfo<'a>,
26) -> Result<TokenMetadata, ProgramError> {
27    invoke(
28        &emit(owner_program_info.key, metadata_info.key, None, None),
29        &[metadata_info.clone()],
30    )?;
31
32    if let Some((program_key, data)) = get_return_data() {
33        // This check ensures this data comes from the program we just called
34        if program_key == *owner_program_info.key {
35            return TokenMetadata::unpack_from_slice(&data);
36        }
37    }
38
39    Err(TokenWrapError::ExternalProgramReturnedNoData.into())
40}
41
42/// Resolve the canonical metadata source for an unwrapped Token-2022 mint
43/// by following its `MetadataPointer`.
44///
45/// Supported pointer targets:
46/// - Self
47/// - Token-2022 account
48/// - `Metaplex` PDA
49/// - Third-party program
50pub fn resolve_token_2022_source_metadata<'a>(
51    unwrapped_mint_info: &AccountInfo<'a>,
52    maybe_source_metadata_info: Option<&AccountInfo<'a>>,
53    maybe_owner_program_info: Option<&AccountInfo<'a>>,
54) -> Result<TokenMetadata, ProgramError> {
55    let data = unwrapped_mint_info.try_borrow_data()?;
56    let mint_state = PodStateWithExtensions::<PodMint>::unpack(&data)?;
57    let pointer = mint_state
58        .get_extension::<MetadataPointer>()
59        .map_err(|_| TokenWrapError::MetadataPointerMissing)?;
60    let metadata_addr =
61        Option::from(pointer.metadata_address).ok_or(TokenWrapError::MetadataPointerUnset)?;
62
63    // Scenario 1: points to self, read off unwrapped mint
64    if metadata_addr == *unwrapped_mint_info.key {
65        return mint_state.get_variable_len_extension::<TokenMetadata>();
66    }
67
68    // Metadata account must be passed by this point
69    let metadata_info = maybe_source_metadata_info.ok_or(ProgramError::NotEnoughAccountKeys)?;
70    if metadata_info.key != &metadata_addr {
71        return Err(TokenWrapError::MetadataPointerMismatch.into());
72    }
73
74    if metadata_info.owner == &token_2022_id() {
75        // This is explicitly unsupported. A metadata pointer should not point to
76        // another mint account.
77        Err(ProgramError::InvalidAccountData)
78    } else if metadata_info.owner == &MPL_TOKEN_METADATA_ID {
79        // Scenario 2: points to a Metaplex PDA
80        metaplex_to_token_2022_metadata(unwrapped_mint_info, metadata_info)
81    } else {
82        // Scenario 3: points to an external program
83        let owner_program_info =
84            maybe_owner_program_info.ok_or(ProgramError::NotEnoughAccountKeys)?;
85        if owner_program_info.key != metadata_info.owner {
86            return Err(ProgramError::InvalidAccountOwner);
87        }
88        cpi_emit_and_decode(owner_program_info, metadata_info)
89    }
90}
91
92/// Extracts the token metadata from the unwrapped mint
93pub fn extract_token_metadata<'a>(
94    unwrapped_mint_info: &AccountInfo<'a>,
95    source_metadata_info: Option<&AccountInfo<'a>>,
96    owner_program_info: Option<&AccountInfo<'a>>,
97) -> Result<TokenMetadata, ProgramError> {
98    let unwrapped_metadata = if *unwrapped_mint_info.owner == spl_token_2022::id() {
99        // Source is Token-2022: resolve metadata pointer
100        resolve_token_2022_source_metadata(
101            unwrapped_mint_info,
102            source_metadata_info,
103            owner_program_info,
104        )?
105    } else if *unwrapped_mint_info.owner == spl_token::id() {
106        // Source is spl-token: read from Metaplex PDA
107        let metaplex_metadata_info =
108            source_metadata_info.ok_or(ProgramError::NotEnoughAccountKeys)?;
109        let (expected_metaplex_pda, _) = MetaplexMetadata::find_pda(unwrapped_mint_info.key);
110        if *metaplex_metadata_info.owner != mpl_token_metadata::ID {
111            return Err(ProgramError::InvalidAccountOwner);
112        }
113        if *metaplex_metadata_info.key != expected_metaplex_pda {
114            return Err(TokenWrapError::MetaplexMetadataMismatch.into());
115        }
116        metaplex_to_token_2022_metadata(unwrapped_mint_info, metaplex_metadata_info)?
117    } else {
118        return Err(ProgramError::IncorrectProgramId);
119    };
120
121    Ok(unwrapped_metadata)
122}