6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Web3 DevAdvent Calendar 2022

Day 5

✨Solidity + React.jsでNFTのミント&表示機能を実装してみた✨

Last updated at Posted at 2022-10-19

nftmint.001.jpeg

導入

皆さん、こんにちは。
前回の投稿から少し時間が空いてしまいましたが、色々とブロックチェーン技術をインプットしていましてようやく落ち着くことができたので、一旦アウトプットとしてDapp開発の記録をまとめたいと思います。

今回は、React.jsとSolidityを利用してNFTのミントサイトとそれを表示する機能を持つDAppを実装してみましたので、色々とまとめていきたいと思います。

それでは今回の記事の目次になります。

目次

  1. 今回実装したアプリのイメージとリポジトリ
  2. 実装にあたり利用したフレームワークやAPIなど
  3. ポイントとなったソースコードの紹介
  4. 課題とまとめ
  5. 参考文献

今回実装したアプリのイメージとリポジトリ

今回実装したDAppですが、Web3エンジニアコミュニティUNCHAINの学習コンテンツの一つであるGenerativeNFTで実装したDAppをベースに機能強化したものになります。

少し話は逸れますが、UNCHAINは良質な学習コンテンツを無料で参照することができ、DApp開発の基礎力を身につけることができるのでまだ参加されていない方はぜひ下記からエントリーしてみてください!審査に通過する必要がありますが、専用のdiscordでは同じような悩みを持つエンジニアと勉強会を開催したり、実際にWeb3関連の仕事にトライしてみたりすることができます!日本語系のコンテンツでは私の知る限り、最も洗練されているものだと考えています!

さて、本題に戻ります。今回作成したDAppのソースコートは下記に格納してあります。

vercel上にもアプリをデプロイしていますので完成系をみてみたいという方は、したのリンクからアクセスしてみてください!

mumbai ネットワークで接続してみてください。

アプリを起動させると下記のような画面が表示されます。
この画面では、コントラクトへのリンクの他、NFTをミントすることができます。

一度に最大3枚までミントすることが可能となります。

また、NFTを確認するというボタンをクリックするとこのコントラクトから発行したNFTを一覧表示する画面に遷移します。

home.png

サイトの上の方に提示されているコントラクトアドレスはリンクになっていて、クリックするとPolygonScanに飛ぶことができます。
ちなみに今回デプロイしたNFTコントラクトは下記から確認することができます。

NFTを一覧表示するView画面はこんな感じです。
メタデータである画像がうまく表示されていない件については4.の課題とまとめの項目で触れたいと思います。正直ここが一番難しかったです。
対応できているチェーンもMoralisのAPIの仕様に合わせる形になるのでMumbai以外は挙動が不安定になっています。

view.png

実装にあたり利用したフレームワークやAPIなど

今回、DAppを実装するにあたり利用したフレームワークなどをまとめてみました。
もしご自分で開発される際にはぜひ参考にして見てください。

No. 名称 概要
1 hardhat スマートコントラクト用のフレームワーク
2 React.js フロンエンド用のフレームワーク
3 generative-nft-library ジェネラティブNFT用のメタデータ類を生成するためのライブラリ
4 IPFS NFTのメタデータを格納するために使用。
5 Pinata IPFSへのファイルアップロードなどのAPIを提供しているサービス
6 Moralis NFTやブロックチェーン機能を利用するための各種APIを提供しているサービス
7 Mui Component React.js向けの便利なモジュール。ここのコンポーネントを使うだけでかなりデザインが整ったサイトを作ることができます。
8 React spinners こちらもReact.js向けの便利なモジュール。非同期処理時に表示する際には是非利用してみてください

また、NFTコントラクトもmumbaiネットワークのみでなく、色々なブロックチェーンにデプロイしてマルチチェーン対応にもチャレンジしてみましたので、参考までに各コントラクトアドレス情報を記載いたします。

メインネットとして初めてAstar Networkにもコントラクトをデプロイしてみました! メインネットにデプロイすると感動しますね!

ネットワーク コントラクトアドレス
Munbai Network 0xfe03B6a6B4B095248F06Ed9528e913995ED58f97
Shibuya Network 0xAa363921A48Eac63F802C57658CdEde768B3DAe1
Shiden 0xAa363921A48Eac63F802C57658CdEde768B3DAe1
Avalanche testnet 0x8DF7e6234f76e8fAC829feF83E7520635359094C
Rinkeby 0x587E68B8b22d803Ac0aAF568e87c6fE12DA103E7
BSC Testnet 0x67ADc29278d87D87b212C59fDffd2749fe7418c4
Astar Network 0x599c542e6FF0e009D929091e948d2BA510136741

ジェネラティブNFTとは何か?

コンピューターによって自動生成されたNFTアートのことになります。
今回は、generative-nft-libraryを利用して生成しました。

PinataとMoralisのAPIはNFTのデータの取得のために利用しています。

