この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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を登録し、
値段をつけて販売することも可能になります。