こんにちは、フルオンチェーンNFTクリエイターのnawooです。
先日、GoerliテストネットでNFTのデプロイをおこなっていた際に、「ガス代が不足している」というエラーが出てしまいました。
確認してみると、ガス価格がなんと100gweiを超えていたのです。

以前は0.0000001gweiぐらいと安かったのですが、最近は時間帯によってすごく高くなっているようです。
これがEthereumメインネットであれば、こういう時に筆者は以下の「GasNow」というサイトを確認しています。


GasNowでは、1週間分のガス価格を1時間ごとのヒートマップで確認できるので、「平日の夜は高いな」「土日の昼間は安いぞ」といった情報が、一目で分かるようになっています。
しかし、残念ながらGoerliテストネット版のGasNowのようなwebサイトは、執筆時点では見つけられませんでした。



Goerliテストネットでもこういうサイトがあるかと思ったのですが、見つけられなかったため、今回は勉強を兼ねて自分で作ってみることにしました。
こうして完成したサイトが、以下の「Goerli Gas Heatmap」です。





できるだけ簡単に作りたかったので、ガス価格のヒートマップを表示するだけのシンプルなサイトにしました。
また、ネットワークはGoerliのみです。1時間ごとのガス価格は、単純に毎時00分の最新ブロックのガス価格を使うことにしました。


でははじめに、この記事の構成について説明します。
さらに、今回の開発をおこなうに当たり必要となるアカウント作成やAPIキーの取得、設定などをおこなっていきます。
続いてバックエンド開発として、Next.jsでAPIを作り、それをVercel Serverless Functionsで実行し、そしてその後でAPIを定期的に呼び出すためにQStashを使っていきます。
最後に、STEP3で作成したデータが溜まるまでの時間を使い、データベースから情報を読み込んでヒートマップを表示するサイトを作成します。
本記事が、ガス価格のヒートマップを表示するサイトの構築方法、ならびにweb3サービスの具体的な開発手順などについて理解したいと思われている方にとって、少しでもお役に立てれば幸いです。
※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。


開発計画
今回のサイトでは、バックエンドとフロントエンドの両方が必要です。
- バックエンド
- 1時間ごとにガス価格を取得してデータベースに保存する機能
- フロントエンド
- データベースから過去のガス価格を読み込んでヒートマップを表示するWebサイト





さて、これをどうやって実現すればいいか、あれこれ調べてみたところ、以下の参考になりそうな記事を見つけました。
こちらの記事では、バックエンドで1時間ごとにBTC価格を取得してデータベースに保存する方法が、分かりやすく解説されています。
なお、上記事で採用している技術スタックは、以下の通りです。
- BTC価格の取得
- データベース
- 定期的な実行
- Webサイト



すべて無料で使えるサービスです。筆者も上記事と全く同じ方法を採用することにしました。


また、ブロックチェーンへのアクセスには、Alchemyのノードとethers.jsを使いました。
準備
まずは、Alchemy, Upstash, Vecelでそれぞれのアカウントが必要です(いずれも無料で作成できます)。
AlchemyのAPIキーを取得
Alchemyのダッシュボードを開きます。
AppのVIEW KEYボタンをクリックします。(Appが無ければCREATE APPボタンで作成してください。)


CopyボタンでAPI KEYをコピー・メモしておきます。


Upstash Redisの設定
Upstash Redisのコンソールを開きます。Create databaseボタンをクリックします。


NameとRegionを設定して、Createボタンをクリックします。


Details画面が開くので、REST APIのUPSTASH_REDIS_REST_URL
とUPSTASH_REDIS_REST_TOKEN
をそれぞれコピー・メモしておきます。


Upstash QStashのキーを取得
Upstash QStashのコンソールを開きます。
Request BuilderのSIGNING KEYS
をクリックすると、QSTASH_CURRENT_SIGNING_KEY
とQSTASH_NEXT_SIGNINIG_KEY
が表示されるので、こちらもコピーしてメモしておきます。


これで準備は完了です。いよいよバックエンドの開発に進みます。
バックエンド開発
Next.jsの設定
Next.jsのAPI機能で、update APIを作成します。



