Skip to main content

name_tokenizer/processor/
create_nft.rs

1//! Tokenize a domain name
2
3use mpl_token_metadata::{
4    accounts::{MasterEdition, Metadata},
5    instructions::{
6        CreateMetadataAccountV3Cpi, CreateMetadataAccountV3CpiAccounts,
7        CreateMetadataAccountV3InstructionArgs, SetAndVerifyCollectionCpi,
8        SetAndVerifyCollectionCpiAccounts, UnverifyCollectionCpi, UnverifyCollectionCpiAccounts,
9        UpdateMetadataAccountV2Cpi, UpdateMetadataAccountV2CpiAccounts,
10        UpdateMetadataAccountV2InstructionArgs,
11    },
12};
13
14use crate::{
15    cpi::Cpi,
16    state::{
17        NftRecord, Tag, COLLECTION_PREFIX, CREATOR_FEE, METADATA_SIGNER, META_SYMBOL, MINT_PREFIX,
18        SELLER_BASIS,
19    },
20    utils::check_name,
21};
22
23use {
24    bonfida_utils::{
25        checks::{check_account_key, check_account_owner, check_signer},
26        BorshSize, InstructionsAccount,
27    },
28    borsh::{BorshDeserialize, BorshSerialize},
29    mpl_token_metadata::types::{Creator, DataV2},
30    solana_program::{
31        account_info::{next_account_info, AccountInfo},
32        entrypoint::ProgramResult,
33        msg,
34        program::{invoke, invoke_signed},
35        program_error::ProgramError,
36        program_pack::Pack,
37        pubkey::Pubkey,
38        system_program, sysvar,
39    },
40    spl_name_service::instruction::transfer,
41    spl_token::{instruction::mint_to, state::Mint},
42};
43
44#[derive(BorshDeserialize, BorshSerialize, BorshSize)]
45pub struct Params {
46    /// The domain name (without .sol)
47    pub name: String,
48
49    /// The URI of the metadata
50    pub uri: String,
51}
52
53#[derive(InstructionsAccount)]
54pub struct Accounts<'a, T> {
55    /// The mint of the NFT
56    #[cons(writable)]
57    pub mint: &'a T,
58
59    /// The NFT token destination
60    #[cons(writable)]
61    pub nft_destination: &'a T,
62
63    /// The domain name account
64    #[cons(writable)]
65    pub name_account: &'a T,
66
67    /// The NFT record account
68    #[cons(writable)]
69    pub nft_record: &'a T,
70
71    /// The domain name owner
72    #[cons(writable, signer)]
73    pub name_owner: &'a T,
74
75    /// The metadata account
76    #[cons(writable)]
77    pub metadata_account: &'a T,
78
79    /// Master edition account
80    pub edition_account: &'a T,
81
82    /// Collection
83    pub collection_metadata: &'a T,
84
85    /// Mint of the collection
86    pub collection_mint: &'a T,
87
88    /// The central state account
89    #[cons(writable)]
90    pub central_state: &'a T,
91
92    /// The fee payer account
93    #[cons(writable, signer)]
94    pub fee_payer: &'a T,
95
96    /// The SPL token program account
97    pub spl_token_program: &'a T,
98
99    /// The metadata program account
100    pub metadata_program: &'a T,
101
102    /// The system program account
103    pub system_program: &'a T,
104
105    /// The SPL name service program account
106    pub spl_name_service_program: &'a T,
107
108    /// Rent sysvar account
109    pub rent_account: &'a T,
110
111    /// The metadata signer
112    #[cons(signer)]
113    #[cfg(not(feature = "devnet"))]
114    pub metadata_signer: &'a T,
115}
116
117impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
118    pub fn parse(
119        accounts: &'a [AccountInfo<'b>],
120        program_id: &Pubkey,
121    ) -> Result<Self, ProgramError> {
122        let accounts_iter = &mut accounts.iter();
123        let accounts = Accounts {
124            mint: next_account_info(accounts_iter)?,
125            nft_destination: next_account_info(accounts_iter)?,
126            name_account: next_account_info(accounts_iter)?,
127            nft_record: next_account_info(accounts_iter)?,
128            name_owner: next_account_info(accounts_iter)?,
129            metadata_account: next_account_info(accounts_iter)?,
130            edition_account: next_account_info(accounts_iter)?,
131            collection_metadata: next_account_info(accounts_iter)?,
132            collection_mint: next_account_info(accounts_iter)?,
133            central_state: next_account_info(accounts_iter)?,
134            fee_payer: next_account_info(accounts_iter)?,
135            spl_token_program: next_account_info(accounts_iter)?,
136            metadata_program: next_account_info(accounts_iter)?,
137            system_program: next_account_info(accounts_iter)?,
138            spl_name_service_program: next_account_info(accounts_iter)?,
139            rent_account: next_account_info(accounts_iter)?,
140            #[cfg(not(feature = "devnet"))]
141            metadata_signer: next_account_info(accounts_iter)?,
142        };
143
144        // Check keys
145        check_account_key(accounts.central_state, &crate::central_state::KEY)?;
146        check_account_key(accounts.spl_token_program, &spl_token::ID)?;
147        check_account_key(accounts.metadata_program, &mpl_token_metadata::ID)?;
148        check_account_key(accounts.system_program, &system_program::ID)?;
149        check_account_key(accounts.spl_name_service_program, &spl_name_service::ID)?;
150        check_account_key(accounts.rent_account, &sysvar::rent::ID)?;
151        #[cfg(not(feature = "devnet"))]
152        check_account_key(accounts.metadata_signer, &METADATA_SIGNER)?;
153
154        // Check owners
155        check_account_owner(accounts.mint, &spl_token::ID)?;
156        check_account_owner(accounts.nft_destination, &spl_token::ID)?;
157        check_account_owner(accounts.name_account, &spl_name_service::ID)?;
158        check_account_owner(accounts.nft_record, &system_program::ID)
159            .or_else(|_| check_account_owner(accounts.nft_record, program_id))?;
160        check_account_owner(accounts.metadata_account, &system_program::ID)
161            .or_else(|_| check_account_owner(accounts.metadata_account, &mpl_token_metadata::ID))?;
162        check_account_owner(accounts.edition_account, &mpl_token_metadata::ID)?;
163        check_account_owner(accounts.collection_metadata, &mpl_token_metadata::ID)?;
164        check_account_owner(accounts.collection_mint, &spl_token::ID)?;
165
166        // Check signer
167        check_signer(accounts.name_owner)?;
168        #[cfg(not(feature = "devnet"))]
169        check_signer(accounts.metadata_signer)?;
170
171        Ok(accounts)
172    }
173}
174
175pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], params: Params) -> ProgramResult {
176    let accounts = Accounts::parse(accounts, program_id)?;
177    let Params { name, uri } = params;
178
179    let (mint, _) = Pubkey::find_program_address(
180        &[MINT_PREFIX, &accounts.name_account.key.to_bytes()],
181        program_id,
182    );
183    check_account_key(accounts.mint, &mint)?;
184
185    // Create NFT record
186    let (nft_record_key, nft_record_nonce) =
187        NftRecord::find_key(accounts.name_account.key, program_id);
188    check_account_key(accounts.nft_record, &nft_record_key)?;
189
190    // Verify name derivation
191    check_name(&name, accounts.name_account)?;
192
193    // Verify metadata PDA
194    let (metadata_key, _) = Metadata::find_pda(&mint);
195    check_account_key(accounts.metadata_account, &metadata_key)?;
196
197    // Verify edition PDA
198    let (collection_mint, _) =
199        Pubkey::find_program_address(&[COLLECTION_PREFIX, &program_id.to_bytes()], program_id);
200    check_account_key(accounts.collection_mint, &collection_mint)?;
201
202    let (edition_key, _) = MasterEdition::find_pda(&collection_mint);
203    check_account_key(accounts.edition_account, &edition_key)?;
204
205    // Verify collection metadata PDA
206    let (collection_metadata, _) = Metadata::find_pda(&collection_mint);
207    check_account_key(accounts.collection_metadata, &collection_metadata)?;
208
209    // Verify mint
210    let mint_info = Mint::unpack(&accounts.mint.data.borrow())?;
211    if mint_info.supply != 0 {
212        msg!("Expected supply == 0 and received {}", mint_info.supply);
213        return Err(ProgramError::InvalidAccountData);
214    }
215
216    if accounts.nft_record.data_is_empty() {
217        msg!("+ Creating NFT record");
218        let nft_record = NftRecord::new(
219            nft_record_nonce,
220            *accounts.name_owner.key,
221            *accounts.name_account.key,
222            mint,
223        );
224        let seeds: &[&[u8]] = &[
225            NftRecord::SEED,
226            &accounts.name_account.key.to_bytes(),
227            &[nft_record_nonce],
228        ];
229        Cpi::create_account(
230            program_id,
231            accounts.system_program,
232            accounts.fee_payer,
233            accounts.nft_record,
234            seeds,
235            nft_record.borsh_len(),
236        )?;
237
238        nft_record.save(&mut accounts.nft_record.data.borrow_mut());
239    } else {
240        msg!("+ NFT record already exists");
241        let mut nft_record =
242            NftRecord::from_account_info(accounts.nft_record, Tag::InactiveRecord)?;
243
244        check_account_key(accounts.mint, &nft_record.nft_mint)?;
245
246        nft_record.tag = Tag::ActiveRecord;
247        nft_record.owner = *accounts.name_owner.key;
248
249        nft_record.save(&mut accounts.nft_record.data.borrow_mut());
250    }
251
252    // Mint token
253    let ix = mint_to(
254        &spl_token::ID,
255        &mint,
256        accounts.nft_destination.key,
257        &crate::central_state::KEY,
258        &[],
259        1,
260    )?;
261    let seeds: &[&[u8]] = &[&program_id.to_bytes(), &[crate::central_state::NONCE]];
262
263    invoke_signed(
264        &ix,
265        &[
266            accounts.spl_token_program.clone(),
267            accounts.mint.clone(),
268            accounts.nft_destination.clone(),
269            accounts.central_state.clone(),
270        ],
271        &[seeds],
272    )?;
273
274    // Create metadata
275    let central_creator = Creator {
276        address: crate::central_state::KEY,
277        verified: true,
278        share: 0,
279    };
280    if accounts.metadata_account.data_is_empty() {
281        msg!("+ Creating metadata");
282        CreateMetadataAccountV3Cpi::new(
283            accounts.metadata_program,
284            CreateMetadataAccountV3CpiAccounts {
285                metadata: accounts.metadata_account,
286                mint: accounts.mint,
287                mint_authority: accounts.central_state,
288                payer: accounts.fee_payer,
289                update_authority: (accounts.central_state, true),
290                system_program: accounts.system_program,
291                rent: Some(accounts.rent_account),
292            },
293            CreateMetadataAccountV3InstructionArgs {
294                data: DataV2 {
295                    name,
296                    symbol: META_SYMBOL.to_string(),
297                    uri,
298                    seller_fee_basis_points: SELLER_BASIS,
299                    creators: Some(vec![central_creator, CREATOR_FEE]),
300                    collection: None,
301                    uses: None,
302                },
303                is_mutable: true,
304                collection_details: None,
305            },
306        )
307        .invoke_signed(&[seeds])?;
308    } else {
309        msg!("+ Metadata already exists");
310        // Unverify collection first
311        UnverifyCollectionCpi::new(
312            accounts.metadata_program,
313            UnverifyCollectionCpiAccounts {
314                metadata: accounts.metadata_account,
315                collection_authority: accounts.central_state,
316                collection_mint: accounts.collection_mint,
317                collection: accounts.collection_metadata,
318                collection_master_edition_account: accounts.edition_account,
319                collection_authority_record: None,
320            },
321        )
322        .invoke_signed(&[seeds])?;
323
324        let data = DataV2 {
325            name,
326            symbol: META_SYMBOL.to_string(),
327            uri,
328            seller_fee_basis_points: SELLER_BASIS,
329            creators: Some(vec![central_creator, CREATOR_FEE]),
330            collection: None,
331            uses: None,
332        };
333        UpdateMetadataAccountV2Cpi::new(
334            accounts.metadata_program,
335            UpdateMetadataAccountV2CpiAccounts {
336                metadata: accounts.metadata_account,
337                update_authority: accounts.central_state,
338            },
339            UpdateMetadataAccountV2InstructionArgs {
340                data: Some(data),
341                new_update_authority: Some(crate::central_state::KEY),
342                primary_sale_happened: None,
343                is_mutable: None,
344            },
345        )
346        .invoke_signed(&[seeds])?;
347    }
348
349    msg!("+ Verifying collection");
350    SetAndVerifyCollectionCpi::new(
351        accounts.metadata_program,
352        SetAndVerifyCollectionCpiAccounts {
353            metadata: accounts.metadata_account,
354            update_authority: accounts.central_state,
355            collection_authority: accounts.central_state,
356            payer: accounts.fee_payer,
357            collection_mint: accounts.collection_mint,
358            collection: accounts.collection_metadata,
359            collection_master_edition_account: accounts.edition_account,
360            collection_authority_record: None,
361        },
362    )
363    .invoke_signed(&[seeds])?;
364
365    // Transfer domain
366    let ix = transfer(
367        spl_name_service::ID,
368        nft_record_key,
369        *accounts.name_account.key,
370        *accounts.name_owner.key,
371        None,
372    )?;
373    invoke(
374        &ix,
375        &[
376            accounts.spl_name_service_program.clone(),
377            accounts.nft_record.clone(),
378            accounts.name_account.clone(),
379            accounts.name_owner.clone(),
380        ],
381    )?;
382
383    Ok(())
384}