MetaMask Snaps 是讓 MetaMask 成為全球最具擴展性錢包的發展藍圖。身為開發者,您可以透過全新方式將您的功能與 API 帶入 MetaMask。Web3 開發者是這項成長的核心,本系列旨在展示當今正在建構的各種創新 MetaMask Snaps。
Ether Eyes Snap
Snap 程式庫: https://github.com/aritroCoder/EtherEyes
為什麼要建構它?
以太坊區塊鏈上的 Gas 費用非常難以預測,這對進行付款時造成了相當大的阻礙。使用者在嘗試付款時,可能最終支付遠超所需的費用。區塊鏈上短時間內的高流量可能對 Gas 費用造成巨大影響。雖然 L2 的設計初衷是降低 Gas 費用,但有時我們需要一個比使用 L2 進行交易更通用、更簡便的解決方案。我們認為,若能為使用者提供一款可根據歷史數據與當前趨勢預測未來 Gas 費用的應用程式,將會非常實用。
能否帶我們了解技術實作的細節?
EtherEyes 由四個部分組成:Gas 費用伺服器、Snap、SARIMA 模型、網頁前端。Gas 費用伺服器提供歷史與當前 Gas 費用的相關數據。網站負責託管 Snap,並提供介面讓使用者安裝 Snap,以及透過呼叫 Snap API 方法與其互動。我們使用季節性自迴歸整合移動平均(SARIMA)模型來預測 Gas 費用。
Gas 費用伺服器
我們使用 Owlracle API 收集即時 Gas 費用數據,以約 200 根 K 線的列表形式呈現(每根 K 線為過去 30 分鐘數據的彙總)。這為我們提供了過去 100 小時 Gas 費用的時間序列數據。
Snap
對於 Snap,我們使用交易洞察 API 來提供即時 Gas 費用預測。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 };
};
若使用者決定等待,可以等候並從網站啟動通知系統,該系統透過 onCronjob 處理器與 Snap 互動,每三分鐘以原生瀏覽器通知的形式傳送 Gas 費用資訊。當費用非常接近預測值時,系統會提示使用者完成交易。
export const onCronjob: OnCronjobHandler = async ({ request }) => {
switch (request.method) {
case 'exampleMethodOne': {
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;
}
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": {}
}
}
]
}
此外還有一項緊急程度功能,用於設定交易的緊急程度(技術上來說,即決定預測值與當前 Gas 費用之間最大差距的參數,以判斷使用者是否應繼續進行交易)。調整緊急程度會改變此閾值。我們使用 Snap 的 onRpcRequest 方法實作緊急程度設定。
export const onRpcRequest: OnRpcRequestHandler = async ({
origin,
request,
}) => {
switch (request.method) {
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': {
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 ({
origin,
request,
}) => {
switch (request.method) {
case 'notif_toggle_true': {
console.log('Notification toggled to true');
notifToggle = true;
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;
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(季節性 ARIMA),對時間序列數據進行分析,以預測未來一小時內預期的最低 Gas 費用。
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 的效能表現良好。我們也參考了一項關於以太坊 Gas 費用統計的研究,該研究使用了 SARIMA 並獲得了令人期待的結果。
另一個替代方案是深度學習技術,但通常涉及較大的模型,重新訓練所需時間也較長,因此對於串流數據而言並不理想。
部分參考文獻:- 混合 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 與 Ewerhart, Christian,以太坊 Gas 費用統計(2020 年 12 月 22 日)。蘇黎世大學經濟學系工作論文第 373 號,2020 年,可於以下網址取得:https://dx.doi.org/10.2139/ssrn.3754217
網頁前端
我們使用簡單的 React 應用程式作為網頁前端介面。其中包含安裝 Snap、傳送測試交易、設定使用者緊急程度、切換 Gas 費用通知以及深色模式切換等按鈕。介面非常簡潔易用,任何使用者都能輕鬆上手。
若要實作此功能,下一步是什麼?
還有許多可以改進的地方。我們可以從調整學習演算法入手,讓模型更可靠,以做出更穩健的預測。另一個改進方向是緊急程度閾值,我們需要設計出更動態的表達式,以同時處理市場狀況的大幅波動、使用者緊急程度以及交易 Gas 費用節省等因素,並快速找到使用者應進行付款的近似最佳時機點。此外,我們也需要讓網頁介面更加友善,並針對少數可能發生的邊緣情況加入整體錯誤處理機制。
能否介紹一下您自己和您的團隊?
我們是一支由八名就讀於印度理工學院巴特那分校(IIT Patna)的 BTech 工程學生所組成的團隊。我們組成這支團隊是為了參加 Inter IIT Tech Meet 11(2023 年),其中 Consensys 提出了一個 MetaMask Snaps 的問題陳述。
Vaishakh:我是 IIT Patna 電腦科學系的應屆畢業生,熱愛我的研究領域。近來,我積極參與黑客松活動,並從中獲得了極大的樂趣。我迫不及待地想探索電腦科學中更多廣闊的可能性,並期待它將帶領我走向何方。
Aritra B.:Aritra 是印度理工學院巴特那分校人工智慧與數據科學系的大二學生,對區塊鏈及其在現實世界大規模應用充滿熱情。他此前參與了多個小型區塊鏈專案,為學習和探索這一領域奠定了基礎。他致力於結合機器學習等電腦科學領域,創造智慧型區塊鏈解決方案以推動新創新。他於 2019 年獲得印度科學院(IISC)頒發的 KVPY 獎學金,目前正在學習 ZK EVM 及其在此領域的獨特應用場景。
Padmaksh:就讀於印度理工學院巴特那分校電腦科學與工程系 BTech,對網路安全有濃厚興趣。他同時也非常熱衷於解題和演算法謎題,並持續深入研究區塊鏈架構,探索其他相關領域。
Kartikay:IIT Patna 機械工程系 B.Tech 大三學生,對網頁開發和競技程式設計有濃厚興趣。他為多個大學社團和活動設計並開發了多個網站,並具備數據科學和 Web3 的基礎知識。
Harsh:我是 IIT Patna 化學工程系的大二學生,主要專注於案例研究和解題。
Dhushyanth:我是 IIT Patna 電機工程系的應屆畢業生,對技術與開發充滿熱忱。我涉獵過多個領域,從以 Rust 實作 CRDT 伺服器到在矽光子學中設計感知器。我喜歡就任何我能發表意見的話題進行熱烈討論,並從中獲得極大的樂趣!
Vishwas S:Vishwas 是 IIT Patna 電腦科學系一年級學生,對自己的研究領域充滿熱情,積極尋找探索機會。近來,他對區塊鏈技術產生了濃厚興趣,並投入其中。
Ankur:我是 IIT Patna 電機與電子工程系的大二學生,近期為 IIT Patna 開發了幾個網站,並享受解題的過程。近來我對 UI 和 UX 產生了濃厚興趣,尤其鍾情於前端開發。我喜歡研究人類心理學,並致力於為各種問題建構解決方案。
您是何時首次接觸到 MetaMask Snaps 的?體驗如何?
我們是在 2023 年的 Inter IIT Tech Meet 上首次接觸到 MetaMask Snaps 的。它是 MetaMask 一個非常獨特且有趣的附加功能,可用於為錢包帶來無限的客製化可能。開發流程也相當直觀,儘管當時可用的文件和討論論壇內容較少,我們在開發過程中並未遭遇任何重大阻礙。雖然它目前仍在開發中,且目前尚缺少一些重要功能,但我們已經能看到它在未來幾年所具備的巨大潛力。
MetaMask Snaps 與其他錢包有何不同?
MetaMask Snaps 類似於網頁瀏覽器中的擴充功能,用於擴展 MetaMask 的功能。它的安全性有所保障,因為 Snap 在隔離環境中運行,只能存取使用者在安裝時授予權限所決定的有限功能集。這是一種安全且優質的方式,讓您能夠客製化您的錢包體驗。
請告訴我們您和您的團隊建構 MetaMask Snaps 的過程是什麼感受?
這是一個讓我們親眼見證不同電腦科學領域如何協同合作以解決挑戰的專案。對我和我的團隊而言,建構這個 Snap 是一次絕佳的學習機會。我們不僅學到了如何有效實現我們的想法,也學到了如何運用機器學習解決區塊鏈上的問題。在這個過程中,我們也建立了一些新的人脈連結。另一方面,其他錢包對開發者而言感覺相當受限。
MetaMask Snaps 對您意味著什麼?
MetaMask Snaps 為加密貨幣錢包加入了擴充功能的特性。它極具創新性,可應用於許多關鍵使用場景,以促進加密貨幣的大規模普及。
您認為 MetaMask Snaps 為 Web3 領域帶來了哪些機遇與可能性?
MetaMask Snaps 有望成為 Web3 生態系統的重大突破,因為它將一個簡單的以太坊錢包轉變為完整的 Web3 管理工具,讓我們能夠客製化並提升我們的 Web3 體驗。
對於有意嘗試 MetaMask Snaps 的開發者,您有什麼建議? 從閱讀說明文件開始。文件內容詳盡,能夠解釋大多數可能遇到的使用場景。對於解決方案不明確的問題,我們建議在 Consensys Discord 上提問,那裡有一個專門為此目的設立的論壇。
使用 MetaMask Snaps 進行建構
開始使用 MetaMask Snaps:
查閱開發者文件
安裝 MetaMask Flask
查看 MetaMask Snaps 指南
在 Twitter、GitHub 討論區和 Discord 上與我們保持聯繫
請留意我們的團隊將出現在您附近的下一場黑客松!祝 BUIDL 愉快 ⚒️
免責聲明:MetaMask Snaps 通常由 Consensys Software 以外的第三方開發。使用第三方開發的 MetaMask Snaps 須由您自行斟酌判斷並承擔風險,且您同意將對因此類活動所造成的任何損失或損害負全部責任。Consensys 對第三方開發的 MetaMask Snaps 不提供任何明示或暗示的保證,並對第三方開發的 Snaps 免除一切責任。使用區塊鏈相關軟體存在風險,使用 MetaMask Snaps 即表示您完全承擔這些風險。