ということで、最初にNext.jsのプロジェクトを作成します。 プロジェクト名は「goerli-gas-heatmap」にしました。
npx create-next-app --ts
続いて、必要なパッケージをインストールします。
npm install @upstash/redis @upstash/qstash ethers
/pages/api
ディレクトリに、update.ts
ファイルを作成します。このファイルがupdate APIになります。
update APIを作成
update.ts
のコードは、以下の通りです(それぞれの詳細は後述します)。
- (A) RPC URLを取得します
- (B)
eth_feeHistory
で最新のガス価格を取得します - (C) ガス情報(
feeInfo
)を作成します - (D) Redisデータベースにガス情報を保存します
- (E) ステータスコードとガス情報を
response
に設定します - (F)
verifySignature
で呼び出し元をチェックします
import type { NextApiRequest, NextApiResponse } from "next";
import { Redis } from "@upstash/redis/with-fetch";
import { ethers } from "ethers";
import { verifySignature } from "@upstash/qstash/nextjs";
const MAX_DATA_COUNT = 24 * 30; // 最大30日分をデータベースに保存します
async function handler(req: NextApiRequest, res: NextApiResponse) {
// (A) alchemyのRPC URLを取得します
const alchemyApiKey = process.env.ALCHEMY_API_KEY || "";
if (alchemyApiKey === "") {
res.status(500).send("ALCHEMY_API_KEY is empty");
return;
}
const url = `https://eth-goerli.g.alchemy.com/v2/${alchemyApiKey}`;
// (B) eth_feeHistoryを呼び出します
const provider = new ethers.providers.JsonRpcProvider(url);
const feeHistory = await provider.send("eth_feeHistory", [
1,
"latest",
[20, 50, 80],
]);
// (C) ガス情報(feeInfo)を作成します
const feeInfo = {
block: Number(feeHistory.oldestBlock),
time: new Date().setUTCMinutes(0, 0, 0),
base: Number(feeHistory.baseFeePerGas[0]),
rewards: feeHistory.reward[0].map((s: any) => Number(s)),
};
// (D) Redisにガス情報を保存します
const redis = Redis.fromEnv();
const redisKey = `goerli-hourly`;
const lastFeeInfo = await redis.lindex(redisKey, 0);
const lastTime = lastFeeInfo == null ? 0 : lastFeeInfo.time;
if (lastTime !== feeInfo.time) {
await redis.lpush(redisKey, feeInfo);
await redis.ltrim(redisKey, 0, MAX_DATA_COUNT - 1);
}
// (E) status 200を返します (デバッグ用にfeeInfoも返す)
res.status(200).json(feeInfo);
}
// (F) API呼び出し元がQStashかどうかをチェックします
export default verifySignature(handler);
export const config = {
api: {
bodyParser: false,
},
};
(A) AlchemyのRPC URLを取得
環境変数からAlchemyのAPIキーを読み込んでURLを作成します。
環境変数で直接URLを指定してもよいのですが、今後、Goerli以外にEthereumメインネットなど他のネットワークにも対応させるときのことを考えて、APIキーからURLを作成するようにしました。
(B) eth_feeHistoryでガス価格を取得
EIP-1559とガス価格について
ロンドンアップデートで導入されたEIP-1559で、ガス価格は基本料金(Base fee)とチップ(Priority fee)の2つに分かれました。



下の画像を見ると、基本料金(Base)は3つとも同じ18ですが、チップ(Priority)が多いほどブロックに取り込まれるまでの時間が短くなっていることが分かります。


基本料金は同じブロックなら共通ですが、ブロックの混雑具合によって次のブロックの基本料金が増減する仕組みになっています。
- ブロックのガス使用率が50%より高い場合
- 次のブロックの基本料金は上がって高くなる
- ガス使用率が50%より低い
- 次のブロックの基本料金は下がって安くなる


例えば上写真の場合、ガスの上限(Gas Limit)が30,000,000ありますが、ガス使用量(Gas Used)は10,814,605であり、使用率は36.05%となっています。
よって「50%より低いため、次のブロックの基本料金は下がる」ということが分かります。
eth_feeHistoryについて
ガス価格を取得するために eth_feeHistory
メソッドを使います。
eth_feeHistory
では、複数ブロックの「BaseFee」と「PriorityFeeの分布」をまとめて取得できます。



ethers.jsでは関数として定義されていないようなので、今回はsend関数を使いました。
const feeHistory = await provider.send("eth_feeHistory", [
3, // BlockCount
"latest", // NewestBlock
[20, 50, 80], // RewardPercentiles
]);
同じブロックであれば基本料金(BaseFee)は同じですが、チップ(PriorityFee)はトランザクションごとに違うので、パーセンタイルを指定して分布を取得するようになっています。上の例では、20・50・80パーセンタイルのチップ(PriorityFee)を取得しています。



