# Solana project Solana chain 上で NFT を購買 (mint) するためのスマートコントラクト (コード) を作る ## 要件 - Rust を使う - Solana の制約 - ERC721 ではない独自プロトコルが使われている - Magic Eden 上から確認できる - トランザクションを上手く作ると Magic Eden のコントラクトがそれを見つけてくれるらしい? (要調査) - 特定のユーザーに対して先行販売や割引価格で token を発行できる(6/18 追加) ## 参考 - [Solana, Rust関連の資料まとめ](https://zenn.dev/yyokii/scraps/eb35a491b76276) - [OpenZeppelin の ERC721 実装](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721.sol) - [aster chain 上での ERC721 実装](https://github.com/astardegens/astardegens/blob/main/contract/contracts/AstarDegens.sol) - [Solana 分散ストレージの Shadow Drive を利用して Solana エコシステム上で NFT を発行する](https://zenn.dev/regonn/articles/shadow-drive-nft) - [SolanaのNFTをMintするまで解説 1/4 NFTのデータ構成を把握してToken情報からNFT画像まで辿れるようにする](https://zenn.dev/regonn/articles/solana-nft-00) - [Solanaの学習](https://zenn.dev/razokulover/scraps/31398dd485ff26) - [ソラナ(Solana)ブロックチェーン及び ソラナ上のトークンの発行について](https://recruit.gmo.jp/engineer/jisedai/blog/solana_blockchain_and_token/) - [Solana’s Token Program, Explained](https://pencilflip.medium.com/solanas-token-program-explained-de0ddce29714) - [Account Model | Solana Wiki](https://solana.wiki/zh-cn/docs/account-model/#account-storage) - [How To Mint NFTs on Solana Using Rust and Metaplex](https://betterprogramming.pub/how-to-mint-nfts-on-solana-using-rust-and-metaplex-f66bac717cb8) - [イーサリアムキラーの本命?Solanaのプログラムを作って遊んでみる!](https://labs.septeni.co.jp/entry/2021/11/08/090000) - [永久クラウドArweaveのハマりポイント](https://blog.tanebox.com/archives/1949/#Arweave%E3%81%AEDevnetTestnet%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84) - [Program Derived Address日本語で](https://efficacious-flat-24a.notion.site/Program-Derived-Address-8537ebca002245639beb531842f87f2c) - [Non Fungible Tokens (NFTs) - Solana CookBook](https://solanacookbook.com/references/nfts.html) - [Rust で Solana のオンチェーンプログラムをデプロイする](https://note.com/cml_2010/n/n3b0895215b64) - [Anchor + React のサンプルプロジェクト](https://github.com/256hax/solana-anchor-react-minimal-example) - [Candy Machine v2 - Metaplex](https://docs.metaplex.com/candy-machine-v2/introduction) - [Overview/Token Metadata - Metaplex](https://docs.metaplex.com/programs/token-metadata/overview) ## TODO 適宜更新 - [x] Solana 上での NFT の扱い・ mint 方法の調査 (Metaplex) - 参考 - https://zenn.dev/regonn/articles/solana-nft-00 - https://betterprogramming.pub/how-to-mint-nfts-on-solana-using-rust-and-metaplex-f66bac717cb8 - [x] コントラクトの実装 - [x] Rust (anchor) を使う - [x] Candy Machine を使う - [x] (どっちでもいいので) Collection を使う (Candy Machine で実現できた) - [x] Arweave にデータを保存する - [x] Magic Eden 上から Solana chain 上の NFT を認識 (?) させる方法の調査 - [ ] コントラクトの修正 ## Solana のアカウントモデル Solana における**アカウント**は SOL やトークンの保有数など、様々なデータが格納されているデータストレージのことを指す。 ユーザーが Solana のウォレットを作成するとき、裏ではこのアカウントが作成されている。 アカウント上のデータは、送金や mint などにより書き換えることができる。 この書き換え操作を行うものを Solana では **プログラム** と呼んでいる。 Solana におけるプログラムの実態は `executable` というフラグが付けられ、上書き不可能な実行可能なデータが格納されている特殊なアカウントである。 簡便化のためにプログラムのことを Program Account, 通常のアカウントを Data Account と呼ぶこともある。 Ethereum のスマートコントラクトは実行可能なコードと、コードが参照するデータを一つのコントラクトアドレスで管理していたが、 Solana ではプログラムのロジックとデータを明確に区別しており、プログラムが自身のデータを変更するのではなく、自身とは別に用意されたアカウントのデータを読み書きして様々な処理を行うというモデルを採用している。 この特徴のため、 Solana のプログラムは機能の凝集度が高まるうえ、並列で複数のアカウントを操作することが可能になるなどのメリットがある。 また、アカウント間には 「owner」 という概念がある。 あるアカウントの owner はそのアカウントに対してデータの変更などの様々な操作を行うことができる。 Program account は他の Data account を「所有する」ことで書き込み用のストレージとして利用している。 どの Program account が Data account を所有しているかはアカウントの `owner` フィールドを参照すれば良い。 ![](https://miro.medium.com/max/700/1*ZilLS_ogJZLZ2jbBhYjl4w.png) ところで、 `owner` フィールドは Program account にも付与されており、デフォルトでは `System Program` というビルトインのプログラムが owner に設定される。 - [Solana’s Token Program, Explained](https://pencilflip.medium.com/solanas-token-program-explained-de0ddce29714) - [Account Model | Solana Wiki](https://solana.wiki/zh-cn/docs/account-model/#account-storage) ## Solana における token Solana 上では `Token Program` という program account が新しい token の作成をする機能を担っている。 このプログラムを呼び出すと、誰でもオリジナルの token を発行することができる。 Solana の token の実態は、 token の発行数などを管理する Data account である。 Solana の世界ではその他のアカウントと区別するため、 *Mint account* という呼び方をされる(ことが多い?)。 owner の関連を図で表すと以下のようになる。 ![](https://miro.medium.com/max/1400/1*GVI2s7DPTivcxJRwyBjjXA.png) また token を mint するには Mint account を指定して Mint 用の Token Program の instruction を呼び出せば良い。ただし、 mint した token を受け取るためには、 token 毎に専用の 「token 所持用アカウント」 を作成しなければならない。このアカウントのことを *Associated Token account* (あるいは Token Account) と呼ぶ (ことが多い?) そのため、 token を作るには、まず Associated Token account を作成し、 - Mint account - Associated Token account - wallet account (ユーザーが秘密鍵を所持している Data account) の3つのアカウント情報を元に Token Program を呼び出せばよい。 ![](https://miro.medium.com/max/1400/1*JPilr0jjqavDXPn3PQ7c1w.png) ## Solana 上の NFT の扱われ方 Solana の token は発行数や最小の発行単位を設定できる。 ![](https://storage.googleapis.com/zenn-user-upload/f2da3dcda197-20211213.png) Token の decimals が 0 であるもの = supply が 1 で固定になるもの ## Rust で Solana の Program を作成する https://note.com/cml_2010/n/n3b0895215b64 をなぞって作成。 ログにメッセージを残すだけのプログラム。 リポジトリ: https://github.com/keis94/solana-program-sample solana のツールチェインをインストールしておく必要があるが、 brew などでインストールしてしまうとビルド時に一部失敗することがあった。 そのため [公式のインストール手順](https://docs.solana.com/cli/install-solana-cli-tools) を使ってインストールするのが望ましい。 プログラムの本体は以下のようになっている。 `entrypoint!` は名前の通りエントリーポイントを定義するマクロで、 `process_instruction` が呼び出されることになっている。 引数の `accounts` は instruction の実行に関係するすべてのアカウントが配列として渡されている。 また `instruction_data` はプログラム毎に利用するシリアライズされたデータ列。 今回のプログラムではどちらも利用していない。 参考: https://docs.solana.com/developing/programming-model/transactions#instructions ```rust= entrypoint!(process_instruction); fn process_instruction( _program_id: &Pubkey, _accounts: &[AccountInfo], _instruction_data: &[u8], ) -> ProgramResult { msg!("Program Executed!!!"); Ok(()) } ``` ビルドとデプロイは以下のコマンドで行う。 ```rust= cargo build-bpf solana program deploy ./target/deploy/solana_program_sample.so ``` また、実行したプログラムを上記のプログラムをデプロイした後、動作を確認する TS のコードは以下。 Rust の solana-program と同じ用に呼び出しが可能な `@solana/web3.js` というパッケージを使っている。 ```typescript= const invokeSample = async () => { const connection = new Connection(clusterApiUrl("devnet"), "confirmed"); const instruction = new TransactionInstruction({ keys: [], programId: new PublicKey("JCPbwdUAbpG91GXQjb198LLSxmoursfWTBzKg3kc1Waj"), }); const transaction = new Transaction().add(instruction); const payer = Keypair.fromSecretKey(new Uint8Array(wallet)); const res = await sendAndConfirmTransaction(connection, transaction, [payer]); console.dir(`Transaction: ${res}`); }; ``` ## Anchor で NFT 作成 [Anchor](https://github.com/coral-xyz/anchor) は Solana 上で Program を作るためのフレームワーク。 簡単な設定でビルドとデプロイを行えたり、ビルドしたプログラムの型などを自動で生成してくれたりなど、便利な機能が多数用意されている。 実際に作成する際は https://betterprogramming.pub/how-to-mint-nfts-on-solana-using-rust-and-metaplex-f66bac717cb8 をほぼそのままなぞった。 リポジトリ: https://github.com/keis94/mint-nft まずは anchor をインストール ``` cargo install --git https://github.com/project-serum/anchor --tag v0.24.2 anchor-cli --locked ``` 次にソースコードを編集していく。 このリポジトリは大きく 2 つの構成に分かれている。 - Rust で書かれた NFT を mint する on-chain のプログラム - ↑ を実際に呼び出す TS スクリプト (anchor では mocha のテストとして実装する) まずは呼び出し側の TS スクリプトを見ていく。 このスクリプトでは最後に NFT を mint する mintNft を呼び出している。 前章で解説した `process_instruction` と比較すると、 `mintNft` の引数は `_instruction_data` に、 `accounts` はそのまま `_accounts` に対応している。 また、 mintNft には mintAccount の pubkey, NFT に参照させたい画像リンク、 NFT のタイトルが引数として渡されている。 ```typescript= import { MintNft } from "../target/types/mint_nft"; ... const program = anchor.workspace.MintNft as Program<MintNft>; ... const tx = await program.methods .mintNft( mintAccount.publicKey, "https://arweave.net/y5e5DJsiwH0s_ayfMwYk-SnrZtVZzHLQDSTZ5dNRUHA", "Sample NFT" ) .accounts({ mintAuthority: wallet.publicKey, mint: mintAccount.publicKey, tokenAccount: associatedTokenAccount, 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); ``` accounts で参照しているアカウントを作成している処理を探すと、以下のようになっている。 やっていることは 1. Mint account 用のアドレス (鍵ペア) 作成 2. NFT 受け取り用の associatedTokenAccount のアドレスの計算 - アカウントのアドレスは seed が分かれば instruction を行わなくとも事前に計算できるものがある - Associated Token Account の場合は Mint account + 受け取り人のウォレットの2つのアドレスがあれば計算可能。 3. 用意したアドレスを使って mintAccount と associatedTokenAccount を作成 4. NFT のメタデータと master edition (Copy 元の NFT のオリジナル版) のアドレスの計算 ```typescript= const mintAccount: anchor.web3.Keypair = anchor.web3.Keypair.generate(); const associatedTokenAccount = await getAssociatedTokenAddress( mintAccount.publicKey, wallet.publicKey ); console.log("NFT Account: ", associatedTokenAccount.toBase58()); const mint_tx = new anchor.web3.Transaction().add( anchor.web3.SystemProgram.createAccount({ fromPubkey: wallet.publicKey, newAccountPubkey: mintAccount.publicKey, space: MINT_SIZE, programId: TOKEN_PROGRAM_ID, lamports, }), createInitializeMintInstruction( mintAccount.publicKey, 0, wallet.publicKey, wallet.publicKey ), createAssociatedTokenAccountInstruction( wallet.publicKey, associatedTokenAccount, wallet.publicKey, mintAccount.publicKey ) ); ... const metadataAddress = await getMetadata(mintAccount.publicKey); const masterEdition = await getMasterEdition(mintAccount.publicKey); ``` これらのアカウント情報を使っていることがわかったので、次に Rust プログラムを見ていく。 プログラム中からは - NFT の Mint - metadata account (NFT のメタデータを保持する Data account) の作成 - Master Edition の作成 する instruction を呼び出している。 (参考: 公式ドキュメントに NFT の生成に関わるアカウントの関係図 https://docs.metaplex.com/programs/understanding-programs#understanding-diagrams) CPI は Cross-Program Invocations の略で、 instruction ないから別の instruction を呼び出す際に利用されるインターフェース。 ただ、 anchor で CPI をするときは、呼び出しの設定に CpiContext というを専用の struct を使うぐらいの違いしかない。 ```rust= // NFT の Mint 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)?; ``` metadata は Metaplex が公開している [Token Standard のフォーマット](https://docs.metaplex.com/programs/token-metadata/token-standard#the-non-fungible-standard) に従って作成する。 `create_metadata_accounts_v2` の引数がコードだけでは分かりづらいのでエディタ画面を貼り付け。 今回は collection を指定しなかったが、これを指定すれば複数の作品を関連付けて管理することができる。 ![](https://i.imgur.com/9ye18n2.png) ```rust= // metadata の作成 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 = "symb".to_string(); 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(), )?; ``` `create_master_edition_v3` の引数はこうなっている。 ![](https://i.imgur.com/IAhCFHl.png) ```rust= // Master Edition の作成 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(), )?; ``` 以上で Master Edition の作成が完了したので、無事 NFT が Mint できた。 ただし、今回はオフチェーンのメタデータを作成していないため、情報としては不完全な状態。 ## Candy Machine v2 で NFT 作成 Candy Machine は NFT を Mint するサービスを簡単に立ち上げられるツール群。 https://docs.metaplex.com/candy-machine-v2/introduction に従って進める ### Candy Machine CLI のダウンロード metaplex のリポジトリに Candy Machine の CLI ツールがあるのでダウンロードする。 ``` git clone https://github.com/metaplex- foundation/metaplex.git ~/metaplex yarn install --cwd ~/metaplex/js/ ``` ### 設定ファイルの作成 Candy Machine の設定ファイル `config.json` を作る。 オプションの詳細は [ドキュメント](https://docs.metaplex.com/candy-machine-v2/configuration) にまとまっている。 ```json { "price": 1.0, // NFT の価格 "number": 10, // NFT の発行数 "gatekeeper": null, // Captcha を利用するか "solTreasuryAccount": "GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ", // 決済用の SOL アカウント (wallet アドレス) "splTokenAccount": null, // 決済に token を使う場合に利用するアカウント "splToken": null, "goLiveDate": "25 Dec 2021 00:00:00 GMT", // Mint の受付の開始時刻 "endSettings": null, // Mint 受付の終了設定 (時刻 or Mint 数量で指定可) "whitelistMintSettings": null, // Mint 受付前に presale するかや discount の設定など "hiddenSettings": null, "storage": "arweave-sol", // Arweave に SOL を使ってアップロード "ipfsInfuraProjectId": null, "ipfsInfuraSecret": null, "nftStorageKey": null, "awsS3Bucket": null, "noRetainAuthority": false, "noMutable": false } ``` ### NFT のアセット & メタデータ作成 Candy Machine で作成する NFT の元となるデータを作成する。 Candy Machine では Mint したいデータファイルと、そのメタデータを記述した json を 1:1 に対応付けて管理している。 例えば nft.png を NFT として Mint したい場合、対応するメタデータは nft.json というファイルに記述すれば良い。 メタデータの json に設定できるメタデータの一覧は [ドキュメント](https://docs.metaplex.com/programs/token-metadata/token-standard) にまとまっている。 以下はメタデータの例。 ```json= { "name": "Number #000", "symbol": "metamask", "description": "Collection of 1 numbers on the blockchain. This is the number 2/2.", "seller_fee_basis_points": 500, "image": "0.png", "attributes": [ { "trait_type": "Layer-1", "value": "0" }, { "trait_type": "Layer-2", "value": "0" }, { "trait_type": "Layer-3", "value": "1" }, { "trait_type": "Layer-4", "value": "0" } ], "properties": { "creators": [ { "address": "GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ", "share": 100 } ], "files": [ { "uri": "0.png", "type": "image/png" } ] }, "collection": { "name": "demo-samples", "family": "demo-samples" } } ``` メタデータが正しく設定できているかは candy-machine-v2-cli.ts で確認できる。 ```shell-session ❯ ls ./assets 0.json 0.png 1.json 1.png ❯ npx ts-node ./metaplex/js/packages/cli/src/candy-machine-v2-cli.ts verify_assets metaplex/js/assets npx: installed 17 in 2.314s started at: 1655401847754 Verifying token metadata for 2 (img+json) pairs Checking manifest file: /Users/keis/metaplex/js/assets/0.json Checking manifest file: /Users/keis/metaplex/js/assets/1.json ended at: Thu Jun 15 2022 22:50:47 GMT+0900 (Japan Standard Time). time taken: 00:00:00 ``` ### 各種データのアップロード 作成したアセットをアップロードし、各種アカウントを作成する。 試した時点では Collection の作成も自動で行ってくれた。 ```shell-session ❯ npx ts-node metaplex/js/packages/cli/src/candy-machine-v2-cli.ts upload -e devnet -k ~/.config/solana/id.json -cp ./config.json -c example ./assets npx: installed 17 in 1.852s wallet public key: GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ Using cluster devnet ... Beginning the upload for 2 (img+json) pairs started at: 1655402781424 initializing candy machine Candy machine address: 9KWYwGjYny2znU1QGkPA3BMdNJjt41JaETK6DczFZihM Collection metadata address: BkKt938CSHCdDzTRC5rGR3KVaujer7HZJ9dbZHWxjxbY Collection metadata authority: GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ Collection master edition address: 4542NBb5BszrU5xgmbL2zg2Gx38ruzeUKvKyPHqdT6Lr Collection mint address: 43g5M1iSzZTEL3A3GzSZXScQMEXKsumuT5isBa68ADfA Collection PDA address: FPsBP55kqVR4Yq7fCMwqL6g4HKoWMp9HaoyZEtn9E8G Collection authority record address: 7dwBQDURr9EgAg97SMLUVi4M4G7FnnREdWM5FXhVwgD1 Collection: { collectionMetadata: 'BkKt938CSHCdDzTRC5rGR3KVaujer7HZJ9dbZHWxjxbY', collectionPDA: 'FPsBP55kqVR4Yq7fCMwqL6g4HKoWMp9HaoyZEtn9E8G', txId: 'GBumCJmwTh9sDLGJ99UTkiBRvNvJgnoQHDpyzwr5W4Qi8QZ4tGMaBwjED2gcbSR17AApLYN5FZ2LDuMwykqARnh' } initialized config for a candy machine with publickey: 9KWYwGjYny2znU1QGkPA3BMdNJjt41JaETK6DczFZihM ... Done. Successful = true. ... ``` アップロードが成功したかどうかは `verify_upload` で確認できる。 ```shell-session ❯ npx ts-node metaplex/js/packages/cli/src/candy-machine-v2-cli.ts verify_upload -e devnet -k ~/.config/solana/id.json -cp metaplex/js/config.json -c example npx: installed 17 in 3.446s wallet public key: GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ Using cluster devnet Checking 2 items that have yet to be checked... Looking at key 0 Looking at key 1 uploaded (2) out of (2) ready to deploy! ``` ### Candy Machine の起動 ```shell-session ❯ cd metaplex/js/packages/candy-machine-ui ❯ yarn install && yarn start ``` wallet を接続して Mint すると NFT が wallet のアドレスに送られる。 実際に Mint した NFT がこちら https://solscan.io/token/5s8FNoqUe5yDnG74S9XDFasJykkjEm79j66uBkaa7XSM?cluster=devnet ### withdraw Mint が終了しても、 CandyMachine にアップロードしたファイルがチェーン上に残ったままになっている。このままだと rent がかかり続けてしまうので、 withdraw でデータを抜いてくる ```shell-session ❯ npx ts-node ./js/packages/cli/src/candy-machine-v2-cli.ts withdraw 9KWYwGjYny2znU1QGkPA3BMdNJjt41JaETK6DczFZihM -e devnet -k ~/.config/solana/id.json npx: installed 17 in 2.225s wallet public key: GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ Using cluster devnet Amount to be drained from 9KWYwGjYny2znU1QGkPA3BMdNJjt41JaETK6DczFZihM: 0.0092916 WARNING: This command will drain the SOL from Candy Machine 9KWYwGjYny2znU1QGkPA3BMdNJjt41JaETK6DczFZihM. This will break your Candy Machine if its still in use 9KWYwGjYny2znU1QGkPA3BMdNJjt41JaETK6DczFZihM has been withdrawn. Transaction Signature: 3g7WogEm1rKtGU38QVskhnwQEGLs7vgfCRrNYNqkGkhUVe9dzTYMvBeEYPfZPEDTpa1xikXyuvPJJJd6SQufMXeo Congratulations, 9KWYwGjYny2znU1QGkPA3BMdNJjt41JaETK6DczFZihM has been successfuly drained! Please consider support Open Source developers ``` ## Arweave への画像アップロード [Arweave の JS クライアント](https://github.com/ArweaveTeam/arweave-js) を使ってアップロードができる。 ### Arweave のウォレット作成 Arweave のウォレット管理には Chrome extension の [ArConnect](https://chrome.google.com/webstore/detail/arconnect/einnioafmpimabjcddiinlhmijaionap) がよく使われている。 パスワードを入力した後、 New Wallet を押してしばらく待つと seed が表示され、ウォレットのダウンロードができる。 その後、 extension 一覧から ArConnect のアイコンをクリックするとウォレットの確認ができる。 ![](https://i.imgur.com/yGgcNFj.png) 今回は Test Net を使うので、右上をクリックして Settings > Gateway Config で www.arweave.run (TESTNET) を選択しておく。 最後に `https://www.arweave.run/mint/<AR WALLET ADDRESS>/<AIRDROP AMOUNT>` に対してアクセスして airdrop を貰う。 10kB の画像データで 0.02 AR 程度必要だったので、 AIRDROP AMOUNT を 1000000000000 にして 1 AR ほどもらっておけば当分困らない。 ```shell-session # wallet: aXhEvtoq1_XomjGemc9ih4EUt3KiCz5Yh_qHaQMpoaQ の場合 curl https://www.arweave.run/mint/aXhEvtoq1_XomjGemc9ih4EUt3KiCz5Yh_qHaQMpoaQ/1000000000000 ``` ### Arweave への画像アップロード Arweave のクライアントを使って簡単に書くことができる。 ```typescript= const arweave = Arweave.init({ host: "www.arweave.run", port: 443, protocol: "https", }); (async () => { const image = fs.readFileSync("./app/data/icon_pict.png"); const transaction = await arweave.createTransaction({ data: image, }); transaction.addTag("Content-Type", "image/png"); await arweave.transactions.sign(transaction, wallet); const response = await arweave.transactions.post(transaction); console.log(`response: ${response}`); const id = transaction.id; const imageUrl = id ? `https://www.arweave.run/${id}` : undefined; console.log(`imageUrl: ${imageUrl}`); })(); ``` 実際にアップロードされた画像は [こちら](https://www.arweave.run/9Ip80K5pedBqP6rp7FDlP62upth-Mu-utRZT2Jb8g0s) ### メタデータのアップロード metaplex が定義している [off chain メタデータのフォーマット](https://docs.metaplex.com/programs/token-metadata/changelog/v1.0#uri-json-schema) に従ってメタデータを作成する。 今回は簡単にこんなものを作った。 ```json= { "name": "Pict Icon", "description": "Pict Icon", "image": "https://www.arweave.run/9Ip80K5pedBqP6rp7FDlP62upth-Mu-utRZT2Jb8g0s", "animation_url": "https://www.arweave.run/9Ip80K5pedBqP6rp7FDlP62upth-Mu-utRZT2Jb8g0s?ext=glb", "external_url": "https://example.com", "attributes": [ { "trait_type": "trait1", "value": "value1" }, { "trait_type": "trait2", "value": "value2" } ], "properties": { "files": [ { "uri": "https://www.arweave.run/9Ip80K5pedBqP6rp7FDlP62upth-Mu-utRZT2Jb8g0s?ext=png", "type": "image/png" } ], "category": "image" } } ``` スクリプトはイメージのアップロードとほとんど同じ。 ```typescript= const arweave = Arweave.init({ host: "www.arweave.run", port: 443, protocol: "https", }); (async () => { const metadataRequest = JSON.stringify(metadata); const metadataTransaction = await arweave.createTransaction({ data: metadataRequest, }); metadataTransaction.addTag("Content-Type", "application/json"); await arweave.transactions.sign(metadataTransaction, wallet); const response = await arweave.transactions.post(metadataTransaction); console.log(response); return response; })(); ``` ## Magic Eden で NFT を表示 (listing) させる方法 Magic Eden の web サイト上から表示させたい NFT を collection という単位でまとめ、審査を経た上でようやく公開できる。 API などを使って programmable に collection を追加する方法は *不明* (提供されていない?) 唯一可能性があるのは [`GET /instructions/sell`](https://api.magiceden.dev/#3bea2772-c43f-4273-8c0d-a6d4320c90c1) API を使うこと。 Magic Eden API の `GET /instructions/*` はチェーン上の Magic Eden が管理する instruction のデータを取得できるもの。 `GET /instructions/sell` はドキュメント曰く `Get instruction to sell (list)` ということなので、この API を直接叩くことで Magic Eden の [オークションページ](https://magiceden.io/auction/fff_zen_fox) に指定した NFT を登録できる可能性がある。 API にわたすパラメータ(一部)の例は以下。 - seller: [EdUK2ijRsAotd33aHFoLAtdSFfe6KxQnwC81Up37WFfj](https://explorer.solana.com/address/EdUK2ijRsAotd33aHFoLAtdSFfe6KxQnwC81Up37WFfj) - auctionHouseAddress: [E8cU1WiRWjanGxmn96ewBgk9vPTcL6AEZ1t6F6fkgUWe](https://explorer.solana.com/address/E8cU1WiRWjanGxmn96ewBgk9vPTcL6AEZ1t6F6fkgUWe) - Auction House は Metaplex が提供するマーケットプレイス上で売買のコントラクトを実装するために必要なプロトコル (Program account) - https://docs.metaplex.com/auction-house/definition - tokenMint: [BTJX36Nno1ZEP9XPZdyXUPJ7FjCGH9k7F6qpyL4VawKX](https://explorer.solana.com/address/BTJX36Nno1ZEP9XPZdyXUPJ7FjCGH9k7F6qpyL4VawKX) - tokenAccount: [HHF4pE3fPnRBoMbYrppjB8RenFazWYq7cgdo2odDkDFe](https://explorer.solana.com/address/HHF4pE3fPnRBoMbYrppjB8RenFazWYq7cgdo2odDkDFe) ただし、 [`GET /instructions` のドキュメント](https://api.magiceden.dev/#82874536-9430-4dae-81dc-1235baaa3ef3) を読むと、 [API のリクエストフォーム](https://airtable.com/shrsYtSEJ8M8ESaNq) から利用申請を出し、 API key を発行する必要があると書かれている。 この利用申請に必要な情報を抜粋したものが以下なのだが、 PoC 検証している程度の段階だと用意するハードルが高い。そのため検証がまだできていない。 - API を利用するプロジェクト or 組織名 - プロジェクト/組織の説明も - Magic Eden 上のアカウント - [Keybase](https://keybase.io/) のアカウント - API key の送信用に使う模様 - Discord ID - twitter account - (API を使う予定の) web アプリの URL - "Important: We will not approve any requests that cannot provide a demo or working application for us to see." とのことなので必須。 ### Magic Eden で Collection を公開する方法 (手動) Magic Eden の web サイト上で Creators > Apply for listing と進み、ログイン後 Create New Collections を押し、 Collection の追加申請を行う。 申請の妥当性が検証でき次第、その Collection が公開される。 Collection 申請には自身のブランド?の Discord Server の招待リンク、及び Twitter アカウントが必要。 また、すでに [Metaplex Certified Collections](https://help.magiceden.io/en/articles/6098746-what-is-metaplex-certified-collection-standard) を満たす Collection を所有している場合は Collection のアドレスを渡すと申請が少し楽になる模様。 ![](https://i.imgur.com/8DKqngn.png) ![](https://i.imgur.com/JtGyKnA.png) ## 先行販売・割引機能の実装検討 ### Candy Machine + Gumdrop を使う Candy Machine には、特定の token を所持するアカウントに対して discount などを設定できる Whitelist と呼ばれる機能がある。 https://docs.metaplex.com/candy-machine-v2/configuration#whitelist-settings Whitelist の設定例は以下。 `mint` に設定した mint address から mint した token を持つユーザーを対象に presale や discount を設定できる。 token が割引券 (or 整理券?) となるイメージ。 ```json= "whitelistMintSettings": { "mode" : { "burnEveryTime": true }, "mint" : "7nE1GmnMmDKiycFkpHF7mKtxt356FQzVonZqBWsTWZNf", "presale" : true, "discountPrice" : 0.5 } ``` ただし、事前に Whitelist 用の token を配っておく手間がかかる。 この手間を軽減するために **Gumdrop** というツールが公開されている。 Gumdrop はコミュニティ (DAO 等?) へ貢献してくれたユーザーへの airdrop の手間を軽減するために作られたツール。 ユーザーが airdrop を受け取れることをメール、 Discord 等で通知したり、ユーザー (アカウント) がホワイトリストに含まれるかどうかをチェックして Creator の代わりに token を mint する機能を持っている。 このハンズオンが詳しいのでそれに沿って進める。 https://hackmd.io/@epomatti/B1kUsyEhF #### Whitelist 対象者に配布する token を作成 まずは token を作成。`$WHITELIST_MINT_ADDRESS` という環境変数で保存しておく。 ```shell-session ❯ spl-token create-token --decimals 0 Creating token GXoTsMAfgjVdyKBnGBfq6UiSzZDKKhahFyinHSa6ny1o Signature: Lwm3qyvH2LG8KnMEWa2dnbMm4ehtdhm16oREov1u8uKmnwECbQDzBStxRoieKJoYgGtJ6rxfQm8mYRefH117HkL ❯ export WHITELIST_MINT_ADDRESS=GXoTsMAfgjVdyKBnGBfq6UiSzZDKKhahFyinHSa6ny1o ``` 続いて assosiated token account を作成し、 10 個の token を作成。 (安全のために追加で token を作れないように mint を無効化しておくと良い) ```shell-session ❯ spl-token create-account $WHITELIST_MINT_ADDRESS Creating account A72qJz87ErSHrGdNGBJ9Jn6TZcEubAQDPm2oEenqNymp Signature: 4tEKJqK46Gx1THPLBKj2brgk84PqaWdk4Hm3nYHh9A5xhmsArGtjdcuPd9BRBM7yv2wiCSats9vimf6MNYKGz3VC ❯ spl-token mint $WHITELIST_MINT_ADDRESS 10 Minting 10 tokens Token: GXoTsMAfgjVdyKBnGBfq6UiSzZDKKhahFyinHSa6ny1o Recipient: A72qJz87ErSHrGdNGBJ9Jn6TZcEubAQDPm2oEenqNymp Signature: VEHaiowMa51GT6gfskFYTbXbLNgmm8BFNA18XVtc5M5YiW29JA7wxGDWzrZ3L6qv1A67666PHmGNJZzxHA4f84R # 追加の mint を禁止する (内部的には authority を None に設定するようなので、一度設定したらしたら戻せなくなると思われる) ❯ spl-token authorize $WHITELIST_MINT_ADDRESS mint --disable ``` #### Candy Machine の作成 Candy Machine の config.json は以下の内容で作成。 ```json= { "price": 1, "number": 10, "gatekeeper": null, "solTreasuryAccount": "GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ", "splTokenAccount": null, "splToken": null, "goLiveDate": "30 Jun 2022 00:00:00 UTC", "endSettings": null, "whitelistMintSettings": { "mode": { "burnEveryTime": true }, "mint": "GXoTsMAfgjVdyKBnGBfq6UiSzZDKKhahFyinHSa6ny1o", "presale": true, "discountPrice": 0.5 }, "hiddenSettings": null, "storage": "arweave-sol", "ipfsInfuraProjectId": null, "ipfsInfuraSecret": null, "awsS3Bucket": null, "noRetainAuthority": false, "noMutable": false } ``` - `solTreasuryAccount`: 自身のアカウントの pubkey - `whitelistMintSettings` - `mint`: `spl-token` で作った mint アカウント (`$WHITELIST_MINT_ACCOUNT`)。この mint アカウントから mint した token を持っているアカウントが whitelist 対象となる。 - `preale`: true なら whitelist 対象のアカウントは Candy Machine で誰でも自由に mint できるようになるタイミング (`goLiveDate` で指定) よりも前に先行で mint が可能 - `discountPrice`: whitelist 対象のアカウントの割引率 - `mode`: `burnEveryTime: true` の場合、 `mint` で指定したアカウントから発行した token は、 Candy Machine から NFT を貰ったタイミングで burn される (presale, discount の権利行使を一回限りにできる)。 asset は以下のものを使った。 https://github.com/epomatti/cmv2-whitelist/blob/main/assets.tar.gz?raw=true ただし、 `properties.creators.address` は自身のアドレス (今回の例では `GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ`) に差し替えておく。 上記の config.json と asset を使い、 Candy Machine を作成 (config.json や asset は material というディレクトリに格納してある) ```shell-session ❯ npx ts-node metaplex/js/packages/cli/src/candy-machine-v2-cli.ts upload -e devnet -k ~/.config/solana/id.json -cp metaplex/material/config.json metaplex/material/assets npx: installed 17 in 1.525s wallet public key: GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ Using cluster devnet WARNING: On Devnet, the arweave-sol storage option only stores your files for 1 week. Please upload via Mainnet Beta for your final collection. Beginning the upload for 10 (img+json) pairs started at: 1655918469187 initializing candy machine Candy machine address: EdxkHdjM8QGJdtfiTAj8JBd8juySAP86PjRn3GU4C5zs Collection metadata address: CVDdVH6sTgDHEizgtDiqv1WWSu9S6yjewm7eWtKJadWe Collection metadata authority: GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ Collection master edition address: EmSHAfDHEpjDxAnqmYFw1zFvEsQ5xCocLd3sKuyfZAsp Collection mint address: J11u5kdu4poTeDfe7LYeS3LYzscZHJ4B5D6sh2y6NMJG Collection PDA address: 3bJvUKUk576Rqf3baP6MPZVoXvV5ubEU7igDy3n4TQxm Collection authority record address: BMoVSHiKHYgW2YyccSeJnZce1Sk9uUVYnBjtxycbJUVq Collection: { collectionMetadata: 'CVDdVH6sTgDHEizgtDiqv1WWSu9S6yjewm7eWtKJadWe', collectionPDA: '3bJvUKUk576Rqf3baP6MPZVoXvV5ubEU7igDy3n4TQxm', txId: '3FCxA3nT5YDQHN5SPKhVq8LVXavM7VfBeBPm7LqNfPsZ6JHbW24iV2gSE2FJyFb5p8Zg5UJ9YCBi2dpbZGza5jvL' } initialized config for a candy machine with publickey: EdxkHdjM8QGJdtfiTAj8JBd8juySAP86PjRn3GU4C5zs ...(snipped) ❯ npx ts-node metaplex/js/packages/cli/src/candy-machine-v2-cli.ts verify_upload -e devnet -k ~/.config/solana/id.json metaplex/material/assets npx: installed 17 in 2.487s wallet public key: GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ Using cluster devnet ...(snipped) ready to deploy! ``` #### Gumdrop の作成 まずはユーザーのアカウンtの作成を行う。 内訳は - whitelist 対象とするユーザー x2 - whitelist 対象としないユーザー x1 ```shell-session ❯ solana-keygen new -o ./wallets/whitelist_user2.json Generating a new keypair ...(snipped) pubkey: 3S1pUN7k7BT1GaxKjsR4khkJz9p8EAAYhanu7z97pjeh ...(snipped) ❯ solana airdrop 2 3S1pUN7k7BT1GaxKjsR4khkJz9p8EAAYhanu7z97pjeh ...(snipped) ``` 作成したユーザーの一覧: - whitelist_user1.json: `DRCEZGJxbnCJBgC4CGjnBFkLRqhHaXEL7hpnNJHoSFiK` - whitelist_user2.json: `3S1pUN7k7BT1GaxKjsR4khkJz9p8EAAYhanu7z97pjeh` - user1.json: `7E3C5MPfUzY2Ag3as5bXdh6D5oYNxQ4R1azCEUacozgE` whitelist に設定したいユーザーを json で指定 ```json= [ { "handle": "DRCEZGJxbnCJBgC4CGjnBFkLRqhHaXEL7hpnNJHoSFiK", "amount": 1 }, { "handle": "3S1pUN7k7BT1GaxKjsR4khkJz9p8EAAYhanu7z97pjeh", "amount": 1 } ] ``` 次に Gumdrop を下記のリポジトリから clone (参考記事では metaplex リポジトリ内にあるが、 2022/06/22 現在では別リポジトリに分離されている) https://github.com/metaplex-foundation/gumdrop.git ただし、現時点での最新コミット ([201e47bddeaa0e978743f80e11ec86f5b362f9de](https://github.com/metaplex-foundation/gumdrop/tree/201e47bddeaa0e978743f80e11ec86f5b362f9de) では一部エラーが発生するため、 [201e47bddeaa0e978743f80e11ec86f5b362f9de](https://github.com/metaplex-foundation/gumdrop/tree/201e47bddeaa0e978743f80e11ec86f5b362f9de) を使う。 `yarn install` してから gumdrop-cli で Gumdrop を作成する。 ```shell-session ❯ yarn --cwd ./gumdrop install ...(snipped) ❯ npx ts-node gumdrop/packages/cli/src/gumdrop-cli.ts create \ --env devnet \ --keypair ~/.config/solana/id.json \ --claim-integration transfer \ --transfer-mint $WHITELIST_MINT_ADDRESS \ --distribution-method wallets \ --distribution-list metaplex/material/whitelist.json \ --host "https://lwus.github.io/metaplex" npx: installed 17 in 3.299s Parsed options: { env: 'devnet', keypair: '/Users/keis/.config/solana/id.json', host: 'https://lwus.github.io/metaplex', claimIntegration: 'transfer', transferMint: 'GXoTsMAfgjVdyKBnGBfq6UiSzZDKKhahFyinHSa6ny1o', distributionMethod: 'wallets', distributionList: 'metaplex/material/whitelist.json' } wallet public key: GPmURHWfbg78GkGwTVnGn7Aupp3wFhDVEVC5KV8a54cJ temporal signer: gdrpGjVffourzkdDRrQmySw4aTHr8a3xmQzzxSwFD1a Hashes 0 [ <Buffer ca 1e 85 fa e3 c8 c3 ca a8 1d 0c ae 5f 82 8b a0 27 09 ac 99 3d cb 94 dc 3e 7f d9 c2 e1 aa 9c 23>, <Buffer 30 9f fc 32 fb 79 32 1a 9f 1f 24 ff eb c9 92 7b 93 3b ea fc ed 39 46 e3 49 5e 75 02 f6 81 bc a1> ] Hashes 1 [ <Buffer c1 6f ca 77 23 46 2f 13 d3 15 ce ac 5d 74 e6 41 27 fe 77 b4 e2 8c 54 f1 90 6f 90 93 d2 44 0a fc> ] writing base to .log/devnet/6G6e8f95kCwS89dFQcvfTYYGizQyendK9dWz9HAZkBsM/id.json writing claims to .log/devnet/6G6e8f95kCwS89dFQcvfTYYGizQyendK9dWz9HAZkBsM/urls.json Timeout Error caught TypeError: Cannot read property '0' of undefined at awaitTransactionSignatureConfirmation (/Users/keis/Documents/solana-test/gumdrop/packages/cli/src/helpers/transactions.ts:209:41) at processTicksAndRejections (internal/process/task_queues.js:95:5) at async sendSignedTransaction (/Users/keis/Documents/solana-test/gumdrop/packages/cli/src/helpers/transactions.ts:48:26) at async Command.<anonymous> (/Users/keis/Documents/solana-test/gumdrop/packages/cli/src/gumdrop-cli.ts:294:26) ...(snipped) # (Optional) Candy Machine のアカウントを指定して Gumdrop を作成することも可能 # 上のコマンドとやれることは同じ ❯ npx ts-node gumdrop/packages/cli/src/gumdrop-cli.ts create \ --env devnet \ --keypair ~/.config/solana/id.json \ --claim-integration candy \ --candy-machine EdxkHdjM8QGJdtfiTAj8JBd8juySAP86PjRn3GU4C5zs \ --distribution-method wallets \ --distribution-list metaplex/material/whitelist.json ``` `.log/devnet/6G6e8f95kCwS89dFQcvfTYYGizQyendK9dWz9HAZkBsM/urls.json` を確認すると、ユーザーアカウント毎に Gumdrop 取得用の URL が作られている。 ```json= [ { "handle": "DRCEZGJxbnCJBgC4CGjnBFkLRqhHaXEL7hpnNJHoSFiK", "amount": 1, "url": "https://lwus.github.io/metaplex/claim?distributor=Qar2nBiLMAyYRzF3eezeADkyMjfJnkgoZdPQjywZGof&method=wallets&handle=DRCEZGJxbnCJBgC4CGjnBFkLRqhHaXEL7hpnNJHoSFiK&amount=1&index=0&proof=4Gp4YnYvNDVGNzpq7P2GL8iZnb7bjYdKmoybLnk8qeXJ&pin=NA&tokenAcc=78iF2Ex96Yhj1Edu3wf8gcbva4BKdygNerwTvVPDVQiF" }, { "handle": "3S1pUN7k7BT1GaxKjsR4khkJz9p8EAAYhanu7z97pjeh", "amount": 1, "url": "https://lwus.github.io/metaplex/claim?distributor=Qar2nBiLMAyYRzF3eezeADkyMjfJnkgoZdPQjywZGof&method=wallets&handle=3S1pUN7k7BT1GaxKjsR4khkJz9p8EAAYhanu7z97pjeh&amount=1&index=1&proof=EbzMJGiqasc4PZCkgPmKcekobGPWzfAemo9grX375VzA&pin=NA&tokenAcc=78iF2Ex96Yhj1Edu3wf8gcbva4BKdygNerwTvVPDVQiF" } ] ``` この URL にアクセスし、該当のウォレットを接続すると **CLAIM GUMDROP** から whitelist 用の token を受け取ることができる。 ![](https://i.imgur.com/OLmHC1n.png) ![](https://i.imgur.com/1dzX0tE.png) #### Candy Machine から NFT を mint する Candy Machine を起動する ```shell-session ❯ yarn --cwd ./metaplex/js/packages/candy-machine-ui start ``` Candy Machine の起動が完了したら、まずは whitelist に所属している (== whitelist 用の token を持っていない) ウォレットで接続してみる。 するとまだ mint できる日時になっていないが、 mint ボタンを押せるようになっている。 また NFT の価格も discount で設定した通りの金額になっている。 ![](https://i.imgur.com/37sEpfD.png) ボタンを押して NFT の mint が完了すると、 whitelist 用の token がなくなり、 NFT が追加されていることが確認できる。 ![](https://i.imgur.com/CQQIFJp.png) ![](https://i.imgur.com/k02Uz5z.png) 最後に、 whitelist に所属していないアカウントで Candy Machine に接続してみる。 すると今度は MINT ボタンが押せなくなっており、きちんと whitelist に所属しているかを判別できていることが確認できる。 ![](https://i.imgur.com/wWrDU2W.png) 今回は .log/ 以下のファイルに発行用リンクが載った json が作られたが、 gumdrop-cli.ts の引数 [`--distribution-method` を変更](https://docs.metaplex.com/airdrops/create-gumdrop#distribution-method) することで AWS SMS や Discord 経由での配布をすることもできる模様 (未検証)