こんにちは、フルオンチェーン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の購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。


Alpha Navigatorは、超アーリーなクリプトプロジェクトに特化して探知し、webサイト, discord, twitterで情報発信を行っています。
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を作ってみよう
本記事の執筆にあたり、筆者は実際にscripty.solを使いながら、p5.jsを使ったフルオンチェーンNFTを作成してみましたので、ステップバイステップで解説していきます。
Example1
Example1では、以下の方針でNFTを作成しました。
- tokenIdごとに異なるスケッチを表示する
- 各スケッチファイルはEthFSにアップロードする
- メタデータはURLエンコードする
まずは p5.js Web Editorなどを使って、p5.jsのスケッチを作成します。


今回は筆者は、3種類のスケッチを作りました。それぞれ nawoo-example1-sketch1.js
, nawoo-example1-sketch2.js
, nawoo-example1-sketch3.js
という名前でPCに保存しておきます。



EthFSにアップロードするときに、ファイル名が他のファイルと重複しないように、長いファイル名にしています。
続いて、この3つのファイルを EthFS公式サイト(Goerli) でアップロードします。


3つのスケッチファイルがEthFSにアップロードされました。ファイルはBase64エンコードされています。
では、いよいよコントラクトの作成です。
コンストラクタの引数で、EthFSFileStorage, ScriptyStorage, ScriptyBuilderのコントラクトアドレスを渡します。



デプロイ後に気づいたのですが、Example1ではScriptyStorageを使っていないので不要でした。。。
address public immutable ethfsFileStorageAddress;
address public immutable scriptyStorageAddress;
address public immutable scriptyBuilderAddress;
constructor(
address _ethfsFileStorageAddress,
address _scriptyStorageAddress,
address _scriptyBuilderAddress
) ERC721("Example1", "SSE1") {
ethfsFileStorageAddress = _ethfsFileStorageAddress;
scriptyStorageAddress = _scriptyStorageAddress;
scriptyBuilderAddress = _scriptyBuilderAddress;
}
各コントラクトアドレスは、scripty.solのサイトに記載されています。今回はGoerliのアドレスを使います。


mint関数では、トークンごとのname
,description
,scriptName
を指定します。
scriptName
は、先ほどEthFSにアップロードしたファイル名になります。(nawoo-example1-sketch1.js
など)
// tokenId => TokenDataのマッピング
mapping(uint256 => TokenData) private tokens;
// mint関数でname,description,scriptNameをマッピングに保存
function mint(
uint256 tokenId,
string memory name,
string memory description,
string memory scriptName
) public onlyOwner {
// マッピングにトークン情報を保存
tokens[tokenId].name = name;
tokens[tokenId].description = description;
tokens[tokenId].scriptName = scriptName;
// ミント
_mint(msg.sender, tokenId);
}
TokenDataは、以下のように定義されています。
struct TokenData {
string name;
string description;
string scriptName;
}
tokenURI関数では、以下の処理を順番に行います。
- tokenIdに対応したTokenDataを取得
- requestsの作成 (今回は3つ)
- bufferSizeの取得
- HTMLの作成
- メタデータの作成
requestsでは、3つのJavaScriptを指定しています。すべてEthFSから読み込みます。
- p5.jsの本体 (gzip圧縮)
- gzipを展開するためのコード (Base64エンコード)
- p5.jsのスケッチ (Base64エンコード)