PinataのAPIは、NFTの画像を表示する際に利用しています。
https://gateway.pinata.cloud/ipfs/ + 各画像までのパス
で画像データを取得することができます。

IPFSに画像をアップロードするのは簡単なんですが、表示させるのが一苦労です。

そしてMoralisのAPIはNFT全体のメタデータを取得するために利用しています。
開発者向けのドキュメントを参考にAPIを利用しました。今回は利用しませんでしたが、Alchemyにも似たようなAPIがあるので色々と試しながら一番良いAPIを利用できるようになりたいと思います。

【開発者向けドキュメント】

このドキュメントを読んでいくとNFTを取得するためのAPIとして、コントラクトに紐づくNFTデータを全て取得するgetContractNFTsなるAPIを発見しました!
このAPIは、curlコマンドでも試すことができ、例えば次のように叩くとアドレスに紐づくNFTのデータを取得することができます。

My-API-Keyには、各自で生成したAPIキーを入力してください。

curl -X 'GET' \
  'https://deep-index.moralis.io/api/v2/nft/0xfe03b6a6b4b095248f06ed9528e913995ed58f97?chain=mumbai&format=decimal' \
  -H 'accept: application/json'\
  -H 'X-API-Key: My-API-Key' 

上記コマンドを叩くと下記のような結果が返ってきます!

