【脱Truffle】Solidity開発ツール「Hardhat」の概要やメリット・使い方について徹底解説

どうも、イーサリアムnavi運営のでりおてんちょーです。

イーサリアムnaviでは、毎日大量に流れてくるクリプトニュースを調査し、その中でも面白いトピックやクリプトネイティブな題材を選び出し、それを分かりやすく読みやすい形でお伝えしています。パラパラと内容を眺めているだけでも、事業やリサーチの新たなヒントに繋がることがあります。世界の最先端では、どのようなクリプトコアな試みが行われているのかを認識するだけでも、 思わず狭くなりがちな視野を広げてくれるでしょう。

今回は、Solidity開発ツール「Hardhat」の完全版記事として、概要や利用するメリット、その使い方について解説していきたいと思います。

筆者自身、Solidityの勉強を始めてからというものTruffleを使用して開発に取り組んでいたのですが、Hardhatの利便性に感銘を受けたため、このたび備忘録も兼ねて記事にまとめておきたいと思いました。

0からHardhatで開発を始めるなら問題ありませんが、TruffleからHardhatへ切り替えるのはかなりエネルギーが必要かと思います。

しかし、できるだけ早いうちに触れて、慣れておいた方が良いのではないかと個人的には思いました。

Truffleからの公式移行ガイドはこちらをご参考ください。
Migrating from Truffle | Hardhat | Ethereum development environment for professionals by Nomic Labs

この記事を機に、Hardhatを使ってみたかったけど踏みとどまっていたという方は、ぜひ試してみてください。

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

STEP
Hardhatの概要・メリット

まずは簡潔に、Hardhatの概要と利用する大きなメリットについて、個人的な体験談も含めて紹介いたします。

STEP
実践編①〜インストールしてみよう〜

実践編からは、スクショ等を織り交ぜながらHardhatの使い方について解説してまいります。

STEP
実践編②〜ローカル環境にコントラクトをデプロイしてみよう〜

STEP2でインストールなど準備が完了したら、まずはローカル環境へのデプロイを試してまいります。

STEP
実践編③〜フルオンチェーンNFTをMintしてOpenSeaで確認してみよう〜

最後に、テストネット(Rinkeby)へのコントラクトデプロイからフルオンチェーンNFTのMint、そしてOpenSeaで確認するという一連の流れについてまとめてまいります。

本記事が、Hardhatの使い方やローカル環境/テストネットへのデプロイから確認までの一連の流れなどについて理解したいと思われている方にとって、少しでもお役に立てれば幸いです。

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

「定期購読プラン」の詳細はこちら

「定期購読プラン」は現在、トライアル期間を設けているため、初回は30日間無料でプランにご登録いただけます。この機会にぜひお試しいただき、サービスの魅力を体験していただければ幸いです。

※ 1ヶ月間の無料トライアル期間終了後に、自動的に有料プランへと移行します。
※ 無料トライアル期間中にご解約いただければ、ご利用料金は発生いたしません。


目次

Hardhatの概要・メリット

Hardhatは、Ethereum上でのスマートコントラクトやdAppの構築を容易にする開発環境です。

今までは、TruffleやGanacheを利用してEthereumのスマートコントラクト/dappなどを開発する方法が主流でしたが、2021年半ばあたりからHardhatを利用する人が徐々に増えていった印象です。

筆者自身、ちょうどそのくらいの時期にSolidityを勉強し始めたのですが、当初はどの本を読んでもTruffle/Ganacheを使った開発ばかりだったこともあり、Hardhatを利用するのは開発に慣れてからで良さそうだと後回しにしていました。

しかし、周りのSolidity開発者の多くがHardhatを使うことを推奨してきたため試しに使ってみるようにしたところ、衝撃的な利便性に感動して今に至ります。

実際に使用してみて、HardhatにはTruffleと比較してさまざまなメリットが存在することを実感できました。

そこでここからは、個人的に大きなメリットであると感じたものについてピックアップして紹介いたします。

開発環境の構築が超簡単

コマンドを一発叩くだけで、開発環境の構築が完了するという優れものです。

