コンテンツへスキップ

Ether Eyes Snap:将来のガス代を予測

Inter IIT ハッカソン優勝

読了時間:13分
Ether Eyes Snap:将来のガス代を予測

MetaMask Snapsは、MetaMaskを世界で最も拡張性の高いウォレットにするためのロードマップです。開発者として、まったく新しい方法でMetaMaskに機能やAPIを追加することができます。Web3開発者はこの成長の中核であり、本シリーズは現在開発中の革新的なMetaMask Snapsを紹介することを目的としています。

Ether Eyes Snap

Snap Repo: https://github.com/aritroCoder/EtherEyes

なぜ開発しようと思ったのですか?

Ethereumブロックチェーンのガス代は予測が非常に難しく、支払いを行う際の障壁となっています。ユーザーが支払いを行おうとする際、必要以上に高い手数料を支払ってしまうことがあります。ブロックチェーン上の短時間の高トラフィックは、ガス代に大きな影響を与える可能性があります。L2はガス代を削減するために開発されましたが、トランザクション目的でL2を使用するよりも、より汎用的で簡単なソリューションが必要な場合もあります。過去のデータと現在のトレンドに基づいて将来のガス代を予測できるアプリがユーザーにとって有用だと考えました。

技術的な実装について説明していただけますか?

EtherEyesは4つのパーツで構成されています:ガス代サーバー、Snap、SARIMAモデル、Webフロントエンドです。ガス代サーバーは過去および現在のガス代に関するデータを提供します。ウェブサイトはSnapをホストし、Snap APIメソッドを呼び出すことでSnapをインストールして操作するためのインターフェースを提供します。ガス代の予測には、季節性自己回帰和分移動平均(SARIMA)モデルを使用しました。

ガス代サーバー

Owlracle APIを使用して、約200本のローソク足(各ローソク足は過去30分間のデータの集計)のリストとしてリアルタイムのガス代データを収集します。これにより、過去100時間のガス代の時系列データが得られます。

Snap

Snapでは、トランザクションインサイトAPIを使用してリアルタイムのガス代予測を提供しました。Snapのフローは以下の通りです:

まず、ユーザーがトランザクションを開始すると、onTransactionHandlerを使用して実装された確認ページにトランザクションインサイトが表示されます。ここでは、次の30分および60分の予測についてユーザーに表示します。実装した内容の簡略版を以下に示します:

export const onTransaction: OnTransactionHandler = async ({ transaction }) => {
 let state: { notifToggle: boolean; urgency: number; lowEth: number };

 state = (await wallet.request({
   method: 'snap_manageState',
   params: ['get'],
 })) as { notifToggle: boolean; urgency: number; lowEth: number };

 if (!state) {
   state = {
     notifToggle,
     urgency,
     lowEth,
   };
 }

 let insights: any = {
   type: 'Gas Fee estimation',
 };

 if (
   !isObject(transaction) ||
   !hasProperty(transaction, 'data') ||
   typeof transaction.data !== 'string'
 ) {
   console.warn('unknown transaction type');
   return { insights };
 }

 const response = await fetch(`${MODEL_API_ENDPOINT}`, {
   method: 'get',
   headers: {
     'Content-Type': 'application/json',
   },
 });

 const currentData = await fetch(CURRENT_DATA_ENDPOINT, {
   method: 'get',
   headers: {
     'Content-Type': 'application/json',
   },
 });

 if (!response.ok) {
   throw new Error('API request failed');
 }

 const data = await response.json();
 const current = await currentData.json();

 state.lowEth = Math.min(data.low_30_minutes, data.low_60_minutes);

 await wallet.request({
   method: 'snap_manageState',
   params: ['update', state],
 });

 insights = {
   'Average Gas Limit': `Current value: ${current.avgGas}`,

   'Estimated current gas price': `Current value (Base + Priority): ${
     current.speeds[0].baseFee + current.speeds[0].maxPriorityFeePerGas
   } GWei; Base Fee: ${current.speeds[0].baseFee} GWei`,

   'Forecasted Avg Gas price (for the next 30 mins)': `
   Gas price within 30 minutes is expected to get as low as: ${data.low_30_minutes} GWei`,

   'Forecasted Avg Gas price (for the next 60 mins)': `Gas price within 60 minutes is expected to get as low as: ${Math.min(
     data.low_30_minutes,
     data.low_60_minutes,
   )} GWei`,

   'Expected savings in 30 mins (For average gas limit)': `
   ${
     current.avgGas *
     (current.speeds[0].baseFee +
       current.speeds[0].maxPriorityFeePerGas -
       data.low_30_minutes)
   } GWei`,

   'Expected savings in 60 mins (For average gas limit)': `${
     current.avgGas *
     (current.speeds[0].baseFee +
       current.speeds[0].maxPriorityFeePerGas -
       Math.min(data.low_30_minutes, data.low_60_minutes))
   } GWei
   `,
 };

 return { insights };
};
コピー