{
  "total":25,"page":0,"page_size":100,"cursor":null,"result":[{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"1","amount":"1","token_hash":"ffd8d9c00dd64a2afdd3661d355e3814","block_number_minted":"26611791","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/1","metadata":"{\"name\":\"My First Collection #1\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/01.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"white\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"}]}","last_token_uri_sync":"2022-06-05T09:24:25.143Z","last_metadata_sync":"2022-06-05T09:24:26.662Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"6","amount":"1","token_hash":"fa172747a92d1bd285b21808a80d3924","block_number_minted":"26611824","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/6","metadata":"{\"name\":\"My First Collection #6\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/06.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"white\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Clothes\",\"value\":\"blue_dot\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"}]}","last_token_uri_sync":"2022-06-05T09:29:56.657Z","last_metadata_sync":"2022-06-05T09:29:57.603Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"20","amount":"1","token_hash":"eed23a18a5810c2c5928d172cd4b0f8b","block_number_minted":"27843309","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/20","metadata":null,"last_token_uri_sync":"2022-08-30T13:08:49.733Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"7","amount":"1","token_hash":"ee20152e89cd189f470fc7993f973789","block_number_minted":"26611905","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/7","metadata":"{\"name\":\"My First Collection #7\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/07.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"blue\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Head Gear\",\"value\":\"std_lord\"},{\"trait_type\":\"Clothes\",\"value\":\"blue_dot\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"}]}","last_token_uri_sync":"2022-06-05T09:38:01.211Z","last_metadata_sync":"2022-06-05T09:38:02.700Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"3","amount":"1","token_hash":"e34c9ab81e4f9afea255e38519c6a7e9","block_number_minted":"26611802","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/3","metadata":"{\"name\":\"My First Collection #3\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/03.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"white\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"},{\"trait_type\":\"Wristband\",\"value\":\"dark-green\"}]}","last_token_uri_sync":"2022-06-05T09:26:15.717Z","last_metadata_sync":"2022-06-05T09:26:16.650Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"4","amount":"1","token_hash":"d974557f106daf89650713e3ee187333","block_number_minted":"26611810","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/4","metadata":"{\"name\":\"My First Collection #4\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/04.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"blue\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Head Gear\",\"value\":\"std_lord\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"}]}","last_token_uri_sync":"2022-06-05T09:27:36.088Z","last_metadata_sync":"2022-06-05T09:27:37.611Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"19","amount":"1","token_hash":"d6af278046d1448f5ff5ef096451d399","block_number_minted":"27843309","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/19","metadata":null,"last_token_uri_sync":"2022-08-30T13:08:46.667Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"11","amount":"1","token_hash":"a01b734b9c004be52bb93d9dd6cd8705","block_number_minted":"26614003","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/11","metadata":"{\"name\":\"My First Collection #11\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/11.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"blue\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Head Gear\",\"value\":\"std_lord\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"},{\"trait_type\":\"Wristband\",\"value\":\"white\"}]}","last_token_uri_sync":"2022-06-05T13:21:22.597Z","last_metadata_sync":"2022-06-05T13:21:24.181Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"10","amount":"1","token_hash":"9e57f63ff5f45dc6c8cdfc9e9f226e61","block_number_minted":"26612229","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/10","metadata":"{\"name\":\"My First Collection #10\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/10.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"white\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Head Gear\",\"value\":\"std_lord\"},{\"trait_type\":\"Clothes\",\"value\":\"blue_dot\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"},{\"trait_type\":\"Wristband\",\"value\":\"light-green\"}]}","last_token_uri_sync":"2022-06-05T10:10:20.891Z","last_metadata_sync":"2022-06-05T10:10:21.932Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"13","amount":"1","token_hash":"929c9834029808e8b6d4fdada63373e4","block_number_minted":"26638061","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/13","metadata":null,"last_token_uri_sync":"2022-06-07T06:56:16.234Z","last_metadata_sync":"2022-10-06T04:59:32.827Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"14","amount":"1","token_hash":"8832f2677a4978bb889142d00f7324e8","block_number_minted":"27024555","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/14","metadata":null,"last_token_uri_sync":"2022-07-04T05:22:49.418Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"24","amount":"1","token_hash":"7ca213ab327feb402f3e16be390ed397","block_number_minted":"27850221","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/24","metadata":null,"last_token_uri_sync":"2022-08-31T01:25:04.371Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"12","amount":"1","token_hash":"711653a2c77894b563c5b4f1a057491e","block_number_minted":"26624012","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/12","metadata":"{\"name\":\"My First Collection #12\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/12.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"white\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Head Gear\",\"value\":\"std_lord\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"},{\"trait_type\":\"Wristband\",\"value\":\"light-green\"}]}","last_token_uri_sync":"2022-06-06T05:31:23.096Z","last_metadata_sync":"2022-06-06T05:31:24.842Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"15","amount":"1","token_hash":"5bbae8a712fb5bf97f028f9859d84161","block_number_minted":"27425346","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/15","metadata":null,"last_token_uri_sync":"2022-08-01T07:19:21.910Z","last_metadata_sync":"2022-10-06T04:59:32.827Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"9","amount":"1","token_hash":"52987a7fd27ab93106d3f2a46638e15e","block_number_minted":"26611986","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/9","metadata":"{\"name\":\"My First Collection #9\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/09.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"blue\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Clothes\",\"value\":\"blue_dot\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"},{\"trait_type\":\"Wristband\",\"value\":\"dark-green\"}]}","last_token_uri_sync":"2022-06-05T09:44:43.041Z","last_metadata_sync":"2022-06-05T09:44:44.665Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"2","amount":"1","token_hash":"4cb7c8665a4f87f3f920d6045cc27305","block_number_minted":"26611798","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/2","metadata":"{\"name\":\"My First Collection #2\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/02.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"blue\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"},{\"trait_type\":\"Wristband\",\"value\":\"gray\"}]}","last_token_uri_sync":"2022-06-05T09:25:35.514Z","last_metadata_sync":"2022-06-05T09:25:36.929Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"22","amount":"1","token_hash":"44aedd9ec425e142d9777ebe3bc0da5e","block_number_minted":"27850221","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/22","metadata":null,"last_token_uri_sync":"2022-08-31T01:25:07.318Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"21","amount":"1","token_hash":"42695efc009f4433de90e161b101bfa6","block_number_minted":"27843309","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/21","metadata":null,"last_token_uri_sync":"2022-08-30T13:08:46.667Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"8","amount":"1","token_hash":"39f361068d86b627c844344fc92f96c8","block_number_minted":"26611918","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/8","metadata":"{\"name\":\"My First Collection #8\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/08.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"blue\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"}]}","last_token_uri_sync":"2022-06-05T09:39:01.730Z","last_metadata_sync":"2022-06-05T09:39:03.167Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"5","amount":"1","token_hash":"384948458d4adaf3451c1fb5f82ad80c","block_number_minted":"26611820","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/5","metadata":"{\"name\":\"My First Collection #5\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/05.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"blue\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Clothes\",\"value\":\"blue_dot\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"}]}","last_token_uri_sync":"2022-06-05T09:29:16.489Z","last_metadata_sync":"2022-06-05T09:29:17.458Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"23","amount":"1","token_hash":"274b84599429668b3d486e9d822ea4ee","block_number_minted":"27850221","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/23","metadata":null,"last_token_uri_sync":"2022-08-31T01:25:07.318Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"17","amount":"1","token_hash":"199dd5feab261452d0b2fa20448c8a6d","block_number_minted":"27843294","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/17","metadata":null,"last_token_uri_sync":"2022-08-30T13:07:33.818Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"0","amount":"1","token_hash":"193ee812b0f66e509d29729d0cbe0484","block_number_minted":"26611545","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/0","metadata":"{\"name\":\"My First Collection #0\",\"description\":\"\",\"image\":\"ipfs://QmTPaTaJwbdjfZ3cNb46bdxST3Fe9gaU4tB46bACwALNd9/00.png\",\"attributes\":[{\"trait_type\":\"Background\",\"value\":\"white\"},{\"trait_type\":\"Body\",\"value\":\"maroon\"},{\"trait_type\":\"Eyes\",\"value\":\"standard\"},{\"trait_type\":\"Head Gear\",\"value\":\"std_lord\"},{\"trait_type\":\"Held Item\",\"value\":\"nut\"},{\"trait_type\":\"Hands\",\"value\":\"standard\"}]}","last_token_uri_sync":"2022-06-05T09:02:33.751Z","last_metadata_sync":"2022-06-05T09:02:35.501Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"16","amount":"1","token_hash":"17335b2b98c2f4901bb6b5bf69dec715","block_number_minted":"27843294","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/16","metadata":null,"last_token_uri_sync":"2022-08-30T13:07:30.788Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"},{"token_address":"0xfe03b6a6b4b095248f06ed9528e913995ed58f97","token_id":"18","amount":"1","token_hash":"02c618fde9a30a292b642424d886c18d","block_number_minted":"27843294","updated_at":null,"contract_type":"ERC721","name":"My NFT Collectible","symbol":"NFTC","token_uri":"https://ipfs.moralis.io:2053/ipfs/QmVBo9Httns6eAbqH2voWMkAGY2RxDeKmMcfafV1uE2gcW/18","metadata":null,"last_token_uri_sync":"2022-08-30T13:07:33.818Z","last_metadata_sync":"2022-10-06T04:56:30.092Z"}]
}

今回は、mumbaiで試していますが他のチェーンにも対応するので気になる方は他のNFTコントラクトのアドレスやチェーンを指定して叩いてみてください。
フロントエンドではsuperagentを利用してこのAPIを呼び出しています。

Pinataとは何か?

IPFSピンニングサービスを提供している。IPFSへファイルやディレクトリごとIPFSにアップロードできる機能があるため、画像データを利用したNFTも簡単に作ることができます。

Moralisとは何か?

APIの他、ブロックチェーンノードをお手軽に構築する機能などを提供してくれていてるDApp構築プラットフォームのことです。アカウントを作れば無料で色んな機能を使うことができるので一つアカウントを持っておくと良いと思います。

【Puinataの公式サイト】

【Moralisの公式サイト】

ポイントとなったソースコードの紹介

次にポイントとなったソースコードの紹介です。

ポイントになるソースコードは、下記3点になります。

  • NFTコントラクト
  • hardhatの設定ファイル
  • フロント側のApp.jsファイル
  • NFTを表示するView.jsコンポーネントファイル

それぞれ紹介していきます。

1.NFTコントラクト

かなり基本的なスマートコントラクトになっていますが、ポイントはmintNFTs関数を実装している点です。
一度のクリックで複数のNFTをミントすることを可能にするために、NFTをミントする処理をループ文の中で行うようにしています。

以前他のNFTのミントサイトで一度にNFTを5枚ミントしたことがあったのですが、どうやって実装しているんだろうと疑問に思いましたが、こうやって手を動かして実装してみて初めて理解することができました!

NFTCollectible.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "hardhat/console.sol";

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract NFTCollectible is ERC721Enumerable, Ownable {

    using SafeMath for uint256;
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIds;
    uint public constant MAX_SUPPLY = 30;
    uint public constant PRICE = 0.0001 ether;
    uint public constant MAX_PER_MINT = 3;

    string public baseTokenURI;

    constructor(string memory baseURI) ERC721("My NFT Collectible", "NFTC") {
        setBaseURI(baseURI);
    }

    // mint 10 NFTs fro free sell
    function reserveNFTs() public onlyOwner {
        uint totalMinted = _tokenIds.current();
        require(totalMinted.add(10) < MAX_SUPPLY, "Not enough NFTs");
        // free mint
        for (uint i = 0; i < 10; i++) {
            _mintSingleNFT();
        }
    }

    // getter func for baseURI
    function _baseURI() internal view virtual override returns (string memory) {
        return baseTokenURI;
    }

    // setter func for baseURI
    function setBaseURI(string memory _baseTokenURI) public onlyOwner {
        baseTokenURI = _baseTokenURI;
    }

    /**
     * mint NFT func 
     * @param _count count of NFT
     */
    function mintNFTs(uint _count) public payable {
        // get token IDs
        uint totalMinted = _tokenIds.current();
        // check fro mint NFT
        require(totalMinted.add(_count) <= MAX_SUPPLY, "Not enough NFTs!");
        require(_count > 0 && _count <= MAX_PER_MINT, "Cannot mint specified number of NFTs.");
        require(msg.value >= PRICE.mul(_count), "Not enough ether to purchase NFTs.");

        for (uint i = 0; i < _count; i++) {
            _mintSingleNFT();
        }
    }

    function _mintSingleNFT() private {
        uint newTokenID = _tokenIds.current();
        // call _safeMint func
        _safeMint(msg.sender, newTokenID);
        _tokenIds.increment();
    }

    // getter owner's all tokensId
    function tokensOfOwner(address _owner) external view returns (uint[] memory) {

        uint tokenCount = balanceOf(_owner);
        uint[] memory tokensId = new uint256[](tokenCount);

        for (uint i = 0; i < tokenCount; i++) {
            tokensId[i] = tokenOfOwnerByIndex(_owner, i);
        }

        return tokensId;
    }

    function withdraw() public payable onlyOwner {
        uint balance = address(this).balance;
        require(balance > 0, "No ether left to withdraw");
        // send ETH to msg.sender
        (bool success, ) = (msg.sender).call{value: balance}("");
        require(success, "Transfer failed.");
    }
}

2.hardhatの設定ファイル

こちらもhardhatのテンプレプロジェクトで生成されたファイルをベースに色々設定を追加しただけになりますが、マルチチェーン対応を目指している方はぜひ参考にしてみてください。

このように設定すると、Polygonのテストネットだけでなく、バイナンスチェーンやAvalancheのテストネット、Astar Networkにもコントラクトをデプロイすることができますよ!!

hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require('solidity-coverage')
require('dotenv').config();

const {
  API_URL_KEY, 
  API_RINKEBY_KEY, 
  PRIVATE_KEY, 
  ETHERSCAN_APIKEY, 
  POLYGONSCAN_APIKEY,
  POLYGON_URL,
  ASTAR_URL 
} = process.env;

const GWEI = 1000 * 1000;

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
  /*
  etherscan: {
    apiKey: ETHERSCAN_APIKEY
  },
  */
  paths: {                         
    artifacts: './../client/src/contracts',  
  },
  etherscan: {
    apiKey: POLYGONSCAN_APIKEY
  },
  networks: {
    goerli: {
      url: API_URL_KEY,
      accounts: [PRIVATE_KEY],
    },
    rinkeby: {
      url: API_RINKEBY_KEY,
      accounts: [PRIVATE_KEY],
    },
    mumbai: {
      url: POLYGON_URL,
      accounts: [PRIVATE_KEY],
    },
    fuji: {
      url: 'https://api.avax-test.network/ext/bc/C/rpc',
      gasPrice: 225000000000,
      chainId: 43113,
      accounts: [PRIVATE_KEY]
    },
    bsctest: {
      url: "https://data-seed-prebsc-1-s1.binance.org:8545",
      chainId: 97,
      gasPrice: 20000000000,
      accounts: [PRIVATE_KEY]
    },
    shibuya: {
      url:"https://shibuya.public.blastapi.io",
      chainId:81,
      accounts:[PRIVATE_KEY],
    },
    shiden: {
      url:"https://shiden.api.onfinality.io/public",
      chainId:336,
      accounts:[PRIVATE_KEY],
    },
    astar: {
      url: "https://evm.astar.network",
      chainId: 592,
      accounts:[PRIVATE_KEY],
    }
  },
};

