[Solana] NFTをmintしてみる

2022.08.29

Introduction

以前の記事では、Solanaのスマートコントラクトを試してみました。
今回は、同じくAnchorでスマートコントラクトを記述して
Metaplexを使ってNFTを作成してみます。

環境(Rust、Anchor、Solana wallet)については前回の記事を参照してください。

Environment

  • rust : 1.62.1
  • Node : 18.7
  • yarn : 1.22.17
  • Anchor : 0.24.2

Try

ここの記事を参考に、NFTをmintしてみます。

まずはsolana configコマンドで、対象をdevnetに設定しておきます。

% solana config set --url devnet
Config File: ・・・
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /path/your/.config/solana/id.json
Commitment: confirmed

Keypairファイルのパスを確認しておきましょう。
まだKeypairがない場合、solana-keygen newコマンドで新たに生成しておきます。

solana-keygen pubkeyコマンドでpublic keyが表示されます。

%solana-keygen pubkey
xxxxxxxxxxxxxxxxxxxxxxxxxxx

NFTをmintするのに必要になるので、SOLを少しdropしておきます。
2〜3SOLくらいあればたぶんOK。  

#何度かairdrop
% solana airdrop 1
Requesting airdrop of 1 SOL

% solana balance
1 SOL

Anchorプロジェクト作成

アンカーCLIを使用して、次のコマンドでアンカープロジェクトを作成します。

% anchor init anchror-example

yarn install v1.22.15
warning package.json: No license field
info No lockfile found.
・・・・

Anchor.tomlのcluster設定もdevnetにします。  

## anchror-example/Anchor.toml

・・・

[provider]
cluster = "devnet"
wallet = "<さっき作成したkeypairのフルパス>"

・・・

rustのCargo.tomlで依存ライブラリを設定します。

# programs/<your-project-name>/Cargo.toml

[dependencies]
anchor-lang = "0.24.2"
anchor-spl = "0.24.2"
mpl-token-metadata = {version = "1.2.7", features = ["no-entrypoint"]}

これでmint関数を作成できます。
src/lib.rsを次のように記述します。
ここのソースほぼそのまま

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::invoke;
use anchor_spl::token;
use anchor_spl::token::{MintTo, Token};
use mpl_token_metadata::instruction::{create_master_edition_v3, create_metadata_accounts_v2};


#[derive(Accounts)]
pub struct MintNFT<'info> {
    #[account(mut)]
    pub mint_authority: Signer<'info>,
        /// CHECK: This is not dangerous because we don't read or write from this account
    #[account(mut)]
    pub mint: UncheckedAccount<'info>,
    // #[account(mut)]
    pub token_program: Program<'info, Token>,
    /// CHECK: This is not dangerous because we don't read or write from this account
    #[account(mut)]
    pub metadata: UncheckedAccount<'info>,
    /// CHECK: This is not dangerous because we don't read or write from this account
    #[account(mut)]
    pub token_account: UncheckedAccount<'info>,
    /// CHECK: This is not dangerous because we don't read or write from this account
    pub token_metadata_program: UncheckedAccount<'info>,
    /// CHECK: This is not dangerous because we don't read or write from this account
    #[account(mut)]
    pub payer: AccountInfo<'info>,
    pub system_program: Program<'info, System>,
    /// CHECK: This is not dangerous because we don't read or write from this account
    pub rent: AccountInfo<'info>,
    /// CHECK: This is not dangerous because we don't read or write from this account
    #[account(mut)]
    pub master_edition: UncheckedAccount<'info>,
}