ユーザーが待機を選択した場合、ウェブサイトからSnapとonCronjobハンドラーを介して通知システムを起動し、3分ごとにネイティブブラウザ通知としてガス代を送信することができます。価格が予測値に非常に近づいた際には、トランザクションを完了するようユーザーに促します。


export const onCronjob: OnCronjobHandler = async ({ request }) => {
 switch (request.method) {
   case 'exampleMethodOne': {
     // get data from state whenever snap is invoked as snaps executions are ephemeral
     let state: { notifToggle: boolean; urgency: number; lowEth: number };

     state = (await wallet.request({
       method: 'snap_manageState',
       params: ['get'],
     })) as { notifToggle: boolean; urgency: number; lowEth: number };

     if (!state) {
       state = {
         notifToggle,
         urgency,
         lowEth,
       };
     }
     notifToggle = state.notifToggle;
     urgency = state.urgency;
     lowEth = state.lowEth;

     await wallet.request({
       method: 'snap_manageState',
       params: ['update', state],
     });

     console.log({ notifToggle });
     if (notifToggle) {
       const currentData = await fetch(CURRENT_DATA_ENDPOINT, {
         method: 'get',
         headers: {
           'Content-Type': 'application/json',
         },
       });
       const current = await currentData.json();
       if (Math.abs(current.speeds[0].baseFee - lowEth) < urgency / 38) {
         return wallet.request({
           method: 'snap_notify',
           params: [
             {
               type: 'native',
               message: `Gasfee is low! Pay now at ${current.speeds[0].baseFee} Gwei`,
             },
           ],
         });
       }
       return wallet.request({
         method: 'snap_notify',
         params: [
           {
             type: 'native',
             message: `Current gasfee is ${current.speeds[0].baseFee} Gwei`,
           },
         ],
       });
     }
     return 0; // basically do nothing
   }

   default:
     throw new Error('Method not found.');
 }
};
コピー

対応するcronjobの設定は(snap.manifest.json内):

"endowment:cronjob": {
      "jobs": [
        {
          "expression": {
            "minute": "*/3",
            "hour": "*",
            "dayOfMonth": "*",
            "month": "*",
            "dayOfWeek": "*"
          },
          "request": {
            "method": "exampleMethodOne",
            "params": {}
          }
        }
      ]
    }
コピー

