mpl_candy_machine_core/
utils.rs

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/// Anchor wrapper for Token program.
30#[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/// Anchor wrapper for Associated Token program.
40#[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    /// CHECK: account checked in CPI
51    pub payer: AccountInfo<'info>,
52    /// CHECK: account checked in CPI
53    pub authority_pda: AccountInfo<'info>,
54    /// CHECK: account checked in CPI
55    pub collection_update_authority: AccountInfo<'info>,
56    /// CHECK: account checked in CPI
57    pub collection_mint: AccountInfo<'info>,
58    /// CHECK: account checked in CPI
59    pub collection_metadata: AccountInfo<'info>,
60    /// CHECK: account checked in CPI
61    pub collection_authority_record: AccountInfo<'info>,
62    /// CHECK: account checked in CPI
63    pub token_metadata_program: AccountInfo<'info>,
64    /// CHECK: account checked in CPI
65    pub system_program: AccountInfo<'info>,
66}
67
68pub struct RevokeCollectionAuthorityHelperAccounts<'info> {
69    /// CHECK: account checked in CPI
70    pub authority_pda: AccountInfo<'info>,
71    /// CHECK: account checked in CPI
72    pub collection_mint: AccountInfo<'info>,
73    /// CHECK: account checked in CPI
74    pub collection_metadata: AccountInfo<'info>,
75    /// CHECK: account checked in CPI
76    pub collection_authority_record: AccountInfo<'info>,
77    /// CHECK: account checked in CPI
78    pub token_metadata_program: AccountInfo<'info>,
79}
80
81pub struct ApproveMetadataDelegateHelperAccounts<'info> {
82    /// CHECK: account checked in CPI
83    pub delegate_record: AccountInfo<'info>,
84    /// CHECK: account checked in CPI
85    pub authority_pda: AccountInfo<'info>,
86    /// CHECK: account checked in CPI
87    pub collection_metadata: AccountInfo<'info>,
88    /// CHECK: account checked in CPI
89    pub collection_mint: AccountInfo<'info>,
90    /// CHECK: account checked in CPI
91    pub collection_update_authority: AccountInfo<'info>,
92    /// CHECK: account checked in CPI
93    pub payer: AccountInfo<'info>,
94    /// CHECK: account checked in CPI
95    pub system_program: AccountInfo<'info>,
96    /// CHECK: account checked in CPI
97    pub sysvar_instructions: AccountInfo<'info>,
98    /// CHECK: account checked in CPI
99    pub authorization_rules_program: Option<AccountInfo<'info>>,
100    /// CHECK: account checked in CPI
101    pub authorization_rules: Option<AccountInfo<'info>>,
102}
103
104pub struct RevokeMetadataDelegateHelperAccounts<'info> {
105    /// CHECK: account checked in CPI
106    pub delegate_record: AccountInfo<'info>,
107    /// CHECK: account checked in CPI
108    pub authority_pda: AccountInfo<'info>,
109    /// CHECK: account checked in CPI
110    pub collection_metadata: AccountInfo<'info>,
111    /// CHECK: account checked in CPI
112    pub collection_mint: AccountInfo<'info>,
113    /// CHECK: account checked in CPI
114    pub collection_update_authority: AccountInfo<'info>,
115    /// CHECK: account checked in CPI
116    pub payer: AccountInfo<'info>,
117    /// CHECK: account checked in CPI
118    pub system_program: AccountInfo<'info>,
119    /// CHECK: account checked in CPI
120    pub sysvar_instructions: AccountInfo<'info>,
121    /// CHECK: account checked in CPI
122    pub authorization_rules_program: Option<AccountInfo<'info>>,
123    /// CHECK: account checked in CPI
124    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
136/// Return the current number of lines written to the account.
137pub 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
145/// Return a padded string up to the specified length. If the specified
146/// string `value` is longer than the allowed `length`, return an error.
147pub fn fixed_length_string(value: String, length: usize) -> Result<String> {
148    if length < value.len() {
149        // the value is larger than the allowed length
150        return err!(CandyError::ExceededLengthError);
151    }
152
153    let padding = NULL_STRING.repeat(length - value.len());
154    Ok(value + &padding)
155}
156
157/// Replace the index pattern variables on the specified string.
158pub fn replace_patterns(value: String, index: usize) -> String {
159    let mut mutable = value;
160    // check for pattern $ID+1$
161    if mutable.contains(REPLACEMENT_INDEX_INCREMENT) {
162        mutable = mutable.replace(REPLACEMENT_INDEX_INCREMENT, &(index + 1).to_string());
163    }
164    // check for pattern $ID$
165    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        // pNFTs do not have a "legacy" collection authority, so we do not try to revoke
255        // it. This would happen when the migration is completed and the candy machine
256        // account version is still V1 - in any case, the "legacy" collection authority
257        // is invalid since it does not apply to pNFTs and it will be replace by a
258        // metadata delegate.
259        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}