1use anchor_lang::prelude::*;
2use arrayref::array_ref;
3use mpl_token_metadata::{
4 error::MetadataError,
5 instruction::{
6 approve_collection_authority,
7 builders::{DelegateBuilder, RevokeBuilder},
8 revoke_collection_authority, DelegateArgs, InstructionBuilder, RevokeArgs,
9 },
10 state::{Metadata, TokenMetadataAccount, TokenStandard, EDITION, PREFIX},
11 utils::assert_derivation,
12};
13use solana_program::{
14 account_info::AccountInfo,
15 program::{invoke, invoke_signed},
16 program_memory::sol_memcmp,
17 program_pack::{IsInitialized, Pack},
18 pubkey::{Pubkey, PUBKEY_BYTES},
19};
20use std::result::Result as StdResult;
21
22use crate::{
23 constants::{
24 AUTHORITY_SEED, HIDDEN_SECTION, NULL_STRING, REPLACEMENT_INDEX, REPLACEMENT_INDEX_INCREMENT,
25 },
26 CandyError,
27};
28
29#[derive(Debug, Clone)]
31pub struct Token;
32
33impl anchor_lang::Id for Token {
34 fn id() -> Pubkey {
35 spl_token::id()
36 }
37}
38
39#[derive(Debug, Clone)]
41pub struct AssociatedToken;
42
43impl anchor_lang::Id for AssociatedToken {
44 fn id() -> Pubkey {
45 spl_associated_token_account::id()
46 }
47}
48
49pub struct ApproveCollectionAuthorityHelperAccounts<'info> {
50 pub payer: AccountInfo<'info>,
52 pub authority_pda: AccountInfo<'info>,
54 pub collection_update_authority: AccountInfo<'info>,
56 pub collection_mint: AccountInfo<'info>,
58 pub collection_metadata: AccountInfo<'info>,
60 pub collection_authority_record: AccountInfo<'info>,
62 pub token_metadata_program: AccountInfo<'info>,
64 pub system_program: AccountInfo<'info>,
66}
67
68pub struct RevokeCollectionAuthorityHelperAccounts<'info> {
69 pub authority_pda: AccountInfo<'info>,
71 pub collection_mint: AccountInfo<'info>,
73 pub collection_metadata: AccountInfo<'info>,
75 pub collection_authority_record: AccountInfo<'info>,
77 pub token_metadata_program: AccountInfo<'info>,
79}
80
81pub struct ApproveMetadataDelegateHelperAccounts<'info> {
82 pub delegate_record: AccountInfo<'info>,
84 pub authority_pda: AccountInfo<'info>,
86 pub collection_metadata: AccountInfo<'info>,
88 pub collection_mint: AccountInfo<'info>,
90 pub collection_update_authority: AccountInfo<'info>,
92 pub payer: AccountInfo<'info>,
94 pub system_program: AccountInfo<'info>,
96 pub sysvar_instructions: AccountInfo<'info>,
98 pub authorization_rules_program: Option<AccountInfo<'info>>,
100 pub authorization_rules: Option<AccountInfo<'info>>,
102}
103
104pub struct RevokeMetadataDelegateHelperAccounts<'info> {
105 pub delegate_record: AccountInfo<'info>,
107 pub authority_pda: AccountInfo<'info>,
109 pub collection_metadata: AccountInfo<'info>,
111 pub collection_mint: AccountInfo<'info>,
113 pub collection_update_authority: AccountInfo<'info>,
115 pub payer: AccountInfo<'info>,
117 pub system_program: AccountInfo<'info>,
119 pub sysvar_instructions: AccountInfo<'info>,
121 pub authorization_rules_program: Option<AccountInfo<'info>>,
123 pub authorization_rules: Option<AccountInfo<'info>>,
125}
126
127pub fn assert_initialized<T: Pack + IsInitialized>(account_info: &AccountInfo) -> Result<T> {
128 let account: T = T::unpack_unchecked(&account_info.data.borrow())?;
129 if !account.is_initialized() {
130 Err(CandyError::Uninitialized.into())
131 } else {
132 Ok(account)
133 }
134}
135
136pub fn get_config_count(data: &[u8]) -> Result<usize> {
138 Ok(u32::from_le_bytes(*array_ref![data, HIDDEN_SECTION, 4]) as usize)
139}
140
141pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool {
142 sol_memcmp(a.as_ref(), b.as_ref(), PUBKEY_BYTES) == 0
143}
144
145pub fn fixed_length_string(value: String, length: usize) -> Result<String> {
148 if length < value.len() {
149 return err!(CandyError::ExceededLengthError);
151 }
152
153 let padding = NULL_STRING.repeat(length - value.len());
154 Ok(value + &padding)
155}
156
157pub fn replace_patterns(value: String, index: usize) -> String {
159 let mut mutable = value;
160 if mutable.contains(REPLACEMENT_INDEX_INCREMENT) {
162 mutable = mutable.replace(REPLACEMENT_INDEX_INCREMENT, &(index + 1).to_string());
163 }
164 if mutable.contains(REPLACEMENT_INDEX) {
166 mutable = mutable.replace(REPLACEMENT_INDEX, &index.to_string());
167 }
168
169 mutable
170}
171
172pub fn assert_edition_from_mint(
173 edition_account: &AccountInfo,
174 mint_account: &AccountInfo,
175) -> StdResult<(), ProgramError> {
176 assert_derivation(
177 &mpl_token_metadata::id(),
178 edition_account,
179 &[
180 PREFIX.as_bytes(),
181 mpl_token_metadata::id().as_ref(),
182 mint_account.key().as_ref(),
183 EDITION.as_bytes(),
184 ],
185 )
186 .map_err(|_| MetadataError::CollectionMasterEditionAccountInvalid)?;
187 Ok(())
188}
189
190pub fn approve_collection_authority_helper(
191 accounts: ApproveCollectionAuthorityHelperAccounts,
192) -> Result<()> {
193 let ApproveCollectionAuthorityHelperAccounts {
194 payer,
195 authority_pda,
196 collection_update_authority,
197 collection_mint,
198 collection_metadata,
199 collection_authority_record,
200 token_metadata_program,
201 system_program,
202 } = accounts;
203
204 let collection_data: Metadata = Metadata::from_account_info(&collection_metadata)?;
205
206 if !cmp_pubkeys(
207 &collection_data.update_authority,
208 &collection_update_authority.key(),
209 ) {
210 return err!(CandyError::IncorrectCollectionAuthority);
211 }
212
213 if !cmp_pubkeys(&collection_data.mint, &collection_mint.key()) {
214 return err!(CandyError::MintMismatch);
215 }
216
217 let approve_collection_authority_ix = approve_collection_authority(
218 token_metadata_program.key(),
219 collection_authority_record.key(),
220 authority_pda.key(),
221 collection_update_authority.key(),
222 payer.key(),
223 collection_metadata.key(),
224 collection_mint.key(),
225 );
226
227 if collection_authority_record.data_is_empty() {
228 let approve_collection_infos = vec![
229 collection_authority_record,
230 authority_pda,
231 collection_update_authority,
232 payer,
233 collection_metadata,
234 collection_mint,
235 system_program,
236 ];
237
238 invoke(
239 &approve_collection_authority_ix,
240 approve_collection_infos.as_slice(),
241 )?;
242 }
243
244 Ok(())
245}
246
247pub fn revoke_collection_authority_helper(
248 accounts: RevokeCollectionAuthorityHelperAccounts,
249 candy_machine: Pubkey,
250 signer_bump: u8,
251 token_standard: Option<TokenStandard>,
252) -> Result<()> {
253 if matches!(token_standard, Some(TokenStandard::ProgrammableNonFungible)) {
254 Ok(())
260 } else {
261 let revoke_collection_infos = vec![
262 accounts.collection_authority_record.to_account_info(),
263 accounts.authority_pda.to_account_info(),
264 accounts.collection_metadata.to_account_info(),
265 accounts.collection_mint.to_account_info(),
266 ];
267
268 let authority_seeds = [
269 AUTHORITY_SEED.as_bytes(),
270 candy_machine.as_ref(),
271 &[signer_bump],
272 ];
273
274 invoke_signed(
275 &revoke_collection_authority(
276 accounts.token_metadata_program.key(),
277 accounts.collection_authority_record.key(),
278 accounts.authority_pda.key(),
279 accounts.authority_pda.key(),
280 accounts.collection_metadata.key(),
281 accounts.collection_mint.key(),
282 ),
283 revoke_collection_infos.as_slice(),
284 &[&authority_seeds],
285 )
286 .map_err(|error| error.into())
287 }
288}
289
290pub fn approve_metadata_delegate(accounts: ApproveMetadataDelegateHelperAccounts) -> Result<()> {
291 let mut delegate_builder = DelegateBuilder::new();
292 delegate_builder
293 .delegate_record(accounts.delegate_record.key())
294 .delegate(accounts.authority_pda.key())
295 .mint(accounts.collection_mint.key())
296 .metadata(accounts.collection_metadata.key())
297 .payer(accounts.payer.key())
298 .authority(accounts.collection_update_authority.key());
299
300 let mut delegate_infos = vec![
301 accounts.delegate_record.to_account_info(),
302 accounts.authority_pda.to_account_info(),
303 accounts.collection_metadata.to_account_info(),
304 accounts.collection_mint.to_account_info(),
305 accounts.collection_update_authority.to_account_info(),
306 accounts.payer.to_account_info(),
307 accounts.system_program.to_account_info(),
308 accounts.sysvar_instructions.to_account_info(),
309 ];
310
311 if let Some(authorization_rules_program) = &accounts.authorization_rules_program {
312 delegate_builder.authorization_rules_program(authorization_rules_program.key());
313 delegate_infos.push(authorization_rules_program.to_account_info());
314 }
315
316 if let Some(authorization_rules) = &accounts.authorization_rules {
317 delegate_builder.authorization_rules(authorization_rules.key());
318 delegate_infos.push(authorization_rules.to_account_info());
319 }
320
321 let delegate_ix = delegate_builder
322 .build(DelegateArgs::CollectionV1 {
323 authorization_data: None,
324 })
325 .map_err(|_| CandyError::InstructionBuilderFailed)?
326 .instruction();
327
328 invoke(&delegate_ix, &delegate_infos).map_err(|error| error.into())
329}
330
331pub fn revoke_metadata_delegate(
332 accounts: RevokeMetadataDelegateHelperAccounts,
333 candy_machine: Pubkey,
334 signer_bump: u8,
335) -> Result<()> {
336 let mut revoke_builder = RevokeBuilder::new();
337 revoke_builder
338 .delegate_record(accounts.delegate_record.key())
339 .delegate(accounts.authority_pda.key())
340 .mint(accounts.collection_mint.key())
341 .metadata(accounts.collection_metadata.key())
342 .payer(accounts.payer.key())
343 .authority(accounts.authority_pda.key());
344
345 let mut revoke_infos = vec![
346 accounts.delegate_record.to_account_info(),
347 accounts.authority_pda.to_account_info(),
348 accounts.collection_metadata.to_account_info(),
349 accounts.collection_mint.to_account_info(),
350 accounts.collection_update_authority.to_account_info(),
351 accounts.payer.to_account_info(),
352 accounts.system_program.to_account_info(),
353 accounts.sysvar_instructions.to_account_info(),
354 ];
355
356 if let Some(authorization_rules_program) = &accounts.authorization_rules_program {
357 revoke_builder.authorization_rules_program(authorization_rules_program.key());
358 revoke_infos.push(authorization_rules_program.to_account_info());
359 }
360
361 if let Some(authorization_rules) = &accounts.authorization_rules {
362 revoke_builder.authorization_rules(authorization_rules.key());
363 revoke_infos.push(authorization_rules.to_account_info());
364 }
365
366 let revoke_ix = revoke_builder
367 .build(RevokeArgs::CollectionV1)
368 .map_err(|_| CandyError::InstructionBuilderFailed)?
369 .instruction();
370
371 let authority_seeds = [
372 AUTHORITY_SEED.as_bytes(),
373 candy_machine.as_ref(),
374 &[signer_bump],
375 ];
376
377 invoke_signed(&revoke_ix, &revoke_infos, &[&authority_seeds]).map_err(|error| error.into())
378}
379
380pub fn assert_token_standard(token_standard: u8) -> Result<()> {
381 if token_standard == TokenStandard::NonFungible as u8
382 || token_standard == TokenStandard::ProgrammableNonFungible as u8
383 {
384 Ok(())
385 } else {
386 err!(CandyError::InvalidTokenStandard)
387 }
388}
389
390#[cfg(test)]
391pub mod tests {
392 use super::*;
393
394 #[test]
395 fn check_keys_equal() {
396 let key1 = Pubkey::new_unique();
397 assert!(cmp_pubkeys(&key1, &key1));
398 }
399
400 #[test]
401 fn check_keys_not_equal() {
402 let key1 = Pubkey::new_unique();
403 let key2 = Pubkey::new_unique();
404 assert!(!cmp_pubkeys(&key1, &key2));
405 }
406}