pub fn mint_nft(
    ctx: Context<MintNFT>,
    creator_key: Pubkey,
    uri: String,
    title: String,
) -> Result<()> {
    msg!("Initializing Mint NFT");
    let cpi_accounts = MintTo {
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.token_account.to_account_info(),
        authority: ctx.accounts.payer.to_account_info(),
    };

    msg!("CPI Accounts Assigned");
    let cpi_program = ctx.accounts.token_program.to_account_info();

    msg!("CPI Program Assigned");
    let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

    msg!("CPI Context Assigned");
    token::mint_to(cpi_ctx, 1)?;

    msg!("Token Minted");
    let account_info = vec![
        ctx.accounts.metadata.to_account_info(),
        ctx.accounts.mint.to_account_info(),
        ctx.accounts.mint_authority.to_account_info(),
        ctx.accounts.payer.to_account_info(),
        ctx.accounts.token_metadata_program.to_account_info(),
        ctx.accounts.token_program.to_account_info(),
        ctx.accounts.system_program.to_account_info(),
        ctx.accounts.rent.to_account_info(),
    ];
    msg!("Account Info Assigned");
    let creator = vec![
        mpl_token_metadata::state::Creator {
            address: creator_key,
            verified: false,
            share: 100,
        },
        mpl_token_metadata::state::Creator {
            address: ctx.accounts.mint_authority.key(),
            verified: false,
            share: 0,
        },
    ];
    msg!("Creator Assigned");
    let symbol = std::string::ToString::to_string("symb");
    invoke(
        &create_metadata_accounts_v2(
            ctx.accounts.token_metadata_program.key(),
            ctx.accounts.metadata.key(),
            ctx.accounts.mint.key(),
            ctx.accounts.mint_authority.key(),
            ctx.accounts.payer.key(),
            ctx.accounts.payer.key(),
            title,
            symbol,
            uri,
            Some(creator),
            1,
            true,
            false,
            None,
            None,
        ),
        account_info.as_slice(),
    )?;
    msg!("Metadata Account Created !!!");
    let master_edition_infos = vec![
        ctx.accounts.master_edition.to_account_info(),
        ctx.accounts.mint.to_account_info(),
        ctx.accounts.mint_authority.to_account_info(),
        ctx.accounts.payer.to_account_info(),
        ctx.accounts.metadata.to_account_info(),
        ctx.accounts.token_metadata_program.to_account_info(),
        ctx.accounts.token_program.to_account_info(),
        ctx.accounts.system_program.to_account_info(),
        ctx.accounts.rent.to_account_info(),
    ];
    msg!("Master Edition Account Infos Assigned");
    invoke(
        &create_master_edition_v3(
            ctx.accounts.token_metadata_program.key(),
            ctx.accounts.master_edition.key(),
            ctx.accounts.mint.key(),
            ctx.accounts.payer.key(),
            ctx.accounts.mint_authority.key(),
            ctx.accounts.metadata.key(),
            ctx.accounts.payer.key(),
            Some(0),
        ),
        master_edition_infos.as_slice(),
    )?;
    msg!("Master Edition Nft Minted !!!");
    Ok(())
}


declare_id!("<Program ID>");

#[program]
pub mod anchror_example {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

NFT用の構造体とmintする関数を定義しています。
プログラムをデバッグする場合はmsg!()を使い、
ログはターミナル画面か.anchor/program-logs/に出力されます。

ファイルを記述したらビルド&デプロイ。

% anchor build && anchor deploy

Deploying workspace: https://api.devnet.solana.com
Upgrade authority: /path/your/my-solana-wallet/my-keypair.json
Deploying program "anchror-example"...
Program path: /path/your/anchror-example/target/deploy/anchror_example.so...
Program Id: xxxxxxxxxxxxxxxxxxxxxx

Deploy success

デプロイが成功すると↑のようにProgram Idが表示されます。
このIDをAnchor.tomlとlib.rsのdeclare_idに記述します。

なお、プログラムを修正して再度ビルド/デプロイしたい場合、
anchor cleanしてtarget以下を削除してから再度ビルドしましょう。  

次にmochaテストを実行することでデプロイしたスマートコントラクトを実行し、
NFTをmintします。

yarnかnpmで必要なライブラリをインストールします。

% yarn add @solana/web3.js
% yarn add @solana/spl-token
% yarn add ts-mocha

テストファイル(tests/test.ts)を作成し、
↓のように作成。こちらもこれほぼそのままです。

import * as anchor from '@project-serum/anchor'
import { Program, Wallet } from '@project-serum/anchor'
import { Example } from '../target/types/example'
import { TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, createInitializeMintInstruction, MINT_SIZE } from '@solana/spl-token' // IGNORE THESE ERRORS IF ANY
const { SystemProgram } = anchor.web3

describe('metaplex-anchor-nft', () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  const wallet = provider.wallet as Wallet;
  anchor.setProvider(provider);
  const program = anchor.workspace.Example as Program<Example>

  it("Is initialized!", async () => {
    const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey(
      "<Public Key>"
    );
    const lamports: number =
      await program.provider.connection.getMinimumBalanceForRentExemption(
        MINT_SIZE
      );
    const getMetadata = async (
      mint: anchor.web3.PublicKey
    ): Promise<anchor.web3.PublicKey> => {
      return (
        await anchor.web3.PublicKey.findProgramAddress(
          [
            Buffer.from("metadata"),
            TOKEN_METADATA_PROGRAM_ID.toBuffer(),
            mint.toBuffer(),
          ],
          TOKEN_METADATA_PROGRAM_ID
        )
      )[0];
    };

    const getMasterEdition = async (
      mint: anchor.web3.PublicKey
    ): Promise<anchor.web3.PublicKey> => {
      return (
        await anchor.web3.PublicKey.findProgramAddress(
          [
            Buffer.from("metadata"),
            TOKEN_METADATA_PROGRAM_ID.toBuffer(),
            mint.toBuffer(),
            Buffer.from("edition"),
          ],
          TOKEN_METADATA_PROGRAM_ID
        )
      )[0];
    };

    const mintKey: anchor.web3.Keypair = anchor.web3.Keypair.generate();
    const NftTokenAccount = await getAssociatedTokenAddress(
      mintKey.publicKey,
      wallet.publicKey
    );
    console.log("NFT Account: ", NftTokenAccount.toBase58());

    const mint_tx = new anchor.web3.Transaction().add(
      anchor.web3.SystemProgram.createAccount({
        fromPubkey: wallet.publicKey,
        newAccountPubkey: mintKey.publicKey,
        space: MINT_SIZE,
        programId: TOKEN_PROGRAM_ID,
        lamports,
      }),
      createInitializeMintInstruction(
        mintKey.publicKey,
        0,
        wallet.publicKey,
        wallet.publicKey
      ),
      createAssociatedTokenAccountInstruction(
        wallet.publicKey,
        NftTokenAccount,
        wallet.publicKey,
        mintKey.publicKey
      )
    );

    const res = await program.provider.sendAndConfirm(mint_tx, [mintKey]);
    console.log(
      await program.provider.connection.getParsedAccountInfo(mintKey.publicKey)
    );

    console.log("Account: ", res);
    console.log("Mint key: ", mintKey.publicKey.toString());
    console.log("User: ", wallet.publicKey.toString());

    const metadataAddress = await getMetadata(mintKey.publicKey);
    const masterEdition = await getMasterEdition(mintKey.publicKey);

    console.log("Metadata address: ", metadataAddress.toBase58());
    console.log("MasterEdition: ", masterEdition.toBase58());

    const tx = await program.methods.mintNft(
      mintKey.publicKey,
      <Metadataのパス>,
      "My Icon",
    )
      .accounts({
        mintAuthority: wallet.publicKey,
        mint: mintKey.publicKey,
        tokenAccount: NftTokenAccount,
        tokenProgram: TOKEN_PROGRAM_ID,
        metadata: metadataAddress,
        tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
        payer: wallet.publicKey,
        systemProgram: SystemProgram.programId,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
        masterEdition: masterEdition,
      },
      )
      .rpc();
    console.log("Your transaction signature", tx);
  });
});