3.フロント側のApp.jsファイル

マルチチェーン対応するために、各チェーンごとのコントラクトアドレスとそれぞれのBlockchain ExplorerのURLを定義してあります。

基本的にはNFTをミントするだけのコンポーネントになりますが、コントラクトのアドレスの表示などはチェーンによって切り替える必要があるので少し長いコードになっています。本当はもっと細かくコンポーネント化できてスッキリできるはずなのですが、今回はプロトタイプということでこのようなコードとなっています。

App.js
import './css/App.css';
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle';
import IconButton from '@mui/material/IconButton';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import { styled } from "@mui/material/styles";
import Paper from "@mui/material/Paper";
import Contract from "./contracts/contracts/NFTCollectible.sol/NFTCollectible.json";
import Footer from './Components/Footer';
import { ethers } from "ethers";
import ClipLoader from "react-spinners/ClipLoader";
import { css } from "@emotion/react";
import SquirrelsSvg from "./assets/rinkeby_squirrels.gif";
import View from './Components/View';

// コントラクトのアドレスとABIを設定
const CONTRACT_ADDRESS = [
  "0xfe03B6a6B4B095248F06Ed9528e913995ED58f97",
  "0xAa363921A48Eac63F802C57658CdEde768B3DAe1",
  "0xAa363921A48Eac63F802C57658CdEde768B3DAe1",
  "0x8DF7e6234f76e8fAC829feF83E7520635359094C",
  "0x67ADc29278d87D87b212C59fDffd2749fe7418c4",
  "0x599c542e6FF0e009D929091e948d2BA510136741"
];
const ABI = Contract.abi;
const MAX_SUPPLY = 30;
const POLYGONSCAN_LINK = `https://mumbai.polygonscan.com/address/${CONTRACT_ADDRESS[0]}`;
const BLOCKSCOUT_LINK = `https://blockscout.com/shibuya/address/${CONTRACT_ADDRESS[1]}/transactions`;
const BLOCKSCOUT_LINK2 = `https://blockscout.com/shiden/address/${CONTRACT_ADDRESS[2]}/transactions`;
const BLOCKSCOUT_LINK3 = `https://blockscout.com/astar/address/${CONTRACT_ADDRESS[5]}`;
const SNOWTRACE_LINK = `https://testnet.snowtrace.io/address/${CONTRACT_ADDRESS[3]}`;
const BSCSCAN_LINK = `https://testnet.bscscan.com/address/${CONTRACT_ADDRESS[4]}`;
const OPENSEA_LINK = "https://testnets.opensea.io/account";