これについての詳細は、後ほど実践編の方で進めていただくうちに実感いただけると思います。

ローカル環境でのテストが手軽にできる

Hardhatには、開発用に設計されたローカルなEthereumネットワークであるHardhat Networkが組み込まれています。

  • コントラクトのデプロイ
  • テストの実行
  • コードのデバッグ

といった作業を、簡単にローカル環境でおこなうことが可能です。

Truffleでの作業時のように、ローカル環境でのデプロイ/テストの際にGanacheを立ち上げるなどの作業が必要がない点は、非常に便利なので重宝しています。

また、「コードのデバッグ」に関して少し深掘りすると、Hardhat Network上ではhardhat/console.solをインポートすることで、バグを見つける際などに用いられるconsole.log()というコマンドを使うことができるようになります。

公式ドキュメントによると、public/view関数では使用できますが、pure関数では使えないそうです。
細かい仕様など知りたいは、以下のリンクからご確認ください。

You can use it in calls and transactions. It works with 「view」 functions, but not in 「pure」 ones.

引用:Hardhat Network Reference

便利なプラグインが豊富

例えば、Hardhatが提供してるhardhat-etherscanを利用すれば、EtherscanやPolygonscanでソースコードをVerify and Publishする必要がなくなるため、非常に便利です。

これに慣れてしまうと、Truffleでおこなっていた手動Verifyが面倒に感じてしまい、後戻りができなくなってしまいます。

他にも公式/コミュニティ作成のプラグインがいくつかありますので、興味のある方は以下の公式ドキュメントをご参考ください。
hardhat.org/plugins/

実践編①〜インストールしてみよう〜

ではここからは、実際にHardhatを使ったことがない方向けに、実践編としてHardhatのインストールから始めていきます。

今回、筆者の作業実行環境は以下です。

  • macOS Big Sur v11.6
  • Visual Studio Code – コード エディター
  • Node.js v14.17.3

下準備編

  1. お好きな場所にディレクトリ(ここでは「testHardhat」)を作成
  2. 「testHardhat」に移動
  3. Node.jsプロジェクトとして初期化
  4. hardhatをインストール
  5. 必要なパッケージをインストール
$ mkdir testHardhat
$ cd testHardhat
$ npm init -y
$ npm install --save-dev hardhat
$ npm install --save-dev @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai @openzeppelin/contracts

ちなみに今回インストールしたパッケージは以下で、これによりデプロイやテストで必要なもの、ERC721コントラクトなどが含まれた@openzeppelin/contractsなど一式を外部から取り込みました。

  • Ethers.js
  • Waffle
  • openzeppelin

今回は割愛しますが、メリットの項で紹介したhardhat-etherscanについても、必要であれば以下コマンドでインストールすることができますよ。

$ npm install --save-dev @nomiclabs/hardhat-etherscan

Hardhat/Hardhatのプラグインのインストールが完了したら、以下コマンドを実行してHardhatの開発環境の初期化をおこない、必要なファイルなどを自動生成してもらいます。

$ npx hardhat

「What do you want to do?」と問われるので、上から4番目の「Create an empty hardhat.config.js」を選択してください。

完了したら、VSCodeなどのコードエディターで「testHardhat」ディレクトリを展開します。

secrets.jsonファイルを作成

今回は上の記事を参考に、walletの秘密鍵や各API KEYの情報を管理するsecrets.jsonファイルを、最初に作成しておきます。

{
  "privateKey": "~",
  "polygonscanApiKey": "~",
  "etherscanApiKey": "~",
  "alchemyApiKey": "~"
}

各API KEYの取得まとめ

API KEYが未取得の方は、以下必要箇所の▼タブからご参考ください。
必要ない方は、次節「hardhat.config.jsについて」まで読み飛ばしてください。

alchemy

まずは、公式サイトから「Get started for free」をクリックします。

Googleサインアップ、もしくは各項目の手入力により、alchemyにサインアップします。

ログインが完了したら、右上の方にある「VIEW KEY」ボタンをクリックします。

