導入
皆さん、こんにちは。
前回の投稿から少し時間が空いてしまいましたが、色々とブロックチェーン技術をインプットしていましてようやく落ち着くことができたので、一旦アウトプットとしてDapp開発の記録をまとめたいと思います。
今回は、React.jsとSolidityを利用してNFTのミントサイトとそれを表示する機能を持つDAppを実装してみましたので、色々とまとめていきたいと思います。
それでは今回の記事の目次になります。
目次
- 今回実装したアプリのイメージとリポジトリ
- 実装にあたり利用したフレームワークやAPIなど
- ポイントとなったソースコードの紹介
- 課題とまとめ
- 参考文献
今回実装したアプリのイメージとリポジトリ
今回実装したDAppですが、Web3エンジニアコミュニティUNCHAINの学習コンテンツの一つであるGenerativeNFTで実装したDAppをベースに機能強化したものになります。
少し話は逸れますが、UNCHAINは良質な学習コンテンツを無料で参照することができ、DApp開発の基礎力を身につけることができるのでまだ参加されていない方はぜひ下記からエントリーしてみてください!審査に通過する必要がありますが、専用のdiscordでは同じような悩みを持つエンジニアと勉強会を開催したり、実際にWeb3関連の仕事にトライしてみたりすることができます!日本語系のコンテンツでは私の知る限り、最も洗練されているものだと考えています!
さて、本題に戻ります。今回作成したDAppのソースコートは下記に格納してあります。
vercel上にもアプリをデプロイしていますので完成系をみてみたいという方は、したのリンクからアクセスしてみてください!
mumbai ネットワークで接続してみてください。
アプリを起動させると下記のような画面が表示されます。
この画面では、コントラクトへのリンクの他、NFTをミントすることができます。
一度に最大3枚までミントすることが可能となります。
また、NFTを確認するというボタンをクリックするとこのコントラクトから発行したNFTを一覧表示する画面に遷移します。
サイトの上の方に提示されているコントラクトアドレスはリンクになっていて、クリックするとPolygonScanに飛ぶことができます。
ちなみに今回デプロイしたNFTコントラクトは下記から確認することができます。
NFTを一覧表示するView画面はこんな感じです。
メタデータである画像がうまく表示されていない件については4.の課題とまとめの項目で触れたいと思います。正直ここが一番難しかったです。
対応できているチェーンもMoralisのAPIの仕様に合わせる形になるのでMumbai以外は挙動が不安定になっています。
実装にあたり利用したフレームワークや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枚ミントしたことがあったのですが、どうやって実装しているんだろうと疑問に思いましたが、こうやって手を動かして実装してみて初めて理解することができました!
//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にもコントラクトをデプロイすることができますよ!!
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をミントするだけのコンポーネントになりますが、コントラクトのアドレスの表示などはチェーンによって切り替える必要があるので少し長いコードになっています。本当はもっと細かくコンポーネント化できてスッキリできるはずなのですが、今回はプロトタイプということでこのようなコードとなっています。
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を利用することにしました。
OpenseaやTofuNFTの凄さが分かります。
どんな実装しているのか教えてもらいたいです笑。
なんとかNFTを表示することはできたものの、全て表示できるわけではなく挙動は不安定な状態ですがここまでできたので、第1段階はクリアとしました。
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実装にあたり、参考にしたドキュメントになります。