Skip to content

Instantly share code, notes, and snippets.

@discountry
Created December 20, 2025 13:26
Show Gist options
  • Select an option

  • Save discountry/6aa12b70c722724032bb04b0151724a6 to your computer and use it in GitHub Desktop.

Select an option

Save discountry/6aa12b70c722724032bb04b0151724a6 to your computer and use it in GitHub Desktop.
scriptable eth-gas-tracker
// ============================================
// ETH Gas Elite Widget (v6.1 - Enhanced Breathing Room)
// - 1x1 专项优化:显著增加全局边距,提升呼吸感与留白艺术
// - 结构:顶部胶囊价格 + 中部大字 + 底部独立玻璃卡片
// - 背景:彩色高斯模糊弥散背景
// ============================================
const ETHERSCAN_API_BASE = "https://api.etherscan.io/v2/api";
const KEYCHAIN_KEY = "ETHERSCAN_API_KEY_V2";
const BINANCE_PRICE_URL = "https://api.binance.com/api/v3/ticker/price?symbol=ETHUSDT";
const SWAP_GAS_LIMIT = 356190;
const SWAP_GAS_TIER = "ProposeGasPrice";
// ================== UI 常量:呼吸感优化 ================== //
const size = config.widgetFamily || "medium";
const UI = {
small: {
widgetPadding: 15, // 显著增加边距,解决“窄”的问题
mainFontSize: 28, // 略微缩小字号以配合大边距
cardPadding: 8, // 收紧卡片内边距,提升精致感
headerIcon: 10,
priceFontSize: 9
},
medium: {
widgetPadding: 16,
mainFontSize: 42,
cardPadding: 12,
headerIcon: 14,
priceFontSize: 12
}
}[size];
const THEME = {
accent: new Color("#A5B4FC"),
success: new Color("#34D399"),
warning: new Color("#FBBF24"),
danger: new Color("#F87171"),
glass: new Color("#000000", 0.55), // 增强卡片深色对比
glassStroke: new Color("#FFFFFF", 0.12),
textMain: new Color("#FFFFFF", 1.0),
textSecondary: new Color("#FFFFFF", 0.5),
pillBg: new Color("#FFFFFF", 0.12)
};
// ================== 核心逻辑 ================== //
function formatGas(val) {
const n = parseFloat(val);
if (isNaN(n)) return "0";
return n < 1 ? n.toFixed(4) : Math.round(n).toString();
}
function fmtUSD(x) {
const n = parseFloat(x);
return isNaN(n) ? "0.00" : n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function getStatusColor(val) {
const n = parseFloat(val);
if (n > 50) return THEME.danger;
if (n > 20) return THEME.warning;
return THEME.success;
}
function addSymbol(stack, name, size, color) {
let sym = SFSymbol.named(name) || SFSymbol.named("circle.fill");
const img = stack.addImage(sym.image);
img.imageSize = new Size(size, size);
img.tintColor = color || THEME.textMain;
}
function createGlassCard(parent, padding) {
const card = parent.addStack();
card.layoutVertically();
card.backgroundColor = THEME.glass;
card.cornerRadius = 14;
card.setPadding(padding, padding, padding, padding);
card.borderWidth = 1.0;
card.borderColor = THEME.glassStroke;
return card;
}
// ================== 排版构建 ================== //
// --- 1*1 (Small) 呼吸感加强版 ---
function buildSmall(widget, data, swap) {
// 1. 顶部栏
const header = widget.addStack();
header.centerAlignContent();
addSymbol(header, "fuelpump.fill", UI.headerIcon, THEME.accent);
header.addSpacer(4);
const title = header.addText("GAS");
title.font = Font.systemFont(10, "heavy");
title.textColor = THEME.textMain;
header.addSpacer();
const priceBox = header.addStack();
priceBox.backgroundColor = THEME.pillBg;
priceBox.cornerRadius = 5;
priceBox.setPadding(2, 6, 2, 6);
const prText = priceBox.addText(`$${Math.round(swap.ethPrice)}`);
prText.font = Font.boldSystemFont(UI.priceFontSize);
widget.addSpacer(10); // 增加顶部与主数值的间距
// 2. 中部核心
const val = widget.addText(data.propose);
val.font = Font.systemFont(UI.mainFontSize, "rounded");
val.textColor = getStatusColor(data.propose);
val.lineLimit = 1;
val.minimumScaleFactor = 0.5;
const unit = widget.addText("GWEI");
unit.font = Font.systemFont(8, "heavy");
unit.textColor = THEME.textSecondary;
widget.addSpacer(); // 自动占据剩余空间,将卡片推向底部
// 3. 底部玻璃卡片
const card = createGlassCard(widget, UI.cardPadding);
const cardHead = card.addStack();
cardHead.centerAlignContent();
addSymbol(cardHead, "arrow.2.circlepath", 9, THEME.accent);
cardHead.addSpacer(4);
const ct = cardHead.addText("SWAP");
ct.font = Font.systemFont(8, "bold");
ct.textColor = THEME.textSecondary;
cardHead.addSpacer();
const costUSD = cardHead.addText(`$${fmtUSD(swap.feeUSD)}`);
costUSD.font = Font.systemFont(11, "rounded");
costUSD.textColor = THEME.textMain;
}
// --- 1*2 (Medium) ---
async function buildMedium(widget, data, meta, swap) {
const header = widget.addStack();
header.centerAlignContent();
const titleGroup = header.addStack();
titleGroup.centerAlignContent();
addSymbol(titleGroup, "fuelpump.fill", UI.headerIcon, THEME.accent);
titleGroup.addSpacer(6);
const title = titleGroup.addText("GAS TRACKER");
title.font = Font.systemFont(14, "heavy");
header.addSpacer();
const priceBox = header.addStack();
priceBox.backgroundColor = THEME.pillBg;
priceBox.cornerRadius = 10;
priceBox.setPadding(5, 12, 5, 12);
priceBox.addText(`ETH $${fmtUSD(swap.ethPrice)}`).font = Font.boldSystemFont(12);
widget.addSpacer(14);
const body = widget.addStack();
body.bottomAlignContent();
const left = body.addStack();
left.layoutVertically();
left.flexWeight = 0.45;
left.addText("CURRENT").font = Font.systemFont(10, "bold");
const val = left.addText(data.propose);
val.font = Font.systemFont(UI.mainFontSize, "rounded");
val.textColor = getStatusColor(data.propose);
left.addText("GWEI / NET").font = Font.systemFont(9, "medium");
body.addSpacer(12);
const right = body.addStack();
right.layoutVertically();
right.flexWeight = 0.55;
const card = createGlassCard(right, UI.cardPadding);
const cardHead = card.addStack();
cardHead.centerAlignContent();
cardHead.addSpacer();
addSymbol(cardHead, "arrow.2.circlepath", 11, THEME.accent);
cardHead.addSpacer(6);
cardHead.addText("SWAP COST").font = Font.systemFont(10, "bold");
card.addSpacer(4);
const usdStack = card.addStack();
usdStack.addSpacer();
const feeUSD = usdStack.addText(`$${fmtUSD(swap.feeUSD)}`);
feeUSD.font = Font.systemFont(24, "rounded");
card.addSpacer(2);
const ethStack = card.addStack();
ethStack.addSpacer();
ethStack.addText(`${swap.feeETH.toFixed(6)} ETH`).font = Font.mediumMonospacedSystemFont(9);
widget.addSpacer();
const footer = widget.addStack();
footer.centerAlignContent();
addSymbol(footer, "cube.fill", 8, THEME.textSecondary);
footer.addSpacer(6);
const ft = footer.addText(`BLOCK ${meta.lastBlock} · SYNCHRONIZED`);
ft.font = Font.systemFont(8, "bold");
ft.textColor = THEME.textSecondary;
ft.textOpacity = 0.4;
}
// ================== 主运行逻辑 ================== //
async function main() {
const widget = new ListWidget();
widget.setPadding(UI.widgetPadding, UI.widgetPadding, UI.widgetPadding, UI.widgetPadding);
try {
const seed = Math.floor(Math.random() * 1000);
const imgReq = new Request(`https://picsum.photos/seed/${seed}/800/800?blur=10`);
widget.backgroundImage = await imgReq.loadImage();
const gradient = new LinearGradient();
gradient.colors = [new Color("#000000", 0.92), new Color("#000000", 0.45)];
gradient.locations = [0, 1];
widget.backgroundGradient = gradient;
} catch(e) {
widget.backgroundColor = new Color("#0C0C0E");
}
try {
const apiKey = (args.widgetParameter || "").trim().split(',')[0] || Keychain.get(KEYCHAIN_KEY);
const [priceRes, gasRes] = await Promise.all([
new Request(BINANCE_PRICE_URL).loadJSON(),
new Request(`${ETHERSCAN_API_BASE}?chainid=1&module=gastracker&action=gasoracle&apikey=${apiKey}`).loadJSON()
]);
const ethPrice = parseFloat(priceRes.price);
const r = gasRes.result;
const data = { propose: formatGas(r.ProposeGasPrice) };
const feeETH = SWAP_GAS_LIMIT * parseFloat(r[SWAP_GAS_TIER] || r.ProposeGasPrice) * 1e-9;
const swap = { feeETH, feeUSD: feeETH * ethPrice, ethPrice };
if (size === "small") buildSmall(widget, data, swap);
else await buildMedium(widget, data, { lastBlock: r.LastBlock }, swap);
} catch (e) {
widget.addText("API ERROR").font = Font.boldSystemFont(11);
}
return widget;
}
const finalWidget = await main();
if (config.runsInWidget) Script.setWidget(finalWidget);
else {
if (size === "small") await finalWidget.presentSmall();
else await finalWidget.presentMedium();
}
Script.complete();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment