こんにちはサイオステクノロジーの佐々木です。
今までいくつかNFTに関する記事を上げてきたのですが、一般的なNFTを見ているとこんなことを思いませんか?
「NFTの画像はストレージに保存されているからそれを削除したり変更したらNFTの画像消せるし書き換えもできるのでは?」
実際にその通りです。
では画像もブロックチェーン上に置けばよいのでしょうか? これが理想の状態ではありますが、ブロックチェーンには容量の大きいデータは置くことができません。
ではどうするか。
そうだSVGを使おう。
SVGとは
SVGとは画像フォーマットの一種でベクタ形式のデータです。 XMLに準拠しておりテキストエディタで編集することも可能です。
JPGやPNGはラスタ形式でpixelで画像を表現しています。
SVGではJPGやPNGよりも軽量でcssやjavascriptを使用して動きをつけることも可能です。 この特性によりSVGファイルをスマートコントラクトにデプロイすることでmetadata, 画像データともにブロックチェーン上に保存し、NFT作成に必要なすべての情報をブロックチェーン上で保存することができます。
ただしすべてのSVGが保存できるわけではなく、あくまでも容量には注意しないといけません。 ここの使い分けはcase by caseで使い分けていく必要があります。
フルオンチェーンNFTの仕組み
ではフルオンチェーンのNFTはどのように実装するのか解説します。
とても簡単にフローを書くとこのような形になります。(かなり簡略化して書いているので厳密ではありません)
通常のNFTとの違いを解説します
- 画像データ(SVG)をbase64に変換しその値をmetadataのimageタグにセットする点
- metadataもブロックチェーン上で生成する必要がある点
- 生成したmetadataもbase64にエンコードしマーケット側に返す点
- マーケット側が通常metadataのURIにアクセスして中身を確認するところをdecodeする処理をする点
実際のソースコード
mint.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./svg.sol";
contract NFT is ERC721 {
address immutable tokenDescriptor;
constructor(address _tokenDescriptor) ERC721("ERC721 Mock Token", "EMT") {
tokenDescriptor = _tokenDescriptor;
_mint(msg.sender, 1);
}
function mint(address to, uint256 tokenId) external {
_mint(to, tokenId);
}
function tokenURI(uint256 tokenId) public view override returns(string memory) {
require(_exists(tokenId));
return FullOnChainNFTDescriptor(tokenDescriptor).tokenURI(tokenId);
}
}
svg.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/utils/Strings.sol";
import 'base64-sol/base64.sol';
contract FullOnChainNFTDescriptor {
function tokenURI(uint256 tokenId)
external
pure
returns (string memory)
{
string memory image = Base64.encode(bytes(svgGenerator(tokenId)));
return
string(
abi.encodePacked(
'data:application/json;base64,',
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"Full on Chain NFT"',
', "description":"This is Full on Chain NFT", "image": "',
'data:image/svg+xml;base64,',
image,
'"}'
)
)
)
)
);
}
function svgGenerator(uint256 tokenId)
internal
pure
returns (string memory)
{
return
string(
abi.encodePacked(
'<svg xmlns="<http://www.w3.org/2000/svg>" viewBox="0 0 480 300">',
'<text x="0" y="160" class="text1" filter="url(#shadow)">',
'Full on Chain NFT',
'<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="-10 240 150" to="10 240 150" dur="10s" repeatCount="indefinite"/>',
'</text>',
'<text x="20" y="290" class="text3" fill="#ddd">',
'#',
Strings.toString(tokenId),
'</text>',
'</svg>'
)
);
}
}
こちらをRemixでデプロイする場合にはsvg.solからデプロイし、その後mint.solをデプロイする際にsvg.solのコントラクトアドレスをセットしてデプロイしてください。
ソースコードはsvgGeneratorでtokenIdをSVGにセットしてエンコードした結果を返し、tokenURIはsvgGeneratorの返り値をimageにセットしてmetadataをエンコードしています。
実際にRemixでデプロイし、mintをしてtokenURIでどのような値が返ってくるか確認すると以下のような結果になります。
通常tokenURIを実行するとmetadataが保存されているIPFSのURIやどこかのクラウドストレージのURIが返ってきますが、ここではjsonをbase64でエンコードした結果が返ってきます。
では実際にこれをdecodeしてみましょう。
{
"name": "Full on Chain NFT",
"description": "This is Full on Chain NFT",
"image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODAgMzAwIj48dGV4dCB4PSIwIiB5PSIxNjAiIGNsYXNzPSJ0ZXh0MSIgZmlsdGVyPSJ1cmwoI3NoYWRvdykiPk5vbmZ1bmdpbmJsZVRva2VuPGFuaW1hdGVUcmFuc2Zvcm0gYXR0cmlidXRlTmFtZT0idHJhbnNmb3JtIiBhdHRyaWJ1dGVUeXBlPSJYTUwiIHR5cGU9InJvdGF0ZSIgZnJvbT0iLTEwIDI0MCAxNTAiIHRvPSIxMCAyNDAgMTUwIiBkdXI9IjEwcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiLz48L3RleHQ+PHRleHQgeD0iMjAiIHk9IjI5MCIgY2xhc3M9InRleHQzIiBmaWxsPSIjZGRkIj4jMjwvdGV4dD48L3N2Zz4="
}
jsonデータが返ってきていることが確認できますね。
imageタグにも通常画像の保存場所が書かれているのですが、ここではbase64に変換されたstringが入っています。 SVGはこのままhtmlのimgタグのsrc属性などで指定してあげれば画像を表示することができます。
実際に上記のコードをテストネットにデプロイしOpenSeaで確認してみた結果がこちらです。 きちんと画像が表示され説明欄にもmetadataのdescriptionが記載されていることが確認できました!
逆にもしこれからNFTを表示するようなサービスを作ろうと考えた場合にはこのようなパターンを考慮してサービスを作らないといけないですね!
終わりに
今回はフルオンチェーンでNFTを実装する方法について解説しました。
私の知る限り現時点では規格化されていないのでこのやり方がデファクトスタンダートになるのかは不明ですが、OpenSeaが対応しているという点で一つ指標になるのではないかと思います。
よく紹介されているNFTを簡単に作ってみる記事はこちら
アイキャッチ画像 著作者:Freepik