mpl_token_metadata/assertions/
metadata.rs1use 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 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 let new_creators_map: HashMap<&Pubkey, &Creator> =
67 creators.iter().map(|c| (&c.address, c)).collect();
68
69 if new_creators_map.len() != creators.len() {
71 return Err(MetadataError::DuplicateCreatorAddress.into());
72 }
73
74 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 let mut share_total: u8 = 0;
83 for (address, creator) in &new_creators_map {
84 share_total = share_total
87 .checked_add(creator.share)
88 .ok_or(MetadataError::NumericalOverflowError)?;
89
90 if allow_direct_creator_writes {
95 continue;
96 }
97
98 if update_authority_is_signer && **address == *update_authority {
102 continue;
103 }
104
105 if let Some(existing_creators_map) = &existing_creators_map {
108 if existing_creators_map.contains_key(address) {
109 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 return Err(MetadataError::CannotVerifyAnotherCreator.into());
121 }
122 } else if creator.verified {
123 return Err(MetadataError::CannotVerifyAnotherCreator.into());
125 }
126 }
127
128 if share_total != 100 {
130 return Err(MetadataError::ShareTotalMustBe100.into());
131 }
132
133 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 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}