すると、このようにAPIが表示されるので、このモザイク部分をsecrets.jsonファイルに記入(もしくは全箇所をhardhat.config.jsに直接記入)します。

Polygonscan

まずは公式サイトから、ユーザーネーム・メールアドレス・パスワードを入力して「Create an Account」をクリックします。

届いたメールを認証して、ログイン(Sign In)します。

「API-KEYs」をクリックします。

「Add」をクリックします。

AppNameを入力して「Continue」をクリックします。

以上でPolygonscanのAPI KEYが発行されたので、このモザイク部分をsecrets.jsonファイルに記入(もしくはhardhat.config.jsに直接記入)します。

Etherscan

Polygonscanの場合と同じ流れなので、公式サイトから同様の手順でAPI KEYを取得してください。

hardhat.config.jsについて

hardhat.config.jsには、Hardhatに関する諸々の設定が書かれており、簡潔に表現すると設計書/説明書のようなものです。

hardhat.config.jsではコンパイラの設定(バージョンや最適化の有無)、ネットワークの設定、Verify用のAPI KEYの設定をします。
ネットワークは Rinkeby、Polygonメインネット、Polygonテストネットを追加しておきました。

引用:Hardhatの使い方メモ (1) セットアップ~コンパイル

こちらのファイルを、以下のように書き換えて保存しておきましょう。

const { privateKey, polygonscanApiKey, etherscanApiKey, alchemyApiKey } = require("./secrets.json");

require("@nomiclabs/hardhat-waffle");
require('@nomiclabs/hardhat-ethers');
// require("@nomiclabs/hardhat-etherscan"); // Etherscan/PolygonscanでVerifyする際にコメントアウトを外して使用

module.exports = {
  solidity: {
    version: "0.8.7",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    hardhat: {},
    rinkeby: {
      url: "https://eth-rinkeby.alchemyapi.io/v2/" + alchemyApiKey,
      chainId: 4,
      accounts: [ privateKey ]
    },
    matic_mainnet: {
      url: "https://matic-mainnet.chainstacklabs.com",
      chainId: 137,
      accounts: [ privateKey ]
    },
    matic_testnet: {
      url: "https://matic-mumbai.chainstacklabs.com",
      chainId: 80001,
      accounts: [ privateKey ]
    }
  },
  // etherscan: {
    // apiKey: etherscanApiKey // EtherscanでVerifyする際にコメントアウトを外して使用
    // apiKey: polygonscanApiKey // PolygonscanでVerifyする際にコメントアウトを外して使用
  // }
};

実践編②〜ローカル環境にコントラクトをデプロイしてみよう〜

ローカル環境でのテストが手軽にできることは、Hardhatの大きな魅力の一つです。

そこでまずはそれを体験するために、ローカル環境にコントラクトをデプロイしてみましょう。

まずはルートディレクトリに、「contracts」「scripts」ディレクトリを作成しておきます。

$ mkdir contracts scripts

次に、contractsディレクトリに簡易なコントラクトを作成していくので、「TestNFT.sol」というファイルを作成します。

$ touch contracts/TestNFT.sol

次項の実践編③でフルオンチェーンNFTをEthereum Testnet (Rinkeby)でMintしてみるので、それ専用の以下コードをコピペしてください。

細かいコードの解説はここでは割愛しますが、需要が多ければ別途記事で解説します。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./Base64.sol";

