【Solidity/NFT】ERC721URIStorageを使わないERC721実装
はじめに
NFTコントラクト実装の入門記事では、OpenZeppelinのERC721URIStorage._setTokenURI
を使っていることが多く、恥ずかしながらこれしか手段知らなかった😇
BAYCのコントラクトを読んでいると_setTokenURI
を使ってなくて、IPFSへの配置方法を工夫すればERC721._safeMint
だけでもいけることに気づいたのでそのメモ
要約
ありがちな入門コード
ERC721URIStorage
を使うことが悪いとかではない
普通にこれ使うことも多いだろうし、むしろ入門ではこっちの方が手軽に試せる
//Contract based on [https://docs.openzeppelin.com/contracts/3.x/erc721](https://docs.openzeppelin.com/contracts/3.x/erc721)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract MyNFT is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721("MyNFT", "NFT") {}
function mintNFT(address recipient, string memory tokenURI)
public onlyOwner
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
引用; https://ethereum.org/en/developers/tutorials/how-to-write-and-deploy-an-nft/
OpenZeppelin/ERC721だけで済ませる方法
IPFSで一手間ありますが、以下を満たすのであればこっちの方がシンプル
- IPFSに配置するmetadataの数・内容に変更がない
理由は、IPFSに一度アップロードすると基本的に後から変更出来ないためです
(IPFSのCIDを上書きすれば可能だけど)
IPFS
必要な手順は以下の通り
- metadataファイルを全て同じフォルダに格納
- ファイル名は1, 2, 3, ...とする
- フォルダ毎IPFSへアップロード
このファイル名がtoken idと紐付きます
token idが1のNFTは名前1
のファイルを参照することになります
参考; BAYCのIPFS; https://ipfs.io/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
コントラクト
こんな感じ。以下ポイント
-
_baseURI()
の返り値をベタがきしているが、実際にはコンストラクタに入れたり、setterを良いした方が良さそう - 上記コードとは異なり、
mintNFT()
の引数にtokenURI
を渡す必要はない
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract myNFT is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721("myNFT", "MN") {}
function _baseURI() internal view virtual override returns (string memory) {
// TODO 自分でアップしたIPFSのCIDを使う
return "ipfs://QmR9gMXXoC3RRnZVWGgJcHkRLjq2UqEkA7ifQqiMc7QGPN/";
}
function mintNFT(address recipient) external returns (uint256) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(recipient, newTokenId);
return newTokenId;
}
}
解説
OpenZeppelinのERC721.sol
/**
* @dev See {IERC721Metadata-tokenURI}.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
_requireMinted(tokenId);
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
/**
* @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
* token will be the concatenation of the `baseURI` and the `tokenId`. Empty
* by default, can be overridden in child contracts.
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
上記コードより、_baseURI()
を子コントラクトでoverrideしておけば、tokenURI()
の返り値が_baseURI + tokenId
となることがわかります
簡単な話ですね。。
なので、例えば、
-
_baseURI()
の返り値が"ipfs://QmR9gMXXoC3RRnZVWGgJcHkRLjq2UqEkA7ifQqiMc7QGPN/"
-
tokenId
が1
の時は、tokenURIの返り値は"ipfs://QmR9gMXXoC3RRnZVWGgJcHkRLjq2UqEkA7ifQqiMc7QGPN/1
となり、名前1
のファイルが参照されます
サンプル
- コントラクト
- OpenSea
- IPFS
- metadata
ちなみに、例えばmetadataを2つだけ配置しておくと、3つ目以降の画像は当然表示されない
さいごに
ユースケース限定的かも知れませんが、誰かの参考になればと!
Twitterの方でも、モダンな技術習得やサービス開発の様子を発信したりしているので良かったらチェックしてみてください!
また、個人開発したdAppsの解説記事もありますので、良かったらそちらもご覧ください!
Discussion