BlockCount
は、最大1024ブロックまで指定できるようです。また、NewestBlock
はブロック番号を指定することもできますし、上のように"latest"
を指定すると最新ブロックになります。
戻り値のfeeHistory
は、以下のようになります。
{
oldestBlock: '0x7955eb',
reward: [
[ '0x59682f00', '0x59682f00', '0x59682f00' ],
[ '0x59682f00', '0x59682f00', '0x9502f900' ],
[ '0x59682f00', '0xa4443ed4', '0xa4443ed4' ]
],
baseFeePerGas: [ '0x139a6dc6ed', '0x12be725db3', '0x12fc375570', '0x12710abae0' ],
gasUsedRatio: [ 0.3246606, 0.5514911666666666, 0.3854584 ]
}
見やすいように10進数に変換すると、以下のようになります。
{
oldestBlock: 7951851,
reward: [
[ 1500000000, 1500000000, 1500000000 ],
[ 1500000000, 1500000000, 2500000000 ],
[ 1500000000, 2755935956, 2755935956 ]
],
baseFeePerGas: [ 84195264237, 80504577459, 81540896112, 79205939936 ],
gasUsedRatio: [ 0.3246606, 0.5514911666666666, 0.3854584 ]
}
ご覧の通り、こちらには3ブロック分の情報が含まれています。
oldestBlock
- 一番古いブロック番号
- 取得した3ブロックは 7951851, 7951852, 7951853 であることがわかります。
- 一番古いブロック番号
reward
- 3ブロックの20・50・80パーセンタイルのチップ(PriorityFee)
baseFeePerGas
- 3ブロック+次のブロックの基本料金(BaseFee)
gasUsedRatio
- 3ブロックの混雑具合(ガス使用量の比率)



ちなみにbaseFeePerGas
は要素数が4つありますが、これは3ブロックに加えて、次のブロックのBaseFeeが含まれているからです。
<補足>
次のブロックのBaseFeeはどうやって決まるのか?
先ほど「ブロックの混雑具合によって次のブロックの基本料金が増減するしくみになっています。」と書きましたが、より詳しく説明すると、現在のブロックのgasUsedRatio
によって、次のブロックのBaseFee
は+12.5%から-12.5%の範囲で増減します。
- gasUsedRatio=1.0 → 次のブロックのBaseFeeは +12.5%
- gasUsedRatio=0.5 → 次のブロックのBaseFeeは ±0%
- gasUsedRatio=0.0 → 次のブロックのBaseFeeは -12.5%
上の例では ブロック番号 7951853 の baseFee=81540896112, gasUsedRatio=0.3854584 なので、 BaseFeeの変化率は -12.5% * (0.5-0.3854584)/0.5 = -2.86354%
になります。
つまり、次のブロックのBaseFeeは 81540896112 * (100-2.86354)/100 = 79205939936
と求めることができます。
上記の戻り値のbaseFeePerGas
の4番目の値とも一致しています。
ヒートマップの作成に話を戻します。
今回は、1時間ごとに最新ブロックの情報を取得すればよいので、BlockCount
は1
、NewestBlock
は"latest"
を指定し、チップの分布は20・50・80パーセンタイルを指定しておきます。



20がSlow、50がAverage、80がFastのイメージです。 (Etherscan等で表示されているSlow、Average、Fastのガス価格はもっと複雑な計算をしているようですが、今回は単純にしています。)
const feeHistory = await provider.send("eth_feeHistory", [
1,
"latest",
[20, 50, 80],
]);
基本料金+20パーセンタイルのチップの合計値を代表的なガス価格として、ヒートマップで使用します。
50・80パーセンタイルは、詳細情報(ツールチップ)で表示することにしました。



最初は50パーセンタイルの値をヒートマップに使っていたのですが、ちょっと高すぎるように感じたので20パーセンタイルに変更しました。
<補足>
provider.getFeeData()関数によるガス価格の取得
ethers.jsではprovider.getFeeData()
という関数があり、こちらでもガス価格を取得することができます。
const feeData = await provider.getFeeData();
// {
// lastBaseFeePerGas: 115210245590,
// maxFeePerGas: 231920491180,
// maxPriorityFeePerGas: 1500000000,
// gasPrice: 115311813421
// }
(C) ガス情報(feeInfo)を作成
次にeth_feeHistory
で取得したガス情報を、データベースに保存するためのfeeInfo
というオブジェクトに変換します。
feeInfo
の型は、以下のようになっています。
{
block: number; // ブロック番号
time: number; // 現在日時の年・月・日・時 までをtimestampにしたもの
base: number; // baseFee (単位はwei)
rewards: number[] ; // rewards 20・50・80パーセンタイル (単位はwei)
}
1時間ごとのガス価格を保存するので、timeの分・秒・ミリ秒の値は0にしています。



