// ==UserScript== // @name Binance Alpha Farm Agent // @namespace http://baf.thuanle.me // @version 2025.08.01 // @author TL // @description Automated trading agent for Binance Alpha Farm // @match https://www.binance.com/* // @match https://accounts.binance.com/* // @run-at document-idle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_log // @connect baf.thuanle.me // @connect localhost // @downloadURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js // @updateURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js // ==/UserScript== if (window.top !== window) { GM_log(`[TL] ❌ Skipping in iframe ${window.location.href}`); return; } GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); (async () => { 'use strict'; const uuid = () => ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); const KEYS = { SESSION_ID: 'aRah9OhHeijee6sho3baequu9phoovah', SESSION_TOKEN: 'ThiegiecohViuZ1Iecio7gahphiechub', AGENT_TOKEN: 'baf-agent-token', } if (!sessionStorage.getItem(KEYS.SESSION_ID)) { sessionStorage.setItem(KEYS.SESSION_ID, uuid()); } // ====== CONFIGURATION ====== const CONFIG = { HEARTBEAT_INTERVAL: 10000, DASHBOARD_UPDATE_INTERVAL: 500, // Update dashboard overlay every 0.5 seconds SERVERS: { local: { label: '🏠 Local', url: 'http://localhost:3000' }, prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' }, }, }; const AppSession = { SESSION_ID: sessionStorage.getItem(KEYS.SESSION_ID), getSessionToken: () => sessionStorage.getItem(KEYS.SESSION_TOKEN), setSessionToken: token => sessionStorage.setItem(KEYS.SESSION_TOKEN, token), } // ====== APP ENUMS ====== const AppEnums = { BOT_STATUS: { IDLE: 'idle', RUNNING: 'running', } }; // ====== STATE MANAGEMENT ====== /** * AppStateClass - Quản lý state của ứng dụng * Lưu trữ runtime state (không persistent), persistent data được lưu trong AppSettings */ class AppStateClass { constructor() { this.data = { // Server configuration - Thông tin server hiện tại server_last_seen: null, // Thời gian cuối cùng ping server thành công server_latency: null, // Thời gian ping server (ms) // Page state - Trạng thái trang hiện tại current_page: null, // Trang hiện tại (detected từ URL) is_logged_in: false, // Trạng thái đăng nhập Binance // UI state - Trạng thái UI is_loading: false, // Đang loading error_message: null, // Thông báo lỗi // Session flags - Flags cho session hiện tại (reset khi reload) menuCreated: false, // Menu đã được tạo popupInitialized: false, // Popup đã được khởi tạo appInitialized: false // App đã được khởi tạo hoàn toàn }; this.observers = new Map(); // Observer pattern cho state changes this.initialized = false; // Flag khởi tạo } /** * Khởi tạo state từ AppSettings và detect trang hiện tại * Chỉ chạy một lần khi app khởi động */ async initialize() { if (this.initialized) return; this.initialized = true; // Detect trang hiện tại (không block) const currentPage = BINANCE.page.detectPage(); // Update state (không bao gồm login status) await this.update({ current_page: currentPage, }); TL.log('APP-STATE', 'AppState initialized'); } /** * Lấy copy của data hiện tại * @returns {Object} Copy của state data */ getData() { return { ...this.data }; } // ====== GETTER METHODS ====== getServer() { return CONFIG.SERVERS[AppSettings.getServerType()]; } getServerLastSeen() { return this.data.server_last_seen; } getServerLatency() { return this.data.server_latency; } /** * Kiểm tra xem server có kết nối không * @returns {boolean} true nếu server kết nối trong 1 phút, false nếu không */ getServerConnected() { return this.data.server_last_seen && Date.now() - this.data.server_last_seen < 60000; } getCurrentPage() { return this.data.current_page; } getIsLoggedIn() { return this.data.is_logged_in; } getIsLoading() { return this.data.is_loading; } getMenuCreated() { return this.data.menuCreated; } getPopupInitialized() { return this.data.popupInitialized; } getAppInitialized() { return this.data.appInitialized; } /** * Cập nhật state với partial update * @param {Object} updates - Object chứa cặp key-value cần cập nhật vào state * Ví dụ: { current_task: task, is_loading: true } */ async update(updates) { const oldData = { ...this.data }; Object.assign(this.data, updates); await this.notifyObservers(oldData, this.data); // TL.debug('STATE', 'Updated', updates); } /** * Đăng ký observer cho state change * @param {string} key - Key của state cần observe * @param {Function} callback - Callback function khi state thay đổi */ subscribe(key, callback) { if (!this.observers.has(key)) { this.observers.set(key, []); } this.observers.get(key).push(callback); } /** * Hủy đăng ký observer * @param {string} key - Key của state * @param {Function} callback - Callback function cần hủy */ unsubscribe(key, callback) { const callbacks = this.observers.get(key); if (callbacks) { const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } } /** * Thông báo tất cả observers khi state thay đổi * @param {Object} oldData - State cũ * @param {Object} newData - State mới */ async notifyObservers(oldData, newData) { for (const [key, callbacks] of this.observers) { const oldValue = oldData[key]; const newValue = newData[key]; if (oldValue !== newValue) { for (const callback of callbacks) { try { await callback(oldValue, newValue, newData); } catch (error) { TL.error('STATE', `Observer error for ${key}:`, error); } } } } } } const AppState = new AppStateClass(); // ====== APP SETTINGS ====== /** * AppSettings - Quản lý persistent settings của ứng dụng * Lưu trữ cài đặt người dùng trong GM storage (vĩnh viễn) */ const AppSettings = { key_agent_token: 'baf-agent-token', key_server_type: 'baf-server-type', key_bot_status: 'baf-bot-status', key_popup_position: 'baf-popup-position', key_popup_visible: 'baf-popup-visible', key_debug: 'baf-debug', key_safety_guard: 'baf-safety-guard', getAgentToken: () => GM_getValue(AppSettings.key_agent_token, ''), setAgentToken: token => GM_setValue(AppSettings.key_agent_token, String(token || '').trim().replace(/^Bearer\s+/i, '')), // Server configuration getServerType: () => GM_getValue(AppSettings.key_server_type, 'prod'), setServerType: (type) => { GM_setValue(AppSettings.key_server_type, type); AppState.update({ server_last_seen: null }); }, getBotStatus: () => GM_getValue(AppSettings.key_bot_status, AppEnums.BOT_STATUS.IDLE), setBotStatus: (status) => GM_setValue(AppSettings.key_bot_status, status), // Popup configuration getPopupPosition: () => GM_getValue(AppSettings.key_popup_position, 4), setPopupPosition: (position) => GM_setValue(AppSettings.key_popup_position, position), getPopupVisible: () => GM_getValue(AppSettings.key_popup_visible, true), setPopupVisible: (visible) => GM_setValue(AppSettings.key_popup_visible, visible), // Debug mode getDebug: () => GM_getValue(AppSettings.key_debug, true), setDebug: (debug) => GM_setValue(AppSettings.key_debug, debug), // Safety guard getSafetyGuard: () => GM_getValue(AppSettings.key_safety_guard, true), setSafetyGuard: (safetyGuard) => { GM_setValue(AppSettings.key_safety_guard, safetyGuard); GM_log(`[TL] Safety guard set to: ${safetyGuard}`); }, }; // ====== COMMON UTILITIES ====== /** * TL - Common utilities cho logging, notifications, và DOM helpers */ const TL = { debug: (tag, msg, ...args) => AppSettings.getDebug() && GM_log(`[TL] [${tag}]\n${msg}`, ...args), log: (tag, msg, ...args) => GM_log(`[TL] [${tag}]\n${msg}`, ...args), warn: (tag, msg, ...args) => GM_log(`[TL] [WARN] [${tag}] ⚠️\n${msg}`, ...args), error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${tag}] ❌\n${msg}`, ...args), noti: (title, text, timeout = 2500) => { if (typeof GM_notification === 'function') { GM_notification({ title, text, timeout }); return true; } alert(`${title}\n${text}`); return false; }, delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)), // DOM helpers dom: { isVisible: (ele) => { if (!ele) return false; const cs = getComputedStyle(ele); if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; return (ele.offsetWidth + ele.offsetHeight) > 0; }, isDisabled: (ele) => { return ele?.disabled === true || ele?.getAttribute('aria-disabled') === 'true'; }, isInViewport: (ele) => { if (!ele) return false; const rect = ele.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }, click: (ele) => { if (ele && typeof ele.click === 'function') ele.click(); }, scrollToView: (ele, behavior = 'smooth') => { ele?.scrollIntoView?.({ behavior, block: 'center' }); }, setHover: (ele, eventType, styles) => { if (!ele) return; const applyStyles = () => { if (styles.scale) ele.style.transform = `scale(${styles.scale})`; if (styles.transform) ele.style.transform = styles.transform; if (styles.background) ele.style.background = styles.background; if (styles.color) ele.style.color = styles.color; if (styles.border) ele.style.border = styles.border; }; ele.addEventListener(eventType, applyStyles); }, }, // Browser helpers browser: { /** * Navigate to URL với option forceReload * @param {string} url - URL cần navigate */ navigate: (url) => { window.location.href = url; } }, // Network helpers net: { gmRequest(url, init = {}) { const headersToObject = (h) => { const o = {}; (h || new Headers()).forEach((v, k) => { o[k] = v; }); return o; }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url, method: (init.method || 'GET').toUpperCase(), headers: headersToObject(init.headers), data: init.body, onload: (resp) => { const text = resp.responseText || ''; let data = text; const isJSON = /content-type:\s*application\/json/i.test(resp.responseHeaders || ''); if (isJSON) { try { data = JSON.parse(text); } catch { } } TL.debug(`net`, `${init.method} ${resp.status} ${url}\n${init.body}`, data); resolve({ status: resp.status, ok: resp.status >= 200 && resp.status < 300, data, rawText: text, headers: null }); }, onerror: reject, ontimeout: () => reject(new Error('GM_xmlhttpRequest timeout')), }); }); }, } }; // ====== BAF API MODULE ====== const BAF = { _getHost: () => AppState.getServer()?.url, _request: async (method, path, { params, body, headers, token } = {}) => { const base = BAF._getHost(); const url = new URL(path, base); if (params) for (const [k, v] of Object.entries(params)) url.searchParams.append(k, v); const h = new Headers(headers || {}); if (token && !h.has('Authorization')) h.set('Authorization', `Bearer ${token}`); const init = { method, headers: h }; if (body !== undefined && body !== null) { if (!(body instanceof FormData) && !h.has('Content-Type')) h.set('Content-Type', 'application/json'); init.body = (h.get('Content-Type') || '').includes('application/json') && typeof body !== 'string' ? JSON.stringify(body) : body; } return TL.net.gmRequest(url.toString(), init); }, _agentGet: (path, params, init = {}) => BAF._request('GET', path, { params, headers: init.headers, token: AppSettings.getAgentToken() }), _agentPost: (path, body, init = {}) => BAF._request('POST', path, { body, headers: init.headers, token: AppSettings.getAgentToken() }), _sessionGet: (path, params, init = {}) => BAF._request('GET', path, { params, headers: init.headers, token: AppSession.getSessionToken() }), _sessionPost: (path, body, init = {}) => BAF._request('POST', path, { body, headers: init.headers, token: AppSession.getSessionToken() }), register: (data) => BAF._agentPost('/agent/register', data), ping: (data) => BAF._sessionPost('/session/ping', data), getTask: (data) => BAF._sessionPost('/session/task', data), }; // ====== BINANCE MODULE ====== // Binance Pages constants const BINANCE_PAGES = { LOGIN: 'login', ALPHA_SWAP: 'alpha-swap', ALPHA_ORDER_HISTORY: 'alpha-order-history', WALLET_EARN: 'wallet-earn', WALLET_FUNDING: 'wallet-funding', WALLET_SPOT: 'wallet-spot', IGNORE: 'ignore', UNKNOWN: 'unknown' }; // Page patterns for detection const BINANCE_PAGE_PATTERNS = [ { host: 'accounts.binance.com', patterns: [ { includes: '/login', page: BINANCE_PAGES.LOGIN } ] }, { host: 'www.binance.com', patterns: [ { includes: '/alpha/bsc/', page: BINANCE_PAGES.ALPHA_SWAP }, { includes: '/my/orders/alpha/orderhistory', page: BINANCE_PAGES.ALPHA_ORDER_HISTORY }, { includes: '/my/wallet/account/earn', page: BINANCE_PAGES.WALLET_EARN }, { includes: '/my/wallet/funding', page: BINANCE_PAGES.WALLET_FUNDING }, { includes: '/my/wallet/account/main', page: BINANCE_PAGES.WALLET_SPOT } ] } ]; const BINANCE = { page: { detectPage: () => { const { hostname, pathname } = window.location; if (window.top !== window) { TL.debug('BINANCE-PAGE-DETECT', 'Call from iframe ❌'); return BINANCE_PAGES.IGNORE; } const hostConfig = BINANCE_PAGE_PATTERNS.find(cfg => cfg.host === hostname); if (!hostConfig) { return BINANCE_PAGES.UNKNOWN; } // Check patterns in order (most specific first) for (const pattern of hostConfig.patterns) { if (pathname.includes(pattern.includes)) { TL.debug('BINANCE-PAGE-DETECT', `Matched pattern: ${pattern.includes} -> ${pattern.page}`); return pattern.page; } } TL.debug('BINANCE-PAGE-DETECT', `No pattern matched: ${hostname}${pathname}`); return BINANCE_PAGES.UNKNOWN; }, isOnLoginPage: () => AppState.getCurrentPage() === BINANCE_PAGES.LOGIN, isOnAlphaSwapPage: (contractAddress) => { const page = AppState.getCurrentPage(); return page === BINANCE_PAGES.ALPHA_SWAP && (!contractAddress || window.location.pathname.includes(contractAddress)); }, isOnAlphaOrderHistoryPage: () => AppState.getCurrentPage() === BINANCE_PAGES.ALPHA_ORDER_HISTORY, isOnWalletEarnPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_EARN, isOnWalletFundingPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_FUNDING, isOnWalletSpotPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_SPOT, // Navigation URL generators - chỉ trả về address, không thực hiện navigate getLoginUrl: () => 'https://www.binance.com/en/login', getAlphaSwapUrl: (contractAddress) => { return contractAddress ? `https://www.binance.com/en/alpha/bsc/${contractAddress}` : `https://www.binance.com/en/alpha`; }, getAlphaOrderHistoryUrl: () => 'https://www.binance.com/en/my/orders/alpha/orderhistory', getWalletEarnUrl: () => 'https://www.binance.com/en/my/wallet/account/earn', getWalletFundingUrl: () => 'https://www.binance.com/en/my/wallet/funding', getWalletSpotUrl: () => 'https://www.binance.com/en/my/wallet/account/main' }, auth: { detectLoginState: () => { if (!AppSettings.getSafetyGuard()) return null; if (window.top !== window) { TL.debug('BINANCE-LOGIN', 'In iframe - cannot determine login state'); return null; } if (BINANCE.page.isOnLoginPage()) { return false; } // Method 1: Check for login/register buttons (indicates NOT logged in) const loginBtn = document.querySelector('#toLoginPage, [data-testid="login-button"], .login-btn'); const regBtn = document.querySelector('#toRegisterPage, [data-testid="register-button"], .register-btn'); let msg = `loginBtn: ${TL.dom.isVisible?.(loginBtn)} regBtn: ${TL.dom.isVisible?.(regBtn)}\n`; // If login/register buttons are visible, definitely not logged in if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) { TL.debug('BINANCE-LOGIN', msg + 'Login/Register buttons visible -> NOT logged in'); return false; } // Method 2: Check for user account elements (indicates logged in) const dashboardLink = document.querySelector('a[href*="/my/dashboard"]'); const accountIcon = document.querySelector('.header-account-icon'); const walletBtn = document.querySelector('#ba-wallet'); const isDashboardVisible = dashboardLink && TL.dom.isVisible?.(dashboardLink); const isAccountIconVisible = !!accountIcon; const isWalletVisible = walletBtn && TL.dom.isVisible?.(walletBtn); msg += `Visible: dashboard: ${isDashboardVisible ? '✓' : '✗'}, accountIcon: ${isAccountIconVisible ? '✓' : '✗'}, walletBtn: ${isWalletVisible ? '✓' : '✗'}\n`; // If we see user account elements, likely logged in if (isDashboardVisible && isAccountIconVisible && isWalletVisible) { TL.debug('BINANCE-LOGIN', msg + 'User account elements found -> LIKELY logged in'); return true; } TL.debug('BINANCE-LOGIN', msg + 'Cannot determine login state - returning null'); return null; }, isLoggedIn: async (timeoutMs = 30000, pollMs = 500) => { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const state = BINANCE.auth.detectLoginState(); if (state !== null) { TL.debug(`BINANCE-LOGIN`, `isLoggedIn: ${state ? 'true' : 'false'}`); return state; } TL.debug(`BINANCE-LOGIN`, `isLoggedIn: not found. Sleeping...`); await TL.delay(pollMs); } const fallback = BINANCE.auth.detectLoginState() ?? false; TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`); return fallback; } } }; // ====== APP UI ====== const AppUi = { // ====== DASHBOARD OVERLAY ====== dashboardOverlay: { element: null, // Get position CSS based on position number getPositionCSS: (position) => { switch (position) { case 1: return { top: '10px', left: '10px', bottom: 'auto', right: 'auto' }; // top-left case 2: return { top: '10px', right: '10px', bottom: 'auto', left: 'auto' }; // top-right case 3: return { bottom: '10px', left: '10px', top: 'auto', right: 'auto' }; // bottom-left case 4: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' }; // bottom-right default: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' }; } }, // Create dashboard overlay element create: () => { if (AppUi.dashboardOverlay.element) { document.body.removeChild(AppUi.dashboardOverlay.element); } const overlay = document.createElement('div'); overlay.id = 'baf-status-overlay'; overlay.style.cssText = ` position: fixed; z-index: 999999; background: rgba(0, 0, 0, 0.9); color: white; padding: 12px; border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; line-height: 1.4; min-width: 200px; max-width: 300px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; user-select: none; `; // Add position toggle button const positionBtn = document.createElement('div'); positionBtn.style.cssText = ` position: absolute; top: -8px; right: -8px; width: 20px; height: 20px; background: #007bff; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 10px; color: white; border: 2px solid rgba(255, 255, 255, 0.2); transition: all 0.2s ease; `; positionBtn.innerHTML = '⇄'; positionBtn.title = 'Change position'; positionBtn.addEventListener('click', () => { const newPosition = (AppSettings.getPopupPosition() % 4) + 1; AppSettings.setPopupPosition(newPosition); AppUi.statusOverlay.updatePosition(); }); TL.dom.setHover(positionBtn, 'mouseenter', { scale: 1.5, background: '#0056b3' }); TL.dom.setHover(positionBtn, 'mouseleave', { scale: 1, background: '#007bff' }); overlay.appendChild(positionBtn); AppUi.dashboardOverlay.element = overlay; document.body.appendChild(overlay); return overlay; }, // Update overlay position updatePosition: () => { if (!AppUi.dashboardOverlay.element) return; const positionCSS = AppUi.dashboardOverlay.getPositionCSS(AppSettings.getPopupPosition()); Object.assign(AppUi.dashboardOverlay.element.style, positionCSS); }, // Update overlay content updateContent: () => { // Get status display let statusDisplay = ''; switch (AppSettings.getBotStatus()) { case AppEnums.BOT_STATUS.IDLE: statusDisplay = '💤 Idle'; break; case AppEnums.BOT_STATUS.RUNNING: statusDisplay = '▶️ Running'; break; default: statusDisplay = AppSettings.getBotStatus(); } // Get server info const serverLabel = AppState.getServer()?.label || 'Unknown'; const serverConnected = AppState.getServerConnected() ? `🟢 (${AppState.getServerLatency()}ms)` : '🔴'; // Get page info const pageDisplay = AppState.getCurrentPage() || 'unknown'; // Get login status const loginStatus = AppState.getIsLoggedIn() ? '✅' : '❌'; // Get SafetyGuard status const safetyStatus = { enabled: AppSettings.getSafetyGuard(), message: AppSettings.getSafetyGuard() ? '✅ Operations Enabled' : '🚨 Operations Blocked' }; // Build content const content = `