// スピナー用の変数
const override = css`
  display: block;
  margin: 0 auto;
`;

/**
 * StyledPaperコンポーネント
 */
const StyledPaper = styled(Paper)(({ theme }) => ({
  padding: theme.spacing(2),
  maxWidth: 600,
  height: 400,
  backgroundColor: '#fde9e8',
}));

/**
 * Appコンポーネント
 */
function App() {
  // ステート変数
  const [supply, setSupply] = useState(0);
  const [currentAccount, setCurrentAccount] = useState(null);
  const [mintingFlg, setMintingFlg] = useState(false);
  const [networkId, setNetworkId] = useState(null);
  const [contractAddr, setContractAddr] = useState(null);
  const [count, setCount] = useState(1);
  const [viewFlg, setViewFlg] = useState(false);
  const [baseURI, setBaseURI] = useState(null);

  /**
   * ウォレットの接続状態を確認するメソッド
   */
  const checkWalletIsConnected = async() => {
    const { ethereum } = window;

    if (!ethereum) {
      console.log("Make sure you have MetaMask installed!");
      return;
    } else {
       // 接続しているチェーンが Rinkebyであることを確認する。
       let chainId = await ethereum.request({ method: "eth_chainId" });
       if (chainId === "0x13881" || "0x51" || "0x150") {
        setNetworkId(chainId);
        // ネットワークによってセットするコントラクトのアドレスを変更する。
        if (chainId === "0x13881") { // Munbai network
          setContractAddr(CONTRACT_ADDRESS[0]);
        } else if (chainId === "0x51") { // Shibuya network
          setContractAddr(CONTRACT_ADDRESS[1]);
        } else if (chainId === "0x150") { // Shiden network
          setContractAddr(CONTRACT_ADDRESS[2])
        } else if (chainId === "0xa869") { // fuji network
          setContractAddr(CONTRACT_ADDRESS[3]);
        } else if (chainId === "0x61") { // bsc testnet network
          setContractAddr(CONTRACT_ADDRESS[4]);
        } else if (chainId === "0x250") { // astar network
          setContractAddr(CONTRACT_ADDRESS[5]);
        }

        // アカウント情報を要求する
        const accounts = await ethereum.request({ method: "eth_accounts" });

        if (accounts.length !== 0) {
          const account = accounts[0];
          console.log("Found an authorized account: ", account);
          setCurrentAccount(account);
          // 発行数を取得する。
          let totalSupply = await getTotalSupply();
          setSupply(totalSupply);
        } else {
          console.log("No authorized account found");
        }
       } else {
        alert("You are not connected to the Polygon Test Network!");
       } 
    }
  };

  /**
   * ウォレットを接続するためのイベントハンドラー
   */
  const connectWalletHandler = async() => {
    const { ethereum } = window;

    if (!ethereum) {
      alert("Please install MetaMask!");
    } else {
      // 接続しているチェーンが Rinkebyであることを確認する。
      let chainId = await ethereum.request({ method: "eth_chainId" });
      console.log("chain id", chainId);
      if (chainId === "0x13881" || "0x51" || "0x150") {
        setNetworkId(chainId);
        // ネットワークによってセットするコントラクトのアドレスを変更する。
        if (chainId === "0x13881") { // Munbai network
          setContractAddr(CONTRACT_ADDRESS[0]);
        } else if (chainId === "0x51") { // Shibuya network
          setContractAddr(CONTRACT_ADDRESS[1]);
        } else if (chainId === "0x150") { // Shiden network
          setContractAddr(CONTRACT_ADDRESS[2])
        } else if (chainId === "0xa869") { // fuji network
          setContractAddr(CONTRACT_ADDRESS[3]);
        } else if (chainId === "0x61") { // bsc testnet network
          setContractAddr(CONTRACT_ADDRESS[4]);
        } else if (chainId === "0x250") { // astar network
          setContractAddr(CONTRACT_ADDRESS[5]);
        }

        try {
          const accounts = await ethereum.request({ method: "eth_requestAccounts" });
          console.log("Found an account! Address: ", accounts[0]);
          // アカウント情報をステート変数にセットする。
          setCurrentAccount(accounts[0]);
          // 発行数を取得する。
          let totalSupply = await getTotalSupply();
          setSupply(totalSupply);
        } catch (err) {
          console.log(err);
        }
      } else {
        alert("You are not connected to the Polygon Test Network!");
      }
    }
  };

  /**
   * NFTの発行数を取得するメソッド
   */
  const getTotalSupply = async() => {
    try {
      const { ethereum } = window;
  
      if (ethereum) {
        // コントラクトにアクセスするための準備
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const nftContract = new ethers.Contract(
          contractAddr, 
          ABI, 
          signer
        );
  
        // 発行数を取得する。
        let totalSupply = await nftContract.totalSupply();
        // get baseURI
        let baseUri = await nftContract.baseTokenURI();
        setBaseURI(baseUri);

        return totalSupply.toNumber();
      }
    } catch (err) {
      console.log(err);
      return 0;
    }
  };

  /**
   * NFTを実際にMintするためのメソッド
   */
  const mintNftHandler = async() => {
    try {
      const { ethereum } = window;
  
      if (ethereum) {
        // コントラクトにアクセスするための準備
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const nftContract = new ethers.Contract(
          contractAddr, 
          ABI, 
          signer
        );
  
        console.log("Initialize payment");
        // value 
        var value = (0.01 * count).toString();
        // NFTを一つMintする。
        let nftTxn = await nftContract.mintNFTs(count, {
          value: ethers.utils.parseEther(value),
          gasLimit: 500_000,
        });
  
        setCount(1);
        setMintingFlg(true);
        await nftTxn.wait();
  
        console.log(`Mined, see transaction: ${nftTxn.hash}`);
        setMintingFlg(false);
        alert("Mint Success!!")
      } else {
        console.log("Ethereum object does not exist");
        alert("Mint failed...");
      }
    } catch (err) {
      console.log(err);
    }
  };

  /**
   * Connect Walletボタンコンポーネント
   */
  const connectWalletButton = () => {
    return (
      <button
        onClick={connectWalletHandler}
        className="cta-button connect-wallet-button"
      >
        Connect Wallet
      </button>
    );
  };

  /**
   * NFTMintボタンコンポーネント
   */
  const mintNftButton = () => {
    return (
      <>
      <Box sx={{ flexGrow: 1, overflow: "hidden", mt: 1, my: 1}}>
        <Stack 
          direction="row" 
          justifyContent="center" 
          alignItems="center" 
          spacing={1}
        >
          <IconButton 
            aria-label="remove"
            size='large' 
            onClick={() => setCount(count - 1)}
          >
            <RemoveCircleIcon/>
          </IconButton>
          <TextField
            id="outlined-number"
            type="number"
            InputLabelProps={{
              shrink: true,
            }}
            value={count}
          />
          <IconButton 
            aria-label="add" 
            size='large' 
            onClick={() => setCount(count + 1)}
          >
            <AddCircleIcon/>
          </IconButton>
        </Stack>
        </Box>
        <button 
          onClick={mintNftHandler} 
          className="cta-button mint-nft-button"
        >
          Mint NFT
        </button>
      </>
    );
  };

  useEffect(() => {
    checkWalletIsConnected();
  }, [contractAddr]);

  useEffect(() => {
    checkWalletIsConnected();
  }, [networkId]);

  return (
    <div className="main-app">
      { viewFlg ? (
        <>
          <h1>NFT View</h1>
          <Box sx={{ flexGrow: 1, mt: 2, my: 1}}>
            <Grid
                container
                justifyContent="center"
                alignItems="center"
              >
                <View 
                  address={contractAddr} 
                  networkId={networkId}
                  baseURI={baseURI}
                />
            </Grid>
          </Box>
          <button 
            className="opensea-button cta-button"
            onClick={() => { setViewFlg(false) }}
          >
            NFTを発行する
          </button>
          <Footer/>
        </>
      ) : (
        <>
          <h1>Let's Mint Generative NFT !!</h1>
            <Box sx={{ flexGrow: 1, overflow: "hidden", mt: 4, my: 2}}>
              <strong>
                contract address : 
                {(networkId === "0x13881") && (
                  <a href={POLYGONSCAN_LINK}>
                    {contractAddr}
                  </a>
                )} 
                {(networkId === "0x51") && (
                  <a href={BLOCKSCOUT_LINK}>
                    {contractAddr}
                  </a>
                )} 
                {(networkId === "0x150") && (
                  <a href={BLOCKSCOUT_LINK2}>
                    {contractAddr}
                  </a>
                )} 
                {(networkId === "0xa869") && (
                  <a href={SNOWTRACE_LINK}>
                    {contractAddr}
                  </a>
                )} 
                {(networkId === "0x61") && (
                  <a href={BSCSCAN_LINK}>
                    {contractAddr}
                  </a>
                )} 
                {(networkId === "0x250") && (
                  <a href={BLOCKSCOUT_LINK3}>
                    {contractAddr}
                  </a>
                )} 
              </strong>
            </Box>
            <Grid
              container
              direction="row"
              justifyContent="center"
              alignItems="center"
            >
              <Box sx={{ flexGrow: 1, overflow: "hidden", px: 3, mt: 10}}>
                <StyledPaper sx={{my: 1, mx: "auto", p: 0, paddingTop: 2, borderRadius: 4}}>  
                  <Box sx={{ flexGrow: 1, overflow: "hidden", mt: 1, my: 1}}>
                    <strong>発行状況:{supply} / {MAX_SUPPLY}</strong>
                  </Box>
                  <img src={SquirrelsSvg} alt="Polygon Squirrels" height="40%" /><br/>
                  { mintingFlg ?
                      (
                        <div>
                          <ClipLoader color="#99FF99" loading={mintingFlg} css={override} size={35} /><br/>
                          <div className="spin-color">
                            Now Minting ...
                          </div>
                        </div>
                      ) :( 
                      <>
                        { currentAccount ? mintNftButton() : connectWalletButton()}
                      </>
                      )
                  }
                </StyledPaper>
                <button 
                  className="opensea-button cta-button"
                  onClick={() => { setViewFlg(true) }}
                >
                  NFTを確認する
                </button>
                <Footer/>
              </Box>
            </Grid>
        </>
      )}
    </div>
  );
}

