LoginSignup
15
9

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-18

はじめに

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

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

OpenSeaのメタデータの詳細ページで "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

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

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

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

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

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

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

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

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

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

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

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

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

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
決まった時間が来るまでは、中身が見えない NFTとなります。

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

solidity 側の実装

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

// 時間が経過しているので "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>
  <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

  2. 時間が経過していたら「Open」ボタンが表示される
    ss01

  3. 「Open」ボタンが押されたら「refresh metadata」のAPIがよばれ、OpenSeaが NFTのメタデータを読み直す
    ss02

  4. OpenSeaに中身の見えるメタデータが返却され、NFTの中身の画像が表示されるようになる
    ss03

ちなみに、プレゼントボックスは誰でも開けることができ、ガス代もかかりません(メタデータの更新を呼ぶだけなので)。
1日に1つか2つのプレゼントボックスが開けられるようになっているので、興味のある方はコレクションページでクリスマス気分をお楽しみくださいませ。

おわりに

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

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

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

補足① ソースコード

今回テストした2つのコントラクトのコードは githubへあげてあります
https://github.com/hakumai-iida/AnimationUrlFormatTest
https://github.com/hakumai-iida/PresentBoxNft

補足② HTMLとはモノなのか?

どうでもよい話なんですが...。

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

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

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

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

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

15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9