mpl_token_metadata/utils/
programmable_asset.rs

1use mpl_token_auth_rules::{
2    instruction::{builders::ValidateBuilder, InstructionBuilder, ValidateArgs},
3    payload::PayloadType,
4};
5use mpl_utils::{create_or_allocate_account_raw, token::TokenTransferParams};
6use solana_program::{
7    account_info::AccountInfo, entrypoint::ProgramResult, program::invoke_signed,
8    program_error::ProgramError, program_option::COption, pubkey::Pubkey,
9};
10use spl_token::{
11    instruction::{freeze_account, thaw_account, AuthorityType as SplAuthorityType},
12    state::Account,
13};
14
15use crate::{
16    assertions::{assert_derivation, programmable::assert_valid_authorization},
17    edition_seeds,
18    error::MetadataError,
19    pda::{EDITION, PREFIX},
20    processor::{AuthorizationData, TransferScenario},
21    state::{
22        Operation, PayloadKey, ProgrammableConfig, Resizable, ToAccountMeta, TokenMetadataAccount,
23        TokenRecord, TOKEN_RECORD_SEED,
24    },
25};
26
27pub fn create_token_record_account<'a>(
28    program_id: &Pubkey,
29    token_record_info: &'a AccountInfo<'a>,
30    mint_info: &'a AccountInfo<'a>,
31    token_info: &'a AccountInfo<'a>,
32    payer_info: &'a AccountInfo<'a>,
33    system_program_info: &'a AccountInfo<'a>,
34) -> ProgramResult {
35    if !token_record_info.data_is_empty() {
36        return Err(MetadataError::DelegateAlreadyExists.into());
37    }
38
39    let mut signer_seeds = Vec::from([
40        PREFIX.as_bytes(),
41        crate::ID.as_ref(),
42        mint_info.key.as_ref(),
43        TOKEN_RECORD_SEED.as_bytes(),
44        token_info.key.as_ref(),
45    ]);
46
47    let bump = &[assert_derivation(
48        program_id,
49        token_record_info,
50        &signer_seeds,
51    )?];
52    signer_seeds.push(bump);
53
54    // allocate the delegate account
55
56    create_or_allocate_account_raw(
57        *program_id,
58        token_record_info,
59        system_program_info,
60        payer_info,
61        TokenRecord::size(),
62        &signer_seeds,
63    )?;
64
65    let token_record = TokenRecord {
66        bump: bump[0],
67        ..Default::default()
68    };
69
70    token_record.save(token_record_info, payer_info, system_program_info)
71}
72
73pub fn freeze<'a>(
74    mint: AccountInfo<'a>,
75    token: AccountInfo<'a>,
76    edition: AccountInfo<'a>,
77    spl_token_program: AccountInfo<'a>,
78) -> ProgramResult {
79    let edition_info_path = Vec::from([
80        PREFIX.as_bytes(),
81        crate::ID.as_ref(),
82        mint.key.as_ref(),
83        EDITION.as_bytes(),
84    ]);
85    let edition_info_path_bump_seed =
86        &[assert_derivation(&crate::ID, &edition, &edition_info_path)?];
87    let mut edition_info_seeds = edition_info_path.clone();
88    edition_info_seeds.push(edition_info_path_bump_seed);
89
90    invoke_signed(
91        &freeze_account(spl_token_program.key, token.key, mint.key, edition.key, &[]).unwrap(),
92        &[token, mint, edition],
93        &[&edition_info_seeds],
94    )?;
95    Ok(())
96}
97
98pub fn thaw<'a>(
99    mint_info: AccountInfo<'a>,
100    token_info: AccountInfo<'a>,
101    edition_info: AccountInfo<'a>,
102    spl_token_program: AccountInfo<'a>,
103) -> ProgramResult {
104    let edition_info_path = Vec::from([
105        PREFIX.as_bytes(),
106        crate::ID.as_ref(),
107        mint_info.key.as_ref(),
108        EDITION.as_bytes(),
109    ]);
110    let edition_info_path_bump_seed = &[assert_derivation(
111        &crate::ID,
112        &edition_info,
113        &edition_info_path,
114    )?];
115    let mut edition_info_seeds = edition_info_path.clone();
116    edition_info_seeds.push(edition_info_path_bump_seed);
117
118    invoke_signed(
119        &thaw_account(
120            spl_token_program.key,
121            token_info.key,
122            mint_info.key,
123            edition_info.key,
124            &[],
125        )
126        .unwrap(),
127        &[token_info, mint_info, edition_info],
128        &[&edition_info_seeds],
129    )?;
130    Ok(())
131}
132
133pub fn validate<'a>(
134    ruleset: &'a AccountInfo<'a>,
135    operation: Operation,
136    mint_info: &'a AccountInfo<'a>,
137    additional_rule_accounts: Vec<&'a AccountInfo<'a>>,
138    auth_data: &AuthorizationData,
139    rule_set_revision: Option<usize>,
140) -> Result<(), ProgramError> {
141    let account_metas = additional_rule_accounts
142        .iter()
143        .map(|account| account.to_account_meta())
144        .collect();
145
146    let validate_ix = ValidateBuilder::new()
147        .rule_set_pda(*ruleset.key)
148        .mint(*mint_info.key)
149        .additional_rule_accounts(account_metas)
150        .build(ValidateArgs::V1 {
151            operation: operation.to_string(),
152            payload: auth_data.payload.clone(),
153            update_rule_state: false,
154            rule_set_revision,
155        })
156        .map_err(|_error| MetadataError::InvalidAuthorizationRules)?
157        .instruction();
158
159    let mut account_infos = vec![ruleset.clone(), mint_info.clone()];
160    account_infos.extend(additional_rule_accounts.into_iter().cloned());
161    invoke_signed(&validate_ix, account_infos.as_slice(), &[])
162}
163
164#[derive(Debug, Clone)]
165pub struct AuthRulesValidateParams<'a> {
166    pub mint_info: &'a AccountInfo<'a>,
167    pub source_info: Option<&'a AccountInfo<'a>>,
168    pub destination_info: Option<&'a AccountInfo<'a>>,
169    pub authority_info: Option<&'a AccountInfo<'a>>,
170    pub owner_info: Option<&'a AccountInfo<'a>>,
171    pub programmable_config: Option<ProgrammableConfig>,
172    pub amount: u64,
173    pub auth_data: Option<AuthorizationData>,
174    pub auth_rules_info: Option<&'a AccountInfo<'a>>,
175    pub operation: Operation,
176    pub is_wallet_to_wallet: bool,
177    pub rule_set_revision: Option<usize>,
178}
179
180pub fn auth_rules_validate(params: AuthRulesValidateParams) -> ProgramResult {
181    let AuthRulesValidateParams {
182        mint_info,
183        owner_info,
184        source_info,
185        destination_info,
186        authority_info,
187        programmable_config,
188        amount,
189        auth_data,
190        auth_rules_info,
191        operation,
192        is_wallet_to_wallet,
193        rule_set_revision,
194    } = params;
195
196    if is_wallet_to_wallet {
197        return Ok(());
198    }
199
200    if let Operation::Transfer { scenario } = &operation {
201        // Migration delegate is allowed to skip auth rules to guarantee that
202        // it can transfer the asset.
203        if matches!(scenario, TransferScenario::MigrationDelegate) {
204            return Ok(());
205        }
206    }
207
208    if let Some(ref config) = programmable_config {
209        if let ProgrammableConfig::V1 { rule_set: Some(_) } = config {
210            assert_valid_authorization(auth_rules_info, config)?;
211
212            // We can safely unwrap here because they were all checked for existence
213            // in the assertion above.
214            let auth_pda = auth_rules_info.unwrap();
215
216            let mut auth_data = if let Some(auth_data) = auth_data {
217                auth_data
218            } else {
219                AuthorizationData::new_empty()
220            };
221
222            let mut additional_rule_accounts = vec![];
223            if let Some(source_info) = source_info {
224                additional_rule_accounts.push(source_info);
225            }
226            if let Some(destination_info) = destination_info {
227                additional_rule_accounts.push(destination_info);
228            }
229            if let Some(authority_info) = authority_info {
230                additional_rule_accounts.push(authority_info);
231            }
232            if let Some(owner_info) = owner_info {
233                additional_rule_accounts.push(owner_info);
234            }
235
236            // Insert auth rules for the operation type.
237            match operation {
238                Operation::Transfer { scenario: _ } => {
239                    // Get account infos
240                    let authority_info = authority_info.ok_or(MetadataError::InvalidOperation)?;
241                    let source_info = source_info.ok_or(MetadataError::InvalidOperation)?;
242                    let destination_info =
243                        destination_info.ok_or(MetadataError::InvalidOperation)?;
244
245                    // Transfer Amount
246                    auth_data
247                        .payload
248                        .insert(PayloadKey::Amount.to_string(), PayloadType::Number(amount));
249
250                    // Transfer Authority
251                    auth_data.payload.insert(
252                        PayloadKey::Authority.to_string(),
253                        PayloadType::Pubkey(*authority_info.key),
254                    );
255
256                    // Transfer Source
257                    auth_data.payload.insert(
258                        PayloadKey::Source.to_string(),
259                        PayloadType::Pubkey(*source_info.key),
260                    );
261
262                    // Transfer Destination
263                    auth_data.payload.insert(
264                        PayloadKey::Destination.to_string(),
265                        PayloadType::Pubkey(*destination_info.key),
266                    );
267                }
268                Operation::Delegate { scenario: _ } => {
269                    // get account infos
270                    let destination_info =
271                        destination_info.ok_or(MetadataError::InvalidOperation)?;
272
273                    // delegate amount
274                    auth_data
275                        .payload
276                        .insert(PayloadKey::Amount.to_string(), PayloadType::Number(amount));
277
278                    // delegate authority
279                    auth_data.payload.insert(
280                        PayloadKey::Delegate.to_string(),
281                        PayloadType::Pubkey(*destination_info.key),
282                    );
283                }
284                _ => {
285                    return Err(MetadataError::InvalidOperation.into());
286                }
287            }
288
289            validate(
290                auth_pda,
291                operation,
292                mint_info,
293                additional_rule_accounts,
294                &auth_data,
295                rule_set_revision,
296            )?;
297        }
298    }
299    Ok(())
300}
301
302pub fn frozen_transfer<'a>(
303    params: TokenTransferParams<'a, '_>,
304    edition_opt_info: Option<&'a AccountInfo<'a>>,
305) -> ProgramResult {
306    if edition_opt_info.is_none() {
307        return Err(MetadataError::MissingEditionAccount.into());
308    }
309    let master_edition_info = edition_opt_info.unwrap();
310
311    thaw(
312        params.mint.clone(),
313        params.source.clone(),
314        master_edition_info.clone(),
315        params.token_program.clone(),
316    )?;
317
318    let mint_info = params.mint.clone();
319    let dest_info = params.destination.clone();
320    let token_program_info = params.token_program.clone();
321
322    mpl_utils::token::spl_token_transfer(params).unwrap();
323
324    freeze(
325        mint_info,
326        dest_info.clone(),
327        master_edition_info.clone(),
328        token_program_info.clone(),
329    )?;
330
331    Ok(())
332}
333
334pub(crate) struct ClearCloseAuthorityParams<'a> {
335    pub token: Account,
336    pub mint_info: &'a AccountInfo<'a>,
337    pub token_info: &'a AccountInfo<'a>,
338    pub master_edition_info: &'a AccountInfo<'a>,
339    pub authority_info: &'a AccountInfo<'a>,
340    pub spl_token_program_info: &'a AccountInfo<'a>,
341}
342
343pub(crate) fn clear_close_authority(params: ClearCloseAuthorityParams) -> ProgramResult {
344    let ClearCloseAuthorityParams {
345        token,
346        mint_info,
347        token_info,
348        master_edition_info,
349        authority_info,
350        spl_token_program_info,
351    } = params;
352
353    // If there's an existing close authority that is not the metadata account,
354    // it will need to be revoked by the original UtilityDelegate.
355    if let COption::Some(close_authority) = token.close_authority {
356        if &close_authority != master_edition_info.key {
357            return Err(MetadataError::InvalidCloseAuthority.into());
358        }
359        let seeds = edition_seeds!(mint_info.key);
360
361        invoke_signed(
362            &spl_token::instruction::set_authority(
363                spl_token_program_info.key,
364                token_info.key,
365                None,
366                SplAuthorityType::CloseAccount,
367                authority_info.key,
368                &[],
369            )?,
370            &[token_info.clone(), authority_info.clone()],
371            &[seeds.as_slice()],
372        )?;
373    }
374
375    Ok(())
376}