mpl_token_metadata/assertions/
metadata.rs

1use std::collections::HashMap;
2
3use solana_program::{
4    account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
5    pubkey::Pubkey,
6};
7use spl_token::state::Account;
8
9use crate::{
10    assertions::{assert_initialized, assert_owned_by},
11    error::MetadataError,
12    pda::PREFIX,
13    state::{
14        Creator, Data, Metadata, TokenRecord, TokenState, MAX_CREATOR_LIMIT, MAX_NAME_LENGTH,
15        MAX_SYMBOL_LENGTH, MAX_URI_LENGTH,
16    },
17};
18
19pub fn assert_data_valid(
20    data: &Data,
21    update_authority: &Pubkey,
22    existing_metadata: &Metadata,
23    allow_direct_creator_writes: bool,
24    update_authority_is_signer: bool,
25) -> ProgramResult {
26    if data.name.len() > MAX_NAME_LENGTH {
27        return Err(MetadataError::NameTooLong.into());
28    }
29
30    if data.symbol.len() > MAX_SYMBOL_LENGTH {
31        return Err(MetadataError::SymbolTooLong.into());
32    }
33
34    if data.uri.len() > MAX_URI_LENGTH {
35        return Err(MetadataError::UriTooLong.into());
36    }
37
38    if data.seller_fee_basis_points > 10000 {
39        return Err(MetadataError::InvalidBasisPoints.into());
40    }
41
42    // If the user passes in creators we get a reference to it, otherwise if the user passes in
43    // None we make sure no current creators are verified before returning and allowing them to set
44    // creators field to None.
45    let creators = match data.creators {
46        Some(ref creators) => creators,
47        None => {
48            if let Some(ref existing_creators) = existing_metadata.data.creators {
49                if existing_creators.iter().any(|c| c.verified) {
50                    return Err(MetadataError::CannotRemoveVerifiedCreator.into());
51                }
52            }
53            return Ok(());
54        }
55    };
56
57    if creators.len() > MAX_CREATOR_LIMIT {
58        return Err(MetadataError::CreatorsTooLong.into());
59    }
60
61    if creators.is_empty() {
62        return Err(MetadataError::CreatorsMustBeAtleastOne.into());
63    }
64
65    // Store caller-supplied creator's array into a hashmap for direct lookup.
66    let new_creators_map: HashMap<&Pubkey, &Creator> =
67        creators.iter().map(|c| (&c.address, c)).collect();
68
69    // Do not allow duplicate entries in the creator's array.
70    if new_creators_map.len() != creators.len() {
71        return Err(MetadataError::DuplicateCreatorAddress.into());
72    }
73
74    // If there is an existing creator's array, store this in a hashmap as well.
75    let existing_creators_map: Option<HashMap<&Pubkey, &Creator>> = existing_metadata
76        .data
77        .creators
78        .as_ref()
79        .map(|existing_creators| existing_creators.iter().map(|c| (&c.address, c)).collect());
80
81    // Loop over new creator's map.
82    let mut share_total: u8 = 0;
83    for (address, creator) in &new_creators_map {
84        // Add up creator shares.  After looping through all creators, will
85        // verify it adds up to 100%.
86        share_total = share_total
87            .checked_add(creator.share)
88            .ok_or(MetadataError::NumericalOverflowError)?;
89
90        // If this flag is set we are allowing any and all creators to be marked as verified
91        // without further checking.  This can only be done in special circumstances when the
92        // metadata is fully trusted such as when minting a limited edition.  Note we are still
93        // checking that creator share adds up to 100%.
94        if allow_direct_creator_writes {
95            continue;
96        }
97
98        // If this specific creator (of this loop iteration) is a signer and an update
99        // authority, then we are fine with this creator either setting or clearing its
100        // own `creator.verified` flag.
101        if update_authority_is_signer && **address == *update_authority {
102            continue;
103        }
104
105        // If the previous two conditions are not true then we check the state in the existing
106        // metadata creators array (if it exists) before allowing `creator.verified` to be set.
107        if let Some(existing_creators_map) = &existing_creators_map {
108            if existing_creators_map.contains_key(address) {
109                // If this specific creator (of this loop iteration) is in the existing
110                // creator's array, then it's `creator.verified` flag must match the existing
111                // state.
112                if creator.verified && !existing_creators_map[address].verified {
113                    return Err(MetadataError::CannotVerifyAnotherCreator.into());
114                } else if !creator.verified && existing_creators_map[address].verified {
115                    return Err(MetadataError::CannotUnverifyAnotherCreator.into());
116                }
117            } else if creator.verified {
118                // If this specific creator is not in the existing creator's array, then we
119                // cannot set `creator.verified`.
120                return Err(MetadataError::CannotVerifyAnotherCreator.into());
121            }
122        } else if creator.verified {
123            // If there is no existing creators array, we cannot set `creator.verified`.
124            return Err(MetadataError::CannotVerifyAnotherCreator.into());
125        }
126    }
127
128    // Ensure share total is 100%.
129    if share_total != 100 {
130        return Err(MetadataError::ShareTotalMustBe100.into());
131    }
132
133    // Next make sure there were not any existing creators that were already verified but not
134    // listed in the new creator's array.
135    if allow_direct_creator_writes {
136        return Ok(());
137    } else if let Some(existing_creators_map) = &existing_creators_map {
138        for (address, existing_creator) in existing_creators_map {
139            // If this specific existing creator (of this loop iteration is a signer and an
140            // update authority, then we are fine with this creator clearing its own
141            // `creator.verified` flag.
142            if update_authority_is_signer && **address == *update_authority {
143                continue;
144            } else if !new_creators_map.contains_key(address) && existing_creator.verified {
145                return Err(MetadataError::CannotUnverifyAnotherCreator.into());
146            }
147        }
148    }
149
150    Ok(())
151}
152
153pub fn assert_update_authority_is_correct(
154    metadata: &Metadata,
155    update_authority_info: &AccountInfo,
156) -> ProgramResult {
157    if metadata.update_authority != *update_authority_info.key {
158        return Err(MetadataError::UpdateAuthorityIncorrect.into());
159    }
160
161    if !update_authority_info.is_signer {
162        return Err(MetadataError::UpdateAuthorityIsNotSigner.into());
163    }
164
165    Ok(())
166}
167
168pub fn assert_verified_member_of_collection(
169    item_metadata: &Metadata,
170    collection_metadata: &Metadata,
171) -> ProgramResult {
172    if let Some(ref collection) = item_metadata.collection {
173        if collection_metadata.mint != collection.key {
174            return Err(MetadataError::NotAMemberOfCollection.into());
175        }
176        if !collection.verified {
177            return Err(MetadataError::NotVerifiedMemberOfCollection.into());
178        }
179    } else {
180        return Err(MetadataError::NotAMemberOfCollection.into());
181    }
182
183    Ok(())
184}
185
186pub fn assert_currently_holding(
187    program_id: &Pubkey,
188    owner_info: &AccountInfo,
189    metadata_info: &AccountInfo,
190    metadata: &Metadata,
191    mint_info: &AccountInfo,
192    token_account_info: &AccountInfo,
193) -> ProgramResult {
194    assert_holding_amount(
195        program_id,
196        owner_info,
197        metadata_info,
198        metadata,
199        mint_info,
200        token_account_info,
201        1,
202    )
203}
204
205pub fn assert_holding_amount(
206    program_id: &Pubkey,
207    owner_info: &AccountInfo,
208    metadata_info: &AccountInfo,
209    metadata: &Metadata,
210    mint_info: &AccountInfo,
211    token_account_info: &AccountInfo,
212    amount: u64,
213) -> ProgramResult {
214    assert_owned_by(metadata_info, program_id)?;
215    assert_owned_by(mint_info, &spl_token::ID)?;
216
217    let token_account: Account = assert_initialized(token_account_info)?;
218
219    assert_owned_by(token_account_info, &spl_token::ID)?;
220
221    if token_account.owner != *owner_info.key {
222        return Err(MetadataError::InvalidOwner.into());
223    }
224
225    if token_account.mint != *mint_info.key {
226        return Err(MetadataError::MintMismatch.into());
227    }
228
229    if token_account.amount < amount {
230        return Err(MetadataError::InsufficientTokenBalance.into());
231    }
232
233    if token_account.mint != metadata.mint {
234        return Err(MetadataError::MintMismatch.into());
235    }
236    Ok(())
237}
238
239pub fn assert_metadata_valid(
240    program_id: &Pubkey,
241    mint_pubkey: &Pubkey,
242    metadata_account_info: &AccountInfo,
243) -> ProgramResult {
244    let seeds = &[PREFIX.as_bytes(), program_id.as_ref(), mint_pubkey.as_ref()];
245    let (metadata_pubkey, _) = Pubkey::find_program_address(seeds, program_id);
246    if metadata_pubkey != *metadata_account_info.key {
247        return Err(MetadataError::InvalidMetadataKey.into());
248    }
249
250    Ok(())
251}
252
253pub fn assert_state(token_record: &TokenRecord, state: TokenState) -> ProgramResult {
254    match state {
255        TokenState::Locked => {
256            if !token_record.is_locked() {
257                return Err(MetadataError::UnlockedToken.into());
258            }
259        }
260        TokenState::Unlocked => {
261            if token_record.is_locked() {
262                return Err(MetadataError::LockedToken.into());
263            }
264        }
265        TokenState::Listed => {
266            if !matches!(token_record.state, TokenState::Listed) {
267                return Err(MetadataError::IncorrectTokenState.into());
268            }
269        }
270    }
271
272    Ok(())
273}
274
275pub fn assert_not_locked(token_record: &TokenRecord) -> ProgramResult {
276    assert_state(token_record, TokenState::Unlocked)
277}
278
279pub fn assert_metadata_derivation(
280    program_id: &Pubkey,
281    metadata_info: &AccountInfo,
282    mint_info: &AccountInfo,
283) -> Result<u8, ProgramError> {
284    let path = &[
285        PREFIX.as_bytes(),
286        program_id.as_ref(),
287        mint_info.key.as_ref(),
288    ];
289    let (pubkey, bump) = Pubkey::find_program_address(path, program_id);
290    if pubkey != *metadata_info.key {
291        return Err(MetadataError::MintMismatch.into());
292    }
293    Ok(bump)
294}