メタデータはURLエンコードするため、メタデータ中の文字列はURLエンコードしています。
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (!_exists(tokenId)) revert TokenDoesNotExist();
// TokenDataを取得
TokenData storage token = tokens[tokenId];
// requestsの作成
WrappedScriptRequest[] memory requests = new WrappedScriptRequest[](3);
requests[0].name = "p5-v1.5.0.min.js.gz"; // p5.js本体
requests[0].wrapType = 2; // gzip圧縮されている
requests[0].contractAddress = ethfsFileStorageAddress; // EthFSから読み込み
requests[1].name = "gunzipScripts-0.0.1.js"; // gunzip (gzip展開用のJavaScript)
requests[1].wrapType = 1; // Base64エンコードされている
requests[1].contractAddress = ethfsFileStorageAddress; // EthFSから読み込み
requests[2].name = token.scriptName; // tokenIdごとのスケッチファイル名
requests[2].wrapType = 1; // Base64エンコードされている
requests[2].contractAddress = ethfsFileStorageAddress;
// bufferSizeの取得
ScriptyBuilder builder = ScriptyBuilder(scriptyBuilderAddress);
uint256 bufferSize = builder.getBufferSizeForURLSafeHTMLWrapped(requests);
// HTMLの作成 (dataURL形式で取得)
bytes memory html = builder.getHTMLWrappedURLSafe(requests, bufferSize);
// メタデータの作成
return
string.concat(
"data:application/json,",
"%7B%22name%22%3A%22", //'{"name":"',
token.name,
"%22%2C%22description%22%3A%22", //'","description":"',
token.description,
"%22%2C%22animation_url%22%3A%22", //'","animation_url":"',
string(html),
"%22%7D" //'"}'
);
}
これで完成です。デプロイしてみました。



実物はアニメーションしているので、ぜひ上リンク先のOpenSeaページでご覧ください。
Example2 (ジェネラティブNFT)
Example2は、以下の方針でNFTを作成しました。
- スケッチは1種類で、tokenIdごとに表示内容を変える(ジェネラティブ)
- スケッチファイルはScriptyStorageにアップロードする
- メタデータはURLエンコードする
スケッチファイルで、randomSeed
関数にtokenIdを指定しておきながらrandom
関数を使うことで、tokenIdごとにパラメータを変化させることができます。
- tokenIdが異なれば、数値が異なる
- tokenIdが同じであれば、リロードしても毎回同じ数値になる



このスケッチでは、たくさんの球体をアニメーションさせています。
球体の移動方法、2D/3D表示の切り替え、半径、といったパラメータをtokenIdごとに設定することで、表示内容を変えています。
function setup() {
// ...略
// ランダムシードにtokenIdを指定
randomSeed(tokenId);
// 各パラメータをランダムに決める
fx = Math.floor(random(9)); // 球体の移動方法(x座標)
fy = Math.floor(random(9)); // 球体の移動方法(y座標)
fz = Math.floor(random(9)); // 球体の移動方法(z座標)
is3D = random() > 0.9; // 3D表示するかどうか
r = random(5, 10); // 球体の半径
}
ただし、このままではtokenId
という変数が未定義として、エラーになってしまいます。
そこで、コントラクト内で以下のようなJavaScriptコードを動的に作成して、スケッチファイルより先にHTMLに追加します。
const tokenId=XXX;
Example2.solのtokenURI関数では、4つのJavaScriptを指定しています。



3つ目のrequests[2]
が、動的に作成したJavaScriptです。
WrappedScriptRequest[] memory requests = new WrappedScriptRequest[](4);
// p5.jsの本体 (gzip圧縮されている)
requests[0].name = "p5-v1.5.0.min.js.gz";
requests[0].wrapType = 2; // gzip圧縮
requests[0].contractAddress = ethfsFileStorageAddress;
// gzip展開するためのJavaScript
requests[1].name = "gunzipScripts-0.0.1.js";
requests[1].wrapType = 1; // Base64エンコード
requests[1].contractAddress = ethfsFileStorageAddress;
// `const tokenId=XXX;`のコードを動的に作成
requests[2].wrapType = 0; // 無圧縮
requests[2].scriptContent = abi.encodePacked("const tokenId=", tokenIdStr, ";");
// p5.jsのスケッチ
requests[3].name = sketchName;
requests[3].wrapType = 0; // 無圧縮
requests[3].contractAddress = scriptyStorageAddress;
こちらもデプロイしてみました。



ぜひ、OpenSeaでtokenIdごとの違いを確認してみてください。
スケッチファイルはこちらです。 また、Example2はこちらからフリーミントできます。何枚でもご自由にどうぞ。
まとめ


今回は、フルオンチェーン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合同会社では、以下などに関するお問い合わせを受け付けております。
- 広告掲載
- リサーチ代行業務
- アドバイザー業務
- その他(ご依頼・ご提案・ご相談など)
まずはお気軽に、こちらからご連絡ください。
- Webサイト:still-llc.co.jp
- Twitter:@STILL_Corp
- メールアドレス:info@still-llc.com