Browse Source

nft mint init

jpcaulfi 3 years ago
parent
commit
359479b69e

+ 2 - 2
README.md

@@ -56,10 +56,10 @@ Regardless of what you may want to add on top of existing Solana programs, the n
 - [ ] 4. Transferring an account's ownership
 - [ ] 5. Destroying an account
 - ### [ ] Tokens
-- [ ] 1. Creating an SPL Token
+- [x] 1. Creating an SPL Token
 - [ ] 2. Transferring tokens
 - ### [ ] NFTs
-- [ ] 1. Creating an NFT
+- [x] 1. Creating an NFT
 - [ ] 2. Transferring an NFT
 - [ ] 3. Selling an NFT
 - [ ] 4. Adding metadata to an NFT

+ 3 - 0
nfts/README.md

@@ -0,0 +1,3 @@
+### :warning: All NFT examples are on devnet!
+
+`https://api.devnet.solana.com/`

+ 9 - 0
nfts/mint/README.md

@@ -0,0 +1,9 @@
+# Create a New NFT Mint
+
+:notebook_with_decorative_cover: Note: This example is built off of [Mint Token](../../tokens/mint/README.md) and [Mint Token To](../../tokens/mint-to/README.md). If you get stuck, check out those examples.   
+
+___
+
+An NFT is obviously just a token on Solana! So, the process is the same for creating an NFT. There's just a few additional steps:
+- Decimals are set to 0
+- Minting must be disabled after one token is minted (ie. cap the supply at 1).

+ 3 - 3
nfts/mint/anchor/Anchor.toml

@@ -1,13 +1,13 @@
 [features]
 seeds = false
-[programs.localnet]
-hello_solana = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
+[programs.devnet]
+mint_nft = "4Bg2L3bHNk2wPszETtqE76hJHVXmnw2pqeUuumSSx7in"
 
 [registry]
 url = "https://anchor.projectserum.com"
 
 [provider]
-cluster = "localnet"
+cluster = "devnet"
 wallet = "~/.config/solana/id.json"
 
 [scripts]

+ 6 - 0
nfts/mint/anchor/assets/token_metadata.json

@@ -0,0 +1,6 @@
+{
+    "name": "Solana Platinum NFT",
+    "symbol": "SOLP",
+    "description": "Solana NFT - Platinum",
+    "image": "https://www.creativefabrica.com/wp-content/uploads/2021/09/27/Solana-Global-Circle-Line-Icon-Graphics-17928498-1.jpg"
+}

+ 0 - 18
nfts/mint/anchor/programs/hello-solana/src/lib.rs

@@ -1,18 +0,0 @@
-use anchor_lang::prelude::*;
-
-declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
-
-#[program]
-pub mod hello_solana {
-    use super::*;
-
-    pub fn hello(ctx: Context<Hello>) -> Result<()> {
-        
-        msg!("Hello, Solana!");
-        
-        Ok(())
-    }
-}
-
-#[derive(Accounts)]
-pub struct Hello {}

+ 4 - 2
nfts/mint/anchor/programs/hello-solana/Cargo.toml → nfts/mint/anchor/programs/mint-nft/Cargo.toml

@@ -1,12 +1,12 @@
 [package]
-name = "hello-solana"
+name = "mint-nft"
 version = "0.1.0"
 description = "Created with Anchor"
 edition = "2021"
 
 [lib]
 crate-type = ["cdylib", "lib"]
-name = "hello_solana"
+name = "mint_nft"
 
 [features]
 no-entrypoint = []
@@ -17,3 +17,5 @@ default = []
 
 [dependencies]
 anchor-lang = "0.24.2"
+anchor-spl = "0.24.2"
+mpl-token-metadata = { version="1.2.5", features = [ "no-entrypoint" ] }

+ 0 - 0
nfts/mint/anchor/programs/hello-solana/Xargo.toml → nfts/mint/anchor/programs/mint-nft/Xargo.toml


+ 108 - 0
nfts/mint/anchor/programs/mint-nft/src/lib.rs