mintNftの第2引数にはmetadataのパスを指定します。
このmetadataには画像のパスや名前、属性などのメタ情報を記述します。
metadata・NFT画像はarweaveやShadow Driveに置いておくことが多いみたいです。

ここでやっているように、metadataをarweaveにアップしてもよいですが、
今回はとりあえずサンプルにのっている
https://arweave.net/y5e5DJsiwH0s_ayfMwYk-SnrZtVZzHLQDSTZ5dNRUHA
を指定しておきます。

そしてanchor test実行でNFTをmintします。  

% anchor test
BPF SDK: /path/your/.local/share/solana/install/releases/1.10.25/solana-release/bin/sdk/bpf
cargo-build-bpf child: rustup toolchain list -v
cargo-build-bpf child: cargo +bpf build --target bpfel-unknown-unknown --release
    Finished release [optimized] target(s) in 0.38s

To deploy this program:
Deploying workspace: https://api.devnet.solana.com
Deploying program "example"...
Program Id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Deploy success

NFT Account:  xxxxxxxxxxxxxxxx
{
  context: { apiVersion: '1.10.25', slot: 141731498 },
  value: {
    data: { parsed: [Object], program: 'spl-token', space: 82 },
    executable: false,
    lamports: 1461600,
    owner: PublicKey {
      _bn: <BN: xxxxxxxxxxxxxxxxxx>
    },
    rentEpoch: 328
  }
}
Account:  xxxxxxxxxxxxxx
Mint key:  xxxxxxxxxxx
User:  xxxxxxxxxxxx
Metadata address:  xxxxxxxxxxxx
MasterEdition:  xxxxxxxxxxxxxx
Your transaction signature xxxxxxxxxxxx
    ✔ Is initialized! (5484ms)


  1 passing (5s)

✨  Done in 9.85s.

コードに問題なければ、mintされます。
Solscanを使えば、登録したNFTを確認することもできます。
↑のmint keyを使い、下記のURLをブラウザで表示すると登録したNFTが表示されます。

https://solscan.io/token/<Mint Key>?cluster=devnet#txs

Summary

このような感じで、けっこう簡単にNFTを登録ができました。
今回の宛先はDevnet(開発用)でしたが、
本番用に向けて実際のSOLを使えば本番環境にNFTを登録し、
値段をつけて販売することも可能になります。

References