Created
December 20, 2025 13:26
-
-
Save discountry/6aa12b70c722724032bb04b0151724a6 to your computer and use it in GitHub Desktop.
scriptable eth-gas-tracker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================ | |
| // 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