@@ -0,0 +1,108 @@
+use {
+    anchor_lang::{
+        prelude::*,
+        solana_program::program::invoke,
+        system_program,
+    },
+    anchor_spl::token,
+    mpl_token_metadata::instruction as mpl_instruction,
+};
+
+
+declare_id!("4Bg2L3bHNk2wPszETtqE76hJHVXmnw2pqeUuumSSx7in");
+
+
+#[program]
+pub mod mint_nft {
+    use super::*;
+
+    pub fn mint_token(
+        ctx: Context<MintNft>, 
+        metadata_title: String, 
+        metadata_symbol: String, 
+        metadata_uri: String,
+    ) -> Result<()> {
+
+        const MINT_SIZE: u64 = 82;
+
+        msg!("Creating mint account...");
+        msg!("Mint: {}", &ctx.accounts.mint_account.key());
+        system_program::create_account(
+            CpiContext::new(
+                ctx.accounts.token_program.to_account_info(),
+                system_program::CreateAccount {
+                    from: ctx.accounts.mint_authority.to_account_info(),
+                    to: ctx.accounts.mint_account.to_account_info(),
+                },
+            ),
+            (Rent::get()?).minimum_balance(MINT_SIZE as usize),
+            MINT_SIZE,
+            &ctx.accounts.token_program.key(),
+        )?;
+
+        msg!("Initializing mint account...");
+        msg!("Mint: {}", &ctx.accounts.mint_account.key());
+        token::initialize_mint(
+            CpiContext::new(
+                ctx.accounts.token_program.to_account_info(),
+                token::InitializeMint {
+                    mint: ctx.accounts.mint_account.to_account_info(),
+                    rent: ctx.accounts.rent.to_account_info(),
+                },
+            ),
+            0,                                              // 0 Decimals
+            &ctx.accounts.mint_authority.key(),
+            Some(&ctx.accounts.mint_authority.key()),
+        )?;
+
+        msg!("Creating metadata account...");
+        msg!("Metadata account address: {}", &ctx.accounts.metadata_account.key());
+        invoke(
+            &mpl_instruction::create_metadata_accounts_v2(
+                ctx.accounts.token_metadata_program.key(),      // Program ID (the Token Metadata Program)
+                ctx.accounts.metadata_account.key(),            // Metadata account
+                ctx.accounts.mint_account.key(),                // Mint account
+                ctx.accounts.mint_authority.key(),              // Mint authority
+                ctx.accounts.mint_authority.key(),              // Payer
+                ctx.accounts.mint_authority.key(),              // Update authority
+                metadata_title,                                 // Name
+                metadata_symbol,                                // Symbol
+                metadata_uri,                                   // URI
+                None,                                           // Creators
+                0,                                              // Seller fee basis points
+                true,                                           // Update authority is signer
+                false,                                          // Is mutable
+                None,                                           // Collection
+                None,                                           // Uses
+            ),
+            &[
+                ctx.accounts.metadata_account.to_account_info(),
+                ctx.accounts.mint_account.to_account_info(),
+                ctx.accounts.mint_authority.to_account_info(),
+                ctx.accounts.token_metadata_program.to_account_info(),
+                ctx.accounts.rent.to_account_info(),
+            ],
+        )?;
+
+        msg!("Token mint process completed successfully.");
+
+        Ok(())
+    }
+}
+
+
+#[derive(Accounts)]
+pub struct MintNft<'info> {
+    /// CHECK: We're about to create this with Metaplex
+    #[account(mut)]
+    pub metadata_account: UncheckedAccount<'info>,
+    #[account(mut)]
+    pub mint_account: Signer<'info>,
+    #[account(mut)]
+    pub mint_authority: Signer<'info>,
+    pub rent: Sysvar<'info, Rent>,
+    pub system_program: Program<'info, System>,
+    pub token_program: Program<'info, token::Token>,
+    /// CHECK: Metaplex will check this
+    pub token_metadata_program: UncheckedAccount<'info>,
+}

+ 42 - 12
nfts/mint/anchor/tests/test.ts

@@ -1,22 +1,52 @@
 import * as anchor from "@project-serum/anchor";
-import { HelloSolana } from "../target/types/hello_solana";
+import { MintNft } from "../target/types/mint_nft";
 
