tensor_toolbox/
token_metadata.rs

1#![allow(clippy::result_large_err)]
2
3use anchor_lang::error::ErrorCode;
4use anchor_lang::prelude::*;
5use anchor_spl::{
6    associated_token::AssociatedToken,
7    token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked},
8};
9use mpl_token_metadata::{
10    accounts::{Edition, MasterEdition, Metadata},
11    instructions::{DelegateTransferV1CpiBuilder, TransferV1CpiBuilder},
12    types::{AuthorizationData, Key as MplKey, TokenStandard},
13};
14use tensor_vipers::{throw_err, unwrap_opt};
15
16use crate::TensorError;
17
18pub use mpl_token_metadata::ID;
19
20#[inline(never)]
21pub fn assert_decode_metadata(mint: &Pubkey, metadata: &AccountInfo) -> Result<Metadata> {
22    if *metadata.owner != mpl_token_metadata::ID {
23        throw_err!(TensorError::InvalidProgramOwner);
24    }
25
26    // We must use `safe_deserialize` since there are variations on the metadata struct
27    // which are not compatible with borsh's default deserialization. Using `try_from` will
28    // fail when there are missing fields.
29    let metadata = Metadata::safe_deserialize(&metadata.try_borrow_data()?)
30        .map_err(|_error| TensorError::BadMetadata)?;
31
32    if metadata.mint != *mint {
33        throw_err!(TensorError::BadMetadata);
34    }
35
36    Ok(metadata)
37}
38
39#[inline(never)]
40pub fn assert_decode_master_edition(edition: &AccountInfo) -> Result<MasterEdition> {
41    if *edition.owner != mpl_token_metadata::ID {
42        throw_err!(TensorError::InvalidProgramOwner);
43    }
44
45    let edition = MasterEdition::safe_deserialize(&edition.try_borrow_data()?)
46        .map_err(|_error| TensorError::InvalidEdition)?;
47
48    Ok(edition)
49}
50
51#[inline(never)]
52pub fn assert_decode_edition(edition: &AccountInfo) -> Result<Edition> {
53    if *edition.owner != mpl_token_metadata::ID {
54        throw_err!(TensorError::InvalidProgramOwner);
55    }
56
57    let data = edition.try_borrow_data()?;
58
59    if data.is_empty() || data[0] != MplKey::EditionV1 as u8 {
60        throw_err!(TensorError::InvalidEdition);
61    }
62
63    let edition = Edition::from_bytes(&edition.try_borrow_data()?)
64        .map_err(|_error| TensorError::InvalidEdition)?;
65
66    Ok(edition)
67}
68
69/// Transfer Args using AccountInfo types to be more generic.
70pub struct TransferArgsAi<'a, 'info> {
71    /// Account that will pay for any associated fees.
72    pub payer: &'a AccountInfo<'info>,
73
74    /// Account that will transfer the token.
75    pub source: &'a AccountInfo<'info>,
76
77    /// Associated token account of the source.
78    pub source_ata: &'a AccountInfo<'info>,
79
80    /// Token record of the source.
81    pub source_token_record: Option<&'a AccountInfo<'info>>,
82
83    /// Account that will receive the token.
84    pub destination: &'a AccountInfo<'info>,
85
86    /// Associated token account of the destination.
87    pub destination_ata: &'a AccountInfo<'info>,
88
89    /// Token record of the destination.
90    pub destination_token_record: Option<&'a AccountInfo<'info>>,
91
92    /// Mint of the token.
93    pub mint: &'a AccountInfo<'info>,
94
95    /// Metadata of the token.
96    pub metadata: &'a AccountInfo<'info>,
97
98    /// Edition of the token.
99    pub edition: &'a AccountInfo<'info>,
100
101    /// System program account.
102    pub system_program: &'a AccountInfo<'info>,
103
104    /// SPL Token program account.
105    pub spl_token_program: &'a AccountInfo<'info>,
106
107    /// SPL ATA program account.
108    pub spl_ata_program: &'a AccountInfo<'info>,
109
110    /// Sysvar instructions account.
111    pub sysvar_instructions: Option<&'a AccountInfo<'info>>,
112
113    /// Token Metadata program account.
114    pub token_metadata_program: Option<&'a AccountInfo<'info>>,
115
116    /// Authorization rules program account.
117    pub authorization_rules_program: Option<&'a AccountInfo<'info>>,
118
119    /// Authorization rules account.
120    pub authorization_rules: Option<&'a AccountInfo<'info>>,
121
122    /// Authorization data.
123    pub authorization_data: Option<AuthorizationData>,
124
125    /// Delegate to use in the transfer.
126    ///
127    /// If passed, we assign a delegate first, and the call invoke_signed() instead of invoke().
128    pub delegate: Option<&'a AccountInfo<'info>>,
129}
130
131/// Transfer Args using Anchor types to be more ergonomic.
132pub struct TransferArgs<'a, 'info> {
133    /// Account that will pay for any associated fees.
134    pub payer: &'a AccountInfo<'info>,
135
136    /// Account that will transfer the token.
137    pub source: &'a AccountInfo<'info>,
138
139    /// Associated token account of the source.
140    pub source_ata: &'a InterfaceAccount<'info, TokenAccount>,
141
142    /// Token record of the source.
143    pub source_token_record: Option<&'a UncheckedAccount<'info>>,
144
145    /// Account that will receive the token.
146    pub destination: &'a AccountInfo<'info>,
147
148    /// Associated token account of the destination.
149    pub destination_ata: &'a InterfaceAccount<'info, TokenAccount>,
150
151    /// Token record of the destination.
152    pub destination_token_record: Option<&'a UncheckedAccount<'info>>,
153
154    /// Mint of the token.
155    pub mint: &'a InterfaceAccount<'info, Mint>,
156
157    /// Metadata of the token.
158    pub metadata: &'a UncheckedAccount<'info>,
159
160    /// Edition of the token.
161    pub edition: &'a UncheckedAccount<'info>,
162
163    /// System program account.
164    pub system_program: &'a Program<'info, System>,
165
166    /// SPL Token program account.
167    pub spl_token_program: &'a Interface<'info, TokenInterface>,
168
169    /// SPL ATA program account.
170    pub spl_ata_program: &'a Program<'info, AssociatedToken>,
171
172    /// Sysvar instructions account.
173    pub sysvar_instructions: Option<&'a UncheckedAccount<'info>>,
174
175    /// Token Metadata program account.
176    pub token_metadata_program: Option<&'a UncheckedAccount<'info>>,
177
178    /// Authorization rules program account.
179    pub authorization_rules_program: Option<&'a UncheckedAccount<'info>>,
180
181    /// Authorization rules account.
182    pub authorization_rules: Option<&'a UncheckedAccount<'info>>,
183
184    /// Authorization data.
185    pub authorization_data: Option<AuthorizationData>,
186
187    /// Delegate to use in the transfer.
188    ///
189    /// If passed, we assign a delegate first, and the call invoke_signed() instead of invoke().
190    pub delegate: Option<&'a AccountInfo<'info>>,
191}
192
193fn cpi_transfer_ai(args: TransferArgsAi, signer_seeds: Option<&[&[&[u8]]]>) -> Result<()> {
194    let token_metadata_program =
195        unwrap_opt!(args.token_metadata_program, ErrorCode::AccountNotEnoughKeys);
196    let sysvar_instructions =
197        unwrap_opt!(args.sysvar_instructions, ErrorCode::AccountNotEnoughKeys);
198
199    // prepares the CPI instruction
200    let mut transfer_cpi = TransferV1CpiBuilder::new(token_metadata_program);
201    transfer_cpi
202        .authority(args.source)
203        .token_owner(args.source)
204        .token(args.source_ata.as_ref())
205        .destination_owner(args.destination)
206        .destination_token(args.destination_ata.as_ref())
207        .mint(args.mint.as_ref())
208        .metadata(args.metadata.as_ref())
209        .edition(Some(args.edition))
210        .payer(args.payer)
211        .spl_ata_program(args.spl_ata_program)
212        .spl_token_program(args.spl_token_program)
213        .system_program(args.system_program)
214        .sysvar_instructions(sysvar_instructions)
215        .token_record(args.source_token_record)
216        .destination_token_record(args.destination_token_record)
217        .authorization_rules_program(args.authorization_rules_program)
218        .authorization_rules(args.authorization_rules)
219        .amount(1);
220
221    // set the authorization data if passed in
222    args.authorization_data
223        .clone()
224        .map(|data| transfer_cpi.authorization_data(data));
225
226    // invoke delegate if necessary
227    if let Some(delegate) = args.delegate {
228        // replace authority on the builder with the newly assigned delegate
229        transfer_cpi.authority(delegate);
230
231        let mut delegate_cpi = DelegateTransferV1CpiBuilder::new(token_metadata_program);
232        delegate_cpi
233            .authority(args.source)
234            .delegate(delegate)
235            .token(args.source_ata.as_ref())
236            .mint(args.mint.as_ref())
237            .metadata(args.metadata)
238            .master_edition(Some(args.edition))
239            .payer(args.payer)
240            .spl_token_program(Some(args.spl_token_program))
241            .token_record(args.source_token_record)
242            .authorization_rules(args.authorization_rules)
243            .authorization_rules_program(args.authorization_rules_program)
244            .amount(1);
245
246        args.authorization_data
247            .map(|data| delegate_cpi.authorization_data(data));
248
249        delegate_cpi.invoke()?;
250    }
251
252    if let Some(signer_seeds) = signer_seeds {
253        transfer_cpi.invoke_signed(signer_seeds)?;
254    } else {
255        transfer_cpi.invoke()?;
256    }
257
258    Ok(())
259}
260
261fn cpi_transfer(args: TransferArgs, signer_seeds: Option<&[&[&[u8]]]>) -> Result<()> {
262    let token_metadata_program =
263        unwrap_opt!(args.token_metadata_program, ErrorCode::AccountNotEnoughKeys);
264    let sysvar_instructions =
265        unwrap_opt!(args.sysvar_instructions, ErrorCode::AccountNotEnoughKeys);
266
267    // prepares the CPI instruction
268    let mut transfer_cpi = TransferV1CpiBuilder::new(token_metadata_program);
269    transfer_cpi
270        .authority(args.source)
271        .token_owner(args.source)
272        .token(args.source_ata.as_ref())
273        .destination_owner(args.destination)
274        .destination_token(args.destination_ata.as_ref())
275        .mint(args.mint.as_ref())
276        .metadata(args.metadata.as_ref())
277        .edition(Some(args.edition))
278        .payer(args.payer)
279        .spl_ata_program(args.spl_ata_program)
280        .spl_token_program(args.spl_token_program)
281        .system_program(args.system_program)
282        .sysvar_instructions(sysvar_instructions)
283        .token_record(args.source_token_record.map(|account| account.as_ref()))
284        .destination_token_record(
285            args.destination_token_record
286                .map(|account| account.as_ref()),
287        )
288        .authorization_rules_program(
289            args.authorization_rules_program
290                .map(|account| account.as_ref()),
291        )
292        .authorization_rules(args.authorization_rules.map(|account| account.as_ref()))
293        .amount(1);
294
295    // set the authorization data if passed in
296    args.authorization_data
297        .clone()
298        .map(|data| transfer_cpi.authorization_data(data));
299
300    // invoke delegate if necessary
301    if let Some(delegate) = args.delegate {
302        // replace authority on the builder with the newly assigned delegate
303        transfer_cpi.authority(delegate);
304
305        let mut delegate_cpi = DelegateTransferV1CpiBuilder::new(token_metadata_program);
306        delegate_cpi
307            .authority(args.source)
308            .delegate(delegate)
309            .token(args.source_ata.as_ref())
310            .mint(args.mint.as_ref())
311            .metadata(args.metadata)
312            .master_edition(Some(args.edition))
313            .payer(args.payer)
314            .spl_token_program(Some(args.spl_token_program))
315            .token_record(args.source_token_record.map(|account| account.as_ref()))
316            .authorization_rules(args.authorization_rules.map(|account| account.as_ref()))
317            .authorization_rules_program(
318                args.authorization_rules_program
319                    .map(|account| account.as_ref()),
320            )
321            .amount(1);
322
323        args.authorization_data
324            .map(|data| delegate_cpi.authorization_data(data));
325
326        delegate_cpi.invoke()?;
327    }
328
329    if let Some(signer_seeds) = signer_seeds {
330        transfer_cpi.invoke_signed(signer_seeds)?;
331    } else {
332        transfer_cpi.invoke()?;
333    }
334
335    Ok(())
336}
337
338/// Transfer a NFT or PNFT using AccountInfos rather than Anchor types.
339pub fn transfer_with_ai(
340    args: TransferArgsAi,
341    //if passed, use signed_invoke() instead of invoke()
342    signer_seeds: Option<&[&[&[u8]]]>,
343) -> Result<()> {
344    let metadata = assert_decode_metadata(&args.mint.key(), args.metadata)?;
345
346    if matches!(
347        metadata.token_standard,
348        Some(TokenStandard::ProgrammableNonFungible)
349            | Some(TokenStandard::ProgrammableNonFungibleEdition)
350    ) {
351        // pnft transfer
352        return cpi_transfer_ai(args, signer_seeds);
353    }
354
355    // non-pnft / no token std, normal transfer
356
357    let ctx = CpiContext::new(
358        args.spl_token_program.to_account_info(),
359        TransferChecked {
360            from: args.source_ata.to_account_info(),
361            to: args.destination_ata.to_account_info(),
362            authority: args.source.to_account_info(),
363            mint: args.mint.to_account_info(),
364        },
365    );
366
367    if let Some(signer_seeds) = signer_seeds {
368        token_interface::transfer_checked(ctx.with_signer(signer_seeds), 1, 0)
369    } else {
370        token_interface::transfer_checked(ctx, 1, 0)
371    }
372}
373
374/// Transfer a NFT or PNFT.
375pub fn transfer(
376    args: TransferArgs,
377    //if passed, use signed_invoke() instead of invoke()
378    signer_seeds: Option<&[&[&[u8]]]>,
379) -> Result<()> {
380    let metadata = assert_decode_metadata(&args.mint.key(), args.metadata)?;
381
382    if matches!(
383        metadata.token_standard,
384        Some(TokenStandard::ProgrammableNonFungible)
385            | Some(TokenStandard::ProgrammableNonFungibleEdition)
386    ) {
387        // pnft transfer
388        return cpi_transfer(args, signer_seeds);
389    }
390
391    // non-pnft / no token std, normal transfer
392
393    let ctx = CpiContext::new(
394        args.spl_token_program.to_account_info(),
395        TransferChecked {
396            from: args.source_ata.to_account_info(),
397            to: args.destination_ata.to_account_info(),
398            authority: args.source.to_account_info(),
399            mint: args.mint.to_account_info(),
400        },
401    );
402
403    if let Some(signer_seeds) = signer_seeds {
404        token_interface::transfer_checked(ctx.with_signer(signer_seeds), 1, 0)
405    } else {
406        token_interface::transfer_checked(ctx, 1, 0)
407    }
408}