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 模型和 Web 前端。Gas 价格服务器提供历史和当前 Gas 价格的数据。网站托管该 Snap,并提供通过调用 Snap API 方法来安装和与 Snap 交互的界面。我们使用了季节性自回归积分滑动平均(SARIMA)模型来预测 Gas 价格。
Gas 价格服务器
我们使用 Owlracle API 以约 200 根 K 线的列表形式收集实时 Gas 价格数据(每根 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.');
}
};
对应的定时任务设置(在 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 与支持向量机的混合应用。《专家系统与应用》,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
Web 前端
我们使用了一个简单的 React 应用作为 Web 前端界面。它包含用于安装 Snap、发送模拟交易、设置用户紧急程度、切换 Gas 价格通知以及深色模式切换的按钮。界面非常简洁,任何用户都能轻松上手。
如果您想实现这一功能,下一步计划是什么?
还有很多工作可以做。我们可以从提升模型可靠性入手,通过调整学习算法来实现更稳健的预测。另一个需要改进的方面是紧急程度阈值,我们需要提出一个更动态的表达式,以便同时处理市场状况的大幅波动、用户紧急程度和交易 Gas 节省,并快速找到用户应进行支付的近似最优点。此外,我们还需要让 Web 界面更加友好,并针对一些极少出现的边界情况添加整体错误处理。
能否介绍一下您自己和您的团队?
我们是一支由印度理工学院巴特那分校(IIT Patna)八名工程专业本科生组成的团队。我们组建团队是为了参加 Inter IIT Tech Meet 11(2023 年),Consensys 在该活动中提出了一个 MetaMask Snaps 的问题陈述。
Vaishakh:我是 IIT Patna 计算机科学专业的大四学生,热爱自己的专业领域。最近,我一直在参加黑客马拉松,乐在其中。我迫不及待地想要探索计算机科学领域更广阔的可能性,期待它将带我走向何方。
Aritra B.:Aritra 是印度理工学院巴特那分校人工智能与数据科学专业的大二本科生,对区块链及其在现实世界大规模落地中的应用充满热情。他此前参与过多个小型区块链项目,为深入学习和探索这一领域奠定了基础。他致力于利用机器学习等计算机科学领域的知识创建智能区块链解决方案,推动新的创新。他于 2019 年入选印度科学院(IISC)颁发的 KVPY 奖学金,目前正在学习 ZK EVM 及其在该领域的独特应用场景。
Padmaksh:就读于印度理工学院巴特那分校计算机科学与工程专业,对网络安全有浓厚兴趣,同时也非常热衷于解题和算法谜题。他正在进一步研究区块链架构并探索其他领域。
Kartikay:IIT Patna 机械工程专业大三本科生,对 Web 开发和竞技编程有浓厚兴趣。他为多个学院社团和节庆活动设计并开发了多个网站,并具备数据科学和 Web3 的基础知识。
Harsh:我是 IIT Patna 化学工程专业的大二本科生,主要专注于案例研究和解决问题。
Dhushyanth:我是 IIT Patna 电气工程专业的大四学生,对技术和开发充满热情。我涉猎过多个领域,从用 Rust 实现 CRDT 服务器到在硅光子学中设计感知器。我喜欢就任何我能发表见解的话题进行深入探讨,乐此不疲!
Vishwas S:Vishwas 是 IIT Patna 计算机科学专业的大一学生,对自己的专业充满热情,正在积极寻找探索机会。最近,他对区块链技术产生了浓厚兴趣,并正在从事相关工作。
Ankur:我是 IIT Patna 电气与电子工程专业的大二本科生。我最近为 IIT Patna 开发了几个网站,享受解决问题的过程。最近我对 UI 和 UX 产生了浓厚兴趣,尤其钟情于前端开发。我喜欢研究人类心理学,并致力于为各种问题构建解决方案。
您是什么时候第一次接触 MetaMask Snaps 的,体验如何?
我们第一次接触 MetaMask Snaps 是在 2023 年的 Inter IIT Tech Meet 上。它是 MetaMask 一个非常独特且有趣的扩展功能,可用于为钱包带来无限的自定义可能。开发流程也相当简单直接,尽管当时可用的文档和讨论论坛内容较少,我们在开发过程中也没有遇到重大阻碍。虽然它目前仍在开发中,暂时缺少一些重要功能,但我们已经能看到它在未来几年所具备的巨大潜力。
MetaMask Snaps 与其他钱包有何不同?
MetaMask Snaps 类似于 Web 浏览器中的扩展程序,用于扩展 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 时须自行承担全部风险。