contract TestNFT is ERC721("TestNFT", "TNFT") {
    uint256 public nextTokenId = 0;

    function mint() external {
        uint256 tokenId = nextTokenId;
        nextTokenId++;
        _safeMint(_msgSender(), tokenId);
    }

       function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory svg = getSVG();
        bytes memory json = abi.encodePacked(
            '{"name": "TestNFT #',
            Strings.toString(tokenId),
            '", "description": "TestNFT is a full on-chain text NFT.", "image": "data:image/svg+xml;base64,',
            Base64.encode(bytes(svg)),
            '"}'
        );
        return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json)));
       }

        function getSVG() public pure returns (string memory) {
            return string(abi.encodePacked(
                '<svg width="300" height="300" viewBox="0, 0, 300, 300" xmlns="http://www.w3.org/2000/svg">',
                '<rect width="100%" height="100%" fill="#f2eecb" />',
                '<foreignObject x="10" y="10" width="280" height="280">',
                '<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:x-small;text-indent:1em">',
                '<p>A. D. 10,000. more than six hundred years of age, was walking with a boy through a great museum. The people who were moving around them had beautiful forms, and faces which were indescribably refined and spiritual.</p>',
                '<p>"Father," said the boy, "you promised to tell me to-day about the Dark Ages. I like to hear how men lived and thought long ago."</p>',
                '<p>"It is no easy task to make you understand the past," was the reply. "It is hard to realize that man could have been so ignorant as he was eight thousand years ago, but come with me; I will show you something."</p>',
                '<p>He led the boy to a cabinet containing a few time-worn books bound in solid gold.</p>',
                '</div></foreignObject></svg>'
            ));
        }
    }

すると以下写真のように、import "./Base64.sol";に赤線が引かれてしまうでしょう。

これは「importと書かれているけどBase64.solがどこにも見当たりませんよ〜」という警告なので、Base64.solをcontractsディレクトリに追加しておく必要があります。

Base64.solファイルを作成して、以下をそのままコピペして保存してください。

// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0;

/// @title Base64
/// @author Brecht Devos - <brecht@loopring.org>
/// @notice Provides functions for encoding/decoding base64
library Base64 {
    string internal constant TABLE_ENCODE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    bytes  internal constant TABLE_DECODE = hex"0000000000000000000000000000000000000000000000000000000000000000"
                                            hex"00000000000000000000003e0000003f3435363738393a3b3c3d000000000000"
                                            hex"00000102030405060708090a0b0c0d0e0f101112131415161718190000000000"
                                            hex"001a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132330000000000";

    function encode(bytes memory data) internal pure returns (string memory) {
        if (data.length == 0) return '';

        // load the table into memory
        string memory table = TABLE_ENCODE;

        // multiply by 4/3 rounded up
        uint256 encodedLen = 4 * ((data.length + 2) / 3);

        // add some extra buffer at the end required for the writing
        string memory result = new string(encodedLen + 32);

        assembly {
            // set the actual output length
            mstore(result, encodedLen)

            // prepare the lookup table
            let tablePtr := add(table, 1)

            // input ptr
            let dataPtr := data
            let endPtr := add(dataPtr, mload(data))

            // result ptr, jump over length
            let resultPtr := add(result, 32)

            // run over the input, 3 bytes at a time
            for {} lt(dataPtr, endPtr) {}
            {
                // read 3 bytes
                dataPtr := add(dataPtr, 3)
                let input := mload(dataPtr)

                // write 4 characters
                mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
                resultPtr := add(resultPtr, 1)
                mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F))))
                resultPtr := add(resultPtr, 1)
                mstore8(resultPtr, mload(add(tablePtr, and(shr( 6, input), 0x3F))))
                resultPtr := add(resultPtr, 1)
                mstore8(resultPtr, mload(add(tablePtr, and(        input,  0x3F))))
                resultPtr := add(resultPtr, 1)
            }

            // padding with '='
            switch mod(mload(data), 3)
            case 1 { mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) }
            case 2 { mstore(sub(resultPtr, 1), shl(248, 0x3d)) }
        }

        return result;
    }

    function decode(string memory _data) internal pure returns (bytes memory) {
        bytes memory data = bytes(_data);

        if (data.length == 0) return new bytes(0);
        require(data.length % 4 == 0, "invalid base64 decoder input");

        // load the table into memory
        bytes memory table = TABLE_DECODE;

        // every 4 characters represent 3 bytes
        uint256 decodedLen = (data.length / 4) * 3;

        // add some extra buffer at the end required for the writing
        bytes memory result = new bytes(decodedLen + 32);

        assembly {
            // padding with '='
            let lastBytes := mload(add(data, mload(data)))
            if eq(and(lastBytes, 0xFF), 0x3d) {
                decodedLen := sub(decodedLen, 1)
                if eq(and(lastBytes, 0xFFFF), 0x3d3d) {
                    decodedLen := sub(decodedLen, 1)
                }
            }

            // set the actual output length
            mstore(result, decodedLen)

            // prepare the lookup table
            let tablePtr := add(table, 1)

            // input ptr
            let dataPtr := data
            let endPtr := add(dataPtr, mload(data))

            // result ptr, jump over length
            let resultPtr := add(result, 32)

            // run over the input, 4 characters at a time
            for {} lt(dataPtr, endPtr) {}
            {
               // read 4 characters
               dataPtr := add(dataPtr, 4)
               let input := mload(dataPtr)

               // write 3 bytes
               let output := add(
                   add(
                       shl(18, and(mload(add(tablePtr, and(shr(24, input), 0xFF))), 0xFF)),
                       shl(12, and(mload(add(tablePtr, and(shr(16, input), 0xFF))), 0xFF))),
                   add(
                       shl( 6, and(mload(add(tablePtr, and(shr( 8, input), 0xFF))), 0xFF)),
                               and(mload(add(tablePtr, and(        input , 0xFF))), 0xFF)
                    )
                )
                mstore(resultPtr, shl(232, output))
                resultPtr := add(resultPtr, 3)
            }
        }

        return result;
    }
}

