// ==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'; // ====== CONFIGURATION ====== const CONFIG = { heartbeat_interval: 10000, task_poll_interval: 10000, servers: { local: { label: 'π Local', url: 'http://localhost:3000' }, prod: { label: 'π Prod', url: 'https://baf.thuanle.me' }, } }; // ====== STATE MANAGEMENT ====== class AppState { constructor() { this.data = { // Server configuration server_type: 'prod', server_url: null, server_last_seen: null, // Page state current_page: null, is_logged_in: false, // Bot status bot_status: BOT_STATUS.WAITING, // Task state current_task: null, task_data: null, current_step: null, step_data: null, // UI state is_loading: false, error_message: null, // Popup state popup_position: 4, // 1: top-left, 2: top-right, 3: bottom-left, 4: bottom-right popup_visible: true, // Debug state is_debug: true }; this.observers = new Map(); this.initialized = false; } // Initialize state from storage and page detection async initialize() { if (this.initialized) return; this.initialized = true; // Load from storage this.data.server_type = await STORAGE.getServerType(); this.data.server_url = CONFIG.servers[this.data.server_type] || CONFIG.servers.prod; this.data.bot_status = await STORAGE.getBotStatus(); this.data.popup_position = await STORAGE.getPopupPosition(); this.data.popup_visible = await STORAGE.getPopupVisible(); this.data.is_debug = await STORAGE.getDebug(); // Detect current page and login state this.data.current_page = BINANCE.detectPage(); this.data.is_logged_in = await BINANCE.isLoggedIn(); TL.debug('STATE', 'Initialized', this.data); } getData() { return { ...this.data }; } // Update state with partial update async update(updates) { const oldData = { ...this.data }; Object.assign(this.data, updates); await this.notifyObservers(oldData, this.data); TL.debug('STATE', 'Updated', updates); } subscribe(key, callback) { if (!this.observers.has(key)) { this.observers.set(key, []); } this.observers.get(key).push(callback); } unsubscribe(key, callback) { const callbacks = this.observers.get(key); if (callbacks) { const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } } // Notify all observers 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); } } } } } } // ====== STORAGE MODULE ====== const STORAGE = { key_token: 'baf-agent-token', key_server_type: 'baf-server-type', key_bot_status: 'baf-bot-status', key_navigation_state: 'baf-navigation-state', key_popup_position: 'baf-popup-position', key_popup_visible: 'baf-popup-visible', key_debug: 'baf-debug', getToken: () => GM_getValue(STORAGE.key_token, ''), setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')), getServerType: () => GM_getValue(STORAGE.key_server_type, 'prod'), setServerType: (type) => GM_setValue(STORAGE.key_server_type, type), getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.WAITING), setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status), getNavigationState: () => { try { return JSON.parse(GM_getValue(STORAGE.key_navigation_state, 'null')); } catch { return null; } }, setNavigationState: (state) => { GM_setValue(STORAGE.key_navigation_state, JSON.stringify(state)); }, clearNavigationState: () => { GM_setValue(STORAGE.key_navigation_state, 'null'); }, getPopupPosition: () => GM_getValue(STORAGE.key_popup_position, 4), setPopupPosition: (position) => GM_setValue(STORAGE.key_popup_position, position), getPopupVisible: () => GM_getValue(STORAGE.key_popup_visible, true), setPopupVisible: (visible) => GM_setValue(STORAGE.key_popup_visible, visible), getDebug: () => GM_getValue(STORAGE.key_debug, true), setDebug: (debug) => GM_setValue(STORAGE.key_debug, debug) }; // ====== UTILITY MODULE ====== const TL = { debug: (tag, msg, ...args) => APP_STATE.getData().is_debug && 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)), // Guard against double execution during navigation guardDoubleRun: async (ctx, stepLogic) => { // Check if we're currently navigating if (ctx.step_data.navigating) { TL.debug('STEP', 'Currently navigating, skipping step execution'); return; } // Check if this step was already completed after navigation if (ctx.step_data.navigation_complete && ctx.step_data.last_completed_step === ctx.current_step_name) { TL.debug('STEP', 'Step already completed after navigation, skipping'); return; } try { await stepLogic(); // Mark step as completed (unless navigation was triggered) if (!ctx.step_data.navigating) { ctx.step_data.last_completed_step = ctx.current_step_name; } } catch (error) { TL.error('STEP', `Error in step ${ctx.current_step_name}:`, error); throw error; } }, }; // DOM helpers TL.dom = { isVisible: (el) => { if (!el) return false; const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; return (el.offsetWidth + el.offsetHeight) > 0; }, isDisabled: (el) => { return el?.disabled === true || el?.getAttribute('aria-disabled') === 'true'; }, isInViewport: (el) => { if (!el) return false; const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }, click: (el) => { if (el && typeof el.click === 'function') el.click(); }, scrollToView: (el, behavior = 'smooth') => { el?.scrollIntoView?.({ behavior, block: 'center' }); }, }; // Network helpers TL.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 = { getServer: async () => { const data = APP_STATE.getData(); return CONFIG.servers[data.server_type] || CONFIG.servers.prod; }, getHost: async () => { const s = await BAF.getServer(); return s.url; }, request: async (method, path, { params, body, headers } = {}) => { const base = await 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 || {}); const token = await STORAGE.getToken(); 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); }, get: (path, params, init = {}) => BAF.request('GET', path, { params, headers: init.headers }), post: (path, body, init = {}) => BAF.request('POST', path, { body, headers: init.headers }), ping: (status) => BAF.post('/agent/ping', status), getTask: (status) => BAF.get('/agent/task', status), submitTaskResult: (taskId, result) => BAF.post(`/agent/tasks/${taskId}/result`, result), }; // ====== BINANCE MODULE ====== 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' }; 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 = { detectPage: () => { if (!SafetyGuard.check('detectPage')) return BINANCE_PAGES.IGNORE; 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; } // Log current URL details const msg = `Current URL: ${window.location.href}\n` + `Hostname: ${hostname}, Pathname: ${pathname}\n` + `window.top === window : ${window.top === window}\n`; // Check patterns in order (most specific first) for (const pattern of hostConfig.patterns) { if (pathname.includes(pattern.includes)) { TL.debug('PAGE_DETECT', msg + `Matched pattern: ${pattern.includes} -> ${pattern.page}`); return pattern.page; } } TL.debug('PAGE_DETECT', msg + `No pattern matched: ${hostname}${pathname}`); return BINANCE_PAGES.UNKNOWN; }, isOnLoginPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.LOGIN, isOnAlphaSwapPage: (contractAddress) => { const page = APP_STATE.getData().current_page; return page === BINANCE_PAGES.ALPHA_SWAP && (!contractAddress || window.location.pathname.includes(contractAddress)); }, isOnAlphaOrderHistoryPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.ALPHA_ORDER_HISTORY, isOnWalletEarnPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_EARN, isOnWalletFundingPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_FUNDING, isOnWalletSpotPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_SPOT, detectLoginState: () => { if (!SafetyGuard.check('detectLoginState')) return null; if (window.top !== window) { TL.debug('BINANCE-LOGIN', 'In iframe - cannot determine login state'); return null; } if (BINANCE.isOnLoginPage()) { return false; } // otherwise // if (BINANCE.isOnAlphaSwapPage()) { // 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 += `dashboard: ${dashboardLink}, accountIcon: ${accountIcon}, walletBtn: ${walletBtn}\n`; 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.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.detectLoginState() ?? false; TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`); return fallback; }, navigateToLogin: () => { const loginBtn = document.querySelector('#toLoginPage'); if (TL.dom.isVisible?.(loginBtn)) { TL.dom.click(loginBtn); } }, navigateToAlphaSwap: (contractAddress) => { if (BINANCE.isOnAlphaSwapPage(contractAddress)) return; window.location.href = contractAddress ? `https://www.binance.com/en/alpha/bsc/${contractAddress}` : `https://www.binance.com/en/alpha`; }, navigateToAlphaOrderHistory: () => { if (!BINANCE.isOnAlphaOrderHistoryPage()) { window.location.href = 'https://www.binance.com/en/my/orders/alpha/orderhistory'; } }, navigateToWalletEarn: () => { if (!BINANCE.isOnWalletEarnPage()) { window.location.href = 'https://www.binance.com/en/my/wallet/account/earn'; } }, navigateToWalletFunding: () => { if (!BINANCE.isOnWalletFundingPage()) { window.location.href = 'https://www.binance.com/en/my/wallet/funding'; } }, navigateToWalletSpot: () => { if (!BINANCE.isOnWalletSpotPage()) { window.location.href = 'https://www.binance.com/en/my/wallet/account/main'; } } }; // ====== TASK & STEP SYSTEM ====== // Task Types Enum const TASK_TYPES = { LOGIN: 'login', GET_ORDER_HISTORY: 'get_order_history', GET_BALANCE: 'get_balance', SWAP: 'swap', NO_TASK: 'no_task' }; // Bot Status Enum const BOT_STATUS = { WAITING: 'waiting', // π€ waiting for new task RUNNING: 'running', // βΆοΈ Δang chαΊ‘y task PAUSED: 'paused', // βΈοΈ Δang pause STOPPED: 'stopped' // βΉοΈ ΔΓ£ stop }; // Login Method Enum const LOGIN_METHOD = { QR_CODE: 'qr_code', }; // Order Type Enum const ORDER_TYPE = { SWAP: 'swap', LIMIT: 'limit', MARKET: 'market' }; // Order Status Enum const ORDER_STATUS = { COMPLETED: 'completed', PENDING: 'pending', CANCELLED: 'cancelled' }; // Balance Format Enum const BALANCE_FORMAT = { JSON: 'json', CSV: 'csv' }; // Gas Fee Type Enum const GAS_FEE_TYPE = { AUTO: 'auto', FAST: 'fast', SLOW: 'slow' }; const STEP_TYPES = { // Login task steps NAVIGATE_TO_LOGIN: 'navigate_to_login', SELECT_QR_CODE: 'select_qr_code', SEND_TO_SERVER: 'send_to_server', WAIT_FOR_LOGIN: 'wait_for_login', REPORT_RESULT: 'report_result', // Order history task steps NAVIGATE_TO_ORDER_HISTORY: 'navigate_to_order_history', EXTRACT_ORDER_DATA: 'extract_order_data', SEND_ORDER_DATA: 'send_order_data', // Balance task steps NAVIGATE_TO_BALANCE: 'navigate_to_balance', EXTRACT_BALANCE_DATA: 'extract_balance_data', SEND_BALANCE_DATA: 'send_balance_data', // Swap task steps NAVIGATE_TO_SWAP: 'navigate_to_swap', FILL_SWAP_FORM: 'fill_swap_form', CONFIRM_SWAP: 'confirm_swap', WAIT_FOR_SWAP: 'wait_for_swap', // Common steps WAIT: 'wait', ERROR_HANDLING: 'error_handling' }; // ====== STEP FUNCTIONS ====== // Login Step Functions const LoginSteps = { matchNavigateToLogin: (url, ctx) => { // // const isOnLoginPage = url.includes("/login"); // // const isLoggedIn = ctx?.is_logged_in || false; // // // Only navigate to login if: // // // 1. Already on login page, OR // // // 2. Not logged in AND on a page that doesn't require login // // if (isOnLoginPage) { // // return true; // // } // // if (!isLoggedIn) { // // // Check if we're on a page that requires login // // // const currentPage = BINANCE.detectPage(); // // const requiresLoginPages = [ // // BINANCE_PAGES.ALPHA_SWAP, // // BINANCE_PAGES.ALPHA_ORDER_HISTORY, // // BINANCE_PAGES.WALLET_EARN, // // BINANCE_PAGES.WALLET_FUNDING, // // BINANCE_PAGES.WALLET_SPOT // // ]; // // // If we're on a page that requires login but not logged in, // // // don't navigate to login (let the page handle it) // // if (requiresLoginPages.includes(currentPage)) { // // return false; // // } // // // Only navigate to login if on a public page // return true; // } return false; }, runNavigateToLogin: async (ctx) => { await TL.guardDoubleRun(ctx, async () => { // TL.log('STEP', 'NavigateToLogin: Starting login process'); // if (!BINANCE.isOnLoginPage()) { // BINANCE.navigateToLogin(); // ctx.step_data.page_navigated = true; // } // ctx.step_data.page_loaded = true; // ctx.step_data.start_time = Date.now(); }); }, matchSelectQRCode: (url, ctx) => { return url.includes("/login") && ctx?.step_data?.page_loaded && ctx?.task_data?.method === LOGIN_METHOD.QR_CODE; }, runSelectQRCode: async (ctx) => { // await TL.guardDoubleRun(ctx, async () => { // TL.log('STEP', 'SelectQRCode: Selecting QR code method'); // const qrButton = document.querySelector('[data-testid="qr-code-tab"], .qr-code-btn, .qr-tab'); // if (qrButton) { // qrButton.click(); // ctx.step_data.qr_selected = true; // TL.debug('STEP', 'QR code method selected'); // } else { // TL.debug('STEP', 'QR code button not found, may already be selected'); // ctx.step_data.qr_selected = true; // } // }); }, matchWaitForLogin: (url, ctx) => { return !url.includes("/login") && ctx?.step_data?.qr_selected; }, runWaitForLogin: async (ctx) => { // is } }; // Order History Step Functions const OrderHistorySteps = { //TODO: Implement }; // Balance Step Functions const BalanceSteps = { //TODO: Implement }; // ====== TASK DEFINITIONS ====== const TASK_DEFINITIONS = { [TASK_TYPES.LOGIN]: { id: TASK_TYPES.LOGIN, steps: [ { name: "NavigateToLogin", match: LoginSteps.matchNavigateToLogin, run: LoginSteps.runNavigateToLogin }, { name: "SelectQRCode", match: LoginSteps.matchSelectQRCode, run: LoginSteps.runSelectQRCode }, { name: "WaitForLogin", match: LoginSteps.matchWaitForLogin, run: LoginSteps.runWaitForLogin } ] }, [TASK_TYPES.GET_ORDER_HISTORY]: { id: TASK_TYPES.GET_ORDER_HISTORY, steps: [ //TODO: Implement ] }, [TASK_TYPES.GET_BALANCE]: { id: TASK_TYPES.GET_BALANCE, steps: [ //TODO: Implement ] }, [TASK_TYPES.SWAP]: { id: TASK_TYPES.SWAP, steps: [ //TODO: Implement ] }, [TASK_TYPES.NO_TASK]: { id: TASK_TYPES.NO_TASK, steps: [ //TODO: Implement ] } }; // ====== STEP RUNNER ====== class StepRunner { constructor() { this.currentStepIndex = 0; this.context = null; } // Create context for task execution createContext(task, taskData) { return { task_id: task.id, task_type: task.type, task_data: taskData, step_data: {}, is_logged_in: APP_STATE.getData().is_logged_in, current_page: APP_STATE.getData().current_page, current_step_name: null, // Will be set by executeCurrentStep done: (result) => this.completeTask(result), goto: (url) => this.navigateTo(url), wait: (ms) => this.wait(ms), retry: (fn, maxAttempts = 3) => this.retry(fn, maxAttempts) }; } // Navigate to URL navigateTo(url) { if (!url || typeof url !== 'string') { TL.error('STEP', 'Invalid URL provided for navigation:', url); return; } try { // Handle different URL formats let targetUrl = url; if (url.startsWith('/')) { // Relative URL - append to current domain targetUrl = window.location.origin + url; } else if (!url.startsWith('http')) { // Relative URL without leading slash targetUrl = window.location.origin + '/' + url; } // Absolute URL (starts with http) - use as is TL.debug('STEP', `Navigating to: ${targetUrl}`); // Mark that we're navigating to avoid double execution this.context.step_data.navigating = true; this.context.step_data.navigation_start = Date.now(); this.context.step_data.navigation_target = targetUrl; // Save navigation state to storage for persistence across page reload STORAGE.setNavigationState({ navigating: true, navigation_start: this.context.step_data.navigation_start, navigation_target: targetUrl, task_id: this.context.task_id, step_index: this.currentStepIndex }); window.location.href = targetUrl; } catch (error) { TL.error('STEP', 'Navigation failed:', error); // Clear navigation state on error this.context.step_data.navigating = false; delete this.context.step_data.navigation_start; delete this.context.step_data.navigation_target; } } // Wait for specified milliseconds async wait(ms) { if (ms && ms > 0) { TL.debug('STEP', `Waiting for ${ms}ms`); await TL.delay(ms); } } // Retry function with exponential backoff async retry(fn, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { TL.debug('STEP', `Attempt ${attempt} failed:`, error.message); if (attempt === maxAttempts) { throw error; } // Exponential backoff: 1s, 2s, 4s const delay = Math.pow(2, attempt - 1) * 1000; await this.wait(delay); } } } // Complete task and return to ready state async completeTask(result) { try { TL.log('STEP', `Task completed with result:`, result); const data = APP_STATE.getData(); if (data.current_task) { // Submit result to server try { await BAF.submitTaskResult(data.current_task.id, result); TL.debug('STEP', 'Task result submitted to server successfully'); } catch (error) { TL.error('STEP', 'Failed to submit task result to server:', error); // Continue with cleanup even if server submission fails } } // Return to waiting state await APP_STATE.update({ current_task: null, task_data: null, current_step: null, step_data: null, bot_status: BOT_STATUS.WAITING, is_loading: false, error_message: result.success ? null : result.error }); // Reset step runner state this.currentStepIndex = 0; this.context = null; // Clear navigation state STORAGE.clearNavigationState(); // Check for next task after a short delay setTimeout(() => TaskRunner.checkForNewTasks(), 1000); } catch (error) { TL.error('STEP', 'Error during task completion:', error); // Force return to ready state even if cleanup fails await APP_STATE.update({ bot_status: BOT_STATUS.WAITING, is_loading: false, error_message: 'Task completion error: ' + error.message }); } } // Execute current step async executeCurrentStep() { if (!SafetyGuard.check('executeCurrentStep')) return; const data = APP_STATE.getData(); if (!data.current_task || !this.context) { TL.debug('STEP', 'No current task or context available'); return; } // Check if bot is paused or stopped if (data.bot_status === BOT_STATUS.PAUSED) { TL.debug('STEP', 'Bot is paused - skipping step execution'); return; } if (data.bot_status === BOT_STATUS.STOPPED) { TL.debug('STEP', 'Bot is stopped - skipping step execution'); return; } const taskDefinition = TASK_DEFINITIONS[data.current_task.type]; if (!taskDefinition) { TL.error('STEP', `No task definition found for type: ${data.current_task.type}`); await this.completeTask({ success: false, error: 'Unknown task type' }); return; } const currentStep = taskDefinition.steps[this.currentStepIndex]; if (!currentStep) { TL.error('STEP', `No step found at index: ${this.currentStepIndex}`); await this.completeTask({ success: false, error: 'Step not found' }); return; } // Set current step name in context this.context.current_step_name = currentStep.name; const url = window.location.href; TL.debug('STEP', `Checking step: ${currentStep.name} at URL: ${url}`); if (currentStep.match(url, this.context)) { try { TL.log('STEP', `Executing step: ${currentStep.name}`); // Update context with current state this.context.is_logged_in = APP_STATE.getData().is_logged_in; this.context.current_page = APP_STATE.getData().current_page; // Execute step (guardDoubleRun is already wrapped in step functions) await currentStep.run(this.context); // Check if navigation was triggered if (this.context.step_data.navigating) { TL.debug('STEP', 'Navigation triggered, stopping execution'); return; } // Move to next step this.currentStepIndex++; await APP_STATE.update({ current_step: currentStep.name, step_data: this.context.step_data }); // Continue with next step if available if (this.currentStepIndex < taskDefinition.steps.length) { TL.debug('STEP', `Moving to next step: ${this.currentStepIndex + 1}/${taskDefinition.steps.length}`); setTimeout(() => this.executeCurrentStep(), 100); } else { // All steps completed TL.log('STEP', 'All steps completed successfully'); await this.completeTask({ success: true, message: 'All steps completed' }); } } catch (error) { TL.error('STEP', `Error executing step ${currentStep.name}:`, error); await this.completeTask({ success: false, error: error.message }); } } else { TL.debug('STEP', `Step ${currentStep.name} conditions not met, waiting for page change...`); // Don't retry immediately, wait for page change or state change } } // Resume task from saved state async resumeTask() { const data = APP_STATE.getData(); if (!data.current_task) { return; } // Check for navigation state const navigationState = STORAGE.getNavigationState(); if (navigationState && navigationState.navigating) { await this.handleNavigationResume(navigationState); return; } // Create context from saved state this.context = this.createContext(data.current_task, data.task_data); this.context.step_data = data.step_data || {}; // Resume from current step await this.executeCurrentStep(); } // Handle navigation resume async handleNavigationResume(navigationState) { TL.log('STEP', 'Resuming after navigation:', navigationState); const data = APP_STATE.getData(); if (!data.current_task || data.current_task.id !== navigationState.task_id) { TL.warn('STEP', 'Task ID mismatch, clearing navigation state'); STORAGE.clearNavigationState(); return; } // Check if we're on the target page const currentUrl = window.location.href; const targetUrl = navigationState.navigation_target; if (currentUrl.includes(targetUrl) || this.isSamePage(currentUrl, targetUrl)) { TL.log('STEP', 'Successfully navigated to target page'); // Clear navigation state STORAGE.clearNavigationState(); // Create context and continue this.context = this.createContext(data.current_task, data.task_data); this.context.step_data = data.step_data || {}; this.currentStepIndex = navigationState.step_index; // Mark navigation as complete this.context.step_data.navigating = false; this.context.step_data.navigation_complete = true; this.context.step_data.navigation_complete_time = Date.now(); // Continue with next step this.currentStepIndex++; await this.executeCurrentStep(); } else { // Still navigating or navigation failed const elapsed = Date.now() - navigationState.navigation_start; const timeout = 10000; // 10 seconds timeout if (elapsed > timeout) { TL.error('STEP', 'Navigation timeout, clearing state'); STORAGE.clearNavigationState(); await this.completeTask({ success: false, error: 'Navigation timeout' }); } else { TL.debug('STEP', 'Still navigating, waiting...'); // Wait a bit more for navigation to complete setTimeout(() => this.handleNavigationResume(navigationState), 500); } } } // Check if two URLs represent the same page isSamePage(url1, url2) { try { const parsed1 = new URL(url1); const parsed2 = new URL(url2); // Compare hostname and pathname return parsed1.hostname === parsed2.hostname && parsed1.pathname === parsed2.pathname; } catch { // Fallback to string comparison return url1 === url2; } } // Validate task definition validateTaskDefinition(taskType) { const taskDefinition = TASK_DEFINITIONS[taskType]; if (!taskDefinition) { throw new Error(`No task definition found for type: ${taskType}`); } if (!taskDefinition.steps || !Array.isArray(taskDefinition.steps)) { throw new Error(`Invalid steps array for task type: ${taskType}`); } taskDefinition.steps.forEach((step, index) => { if (!step.name || !step.match || !step.run) { throw new Error(`Invalid step at index ${index} for task type: ${taskType}`); } }); return taskDefinition; } // Start new task async startTask(task, taskData) { try { TL.log('STEP', `Starting task: ${task.type}`, task); // Validate task definition this.validateTaskDefinition(task.type); this.currentStepIndex = 0; this.context = this.createContext(task, taskData); await APP_STATE.update({ current_task: task, task_data: taskData, current_step: null, step_data: {}, bot_status: BOT_STATUS.RUNNING, is_loading: true, error_message: null }); // Start executing steps await this.executeCurrentStep(); } catch (error) { TL.error('STEP', 'Failed to start task:', error); await this.completeTask({ success: false, error: error.message }); } } } // ====== TASK RUNNER ====== const TaskRunner = { stepRunner: new StepRunner(), // Helper function to validate enum values validateEnum(value, enumObj, defaultValue) { const validValues = Object.values(enumObj); if (validValues.includes(value)) { return value; } TL.debug('TASK', `Invalid enum value: ${value}, using default: ${defaultValue}`); return defaultValue; }, // Helper function to validate and merge task data with defaults validateTaskData(taskType, taskData = {}) { const mergedData = { ...taskData }; // Validate enum fields if present if (taskType === TASK_TYPES.LOGIN && mergedData.method) { mergedData.method = this.validateEnum(mergedData.method, LOGIN_METHOD, LOGIN_METHOD.QR_CODE); } if (taskType === TASK_TYPES.GET_ORDER_HISTORY) { if (mergedData.order_types) { mergedData.order_types = mergedData.order_types.map(type => this.validateEnum(type, ORDER_TYPE, ORDER_TYPE.SWAP) ); } if (mergedData.status) { mergedData.status = mergedData.status.map(status => this.validateEnum(status, ORDER_STATUS, ORDER_STATUS.COMPLETED) ); } } if (taskType === TASK_TYPES.GET_BALANCE && mergedData.format) { mergedData.format = this.validateEnum(mergedData.format, BALANCE_FORMAT, BALANCE_FORMAT.JSON); } if (taskType === TASK_TYPES.SWAP && mergedData.gas_fee && typeof mergedData.gas_fee === 'string') { mergedData.gas_fee = this.validateEnum(mergedData.gas_fee, GAS_FEE_TYPE, GAS_FEE_TYPE.AUTO); } TL.debug('TASK', `Validated task data for ${taskType}:`, mergedData); return mergedData; }, // Helper function to get current task data getCurrentTaskData() { const data = APP_STATE.getData(); return data.task_data || {}; }, // Helper function to update task data async updateTaskData(newData) { const currentData = this.getCurrentTaskData(); const updatedData = { ...currentData, ...newData }; await APP_STATE.update({ task_data: updatedData }); return updatedData; }, // Helper function to check if current task is of specific type isCurrentTaskType(taskType) { const data = APP_STATE.getData(); return data.current_task?.type === taskType; }, // Helper function to get current task type getCurrentTaskType() { const data = APP_STATE.getData(); return data.current_task?.type || null; }, async checkForNewTasks() { if (!SafetyGuard.check('checkForNewTasks')) return; try { const data = APP_STATE.getData(); if (data.bot_status !== BOT_STATUS.WAITING && data.bot_status !== BOT_STATUS.RUNNING) { TL.debug('TASK', 'Bot not ready for new tasks, status:', data.bot_status); return; } const response = await BAF.getTask(); if (response.ok && response.data.task) { const task = response.data.task; TL.log('TASK', `Received new task: ${task.type}`, task); // Validate task data const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data); // Start task using StepRunner await this.stepRunner.startTask(task, validatedTaskData); } else if (response.ok && response.data.no_task) { TL.debug('TASK', 'No new tasks available'); // Sleep for interval then check again setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval); } } catch (error) { TL.error('TASK', 'Failed to check for new tasks:', error); // Retry after interval setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval); } }, // Continue current step (called by observers) async continueCurrentStep() { await this.stepRunner.executeCurrentStep(); } }; // ====== STATE OBSERVERS ====== const StateWatchers = { // Storage observer - saves state changes to storage async onServerTypeChange(oldValue, newValue, data) { TL.debug('OBSERVER', `Server mode changed: ${oldValue} -> ${newValue}`); await STORAGE.setServerType(newValue); // Update server info await APP_STATE.update({ server_url: CONFIG.servers[newValue] || CONFIG.servers.prod }); // Reload page after a short delay setTimeout(() => location.reload(), 500); }, async onBotStatusChange(oldValue, newValue, data) { TL.debug('OBSERVER', `Bot status changed: ${oldValue} -> ${newValue}`); await STORAGE.setBotStatus(newValue); // Handle status-specific logic if (newValue === BOT_STATUS.WAITING) { // Start checking for new tasks TaskRunner.checkForNewTasks(); } else if (newValue === BOT_STATUS.RUNNING) { // Continue with current task or start new task checking if (data.current_task) { // Continue current task TaskRunner.continueCurrentStep(); } else { // Start checking for new tasks TaskRunner.checkForNewTasks(); } } else if (newValue === BOT_STATUS.PAUSED) { // Pause current operations TL.debug('OBSERVER', 'Bot paused - stopping task operations'); } else if (newValue === BOT_STATUS.STOPPED) { // Stop all operations TL.debug('OBSERVER', 'Bot stopped - clearing current task'); await APP_STATE.update({ current_task: null, task_data: null, current_step: null, step_data: null, is_loading: false, error_message: null }); } // Refresh menu to show new status setTimeout(() => createGM_Menu(), 100); }, async onCurrentPageChange(oldValue, newValue, data) { TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`); // Handle page-specific logic for current task if (data.current_task && data.bot_status === BOT_STATUS.RUNNING) { // Continue with current step based on page change await TaskRunner.continueCurrentStep(); } }, // Popup observers async onPopupPositionChange(oldValue, newValue, data) { TL.debug('OBSERVER', `Popup position changed: ${oldValue} -> ${newValue}`); UI.statusOverlay.updatePosition(); }, async onPopupVisibleChange(oldValue, newValue, data) { TL.debug('OBSERVER', `Popup visibility changed: ${oldValue} -> ${newValue}`); if (newValue) { UI.statusOverlay.show(); } else { UI.statusOverlay.hide(); } }, // General state change observer for popup content updates async onAnyStateChange(oldValue, newValue, data) { // Update popup content when any relevant state changes if (UI.statusOverlay.element && data.popup_visible) { UI.statusOverlay.updateContent(); } } }; // ====== MENU SYSTEM ====== let menuCreated = false; async function createGM_Menu() { if (menuCreated) { // Clear existing menu items if recreating // Note: GM_registerMenuCommand doesn't have a clear method, // so we'll just set a flag to prevent recreation return; } menuCreated = true; GM_registerMenuCommand('π Safety Guard Status', () => { const status = SafetyGuard.getStatus(); alert(`Safety Guard Status:\n\n${status.message}\nEnabled: ${status.enabled}`); }); GM_registerMenuCommand(' - β Enable Operations', () => { SafetyGuard.enable(); }); GM_registerMenuCommand(' - π¨ Block Operations', () => { SafetyGuard.disable(); }); const data = APP_STATE.getData(); const curSrv = data.server_url; GM_registerMenuCommand(`π Server: ${curSrv.label} (${curSrv.url})`, async () => { try { const next = (data.server_type === 'local') ? 'prod' : 'local'; await APP_STATE.update({ server_type: next }); const nsv = CONFIG.servers[next]; const msg = `Switched to ${nsv.label} (${nsv.url})`; TL.debug(`BAF`, msg); TL.noti('BAF Server Switched', msg); } catch (e) { TL.error('BAF', 'switch server error', e); TL.noti('BAF Server Switched', `Switch server error: ${e.message}`); } }); GM_registerMenuCommand('π Token', async () => { try { const curToken = await STORAGE.getToken(); const input = prompt('Bearer token:', curToken || ''); if (input !== null && input.trim() && input.trim() !== curToken) { await STORAGE.setToken(input); } const s = await BAF.getServer(); const res = await BAF.ping(); 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", () => { UI.statusOverlay.toggle(); }); } // ====== HEARTBEAT ====== async function heartbeat_report() { if (!SafetyGuard.check('heartbeat_report')) return null; try { const data = APP_STATE.getData(); const status = { logged_in: data.is_logged_in, current_page: data.current_page, bot_status: data.bot_status, current_task: data.current_task?.type || null, task_data: data.task_data || null, current_step: data.current_step || null, }; // TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`); const res = await BAF.ping(status); if (res.ok) { APP_STATE.update({ server_last_seen: new Date() }); } return status; } catch (e) { TL.error('HEARTBEAT', e.message); return null; } } // ====== UI SYSTEM ====== const UI = { // ====== STATUS OVERLAY ====== statusOverlay: { 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 status overlay element create: () => { if (UI.statusOverlay.element) { document.body.removeChild(UI.statusOverlay.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 data = APP_STATE.getData(); const newPosition = (data.popup_position % 4) + 1; APP_STATE.update({ popup_position: newPosition }); STORAGE.setPopupPosition(newPosition); UI.statusOverlay.updatePosition(); }); positionBtn.addEventListener('mouseenter', () => { positionBtn.style.transform = 'scale(1.5)'; positionBtn.style.background = '#0056b3'; }); positionBtn.addEventListener('mouseleave', () => { positionBtn.style.transform = 'scale(1)'; positionBtn.style.background = '#007bff'; }); overlay.appendChild(positionBtn); UI.statusOverlay.element = overlay; document.body.appendChild(overlay); return overlay; }, // Update overlay position updatePosition: () => { if (!UI.statusOverlay.element) return; const data = APP_STATE.getData(); const positionCSS = UI.statusOverlay.getPositionCSS(data.popup_position); Object.assign(UI.statusOverlay.element.style, positionCSS); }, // Update overlay content updateContent: () => { const data = APP_STATE.getData(); const taskName = data.current_task?.type || 'No task'; const stepName = data.current_step || 'No step'; // Get status display let statusDisplay = ''; switch (data.bot_status) { case BOT_STATUS.WAITING: statusDisplay = 'π€ Waiting'; break; case BOT_STATUS.RUNNING: statusDisplay = 'βΆοΈ Running'; break; case BOT_STATUS.PAUSED: statusDisplay = 'βΈοΈ Paused'; break; case BOT_STATUS.STOPPED: statusDisplay = 'βΉοΈ Stopped'; break; default: statusDisplay = 'β Unknown'; } // Get server info const serverLabel = data.server_url?.label || 'Unknown'; // data.server_last_seen < 1min == connected const serverConnected = data.server_last_seen && new Date() - data.server_last_seen < 60000 ? 'π’' : 'π΄'; // Get page info const pageDisplay = data.current_page || 'unknown'; // Get login status const loginStatus = data.is_logged_in ? 'β ' : 'β'; // Get SafetyGuard status const safetyStatus = SafetyGuard.getStatus(); // Build content const content = `