From fdd71665b6989cfae48a4e95918597661a06c5be Mon Sep 17 00:00:00 2001 From: thuanle Date: Sun, 3 Aug 2025 00:48:03 +0700 Subject: [PATCH] clean up version --- README.md | 575 --------------- agent.user.js | 1939 +++++++++++++------------------------------------ 2 files changed, 489 insertions(+), 2025 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 499d8d7..0000000 --- a/README.md +++ /dev/null @@ -1,575 +0,0 @@ -# Binance Alpha Farm Agent - -Automated trading agent for Binance Alpha Farm with robust state management and task-step architecture. - -## πŸ“‹ Table of Contents - -- [System Architecture](#system-architecture) -- [Code Structure & Module Order](#code-structure--module-order) -- [Task-Step System](#task-step-system) -- [Bot Status Flow](#bot-status-flow) -- [State Management](#state-management) -- [Navigation State Management](#navigation-state-management) -- [Helper Functions](#helper-functions) -- [Configuration](#configuration) -- [API Endpoints](#api-endpoints) -- [Development Notes](#development-notes) - -## πŸ—οΈ System Architecture - -The agent follows a **hybrid architecture** combining centralized task definitions with dedicated step execution: - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ AppState β”‚ β”‚ TaskRunner β”‚ β”‚ StepRunner β”‚ -β”‚ (State Mgmt) │◄──►│ (Task Mgmt) │◄──►│ (Step Exec) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β–Ό β–Ό β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ StateWatchers β”‚ β”‚ TASK_DEFINITIONSβ”‚ β”‚ Step Functions β”‚ -β”‚ (Observers) β”‚ β”‚ (Task Config) β”‚ β”‚ (Step Logic) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Key Components: -- **AppState**: Centralized state management with observer pattern -- **TaskRunner**: Handles task polling and coordination -- **StepRunner**: Executes individual steps with navigation handling -- **StateWatchers**: React to state changes (page, login, bot status) -- **TASK_DEFINITIONS**: Centralized task configurations -- **Step Functions**: Modular step logic (LoginSteps, OrderHistorySteps, BalanceSteps) - -## πŸ“ Code Structure & Module Order - -The code follows this specific order for maintainability: - -### 1. **Configuration & Constants** -```javascript -// ====== CONFIGURATION ====== -const CONFIG = { ... } - -// ====== ENUMS & CONSTANTS ====== -const BOT_STATUS = { ... } -const TASK_TYPES = { ... } -const STEP_TYPES = { ... } -const LOGIN_METHOD = { ... } -const ORDER_TYPE = { ... } -const ORDER_STATUS = { ... } -const BALANCE_FORMAT = { ... } -const GAS_FEE_TYPE = { ... } -``` - -### 2. **State Management** -```javascript -// ====== STATE MANAGEMENT ====== -class AppState { ... } - -// ====== STORAGE MODULE ====== -const STORAGE = { ... } -``` - -### 3. **Utility Modules** -```javascript -// ====== TL UTILITY MODULE ====== -const TL = { ... } - -// ====== BAF API MODULE ====== -const BAF = { ... } - -// ====== BINANCE PAGE MODULE ====== -const BINANCE = { ... } -``` - -### 4. **Step Functions (Modular Logic)** -```javascript -// ====== STEP FUNCTIONS ====== -const LoginSteps = { ... } -const OrderHistorySteps = { ... } -const BalanceSteps = { ... } -``` - -### 5. **Task Definitions** -```javascript -// ====== TASK DEFINITIONS ====== -const TASK_DEFINITIONS = { ... } -``` - -### 6. **Core Execution Engine** -```javascript -// ====== STEP RUNNER ====== -class StepRunner { ... } - -// ====== TASK RUNNER ====== -const TaskRunner = { ... } - -// ====== STATE WATCHERS ====== -const StateWatchers = { ... } -``` - -### 7. **UI & Initialization** -```javascript -// ====== UI & MENU ====== -async function createGM_Menu() { ... } - -// ====== MONITORING ====== -async function heartbeat_report() { ... } -async function monitorPageChanges() { ... } - -// ====== INITIALIZATION ====== -async function initialize() { ... } -``` - -## πŸ”„ Task-Step System - -### Task Types -```javascript -const TASK_TYPES = { - LOGIN: 'login', - GET_ORDER_HISTORY: 'get_order_history', - GET_BALANCE: 'get_balance', - SWAP: 'swap', - NO_TASK: 'no_task' -}; -``` - -### Step Types by Task - -#### **Login Task** -```javascript -const STEP_TYPES = { - LOGIN: { - NAVIGATE_TO_LOGIN: 'NavigateToLogin', - SELECT_QR_CODE: 'SelectQRCode', - WAIT_FOR_LOGIN: 'WaitForLogin' - } -}; -``` - -#### **Order History Task** -```javascript -const STEP_TYPES = { - GET_ORDER_HISTORY: { - NAVIGATE_TO_ORDER_HISTORY: 'NavigateToOrderHistory', - EXTRACT_ORDER_DATA: 'ExtractOrderData', - SEND_ORDER_DATA: 'SendOrderData' - } -}; -``` - -#### **Balance Task** -```javascript -const STEP_TYPES = { - GET_BALANCE: { - NAVIGATE_TO_BALANCE: 'NavigateToBalance', - EXTRACT_BALANCE_DATA: 'ExtractBalanceData', - SEND_BALANCE_DATA: 'SendBalanceData' - } -}; -``` - -### Task Data Examples - -#### **Login Task** -```javascript -{ - type: TASK_TYPES.LOGIN, - id: "login_123", - data: { - method: LOGIN_METHOD.QR_CODE, - timeout: 300000, // 5 minutes - retry_count: 3 - } -} -``` - -#### **Order History Task** -```javascript -{ - type: TASK_TYPES.GET_ORDER_HISTORY, - id: "order_history_456", - data: { - limit: 50, - date_from: "2024-01-01", - date_to: "2024-12-31", - status: ORDER_STATUS.COMPLETED - } -} -``` - -#### **Balance Task** -```javascript -{ - type: TASK_TYPES.GET_BALANCE, - id: "balance_789", - data: { - currencies: ["USDT", "BTC", "ETH"], - format: BALANCE_FORMAT.DECIMAL - } -} -``` - -## πŸ€– Bot Status Flow - -### Status Transitions -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ ready-for-new- │────►│ performing- │────►│ ready-for-new- β”‚ -β”‚ tasks β”‚ β”‚ tasks β”‚ β”‚ tasks β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β–² - β–Ό β–Ό β”‚ -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ pause- │◄───── Task Complete β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -β”‚ automation β”‚ β”‚ or Error β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Flow Description: -1. **ready-for-new-tasks**: Bot polls server for new tasks -2. **performing-tasks**: Bot executes task steps -3. **pause-automation**: Bot stops (manual or error) -4. **Back to ready-for-new-tasks**: After task completion or resume - -### Task Polling Logic: -```javascript -// Poll only when ready-for-new-tasks -if (bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) { - const task = await BAF.getTask(); - if (task.type === TASK_TYPES.NO_TASK) { - // Sleep for interval before next poll - await delay(CONFIG.task_poll_interval); - } else { - // Start task execution - bot_status = BOT_STATUS.PERFORMING_TASKS; - } -} -``` - -## 🎯 State Management - -### AppState Structure -```javascript -class AppState { - data = { - // Server configuration - server_type: 'prod', - server_url: null, - - // Page state - current_page: null, - is_logged_in: false, - - // Bot status - bot_status: BOT_STATUS.READY_FOR_NEW_TASKS, - - // Task state - current_task: null, - task_data: null, - current_step: null, - step_data: null, - - // UI state - is_loading: false, - error_message: null - }; -} -``` - -### Observer Pattern -```javascript -// Subscribe to state changes -APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange); -APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange); -APP_STATE.subscribe('is_logged_in', StateWatchers.onLoginStateChange); - -// Notify observers on state change -await APP_STATE.update({ bot_status: BOT_STATUS.PERFORMING_TASKS }); -``` - -## 🧭 Navigation State Management - -### Navigation State Structure -```javascript -{ - navigating: true, - navigation_start: 1703123456789, - navigation_target: "https://accounts.binance.com/login", - task_id: "task_123", - step_index: 0 -} -``` - -### Navigation Flow -``` -1. Step executes β†’ ctx.goto('/login') - ↓ -2. Mark navigating = true - ↓ -3. Save navigation state to storage - ↓ -4. window.location.href = '/login' - ↓ -5. Page reloads, userscript restarts - ↓ -6. resumeTask() β†’ detect navigation state - ↓ -7. handleNavigationResume() β†’ check target page - ↓ -8. Skip navigation step, continue next step -``` - -### guardDoubleRun Helper -```javascript -// Prevents double execution during navigation -await TL.guardDoubleRun(ctx, async () => { - // Step logic here - if (!BINANCE.isOnLoginPage()) { - BINANCE.navigateToLogin(); - } -}); -``` - -## πŸ› οΈ Helper Functions - -### TL Utility Module -```javascript -const TL = { - // Logging - log: (level, message, data) => { ... }, - debug: (level, message, data) => { ... }, - error: (level, message, error) => { ... }, - - // Utilities - delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)), - notification: (title, text, timeout) => { ... }, - - // Navigation guard - guardDoubleRun: async (ctx, stepLogic) => { ... }, - - // DOM helpers - dom: { - isVisible: (el) => { ... }, - isDisabled: (el) => { ... }, - click: (el) => { ... }, - scrollToView: (el, behavior) => { ... } - }, - - // Network helpers - net: { - gmRequest: (url, init) => { ... } - } -}; -``` - -### TaskRunner Helpers -```javascript -const TaskRunner = { - // Task validation - validateEnum: (value, enumObj, defaultValue) => { ... }, - validateTaskData: (taskType, taskData) => { ... }, - - // Task state - getCurrentTaskData: () => { ... }, - updateTaskData: (newData) => { ... }, - isCurrentTaskType: (taskType) => { ... }, - - // Task execution - checkForNewTasks: async () => { ... }, - continueCurrentStep: async () => { ... } -}; -``` - -### StepRunner Context -```javascript -// Context passed to step functions -{ - task_id: "task_123", - task_type: TASK_TYPES.LOGIN, - task_data: { method: LOGIN_METHOD.QR_CODE }, - step_data: {}, - is_logged_in: false, - current_page: BINANCE_PAGES.LOGIN, - current_step_name: "NavigateToLogin", - - // Helper methods - done: (result) => this.completeTask(result), - goto: (url) => this.navigateTo(url), - wait: (ms) => this.wait(ms), - retry: (fn, maxAttempts) => this.retry(fn, maxAttempts) -} -``` - -## βš™οΈ Configuration - -### Server Configuration -```javascript -const CONFIG = { - heartbeat_interval: 10000, // 10 seconds - task_poll_interval: 10000, // 10 seconds - is_debug: true, - servers: { - local: { - label: '🏠 Local', - url: 'http://localhost:3000' - }, - prod: { - label: '🌐 Prod', - url: 'https://baf.thuanle.me' - } - } -}; -``` - -### Storage Keys -```javascript -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' -}; -``` - -## 🌐 API Endpoints - -### BAF API Methods -```javascript -const BAF = { - // Server management - getServer: async () => { ... }, - getHost: async () => { ... }, - - // API requests - request: async (method, path, options) => { ... }, - - // Task management - getTask: async () => { ... }, - submitTaskResult: async (taskId, result) => { ... }, - - // Heartbeat - heartbeat: async (data) => { ... } -}; -``` - -### Page Detection -```javascript -const BINANCE = { - // Page detection - detectPage: () => { ... }, - isOnLoginPage: () => { ... }, - isOnAlphaSwapPage: () => { ... }, - isOnAlphaOrderHistoryPage: () => { ... }, - isLoggedIn: async () => { ... }, - - // Navigation - navigateToLogin: () => { ... }, - navigateToAlphaSwap: () => { ... }, - navigateToAlphaOrderHistory: () => { ... } -}; -``` - -## πŸš€ Development Notes - -### Adding New Tasks - -1. **Define Task Type**: -```javascript -const TASK_TYPES = { - // ... existing types - NEW_TASK: 'new-task' -}; -``` - -2. **Create Step Functions**: -```javascript -const NewTaskSteps = { - matchStep1: (url, ctx) => { /* match logic */ }, - runStep1: async (ctx) => { - await TL.guardDoubleRun(ctx, async () => { - // Step logic - }); - } -}; -``` - -3. **Add Task Definition**: -```javascript -const TASK_DEFINITIONS = { - [TASK_TYPES.NEW_TASK]: { - id: "new-task", - steps: [ - { - name: "Step1", - match: NewTaskSteps.matchStep1, - run: NewTaskSteps.runStep1 - } - ] - } -}; -``` - -### Adding New Steps - -1. **Add Step Type**: -```javascript -const STEP_TYPES = { - NEW_TASK: { - // ... existing steps - NEW_STEP: 'NewStep' - } -}; -``` - -2. **Implement Step Functions**: -```javascript -const NewTaskSteps = { - // ... existing steps - matchNewStep: (url, ctx) => { - return url.includes("/target-page") && ctx?.step_data?.previous_step_completed; - }, - - runNewStep: async (ctx) => { - await TL.guardDoubleRun(ctx, async () => { - // Step logic here - ctx.step_data.new_step_completed = true; - }); - } -}; -``` - -3. **Update Task Definition**: -```javascript -[TASK_TYPES.NEW_TASK]: { - id: "new-task", - steps: [ - // ... existing steps - { - name: "NewStep", - match: NewTaskSteps.matchNewStep, - run: NewTaskSteps.runNewStep - } - ] -} -``` - -### Best Practices - -1. **Always use `guardDoubleRun`** for step functions to prevent double execution -2. **Use enums** for type safety (TASK_TYPES, LOGIN_METHOD, etc.) -3. **Validate task data** using `TaskRunner.validateTaskData` -4. **Handle navigation** using `ctx.goto()` instead of direct `window.location` -5. **Use context helpers** (`ctx.wait()`, `ctx.retry()`) for robust execution -6. **Log appropriately** using `TL.log()`, `TL.debug()`, `TL.error()` -7. **Update step_data** to track progress and avoid duplicate work -8. **Handle errors gracefully** with try-catch and proper error reporting - -### Debugging Tips - -1. **Enable debug mode**: Set `CONFIG.is_debug = true` -2. **Check navigation state**: Use `STORAGE.getNavigationState()` -3. **Monitor state changes**: Watch `APP_STATE.getData()` values -4. **Check step execution**: Look for `[STEP]` logs in console -5. **Verify page detection**: Use `BINANCE.detectPage()` to check current page -6. **Test step matching**: Use step `match` functions directly -7. **Check task data**: Use `TaskRunner.getCurrentTaskData()` \ No newline at end of file diff --git a/agent.user.js b/agent.user.js index 6bbe864..62428f8 100644 --- a/agent.user.js +++ b/agent.user.js @@ -23,7 +23,6 @@ if (window.top !== window) { GM_log(`[TL] ❌ Skipping in iframe ${window.location.href}`); return; } - GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); (async () => { @@ -31,90 +30,121 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); // ====== CONFIGURATION ====== const CONFIG = { - heartbeat_interval: 10000, - task_poll_interval: 10000, - - servers: { + HEARTBEAT_INTERVAL: 10000, + SERVERS: { local: { label: '🏠 Local', url: 'http://localhost:3000' }, prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' }, } }; + + // ====== APP ENUMS ====== + const AppEnums = { + BOT_STATUS: { + IDLE: 'idle', + RUNNING: 'running', + } + }; + // ====== STATE MANAGEMENT ====== - class AppState { + /** + * 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 - server_type: 'prod', - server_url: null, - server_last_seen: null, + // Server configuration - ThΓ΄ng tin server hiện tαΊ‘i + server: null, // URL cα»§a server hiện tαΊ‘i (tα»« AppSettings) + server_last_seen: null, // Thời gian cuα»‘i cΓΉng ping server thΓ nh cΓ΄ng - // 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 + // 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(); - this.initialized = false; + this.observers = new Map(); // Observer pattern cho state changes + this.initialized = false; // Flag khởi tαΊ‘o } - // Initialize state from storage and page detection + /** + * 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; - // 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(); + // Load server URL tα»« AppSettings + const serverType = await AppSettings.getServerType(); + const server = CONFIG.SERVERS[serverType] || CONFIG.SERVERS.prod; - // Detect current page and login state - this.data.current_page = BINANCE.detectPage(); - this.data.is_logged_in = await BINANCE.isLoggedIn(); + // Detect trang hiện tαΊ‘i (khΓ΄ng block) + const currentPage = BINANCE.page.detectPage(); - TL.debug('STATE', 'Initialized', this.data); + // Update state (khΓ΄ng bao gα»“m login status) + await this.update({ + server, + 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 }; } - // Update state with partial update + // ====== GETTER METHODS ====== + getServer() { return this.data.server; } + getServerLastSeen() { return this.data.server_last_seen; } + /** + * 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, []); @@ -122,6 +152,11 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); 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) { @@ -132,7 +167,11 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); } } - // Notify all observers + /** + * 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]; @@ -150,55 +189,59 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); } } } + const AppState = new AppStateClass(); - // ====== STORAGE MODULE ====== - const STORAGE = { + + // ====== 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_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', + key_safety_guard: 'baf-safety-guard', - getToken: () => GM_getValue(STORAGE.key_token, ''), - setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')), + // Token management + getToken: () => GM_getValue(AppSettings.key_token, ''), + setToken: token => GM_setValue(AppSettings.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), + // Server configuration + getServerType: () => GM_getValue(AppSettings.key_server_type, 'prod'), + setServerType: (type) => GM_setValue(AppSettings.key_server_type, type), - getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.WAITING), - setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status), + getBotStatus: () => GM_getValue(AppSettings.key_bot_status, AppEnums.BOT_STATUS.IDLE), + setBotStatus: (status) => GM_setValue(AppSettings.key_bot_status, status), - getNavigationState: () => { - try { - return JSON.parse(GM_getValue(STORAGE.key_navigation_state, 'null')); - } catch { - return null; - } + // 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}`); }, - - 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 ====== + // ====== COMMON UTILITIES ====== + /** + * TL - Common utilities cho logging, notifications, vΓ  DOM helpers + */ const TL = { - debug: (tag, msg, ...args) => APP_STATE.getData().is_debug && GM_log(`[TL] [${tag}]\n${msg}`, ...args), + 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), @@ -214,121 +257,111 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); 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; - } + // 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; + }, - // 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; - } + isDisabled: (ele) => { + return ele?.disabled === true || ele?.getAttribute('aria-disabled') === 'true'; + }, - try { - await stepLogic(); + 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) + ); + }, - // 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; + 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; } }, - }; - // 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')), + // 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 = { - 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; - }, + getHost: () => AppState.getServer()?.url, request: async (method, path, { params, body, headers } = {}) => { - const base = await BAF.getHost(); + 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 || {}); - const token = await STORAGE.getToken(); + const token = await AppSettings.getToken(); if (token && !h.has('Authorization')) h.set('Authorization', `Bearer ${token}`); const init = { method, headers: h }; @@ -346,12 +379,11 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); 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 ====== - + + // Binance Pages constants const BINANCE_PAGES = { LOGIN: 'login', ALPHA_SWAP: 'alpha-swap', @@ -363,6 +395,7 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); UNKNOWN: 'unknown' }; + // Page patterns for detection const BINANCE_PAGE_PATTERNS = [ { host: 'accounts.binance.com', @@ -382,1053 +415,127 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); } ]; - const BINANCE = { - detectPage: () => { - if (!SafetyGuard.check('detectPage')) return BINANCE_PAGES.IGNORE; + page: { + detectPage: () => { + const { hostname, pathname } = window.location; + if (window.top !== window) { + TL.debug('BINANCE-PAGE-DETECT', 'Call from iframe ❌'); + 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; + } - const hostConfig = BINANCE_PAGE_PATTERNS.find(cfg => cfg.host === hostname); - if (!hostConfig) { + // 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; - } + }, - // Log current URL details - const msg = `Current URL: ${window.location.href}\n` + - `Hostname: ${hostname}, Pathname: ${pathname}\n` + - `window.top === window : ${window.top === window}\n`; + 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, - // 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; + // 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; } - } - TL.debug('PAGE_DETECT', msg + `No pattern matched: ${hostname}${pathname}`); - return BINANCE_PAGES.UNKNOWN; - }, + if (BINANCE.page.isOnLoginPage()) { + return false; + } - 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, + // 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'); - detectLoginState: () => { - if (!SafetyGuard.check('detectLoginState')) return null; + let msg = `loginBtn: ${TL.dom.isVisible?.(loginBtn)} regBtn: ${TL.dom.isVisible?.(regBtn)}\n`; - if (window.top !== window) { - TL.debug('BINANCE-LOGIN', 'In iframe - cannot determine login state'); + // 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; - } - - 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 + }, + + 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); } - // 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(); + const fallback = BINANCE.auth.detectLoginState() ?? false; + TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`); + return fallback; } } }; - - // ====== 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 = { + + // ====== APP UI ====== + const AppUi = { // ====== STATUS OVERLAY ====== statusOverlay: { element: null, @@ -1446,8 +553,8 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); // Create status overlay element create: () => { - if (UI.statusOverlay.element) { - document.body.removeChild(UI.statusOverlay.element); + if (AppUi.statusOverlay.element) { + document.body.removeChild(AppUi.statusOverlay.element); } const overlay = document.createElement('div'); @@ -1492,25 +599,16 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); `; 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(); + const newPosition = (AppSettings.getPopupPosition() % 4) + 1; + AppSettings.setPopupPosition(newPosition); + AppUi.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'; - }); - + TL.dom.setHover(positionBtn, 'mouseenter', { scale: 1.5, background: '#0056b3' }); + TL.dom.setHover(positionBtn, 'mouseleave', { scale: 1, background: '#007bff' }); overlay.appendChild(positionBtn); - UI.statusOverlay.element = overlay; + + AppUi.statusOverlay.element = overlay; document.body.appendChild(overlay); return overlay; @@ -1518,53 +616,43 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); // Update overlay position updatePosition: () => { - if (!UI.statusOverlay.element) return; + if (!AppUi.statusOverlay.element) return; - const data = APP_STATE.getData(); - const positionCSS = UI.statusOverlay.getPositionCSS(data.popup_position); + const positionCSS = AppUi.statusOverlay.getPositionCSS(AppSettings.getPopupPosition()); - Object.assign(UI.statusOverlay.element.style, positionCSS); + Object.assign(AppUi.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'; + switch (AppSettings.getBotStatus()) { + case AppEnums.BOT_STATUS.IDLE: + statusDisplay = 'πŸ’€ Idle'; break; - case BOT_STATUS.RUNNING: + case AppEnums.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 ? '🟒' : 'πŸ”΄'; + const serverLabel = AppState.getServer()?.label || 'Unknown'; + const serverConnected = AppState.getServerConnected() ? '🟒' : 'πŸ”΄'; // Get page info - const pageDisplay = data.current_page || 'unknown'; + const pageDisplay = AppState.getCurrentPage() || 'unknown'; // Get login status - const loginStatus = data.is_logged_in ? 'βœ…' : '❌'; + const loginStatus = AppState.getIsLoggedIn() ? 'βœ…' : '❌'; // Get SafetyGuard status - const safetyStatus = SafetyGuard.getStatus(); + const safetyStatus = { + enabled: AppSettings.getSafetyGuard(), + message: AppSettings.getSafetyGuard() ? 'βœ… Operations Enabled' : '🚨 Operations Blocked' + }; // Build content const content = ` @@ -1574,7 +662,7 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); πŸ›Ÿ Safety: ${safetyStatus.enabled ? 'πŸ›‘οΈ Safe' : '🚨 Blocked'}
- πŸ› Debug: ${data.is_debug ? 'βœ”οΈ ON' : '❌ OFF'} + πŸ› Debug: ${AppSettings.getDebug() ? 'βœ”οΈ ON' : '❌ OFF'}
Bot Status: ${statusDisplay} @@ -1585,40 +673,35 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.');
πŸ‘€ Login:${loginStatus}
-
- Task: ${taskName} -
-
- Step: ${stepName} -
Page: ${pageDisplay} -
- - ${data.error_message ? `
Error: ${data.error_message}
` : ''} +
`; // Update content (excluding position button) - const contentDiv = UI.statusOverlay.element.querySelector('.baf-content') || document.createElement('div'); + const contentDiv = AppUi.statusOverlay.element.querySelector('.baf-content') || document.createElement('div'); contentDiv.className = 'baf-content'; contentDiv.innerHTML = content; - if (!UI.statusOverlay.element.querySelector('.baf-content')) { - UI.statusOverlay.element.appendChild(contentDiv); + if (!AppUi.statusOverlay.element.querySelector('.baf-content')) { + AppUi.statusOverlay.element.appendChild(contentDiv); } // Add event listener for safety toggle button - const toggleBtn = contentDiv.querySelector('#safety-toggle-btn'); - if (toggleBtn) { - toggleBtn.onclick = () => { - const currentStatus = SafetyGuard.getStatus(); + const safetyToggleBtn = contentDiv.querySelector('#safety-toggle-btn'); + if (safetyToggleBtn) { + safetyToggleBtn.onclick = () => { + const currentStatus = { + enabled: AppSettings.getSafetyGuard(), + message: AppSettings.getSafetyGuard() ? 'βœ… Operations Enabled' : '🚨 Operations Blocked' + }; if (currentStatus.enabled) { - SafetyGuard.disable(); + AppSettings.setSafetyGuard(false); } else { - SafetyGuard.enable(); + AppSettings.setSafetyGuard(true); } // Update the content to reflect the change - UI.statusOverlay.updateContent(); + AppUi.statusOverlay.updateContent(); }; } @@ -1626,218 +709,174 @@ GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.'); const debugToggleBtn = contentDiv.querySelector('#debug-toggle-btn'); if (debugToggleBtn) { debugToggleBtn.onclick = () => { - const currentData = APP_STATE.getData(); - const newDebugState = !currentData.is_debug; - APP_STATE.update({ is_debug: newDebugState }); - STORAGE.setDebug(newDebugState); + const newDebugState = !AppSettings.getDebug(); + AppSettings.setDebug(newDebugState); // Update the content to reflect the change - UI.statusOverlay.updateContent(); + AppUi.statusOverlay.updateContent(); }; } }, // Show status overlay show: () => { - if (!UI.statusOverlay.element) { - UI.statusOverlay.create(); + if (!AppUi.statusOverlay.element) { + AppUi.statusOverlay.create(); } - UI.statusOverlay.updatePosition(); - UI.statusOverlay.updateContent(); - UI.statusOverlay.element.style.display = 'block'; + AppUi.statusOverlay.updatePosition(); + AppUi.statusOverlay.updateContent(); + AppUi.statusOverlay.element.style.display = 'block'; }, // Hide status overlay hide: () => { - if (UI.statusOverlay.element) { - UI.statusOverlay.element.style.display = 'none'; + if (AppUi.statusOverlay.element) { + AppUi.statusOverlay.element.style.display = 'none'; } }, // Toggle status overlay toggle: () => { - const data = APP_STATE.getData(); - if (data.popup_visible) { - UI.statusOverlay.hide(); - APP_STATE.update({ popup_visible: false }); - STORAGE.setPopupVisible(false); + if (AppSettings.getPopupVisible()) { + AppUi.statusOverlay.hide(); + AppSettings.setPopupVisible(false); } else { - UI.statusOverlay.show(); - APP_STATE.update({ popup_visible: true }); - STORAGE.setPopupVisible(true); + AppUi.statusOverlay.show(); + AppSettings.setPopupVisible(true); } }, // Initialize status overlay init: () => { - const data = APP_STATE.getData(); - if (data.popup_visible) { - UI.statusOverlay.show(); + if (AppSettings.getPopupVisible()) { + AppUi.statusOverlay.show(); } } }, - // ====== NOTIFICATION SYSTEM ====== - notification: { - // Show success notification - success: (title, message, duration = 3000) => { - TL.noti(title, message, duration); - }, + // ====== MENU SYSTEM ====== + menu: { + // Create GM menu + create: async () => { + try { + if (AppState.getMenuCreated()) { + return; + } - // Show error notification - error: (title, message, duration = 5000) => { - TL.noti(`❌ ${title}`, message, duration); - }, + AppState.update({ menuCreated: true }); - // Show warning notification - warning: (title, message, duration = 4000) => { - TL.noti(`⚠️ ${title}`, message, duration); - }, + GM_registerMenuCommand('πŸ›Ÿ Safety Guard: -> βœ… Enable Operations', () => { + AppSettings.setSafetyGuard(true); + }); - // Show info notification - info: (title, message, duration = 3000) => { - TL.noti(`ℹ️ ${title}`, message, duration); + 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.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", () => { + AppUi.statusOverlay.toggle(); + }); + + } catch (error) { + TL.error('MENU', 'Failed to create menu:', error); + throw error; + } } }, + }; - // ====== LOADING INDICATOR ====== - loading: { - element: null, + // ====== APP SERVICE ====== + const AppServices = { + initialize: async () => { + await AppState.initialize(); + await AppUi.menu.create(); + AppUi.statusOverlay.init(); - // Show loading indicator - show: (message = 'Loading...') => { - if (UI.loading.element) { - UI.loading.hide(); + // Setup interval services + AppServices.initInterval(); + + // Detect login status after initialization (non-blocking) + setTimeout(async () => { + await AppState.detectLoginStatus(); + }, 1000); // Delay 1 second to let page load + + TL.log('APP-SERVICES', 'AppServices initialized'); + }, + + // Setup interval services + initInterval: () => { + // Setup heartbeat interval + setInterval(() => AppServices.heartbeat(), CONFIG.HEARTBEAT_INTERVAL); + + TL.debug('APP-SERVICES', 'Interval services started'); + }, + + isConnected: () => { + const serverLastSeen = AppState.getServerLastSeen(); + return serverLastSeen && new Date() - serverLastSeen < 60000; + }, + + // Refresh login status + refreshLoginStatus: async () => { + await AppState.detectLoginStatus(); + }, + + // ====== HEARTBEAT SYSTEM ====== + heartbeat: async () => { + if (!AppSettings.getSafetyGuard()) return null; + + try { + const status = { + logged_in: AppState.getIsLoggedIn(), + current_page: AppState.getCurrentPage(), + bot_status: AppSettings.getBotStatus(), + current_task: null, // TODO: Add to AppState if needed + task_data: null, // TODO: Add to AppState if needed + current_step: null, // TODO: Add to AppState if needed + }; + + // TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`); + const res = await BAF.ping(status); + if (res.ok) { + AppState.update({ server_last_seen: new Date() }); } - const loading = document.createElement('div'); - loading.id = 'baf-loading-indicator'; - loading.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 1000000; - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 20px; - border-radius: 8px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 14px; - text-align: center; - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - `; - loading.innerHTML = ` -
⏳
-
${message}
- `; - - UI.loading.element = loading; - document.body.appendChild(loading); - }, - - // Hide loading indicator - hide: () => { - if (UI.loading.element) { - document.body.removeChild(UI.loading.element); - UI.loading.element = null; - } + return status; + } catch (e) { + TL.error('HEARTBEAT', e.message); + return null; } } }; - // ====== SAFETY GUARD SYSTEM ====== - const SafetyGuard = { - status: true, // true = safe to proceed, false = blocked - - // Check if it's safe to proceed with an action - check: (actionName = 'unknown') => { - return SafetyGuard.status; // true = safe to proceed - }, - - // Enable safety (allow actions) - enable: () => { - SafetyGuard.status = true; - TL.noti('βœ… Safety Guard', 'Bot operations enabled', 3000); - TL.debug('SAFETY_GUARD', 'Safety guard enabled'); - }, - - // Disable safety (block all actions) - disable: () => { - SafetyGuard.status = false; - TL.noti('🚨 Safety Guard', 'Bot operations blocked', 10000); - TL.debug('SAFETY_GUARD', 'Safety guard disabled'); - }, - - // Get current status - getStatus: () => ({ - enabled: SafetyGuard.status, - message: SafetyGuard.status ? 'βœ… Operations Enabled' : '🚨 Operations Blocked' - }) - }; - - - // ====== INITIALIZATION ====== - const APP_STATE = new AppState(); - - async function initialize() { - // Initialize state - await APP_STATE.initialize(); - - // Register observers - APP_STATE.subscribe('server_type', StateWatchers.onServerTypeChange); - APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange); - APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange); - APP_STATE.subscribe('popup_position', StateWatchers.onPopupPositionChange); - APP_STATE.subscribe('popup_visible', StateWatchers.onPopupVisibleChange); - - // Register general state observer for popup content updates - APP_STATE.subscribe('current_task', StateWatchers.onAnyStateChange); - APP_STATE.subscribe('current_step', StateWatchers.onAnyStateChange); - APP_STATE.subscribe('error_message', StateWatchers.onAnyStateChange); - APP_STATE.subscribe('bot_status', StateWatchers.onAnyStateChange); - - // Create menu - await createGM_Menu(); - - // Initialize popup helper - UI.statusOverlay.init(); - - // Start heartbeat - setInterval(heartbeat_report, CONFIG.heartbeat_interval); - - // Update popup content periodically - setInterval(() => { - if (UI.statusOverlay.element && APP_STATE.getData().popup_visible) { - UI.statusOverlay.updateContent(); - } - }, 1000); // Update every 2 seconds - - // Welcome message - const ping_res = await BAF.ping(); - const data = APP_STATE.getData(); - const res = - `Server: ${data.server_url.label} (${data.server_url.url})\n` + - `Status: ${ping_res.ok ? 'Connected βœ…' : 'Failed ❌'} (${ping_res.status})\n` + - `Page: ${data.current_page || 'unknown'} (${window.location.href})\n` + - `Bot Status: ${data.bot_status}`; - - TL.log(`BAF`, res); - - // Resume task if performing or start new task checking if ready - if (data.bot_status === BOT_STATUS.RUNNING && data.current_task) { - TL.log('INIT', 'Resuming interrupted task'); - TaskRunner.stepRunner.resumeTask(); - } else if (data.bot_status === BOT_STATUS.WAITING) { - TaskRunner.checkForNewTasks(); - } else if (data.bot_status === BOT_STATUS.PAUSED) { - TL.log('INIT', 'Bot is paused - waiting for resume'); - } else if (data.bot_status === BOT_STATUS.STOPPED) { - TL.log('INIT', 'Bot is stopped - waiting for start'); - } - } - // Start the application - await initialize(); + try { + await AppServices.initialize(); + } catch (error) { + TL.error('MAIN', 'Failed to initialize app:', error); + } + + TL.log('MAIN', 'App started'); })(); \ No newline at end of file