int artの「the metro」で用いられているscripty.solの概要とその使い方、そしてp5.jsを使ったNFTの開発方法について解説

こんにちは、フルオンチェーンNFTクリエイターのnawooです。

nawoo

今回は、フルオンチェーンNFTを作成するときに役立つライブラリ「scripty.sol」について解説します。

前回の記事で、the metroというインタラクティブなフルオンチェーンNFTコレクションについて解説しましたが、そこで用いられているgenerativeアート作成のためのオンチェーンツールセットが、今回ご紹介する「scripty.sol」です。

scripty.solというコントラクトは、自分で作ったJavaScriptコードや、「Three.js」「p5.js」といったJavaScriptを、自由に組み合わせることができます。そして、それを使ってオンチェーンでHTMLを作成することができるのです。

scripty.solはフルオンチェーンNFTの幅を広げることができると同時に、そのようなNFTの構築を簡単にしてくれるため、web3開発者はフルオンチェーンNFTに関心があるという方は、知っておいて損はないと思います。

ということで今回は、フルオンチェーンNFTを作成するときに役立つライブラリ「scripty.sol」について、概要やその使い方などを中心に解説します。

でははじめに、この記事の構成について説明します。

STEP
scripty.solとは

まずは、scripty.solの基本的な概要について、簡潔に解説します。

STEP
scripty.solの使い方

続いて、どのような流れでscripty.solを使用すれば良いのかについて、ステップバイステップで解説します。

STEP
p5.jsを使ったNFTを作ってみよう

最後に、実際にscripty.solを使いながらp5.jsを使ったフルオンチェーンNFTを作成しつつ、全体的な流れや完成物などについて概観します。

本記事が、scripty.solの概要や使い方、p5.jsを使ったフルオンチェーンNFTの構築方法などについて理解したいと思われている方にとって、少しでもお役に立てれば幸いです。

※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。

Alpha Navigatorは、超アーリーなクリプトプロジェクトに特化して探知し、webサイト, discord, twitterで情報発信を行っています。

目次

scripty.solとは

出典:docs.int.art/open-source-projects/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に指定する必要があります。

ところが、scripty.solを使うことで、こういった一連の作業がとても簡単になります。

nawoo

HTMLに含めたいJavaScriptをrequestsとして指定するだけで、簡単にdataURL化したHTMLを作成できるのです。

scripty.solの作者は、0xdude氏とxtremetom氏です。

先日、xtremetom氏がローンチしたCryptoCoasterというNFTコレクションや、前回の記事で取り上げたint art(0xdude氏)のフルオンチェーン×インタラクティブNFT「the metro」でも、scripty.solが使用されています。

scripty.solでは、様々なタイプのHTMLを作成することができます。

nawoo

次章では、p5.jsを使ったフルオンチェーンNFTを作成する方法を中心に解説していきます。

scripty.solの使い方

まずは、公式サイトでもp5.jsを使ったシンプルなNFTの例がありますので、概観していきます。

p5.jsを使ったexampleには、以下の2種類があります。今回は、後者②のEthFS_P5_URLSafe.solを使います。

  1. Base64エンコードを使ったEthFS_P5.sol
  2. URLエンコードを使ったEthFS_P5_URLSafe.sol
nawoo

では最初に、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>
nawoo

このメタデータをどのように作成しているのか、コントラクトのtokenURI関数を確認してみましょう。

tokenURI関数では、以下3つの処理が行われています。

  1. requestsを作成
  2. bufferSizeを取得
  3. 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パターンあります。

  1. scriptys.sol用のストレージであるScriptyStorageコントラクトから読み込む
  2. EthFSのFileStorageコントラクトから読み込む
  3. コントラクト内でコードを指定する

1と2では、contractAddressで読み込み先のコントラクトを指定し、nameでファイル名を指定します。

3は、コントラクト内でJavaScriptを動的に作成する場合に使います。

nawoo

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を読み込むように指定されていることがわかります。

  1. ScriptyStorageから、無圧縮の"scriptyBase"を読み込む
  2. EthFSから、gzip圧縮された"p5-v1.5.0.min.js.gz"を読み込む
  3. EthFSから、Base64エンコードされた"gunzipScripts-0.0.1.js"を読み込む
  4. 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を作成しています。

これにより、ガス節約しながら大きなサイズのHTMLを作成できるようになっています。

EthFS_P5_URLSafe.solでは、bufferSizeをローカルPCで事前に計算しておいて、コンストラクタの引数として渡すようになっています。

nawoo

ローカル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のサイズを求めます。

Base64エンコードの場合は、getBufferSizeForHTMLWrapped関数を使います。

3. animation_url用のdataURLとメタデータを作成

ここで、いよいよHTMLの作成です。

ScriptyBuilderコントラクトのgetHTMLWrappedURLSafe関数にrequestsbufferSizeを引数に渡すと、 HTMLをdataURL形式で返します。dataURLのデータ部は2回URLエンコードされています。

URLエンコードを2回行う理由について知りたいという方は、以下の記事をご参照ください。

さらに、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関数に関しては、これだけです。

ROSESのtokenURI関数と比べると、かなり簡単ですね。

Base64エンコードの場合