実際に実行された時刻が 9:00:05.345 であっても、9:00:00.000 として保存します。
これによって、仮に9時台に複数回実行された場合でも、データベースには1件だけ保存されるようにします。
(D) Redisデータベースに保存
Redisは、いわゆるNoSQLと言われるタイプのデータベースです。



Redisのデータ型にはいろいろありますが、今回はリスト型(Lists)を使いました。
1時間ごとのガス情報(feeInfo
オブジェクト)をリストに追加し、新しいデータがリストの左側に来るようにします。
さらに30日分(30*24=720件)より前のデータは削除しておきます。
ここで使うのは、データの読み込みlindex
、データの追加lpush
、データのトリムltrim
だけです。Object型をそのままデータとして使うことができます。
// 最新データを読み込む(なければnull)
await redis.lindex(key, 0);
// リストの左にデータを追加
await redis.lpush(key, data);
// トリムする(x件を超えた古いデータを削除)
await redis.ltrim(key, 0, x);
実際のコードを見てみましょう。こちらでは、以下の処理をおこなっています。
- データベースから最新データ(
lastFeeInfo
)を読み込む - 現在のデータと
time
を比較して、異なる場合はデータベースに書き込み+トリム
const redis = Redis.fromEnv();
const redisKey = `goerli-hourly`;
const lastFeeInfo = await redis.lindex(redisKey, 0);
const lastTime = lastFeeInfo == null ? 0 : lastFeeInfo.time;
if (lastTime !== feeInfo.time) {
await redis.lpush(redisKey, feeInfo);
await redis.ltrim(redisKey, 0, MAX_DATA_COUNT - 1);
}
1時間ごとに実行されるので、同じ時間帯のデータが重複することは基本的には無いのですが、9:00に実行されるはずが、8:59に実行されてしまうようなケースを考慮して、time
のチェックをしています。



ひとつ沼にハマったのが、参考サイトの通りにするとfetch is not defined
エラーが出てしまったことです。
これは、VercelのNode.jsのバージョンが16だったことが原因だったため、以下のようにimportを修正することで対応できました。
import { Redis } from '@upstash/redis';
↓
import { Redis } from '@upstash/redis/with-fetch';
(E) ステータスコードとガス情報をresponseに設定
responseにはステータスコードだけを設定しても良いのですが、デバッグのためにガス情報も設定しています。
(F) verifySignatureで呼び出し元をチェック
作成したAPIは、そのままの状態では誰でも実行できてしまうので、QStash以外から呼び出せないようにします。



verifySignature
を使って、QStashからの呼び出しかどうかをチェックします。
逆に、誰でも実行できるようにしたい場合は、以下のようにします。
export default verifySignature(handler);
↓
export default handler;
Vercelにデプロイ
ここまでに作成したNext.jsのコードをGitHubにプッシュしておきます。
Vercelのダッシュボードを開き、Continue with GitHubボタンをクリックして、リポジトリを選択します。


続いて、Environment Variablesに以下5つの内容を追加します。
ALCHEMY_API_KEY
: AlchemyのダッシュボードでコピーしたAPI KEYUPSTASH_REDIS_REST_URL
: Upstash Redisのコンソールでコピーした内容UPSTASH_REDIS_REST_TOKEN
: 〃QSTASH_CURRENT_SIGNING_KEY
: Upstash QStashのコンソールでコピーした内容QSTASH_NEXT_SIGNING_KEY
: 〃
それぞれNameとValueを設定してAddボタンをクリックします。


Deployボタンをクリックして、しばらく待つとデプロイされます。
デプロイが完了すると、下の画面が表示されるので、ドメインを確認します。


今回はドメインがgoerli-gas-heatmap.vercel.app
なので、作成したAPIのURLは https://goerli-gas-heatmap.vercel.app/api/update
になります。



このURLは次で使うので、メモしておきましょう。
QStashでスケジュールを追加
Upstash QStashのコンソールを開きます。
作成したAPIのURLを設定し、TypeとEveryを下の画像のように設定したら、Scheduleボタンをクリックします。


