// ==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 SESSION_STORAGE_KEYS = { SESSION_ID: 'aRah9OhHeijee6sho3baequu9phoovah', SESSION_BINDED: 'Oos2uoth2thae8Ir8iakahj1OohaMahb', SESSION_TOKEN: 'ThiegiecohViuZ1Iecio7gahphiechub', SESSION_INITIALIZED: 'iR4choo2Ieg2ew0ogheeyaer8Aisheet', BOT_STATUS: 'wiethie3boGhoh3iegh3ohnezei2tauj', CURRENT_TASK: 'Reebo1eitahh2aotumai5jae1neetoh3', CURRENT_TASK_DATA: 'cheishailoh5keePoo6oe2Quie1gaxah', CURRENT_STEP: 'eDusaidu2hooweiMoonahng3fua7aoso', TIMER_START_TIME: 'einguh3eshuiqua7Ux7thaNgai5Axahy', TIMER_IS_RUNNING: 'iengoose3vie2aikePhooCaPheeceizu', TIMER_ACCUMULATED: 'aeMaiwae3PohB5Soshaech8eegh5quoo', } if (!sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_ID)) { sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_ID, uuid()); } // ====== CONFIGURATION ====== const CONFIG = { HEARTBEAT_INTERVAL: 10_000, // Send heartbeat every 10 seconds DASHBOARD_UPDATE_INTERVAL: 500, // Update dashboard overlay every 0.5 seconds TASK_INTERVAL: 10_000, // Check for new task every 10 seconds REQUEST_TIMEOUT: 10_000, // Request timeout in milliseconds (10 seconds) DEBUG_MILESTONE: true, // Show milestone debug logs SERVERS: { local: { label: '🏠 Local', url: 'http://localhost:3000' }, prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' }, }, }; const AppSession = { SESSION_ID: sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_ID), getSessionBinded: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_BINDED) === 'true', setSessionBinded: binded => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_BINDED, binded ? 'true' : 'false'), getSessionToken: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_TOKEN), setSessionToken: token => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_TOKEN, token), getSessionStatus: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.BOT_STATUS), setSessionStatus: status => sessionStorage.setItem(SESSION_STORAGE_KEYS.BOT_STATUS, status), getSessionInitialized: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_INITIALIZED) === 'true', setSessionInitialized: initialized => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_INITIALIZED, initialized ? 'true' : 'false'), // Task state - Trạng thái task hiện tại (lưu trong sessionStorage) getCurrentTask: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_TASK), setCurrentTask: task => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_TASK, task), getCurrentTaskData: () => { const data = sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_TASK_DATA); return data ? JSON.parse(data) : null; }, setCurrentTaskData: data => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_TASK_DATA, JSON.stringify(data)), getCurrentStep: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_STEP), setCurrentStep: step => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_STEP, step), initialize: async () => { try { const isInitialized = AppSession.getSessionInitialized(); if (!isInitialized) { AppSession.setSessionStatus(AppEnums.BOT_STATUS.INITIALIZING); AppSession.setCurrentTask(BAF_TASKS.NO_TASK); AppSession.setCurrentStep(BAF_TASKS.NO_STEP); AppSession.setSessionInitialized(true); } else { } } catch (error) { TL.error('APP-SESSION', '1.2.4. Error during session initialization:', error); throw error; } }, } // ====== APP ENUMS ====== const AppEnums = { BOT_STATUS: { INITIALIZING: 'initializing', STOP: 'stop', PAUSE: 'pause', WAITING_FOR_TASK: 'waiting_for_task', PERFORMING_TASK: 'performing_task', ERROR: 'error', }, BOT_STARTUP_MODE: { AUTO: 'auto', MANUAL: 'manual', } }; // ====== 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 // 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 }; 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.debug('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; } getMenuCreated() { return this.data.menuCreated; } getPopupInitialized() { return this.data.popupInitialized; } /** * 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_startup_mode: 'baf-bot-startup-mode', 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 }); }, getBotStartupMode: () => GM_getValue(AppSettings.key_bot_startup_mode, AppEnums.BOT_STARTUP_MODE.MANUAL), setBotStartupMode: (mode) => GM_setValue(AppSettings.key_bot_startup_mode, mode), // 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] [DEBUG] [${new Date().toLocaleString()}] [${tag}] \n${msg}`, ...args), milestone: (tag, msg, ...args) => CONFIG.DEBUG_MILESTONE && GM_log(`[TL] [MILESTONE] [${new Date().toLocaleString()}] [${tag}] 🎯\n${msg}`, ...args), log: (tag, msg, ...args) => GM_log(`[TL] [LOG] [${new Date().toLocaleString()}] [${tag}] \n${msg}`, ...args), warn: (tag, msg, ...args) => GM_log(`[TL] [WARN] [${new Date().toLocaleString()}] [${tag}] ⚠️\n${msg}`, ...args), error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${new Date().toLocaleString()}] [${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 */ navigateTo: (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, timeout: CONFIG.REQUEST_TIMEOUT, 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: (error) => { TL.error('net', `Request failed: ${url}`, error); resolve({ status: 0, ok: false, data: null, rawText: '', headers: null, error: 'Network error' }); }, ontimeout: () => { TL.error('net', `Request timeout after ${CONFIG.REQUEST_TIMEOUT}ms: ${url}`); resolve({ status: 0, ok: false, data: null, rawText: '', headers: null, error: 'Request timeout' }); }, }); }); }, } }; // ====== BAF API MODULE ====== const BAF_TASKS = { NO_TASK: 'no_task', LOGIN: 'login', WALLET_SPOT: 'wallet_spot', WALLET_FUNDING: 'wallet_funding', WALLET_EARN: 'wallet_earn', ALPHA_SWAP: 'alpha_swap', ALPHA_ORDER_HISTORY: 'alpha_order_history', NO_STEP: 'no_step', }; 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 close button (X) const closeBtn = document.createElement('div'); closeBtn.style.cssText = ` position: absolute; top: -8px; left: -8px; width: 20px; height: 20px; background: #dc3545; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 12px; color: white; border: 2px solid rgba(255, 255, 255, 0.2); transition: all 0.2s ease; font-weight: bold; `; closeBtn.innerHTML = '×'; closeBtn.title = 'Close popup'; closeBtn.addEventListener('click', () => { AppUi.dashboardOverlay.hide(); AppSettings.setPopupVisible(false); }); TL.dom.setHover(closeBtn, 'mouseenter', { scale: 1.5, background: '#c82333' }); TL.dom.setHover(closeBtn, 'mouseleave', { scale: 1, background: '#dc3545' }); overlay.appendChild(closeBtn); // 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.dashboardOverlay.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: () => { try { if (!AppUi.dashboardOverlay.element) { AppUi.dashboardOverlay.create(); } // Get status display let statusDisplay = ''; const currentStatus = AppSession.getSessionStatus(); switch (currentStatus) { case AppEnums.BOT_STATUS.INITIALIZING: statusDisplay = '🔄 Initializing'; break; case AppEnums.BOT_STATUS.STOP: statusDisplay = '⏹️ Stop'; break; case AppEnums.BOT_STATUS.PAUSE: statusDisplay = '⏸️ Pause'; break; case AppEnums.BOT_STATUS.WAITING_FOR_TASK: statusDisplay = '⏳ Waiting for Task'; break; case AppEnums.BOT_STATUS.PERFORMING_TASK: statusDisplay = '▶️ Performing Task'; break; case AppEnums.BOT_STATUS.ERROR: statusDisplay = '❌ Error'; break; default: statusDisplay = '❓ Unknown'; break; } // 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 = `
BAF Agent Dashboard
🛟 Safety: ${safetyStatus.enabled ? '🛡️ Safe' : '🚨 Blocked'}
🆔 Tab: ${AppSession.SESSION_ID.slice(-12)}
🤖 Bot Status: ${statusDisplay}
🚀 Startup Mode: ${AppSettings.getBotStartupMode() === AppEnums.BOT_STARTUP_MODE.AUTO ? '🔄 Auto' : '✋ Manual'}
📋 Task: ${AppSession.getCurrentTask() || 'None'}
📍 Step: ${AppSession.getCurrentStep() || 'None'}
👤 Login: ${loginStatus}
Page: ${pageDisplay}
🌐 Server: ${serverLabel} ${serverConnected}
🐛 Debug: ${AppSettings.getDebug() ? '🔈 ON' : '🔇 OFF'}
🎯 Milestone: ${CONFIG.DEBUG_MILESTONE ? '🔈 ON' : '🔇 OFF'}
⏱️ ${AppServices.Timer.getDisplay()}
▶️ ⏸️ ⏹️
`; // Update content (excluding position button) if (!AppUi.dashboardOverlay.element) { TL.error('DASHBOARD-OVERLAY', '8.1. Element is null!'); return; } const contentDiv = AppUi.dashboardOverlay.element.querySelector('.baf-content') || document.createElement('div'); contentDiv.className = 'baf-content'; contentDiv.innerHTML = content; if (!AppUi.dashboardOverlay.element.querySelector('.baf-content')) { AppUi.dashboardOverlay.element.appendChild(contentDiv); } // Add event listener for safety toggle button const safetyToggleBtn = contentDiv.querySelector('#safety-toggle-btn-AithaiG2'); if (safetyToggleBtn) { safetyToggleBtn.onclick = () => { const currentStatus = { enabled: AppSettings.getSafetyGuard(), message: AppSettings.getSafetyGuard() ? '✅ Operations Enabled' : '🚨 Operations Blocked' }; if (currentStatus.enabled) { AppSettings.setSafetyGuard(false); } else { AppSettings.setSafetyGuard(true); } // Update the content to reflect the change AppUi.dashboardOverlay.updateContent(); }; } // Add event listener for debug toggle button const debugToggleBtn = contentDiv.querySelector('#debug-toggle-btn-Tai5guto'); if (debugToggleBtn) { debugToggleBtn.onclick = () => { const newDebugState = !AppSettings.getDebug(); AppSettings.setDebug(newDebugState); // Update the content to reflect the change AppUi.dashboardOverlay.updateContent(); }; } // Add event listeners for control buttons const playBtn = contentDiv.querySelector('#play-btn-uPha3Mah'); if (playBtn) { playBtn.onclick = () => { AppServices.Status.play(); AppUi.dashboardOverlay.updateContent(); }; } const pauseBtn = contentDiv.querySelector('#pause-btn-Gai2quie'); if (pauseBtn) { pauseBtn.onclick = () => { AppServices.Status.pause(); AppUi.dashboardOverlay.updateContent(); }; } const stopBtn = contentDiv.querySelector('#stop-btn-jooK6che'); if (stopBtn) { stopBtn.onclick = () => { AppServices.Status.stop(); AppUi.dashboardOverlay.updateContent(); }; } // Add hover effects for control buttons if (playBtn) { TL.dom.setHover(playBtn, 'mouseenter', { scale: 1.2, background: 'rgba(40, 167, 69, 0.2)' }); TL.dom.setHover(playBtn, 'mouseleave', { scale: 1, background: 'rgba(40, 167, 69, 0.1)' }); } if (pauseBtn) { TL.dom.setHover(pauseBtn, 'mouseenter', { scale: 1.2, background: 'rgba(255, 193, 7, 0.2)' }); TL.dom.setHover(pauseBtn, 'mouseleave', { scale: 1, background: 'rgba(255, 193, 7, 0.1)' }); } if (stopBtn) { TL.dom.setHover(stopBtn, 'mouseenter', { scale: 1.2, background: 'rgba(220, 53, 69, 0.2)' }); TL.dom.setHover(stopBtn, 'mouseleave', { scale: 1, background: 'rgba(220, 53, 69, 0.1)' }); } // Add event listener for startup mode toggle button const startupModeToggleBtn = contentDiv.querySelector('#startup-mode-toggle-btn-tha9ieCa'); if (startupModeToggleBtn) { startupModeToggleBtn.onclick = () => { const currentStartupMode = AppSettings.getBotStartupMode(); const newStartupMode = currentStartupMode === AppEnums.BOT_STARTUP_MODE.AUTO ? AppEnums.BOT_STARTUP_MODE.MANUAL : AppEnums.BOT_STARTUP_MODE.AUTO; AppSettings.setBotStartupMode(newStartupMode); // Update the content to reflect the change AppUi.dashboardOverlay.updateContent(); }; } // Add event listener for server toggle button const serverToggleBtn = contentDiv.querySelector('#server-toggle-btn-Youx4pho'); if (serverToggleBtn) { serverToggleBtn.onclick = async () => { const currentServerType = AppSettings.getServerType(); const newServerType = currentServerType === 'local' ? 'prod' : 'local'; // Update server type in settings AppSettings.setServerType(newServerType); await AppState.update({ server_last_seen: null }); // Update the content to reflect the change AppUi.dashboardOverlay.updateContent(); }; } } catch (error) { TL.error('DASHBOARD-OVERLAY', 'Error updating content:', error); } }, // Show dashboard overlay show: () => { if (!AppUi.dashboardOverlay.element) { AppUi.dashboardOverlay.create(); } AppUi.dashboardOverlay.updatePosition(); AppUi.dashboardOverlay.updateContent(); AppUi.dashboardOverlay.element.style.display = 'block'; }, // Hide dashboard overlay hide: () => { if (AppUi.dashboardOverlay.element) { AppUi.dashboardOverlay.element.style.display = 'none'; } }, // Toggle dashboard overlay toggle: () => { if (AppSettings.getPopupVisible()) { AppUi.dashboardOverlay.hide(); AppSettings.setPopupVisible(false); } else { AppUi.dashboardOverlay.show(); AppSettings.setPopupVisible(true); } }, // Initialize dashboard overlay init: () => { try { const popupVisible = AppSettings.getPopupVisible(); if (popupVisible) { AppUi.dashboardOverlay.show(); } else { } } catch (error) { TL.error('DASHBOARD-OVERLAY', '1.4.4. Error initializing dashboard overlay:', error); throw error; } } }, // ====== MENU SYSTEM ====== menu: { // Create GM menu create: async () => { try { if (AppState.getMenuCreated()) { return; } AppState.update({ menuCreated: true }); GM_registerMenuCommand('🛟 Safety Guard: -> ✅ Enable Operations', () => { AppSettings.setSafetyGuard(true); }); GM_registerMenuCommand('🛟 Safety Guard: -> 🚨 Block Operations', () => { AppSettings.setSafetyGuard(false); }); GM_registerMenuCommand('🔑 Token', async () => { try { const curToken = await AppSettings.getToken(); const input = prompt('Bearer token:', curToken || ''); if (input !== null && input.trim() && input.trim() !== curToken) { await AppSettings.setToken(input); } const s = AppState.getServer(); const res = await BAF.register({ session_id: AppSession.SESSION_ID }); const resStr = `Server: ${s.label} (${s.url})\n` + `Status: ${res.ok ? 'Connected ✅' : 'Failed ❌'} (${res.status})`; TL.debug(`BAF`, resStr); TL.noti('BAF Server', resStr); } catch (e) { const resStr = `ping error: ${e.message}`; TL.error('BAF', resStr); TL.noti('BAF Server', resStr); } }); // Popup toggle menu GM_registerMenuCommand("👁️ Toggle Popup", () => { AppUi.dashboardOverlay.toggle(); }); } catch (error) { TL.error('MENU', '1.3.5. Failed to create menu:', error); throw error; } } }, }; // ====== APP TASKS ====== const AppTasks = { login: async (step, data) => { if (!AppServices.isReadyToRun()) { TL.debug('APP-TASKS', 'Bot is not ready to run'); return; } TL.debug('APP-TASKS', `Login task ${step}`); //set steps switch (step) { case "start": AppSession.setSessionStatus(AppEnums.BOT_STATUS.PERFORMING_TASK); AppSession.setCurrentStep("navigating-to-login-page"); TL.browser.navigateTo(BINANCE.page.getLoginUrl()); break; } }, }; // ====== APP SERVICE ====== const AppServices = { initialize: async () => { try { await AppState.initialize(); await AppSession.initialize(); // Khởi tạo timer AppServices.Timer.initialize(); await AppUi.menu.create(); AppUi.dashboardOverlay.init(); await AppServices.registerSession(); const status = AppSession.getSessionStatus(); if (status === AppEnums.BOT_STATUS.INITIALIZING) { const startupMode = AppSettings.getBotStartupMode(); if (startupMode === AppEnums.BOT_STARTUP_MODE.AUTO) { AppSession.setSessionStatus(AppEnums.BOT_STATUS.WAITING_FOR_TASK); } else { AppSession.setSessionStatus(AppEnums.BOT_STATUS.STOP); } } AppServices.initInterval(); } catch (error) { TL.error('APP-SERVICES', 'Error during initialization:', error); throw error; } }, registerSession: async () => { try { const msg = await BAF.register({ session_id: AppSession.SESSION_ID }); if (msg.ok) { AppSession.setSessionToken(msg.data.token); AppSession.setSessionBinded(true); } else { AppSession.setSessionBinded(false); TL.error('APP-SERVICES', `1.5.3. Failed to register session: ${msg.error}`); } } catch (error) { TL.error('APP-SERVICES', '1.5.3. Error during session registration:', error); AppSession.setSessionBinded(false); throw error; } }, initInterval: () => { try { setInterval(() => AppServices.heartbeat(), CONFIG.HEARTBEAT_INTERVAL); setInterval(() => AppUi.dashboardOverlay.updateContent(), CONFIG.DASHBOARD_UPDATE_INTERVAL); // Timer update interval - cập nhật timer mỗi giây setInterval(() => { const timerDisplay = document.querySelector('#timer-display-ohr1Youj'); if (timerDisplay) { timerDisplay.textContent = `⏱️ ${AppServices.Timer.getDisplay()}`; } }, 1000); } catch (error) { throw error; } }, isConnected: () => { const serverLastSeen = AppState.getServerLastSeen(); return serverLastSeen && new Date() - serverLastSeen < 60000; }, isReadyToRun: () => { return AppSession.getSessionBinded() && AppSettings.getSafetyGuard() && AppSession.getSessionStatus() === AppEnums.BOT_STATUS.WAITING_FOR_TASK; }, // ====== TIMER SYSTEM ====== Timer: { // Timer state management - Quản lý trạng thái timer getStartTime: () => { const time = sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_START_TIME); return time ? parseInt(time) : null; }, setStartTime: time => { if (time === null || time === undefined) { sessionStorage.removeItem(SESSION_STORAGE_KEYS.TIMER_START_TIME); } else { sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_START_TIME, time.toString()); } }, isRunning: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_IS_RUNNING) === 'true', setRunning: running => sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_IS_RUNNING, running ? 'true' : 'false'), getAccumulated: () => { const time = sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_ACCUMULATED); return time ? parseInt(time) : 0; }, setAccumulated: time => sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_ACCUMULATED, time.toString()), // Timer control methods start: () => { if (!AppServices.Timer.isRunning()) { AppServices.Timer.setStartTime(Date.now()); AppServices.Timer.setRunning(true); TL.debug('TIMER', 'Timer started'); } }, pause: () => { if (AppServices.Timer.isRunning()) { const startTime = AppServices.Timer.getStartTime(); const currentTime = Date.now(); const elapsed = currentTime - startTime; const accumulated = AppServices.Timer.getAccumulated(); AppServices.Timer.setAccumulated(accumulated + elapsed); AppServices.Timer.setRunning(false); AppServices.Timer.setStartTime(null); } }, stop: () => { AppServices.Timer.pause(); AppServices.Timer.setAccumulated(0); }, getDisplay: () => { let totalTime = AppServices.Timer.getAccumulated(); if (AppServices.Timer.isRunning()) { const startTime = AppServices.Timer.getStartTime(); if (startTime) { const currentTime = Date.now(); totalTime += (currentTime - startTime); } } const hours = Math.floor(totalTime / 3600000); const minutes = Math.floor((totalTime % 3600000) / 60000); const seconds = Math.floor((totalTime % 60000) / 1000); const display = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; TL.debug('TIMER', `Display: ${display}, Running: ${AppServices.Timer.isRunning()}, Accumulated: ${totalTime}`); return display; }, initialize: () => { AppServices.Timer.setAccumulated(0); AppServices.Timer.setRunning(false); AppServices.Timer.setStartTime(null); TL.debug('TIMER', 'Timer initialized'); } }, // ====== HEARTBEAT SYSTEM ====== heartbeat: async () => { if (!AppServices.isReadyToRun()) return; try { const status = { logged_in: AppState.getIsLoggedIn(), current_page: AppState.getCurrentPage(), bot_status: AppSession.getSessionStatus(), current_task: AppSession.getCurrentTask(), current_task_data: AppSession.getCurrentTaskData(), current_step: AppSession.getCurrentStep(), }; // TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`); const start = performance.now(); const res = await BAF.ping(status); const end = performance.now(); const duration = Math.round(end - start); if (res.ok) { AppState.update({ server_last_seen: new Date(), server_latency: duration }); } return status; } catch (e) { TL.error('HEARTBEAT', e.message); return null; } }, handleNewTask: async () => { const data = { logged_in: AppState.getIsLoggedIn(), current_page: AppState.getCurrentPage(), bot_status: AppSession.getSessionStatus(), current_task: AppSession.getCurrentTask(), current_task_data: AppSession.getCurrentTaskData(), current_step: AppSession.getCurrentStep(), }; const taskResponse = await BAF.getTask(data); TL.debug('APP-SERVICES', `Task: ${JSON.stringify(taskResponse, null, 2)}`); if (taskResponse.ok) { const task = taskResponse.data; AppSession.setCurrentTask(task.type); AppSession.setCurrentTaskData(task.data || null); switch (task.type) { case BAF_TASKS.NO_TASK: TL.debug('APP-TASK', "No task. Sleep now"); break; case BAF_TASKS.LOGIN: AppTasks.login("start", task.data); break; default: TL.debug('APP-TASK', `Unknown task 🛑`); AppSession.setSessionStatus(AppEnums.BOT_STATUS.ERROR); break; } } }, resumeTask: async () => { TL.debug('APP-SERVICES', 'Resume task'); const task = AppSession.getCurrentTask(); const data = AppSession.getCurrentTaskData(); const step = AppSession.getCurrentStep(); switch (task) { case BAF_TASKS.LOGIN: AppTasks.login(step, data); break; }; }, // ====== BOT STATUS CONTROL ====== Status: { play: () => { AppServices.Timer.start(); AppSession.setSessionStatus(AppEnums.BOT_STATUS.WAITING_FOR_TASK); }, pause: () => { AppServices.Timer.pause(); AppSession.setSessionStatus(AppEnums.BOT_STATUS.PAUSE); }, stop: () => { AppServices.Timer.stop(); AppSession.setCurrentTask(BAF_TASKS.NO_TASK); AppSession.setCurrentTaskData(null); AppSession.setCurrentStep(BAF_TASKS.NO_STEP); AppSession.setSessionStatus(AppEnums.BOT_STATUS.STOP); } }, start: async () => { while (true) { switch (AppSession.getSessionStatus()) { case AppEnums.BOT_STATUS.INITIALIZING: TL.debug('APP-SERVICES', 'Bot is initializing, waiting...'); break; case AppEnums.BOT_STATUS.STOP: TL.debug('APP-SERVICES', 'Bot is stopped, skipping task check'); break; case AppEnums.BOT_STATUS.PAUSE: TL.debug('APP-SERVICES', 'Bot is paused, skipping task check'); break; case AppEnums.BOT_STATUS.WAITING_FOR_TASK: await AppServices.handleNewTask(); break; case AppEnums.BOT_STATUS.PERFORMING_TASK: await AppServices.resumeTask(); break; case AppEnums.BOT_STATUS.ERROR: TL.debug('APP-SERVICES', 'Bot is in error state, skipping task check'); break; default: TL.debug('APP-SERVICES', 'Bot is in unknown state, skipping task check'); break; } await TL.delay(CONFIG.TASK_INTERVAL); } } }; // Start the application try { await AppServices.initialize(); } catch (error) { TL.error('MAIN', 'Failed to initialize app:', error); TL.error('MAIN', 'Error stack:', error.stack); } TL.log('MAIN', 'App started'); await AppServices.start(); })();