From d5221d1bf4290b9edebc2c2cfc93893dd73dcbc7 Mon Sep 17 00:00:00 2001 From: thuanle Date: Fri, 1 Aug 2025 02:02:36 +0700 Subject: [PATCH] Update agent.user.js to version 2025.08.01, introducing a comprehensive state management system with observers, enhancing task execution capabilities, and adding a README.md for documentation. Improve page detection and navigation for Binance, along with new task and step types for better automation. --- README.md | 473 ++++++++++++++++++++++++++++++++ agent.user.js | 735 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 1116 insertions(+), 92 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..4943fd9 --- /dev/null +++ b/README.md @@ -0,0 +1,473 @@ +# Binance Alpha Farm Agent + +Automated trading agent for Binance Alpha Farm with Task-Step architecture. + +## ๐Ÿ—๏ธ System Architecture + +### Core 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 + +## ๐Ÿ“‹ Task-Step System + +### Task Types + +```javascript +TASK_TYPES = { + LOGIN: 'login', + GET_ORDER_HISTORY: 'get_order_history', + GET_BALANCE: 'get_balance', + SWAP: 'swap', + NO_TASK: 'no_task' +} +``` + +### Step Types + +```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' +} +``` + +## ๐Ÿค– 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 + ] +} +``` + +### Get Order History Task + +```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' } + } +} +``` + +## ๐Ÿš€ Getting Started + +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 + +## ๐Ÿ“ Development Notes + +### Adding New Task Types + +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` + +### 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 + +### Error Handling + +- 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 diff --git a/agent.user.js b/agent.user.js index 7839b98..4c2a5fb 100644 --- a/agent.user.js +++ b/agent.user.js @@ -1,10 +1,11 @@ // ==UserScript== // @name Binance Alpha Farm Agent // @namespace http://baf.thuanle.me -// @version 2025.07.31 +// @version 2025.08.01 // @author TL // @description Automated trading agent for Binance Alpha Farm // @match https://www.binance.com/* +// @match https://accounts.binance.com/* // @run-at document-idle // @grant GM_setValue // @grant GM_getValue @@ -21,35 +22,137 @@ (async () => { 'use strict'; - // ====== Servers ====== - const BAF_SERVERS = { - local: { label: '๐Ÿ  Local', url: 'http://localhost:3000' }, - prod: { label: '๐ŸŒ Prod', url: 'https://baf.thuanle.me' }, - }; - - + // ====== CONFIGURATION ====== const CONFIG = { heartbeat_interval: 10000, - is_debug: false, + task_poll_interval: 10000, + is_debug: true, + servers: { + local: { label: '๐Ÿ  Local', url: 'http://localhost:3000' }, + prod: { label: '๐ŸŒ Prod', url: 'https://baf.thuanle.me' }, + } + }; + + // ====== STATE MANAGEMENT ====== + class AppState { + constructor() { + this.data = { + // Server configuration + server_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 + }; + + this.observers = new Map(); + this.initialized = false; + } + + // Initialize state from storage and page detection + async initialize() { + if (this.initialized) return; + + // Load from storage + this.data.server_mode = await STORAGE.getServerMode(); + this.data.server = CONFIG.servers[this.data.server_mode] || CONFIG.servers.prod; + this.data.bot_status = await STORAGE.getBotStatus(); + + // Detect current page and login state + this.data.current_page = BINANCE.detectPage(); + this.data.is_logged_in = await BINANCE.isLoggedIn(); + + this.initialized = true; + TL.debug('STATE', 'Initialized', this.data); + } + + // Get current state + getData() { + return { ...this.data }; + } + + // Update state with partial update + async update(updates) { + const oldData = { ...this.data }; + + // Apply updates + Object.assign(this.data, updates); + + // Notify observers + await this.notifyObservers(oldData, this.data); + + TL.debug('STATE', 'Updated', updates); + } + + // Subscribe to state changes + subscribe(key, callback) { + if (!this.observers.has(key)) { + this.observers.set(key, []); + } + this.observers.get(key).push(callback); + } + + // Unsubscribe from state changes + unsubscribe(key, callback) { + const callbacks = this.observers.get(key); + if (callbacks) { + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + } + } + + // Notify all observers + async notifyObservers(oldData, newData) { + for (const [key, callbacks] of this.observers) { + const oldValue = oldData[key]; + const newValue = newData[key]; + + if (oldValue !== newValue) { + for (const callback of callbacks) { + try { + await callback(oldValue, newValue, newData); + } catch (error) { + TL.error('STATE', `Observer error for ${key}:`, error); + } + } + } + } + } } - - - // ====== Storage ====== + // ====== STORAGE MODULE ====== const STORAGE = { key_token: 'baf-agent-token', + key_server_mode: 'baf-server-mode', + key_bot_status: 'baf-bot-status', + getToken: () => GM_getValue(STORAGE.key_token, ''), setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')), - key_server_mode: 'baf-server-mode', // 'local' | 'prod' getServerMode: () => GM_getValue(STORAGE.key_server_mode, 'prod'), setServerMode: (mode) => GM_setValue(STORAGE.key_server_mode, mode), + + getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.READY_FOR_NEW_TASKS), + setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status), }; - - - - // ====== Utility ====== + // ====== UTILITY MODULE ====== const TL = { debug: (tag, msg, ...args) => CONFIG.is_debug && GM_log(`[TL] [${tag}]\n${msg}`, ...args), log: (tag, msg, ...args) => GM_log(`[TL] [${tag}]\n${msg}`, ...args), @@ -60,16 +163,14 @@ GM_notification({ title, text, timeout }); return true; } - - alert(`${title}\n${text}`); // fallback + alert(`${title}\n${text}`); return false; }, delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)), }; - - // ====== DOM helpers ====== + // DOM helpers TL.dom = { isVisible: (el) => { if (!el) return false; @@ -102,7 +203,7 @@ }, }; - // ====== Network helpers (GM_xmlhttpRequest) ====== + // Network helpers TL.net = { gmRequest(url, init = {}) { const headersToObject = (h) => { @@ -137,21 +238,18 @@ } }; - // ====== BAF API ====== + // ====== BAF API MODULE ====== const BAF = { - // Trแบฃ vแป object {label, url} theo mode hiแป‡n tแบกi getServer: async () => { - const mode = await STORAGE.getServerMode(); - return BAF_SERVERS[mode] || BAF_SERVERS.prod; + const data = APP_STATE.getData(); + return CONFIG.servers[data.server_mode] || CONFIG.servers.prod; }, - // Trแบฃ vแป URL host hiแป‡n tแบกi getHost: async () => { const s = await BAF.getServer(); return s.url; }, - // Wrapper GMXHR request: async (method, path, { params, body, headers } = {}) => { const base = await BAF.getHost(); const url = new URL(path, base); @@ -175,62 +273,58 @@ post: (path, body, init = {}) => BAF.request('POST', path, { body, headers: init.headers }), ping: (status) => BAF.post('/agent/ping', status), + + getTasks: () => BAF.get('/agent/tasks'), + submitTaskResult: (taskId, result) => BAF.post(`/agent/tasks/${taskId}/result`, result), }; - // ====== Cแบฅu hรฌnh menu ====== - async function createGM_Menu() { - const curSrv = await BAF.getServer(); - GM_registerMenuCommand(`Server: ${curSrv.label} (${curSrv.url})`, async () => { - try { - const cur = await STORAGE.getServerMode(); - const next = (cur === 'local') ? 'prod' : 'local'; - await STORAGE.setServerMode(next); - const nsv = await BAF.getServer(); - const msg = `Switched to ${nsv.label} (${nsv.url})`; - TL.debug(`BAF`,msg); - TL.noti('BAF Server Switched', msg); + // ====== BINANCE MODULE ====== + const BINANCE_PAGES = { + LOGIN: 'login', + ALPHA_SWAP: 'alpha-swap', + ALPHA_ORDER_HISTORY: 'alpha-order-history', + UNKNOWN: 'unknown' + }; - await TL.delay(300); - location.reload(); - } 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); - } - }); - - } - - // ====== BINANCE (isLoggedIn) ====== const BINANCE = { + detectPage: () => { + const pathname = window.location.pathname; + const hostname = window.location.hostname; + + if (hostname === 'accounts.binance.com') { + if (pathname.includes('/login')) { + return BINANCE_PAGES.LOGIN; + } + } + + if (hostname === 'www.binance.com') { + if (pathname.includes('/alpha/bsc/')) { + return BINANCE_PAGES.ALPHA_SWAP; + } + if (pathname.includes('/my/orders/alpha/orderhistory')) { + return BINANCE_PAGES.ALPHA_ORDER_HISTORY; + } + } + + return BINANCE_PAGES.UNKNOWN; + }, + + isOnLoginPage: () => BINANCE.detectPage() === BINANCE_PAGES.LOGIN, + isOnAlphaSwapPage: () => BINANCE.detectPage() === BINANCE_PAGES.ALPHA_SWAP, + isOnAlphaOrderHistoryPage: () => BINANCE.detectPage() === BINANCE_PAGES.ALPHA_ORDER_HISTORY, + detectLoginState: () => { + if (BINANCE.isOnLoginPage()) { + return false; + } + const loginBtn = document.querySelector('#toLoginPage'); const regBtn = document.querySelector('#toRegisterPage'); - if (TL.dom.isVisible?.(loginBtn) || TL.ui.isVisible?.(regBtn)) return false; + if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) return false; if (!loginBtn && !regBtn) return true; - return null; // ฤ‘ang load hoแบทc chฦฐa rรต + return null; }, isLoggedIn: async (timeoutMs = 6000, pollMs = 200) => { @@ -249,32 +343,440 @@ TL.debug(`BINANCE`,`isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`); return fallback; }, + + navigateToLogin: () => { + const loginBtn = document.querySelector('#toLoginPage'); + if (TL.dom.isVisible?.(loginBtn)) { + TL.dom.click(loginBtn); + } + }, + + navigateToAlphaSwap: () => { + if (!BINANCE.isOnAlphaSwapPage()) { + window.location.href = 'https://www.binance.com/alpha/bsc/'; + } + }, + + navigateToAlphaOrderHistory: () => { + if (!BINANCE.isOnAlphaOrderHistoryPage()) { + window.location.href = 'https://www.binance.com/en/my/orders/alpha/orderhistory'; + } + }, }; + // ====== 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' + }; - // ====== Main ====== - async function welcome_message() { - const s = await BAF.getServer(); - const res = await BAF.ping(); + // Bot Status Enum + const BOT_STATUS = { + READY_FOR_NEW_TASKS: 'ready-for-new-tasks', + PAUSE_AUTOMATION: 'pause-automation', + PERFORMING_TASKS: 'performing-tasks' + }; - const resStr = - `Server: ${s.label} (${s.url})\n` + - `Status: ${res.ok ? 'Connected โœ…' : 'Failed โŒ'} (${res.status})\n`; + // Login Method Enum + const LOGIN_METHOD = { + QR_CODE: 'qr_code', + PASSWORD: 'password', + EMAIL: 'email' + }; - TL.log(`BAF`,resStr); + // 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' + }; + + // ====== TASK RUNNER ====== + const TaskRunner = { + // 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() { + try { + const data = APP_STATE.getData(); + if (data.bot_status !== BOT_STATUS.READY_FOR_NEW_TASKS) { + TL.debug('TASK', 'Bot not ready for new tasks, status:', data.bot_status); + return; + } + + const response = await BAF.getTasks(); + if (response.ok && response.data.task) { + const task = response.data.task; + TL.log('TASK', `Received new task: ${task.type}`, task); + + // Validate and merge task data with defaults + 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); + } 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); + } + }, + + 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: {} }; + } + }; + + // ====== STATE OBSERVERS ====== + const StateWatchers = { + // Storage observer - saves state changes to storage + async onServerModeChange(oldValue, newValue, data) { + TL.debug('OBSERVER', `Server mode changed: ${oldValue} -> ${newValue}`); + await STORAGE.setServerMode(newValue); + + // Update server info + await APP_STATE.update({ + server: 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.READY_FOR_NEW_TASKS) { + // Start checking for new tasks + TaskRunner.checkForNewTasks(); + } + }, + + async onCurrentPageChange(oldValue, newValue, data) { + TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`); + + // Handle page-specific logic for current task + if (data.current_task && data.current_step) { + // Continue with current step based on page change + await TaskRunner.continueCurrentStep(); + } + }, + + async onLoginStateChange(oldValue, newValue, data) { + TL.debug('OBSERVER', `Login state changed: ${oldValue} -> ${newValue}`); + + // Handle login state change for current task + if (data.current_task && data.current_step) { + await TaskRunner.continueCurrentStep(); + } + } + }; + + // ====== MENU SYSTEM ====== + async function createGM_Menu() { + const data = APP_STATE.getData(); + const curSrv = data.server; + + GM_registerMenuCommand(`Server: ${curSrv.label} (${curSrv.url})`, async () => { + try { + const next = (data.server_mode === 'local') ? 'prod' : 'local'; + await APP_STATE.update({ server_mode: 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); + } + }); + + GM_registerMenuCommand('Bot Status', async () => { + const data = APP_STATE.getData(); + const statusOptions = [ + BOT_STATUS.READY_FOR_NEW_TASKS, + BOT_STATUS.PAUSE_AUTOMATION, + BOT_STATUS.PERFORMING_TASKS + ]; + const currentIndex = statusOptions.indexOf(data.bot_status); + const nextIndex = (currentIndex + 1) % statusOptions.length; + const nextStatus = statusOptions[nextIndex]; + + await APP_STATE.update({ bot_status: nextStatus }); + TL.noti('Bot Status', `Changed to: ${nextStatus}`); + }); + + GM_registerMenuCommand('Get Tasks', async () => { + await TaskRunner.checkForNewTasks(); + }); } - // ====== Heartbeat ====== + // ====== HEARTBEAT ====== async function heartbeat_report() { try { - const isLoggedIn = await BINANCE.isLoggedIn(); - + const data = APP_STATE.getData(); const status = { - logged_in: isLoggedIn + 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, + has_current_task: !!data.current_task }; - // Log heartbeat - TL.debug(`HEARTBEAT`,`${JSON.stringify(status, null, 2)}`); + TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`); await BAF.ping(status); return status; } catch (e) { @@ -283,9 +785,58 @@ } } - // ====== KhแปŸi tแบกo ====== - await welcome_message(); - await createGM_Menu(); - setInterval(heartbeat_report, CONFIG.heartbeat_interval); + // ====== PAGE MONITOR ====== + async function monitorPageChanges() { + let lastPage = APP_STATE.getData().current_page; + + setInterval(async () => { + const currentPage = BINANCE.detectPage(); + if (currentPage !== lastPage) { + await APP_STATE.update({ current_page: currentPage }); + lastPage = currentPage; + } + }, 1000); + } + // ====== INITIALIZATION ====== + const APP_STATE = new AppState(); + + async function initialize() { + // Initialize state + await APP_STATE.initialize(); + + // Register observers + APP_STATE.subscribe('server_mode', StateWatchers.onServerModeChange); + APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange); + APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange); + APP_STATE.subscribe('is_logged_in', StateWatchers.onLoginStateChange); + + // Create menu + await createGM_Menu(); + + // Start monitoring + monitorPageChanges(); + + // Start heartbeat + setInterval(heartbeat_report, CONFIG.heartbeat_interval); + + // Welcome message + const ping_res = await BAF.ping(); + const data = APP_STATE.getData(); + const res = + `Server: ${data.server.label} (${data.server.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); + + // Start initial task checking if ready + if (data.bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) { + TaskRunner.checkForNewTasks(); + } + } + + // Start the application + await initialize(); })(); \ No newline at end of file