するとTestNFT.solの赤線エラーが消えるはずですが、もし消えない場合は一度全カット&ペーストしてみてください。

最後に、以下コマンドを実行してコンパイルをおこない、Solidityのコードをデプロイできる形にします。

$ npx hardhat compile
成功するとこんなふうに表示されます。

コンパイルと聞くと難しそうな印象ですが、要は機械が読める形式に翻訳してあげる作業というイメージです。

これでコントラクトをデプロイする準備が完了したので、次はいよいよデプロイ手順に移ります。

まずは以下のコマンドを実行して、scriptsディレクトリにdeploy.jsファイルを作成します。

$ touch scripts/deploy.js

続いて、作成したdeploy.jsファイルに以下をコピペして保存してください。

async function main() {
    const factory = await ethers.getContractFactory("TestNFT");
    const contract = await factory.deploy();
    await contract.deployed();
    console.log("Deployed to:", contract.address)
  }
  main()
    .then(() => process.exit(0))
    .catch((error) => {
      console.error(error);
      process.exit(1);
    });

上のコードを簡潔に説明すると、TestNFTというコントラクトをデプロイしてみて、成功したら「Deployed to:(コントラクトアドレス)」を返しますが、失敗したらエラーメッセージを返すというものです。

ではローカル環境でデプロイしてみるために、まずは以下コマンドでローカルネットワークを起動します。

$ npx hardhat node

するとこのように、ローカル環境用に10,000ETHが入ったアカウントが20個作成されます。

このローカルネットワークを立ち上げたまま、別のターミナルシェルを開いて以下コマンドを実行してみてください。

$ npx hardhat run scripts/deploy.js --network localhost

ローカル環境の場合は、--network localhost は省略してもOKです。

「Deployed to:(コントラクトアドレス)」と返されたらローカル環境でのデプロイ成功です。

このようにHardhatを使うことで、簡単にローカル環境でのデプロイを試すことができるので、非常に便利です。

最後に、コントラクトのデプロイからNFTのMint、そこで生成されたsvg画像の保存まで一括でおこなえるスクリプトを、「svgtest.js」という別のファイルにコピペ保存しておいてください。

async function main() {
    const fs = require("fs");
    const factory = await ethers.getContractFactory("TestNFT");
    const contract = await factory.deploy();
    console.log("Deployed to:", contract.address);
    await contract.mint();
    const svg = await contract.getSVG();    
    fs.writeFileSync("test.svg", svg);
  }
  main()
    .then(() => process.exit(0))
    .catch((error) => {
      console.error(error);
      process.exit(1);
    });

完了したら、先ほどと同じ手順でローカルネットワークを立ち上げたまま、別のターミナルシェルを開いて以下コマンドを実行してみてください。

$ npx hardhat run scripts/svgtest.js --network localhost

