Etherscan Points Assistant

A reimagined auto-claimer for Etherscan points with Mystery Box support, featuring a beautiful, iOS 18-inspired interface with light and dark modes. Fixed display bugs. It just works.

As of 2025-08-26. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Etherscan Points Assistant
// @name:zh-CN   Etherscan 积分助手 (含神秘礼盒)
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  A reimagined auto-claimer for Etherscan points with Mystery Box support, featuring a beautiful, iOS 18-inspired interface with light and dark modes. Fixed display bugs. It just works.
// @description:zh-CN 在 etherscan.io/points 页面自动签到并开启神秘礼盒。以 Apple iOS 18 设计语言重构,拥有精美的琉光玻璃界面与自动深色模式,为你带来无感、愉悦的积分获取体验。新增:自动领礼盒并更新积分。修复:模板字符串显示bug及重复积分问题。
// @author       Mantancoin(AIGC)
// @match        https://etherscan.io/points
// @grant        GM_addStyle
// @grant        GM_log
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 我们相信,伟大的体验始于伟大的设计。保留原色板,支持深浅模式。
    // We believe a great experience starts with great design. Keeping original palette with light/dark modes.
    GM_addStyle(`
        :root {
            /* 浅色模式调色板 - 清新、空灵的感觉。 */
            /* Light Mode Palette - a clean, airy feel. */
            --bg-color-light: rgba(247, 247, 247, 0.8);
            --primary-text-light: #1d1d1f;
            --secondary-text-light: rgba(60, 60, 67, 0.7);
            --border-color-light: rgba(0, 0, 0, 0.1);
            --shadow-color-light: rgba(0, 0, 0, 0.12);
            --apple-blue-light: #007AFF;
            --apple-green-light: #34C759;
            --apple-red-light: #FF3B30;

            /* 深色模式调色板 - 专注且电影感。 */
            /* Dark Mode Palette - focused and cinematic. */
            --bg-color-dark: rgba(28, 28, 30, 0.75);
            --primary-text-dark: #f5f5f7;
            --secondary-text-dark: rgba(235, 235, 245, 0.65);
            --border-color-dark: rgba(255, 255, 255, 0.15);
            --shadow-color-dark: rgba(0, 0, 0, 0.25);
            --apple-blue-dark: #0A84FF;
            --apple-green-dark: #30D158;
            --apple-red-dark: #FF453A;
        }

        #etherscan-panel {
            position: fixed;
            top: 24px;
            right: 24px;
            width: 290px;
            padding: 20px;
            border-radius: 20px; /* 更柔和、更吸引人的曲线。 */
            /* Softer, more inviting curves. */
            font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            z-index: 10000;
            transition: opacity 0.4s cubic-bezier(0.19, 1, 0.22, 1), transform 0.4s cubic-bezier(0.19, 1, 0.22, 1);
            transform: translateX(0);
            backdrop-filter: blur(20px) saturate(180%); /* 我们新材质的魔力。 */
            /* The magic of our new material. */
            background-color: var(--bg-color-light);
            border: 1px solid var(--border-color-light);
            box-shadow: 0 10px 30px var(--shadow-color-light);
            color: var(--primary-text-light);
        }

        /* 无缝适应系统外观。 */
        /* Seamlessly adapt to your system's appearance. */
        @media (prefers-color-scheme: dark) {
            #etherscan-panel {
                background-color: var(--bg-color-dark);
                border-color: var(--border-color-dark);
                box-shadow: 0 10px 35px var(--shadow-color-dark);
                color: var(--primary-text-dark);
            }
        }

        #etherscan-panel.hidden {
            opacity: 0;
            transform: translateX(30px);
            pointer-events: none;
        }

        #etherscan-panel h3 {
            margin: 0 0 16px 0;
            padding-bottom: 12px;
            border-bottom: 1px solid var(--border-color-light);
            font-size: 17px;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 8px; /* 完美间距,由设计决定。 */
            /* Perfect spacing, by design. */
            color: var(--apple-blue-light);
        }

        @media (prefers-color-scheme: dark) {
            #etherscan-panel h3 {
                border-bottom-color: var(--border-color-dark);
                color: var(--apple-blue-dark);
            }
        }

        /* 一个感觉像在家一样的图标。 */
        /* An icon that feels right at home. */
        #etherscan-panel h3::before {
            content: '💎';
            font-size: 20px;
        }

        #etherscan-panel p {
            margin: 10px 0;
            line-height: 1.5;
            font-size: 15px;
            color: var(--secondary-text-light);
        }

        #etherscan-panel p strong {
            font-weight: 500;
            color: var(--primary-text-light);
        }

        @media (prefers-color-scheme: dark) {
            #etherscan-panel p {
                 color: var(--secondary-text-dark);
            }
            #etherscan-panel p strong {
                color: var(--primary-text-dark);
            }
        }

        #etherscan-panel .success { color: var(--apple-green-light) !important; }
        #etherscan-panel .error { color: var(--apple-red-light) !important; }

         @media (prefers-color-scheme: dark) {
            #etherscan-panel .success { color: var(--apple-green-dark) !important; }
            #etherscan-panel .error { color: var(--apple-red-dark) !important; }
        }


        #etherscan-panel-close {
            position: absolute;
            top: 12px;
            right: 15px;
            cursor: pointer;
            font-size: 18px;
            font-weight: 500;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            border-radius: 50%;
            color: var(--secondary-text-light);
            background-color: rgba(0, 0, 0, 0.05);
            transition: background-color 0.2s ease, color 0.2s ease;
        }

        #etherscan-panel-close:hover {
            background-color: rgba(0, 0, 0, 0.1);
            color: var(--primary-text-light);
        }

        @media (prefers-color-scheme: dark) {
            #etherscan-panel-close {
                color: var(--secondary-text-dark);
                background-color: rgba(255, 255, 255, 0.1);
            }
            #etherscan-panel-close:hover {
                background-color: rgba(255, 255, 255, 0.2);
                color: var(--primary-text-dark);
            }
        }
    `);

    // --- UI 创建:这不是一个面板,而是一个窥视进程的窗口。 ---
    // --- UI Creation: It's not a panel, it's a window into the process. ---
    // 保留原UI创建逻辑,结构简单,因为简单是终极的复杂。
    // Keeping original UI creation logic. The structure is simple, because simplicity is the ultimate sophistication.
    function createPanel() {
        const panel = document.createElement('div');
        panel.id = 'etherscan-panel';
        panel.innerHTML = `
            <span id="etherscan-panel-close">&times;</span>
            <h3>Etherscan Assistant</h3>
            <div id="etherscan-panel-content">
                <p>正在与 Etherscan 同步...</p>
            </div>
        `;
        document.body.appendChild(panel);
        document.getElementById('etherscan-panel-close').addEventListener('click', () => {
            panel.classList.add('hidden');
        });
        return panel;
    }

    // --- 数据解析:从数字中找到清晰度。 ---
    // --- Data Parsing: Finding clarity in the numbers. ---
    // 该函数的核心逻辑非常出色,我们予以保留。
    // (The core logic of this function is excellent, we are keeping it.)
    function getPoints(element) {
        if (!element) return 0;
        const text = element.innerText || '';
        const numberString = text.replace(/💎|,/g, '').trim();
        return parseInt(numberString, 10) || 0;
    }

    // --- 核心引擎:美丽体验背后的强大技术。 ---
    // --- Core Engine: The robust technology behind the beautiful experience. ---
    // 核心功能和稳定性是基石,我们保留原逻辑,并新增礼盒点击。修复:使用反引号确保模板解析;合并积分显示,避免重复。
    // (Core functionality and stability are the foundation, which we have preserved untouched.)
    // 新增:检测并点击神秘礼盒按钮,如果可用。先礼盒后签到,确保最大化积分。
    // Addition: Detect and click Mystery Box button if available. Box first, then check-in for max points.
    // 修复:当已签到+无礼盒时,只显示单一积分和状态消息。
    // Fix: When already checked in + no box, display single points and status only.
    function runAutoClaimer() {
        let pointsElement = document.getElementById('spanTotalPoints');
        const checkinButton = document.getElementById('btncheckin');
        const claimTextSpan = document.getElementById('ContentPlaceHolder1_spanBtncheckin');
        const mysteryBoxBtn = document.getElementById('mystery-box-btn'); // 新增:获取礼盒按钮
        const panelContent = document.getElementById('etherscan-panel-content');

        // 检查关键组件是否存在
        if (!pointsElement || !checkinButton || !claimTextSpan) {
            panelContent.innerHTML = '<p>检测不到关键组件,助手无法启动。</p><p class="error">请检查页面是否为最新版本。</p>';
            return;
        }

        const pointsContainer = pointsElement.parentElement;
        if (!pointsContainer) {
            panelContent.innerHTML = '<p class="error">错误:无法定位积分模块,监视器启动失败。</p>';
            GM_log('Script Error: Could not find the parent element of #spanTotalPoints.');
            return;
        }

        const initialPoints = getPoints(pointsElement);
        // 初始显示:单一积分 + 检查状态(使用反引号模板)
        // Initial display: Single points + status check (using backticks for template)
        panelContent.innerHTML = `<p>当前积分: <strong>${initialPoints.toLocaleString()}</strong></p><p>正在检查签到和礼盒状态...</p>`;

        // 检查签到状态
        if (claimTextSpan.style.display === 'none') {
            panelContent.innerHTML += '<p>今日已完成签到。</p>'; // 追加状态
        } else {
            panelContent.innerHTML += '<p>检测到可签到。</p>';
        }

        // 新增:处理神秘礼盒
        // Addition: Handle Mystery Box
        let hasClaimedBox = false; // 标记是否已处理礼盒
        if (mysteryBoxBtn && mysteryBoxBtn.style.display !== 'none' && !mysteryBoxBtn.disabled) {
            try {
                GM_log('检测到神秘礼盒,正在自动开启...');
                panelContent.innerHTML += '<p>✨ 检测到神秘礼盒,正在自动开启...</p>';
                mysteryBoxBtn.click(); // 模拟点击,触发submitMysteryBox('15', '')
                hasClaimedBox = true;
            } catch (error) {
                GM_log('Error clicking Mystery Box: ' + error);
                panelContent.innerHTML += '<p class="error">礼盒点击失败,请手动尝试。</p>';
            }
        } else {
            GM_log('No Mystery Box available or already claimed.');
            panelContent.innerHTML += '<p>无可用神秘礼盒。</p>'; // 追加状态
        }

        // 如果可签到,继续原签到逻辑
        // If check-in available, proceed with original check-in logic
        if (claimTextSpan.style.display !== 'none') {
            GM_log('Setting up observer...');
            const observer = new MutationObserver((mutationsList, obs) => {
                GM_log('DOM change detected! Checking for new points...');

                pointsElement = document.getElementById('spanTotalPoints');
                const newPoints = getPoints(pointsElement);
                GM_log(`Initial points: ${initialPoints}, New points detected: ${newPoints}`);

                if (newPoints > initialPoints) {
                    GM_log('Points increased. Updating panel.');
                    const growth = newPoints - initialPoints;
                    let sourceMsg = '签到成功';
                    if (hasClaimedBox) {
                        sourceMsg = '签到与礼盒成功(总获取)'; // 统一显示总增长,提示含礼盒
                    }
                    // 更新整个面板为最新状态,避免重复(使用反引号)
                    // Update entire panel to latest, avoid duplicates (backticks)
                    panelContent.innerHTML = `
                        <p style="font-weight: 500; color: var(--primary-text-light);">✅ <strong>${sourceMsg}</strong></p>
                        <p>初始积分: ${initialPoints.toLocaleString()}</p>
                        <p>当前积分: <strong>${newPoints.toLocaleString()}</strong></p>
                        <p>本次总获取: <strong class="success">+${growth.toLocaleString()}</strong> ${hasClaimedBox ? '(可能含礼盒积分)' : ''}</p>
                    `;
                    // 深色模式需要动态颜色更新。
                    // Dark mode requires dynamic color update.
                    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
                        panelContent.querySelector('p[style]').style.color = 'var(--primary-text-dark)';
                    }
                    obs.disconnect(); // 任务完成。就这么简单。
                    // Task complete. It's that simple.
                }
            });

            observer.observe(pointsContainer, {
                childList: true,
                subtree: true
            });
            GM_log('Observer started. Watching the points container.');

            panelContent.innerHTML += '<p>正在自动处理签到...</p>';
            GM_log('Clicking the check-in button.');
            try {
                checkinButton.click();
            } catch (error) {
                GM_log('Error clicking check-in: ' + error);
                panelContent.innerHTML += '<p class="error">签到点击失败,请手动尝试。</p>';
            }
        } else {
            // 已签到,无需签到observer。但如果有礼盒,检查变化
            // Already checked in, no observer needed. But if box, check for changes
            if (hasClaimedBox) {
                // 用setTimeout检查礼盒结果,并更新为单一显示
                // Use setTimeout to check box result, update to single display
                setTimeout(() => {
                    pointsElement = document.getElementById('spanTotalPoints');
                    const newPoints = getPoints(pointsElement);
                    if (newPoints > initialPoints) {
                        const growth = newPoints - initialPoints;
                        // 更新面板为最新积分,避免重复
                        // Update panel to latest points, avoid duplicates
                        panelContent.innerHTML = `
                            <p style="font-weight: 500; color: var(--primary-text-light);">✅ <strong>礼盒开启成功</strong></p>
                            <p>初始积分: ${initialPoints.toLocaleString()}</p>
                            <p>当前积分: <strong>${newPoints.toLocaleString()}</strong></p>
                            <p>礼盒获取: <strong class="success">+${growth.toLocaleString()}</strong></p>
                        `;
                        if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
                            panelContent.querySelector('p[style]').style.color = 'var(--primary-text-dark)';
                        }
                    } else {
                        // 无变化,追加状态消息,不重复积分
                        // No change, append status only, no duplicate points
                        panelContent.innerHTML += '<p>一切正常,无积分变化。</p>';
                    }
                }, 2000); // 等待2秒检查礼盒结果
            } else {
                // 已签到 + 无礼盒:只追加状态,不重复积分
                // Already checked in + no box: Append status only, no duplicate points
                panelContent.innerHTML += '<p>一切正常,今日任务已完成。</p>';
            }
        }
    }

    // --- 初始化:一切从这里开始。安静、高效。 ---
    // --- Initialization: It all starts here. Quietly, efficiently. ---
    // 启动逻辑保持不变,确保在最佳时机无缝启动。新增礼盒不影响原流程。
    // (The startup logic remains unchanged to ensure a seamless start at the optimal moment.)
    const panel = createPanel();
    const readyCheckInterval = setInterval(() => {
        if (document.getElementById('spanTotalPoints') && document.getElementById('btncheckin')) {
            clearInterval(readyCheckInterval);
            runAutoClaimer();
        }
    }, 500);

    setTimeout(() => {
        clearInterval(readyCheckInterval);
    }, 30000); // 慷慨的超时,以防万一。增加到30秒以覆盖礼盒延迟。
    // A generous timeout, just in case. Increased to 30s for box delay.

})();