また、トランザクションの緊急度を設定する緊急度機能もあります(技術的には、ユーザーがトランザクションを進めるべき予測値と現在のガス代の最大差を決定するパラメーター)。緊急度を変更するとこのしきい値が変わります。緊急度設定はSnapのonRpcRequestメソッドを使用して実装しました。


 export const onRpcRequest: OnRpcRequestHandler = async ({
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  origin,
  request,
}) => {
  switch (request.method) {
    // Other RPC methods here
    case 'set_urgency_35': {
      urgency = 35;

      let state: { notifToggle: boolean; urgency: number; lowEth: number };

      state = (await wallet.request({
        method: 'snap_manageState',
        params: ['get'],
      })) as { notifToggle: boolean; urgency: number; lowEth: number };

      if (!state) {
        state = {
          notifToggle,
          urgency: 35,
          lowEth,
        };
      }
      state.urgency = 35;

      await wallet.request({
        method: 'snap_manageState',
        params: ['update', state],
      });

      return urgency;
    }

    case 'set_urgency_60': {
      urgency = 60;

      let state: { notifToggle: boolean; urgency: number; lowEth: number };

      state = (await wallet.request({
        method: 'snap_manageState',
        params: ['get'],
      })) as { notifToggle: boolean; urgency: number; lowEth: number };

      if (!state) {
        state = {
          notifToggle,
          urgency: 60,
          lowEth,
        };
      }
      state.urgency = 60;

      await wallet.request({
        method: 'snap_manageState',
        params: ['update', state],
      });

      return urgency;
    }

    case 'set_urgency_90': {
      urgency = 90;

      let state: { notifToggle: boolean; urgency: number; lowEth: number };

      state = (await wallet.request({
        method: 'snap_manageState',
        params: ['get'],
      })) as { notifToggle: boolean; urgency: number; lowEth: number };

      if (!state) {
        state = {
          notifToggle,
          urgency: 90,
          lowEth,
        };
      }
      state.urgency = 90;

      await wallet.request({
        method: 'snap_manageState',
        params: ['update', state],
      });

      return urgency;
    }

    case 'set_urgency_100': {
      urgency = 100;

      let state: { notifToggle: boolean; urgency: number; lowEth: number };

      state = (await wallet.request({
        method: 'snap_manageState',
        params: ['get'],
      })) as { notifToggle: boolean; urgency: number; lowEth: number };

      if (!state) {
        state = {
          notifToggle,
          urgency: 100,
          lowEth,
        };
      }
      state.urgency = 100;

      await wallet.request({
        method: 'snap_manageState',
        params: ['update', state],
      });

      return urgency;
    }

    case 'call_api': {
      // get data from state whenever snap is invoked as snaps executions are ephemeral
      let state: { notifToggle: boolean; urgency: number; lowEth: number };

      state = (await wallet.request({
        method: 'snap_manageState',
        params: ['get'],
      })) as { notifToggle: boolean; urgency: number; lowEth: number };

      if (!state) {
        state = {
          notifToggle,
          urgency,
          lowEth,
        };
      }

      notifToggle = state.notifToggle;
      urgency = state.urgency;
      lowEth = state.lowEth;

      await wallet.request({
        method: 'snap_manageState',
        params: ['update', state],
      });

      console.log(
        `Called inside custom api call: notifToggle = ${notifToggle} and urgency = ${urgency}`,
      );
      const currentData = await fetch(CURRENT_DATA_ENDPOINT, {
        method: 'get',
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const current = await currentData.json();
      if (Math.abs(current.speeds[0].baseFee - lowEth) < urgency / 38) {
        return wallet.request({
          method: 'snap_notify',
          params: [
            {
              type: 'native',
              message: `Gasfee is low! Pay now at ${current.speeds[0].baseFee} Gwei`,
            },
          ],
        });
      }
      return wallet.request({
        method: 'snap_notify',
        params: [
          {
            type: 'native',
            message: `Current gasfee is ${current.speeds[0].baseFee} Gwei`,
          },
        ],
      });
    }

    default: {
      throw new Error('Method not found.');
    }
  }
};
コピー

しきい値の計算には、緊急度設定を38で割りました。異なる値での実証テストの結果、38がほとんどの状況で最も効果的であることがわかりました。より動的なパフォーマンスのために、近い将来このパラメーターを微調整・改善する予定です。

if (Math.abs(current.speeds[0].baseFee - lowEth) < urgency / 38) {
          return wallet.request({
            method: 'snap_notify',
            params: [
              {
                type: 'native',
                message: `Gasfee is low! Pay now at ${current.speeds[0].baseFee} Gwei`,
              },
            ],
          });
        }
コピー

Snapの状態はエフェメラルであるため、Snapストレージ APIを使用してストレージに状態情報(通知のオン/オフ、ユーザーが設定した緊急度など)を保存する必要がありました。


export const onRpcRequest: OnRpcRequestHandler = async ({
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  origin,
  request,
}) => {
  switch (request.method) {
    case 'notif_toggle_true': {
      console.log('Notification toggled to true');
      notifToggle = true; // toggle the notification
      console.log({ notifToggle });

      let state: { notifToggle: boolean; urgency: number; lowEth: number };

      state = (await wallet.request({
        method: 'snap_manageState',
        params: ['get'],
      })) as { notifToggle: boolean; urgency: number; lowEth: number };

      if (!state) {
        state = {
          notifToggle: true,
          urgency,
          lowEth,
        };
      }
      state.notifToggle = true;

      await wallet.request({
        method: 'snap_manageState',
        params: ['update', state],
      });

      return notifToggle;
    }

    case 'notif_toggle_false': {
      console.log('Notification toggled to false');
      notifToggle = false; // toggle the notification
      console.log({ notifToggle });

      let state: { notifToggle: boolean; urgency: number; lowEth: number };

      state = (await wallet.request({
        method: 'snap_manageState',
        params: ['get'],
      })) as { notifToggle: boolean; urgency: number; lowEth: number };

      if (!state) {
        state = {
          notifToggle: false,
          urgency,
          lowEth,
        };
      }
      state.notifToggle = false;

      await wallet.request({
        method: 'snap_manageState',
        params: ['update', state],
      });

      return notifToggle;
    }

     default: {
      throw new Error('Method not found.');
    }
  }
}; 
コピー

モデル

次に、statsforecastパッケージの時系列予測モデルであるSARIMAまたはSeasonal ARIMAを時系列データに適用し、次の1時間に予想される最低ガス代を予測します。


from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import requests
import numpy as np
from statsforecast.models import AutoARIMA
import json

def get_timeseries():
    with open('key.json') as f:
        data = json.load(f)
        print(data)

    key = data['key']

    history_data = requests.get(
        'https://api.owlracle.info/v3/eth/history?apikey={}&candles=100&txfee=true'
        .format(key))
    history_data = history_data.json()

    low = []

    for obj in history_data:
        low.append(obj['gasPrice']['low'])

    low.reverse()

    return np.array(low)

def get_prediction():
    values = get_timeseries()
    model = AutoARIMA()
    predictions = model.forecast(values, 2)
    print("Pred: ", predictions)
    prediction_json = {
        'low_30_minutes': predictions['mean'][0],
        'low_60_minutes': predictions['mean'][1]
    }

    return prediction_json

app = FastAPI()

origins = [
    "*",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/")
async def root():
    pred = get_prediction()

    return pred
コピー

なぜSARIMAなのか?

  • さまざまな分野の時系列予測におけるSARIMAに関する多くの文献が、SARIMAのパフォーマンスが優れていることを示しています。また、SARIMAを使用して有望な結果を得たEthereumガス代統計に関する研究も参照しました。

  • 代替手段としてはディープラーニング技術がありますが、通常はより大きなモデルを必要とし、再トレーニングに時間がかかるため、ストリーミングデータには適していません。

  • 参照した文献:- 台湾の機械産業の生産値予測におけるSARIMAとサポートベクターマシンのハイブリッドモデル。Expert Systems with Applications, 32(1), 254-264. https://doi.org/10.1016/j.eswa.2005.11.027

  • 疫学における予測パフォーマンスの比較:動的線形モデルとSARIMA。https://doi.org/10.1002/sim.963

  • SARIMAおよびARIMAモデルを使用した米国における長期流出量研究。https://doi.org/10.1002/met.1491

  • Carl, David and Ewerhart, Christian, Ethereum Gas Price Statistics (December 22, 2020). University of Zurich, Department of Economics, Working Paper No. 373, 2020, Available at: https://dx.doi.org/10.2139/ssrn.3754217

Webフロントエンド

WebフロントエンドインターフェースとしてシンプルなReactアプリを使用しました。Snapのインストール、ダミートランザクションの送信、ユーザー緊急度の設定、ガス代通知の切り替え、ダークモード切り替えのボタンが含まれています。非常にシンプルで、どのユーザーでも簡単に使用できます。

今後の実装に向けた次のステップは何ですか?

改善できることはたくさんあります。まず、学習アルゴリズムを調整してより堅牢な予測を行えるようにすることで、モデルの信頼性を高めることができます。もう一つの改善点は緊急度しきい値で、市場プロファイル、ユーザーの緊急度、トランザクションのガス代節約の幅広い変動を同時に処理し、ユーザーが支払いを行うべき最適なポイントに素早く到達できる、より動的な表現を考案する必要があります。また、Webインターフェースをより使いやすくし、まれに発生する可能性のあるコーナーケースに対する全体的なエラー処理を追加する必要があります。

あなたとチームについて少し教えていただけますか?

私たちはIIT Patnaに在籍する8名のBTech工学部学生で構成されるチームです。Consensysがメタマスク Snapsの課題を提示したInter IIT Tech meet 11(2023)に参加するためにチームを結成しました。

  • Vaishakh:IIT PatnaのコンピューターサイエンスのFinal yearの学生で、自分の研究分野が大好きです。最近はハッカソンに参加しており、その経験を心から楽しんでいます。コンピューターサイエンスの広大な可能性をさらに探求し、将来どこへ導かれるかを楽しみにしています。

  • Aritra B.:AritraはIIT PatnaのAIおよびデータサイエンス学科のBTech 2年生で、ブロックチェーンとその現実世界での大規模実装への応用に情熱を持っています。以前にいくつかの小規模なブロックチェーンプロジェクトに携わり、この分野の学習と探求の基盤を築きました。機械学習などのコンピューターサイエンスの他の分野を活用して新しいイノベーションを生み出す、スマートなブロックチェーンソリューションの創出を目指しています。2019年に権威ある Indian Institute of Science(IISC)からKVPYフェローに選ばれ、現在はZK EVMとこの分野でのユニークな実装領域について学んでいます。

  • Padmaksh:IIT PatnaでコンピューターサイエンスとエンジニアリングのBTechを取得中で、サイバーセキュリティに強い関心を持っています。問題解決とアルゴリズムパズルにも非常に興味があります。ブロックチェーンアーキテクチャをさらに学び、他の分野も探求しています。

  • Kartikay:IIT PatnaでB.Tech機械工学を専攻する3年生の学部生です。Web開発と競技プログラミングに強い関心を持っています。多くの大学クラブやフェスティバルのために複数のウェブサイトを設計・開発しました。データサイエンスとWeb3の基礎知識を持っています。

  • Harsh:IIT Patnaで化学工学を専攻する2年生の学部生です。主にケーススタディと問題解決に取り組んでいます。

  • Dhushyanth:IIT Patnaの電気工学のFinal yearの学生で、テクノロジーと開発に非常に熱心です。RustでのCRDTサーバー実装からシリコンフォトニクスでのパーセプトロン設計まで、さまざまな分野に携わってきました。自分の意見を述べられることなら何でも熱く議論するのが好きで、それを心から楽しんでいます!

  • Vishwas S:IIT Patnaのコンピューターサイエンス1年生で、自分の研究分野に情熱を持ち、探求の機会を探しています。最近、ブロックチェーン技術への強い関心を持ち、その分野で活動しています。

  • Ankur:IIT Patnaで電気電子工学を専攻する2年生の学部生です。最近IIT Patnaのためにいくつかのウェブサイトを開発し、問題解決を楽しんでいます。最近はUIとUXに興味を持ち、フロントエンドに傾倒しています。人間心理学の研究と、さまざまな問題に対するソリューションの構築に取り組んでいます。

MetaMask Snapsに初めて触れたのはいつで、その経験はどのようなものでしたか?

MetaMask Snapsに初めて触れたのはInter IIT Tech meet 2023でした。MetaMaskへの非常にユニークで興味深いアドオンであり、ウォレットに無限のカスタマイズをもたらすために使用できます。開発プロセスも非常に分かりやすく、ドキュメントやディスカッションフォーラムのコンテンツが少なかったにもかかわらず、開発中に大きな障壁に直面することはありませんでした。まだ開発中であり、現時点では重要な機能がいくつか不足していますが、今後数年間で大きな可能性を持つことはすでに見えています。

MetaMask Snapsが他のウォレットと異なる点は何ですか?

MetaMask SnapsはWebブラウザの拡張機能に似ています。MetaMaskの機能を拡張するために使用されます。Snapsはインストール時にユーザーが付与した権限によって決定される限られた機能セットにアクセスできる隔離された環境で実行されるため、安全です。ウォレット体験をカスタマイズするための優れた安全な方法です。

あなたとチームにとって、MetaMask Snapsの開発はどのようなものでしたか?

これは、異なるコンピューターサイエンスの分野がどのように連携して課題を解決できるかを目の当たりにさせてくれたプロジェクトの一つです。チームと私にとって、このSnapの開発は素晴らしい学習機会でした。アイデアを効果的に実装する方法と、機械学習を使用してブロックチェーン上の問題を解決する方法についての知識を得ました。その過程で、いくつかの新しいつながりも築きました。一方、他のウォレットは開発者にとって非常に制限的に感じられます。

MetaMask Snapsはあなたにとってどのような意味を持ちますか?

MetaMask Snapsは拡張機能の機能をcryptoウォレットに追加します。非常に革新的であり、暗号通貨の大衆普及を促進するための多くの重要なユースケースに使用できます。

MetaMask SnapsとそれがWeb3空間に解放する可能性についてどのような機会を見ていますか?

MetaMask Snapsは、シンプルなEthereumウォレットをWeb3体験をカスタマイズ・強化できる完全なWeb3管理ツールに変革するため、Web3エコシステムにおける真のブレークスルーとなり得ます。

MetaMask Snapsを試したい開発者に共有したいアドバイスはありますか? まずドキュメントを読み始めてください。非常に詳しく説明されており、考えられるほとんどのユースケースを説明しています。解決策が明確でない問題については、この目的のための専用フォーラムがあるConsensys Discordで質問することをお勧めします。

MetaMask Snapsで開発を始める

MetaMask Snapsを始めるには:

  1. 開発者ドキュメントをご確認ください

  2. MetaMask Flaskをインストールしてください

  3. MetaMask Snapsガイドをご覧ください

  4. Twitter、GitHubディスカッション、Discordでつながり続けましょう

お近くで開催される次のハッカソンでも私たちのチームにご注目ください!Happy BUIDLing ⚒️

免責事項:MetaMask Snapsは一般的にConsensys Software以外のサードパーティによって開発されています。サードパーティが開発したMetaMask Snapsの使用は、お客様自身の判断とリスクで行われ、そのような活動から生じる損失や損害についてはお客様が単独で責任を負うことに同意したものとみなされます。Consensysはサードパーティが開発したMetaMask Snapsについて、口頭または書面による明示的または黙示的な保証を行わず、サードパーティが開発したSnapsに関するすべての責任を否認します。ブロックチェーン関連ソフトウェアの使用にはリスクが伴い、MetaMask Snapsを使用する際にはそのリスクをすべて引き受けることになります。

AIによる翻訳です。エラーが含まれている可能性があります。常に情報を確認してください。

翻訳を評価する

この記事の執筆者:

  • MetaMask

    MetaMask(旧Consensys Software Inc)は、世界最大のセルフカストディ型金融プラットフォームです。暗号資産と従来の資産の両方にわたって、資産の保有・支払い・貯蓄・運用を一か所で行える環境をユーザーに提供しています。同社は、決済・貯蓄・投資・デジタル資産をシームレスな体験として統合したコンシューマー向けプラットフォームの構築を進めています。世界で最も広く利用されているセルフカストディウォレットとして成長を遂げたMetaMaskは、約190か国にわたるユーザーが自身の資金と資産を直接管理できる手段を提供しています。MetaMaskは2016年以来、Ethereumの成長において基盤的な役割を果たしてきました。現在、MetaMaskはブロックチェーン経済の中心に位置し、「Open Money」のオペレーティングシステムを構築することで、人々が自らの金融生活を完全にコントロールできる世界を目指しています。

    すべての記事を読む