メタデータをURLエンコードではなくBase64エンコードする場合は、 requestsの作成は同じですが、BufferSizeの計算とHTMLの作成に別の関数を使います。

  • BufferSizeの計算 :
    • getBufferSizeForURLSafeHTMLWrapped → getBufferSizeForHTMLWrapped関数に変更
  • HTMLの作成 :
    • getHTMLWrappedURLSafe → getEncodedHTMLWrapped関数に変更

メタデータはURLエンコードせずに作成しておいて、最後にBase64でエンコードします。

nawoo

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=1tokenId=2tokenId=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を読み込んでいました。

  1. “scriptyBase”
  2. “p5-v1.5.0.min.js.gz”
  3. “gunzipScripts-0.0.1.js”
  4. “pointsAndLines”

1,2,3は、すでにScriptyStorageやEthFSにアップロードされている共有のファイルですが、4の”pointsAndLines”はこのNFT専用のファイルなので、事前にアップロードしておく必要があります。

参考 : pointsAndLines.js → p5.jsのスケッチファイルです

ScriptyStorageには、スクリプトでアップロードします。

  1. ScriptyStorageコントラクトのcreateScript関数でファイルを作成
  2. 最大24,575バイトになるようにファイルを分割 (SSTORE2を使うため)
  3. 分割した各チャンクをaddChunkToScript関数でアップロード

参考 : EthFS_P5_URLSafeのデプロイスクリプトのstoreScript関数

nawoo

以下の記事で紹介したように、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のスケッチを作成します。

出典:editor.p5js.org

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

nawoo

EthFSにアップロードするときに、ファイル名が他のファイルと重複しないように、長いファイル名にしています。

続いて、この3つのファイルを EthFS公式サイト(Goerli) でアップロードします。

参考: https://ethereumnavi.com/2023/03/18/what-is-ethfs/

出典: goerli.ethfs.xyz

3つのスケッチファイルがEthFSにアップロードされました。ファイルはBase64エンコードされています。

では、いよいよコントラクトの作成です。

コンストラクタの引数で、EthFSFileStorage, ScriptyStorage, ScriptyBuilderのコントラクトアドレスを渡します。

nawoo

デプロイ後に気づいたのですが、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のアドレスを使います。

出典: int-art.gitbook.io/scripty.sol/#deployed-contracts

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関数では、以下の処理を順番に行います。

  1. tokenIdに対応したTokenDataを取得
  2. requestsの作成 (今回は3つ)
  3. bufferSizeの取得
  4. HTMLの作成
  5. メタデータの作成

requestsでは、3つのJavaScriptを指定しています。すべてEthFSから読み込みます。

  • p5.jsの本体 (gzip圧縮)
  • gzipを展開するためのコード (Base64エンコード)
  • p5.jsのスケッチ (Base64エンコード)
nawoo

メタデータは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" //'"}'
        );
}

これで完成です。デプロイしてみました。

nawoo

実物はアニメーションしているので、ぜひ上リンク先のOpenSeaページでご覧ください。

Example2 (ジェネラティブNFT)

Example2は、以下の方針でNFTを作成しました。

  • スケッチは1種類で、tokenIdごとに表示内容を変える(ジェネラティブ)
  • スケッチファイルはScriptyStorageにアップロードする
  • メタデータはURLエンコードする

スケッチファイルで、randomSeed関数にtokenIdを指定しておきながらrandom関数を使うことで、tokenIdごとにパラメータを変化させることができます。

  • tokenIdが異なれば、数値が異なる
  • tokenIdが同じであれば、リロードしても毎回同じ数値になる
nawoo

このスケッチでは、たくさんの球体をアニメーションさせています。

球体の移動方法、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を指定しています。

nawoo

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;

こちらもデプロイしてみました。

nawoo

ぜひ、OpenSeaでtokenIdごとの違いを確認してみてください。

スケッチファイルはこちらです。 また、Example2はこちらからフリーミントできます。何枚でもご自由にどうぞ。

まとめ

イーサリアムnaviの「今まで」と「これから」

今回は、フルオンチェーンNFTを作成するときに役立つライブラリ「scripty.sol」について、概要やその使い方などを中心に解説しました。

本記事が、scripty.solの概要や使い方、p5.jsを使ったフルオンチェーンNFTの構築方法などについて理解したいと思われている方にとって、少しでもお役に立ったのであれば幸いです。

また励みになりますので、参考になったという方はぜひTwitterでのシェア・コメントなどしていただけると嬉しいです。

scripty.solはさまざまなパターンのHTMLを作成することができますが、 今回はp5.jsを使ったフルオンチェーンNFTという目的に絞って使い方を説明しました。

nawoo

Three.jsを使う場合にも同じ方法で作成できると思います。 ぜひscripty.solを使って、すごいフルオンチェーンNFTを作ってみてください。

なお、本記事のコントラクトやスクリプトはこちらのGitHubで公開していますので、ぜひ参考にしてください。

イーサリアムnaviを運営するSTILL合同会社では、以下などに関するお問い合わせを受け付けております。

  • 広告掲載
  • リサーチ代行業務
  • アドバイザー業務
  • その他(ご依頼・ご提案・ご相談など)

まずはお気軽に、こちらからご連絡ください。

励みになるので、よかったらSNSなどでシェアしてください!
  • URLをコピーしました!
  • URLをコピーしました!
目次