【NFT】メタデータの可能性 animation_url【OpenSea】


## はじめに
OpenSeaには色々な NFTが並んでいます。
画像の NFTだけではなく、動画、３D、サウンドなど、色々な形式のデータを扱ってくれるようです。

NFTの情報はメタデータで記述されるということを、[前回の投稿](https://qiita.com/hakumai-iida/items/c96d7c053379f42ba9b8)で説明いたしました。例えば、画像なら "**image**"要素で指定され、PNG、JPG、GIF(アニメGIF)だけでなく、SVGにも対応しているようです。一方で、動画や3Dといった画像以外の要素は、"**animation_url**"として、"**image**"とは別に指定されます。

[OpenSeaのメタデータの詳細ページ](https://docs.opensea.io/docs/metadata-standards)で "**animation_url**"を確認すると、色々な拡張子がのっています。
>A URL to a multi-media attachment for the item. The file extensions GLTF, GLB, WEBM, MP4, M4V, OGV, and OGG are supported, along with the audio-only extensions MP3, WAV, and OGA.
>
>Animation_url also supports HTML pages, allowing you to build rich experiences and interactive NFTs using JavaScript canvas, WebGL, and more. >Scripts and relative paths within the HTML page are now supported. However, access to browser extensions is not supported.


"**animaton_url**"はずいぶんと芸達者な要素のようです。

この記事では、そんな "**animation_url**"の各種フォーマットのテストをしてみたいと思います。
また、**HTML(+JavaScript)**によって広がる NFTの可能性についても探ってみたいと思います。

## "animation_url" 対応フォーマット
まずは、"**animation_url**"が対応する各種データを、Rinkebyでテストしてみました（テストデータはフリーのものをお借りしました ＆ NFTの詳細に出典を書いてあります）。
[https://testnets.opensea.io/collection/file-format-test-token](https://testnets.opensea.io/collection/file-format-test-token)

・**GLTF (3Dモデルデータ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/0](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/0)

・**GLB (3Dモデルデータ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/1](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/1)

・**WEBM (動画データ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/2](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/2)

・**MP4 (動画データ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/3](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/3)

・**M4V (動画データ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/4](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/4)
　**音が出ます**

・**OGV (動画データ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/5](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/5)

OGVはうまく再生されませんでした（音声有り／音声なしの両方でテスト）。
ちなみに、IPFSへアップロードしたデータの再生は正常に行われるので OpenSea側の問題のように思われます。
[https://bafybeigxesrikogeqzlisjviugt72lalgmgokmaby66f2xjkxh5fxqcwv4.ipfs.infura-ipfs.io/05_ogv.ogv](https://bafybeigxesrikogeqzlisjviugt72lalgmgokmaby66f2xjkxh5fxqcwv4.ipfs.infura-ipfs.io/05_ogv.ogv)
**音が出ます**

・**OGG (動画データ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/6](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/6)
　**音が出ます**

・**MP3 (音声データ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/7](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/7)
　**音が出ます**

・**WAV (音声データ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/8](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/8)
　**音が出ます**

・**OGA(音声データ)**
　[https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/9](https://testnets.opensea.io/assets/0x17ec0065e83c78f82eb8d4a3f52291a3321e5005/9)
　**音が出ます**

一通りのフォーマットを試したところ、OGV以外はとくに問題なく表示／再生されました。
とはいえ、テストしたのは Rinkebyだけで、テストデータも１種類のみとなります。

OGVにかぎらず、"**animaton_url**"の利用を考えている方は、デプロイを予定しているチェーン上で、実際のデータと同じ形式のダミーデータが正常に表示されるか確認することをお勧めします（個人的にOpenSeaは、Ethereum系(Mainnet/Rinkeby)と、Polygon系(Polgyon/Mumbai)で微妙に実装が違う印象がありまして、Rinkebyで動いたけど、Polygonで動かないなんてことも実際にあったりしたので）。

## 可能性の塊 HTML+JavaScript
さて、真打登場です。

"**animation_url**"には HTMLが指定できます。
そして、HTML内で JavaScriptのコードを書くことができてしまいます。
あんなことや、こんなことが、OpenSeaの詳細ページ上で行えてしまうというわけです。

これは、結構「**こわい**」ことだと思うのですが、やっていいというなら、やらせていただきましょう！
（まあ、Plugin等の処理が使えないなど、いろいろと制限はあるようですので、イタズラはできないと思いますが）

### メリークリスマス
というわけで、テストとして「クリスマスプレゼント」の NFTを作ってみました。
[https://testnets.opensea.io/collection/xmas-present-token-2021](https://testnets.opensea.io/collection/xmas-present-token-2021)
決まった時間が来るまでは、中身が見えない NFTとなります。

まぁ、なんて素敵な NFTでしょう！？
この NFT、なんと今なら 0.1 ETHでお買い求めいただけるんです！
(Rinkebyですけどね...）

### solidity 側の実装
スマートコントラクト側の実装はとてもシンプルで、指定された時間になるまで、tokenURIからは「中身を隠した」メタデータを返却します。
で、指定された時間が経過したら、tokenURIからは「中身の見える」メタデータを返却するようにします。
（メタデータの作成に関しては[前回の記事](https://qiita.com/hakumai-iida/items/c96d7c053379f42ba9b8)を参照ください）

``` solidity
// 時間が経過しているので "image"に中身の画像を指定する
// "animation_url"は不要
if( _unixtimes[tokenId] <= block.timestamp ){
  bytesImage = abi.encodePacked( ',"image":"https://ipfs.infura.io/ipfs/', PNG_HASH ,'/p', bytesId, '.png"' );
  bytesAnimationUrl = '';
}
// 時間が経過していないので "image"には箱の画像を指定する
// "animation_url"で待機表示用のHTMLを指定する
else{
  bytesImage = abi.encodePacked( ',"image":"https://ipfs.infura.io/ipfs/', GIF_HASH, '/b', bytesId, '.gif"' );
  bytesAnimationUrl = abi.encodePacked( ',"animation_url":"https://ipfs.infura.io/ipfs/', _htmls[tokenId], '"' );
}
```
ここで、"**animation_url**"に指定するHTMLにより、OpenSea上での NFTの表示が「中身を隠した」状態となります。

### HTML側の実装
HTML(JavaScript)の処理としては下記となります。
・指定された時間までのカウントダウンを行う
・指定された時間が経過していたらボタンを表示する
・ボタンが押されたらOpenSeaのAPI「refresh metadata」を叩く

コードとしては下記のようになります。

``` html
<html>
  <head>
    <style>
      <!-- 省略 -->
    </style>

    <script>
      let waitSec = 0;
      let intervalId = 0;

      // カウントダウンの処理
      function count(){
        if( waitSec <= 0 ){
          clearInterval( intervalId );          
          ready();
          return;
        }

        waitSec--;

        let day = Math.floor(waitSec/86400);

        let hh = (Math.floor(waitSec/3600))%24;
        hh = ( "00" + hh ).slice( -2 );

        let mm = (Math.floor(waitSec/60))%60;
        mm = ( "00" + mm ).slice( -2 );

        let ss = (waitSec%60);
        ss = ( "00" + ss ).slice( -2 );

        let e = document.getElementById( "sT" );

        if( day <= 0 ){
          e.innerHTML = hh + ":" + mm + ":" + ss;
        }else{
          if( day > 1 ){
            e.innerHTML = day + "days " + hh + ":" + mm + ":" + ss;
          }else{
            e.innerHTML = day + "day " + hh + ":" + mm + ":" + ss;
          }
        }
      }

      // 時間が経過していない場合の「WAIT」表示
      function wait(){
        let e = document.getElementById( "pR" );
        e.style.display = "none";

        e = document.getElementById( "sW" );
        e.innerHTML = "Please Wait...";

        count();
        intervalId = setInterval( count, 1000 );
      }

      // 時間が経過している場合の「OPEN」表示
      function ready(){
        let e = document.getElementById( "pW" );
        e.style.display = "none";

        e = document.getElementById( "pR" );
        e.style.display = "block";

        e = document.getElementById( "sR" );
        e.innerHTML = "Open the Gift!!";

        e = document.getElementById( "bO" );
        e.style.display = "inline";
      }

      // ページが読み込まれた際の処理
      function start(){
        let ut = 1639753200;
        let t = Math.floor(new Date().getTime() / 1000);
        if( t < ut ){
          waitSec = ut - t;
        }

        if( waitSec > 0){
          wait();
        }else{
          ready();
        }
      }

      // 「OPEN」ボタンが押された時の処理
      function updateMeta() {
        let e = document.getElementById( "bO" );
        e.style.display = "none";

        e = document.getElementById( "iL" );
        e.style.display = "inline";

        e = document.getElementById( "sR" );
        e.innerHTML = "Updating...";

        setTimeout( notice, 8000 );

        // このNFTの「refresh metadata]APIのアクセス先
        let url = "https://rinkeby-api.opensea.io/api/v1/asset/0x29E76047aF56D87aB89c6E888D14f62FfC0A5235/0/?force_update=true";
        callRefreshMetadata( url );
      }

      // 指定されたURLのフェッチ(APIを叩く）
      async function callRefreshMetadata( url ){
        await fetch( url );
      }

      // 更新完了通知
      function notice(){
        let e = document.getElementById( "iL" );
        e.style.display = "none";

        e = document.getElementById( "sR" );
        e.innerHTML = "Reload the Page<br>After a While!!";
      }
    </script>
  </head>

  <body onload="start()">
    <p id="pW">
      <span id="sW"></span><br>
      <span id="sT" class="time"></span>
    </p>
    <p id="pR">
      <span id="sR"></span><br>
      <img id="bO" class="bO" onclick="updateMeta()" src="https://ipfs.infura.io/ipfs/QmZq251SckKKb7ZE89EhDFmcgpXe5DzfHjHd2c4JsYyC3a">
      <img id="iL" class="iL" src="https://ipfs.infura.io/ipfs/QmbfNLk7dH8PoWsdW321VWBpVv5sUPHzp4Pk5VYeAnDz9g">
    </p>
  </body>
</html>
```

実装としては、本当に、普通の HTML+JavaScriptを書くのと同じです。
ローカルでブラウザ上に HTMLファイルを読み込ませつつ、希望の挙動を確認できたら、IPFS等へアップロードして、NFTへ割り当てるだけです。

OpenSea上での流れとしては、下記となります。

1. 待ち時間中であればカウントダウンが表示される
![ss00](https://user-images.githubusercontent.com/13220051/146636047-4cebc630-4705-44d4-8dd0-4c53ef9b7006.png)

2. 時間が経過していたら「Open」ボタンが表示される
![ss01](https://user-images.githubusercontent.com/13220051/146636049-9d6a2bbb-9f43-4e11-a1c5-90358a44dc2c.png)

3. 「Open」ボタンが押されたら「refresh metadata」のAPIがよばれ、OpenSeaが NFTのメタデータを読み直す
![ss02](https://user-images.githubusercontent.com/13220051/146636051-75e2b9d4-b3f3-4bdb-98c8-6a158bba75a5.png)

4. OpenSeaに中身の見えるメタデータが返却され、NFTの中身の画像が表示されるようになる
![ss03](https://user-images.githubusercontent.com/13220051/146636053-91cc4d90-9e28-4472-96dd-4cad7a99dc94.png)

ちなみに、プレゼントボックスは誰でも開けることができ、ガス代もかかりません（メタデータの更新を呼ぶだけなので）。
１日に１つか２つのプレゼントボックスが開けられるようになっているので、興味のある方は[コレクションページ](https://testnets.opensea.io/collection/xmas-present-token-2021)でクリスマス気分をお楽しみくださいませ。

## おわりに
"**animation_url**"の提供する各種ファイルフォーマットですが、人によってはあまり使う機会はないかもしれません。
一方で、HTML(JavaScript)を使うことで、本来の NFTの価値に＋アルファをもたせられそうです。

今回テストしたクリスマスプレゼントの NFTは、実際には PNGが割り当てられただけの、ごく普通のNFTです。
ですが、JavaScriptによる一手間で「待つ楽しさ」と「開ける楽しさ」の２つが提供できたと思います。

"**animation_url**"には、工夫とアイデア次第で、色々な可能性が秘められているのです。

### 補足①　ソースコード
今回テストした２つのコントラクトのコードは githubへあげてあります
・[https://github.com/hakumai-iida/AnimationUrlFormatTest](https://github.com/hakumai-iida/AnimationUrlFormatTest)
・[https://github.com/hakumai-iida/PresentBoxNft](https://github.com/hakumai-iida/PresentBoxNft)

### 補足②　HTMLとはモノなのか？
どうでもよい話なんですが...。

例えば、GLB、MP4、WAVといったデータの場合、３Dモデル、動画、音声として、明確に「モノ」としてイメージできるじゃないですか？
NFTの保有者も「私はこの3Dモデル／動画／音声を持っている」と違和感なく認識できると思うのですよね。

では、「モノ」としてみた場合、HTMLとはなんなんでしょうか？
テキストベースなページであったり、画像が掲載されたページであったり、リンク集だったり、千差万別です。

うーん、そういう意味では「情報」なんでしょうか？

一方で、JavaScriptが入ってくる場合、また少し、毛色が変わってきます。
タイマー的な処理をしたり、入力を判定したり、外部APIを叩いたり...。
こちらの場合は、「仕組み」とでも考えるべきでしょうか？

今回、記事を書いていて少しモヤモヤしました。
うーん、不思議。