Ether Eyes Snap: Predicting Future Gas Prices

An Inter IIT Hackathon Winner

by MetaMaskApril 13, 2023
feature

MetaMask Snaps is the roadmap to making MetaMask the most extensible wallet in the world. As a developer, you can bring your features and APIs to MetaMask in totally new ways. Web3 developers are the core of this growth and this series aims to showcase the novel MetMask Snaps being built today.

Ether Eyes Snap



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

Why did you build it?

Gas prices on the Ethereum blockchain can be really hard to predict, and is a hindrance when it comes to making payments. A user, when trying to make payments, can end up paying much more than required. Short time high traffic on the blockchain can lead to a huge impact in gas prices. Although L2s were built to reduce gas prices, we sometimes need a more general and easier solution than using an L2 for transaction purposes. We thought it would be useful for users to have an app that can predict future gas prices based on historical data and current trends.

Can you walk us through the technical implementation?

EtherEyes consists of four parts: Gas price server, the snap, SARIMA Model, web frontend. The gas price server provides data about historical and current gas prices. The website hosts the snap and provides interface to install and interact with the snap by calling snap API methods. We used Seasonal Autoregressive Integrated Moving Average(SARIMA) model to predict gas prices.

Gas Price Server


We collect live gas price data as a list of around 200 candlesticks (each candlestick is an aggregate of the past 30 minutes of data) using the Owlracle API. This gives us the timeseries data of past 100 hours of gas prices.

The Snap


For the snap, we used the transaction insights API to provide realtime gas prices predictions. The flow of the snap is as follows:

First, when the user starts a transaction, the transaction insights shows up in the confirmation page which is implemented using the onTransactionHandler. Here we show the user about upcoming prediction for next 30 and 60 mins. A simplified version of what we implemented is given here:

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 };
};

If the user deciedes to wait, he can wait and start a notification system from the website that interacts with the snap via onCronjob handler and sends gas prices after every three minutes as native browser notifications. When the prices get very close to the prediction, it asks the user to complete the transaction.


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.');
 }
};

The corresponding cronjob settings are (in snap.manifest.json):

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

There is also an urgency feature that sets the urgency of the transaction (technically, the parameter that determines what should be the maximum difference between prediction and current gas prices so that the user should go on with the transaction). Changing the urgency varies this threshold. We implemented urgency setting using snap’s onRpcRequest method.


 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.');
    }
  }
};

To calculate threshhold, we divided the urgency setting by 38 .After empirical testing with different values, 38 seemed to work best in most situations. There is a plan to fine tune and enhance this in the near future for more dynamic performance.

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`,
              },
            ],
          });
        }

As snap states are ephemeral, we needed to store state information (like notification on/off, urgency set by user) into storage using the snap storage 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.');
    }
  }
}; 

The Model


We then use a popular time series forecasting model called SARIMA or Seasonal ARIMA from the statsforecast package on the timeseries data to predict the lowest gas price expected in the next hour.


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

Why SARIMA?


  • A lot of literature on SARIMA for time-series forecasting in varying domains stated that the performance of SARIMA is good. We also referred to a study on ethereum gas price statistics, where they used SARIMA and obtained promising results.
  • An alternative would be deep learning techniques which usually involve larger models which also take more time to re-train hence making it less ideal for streaming data.
  • Some literatures we reffered:

Web Frontend


We used a simple React app as our web frontend interface. It contains the buttons for snap install, sending a dummy transaction, setting user urgency, toggling gas-price notifications and dark mode toggler. Its very simple and easy to use for any user.

HAmiesP

What are the next steps if you want to implement this?

There is a lot that can be done, we can start by making the model more reliable by tweaking the learning algorithm to make more robust predictions. Another area to improve is the urgency threshold where we need to come up with a more dynamic expression to handle wide variations of market profile, user urgency and transaction gas savings at the same time and quickly arrive at an almost optimal point where the user should make payment. Also we need to make the web interface a bit more user friendly and add some overall error handling at some corner cases which can occur rarely.

Can you tell us a little bit about yourself and your team?

We are a team consisting of eight BTech engineering students at IIT Patna. We formed the team to participate in Inter IIT Tech meet 11 (2023) where ConsenSys came up with a MetaMask Snaps problem statement.

Screenshot 2023-04-13 at 2.42.43 PM

  • Vaishakh: I am a final year Computer Science student at IIT Patna and I love my field of study. Lately, I've been participating in hackathons and I am thoroughly enjoying the experience. I can't wait to explore more of the vast possibilities in Computer Science and see where it takes me in the future.
  • Aritra B. : Aritra is a Btech sophomore at AI and Data Science at Indian Institute of Technology, Patna and passionate about Blockchain and its applications in real world mass implementation. He has worked in a number of small blockchain projects before, which made the foundations for learning and exploring this domain. He is aimed at creating smart blockchain solutions using other domains of computer sciences like machine learning to create new innovations. He has been selected a KVPY fellow by the prestigious Indian Instiute of Science(IISC) in 2019 and is currently learning about ZK EVMs and it’s unique areas of implementations in this space.
  • Padmaksh: Pursuing BTech in Computer science and engineering at Indian institute of technology Patna and has a keen interest in cyber security. He's also very much interested in problem solving and algorithmic puzzles. He's further studying blockchain architecture and exploring other fields.
  • Kartikay: Undergrad at IIT Patna pursuing B.Tech in Mechanical Engineering currently in his 3rd year of course curriculum. He has a keen interest in web development and competitive programming. He has designed and developed multiple websites for many college clubs and fests. He has basic knowledge in Data Science and Web3.
  • Harsh: I am a sophomore year undergrad at IIT Patna pursuing Chemical Engineering. I am mostly into case studies and problem solving.
  • Dhushyanth: I am a final year Electrical Engineering student at IIT Patna, highly enthusiastic about technology and development. I’ve dabbled in a variety of fields like CRDT server implementation in rust to designing Perceptrons in Silicon Photonics. I like to have heated conversations about anything I can give my two cents about and thoroughly enjoy it!
  • Vishwas S: Vishwas is first year Computer Science student at IIT Patna who is passionate about his field of study and looking for opportunities to explore. Lately, he has developed a keen interest towards blockchain technology and working in it.
  • Ankur: I'm a sophomore undergrad at IIT Patna pursuing Electrical and Electronics Engineering. I recently developed a few websites for IIT Patna and enjoy problem solving. Lately I have been enjoying UI and UX with a knack for frontend. I like to studying human psychology and working towards building solutions to various problems

When were you first introduced to MetaMask Snaps and what was your experience like?

We got introduced to MetaMask Snaps first at Inter IIT Tech meet 2023. It’s a really unique and interesting add on to MetaMask, and can be used to bring endless customizations in the wallet. The development process is also pretty straightforward and we did not face any major setbacks while developing even though there was less documentation and discussion forum content available. Although it is still in development and lacks few important features as of now, we can already see the great potential it can have in upcoming years.

What makes MetaMask Snaps different from other wallets?

MetaMask Snaps is similar to an extension in a web browser. It is used to extend the functionalities of MetaMask. It is safe as the snaps are run in an isolated environment where they have access to a limited set of capabilities determined by the permissions granted by the user during installation. It is a great and safe way to customize your wallet experience.

Tell us about what building MetaMask Snaps is like for you and your team?

It’s one of the projects that allowed us to witness how different computer science fields could work together to solve a challenge. For my team and I, building this snap was a fantastic learning opportunity. We gained knowledge on how to implement our idea effectively as well as how to use machine learning to solve a problem on blockchain. Along the way, we also established a few new connections. On the other side, rest wallets feel quite restrictive for developers.

What does MetaMask Snaps mean to you?

Metamask Snaps add the features of extensions to a crypto wallet. It is really innovative and can be used for many critical use cases to improve mass adoption of cryptocurrency.

What opportunities do you see with MetaMask Snaps and the possibilities it unlocks for the Web3 space?

Metamask Snaps can really be a breakthrough in the web3 ecosystem as it transforms a simple Ethereum wallet to an entire web3 management tool where we can customize and enhance our web3 experience.

Any advice you would like to share with developers keen to try MetaMask Snaps? Start reading the documentation. It is highly explanatory and can explain most of the use cases one can have. For any issue whose solution is not apparent, we would suggest asking them in the ConsenSys discord where there is a forum dedicated for this purpose.

Building with MetaMask Snaps


To get started with MetaMask Snaps:

  1. Checkout the developer docs
  2. Install MetaMask Flask
  3. Check out a MetaMask Snaps guide
  4. Stay connected with us on Twitter, GitHub discussions, and Discord

Keep an eye out for our team at the next hackathon in an area near you! Happy BUIDLing ⚒️

Disclaimer: MetaMask Snaps are generally developed by third parties other the ConsenSys Software. Use of third-party-developed MetaMask Snaps is done at your own discretion and risk and with agreement that you will solely be responsible for any loss or damage that results from such activities. ConsenSys makes no express or implied warranty, whether oral or written, regarding any third-party-developed MetaMask Snaps and disclaims all liability for third-party developed Snaps. Use of blockchain-related software carries risks, and you assume them in full when using MetaMask Snaps.

Receive our Newsletter