-describe("hello-solana", () => {
+
+const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey(
+  "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
+);
+
+
+describe("mint-NFT", () => {
   
-  // Configure the Anchor provider & load the program IDL
-  // The IDL gives you a typescript module
-  //
   const provider = anchor.AnchorProvider.env();
   anchor.setProvider(provider);
-  const program = anchor.workspace.HelloSolana as anchor.Program<HelloSolana>;
+  const payer = provider.wallet as anchor.Wallet;
+  const program = anchor.workspace.MintNft as anchor.Program<MintNft>;
+
+  it("Mint!", async () => {
 
-  it("Say hello!", async () => {
-    
-    // Just run Anchor's IDL method to build a transaction!
+    const mintKeypair: anchor.web3.Keypair = anchor.web3.Keypair.generate();
+    console.log(`New token: ${mintKeypair.publicKey}`);
+
+    // Derive the metadata account's address and set the metadata
     //
-    await program.methods.hello()
-    .accounts({})
-    .rpc();
+    const metadataAddress = (await anchor.web3.PublicKey.findProgramAddress(
+      [
+        Buffer.from("metadata"),
+        TOKEN_METADATA_PROGRAM_ID.toBuffer(),
+        mintKeypair.publicKey.toBuffer(),
+      ],
+      TOKEN_METADATA_PROGRAM_ID
+    ))[0];
+    const testNftTitle = "Solana Platinum NFT";
+    const testNftSymbol = "SOLP";
+    const testNftUri = "https://raw.githubusercontent.com/solana-developers/program-examples/main/nfts/mint/anchor/assets/token_metadata.json";
 
+    // Transact with the "mint_token" function in our on-chain program
+    //
+    await program.methods.mintToken(
+      testNftTitle, testNftSymbol, testNftUri
+    )
+    .accounts({
+      metadataAccount: metadataAddress,
+      mintAccount: mintKeypair.publicKey,
+      mintAuthority: payer.publicKey,
+      systemProgram: anchor.web3.SystemProgram.programId,
+      tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
+      tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
+    })
+    .signers([payer.payer, mintKeypair])
+    .rpc();
   });
 });

+ 6 - 0
nfts/mint/native/assets/token_metadata.json

@@ -0,0 +1,6 @@
+{
+    "name": "Solana Platinum NFT",
+    "symbol": "SOLP",
+    "description": "Solana NFT - Platinum",
+    "image": "https://www.creativefabrica.com/wp-content/uploads/2021/09/27/Solana-Global-Circle-Line-Icon-Graphics-17928498-1.jpg"
+}

+ 3 - 0
nfts/mint/native/package.json

@@ -3,7 +3,10 @@
     "test": "yarn run ts-mocha -p ./tsconfig.json -t 1000000 ./tests/test.ts"
   },
   "dependencies": {
+    "@solana/spl-token": "^0.2.0",
     "@solana/web3.js": "^1.47.3",
+    "borsh": "^0.7.0",
+    "buffer": "^6.0.3",
     "fs": "^0.0.1-security"
   },
   "devDependencies": {

+ 5 - 1
nfts/mint/native/program/Cargo.toml

@@ -4,7 +4,11 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
-solana-program = "1.10.12"
+borsh = "0.9.3"
+borsh-derive = "0.9.1"
+solana-program = "1.10.26"
+spl-token = { version="3.3.0", features = [ "no-entrypoint" ] }
+mpl-token-metadata = { version="1.2.5", features = [ "no-entrypoint" ] }
 
 [lib]
 crate-type = ["cdylib", "lib"]

+ 107 - 14
nfts/mint/native/program/src/lib.rs

@@ -1,29 +1,122 @@
-use solana_program::{
-    account_info::AccountInfo, 
-    entrypoint, 
-    entrypoint::ProgramResult, 
-    msg, 
-    pubkey::Pubkey,
+use {
+    borsh::{
+        BorshSerialize, BorshDeserialize,
+    },
+    solana_program::{
+        account_info::{next_account_info, AccountInfo}, 
+        entrypoint, 
+        entrypoint::ProgramResult, 
+        msg, 
+        program::invoke,
+        pubkey::Pubkey,
+        rent::Rent,
+        system_instruction,
+        sysvar::Sysvar,
+    },
+    spl_token::{
+        instruction as token_instruction,
+    },
+    mpl_token_metadata::{
+        instruction as mpl_instruction,
+    },
 };
 
 
-// Tells Solana that the entrypoint to this program
-//  is the "process_instruction" function.
-//
 entrypoint!(process_instruction);
 
 
-// Our entrypoint's parameters have to match the
-//  anatomy of a transaction instruction (see README).
-//
 fn process_instruction(
-    program_id: &Pubkey,
+    _program_id: &Pubkey,
     accounts: &[AccountInfo],
     instruction_data: &[u8],
 ) -> ProgramResult {
 
+    const MINT_SIZE: u64 = 82;
+
+    let accounts_iter = &mut accounts.iter();
+
+    let mint_account = next_account_info(accounts_iter)?;
+    let metadata_account = next_account_info(accounts_iter)?;
+    let mint_authority = next_account_info(accounts_iter)?;
+    let rent = next_account_info(accounts_iter)?;
+    let _system_program = next_account_info(accounts_iter)?;
+    let token_program = next_account_info(accounts_iter)?;
+    let token_metadata_program = next_account_info(accounts_iter)?;
+
+    let token_metadata = TokenMetadata::try_from_slice(instruction_data)?;
     
-    msg!("Hello, Solana!");
+    msg!("Creating mint account...");
+    msg!("Mint: {}", mint_account.key);
+    invoke(
+        &system_instruction::create_account(
+            &mint_authority.key,
+            &mint_account.key,
+            (Rent::get()?).minimum_balance(MINT_SIZE as usize),
+            MINT_SIZE,
+            &token_program.key,
+        ),
+        &[
+            mint_account.clone(),
+            mint_authority.clone(),
+            token_program.clone(),
+        ]
+    )?;
+
+    msg!("Initializing mint account...");
+    msg!("Mint: {}", mint_account.key);
+    invoke(
+        &token_instruction::initialize_mint(
+            &token_program.key,
+            &mint_account.key,
+            &mint_authority.key,
+            Some(&mint_authority.key),
+            0,                              // 0 Decimals
+        )?,
+        &[
+            mint_account.clone(),
+            mint_authority.clone(),
+            token_program.clone(),
+            rent.clone(),
+        ]
+    )?;
+
+    msg!("Creating metadata account...");
+    msg!("Metadata account address: {}", metadata_account.key);
+    invoke(
+        &mpl_instruction::create_metadata_accounts_v2(
+            *token_metadata_program.key,    // Program ID (the Token Metadata Program)
+            *metadata_account.key,          // Metadata Account
+            *mint_account.key,              // Mint Account
+            *mint_authority.key,            // Mint Authority
+            *mint_authority.key,            // Payer
+            *mint_authority.key,            // Update Authority
+            token_metadata.title,           // Name
+            token_metadata.symbol,          // Symbol
+            token_metadata.uri,             // URI
+            None,                           // Creators
+            0,                              // Seller fee basis points
+            true,                           // Update authority is signer
+            false,                          // Is mutable
+            None,                           // Collection
+            None,                           // Uses
+        ),
+        &[
+            metadata_account.clone(),
+            mint_account.clone(),
+            mint_authority.clone(),
+            token_metadata_program.clone(),
+            rent.clone(),
+        ],
+    )?;
+
+    msg!("Token mint process completed successfully.");
 
     Ok(())
+}
+
+#[derive(BorshSerialize, BorshDeserialize, Debug)]
+pub struct TokenMetadata {
+    title: String,
+    symbol: String,
+    uri: String,
 }

+ 108 - 14
nfts/mint/native/tests/test.ts

@@ -1,10 +1,22 @@
 import {
     Connection,
     Keypair,
-    sendAndConfirmTransaction,
-    Transaction,
+    PublicKey,
+    SystemProgram,
+    SYSVAR_RENT_PUBKEY,
     TransactionInstruction,
+    Transaction,
+    sendAndConfirmTransaction,
 } from '@solana/web3.js';
+import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
+import * as borsh from "borsh";
+import { Buffer } from "buffer";
+
+
+const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
+    "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
+);
+
 
 function createKeypairFromFile(path: string): Keypair {
     return Keypair.fromSecretKey(
@@ -13,32 +25,114 @@ function createKeypairFromFile(path: string): Keypair {
 };
 
 
-describe("hello-solana", () => {
+describe("mint-token", () => {
 
-    // Loading these from local files for development
-    //
-    const connection = new Connection(`http://localhost:8899`, 'confirmed');
+    const connection = new Connection(`http://api.devnet.solana.com/`, 'confirmed');
     const payer = createKeypairFromFile(require('os').homedir() + '/.config/solana/id.json');
     const program = createKeypairFromFile('./program/target/so/program-keypair.json');
+
+    class Assignable {
+        constructor(properties) {
+            Object.keys(properties).map((key) => {
+                return (this[key] = properties[key]);
+            });
+        };
+    };
+
+    class TokenMetadata extends Assignable {
+        toBuffer() {
+            return Buffer.from(borsh.serialize(TokenMetadataSchema, this));
+        }
+    };
+
+    const TokenMetadataSchema = new Map([
+        [
+            TokenMetadata, {
+                kind: 'struct',
+                fields: [
+                    ['title', 'string'],
+                    ['symbol', 'string'],
+                    ['uri', 'string'],
+                ]
+            }
+        ]
+    ]);
   
-    it("Say hello!", async () => {
+    it("Mint!", async () => {
+
+        const mintKeypair: Keypair = Keypair.generate();
+        console.log(`New token: ${mintKeypair.publicKey}`);
+
+        // Derive the metadata account's address and set the metadata
+        //
+        const metadataAddress = (await PublicKey.findProgramAddress(
+            [
+              Buffer.from("metadata"),
+              TOKEN_METADATA_PROGRAM_ID.toBuffer(),
+              mintKeypair.publicKey.toBuffer(),
+            ],
+            TOKEN_METADATA_PROGRAM_ID
+        ))[0];
+        const metadata = new TokenMetadata({
+            title: "Solana PLatinum NFT",
+            symbol: "SOLP",
+            uri: "https://raw.githubusercontent.com/solana-developers/program-examples/main/nfts/mint/native/assets/token_metadata.json",
+        });
 
-        // We set up our instruction first.
+        // Transact with the "mint_token" function in our on-chain program
         //
         let ix = new TransactionInstruction({
             keys: [
-                {pubkey: payer.publicKey, isSigner: true, isWritable: true}
+                // Mint account
+                {
+                    pubkey: mintKeypair.publicKey,
+                    isSigner: true,
+                    isWritable: true,
+                },
+                // Metadata account
+                {
+                    pubkey: metadataAddress,
+                    isSigner: false,
+                    isWritable: true,
+                },
+                // Mint Authority
+                {
+                    pubkey: payer.publicKey,
+                    isSigner: true,
+                    isWritable: false,
+                },
+                // Rent account
+                {
+                    pubkey: SYSVAR_RENT_PUBKEY,
+                    isSigner: false,
+                    isWritable: false,
+                },
+                // System program
+                {
+                    pubkey: SystemProgram.programId,
+                    isSigner: false,
+                    isWritable: false,
+                },
+                // Token program
+                {
+                    pubkey: TOKEN_PROGRAM_ID,
+                    isSigner: false,
+                    isWritable: false,
+                },
+                {
+                    pubkey: TOKEN_METADATA_PROGRAM_ID,
+                    isSigner: false,
+                    isWritable: false,
+                },
             ],
             programId: program.publicKey,
-            data: Buffer.alloc(0), // No data
+            data: metadata.toBuffer(),
         });
 
-        // Now we send the transaction over RPC
-        //
         await sendAndConfirmTransaction(
             connection, 
-            new Transaction().add(ix), // Add our instruction (you can add more than one)
-            [payer]
+            new Transaction().add(ix),
+            [payer, mintKeypair]
         );
     });
   });

+ 6 - 0
tokens/mint/anchor/assets/token_metadata.json

@@ -0,0 +1,6 @@
+{
+    "name": "Solana Gold",
+    "symbol": "GOLDSOL",
+    "description": "A gold Solana SPL token :)",
+    "image": "https://images.all-free-download.com/images/graphiclarge/solana_coin_sign_icon_shiny_golden_symmetric_geometrical_design_6919941.jpg"
+}

+ 4 - 4
tokens/mint/anchor/tests/test.ts

@@ -29,14 +29,14 @@ describe("mint-token", () => {
       ],
       TOKEN_METADATA_PROGRAM_ID
     ))[0];
-    const testNftTitle = "Solana Gold";
-    const testNftSymbol = "GOLDSOL";
-    const testNftUri = "https://raw.githubusercontent.com/solana-developers/program-examples/main/tokens/mint/native/assets/token_metadata.json";
+    const testTokenTitle = "Solana Gold";
+    const testTokenSymbol = "GOLDSOL";
+    const testTokenUri = "https://raw.githubusercontent.com/solana-developers/program-examples/main/tokens/mint/native/assets/token_metadata.json";
 
     // Transact with the "mint_token" function in our on-chain program
     //
     await program.methods.mintToken(
-      testNftTitle, testNftSymbol, testNftUri
+      testTokenTitle, testTokenSymbol, testTokenUri
     )
     .accounts({
       metadataAccount: metadataAddress,