export default App;

4.NFTを表示するView.jsコンポーネントファイル

さて、今回一番苦しんだ実装部分になります。

NFTのデータを取得してきて画面に表示するってものすごく難しいことが今回の実装で良く分かりました笑。
以前あるブロックチェーンの勉強会でNFTのデータを取得するにはGraphQL使ったりして色々工夫しなければならないと聞いたことがあり、その時はよく分からなかったのですが実際に作ろうとするとその難しさが分かりました。

試行錯誤したのですが、自作でNFTを引っ張ってくるAPIを実装するにはまだ知識不足と判断し、MoralisのAPIを利用することにしました。

OpenseaTofuNFTの凄さが分かります。
どんな実装しているのか教えてもらいたいです笑。

なんとかNFTを表示することはできたものの、全て表示できるわけではなく挙動は不安定な状態ですがここまでできたので、第1段階はクリアとしました。

View.jsの中身は以下の通りです。

View.js
import './../css/App.css';
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import NFT from './NFT';
import superAgent from 'superagent';

/**
 * View component
 * @param contract address
 */
function View(props) {
    // get info from props    
    const { address, networkId, baseURI } = props;
    // state variable
    const [nfts, setNfts] = useState([]);

    // hook
    useEffect(() => { 
        /**
         * init function
         */
        const init = async() => {
            await superAgent
                .get('https://deep-index.moralis.io/api/v2/nft/' + address)
                .query({
                    chain: `${networkId}`, 
                    format: 'decimal'
                })
                .set({ 
                    Accept: 'application/json',
                    'x-api-key': `${process.env.REACT_APP_MORALIS_API_KEY}`, 
                })
                .end((err, res) => {
                    if (err) {
                            console.log("NFTのデータ取得中にエラー発生", err)
                            return err;
                    }
                    console.log("データ取得成功!:", res.body);
                    setNfts(res.body.result);
                });
            }
        init();
    }, []);

    const viewNFT = (nft) => {
        //get metadata
        var metadata = JSON.parse(nft.metadata);
        
        // image URL
        var imageURL;

        if(metadata) {
            var result = metadata.image.substr(7);
            imageURL = "https://gateway.pinata.cloud/ipfs/" + result;
        }

        return (
            <div>
                { metadata ? (
                    <NFT
                        name={metadata.name}
                        description={metadata.description}
                        imageURL={imageURL}
                    />
                ) : <></> }
            </div>
        );
    }

    return (
        <Box sx={{ flexGrow: 1 }}>
            <Grid
                container
                spacing={{ xs: 2, md: 3 }}
                columns={{ xs: 4, sm: 4, md: 12 }}
            >
                {nfts.map((nft, i) => (
                    viewNFT(nft)
                ))}
            </Grid>
        </Box>
    );
}