QStashの無料枠では、1日500回までしか使うことができませんが、この設定なら1日24回だけなので問題ありません。
これでバックエンドは完了です。 あとは何もしなくても自動的に1時間ごとにAPIが実行されて、データが蓄積されていきます。
データが溜まるまでの時間でフロントエンドを作っていきましょう。
フロントエンド開発
フロントエンドでは、データベースから情報を読み込んで、ヒートマップを表示するサイトを作成します。 APIと同じNext.jsのプロジェクトを使います。
Heatmapライブラリについて
Next.js(React)で使えるヒートマップ用のライブラリを探してみたところ、以下のようにいくつか見つかりました。
データ読み出し用のAPIを作成
データベースから情報を読み込む際も、データ読み出し用のAPIを作っておくと便利です。
update APIと同じように、/page/api
ディレクトリにget.ts
ファイルを作成します。
これがget APIとなり、このAPIはデータベースに保存された全データを読み込んで、JSON形式で返します。
import type { NextApiRequest, NextApiResponse } from "next";
import { Redis } from "@upstash/redis/with-fetch";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const redis = Redis.fromEnv();
const key = "goerli-hourly";
const data = await redis.lrange(key, 0, -1);
res.status(200).json(data);
}
RedisでLists型のデータを読み込む際にはlrange
を使います。



0から-1までを指定すると、最初から最後までの全データを読み込みます。
await redis.lrange(key, 0, -1);
メインページを作成
1ページだけのサイトなので、pages/index.tsx
に書いています。
- データ読み出し用のget APIを使って、データを読み出します。
- ヒートマップ用にデータを加工します。
- ヒートマップコンポーネントにデータを渡して表示します。
データ読み出し用APIの利用
APIを使ってデータを読み込む際は、SWRを使うと簡単です。
APIのパス(/api/get
)を指定するだけで、get APIを呼び出して、結果をFeeInfo[]
型として受け取ることができます。
const { data } = useSWR<FeeInfo[], Error>(`/api/get`);
ヒートマップコンポーネント
ヒートマップコンポーネントは、components/heatmap.tsx
にあります。
こちらには、直近8日分×24時間分の<div>
タグを並べており、親<div>
タグのスタイルには、display:flex
を指定しています。



また、レスポンシブ対応で、ブラウザの横幅によってセルサイズが変化するようにしました。
詳細情報(ツールチップ)は、JavaScriptやCSSではなく<div title="〜">
に設定しているだけです。
この方法では、ツールチップが表示されるまで少し待たされるのが難点なので、今後のバージョンアップでCSSを使ってもっと素早く表示されるツールチップに改良していきたいと考えています。


また、ヒートマップの凡例の数値は、0.1 gwei、1 gwei、10 gwei、100 gwei、300 gweiにしています(数値は固定)。



私の個人的な感覚で決めましたが、例えば、過去1週間分の高値安値から自動的に数値を設定する、といった方法もあると思います。これも今後の課題でしょうか。
Vercelにデプロイ
APIと同じで、GitHubにpushすれば自動的にデプロイされます。
Vercelのドメインが goerli-gas-heatmap.vercel.app
なので、サイトのURLはそのまま https://goerli-gas-heatmap.vercel.app/
になります。
まとめ


Nouns DAO JAPANは世界で一番Nounsを広げるコミュニティを目指します。Discord参加はこちら
さて、以上で「Goerliテストネット版のGasNowのようなwebサイト」の完成です。



シンプルですが便利なサイトになったのではないでしょうか? また、今回は趣味の開発なので、時間をかけずに完成させることを優先しました。
あちこち手抜き仕様ですし、テストやエラー処理なども省略していますが、本記事がガス価格のヒートマップを表示するサイトの構築方法、ならびにweb3サービスの具体的な開発手順などについて理解したいと思われている方にとって、少しでもお役に立ったのであれば幸いです。
励みになりますので、参考になったという方はぜひTwitterでのシェア・コメントなどしていただけると嬉しいです。
🆕記事をアップしました🆕
— イーサリアムnavi🧭 (@ethereumnavi) November 25, 2022
今回は、Goerliのガス価格をヒートマップで確認できるサイトを作り、その構築方法ならびにweb3サービスの具体的な開発手順などについて解説しました📝
実はありそうでなかったサービス💫
テストネットのgas代を調べる際に、ご活用いただけます⛽️https://t.co/wUeHvqZsEO


イーサリアムnaviを運営するSTILL合同会社では、以下などに関するお問い合わせを受け付けております。
- 広告掲載
- リサーチ代行業務
- アドバイザー業務
- その他(ご依頼・ご提案・ご相談など)
まずはお気軽に、ご連絡ください。
- Webサイト:still-llc.co.jp
- Twitter:@STILL_Corp
- メールアドレス:info@still-llc.com