こんにちは、フルオンチェーンNFTクリエイターのnawooです。
今回は、フルオンチェーンNFTを作成するときに役立つライブラリ「scripty.sol」について解説します。
前回の記事で、the metroというインタラクティブなフルオンチェーンNFTコレクションについて解説しましたが、そこで用いられているgenerativeアート作成のためのオンチェーンツールセットが、今回ご紹介する「scripty.sol」です。
scripty.solというコントラクトは、自分で作ったJavaScriptコードや、「Three.js」「p5.js」といったJavaScriptを、自由に組み合わせることができます。そして、それを使ってオンチェーンでHTMLを作成することができるのです。
ということで今回は、フルオンチェーンNFTを作成するときに役立つライブラリ「scripty.sol」について、概要やその使い方などを中心に解説します。
でははじめに、この記事の構成について説明します。
まずは、scripty.solの基本的な概要について、簡潔に解説します。
続いて、どのような流れでscripty.solを使用すれば良いのかについて、ステップバイステップで解説します。
最後に、実際にscripty.solを使いながらp5.jsを使ったフルオンチェーンNFTを作成しつつ、全体的な流れや完成物などについて概観します。
本記事が、scripty.solの概要や使い方、p5.jsを使ったフルオンチェーンNFTの構築方法などについて理解したいと思われている方にとって、少しでもお役に立てれば幸いです。
※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。
イーサリアムnaviの活動をサポートしたい方は、「定期購読プラン」をご利用ください。
It looks like someone created a whole tutorial on how to use scripty.sol, in Japanese.
— xtremetom (@xtremetom) April 16, 2023
Thanks @NowAndNawoo https://t.co/DAx7fJkVmG
scripty.solとは
scripty.solは、「Three.js」「p5.js」などのJavaScriptや、自作のJavaScriptコードを自由に組み合わせて、オンチェーンでHTMLを作成することができるコントラクトです。
Three.jsやp5.jsを使ったフルオンチェーンNFTを作成するためには、多くのJavaScriptを組み合わせてHTMLを作成する必要があります。
- Base64を扱うためのJavaScriptライブラリ
- gzipを展開するためのJavaScriptライブラリ
- gzip圧縮されたThree.jsやp5.js本体
- tokenIDを定義するJavaScriptコード
- メインのJavaScriptコード
これらのJavaScriptをHTMLにまとめて、Base64エンコードまたはURLエンコードしてからdataURLに変換し、メタデータのanimation_url
に指定する必要があります。
HTMLに含めたいJavaScriptをrequests
として指定するだけで、簡単にdataURL化したHTMLを作成できるのです。
scripty.solの作者は、0xdude氏とxtremetom氏です。
先日、xtremetom氏がローンチしたCryptoCoasterというNFTコレクションや、前回の記事で取り上げたint art(0xdude氏)のフルオンチェーン×インタラクティブNFT「the metro」でも、scripty.solが使用されています。
scripty.solでは、様々なタイプのHTMLを作成することができます。
次章では、p5.jsを使ったフルオンチェーンNFTを作成する方法を中心に解説していきます。
scripty.solの使い方
まずは、公式サイトでもp5.jsを使ったシンプルなNFTの例がありますので、概観していきます。
p5.jsを使ったexampleには、以下の2種類があります。今回は、後者②のEthFS_P5_URLSafe.sol
を使います。
- Base64エンコードを使った
EthFS_P5.sol
- URLエンコードを使った
EthFS_P5_URLSafe.sol
では最初に、tokenURIを確認してみます。
メタデータはURLエンコードされているのでデコードしたところ、以下のようになっていました。
{
"name":"p5.js Example - GZIP - Base64 - URL Safe",
"description":"Assembles GZIP compressed base64 encoded p5.js that's stored in ethfs's FileStore...",
"animation_url":"data:text/html,%3Cbody%20style%3D%27margin%3A0%3B%27%3E%3Cscript%20src%3..."
}
また、animation_urlはURLエンコードされているのでこちらもデコードしてみると、<body>
タグの中に4つの<script>
が含まれていることがわかります。
<body style="margin: 0">
<script src="data:text/javascript;base64,bGV0IF9zYj17fTtfc2IuZXZlbnRzPVtd..."></script>
<script type="text/javascript+gzip" src="data:text/javascript;base64,H4sIAAAAAAAAE8S923bbWLY..."></script>
<script src="data:text/javascript;base64,InVzZSBzdHJpY3QiOygoKT0..."></script>
<script src="data:text/javascript;base64,ZnVuY3Rpb24gc2V0dXAoKSB7CglsZXQgZCA9IDc..."></script>
</body>
このメタデータをどのように作成しているのか、コントラクトのtokenURI関数を確認してみましょう。
tokenURI関数では、以下3つの処理が行われています。
requests
を作成bufferSize
を取得animation_url
用のdataURLとメタデータを作成
1. requestsの作成
まず、HTMLに含めるJavaScriptをrequests
として順番に指定しています。
function tokenURI(uint256) public view virtual override returns (string memory) {
WrappedScriptRequest[] memory requests = new WrappedScriptRequest[](4);
requests[0].name = "scriptyBase";
requests[0].wrapType = 0; // エンコードや圧縮されていないJavaScript (raw)
requests[0].contractAddress = scriptyStorageAddress; // ScriptyStorageから読み込み
requests[1].name = "p5-v1.5.0.min.js.gz";
requests[1].wrapType = 2; // gzip圧縮されたJavaScript
requests[1].contractAddress = ethfsFileStorageAddress; // EthFSから読み込み
requests[2].name = "gunzipScripts-0.0.1.js";
requests[2].wrapType = 1; // Base64エンコードされたJavaScript
requests[2].contractAddress = ethfsFileStorageAddress; // EthFSから読み込み
requests[3].name = "pointsAndLines";
requests[3].wrapType = 0; // エンコードや圧縮されていないJavaScript (raw)
requests[3].contractAddress = scriptyStorageAddress; // ScriptyStorageから読み込み
// ...略
}
requests
では、JavaScriptの読み込み先とタイプを指定します。読み込み先は、主に以下の3パターンあります。
- scriptys.sol用のストレージであるScriptyStorageコントラクトから読み込む
- EthFSのFileStorageコントラクトから読み込む
- コントラクト内でコードを指定する
1と2では、contractAddress
で読み込み先のコントラクトを指定し、name
でファイル名を指定します。
3は、コントラクト内でJavaScriptを動的に作成する場合に使います。
scriptContent
にコードを指定しますが、これに関しては後ほど具体例を紹介します。
次に、JavaScriptのタイプをwrapType
で指定します。これにより、生成される<script>
タグの種類が決まります。
0
: エンコードや圧縮されていないJavaScript (raw) → Base64エンコードしてからdataURL化してsrc
に指定1
: Base64エンコードされたJavaScript → そのままdataURL化してsrc
に指定2
: gzip圧縮されたJavaScript →type="text/javascript+gzip"
を追加3
: PNG圧縮されたJavaScript →type="text/javascript+png"
を追加4
: カスタムタイプ
つまり、上のコードではrequests
に対して、以下4つのJavaScriptを読み込むように指定されていることがわかります。
- ScriptyStorageから、無圧縮の
"scriptyBase"
を読み込む - EthFSから、gzip圧縮された
"p5-v1.5.0.min.js.gz"
を読み込む - EthFSから、Base64エンコードされた
"gunzipScripts-0.0.1.js"
を読み込む - ScriptyStorageから、無圧縮の
"pointsAndLines"
を読み込む
補足: 各JavaScriptの説明
"scriptyBase"
は、他のスクリプトからイベントの追加やcanvasの生成を行うための_sb
オブジェクトを定義しています。(しかし、他のスクリプトから使われていないので、無くても良さそうです。)"p5-v1.5.0.min.js.gz"
は、EthFSにアップロードされているp5.jsの本体です。gzip圧縮されています。"gunzipScripts-0.0.1.js"
は、gzip展開するためのJavaScriptです。"pointsAndLines"
は、このNFT用のp5.jsのスケッチファイルです。setup関数やdraw関数を含みます。
2. bufferSizeの取得
続いて、bufferSize
を求めます。 bufferSizeは、作成されるHTMLのバイトサイズです。
scripty.solでは、最初にバッファサイズを計算して、その分をメモリ上に確保してからHTMLを作成しています。
EthFS_P5_URLSafe.sol
では、bufferSize
をローカルPCで事前に計算しておいて、コンストラクタの引数として渡すようになっています。
ローカルPCでbufferSize
を計算する方法は、こちらのデプロイスクリプトが参考になります。
// コンストラクタ
constructor(
address _ethfsFileStorageAddress,
address _scriptyStorageAddress,
address _scriptyBuilderAddress,
uint256 _bufferSize // ← コンストラクタの引数でbufferSizeを指定している
) ERC721("example", "EXP") {
ethfsFileStorageAddress = _ethfsFileStorageAddress;
scriptyStorageAddress = _scriptyStorageAddress;
scriptyBuilderAddress = _scriptyBuilderAddress;
bufferSize = _bufferSize;
mint();
}
もちろん、ローカルPCではなく、コントラクト内でbufferSize
を計算することもできます。
その場合、ScriptyBuilderコントラクトのgetBufferSizeForURLSafeHTMLWrapped
関数を使い、requests
を引数に渡すだけです。
// bufferSizeの計算
uint256 bufferSize = scriptyBuilder.getBufferSizeForURLSafeHTMLWrapped(requests);
今回は、URLエンコードされたHTMLを生成するので、URLエンコードされた状態のHTMLのサイズを求めます。
3. animation_url用のdataURLとメタデータを作成
ここで、いよいよHTMLの作成です。
ScriptyBuilderコントラクトのgetHTMLWrappedURLSafe
関数にrequests
とbufferSize
を引数に渡すと、 HTMLをdataURL形式で返します。dataURLのデータ部は2回URLエンコードされています。
さらに、animation_url
にHTMLのdataURLを指定して、メタデータ全体を作成します。 メタデータ内の{"name":〜
などの文字列はURLエンコードされています。
最後に、メタデータをdataURL化して戻り値にします。
function tokenURI(uint256) public view virtual override returns (string memory) {
// ...略 (requestsの作成とBufferSizeの計算)
// HTMLを作成 (dataURL)
bytes memory dataURL = scriptyBuilder.getHTMLWrappedURLSafe(requests, bufferSize);
// メタデータを作成してdataURL化
return string(abi.encodePacked(
"data:application/json,",
// {"name":"p5.js Example...", "description":"...","animation_url":"
"%7B%22name%22%3A%22p5.js%20Example...%22%2C%22animation_url%22%3A%22",
dataURL,
// "}
"%22%7D"));
}
tokenURI関数に関しては、これだけです。
Base64エンコードの場合
メタデータをURLエンコードではなくBase64エンコードする場合は、 requests
の作成は同じですが、BufferSizeの計算とHTMLの作成に別の関数を使います。
- BufferSizeの計算 :
getBufferSizeForURLSafeHTMLWrapped
→getBufferSizeForHTMLWrapped
関数に変更
- HTMLの作成 :
getHTMLWrappedURLSafe
→getEncodedHTMLWrapped
関数に変更
メタデータはURLエンコードせずに作成しておいて、最後にBase64でエンコードします。
Base64エンコードの方がtokenURI関数のガス使用量は多くなるので、out of gasエラーになりやすいです。
function tokenURI(uint256) public view virtual override returns (string memory) {
// ...略 (requestsの作成)
// BufferSizeを計算
uint256 bufferSize = scriptyBuilder.getBufferSizeForHTMLWrapped(requests);
// HTMLを作成(Base64エンコードタイプのdataURL化)
bytes memory base64EncodedHTMLDataURI = scriptyBuilder.getEncodedHTMLWrapped(requests, bufferSize);
// メタデータを作成
bytes memory metadata = abi.encodePacked(
'{"name":"...","description":"...","animation_url":"',
base64EncodedHTMLDataURI,
'"}'
);
// メタデータをBase64エンコードしてdataURL化
return string.concat("data:application/json;base64,", Base64.encode(metadata));
}
コントラクト内でJavaScriptを動的に作成する場合
JavaScriptコードを動的に作成したい場合は、どうすればいいでしょうか。
例えばジェネラティブNFTで、tokenIdごとに内容を変化させたい場合は、次のようなコードを追加して、JavaScript側にtokenIdの値を渡す必要があります。
<script>
const tokenId=XXX;
</script>
このコードは、tokenIdごとにtokenId=1
, tokenId=2
, tokenId=3
…と変化するので、EthFSなどから読み込むわけにはいかず、コントラクト内で動的に作成する必要があります。
こういった場合は、requests
の作成時に、scriptContent
を指定します。
requests[2].wrapType = 0;
requests[2].scriptContent = abi.encodePacked("const tokenId=", tokenId.toString(), ";");
このようにすれば、tokenIdが1の場合、
<script src="data:text/javascript;base64,Y29uc3QgdG9rZW5JZD0xOw=="></script>
というJavaScriptコードがHTMLに追加されます。 これは、以下のコードをdataURL化したものです。
<script>
const tokenId=1;
</script>
const tokenId=1;
をBase64エンコードして、dataURL化して、<script>
タグのsrc
属性に設定して、HTMLに追加する、という作業をscripty.solが自動的に行なってくれるのです。すごいですよね。
以上で、p5.jsのスケッチでtokenId
というグローバル変数を使えるようになります。
ScriptyStorageへのアップロード
EthFS_P5_URLSafe.sol
では、以下4つのJavaScriptを読み込んでいました。
- “scriptyBase”
- “p5-v1.5.0.min.js.gz”
- “gunzipScripts-0.0.1.js”
- “pointsAndLines”
1,2,3は、すでにScriptyStorageやEthFSにアップロードされている共有のファイルですが、4の”pointsAndLines”はこのNFT専用のファイルなので、事前にアップロードしておく必要があります。
ScriptyStorageには、スクリプトでアップロードします。
- ScriptyStorageコントラクトの
createScript
関数でファイルを作成 - 最大24,575バイトになるようにファイルを分割 (SSTORE2を使うため)
- 分割した各チャンクを
addChunkToScript
関数でアップロード
以下の記事で紹介したように、EthFSを使えば公式サイトからアップロードできるので、個人的にはEthFSを使う方がおすすめです。
ただし、EthFSの公式サイトからアップロードした場合はBase64エンコードされるため、requests
の作成の際にwrapType=1
を指定することを忘れないでください。
p5.jsを使ったNFTを作ってみよう
この続き: 2,730文字 / 画像11枚
まとめ
今回は、フルオンチェーンNFTを作成するときに役立つライブラリ「scripty.sol」について、概要やその使い方などを中心に解説しました。
本記事が、scripty.solの概要や使い方、p5.jsを使ったフルオンチェーンNFTの構築方法などについて理解したいと思われている方にとって、少しでもお役に立ったのであれば幸いです。
また励みになりますので、参考になったという方はぜひTwitterでのシェア・コメントなどしていただけると嬉しいです。
◤ scripty.solの概要と使い方 ◢
— イーサリアムnavi🧭 Called "Ethereumnavi" (@ethereumnavi) April 15, 2023
🧭generativeアート作成のためのオンチェーンツールセット
🧭int artの「the metro」でも用いられている
🧭自作のJavaScriptコードや、「Three.js」「p5.js」といったJavaScriptを、自由に組み合わせることが可能に
詳しくはこちら👇https://t.co/4gRYk1qEK5
scripty.solはさまざまなパターンのHTMLを作成することができますが、 今回はp5.jsを使ったフルオンチェーンNFTという目的に絞って使い方を説明しました。
Three.jsを使う場合にも同じ方法で作成できると思います。 ぜひscripty.solを使って、すごいフルオンチェーンNFTを作ってみてください。
イーサリアムnaviを運営するSTILL合同会社では、web3/crypto関連のリサーチ代行、アドバイザー業務、その他(ご依頼・ご提案・ご相談など)に関するお問い合わせを受け付けております。
まずはお気軽に、こちらからご連絡ください。
- 法人プランLP:https://ethereumnavi.com/lp/corporate/
- Twitter:@STILL_Corp
- メールアドレス:info@still-llc.com