export default View;

課題とまとめ

ここまでが実装の記録の共有となります。

残課題としては、さらに対応するブロックチェーンが増えていくことを想定した作りにすることが1点。そして、NFTの表示機能の不安定さを軽減するということの2点となります。

1点目の方はコンポーネントにうまく分解すれば良いのですが、2点目の方はかなり苦戦しそうです笑。
東京Web3ハッカソンに出場する予定のためまずはそちらもプロダクトが優先となるので記事の更新もしばらく止まりそうですが、終わったらまたまとめていきたいと思います。

東京Web3ハッカソンについて気になる方は下のサイトを見てみてください!
イベントの登壇者や勉強会などのコンテンツがものすごく豪華です!!!

ここまで読んでいただきありがとうございました!!
あなたも一緒にWeb3エンジニアになりましょう!

参考文献

今回のDApp実装にあたり、参考にしたドキュメントになります。

  1. わたしの Solidity 開発で最初にやっておくこと with hardhat
  2. Astar Docs
  3. ipfs公式ドキュメント
  4. Moralis docs
  5. Moralis Swagger
  6. Moralis SDK
  7. Web3UIkit
  8. Web3UIkit CodeSandBox
  9. alchemy nftMinter Docs
  10. solidity-coverage
  11. Ethereum Smart Contract Security Best Practices
  12. Security Tools
  13. Solgraph
6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?