diff --git a/README.md b/README.md index 4943fd9..d47e65b 100644 --- a/README.md +++ b/README.md @@ -1,473 +1,575 @@ # Binance Alpha Farm Agent -Automated trading agent for Binance Alpha Farm with Task-Step architecture. +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 -### Core Components +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**: Task execution engine -- **StateWatchers**: Observers for state changes -- **BAF API**: Server communication module -- **BINANCE**: Binance page detection and navigation +- **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) -## ๐Ÿ“‹ Task-Step System +## ๐Ÿ“ 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 -TASK_TYPES = { +const TASK_TYPES = { LOGIN: 'login', - GET_ORDER_HISTORY: 'get_order_history', - GET_BALANCE: 'get_balance', + GET_ORDER_HISTORY: 'get-order-history', + GET_BALANCE: 'get-balance', SWAP: 'swap', - NO_TASK: 'no_task' + 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 + } } ``` -### Step Types - +#### **Order History Task** ```javascript -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' +{ + 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 Types - -```javascript -BOT_STATUS = { - READY_FOR_NEW_TASKS: 'ready-for-new-tasks', - PAUSE_AUTOMATION: 'pause-automation', - PERFORMING_TASKS: 'performing-tasks' -} -``` - -### Main Flow - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Initialize โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Check Bot Statusโ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Ready for Tasks โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Ask Server โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ for Tasks โ”‚ - โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ - โ–ผ โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Sleep โ”‚ โ”‚ No Task Found โ”‚ -โ”‚ (Interval) โ”‚โ—€โ”€โ”€โ”€โ”€โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Check Bot Statusโ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Task Received โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ - โ–ผ โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Ready for Tasks โ”‚ โ”‚ Perform Task โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Task Complete โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Return to Ready โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - ### Status Transitions - -1. **READY_FOR_NEW_TASKS** - - Poll server for new tasks - - Sleep for `task_poll_interval` if no tasks - - Transition to `PERFORMING_TASKS` when task received - -2. **PAUSE_AUTOMATION** - - Stop all automation - - No polling or task execution - - Manual intervention required - -3. **PERFORMING_TASKS** - - Execute current task step by step - - Update task data during execution - - Transition back to `READY_FOR_NEW_TASKS` when complete - -## ๐Ÿ“Š Task Data Examples - -### Login Task - -```javascript -{ - type: TASK_TYPES.LOGIN, - id: "task_123", - data: { - method: LOGIN_METHOD.QR_CODE, // 'qr_code' | 'password' | 'email' - credentials: { - email: 'user@example.com', // For password method - password: 'password123' // For password method - }, - timeout: 300000, // 5 minutes timeout - retry_count: 3 - }, - steps: [ - STEP_TYPES.NAVIGATE_TO_LOGIN, - STEP_TYPES.SELECT_QR_CODE, - STEP_TYPES.SEND_TO_SERVER, - STEP_TYPES.WAIT_FOR_LOGIN, - STEP_TYPES.REPORT_RESULT - ] -} +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ready-for-new- โ”‚โ”€โ”€โ”€โ”€โ–บโ”‚ performing- โ”‚โ”€โ”€โ”€โ”€โ–บโ”‚ ready-for-new- โ”‚ +โ”‚ tasks โ”‚ โ”‚ tasks โ”‚ โ”‚ tasks โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ–ฒ + โ–ผ โ–ผ โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ pause- โ”‚โ—„โ”€โ”€โ”€โ”€โ”ค Task Complete โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”‚ automation โ”‚ โ”‚ or Error โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -### Get Order History Task +### 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 -{ - type: TASK_TYPES.GET_ORDER_HISTORY, - id: "task_124", - data: { - limit: 100, - offset: 0, - date_range: { - start_date: '2024-01-01', - end_date: '2024-12-31' - }, - order_types: [ - ORDER_TYPE.SWAP, - ORDER_TYPE.LIMIT, - ORDER_TYPE.MARKET - ], - status: [ - ORDER_STATUS.COMPLETED, - ORDER_STATUS.PENDING, - ORDER_STATUS.CANCELLED - ] - }, - steps: [ - STEP_TYPES.NAVIGATE_TO_ORDER_HISTORY, - STEP_TYPES.EXTRACT_ORDER_DATA, - STEP_TYPES.SEND_ORDER_DATA - ] -} -``` - -### Get Balance Task - -```javascript -{ - type: TASK_TYPES.GET_BALANCE, - id: "task_125", - data: { - currencies: ['USDT', 'BTC', 'ETH'], // null for all currencies - include_zero: false, - include_locked: true, - format: BALANCE_FORMAT.JSON // 'json' | 'csv' - }, - steps: [ - STEP_TYPES.NAVIGATE_TO_BALANCE, - STEP_TYPES.EXTRACT_BALANCE_DATA, - STEP_TYPES.SEND_BALANCE_DATA - ] -} -``` - -### Swap Task - -```javascript -{ - type: TASK_TYPES.SWAP, - id: "task_126", - data: { - from_token: 'USDT', - to_token: 'BTC', - amount: 100, - slippage: 0.5, // percentage - gas_fee: GAS_FEE_TYPE.AUTO, // 'auto' | 'fast' | 'slow' | number - deadline: 300, // seconds - auto_confirm: true - }, - steps: [ - STEP_TYPES.NAVIGATE_TO_SWAP, - STEP_TYPES.FILL_SWAP_FORM, - STEP_TYPES.CONFIRM_SWAP, - STEP_TYPES.WAIT_FOR_SWAP - ] -} -``` - -## ๐Ÿ”ง Enums & Constants - -### Login Method -```javascript -LOGIN_METHOD = { - QR_CODE: 'qr_code', - PASSWORD: 'password', - EMAIL: 'email' -} -``` - -### Order Type -```javascript -ORDER_TYPE = { - SWAP: 'swap', - LIMIT: 'limit', - MARKET: 'market' -} -``` - -### Order Status -```javascript -ORDER_STATUS = { - COMPLETED: 'completed', - PENDING: 'pending', - CANCELLED: 'cancelled' -} -``` - -### Balance Format -```javascript -BALANCE_FORMAT = { - JSON: 'json', - CSV: 'csv' -} -``` - -### Gas Fee Type -```javascript -GAS_FEE_TYPE = { - AUTO: 'auto', - FAST: 'fast', - SLOW: 'slow' -} -``` - -## ๐ŸŽฏ Page Detection - -### Supported Pages - -```javascript -BINANCE_PAGES = { - LOGIN: 'login', - ALPHA_SWAP: 'alpha-swap', - ALPHA_ORDER_HISTORY: 'alpha-order-history', - UNKNOWN: 'unknown' -} -``` - -### Page URLs - -- **Login**: `https://accounts.binance.com/login` -- **Alpha Swap**: `https://www.binance.com/alpha/bsc/` -- **Alpha Order History**: `https://www.binance.com/en/my/orders/alpha/orderhistory` - -## ๐Ÿ”„ State Management - -### AppState Structure - -```javascript -{ - // Server configuration - server_mode: 'prod', - server: { label: '๐ŸŒ Prod', url: 'https://baf.thuanle.me' }, - - // Page state - current_page: 'alpha-swap', - is_logged_in: true, - - // Bot status - bot_status: BOT_STATUS.READY_FOR_NEW_TASKS, - - // Task state - current_task: { type: 'login', id: 'task_123', ... }, - task_data: { method: 'qr_code', timeout: 300000, ... }, - current_step: 'navigate_to_login', - step_data: { attempts: 0, start_time: 1234567890 }, - - // UI state - is_loading: false, - error_message: null -} -``` - -### Observer Pattern - -State changes trigger observers: - -- **server_mode** โ†’ Save to storage, reload page -- **bot_status** โ†’ Save to storage, start/stop task polling -- **current_page** โ†’ Continue current step if task active -- **is_logged_in** โ†’ Continue current step if task active - -## ๐Ÿ› ๏ธ Helper Functions - -### TaskRunner Helpers - -```javascript -// Validate enum values -TaskRunner.validateEnum(value, enumObj, defaultValue) - -// Validate and merge task data -TaskRunner.validateTaskData(taskType, taskData) - -// Get current task data -TaskRunner.getCurrentTaskData() - -// Update task data during execution -TaskRunner.updateTaskData(newData) - -// Check current task type -TaskRunner.isCurrentTaskType(TASK_TYPES.LOGIN) - -// Get current task type -TaskRunner.getCurrentTaskType() -``` - -### State Helpers - -```javascript -// Get current state -APP_STATE.getData() - -// Update state (triggers observers) -APP_STATE.update({ bot_status: BOT_STATUS.PERFORMING_TASKS }) - -// Subscribe to state changes -APP_STATE.subscribe('bot_status', callback) - -// Unsubscribe from state changes -APP_STATE.unsubscribe('bot_status', callback) -``` - -## ๐ŸŽฎ Menu Commands - -### Available Commands - -1. **Server**: Switch between local/prod servers -2. **Token**: Configure Bearer token for API access -3. **Bot Status**: Cycle through bot statuses (ready/pause/performing) -4. **Get Tasks**: Manually check for new tasks - -### Usage - -```javascript -// Access via Tampermonkey menu -// Right-click extension icon โ†’ Binance Alpha Farm Agent -``` - -## ๐Ÿ“ก API Endpoints - -### Server Communication - -```javascript -// Ping server with status -BAF.ping(status) - -// Get new task -BAF.getTasks() - -// Submit task result -BAF.submitTaskResult(taskId, result) -``` - -### Status Payload - -```javascript -{ - logged_in: true, - current_page: 'alpha-swap', - bot_status: 'ready-for-new-tasks', - current_task: 'login', - task_data: { method: 'qr_code', ... }, - current_step: 'navigate_to_login', - has_current_task: true -} -``` - -## ๐Ÿ”ง Configuration - -### CONFIG Object - -```javascript -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' } +// 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; } } ``` -## ๐Ÿš€ Getting Started +## ๐ŸŽฏ State Management -1. Install Tampermonkey browser extension -2. Install the user script -3. Configure server token via menu -4. Set bot status to "ready-for-new-tasks" -5. Bot will automatically start polling for tasks +### AppState Structure +```javascript +class AppState { + data = { + // Server configuration + server_mode: 'prod', + server: 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 + }; +} +``` -## ๐Ÿ“ Development Notes +### 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); -### Adding New Task Types +// Notify observers on state change +await APP_STATE.update({ bot_status: BOT_STATUS.PERFORMING_TASKS }); +``` -1. Add to `TASK_TYPES` enum -2. Define task data structure in `TASK_DATA_EXAMPLES` -3. Add validation in `validateTaskData()` -4. Implement execution method in `TaskRunner` -5. Define steps in `STEP_TYPES` +## ๐Ÿงญ 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_mode: 'baf-server-mode', + 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 to `STEP_TYPES` enum -2. Implement step logic in task execution -3. Update step data during execution -4. Handle step transitions +1. **Add Step Type**: +```javascript +const STEP_TYPES = { + NEW_TASK: { + // ... existing steps + NEW_STEP: 'NewStep' + } +}; +``` -### Error Handling +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; + }); + } +}; +``` -- Invalid enum values fallback to defaults -- Task execution errors return bot to ready state -- Network errors trigger retry after interval -- Page navigation errors handled by observers \ No newline at end of file +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 4c2a5fb..2a3e9be 100644 --- a/agent.user.js +++ b/agent.user.js @@ -141,6 +141,7 @@ key_token: 'baf-agent-token', key_server_mode: 'baf-server-mode', key_bot_status: 'baf-bot-status', + key_navigation_state: 'baf-navigation-state', getToken: () => GM_getValue(STORAGE.key_token, ''), setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')), @@ -150,6 +151,23 @@ getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.READY_FOR_NEW_TASKS), setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status), + + // Navigation state management + 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'); + } }; // ====== UTILITY MODULE ====== @@ -168,6 +186,33 @@ }, 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 @@ -445,8 +490,645 @@ 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; + return isOnLoginPage || !isLoggedIn; + }, + + 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) => { + await TL.guardDoubleRun(ctx, async () => { + TL.log('STEP', 'WaitForLogin: Checking login status'); + + const isLoggedIn = await BINANCE.isLoggedIn(); + if (isLoggedIn) { + ctx.step_data.login_success = true; + ctx.step_data.login_time = Date.now(); + ctx.done({ success: true, message: 'Login successful' }); + } else { + const elapsed = Date.now() - ctx.step_data.start_time; + const timeout = ctx.task_data?.timeout || 300000; + + if (elapsed > timeout) { + ctx.done({ success: false, error: 'Login timeout' }); + } + } + }); + } + }; + + // Order History Step Functions + const OrderHistorySteps = { + matchNavigateToOrderHistory: (url, ctx) => { + return url.includes("/my/orders/alpha/orderhistory") || + !url.includes("/my/orders/alpha/orderhistory"); + }, + + runNavigateToOrderHistory: async (ctx) => { + await TL.guardDoubleRun(ctx, async () => { + TL.log('STEP', 'NavigateToOrderHistory: Navigating to order history page'); + + if (!BINANCE.isOnAlphaOrderHistoryPage()) { + BINANCE.navigateToAlphaOrderHistory(); + ctx.step_data.page_navigated = true; + } + + ctx.step_data.page_loaded = true; + }); + }, + + matchExtractOrderData: (url, ctx) => { + return url.includes("/my/orders/alpha/orderhistory") && + ctx?.step_data?.page_loaded; + }, + + runExtractOrderData: async (ctx) => { + await TL.guardDoubleRun(ctx, async () => { + TL.log('STEP', 'ExtractOrderData: Extracting order data'); + + await TL.delay(2000); + + const orders = []; + const orderRows = document.querySelectorAll('.order-row, [data-testid="order-row"]'); + + orderRows.forEach((row, index) => { + if (index < (ctx.task_data?.limit || 100)) { + const order = { + id: row.getAttribute('data-order-id') || `order_${index}`, + type: row.querySelector('.order-type')?.textContent || 'unknown', + status: row.querySelector('.order-status')?.textContent || 'unknown', + amount: row.querySelector('.order-amount')?.textContent || '0', + date: row.querySelector('.order-date')?.textContent || new Date().toISOString() + }; + orders.push(order); + } + }); + + ctx.step_data.orders = orders; + ctx.step_data.extracted = true; + }); + }, + + matchSendOrderData: (url, ctx) => { + return ctx?.step_data?.extracted; + }, + + runSendOrderData: async (ctx) => { + await TL.guardDoubleRun(ctx, async () => { + TL.log('STEP', 'SendOrderData: Sending order data to server'); + + const result = { + success: true, + order_count: ctx.step_data.orders.length, + orders: ctx.step_data.orders + }; + + ctx.done(result); + }); + } + }; + + // Balance Step Functions + const BalanceSteps = { + matchNavigateToBalance: (url, ctx) => { + return url.includes("/alpha/bsc/") || !url.includes("/alpha/bsc/"); + }, + + runNavigateToBalance: async (ctx) => { + await TL.guardDoubleRun(ctx, async () => { + TL.log('STEP', 'NavigateToBalance: Navigating to balance page'); + + if (!BINANCE.isOnAlphaSwapPage()) { + BINANCE.navigateToAlphaSwap(); + ctx.step_data.page_navigated = true; + } + + ctx.step_data.page_loaded = true; + }); + }, + + matchExtractBalanceData: (url, ctx) => { + return url.includes("/alpha/bsc/") && ctx?.step_data?.page_loaded; + }, + + runExtractBalanceData: async (ctx) => { + await TL.guardDoubleRun(ctx, async () => { + TL.log('STEP', 'ExtractBalanceData: Extracting balance data'); + + await TL.delay(2000); + + const balances = {}; + const balanceElements = document.querySelectorAll('.balance-item, [data-testid="balance-item"]'); + + balanceElements.forEach(element => { + const currency = element.querySelector('.currency')?.textContent || 'unknown'; + const amount = element.querySelector('.amount')?.textContent || '0'; + + if (ctx.task_data?.currencies === null || + ctx.task_data?.currencies?.includes(currency)) { + balances[currency] = amount; + } + }); + + ctx.step_data.balances = balances; + ctx.step_data.extracted = true; + }); + }, + + matchSendBalanceData: (url, ctx) => { + return ctx?.step_data?.extracted; + }, + + runSendBalanceData: async (ctx) => { + await TL.guardDoubleRun(ctx, async () => { + TL.log('STEP', 'SendBalanceData: Sending balance data to server'); + + const result = { + success: true, + balance_count: Object.keys(ctx.step_data.balances).length, + balances: ctx.step_data.balances + }; + + ctx.done(result); + }); + } + }; + + // ====== TASK DEFINITIONS ====== + const TASK_DEFINITIONS = { + [TASK_TYPES.LOGIN]: { + id: "login-task", + 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: "get-order-history-task", + steps: [ + { + name: "NavigateToOrderHistory", + match: OrderHistorySteps.matchNavigateToOrderHistory, + run: OrderHistorySteps.runNavigateToOrderHistory + }, + { + name: "ExtractOrderData", + match: OrderHistorySteps.matchExtractOrderData, + run: OrderHistorySteps.runExtractOrderData + }, + { + name: "SendOrderData", + match: OrderHistorySteps.matchSendOrderData, + run: OrderHistorySteps.runSendOrderData + } + ] + }, + + [TASK_TYPES.GET_BALANCE]: { + id: "get-balance-task", + steps: [ + { + name: "NavigateToBalance", + match: BalanceSteps.matchNavigateToBalance, + run: BalanceSteps.runNavigateToBalance + }, + { + name: "ExtractBalanceData", + match: BalanceSteps.matchExtractBalanceData, + run: BalanceSteps.runExtractBalanceData + }, + { + name: "SendBalanceData", + match: BalanceSteps.matchSendBalanceData, + run: BalanceSteps.runSendBalanceData + } + ] + } + }; + + // ====== 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 ready state + await APP_STATE.update({ + current_task: null, + task_data: null, + current_step: null, + step_data: null, + bot_status: BOT_STATUS.READY_FOR_NEW_TASKS, + 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.READY_FOR_NEW_TASKS, + is_loading: false, + error_message: 'Task completion error: ' + error.message + }); + } + } + + // Execute current step + async executeCurrentStep() { + const data = APP_STATE.getData(); + if (!data.current_task || !this.context) { + TL.debug('STEP', 'No current task or context available'); + 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.PERFORMING_TASKS, + 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); @@ -529,18 +1211,11 @@ const task = response.data.task; TL.log('TASK', `Received new task: ${task.type}`, task); - // Validate and merge task data with defaults + // Validate task data const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data); - await APP_STATE.update({ - current_task: task, - task_data: validatedTaskData, - current_step: task.steps?.[0] || null, - bot_status: BOT_STATUS.PERFORMING_TASKS - }); - - // Start executing the task - await this.executeTask(task); + // 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 @@ -553,104 +1228,9 @@ } }, - async executeTask(task) { - TL.log('TASK', `Executing task: ${task.type}`, task); - - try { - await APP_STATE.update({ is_loading: true }); - - let result = null; - - switch (task.type) { - case TASK_TYPES.LOGIN: - result = await this.executeLoginTask(task); - break; - case TASK_TYPES.GET_ORDER_HISTORY: - result = await this.executeGetOrderHistoryTask(task); - break; - case TASK_TYPES.GET_BALANCE: - result = await this.executeGetBalanceTask(task); - break; - case TASK_TYPES.SWAP: - result = await this.executeSwapTask(task); - break; - default: - throw new Error(`Unknown task type: ${task.type}`); - } - - // Submit result to server - await BAF.submitTaskResult(task.id, result); - - // Task completed, return to ready state - await APP_STATE.update({ - current_task: null, - task_data: null, - current_step: null, - step_data: null, - bot_status: BOT_STATUS.READY_FOR_NEW_TASKS, - is_loading: false - }); - - // Check for next task after a short delay - setTimeout(() => this.checkForNewTasks(), 1000); - - } catch (error) { - TL.error('TASK', `Task execution failed:`, error); - await APP_STATE.update({ - error_message: error.message, - bot_status: BOT_STATUS.READY_FOR_NEW_TASKS, - is_loading: false - }); - - // Retry after interval - setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval); - } - }, - - async executeLoginTask(task) { - const taskData = this.getCurrentTaskData(); - TL.log('TASK', 'Executing login task', taskData); - - // Implementation for login task with steps - // taskData contains: { method: 'qr_code' | 'password', credentials: {...}, timeout: 300000, retry_count: 3 } - - // Example: Update task data during execution - await this.updateTaskData({ - start_time: Date.now(), - attempts: 0 - }); - - return { success: true, message: 'Login task completed' }; - }, - - async executeGetOrderHistoryTask(task) { - const taskData = this.getCurrentTaskData(); - TL.log('TASK', 'Executing get order history task', taskData); - - // Implementation for get order history task - // taskData contains: { limit: 100, offset: 0, date_range: {...}, order_types: [...], status: [...] } - - return { success: true, order_history: [] }; - }, - - async executeGetBalanceTask(task) { - const taskData = this.getCurrentTaskData(); - TL.log('TASK', 'Executing get balance task', taskData); - - // Implementation for get balance task - // taskData contains: { currencies: ['USDT', 'BTC'], include_zero: false, include_locked: true, format: 'json' } - - return { success: true, balance: {} }; - }, - - async executeSwapTask(task) { - const taskData = this.getCurrentTaskData(); - TL.log('TASK', 'Executing swap task', taskData); - - // Implementation for swap task - // taskData contains: { from_token: 'USDT', to_token: 'BTC', amount: 100, slippage: 0.5, gas_fee: 'auto', deadline: 300, auto_confirm: true } - - return { success: true, swap_result: {} }; + // Continue current step (called by observers) + async continueCurrentStep() { + await this.stepRunner.executeCurrentStep(); } }; @@ -685,7 +1265,7 @@ TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`); // Handle page-specific logic for current task - if (data.current_task && data.current_step) { + if (data.current_task && data.bot_status === BOT_STATUS.PERFORMING_TASKS) { // Continue with current step based on page change await TaskRunner.continueCurrentStep(); } @@ -695,7 +1275,7 @@ TL.debug('OBSERVER', `Login state changed: ${oldValue} -> ${newValue}`); // Handle login state change for current task - if (data.current_task && data.current_step) { + if (data.current_task && data.bot_status === BOT_STATUS.PERFORMING_TASKS) { await TaskRunner.continueCurrentStep(); } } @@ -831,8 +1411,11 @@ TL.log(`BAF`, res); - // Start initial task checking if ready - if (data.bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) { + // Resume task if performing or start new task checking if ready + if (data.bot_status === BOT_STATUS.PERFORMING_TASKS && data.current_task) { + TL.log('INIT', 'Resuming interrupted task'); + TaskRunner.stepRunner.resumeTask(); + } else if (data.bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) { TaskRunner.checkForNewTasks(); } }