成功すると、「test.svg」というファイルが生成されているので、こちらをGoogle Chromeなどで開いてみると実際の画像が確認できます。

このように、何度もローカル環境で試しながら、文字の大きさや色、配置などを調整していくことができるので、無駄にgas代などを消費することなく、かつ簡単・手軽に見た目の変更ができるのは嬉しいポイントですね。

今回はこのまま次項でテストネットへデプロイしていきますが、納得のいく見た目になるまでTestNFT.solファイルの<svg></svg>で囲った部分を変更して遊んでみると、理解も深まるので時間のある方はやってみてください。

実践編③〜フルオンチェーンNFTをMintしてOpenSeaで確認してみよう〜

ローカル環境でのテストが完了したら、次はいよいよテストネットへデプロイしてNFTをMintし、さらにOpenSeaで確認するところまでやってみましょう。

執筆時点では、OpenSeaのテストネット環境でサポートされているのは、以下の3つです。

  1. Rinkeby(Ethereum)
  2. Mumbai(Polygon)
  3. Baobab

今回は、Rinkeby(Ethereum)にコントラクトをデプロイして、SVG形式のフルオンチェーンNFTをMintしてみたいと思います。

derio

Mumbai(Polygon)でも良いのですが、Polygonチェーンはメインネット/テストネットどちらも挙動が安定しないことが多いので、個人的にあまり使わないようにしています。

Rinkeby(Ethereum)にデプロイするためには、テストネット用のETHが必要になります。
まだお持ちでない方は、以下サイトなどを通して入手しておいてください。

あとは、非常に簡単な作業です。

実は準備編のところで、すでにhardhat.config.jsファイルにEthereum Testnet (Rinkeby) にデプロイするための情報を記載済みです。

ということで、ローカル環境ではnpx hardhat run scripts/svgtest.js --network localhostだったところを、npx hardhat run scripts/svgtest.js --network rinkebyに変えて実行するだけでOKです!

$ npx hardhat run scripts/svgtest.js --network rinkeby

ローカル環境とは異なり、テスト環境の場合は少し時間がかかります。

デプロイが成功したらコントラクトアドレスが表示されるので、こちらをコピーしておきます。

続いて、テストネット版OpenSeaを開き、右上にあるプロフィールにある「My Collections」をクリックします。

既にコレクションに表示されていれば良いですが、まだ何も表示されていない方は「Import an existing smart contract」をクリックしてください。

今回はテストネットなので、真ん中をクリックします。

あとはコピーしたコントラクトアドレスをペーストして「Submit」をクリックすれば完了です。

「We couldn’t find this contract. Please ensure that this is a valid ERC721 or ERC1155 contract deployed on Rinkeby and that you have already minted items on the contract.」
と表示される場合は、半日もしくは1日経過してから再度試してみると成功することが多いです。

成功すると、このようにOpenSeaのコレクションとして表示され、当然ながらroyaltyなども設定できるようになっています。

ここまでの一連の作業を終えると、とても感慨深いものがありますよね。お疲れ様でした。

derio

さらに画質の良いものを「フルオンチェーンNFT(Ethereum)」としてmintしてみたい場合は、以下の記事も参考にしてみてください。

まとめ

本記事では、Hardhatの概要やメリット、また実践編などについて解説しました。

本記事が、Hardhatの使い方やローカル環境/テストネットへのデプロイから確認までの一連の流れなどについて理解したいと思われている方にとって、少しでもお役に立ったのであれば幸いです。

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

また今回の記事は、読んでくださった皆さんからのフィードバックによってリライトし続けていくことで、日本語版のHardhatチュートリアル記事として仕上げていきたいとも考えております。

ここまで読んでこられた中で、分かりづらかったところや間違っていると思われる箇所など、ぜひご指摘いただけますと幸いです。

イーサリアムnaviを運営するSTILL合同会社では、web3/crypto関連のリサーチ代行、アドバイザー業務、その他(ご依頼・ご提案・ご相談など)に関するお問い合わせを受け付けております。

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

みんなにも読んでほしいですか?
  • URLをコピーしました!
目次