1398 lines
62 KiB
JavaScript
1398 lines
62 KiB
JavaScript
// ==UserScript==
|
||
// @name Binance Alpha Farm Agent
|
||
// @namespace http://baf.thuanle.me
|
||
// @version 2025.08.01
|
||
// @author TL
|
||
// @description Automated trading agent for Binance Alpha Farm
|
||
// @match https://www.binance.com/*
|
||
// @match https://accounts.binance.com/*
|
||
// @run-at document-idle
|
||
// @grant GM_setValue
|
||
// @grant GM_getValue
|
||
// @grant GM_registerMenuCommand
|
||
// @grant GM_notification
|
||
// @grant GM_xmlhttpRequest
|
||
// @grant GM_log
|
||
// @connect baf.thuanle.me
|
||
// @connect localhost
|
||
// @downloadURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
|
||
// @updateURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
|
||
// ==/UserScript==
|
||
|
||
if (window.top !== window) {
|
||
GM_log(`[TL] ❌ Skipping in iframe ${window.location.href}`);
|
||
return;
|
||
}
|
||
GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.');
|
||
|
||
(async () => {
|
||
'use strict';
|
||
|
||
const uuid = () => ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
|
||
|
||
const SESSION_STORAGE_KEYS = {
|
||
SESSION_ID: 'aRah9OhHeijee6sho3baequu9phoovah',
|
||
SESSION_BINDED: 'Oos2uoth2thae8Ir8iakahj1OohaMahb',
|
||
SESSION_TOKEN: 'ThiegiecohViuZ1Iecio7gahphiechub',
|
||
SESSION_INITIALIZED: 'iR4choo2Ieg2ew0ogheeyaer8Aisheet',
|
||
BOT_STATUS: 'wiethie3boGhoh3iegh3ohnezei2tauj',
|
||
CURRENT_TASK: 'Reebo1eitahh2aotumai5jae1neetoh3',
|
||
CURRENT_TASK_DATA: 'cheishailoh5keePoo6oe2Quie1gaxah',
|
||
CURRENT_STEP: 'eDusaidu2hooweiMoonahng3fua7aoso',
|
||
TIMER_START_TIME: 'einguh3eshuiqua7Ux7thaNgai5Axahy',
|
||
TIMER_IS_RUNNING: 'iengoose3vie2aikePhooCaPheeceizu',
|
||
TIMER_ACCUMULATED: 'aeMaiwae3PohB5Soshaech8eegh5quoo',
|
||
}
|
||
|
||
if (!sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_ID)) {
|
||
sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_ID, uuid());
|
||
}
|
||
|
||
// ====== CONFIGURATION ======
|
||
const CONFIG = {
|
||
HEARTBEAT_INTERVAL: 10_000, // Send heartbeat every 10 seconds
|
||
DASHBOARD_UPDATE_INTERVAL: 500, // Update dashboard overlay every 0.5 seconds
|
||
TASK_INTERVAL: 10_000, // Check for new task every 10 seconds
|
||
REQUEST_TIMEOUT: 10_000, // Request timeout in milliseconds (10 seconds)
|
||
DEBUG_MILESTONE: true, // Show milestone debug logs
|
||
SERVERS: {
|
||
local: { label: '🏠 Local', url: 'http://localhost:3000' },
|
||
prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' },
|
||
},
|
||
};
|
||
|
||
const AppSession = {
|
||
SESSION_ID: sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_ID),
|
||
|
||
getSessionBinded: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_BINDED) === 'true',
|
||
setSessionBinded: binded => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_BINDED, binded ? 'true' : 'false'),
|
||
|
||
getSessionToken: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_TOKEN),
|
||
setSessionToken: token => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_TOKEN, token),
|
||
|
||
getSessionStatus: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.BOT_STATUS),
|
||
setSessionStatus: status => sessionStorage.setItem(SESSION_STORAGE_KEYS.BOT_STATUS, status),
|
||
|
||
getSessionInitialized: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_INITIALIZED) === 'true',
|
||
setSessionInitialized: initialized => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_INITIALIZED, initialized ? 'true' : 'false'),
|
||
|
||
// Task state - Trạng thái task hiện tại (lưu trong sessionStorage)
|
||
getCurrentTask: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_TASK),
|
||
setCurrentTask: task => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_TASK, task),
|
||
|
||
getCurrentTaskData: () => {
|
||
const data = sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_TASK_DATA);
|
||
return data ? JSON.parse(data) : null;
|
||
},
|
||
setCurrentTaskData: data => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_TASK_DATA, JSON.stringify(data)),
|
||
|
||
getCurrentStep: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_STEP),
|
||
setCurrentStep: step => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_STEP, step),
|
||
|
||
initialize: async () => {
|
||
try {
|
||
const isInitialized = AppSession.getSessionInitialized();
|
||
|
||
if (!isInitialized) {
|
||
AppSession.setSessionStatus(AppEnums.BOT_STATUS.INITIALIZING);
|
||
AppSession.setCurrentTask(BAF_TASKS.NO_TASK);
|
||
AppSession.setCurrentStep(BAF_TASKS.NO_STEP);
|
||
AppSession.setSessionInitialized(true);
|
||
} else {
|
||
}
|
||
} catch (error) {
|
||
TL.error('APP-SESSION', '1.2.4. Error during session initialization:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
}
|
||
|
||
// ====== APP ENUMS ======
|
||
const AppEnums = {
|
||
BOT_STATUS: {
|
||
INITIALIZING: 'initializing',
|
||
STOP: 'stop',
|
||
PAUSE: 'pause',
|
||
WAITING_FOR_TASK: 'waiting_for_task',
|
||
PERFORMING_TASK: 'performing_task',
|
||
ERROR: 'error',
|
||
},
|
||
BOT_STARTUP_MODE: {
|
||
AUTO: 'auto',
|
||
MANUAL: 'manual',
|
||
}
|
||
};
|
||
|
||
// ====== STATE MANAGEMENT ======
|
||
/**
|
||
* AppStateClass - Quản lý state của ứng dụng
|
||
* Lưu trữ runtime state (không persistent), persistent data được lưu trong AppSettings
|
||
*/
|
||
class AppStateClass {
|
||
constructor() {
|
||
this.data = {
|
||
// Server configuration - Thông tin server hiện tại
|
||
server_last_seen: null, // Thời gian cuối cùng ping server thành công
|
||
server_latency: null, // Thời gian ping server (ms)
|
||
|
||
// Page state - Trạng thái trang hiện tại
|
||
current_page: null, // Trang hiện tại (detected từ URL)
|
||
is_logged_in: false, // Trạng thái đăng nhập Binance
|
||
|
||
// Session flags - Flags cho session hiện tại (reset khi reload)
|
||
menuCreated: false, // Menu đã được tạo
|
||
popupInitialized: false, // Popup đã được khởi tạo
|
||
};
|
||
|
||
this.observers = new Map(); // Observer pattern cho state changes
|
||
this.initialized = false; // Flag khởi tạo
|
||
}
|
||
|
||
/**
|
||
* Khởi tạo state từ AppSettings và detect trang hiện tại
|
||
* Chỉ chạy một lần khi app khởi động
|
||
*/
|
||
async initialize() {
|
||
if (this.initialized) return;
|
||
this.initialized = true;
|
||
|
||
// Detect trang hiện tại (không block)
|
||
const currentPage = BINANCE.page.detectPage();
|
||
|
||
// Update state (không bao gồm login status)
|
||
await this.update({
|
||
current_page: currentPage,
|
||
});
|
||
|
||
TL.debug('APP-STATE', 'AppState initialized');
|
||
}
|
||
|
||
/**
|
||
* Lấy copy của data hiện tại
|
||
* @returns {Object} Copy của state data
|
||
*/
|
||
getData() {
|
||
return { ...this.data };
|
||
}
|
||
|
||
// ====== GETTER METHODS ======
|
||
getServer() { return CONFIG.SERVERS[AppSettings.getServerType()]; }
|
||
getServerLastSeen() { return this.data.server_last_seen; }
|
||
getServerLatency() { return this.data.server_latency; }
|
||
|
||
/**
|
||
* Kiểm tra xem server có kết nối không
|
||
* @returns {boolean} true nếu server kết nối trong 1 phút, false nếu không
|
||
*/
|
||
getServerConnected() {
|
||
return this.data.server_last_seen && Date.now() - this.data.server_last_seen < 60000;
|
||
}
|
||
|
||
getCurrentPage() { return this.data.current_page; }
|
||
getIsLoggedIn() { return this.data.is_logged_in; }
|
||
getMenuCreated() { return this.data.menuCreated; }
|
||
getPopupInitialized() { return this.data.popupInitialized; }
|
||
|
||
/**
|
||
* Cập nhật state với partial update
|
||
* @param {Object} updates - Object chứa cặp key-value cần cập nhật vào state
|
||
* Ví dụ: { current_task: task, is_loading: true }
|
||
*/
|
||
async update(updates) {
|
||
const oldData = { ...this.data };
|
||
Object.assign(this.data, updates);
|
||
await this.notifyObservers(oldData, this.data);
|
||
// TL.debug('STATE', 'Updated', updates);
|
||
}
|
||
|
||
/**
|
||
* Đăng ký observer cho state change
|
||
* @param {string} key - Key của state cần observe
|
||
* @param {Function} callback - Callback function khi state thay đổi
|
||
*/
|
||
subscribe(key, callback) {
|
||
if (!this.observers.has(key)) {
|
||
this.observers.set(key, []);
|
||
}
|
||
this.observers.get(key).push(callback);
|
||
}
|
||
|
||
/**
|
||
* Hủy đăng ký observer
|
||
* @param {string} key - Key của state
|
||
* @param {Function} callback - Callback function cần hủy
|
||
*/
|
||
unsubscribe(key, callback) {
|
||
const callbacks = this.observers.get(key);
|
||
if (callbacks) {
|
||
const index = callbacks.indexOf(callback);
|
||
if (index > -1) {
|
||
callbacks.splice(index, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Thông báo tất cả observers khi state thay đổi
|
||
* @param {Object} oldData - State cũ
|
||
* @param {Object} newData - State mới
|
||
*/
|
||
async notifyObservers(oldData, newData) {
|
||
for (const [key, callbacks] of this.observers) {
|
||
const oldValue = oldData[key];
|
||
const newValue = newData[key];
|
||
|
||
if (oldValue !== newValue) {
|
||
for (const callback of callbacks) {
|
||
try {
|
||
await callback(oldValue, newValue, newData);
|
||
} catch (error) {
|
||
TL.error('STATE', `Observer error for ${key}:`, error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
const AppState = new AppStateClass();
|
||
|
||
|
||
// ====== APP SETTINGS ======
|
||
/**
|
||
* AppSettings - Quản lý persistent settings của ứng dụng
|
||
* Lưu trữ cài đặt người dùng trong GM storage (vĩnh viễn)
|
||
*/
|
||
const AppSettings = {
|
||
key_agent_token: 'baf-agent-token',
|
||
key_server_type: 'baf-server-type',
|
||
key_bot_startup_mode: 'baf-bot-startup-mode',
|
||
key_popup_position: 'baf-popup-position',
|
||
key_popup_visible: 'baf-popup-visible',
|
||
key_debug: 'baf-debug',
|
||
key_safety_guard: 'baf-safety-guard',
|
||
|
||
getAgentToken: () => GM_getValue(AppSettings.key_agent_token, ''),
|
||
setAgentToken: token => GM_setValue(AppSettings.key_agent_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
|
||
|
||
// Server configuration
|
||
getServerType: () => GM_getValue(AppSettings.key_server_type, 'prod'),
|
||
setServerType: (type) => {
|
||
GM_setValue(AppSettings.key_server_type, type);
|
||
AppState.update({ server_last_seen: null });
|
||
},
|
||
|
||
getBotStartupMode: () => GM_getValue(AppSettings.key_bot_startup_mode, AppEnums.BOT_STARTUP_MODE.MANUAL),
|
||
setBotStartupMode: (mode) => GM_setValue(AppSettings.key_bot_startup_mode, mode),
|
||
|
||
// Popup configuration
|
||
getPopupPosition: () => GM_getValue(AppSettings.key_popup_position, 4),
|
||
setPopupPosition: (position) => GM_setValue(AppSettings.key_popup_position, position),
|
||
|
||
getPopupVisible: () => GM_getValue(AppSettings.key_popup_visible, true),
|
||
setPopupVisible: (visible) => GM_setValue(AppSettings.key_popup_visible, visible),
|
||
|
||
// Debug mode
|
||
getDebug: () => GM_getValue(AppSettings.key_debug, true),
|
||
setDebug: (debug) => GM_setValue(AppSettings.key_debug, debug),
|
||
|
||
// Safety guard
|
||
getSafetyGuard: () => GM_getValue(AppSettings.key_safety_guard, true),
|
||
setSafetyGuard: (safetyGuard) => {
|
||
GM_setValue(AppSettings.key_safety_guard, safetyGuard);
|
||
GM_log(`[TL] Safety guard set to: ${safetyGuard}`);
|
||
},
|
||
};
|
||
|
||
// ====== COMMON UTILITIES ======
|
||
/**
|
||
* TL - Common utilities cho logging, notifications, và DOM helpers
|
||
*/
|
||
const TL = {
|
||
debug: (tag, msg, ...args) => AppSettings.getDebug() && GM_log(`[TL] [DEBUG] [${new Date().toLocaleString()}] [${tag}] \n${msg}`, ...args),
|
||
milestone: (tag, msg, ...args) => CONFIG.DEBUG_MILESTONE && GM_log(`[TL] [MILESTONE] [${new Date().toLocaleString()}] [${tag}] 🎯\n${msg}`, ...args),
|
||
log: (tag, msg, ...args) => GM_log(`[TL] [LOG] [${new Date().toLocaleString()}] [${tag}] \n${msg}`, ...args),
|
||
warn: (tag, msg, ...args) => GM_log(`[TL] [WARN] [${new Date().toLocaleString()}] [${tag}] ⚠️\n${msg}`, ...args),
|
||
error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${new Date().toLocaleString()}] [${tag}] ❌\n${msg}`, ...args),
|
||
|
||
noti: (title, text, timeout = 2500) => {
|
||
if (typeof GM_notification === 'function') {
|
||
GM_notification({ title, text, timeout });
|
||
return true;
|
||
}
|
||
alert(`${title}\n${text}`);
|
||
return false;
|
||
},
|
||
|
||
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
|
||
|
||
// DOM helpers
|
||
dom: {
|
||
isVisible: (ele) => {
|
||
if (!ele) return false;
|
||
const cs = getComputedStyle(ele);
|
||
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
|
||
return (ele.offsetWidth + ele.offsetHeight) > 0;
|
||
},
|
||
|
||
isDisabled: (ele) => {
|
||
return ele?.disabled === true || ele?.getAttribute('aria-disabled') === 'true';
|
||
},
|
||
|
||
isInViewport: (ele) => {
|
||
if (!ele) return false;
|
||
const rect = ele.getBoundingClientRect();
|
||
return (
|
||
rect.top >= 0 &&
|
||
rect.left >= 0 &&
|
||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||
);
|
||
},
|
||
|
||
click: (ele) => {
|
||
if (ele && typeof ele.click === 'function') ele.click();
|
||
},
|
||
|
||
scrollToView: (ele, behavior = 'smooth') => {
|
||
ele?.scrollIntoView?.({ behavior, block: 'center' });
|
||
},
|
||
|
||
setHover: (ele, eventType, styles) => {
|
||
if (!ele) return;
|
||
|
||
const applyStyles = () => {
|
||
if (styles.scale) ele.style.transform = `scale(${styles.scale})`;
|
||
if (styles.transform) ele.style.transform = styles.transform;
|
||
if (styles.background) ele.style.background = styles.background;
|
||
if (styles.color) ele.style.color = styles.color;
|
||
if (styles.border) ele.style.border = styles.border;
|
||
};
|
||
|
||
ele.addEventListener(eventType, applyStyles);
|
||
},
|
||
},
|
||
|
||
// Browser helpers
|
||
browser: {
|
||
/**
|
||
* Navigate to URL với option forceReload
|
||
* @param {string} url - URL cần navigate
|
||
*/
|
||
navigateTo: (url) => {
|
||
window.location.href = url;
|
||
}
|
||
},
|
||
|
||
// Network helpers
|
||
net: {
|
||
gmRequest(url, init = {}) {
|
||
const headersToObject = (h) => {
|
||
const o = {};
|
||
(h || new Headers()).forEach((v, k) => { o[k] = v; });
|
||
return o;
|
||
};
|
||
return new Promise((resolve, reject) => {
|
||
GM_xmlhttpRequest({
|
||
url,
|
||
method: (init.method || 'GET').toUpperCase(),
|
||
headers: headersToObject(init.headers),
|
||
data: init.body,
|
||
timeout: CONFIG.REQUEST_TIMEOUT,
|
||
onload: (resp) => {
|
||
const text = resp.responseText || '';
|
||
let data = text;
|
||
const isJSON = /content-type:\s*application\/json/i.test(resp.responseHeaders || '');
|
||
if (isJSON) { try { data = JSON.parse(text); } catch { } }
|
||
TL.debug(`net`, `${init.method} ${resp.status} ${url}\n${init.body}`, data);
|
||
resolve({
|
||
status: resp.status,
|
||
ok: resp.status >= 200 && resp.status < 300,
|
||
data,
|
||
rawText: text,
|
||
headers: null
|
||
});
|
||
},
|
||
onerror: (error) => {
|
||
TL.error('net', `Request failed: ${url}`, error);
|
||
resolve({
|
||
status: 0,
|
||
ok: false,
|
||
data: null,
|
||
rawText: '',
|
||
headers: null,
|
||
error: 'Network error'
|
||
});
|
||
},
|
||
ontimeout: () => {
|
||
TL.error('net', `Request timeout after ${CONFIG.REQUEST_TIMEOUT}ms: ${url}`);
|
||
resolve({
|
||
status: 0,
|
||
ok: false,
|
||
data: null,
|
||
rawText: '',
|
||
headers: null,
|
||
error: 'Request timeout'
|
||
});
|
||
},
|
||
});
|
||
});
|
||
},
|
||
}
|
||
};
|
||
|
||
// ====== BAF API MODULE ======
|
||
|
||
|
||
const BAF_TASKS = {
|
||
NO_TASK: 'no_task',
|
||
LOGIN: 'login',
|
||
WALLET_SPOT: 'wallet_spot',
|
||
WALLET_FUNDING: 'wallet_funding',
|
||
WALLET_EARN: 'wallet_earn',
|
||
ALPHA_SWAP: 'alpha_swap',
|
||
ALPHA_ORDER_HISTORY: 'alpha_order_history',
|
||
NO_STEP: 'no_step',
|
||
};
|
||
|
||
|
||
|
||
const BAF = {
|
||
_getHost: () => AppState.getServer()?.url,
|
||
|
||
_request: async (method, path, { params, body, headers, token } = {}) => {
|
||
const base = BAF._getHost();
|
||
const url = new URL(path, base);
|
||
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.append(k, v);
|
||
|
||
const h = new Headers(headers || {});
|
||
if (token && !h.has('Authorization')) h.set('Authorization', `Bearer ${token}`);
|
||
|
||
const init = { method, headers: h };
|
||
if (body !== undefined && body !== null) {
|
||
if (!(body instanceof FormData) && !h.has('Content-Type')) h.set('Content-Type', 'application/json');
|
||
init.body = (h.get('Content-Type') || '').includes('application/json') && typeof body !== 'string'
|
||
? JSON.stringify(body)
|
||
: body;
|
||
}
|
||
return TL.net.gmRequest(url.toString(), init);
|
||
},
|
||
|
||
_agentGet: (path, params, init = {}) => BAF._request('GET', path, { params, headers: init.headers, token: AppSettings.getAgentToken() }),
|
||
_agentPost: (path, body, init = {}) => BAF._request('POST', path, { body, headers: init.headers, token: AppSettings.getAgentToken() }),
|
||
|
||
_sessionGet: (path, params, init = {}) => BAF._request('GET', path, { params, headers: init.headers, token: AppSession.getSessionToken() }),
|
||
_sessionPost: (path, body, init = {}) => BAF._request('POST', path, { body, headers: init.headers, token: AppSession.getSessionToken() }),
|
||
|
||
register: (data) => BAF._agentPost('/agent/register', data),
|
||
|
||
ping: (data) => BAF._sessionPost('/session/ping', data),
|
||
getTask: (data) => BAF._sessionPost('/session/task', data),
|
||
};
|
||
|
||
// ====== BINANCE MODULE ======
|
||
|
||
// Binance Pages constants
|
||
const BINANCE_PAGES = {
|
||
LOGIN: 'login',
|
||
ALPHA_SWAP: 'alpha-swap',
|
||
ALPHA_ORDER_HISTORY: 'alpha-order-history',
|
||
WALLET_EARN: 'wallet-earn',
|
||
WALLET_FUNDING: 'wallet-funding',
|
||
WALLET_SPOT: 'wallet-spot',
|
||
IGNORE: 'ignore',
|
||
UNKNOWN: 'unknown'
|
||
};
|
||
|
||
// Page patterns for detection
|
||
const BINANCE_PAGE_PATTERNS = [
|
||
{
|
||
host: 'accounts.binance.com',
|
||
patterns: [
|
||
{ includes: '/login', page: BINANCE_PAGES.LOGIN }
|
||
]
|
||
},
|
||
{
|
||
host: 'www.binance.com',
|
||
patterns: [
|
||
{ includes: '/alpha/bsc/', page: BINANCE_PAGES.ALPHA_SWAP },
|
||
{ includes: '/my/orders/alpha/orderhistory', page: BINANCE_PAGES.ALPHA_ORDER_HISTORY },
|
||
{ includes: '/my/wallet/account/earn', page: BINANCE_PAGES.WALLET_EARN },
|
||
{ includes: '/my/wallet/funding', page: BINANCE_PAGES.WALLET_FUNDING },
|
||
{ includes: '/my/wallet/account/main', page: BINANCE_PAGES.WALLET_SPOT }
|
||
]
|
||
}
|
||
];
|
||
|
||
const BINANCE = {
|
||
page: {
|
||
detectPage: () => {
|
||
const { hostname, pathname } = window.location;
|
||
if (window.top !== window) {
|
||
TL.debug('BINANCE-PAGE-DETECT', 'Call from iframe ❌');
|
||
return BINANCE_PAGES.IGNORE;
|
||
}
|
||
|
||
const hostConfig = BINANCE_PAGE_PATTERNS.find(cfg => cfg.host === hostname);
|
||
if (!hostConfig) {
|
||
return BINANCE_PAGES.UNKNOWN;
|
||
}
|
||
|
||
// Check patterns in order (most specific first)
|
||
for (const pattern of hostConfig.patterns) {
|
||
if (pathname.includes(pattern.includes)) {
|
||
TL.debug('BINANCE-PAGE-DETECT', `Matched pattern: ${pattern.includes} -> ${pattern.page}`);
|
||
return pattern.page;
|
||
}
|
||
}
|
||
|
||
TL.debug('BINANCE-PAGE-DETECT', `No pattern matched: ${hostname}${pathname}`);
|
||
return BINANCE_PAGES.UNKNOWN;
|
||
},
|
||
|
||
isOnLoginPage: () => AppState.getCurrentPage() === BINANCE_PAGES.LOGIN,
|
||
isOnAlphaSwapPage: (contractAddress) => {
|
||
const page = AppState.getCurrentPage();
|
||
return page === BINANCE_PAGES.ALPHA_SWAP && (!contractAddress || window.location.pathname.includes(contractAddress));
|
||
},
|
||
isOnAlphaOrderHistoryPage: () => AppState.getCurrentPage() === BINANCE_PAGES.ALPHA_ORDER_HISTORY,
|
||
isOnWalletEarnPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_EARN,
|
||
isOnWalletFundingPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_FUNDING,
|
||
isOnWalletSpotPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_SPOT,
|
||
|
||
// Navigation URL generators - chỉ trả về address, không thực hiện navigate
|
||
getLoginUrl: () => 'https://www.binance.com/en/login',
|
||
|
||
getAlphaSwapUrl: (contractAddress) => {
|
||
return contractAddress
|
||
? `https://www.binance.com/en/alpha/bsc/${contractAddress}`
|
||
: `https://www.binance.com/en/alpha`;
|
||
},
|
||
|
||
getAlphaOrderHistoryUrl: () => 'https://www.binance.com/en/my/orders/alpha/orderhistory',
|
||
|
||
getWalletEarnUrl: () => 'https://www.binance.com/en/my/wallet/account/earn',
|
||
|
||
getWalletFundingUrl: () => 'https://www.binance.com/en/my/wallet/funding',
|
||
|
||
getWalletSpotUrl: () => 'https://www.binance.com/en/my/wallet/account/main'
|
||
},
|
||
|
||
auth: {
|
||
detectLoginState: () => {
|
||
if (!AppSettings.getSafetyGuard()) return null;
|
||
|
||
if (window.top !== window) {
|
||
TL.debug('BINANCE-LOGIN', 'In iframe - cannot determine login state');
|
||
return null;
|
||
}
|
||
|
||
if (BINANCE.page.isOnLoginPage()) {
|
||
return false;
|
||
}
|
||
|
||
// Method 1: Check for login/register buttons (indicates NOT logged in)
|
||
const loginBtn = document.querySelector('#toLoginPage, [data-testid="login-button"], .login-btn');
|
||
const regBtn = document.querySelector('#toRegisterPage, [data-testid="register-button"], .register-btn');
|
||
|
||
let msg = `loginBtn: ${TL.dom.isVisible?.(loginBtn)} regBtn: ${TL.dom.isVisible?.(regBtn)}\n`;
|
||
|
||
// If login/register buttons are visible, definitely not logged in
|
||
if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) {
|
||
TL.debug('BINANCE-LOGIN', msg + 'Login/Register buttons visible -> NOT logged in');
|
||
return false;
|
||
}
|
||
|
||
// Method 2: Check for user account elements (indicates logged in)
|
||
const dashboardLink = document.querySelector('a[href*="/my/dashboard"]');
|
||
const accountIcon = document.querySelector('.header-account-icon');
|
||
const walletBtn = document.querySelector('#ba-wallet');
|
||
|
||
const isDashboardVisible = dashboardLink && TL.dom.isVisible?.(dashboardLink);
|
||
const isAccountIconVisible = !!accountIcon;
|
||
const isWalletVisible = walletBtn && TL.dom.isVisible?.(walletBtn);
|
||
|
||
msg += `Visible: dashboard: ${isDashboardVisible ? '✓' : '✗'}, accountIcon: ${isAccountIconVisible ? '✓' : '✗'}, walletBtn: ${isWalletVisible ? '✓' : '✗'}\n`;
|
||
|
||
// If we see user account elements, likely logged in
|
||
if (isDashboardVisible && isAccountIconVisible && isWalletVisible) {
|
||
TL.debug('BINANCE-LOGIN', msg + 'User account elements found -> LIKELY logged in');
|
||
return true;
|
||
}
|
||
|
||
TL.debug('BINANCE-LOGIN', msg + 'Cannot determine login state - returning null');
|
||
return null;
|
||
},
|
||
|
||
isLoggedIn: async (timeoutMs = 30000, pollMs = 500) => {
|
||
const deadline = Date.now() + timeoutMs;
|
||
while (Date.now() < deadline) {
|
||
const state = BINANCE.auth.detectLoginState();
|
||
if (state !== null) {
|
||
TL.debug(`BINANCE-LOGIN`, `isLoggedIn: ${state ? 'true' : 'false'}`);
|
||
return state;
|
||
}
|
||
TL.debug(`BINANCE-LOGIN`, `isLoggedIn: not found. Sleeping...`);
|
||
await TL.delay(pollMs);
|
||
}
|
||
|
||
const fallback = BINANCE.auth.detectLoginState() ?? false;
|
||
TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`);
|
||
return fallback;
|
||
}
|
||
}
|
||
};
|
||
|
||
// ====== APP UI ======
|
||
const AppUi = {
|
||
// ====== DASHBOARD OVERLAY ======
|
||
dashboardOverlay: {
|
||
element: null,
|
||
|
||
// Get position CSS based on position number
|
||
getPositionCSS: (position) => {
|
||
switch (position) {
|
||
case 1: return { top: '10px', left: '10px', bottom: 'auto', right: 'auto' }; // top-left
|
||
case 2: return { top: '10px', right: '10px', bottom: 'auto', left: 'auto' }; // top-right
|
||
case 3: return { bottom: '10px', left: '10px', top: 'auto', right: 'auto' }; // bottom-left
|
||
case 4: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' }; // bottom-right
|
||
default: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' };
|
||
}
|
||
},
|
||
|
||
// Create dashboard overlay element
|
||
create: () => {
|
||
|
||
|
||
if (AppUi.dashboardOverlay.element) {
|
||
|
||
document.body.removeChild(AppUi.dashboardOverlay.element);
|
||
}
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'baf-status-overlay';
|
||
overlay.style.cssText = `
|
||
position: fixed;
|
||
z-index: 999999;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
color: white;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
min-width: 200px;
|
||
max-width: 300px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
transition: all 0.3s ease;
|
||
user-select: none;
|
||
`;
|
||
|
||
// Add close button (X)
|
||
const closeBtn = document.createElement('div');
|
||
closeBtn.style.cssText = `
|
||
position: absolute;
|
||
top: -8px;
|
||
left: -8px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: #dc3545;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: white;
|
||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||
transition: all 0.2s ease;
|
||
font-weight: bold;
|
||
`;
|
||
closeBtn.innerHTML = '×';
|
||
closeBtn.title = 'Close popup';
|
||
closeBtn.addEventListener('click', () => {
|
||
AppUi.dashboardOverlay.hide();
|
||
AppSettings.setPopupVisible(false);
|
||
});
|
||
TL.dom.setHover(closeBtn, 'mouseenter', { scale: 1.5, background: '#c82333' });
|
||
TL.dom.setHover(closeBtn, 'mouseleave', { scale: 1, background: '#dc3545' });
|
||
overlay.appendChild(closeBtn);
|
||
|
||
// Add position toggle button
|
||
const positionBtn = document.createElement('div');
|
||
positionBtn.style.cssText = `
|
||
position: absolute;
|
||
top: -8px;
|
||
right: -8px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: #007bff;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
font-size: 10px;
|
||
color: white;
|
||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||
transition: all 0.2s ease;
|
||
`;
|
||
positionBtn.innerHTML = '⇄';
|
||
positionBtn.title = 'Change position';
|
||
positionBtn.addEventListener('click', () => {
|
||
const newPosition = (AppSettings.getPopupPosition() % 4) + 1;
|
||
AppSettings.setPopupPosition(newPosition);
|
||
AppUi.dashboardOverlay.updatePosition();
|
||
});
|
||
TL.dom.setHover(positionBtn, 'mouseenter', { scale: 1.5, background: '#0056b3' });
|
||
TL.dom.setHover(positionBtn, 'mouseleave', { scale: 1, background: '#007bff' });
|
||
overlay.appendChild(positionBtn);
|
||
|
||
|
||
AppUi.dashboardOverlay.element = overlay;
|
||
|
||
document.body.appendChild(overlay);
|
||
|
||
|
||
return overlay;
|
||
},
|
||
|
||
// Update overlay position
|
||
updatePosition: () => {
|
||
if (!AppUi.dashboardOverlay.element) return;
|
||
|
||
const positionCSS = AppUi.dashboardOverlay.getPositionCSS(AppSettings.getPopupPosition());
|
||
|
||
Object.assign(AppUi.dashboardOverlay.element.style, positionCSS);
|
||
},
|
||
|
||
// Update overlay content
|
||
updateContent: () => {
|
||
try {
|
||
|
||
|
||
|
||
if (!AppUi.dashboardOverlay.element) {
|
||
AppUi.dashboardOverlay.create();
|
||
}
|
||
// Get status display
|
||
let statusDisplay = '';
|
||
const currentStatus = AppSession.getSessionStatus();
|
||
|
||
switch (currentStatus) {
|
||
case AppEnums.BOT_STATUS.INITIALIZING:
|
||
statusDisplay = '🔄 Initializing';
|
||
break;
|
||
case AppEnums.BOT_STATUS.STOP:
|
||
statusDisplay = '⏹️ Stop';
|
||
break;
|
||
case AppEnums.BOT_STATUS.PAUSE:
|
||
statusDisplay = '⏸️ Pause';
|
||
break;
|
||
case AppEnums.BOT_STATUS.WAITING_FOR_TASK:
|
||
statusDisplay = '⏳ Waiting for Task';
|
||
break;
|
||
case AppEnums.BOT_STATUS.PERFORMING_TASK:
|
||
statusDisplay = '▶️ Performing Task';
|
||
break;
|
||
case AppEnums.BOT_STATUS.ERROR:
|
||
statusDisplay = '❌ Error';
|
||
break;
|
||
default:
|
||
statusDisplay = '❓ Unknown';
|
||
break;
|
||
}
|
||
|
||
// Get server info
|
||
const serverLabel = AppState.getServer()?.label || 'Unknown';
|
||
const serverConnected = AppState.getServerConnected() ? `🟢 (${AppState.getServerLatency()}ms)` : '🔴';
|
||
|
||
// Get page info
|
||
const pageDisplay = AppState.getCurrentPage() || 'unknown';
|
||
|
||
// Get login status
|
||
const loginStatus = AppState.getIsLoggedIn() ? '✅' : '❌';
|
||
|
||
// Get SafetyGuard status
|
||
const safetyStatus = {
|
||
enabled: AppSettings.getSafetyGuard(),
|
||
message: AppSettings.getSafetyGuard() ? '✅ Operations Enabled' : '🚨 Operations Blocked'
|
||
};
|
||
|
||
// Build content
|
||
const content = `
|
||
<div style="margin-bottom: 8px;"><strong>BAF Agent Dashboard</strong></div>
|
||
|
||
<div style="margin-bottom: 4px;">
|
||
<span id="safety-toggle-btn-AithaiG2" style="color: ${safetyStatus.enabled ? '#28a745' : '#dc3545'}; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid ${safetyStatus.enabled ? '#28a745' : '#dc3545'}; background: ${safetyStatus.enabled ? 'rgba(40, 167, 69, 0.1)' : 'rgba(220, 53, 69, 0.1)'}; transition: all 0.2s ease;" title="Click to toggle safety status">🛟 Safety: ${safetyStatus.enabled ? '🛡️ Safe' : '🚨 Blocked'}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #6c757d; font-size: 11px;">🆔 Tab: ${AppSession.SESSION_ID.slice(-12)}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #007bff; padding: 2px 4px; background: rgba(0, 123, 255, 0.1);" title="Current bot status">🤖 Bot Status: ${statusDisplay}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span id="startup-mode-toggle-btn-tha9ieCa" style="color: #ffc107; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid #ffc107; background: rgba(255, 193, 7, 0.1); transition: all 0.2s ease;" title="Click to toggle startup mode between auto and manual">🚀 Startup Mode: ${AppSettings.getBotStartupMode() === AppEnums.BOT_STARTUP_MODE.AUTO ? '🔄 Auto' : '✋ Manual'}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #28a745;">📋 Task: ${AppSession.getCurrentTask() || 'None'}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #17a2b8;">📍 Step: ${AppSession.getCurrentStep() || 'None'}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #fd7e14;">👤 Login: ${loginStatus}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #6f42c1;">Page: ${pageDisplay}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span id="server-toggle-btn-Youx4pho" style="color: #e83e8c; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid #e83e8c; background: rgba(232, 62, 140, 0.1); transition: all 0.2s ease;" title="Click to switch between local and prod servers">🌐 Server: ${serverLabel} ${serverConnected}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span id="debug-toggle-btn-Tai5guto" style="color: ${AppSettings.getDebug() ? '#17a2b8' : '#6c757d'}; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid ${AppSettings.getDebug() ? '#17a2b8' : '#6c757d'}; background: ${AppSettings.getDebug() ? 'rgba(23, 162, 184, 0.1)' : 'rgba(108, 117, 125, 0.1)'}; transition: all 0.2s ease;" title="Click to toggle debug mode">🐛 Debug: ${AppSettings.getDebug() ? '🔈 ON' : '🔇 OFF'}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: ${CONFIG.DEBUG_MILESTONE ? '#28a745' : '#6c757d'}; padding: 2px 4px; border-radius: 3px; border: 1px solid ${CONFIG.DEBUG_MILESTONE ? '#28a745' : '#6c757d'}; background: ${CONFIG.DEBUG_MILESTONE ? 'rgba(40, 167, 69, 0.1)' : 'rgba(108, 117, 125, 0.1)'};" title="Milestone debug mode (configured in CONFIG)">🎯 Milestone: ${CONFIG.DEBUG_MILESTONE ? '🔈 ON' : '🔇 OFF'}</span>
|
||
</div>
|
||
<div style="margin-top: 8px; border-top: 1px solid rgba(255, 255, 255, 0.2); padding-top: 8px;">
|
||
<div style="text-align: center; margin-bottom: 8px;">
|
||
<span id="timer-display-ohr1Youj" style="color: #20c997; font-family: 'Courier New', monospace; font-size: 16px; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 1px solid #20c997; background: rgba(32, 201, 151, 0.1);" title="Session timer">⏱️ ${AppServices.Timer.getDisplay()}</span>
|
||
</div>
|
||
<div style="display: flex; gap: 4px; justify-content: center;">
|
||
<span id="play-btn-uPha3Mah" style="color: #28a745; cursor: pointer; padding: 4px 8px; border-radius: 4px; border: 1px solid #28a745; background: ${currentStatus === AppEnums.BOT_STATUS.WAITING_FOR_TASK || currentStatus === AppEnums.BOT_STATUS.PERFORMING_TASK ? 'rgba(40, 167, 69, 0.3)' : 'rgba(40, 167, 69, 0.1)'}; transition: all 0.2s ease; font-size: 14px;" title="Play - Start waiting for tasks">▶️</span>
|
||
<span id="pause-btn-Gai2quie" style="color: #ffc107; cursor: pointer; padding: 4px 8px; border-radius: 4px; border: 1px solid #ffc107; background: ${currentStatus === AppEnums.BOT_STATUS.PAUSE ? 'rgba(255, 193, 7, 0.3)' : 'rgba(255, 193, 7, 0.1)'}; transition: all 0.2s ease; font-size: 14px;" title="Pause - Stop but keep current task">⏸️</span>
|
||
<span id="stop-btn-jooK6che" style="color: #dc3545; cursor: pointer; padding: 4px 8px; border-radius: 4px; border: 1px solid #dc3545; background: ${currentStatus === AppEnums.BOT_STATUS.STOP || currentStatus === AppEnums.BOT_STATUS.ERROR ? 'rgba(220, 53, 69, 0.3)' : 'rgba(220, 53, 69, 0.1)'}; transition: all 0.2s ease; font-size: 14px;" title="Stop - Clear task and stop">⏹️</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Update content (excluding position button)
|
||
if (!AppUi.dashboardOverlay.element) {
|
||
TL.error('DASHBOARD-OVERLAY', '8.1. Element is null!');
|
||
return;
|
||
}
|
||
|
||
const contentDiv = AppUi.dashboardOverlay.element.querySelector('.baf-content') || document.createElement('div');
|
||
contentDiv.className = 'baf-content';
|
||
contentDiv.innerHTML = content;
|
||
|
||
if (!AppUi.dashboardOverlay.element.querySelector('.baf-content')) {
|
||
AppUi.dashboardOverlay.element.appendChild(contentDiv);
|
||
}
|
||
|
||
// Add event listener for safety toggle button
|
||
const safetyToggleBtn = contentDiv.querySelector('#safety-toggle-btn-AithaiG2');
|
||
if (safetyToggleBtn) {
|
||
safetyToggleBtn.onclick = () => {
|
||
const currentStatus = {
|
||
enabled: AppSettings.getSafetyGuard(),
|
||
message: AppSettings.getSafetyGuard() ? '✅ Operations Enabled' : '🚨 Operations Blocked'
|
||
};
|
||
if (currentStatus.enabled) {
|
||
AppSettings.setSafetyGuard(false);
|
||
} else {
|
||
AppSettings.setSafetyGuard(true);
|
||
}
|
||
// Update the content to reflect the change
|
||
AppUi.dashboardOverlay.updateContent();
|
||
};
|
||
}
|
||
|
||
// Add event listener for debug toggle button
|
||
const debugToggleBtn = contentDiv.querySelector('#debug-toggle-btn-Tai5guto');
|
||
if (debugToggleBtn) {
|
||
debugToggleBtn.onclick = () => {
|
||
const newDebugState = !AppSettings.getDebug();
|
||
AppSettings.setDebug(newDebugState);
|
||
// Update the content to reflect the change
|
||
AppUi.dashboardOverlay.updateContent();
|
||
};
|
||
}
|
||
|
||
// Add event listeners for control buttons
|
||
const playBtn = contentDiv.querySelector('#play-btn-uPha3Mah');
|
||
if (playBtn) {
|
||
playBtn.onclick = () => {
|
||
AppServices.Status.play();
|
||
AppUi.dashboardOverlay.updateContent();
|
||
};
|
||
}
|
||
|
||
const pauseBtn = contentDiv.querySelector('#pause-btn-Gai2quie');
|
||
if (pauseBtn) {
|
||
pauseBtn.onclick = () => {
|
||
AppServices.Status.pause();
|
||
AppUi.dashboardOverlay.updateContent();
|
||
};
|
||
}
|
||
|
||
const stopBtn = contentDiv.querySelector('#stop-btn-jooK6che');
|
||
if (stopBtn) {
|
||
stopBtn.onclick = () => {
|
||
AppServices.Status.stop();
|
||
AppUi.dashboardOverlay.updateContent();
|
||
};
|
||
}
|
||
|
||
// Add hover effects for control buttons
|
||
if (playBtn) {
|
||
TL.dom.setHover(playBtn, 'mouseenter', { scale: 1.2, background: 'rgba(40, 167, 69, 0.2)' });
|
||
TL.dom.setHover(playBtn, 'mouseleave', { scale: 1, background: 'rgba(40, 167, 69, 0.1)' });
|
||
}
|
||
if (pauseBtn) {
|
||
TL.dom.setHover(pauseBtn, 'mouseenter', { scale: 1.2, background: 'rgba(255, 193, 7, 0.2)' });
|
||
TL.dom.setHover(pauseBtn, 'mouseleave', { scale: 1, background: 'rgba(255, 193, 7, 0.1)' });
|
||
}
|
||
if (stopBtn) {
|
||
TL.dom.setHover(stopBtn, 'mouseenter', { scale: 1.2, background: 'rgba(220, 53, 69, 0.2)' });
|
||
TL.dom.setHover(stopBtn, 'mouseleave', { scale: 1, background: 'rgba(220, 53, 69, 0.1)' });
|
||
}
|
||
|
||
// Add event listener for startup mode toggle button
|
||
const startupModeToggleBtn = contentDiv.querySelector('#startup-mode-toggle-btn-tha9ieCa');
|
||
if (startupModeToggleBtn) {
|
||
startupModeToggleBtn.onclick = () => {
|
||
const currentStartupMode = AppSettings.getBotStartupMode();
|
||
const newStartupMode = currentStartupMode === AppEnums.BOT_STARTUP_MODE.AUTO ? AppEnums.BOT_STARTUP_MODE.MANUAL : AppEnums.BOT_STARTUP_MODE.AUTO;
|
||
AppSettings.setBotStartupMode(newStartupMode);
|
||
// Update the content to reflect the change
|
||
AppUi.dashboardOverlay.updateContent();
|
||
};
|
||
}
|
||
|
||
// Add event listener for server toggle button
|
||
const serverToggleBtn = contentDiv.querySelector('#server-toggle-btn-Youx4pho');
|
||
if (serverToggleBtn) {
|
||
serverToggleBtn.onclick = async () => {
|
||
const currentServerType = AppSettings.getServerType();
|
||
const newServerType = currentServerType === 'local' ? 'prod' : 'local';
|
||
|
||
// Update server type in settings
|
||
AppSettings.setServerType(newServerType);
|
||
await AppState.update({ server_last_seen: null });
|
||
|
||
// Update the content to reflect the change
|
||
AppUi.dashboardOverlay.updateContent();
|
||
};
|
||
}
|
||
} catch (error) {
|
||
TL.error('DASHBOARD-OVERLAY', 'Error updating content:', error);
|
||
}
|
||
},
|
||
|
||
// Show dashboard overlay
|
||
show: () => {
|
||
if (!AppUi.dashboardOverlay.element) {
|
||
AppUi.dashboardOverlay.create();
|
||
}
|
||
|
||
AppUi.dashboardOverlay.updatePosition();
|
||
AppUi.dashboardOverlay.updateContent();
|
||
AppUi.dashboardOverlay.element.style.display = 'block';
|
||
},
|
||
|
||
// Hide dashboard overlay
|
||
hide: () => {
|
||
if (AppUi.dashboardOverlay.element) {
|
||
AppUi.dashboardOverlay.element.style.display = 'none';
|
||
}
|
||
},
|
||
|
||
// Toggle dashboard overlay
|
||
toggle: () => {
|
||
if (AppSettings.getPopupVisible()) {
|
||
AppUi.dashboardOverlay.hide();
|
||
AppSettings.setPopupVisible(false);
|
||
} else {
|
||
AppUi.dashboardOverlay.show();
|
||
AppSettings.setPopupVisible(true);
|
||
}
|
||
},
|
||
|
||
// Initialize dashboard overlay
|
||
init: () => {
|
||
try {
|
||
const popupVisible = AppSettings.getPopupVisible();
|
||
|
||
if (popupVisible) {
|
||
AppUi.dashboardOverlay.show();
|
||
} else {
|
||
}
|
||
} catch (error) {
|
||
TL.error('DASHBOARD-OVERLAY', '1.4.4. Error initializing dashboard overlay:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
},
|
||
|
||
// ====== MENU SYSTEM ======
|
||
menu: {
|
||
// Create GM menu
|
||
create: async () => {
|
||
try {
|
||
if (AppState.getMenuCreated()) {
|
||
return;
|
||
}
|
||
|
||
AppState.update({ menuCreated: true });
|
||
|
||
GM_registerMenuCommand('🛟 Safety Guard: -> ✅ Enable Operations', () => {
|
||
AppSettings.setSafetyGuard(true);
|
||
});
|
||
|
||
GM_registerMenuCommand('🛟 Safety Guard: -> 🚨 Block Operations', () => {
|
||
AppSettings.setSafetyGuard(false);
|
||
});
|
||
|
||
GM_registerMenuCommand('🔑 Token', async () => {
|
||
try {
|
||
const curToken = await AppSettings.getToken();
|
||
const input = prompt('Bearer token:', curToken || '');
|
||
if (input !== null && input.trim() && input.trim() !== curToken) {
|
||
await AppSettings.setToken(input);
|
||
}
|
||
const s = AppState.getServer();
|
||
const res = await BAF.register({ session_id: AppSession.SESSION_ID });
|
||
const resStr =
|
||
`Server: ${s.label} (${s.url})\n` +
|
||
`Status: ${res.ok ? 'Connected ✅' : 'Failed ❌'} (${res.status})`;
|
||
TL.debug(`BAF`, resStr);
|
||
TL.noti('BAF Server', resStr);
|
||
} catch (e) {
|
||
const resStr = `ping error: ${e.message}`;
|
||
TL.error('BAF', resStr);
|
||
TL.noti('BAF Server', resStr);
|
||
}
|
||
});
|
||
|
||
// Popup toggle menu
|
||
GM_registerMenuCommand("👁️ Toggle Popup", () => {
|
||
AppUi.dashboardOverlay.toggle();
|
||
});
|
||
} catch (error) {
|
||
TL.error('MENU', '1.3.5. Failed to create menu:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
},
|
||
};
|
||
|
||
// ====== APP TASKS ======
|
||
const AppTasks = {
|
||
login: async (step, data) => {
|
||
if (!AppServices.isReadyToRun()) {
|
||
TL.debug('APP-TASKS', 'Bot is not ready to run');
|
||
return;
|
||
}
|
||
|
||
TL.debug('APP-TASKS', `Login task ${step}`);
|
||
//set steps
|
||
switch (step) {
|
||
case "start":
|
||
AppSession.setSessionStatus(AppEnums.BOT_STATUS.PERFORMING_TASK);
|
||
AppSession.setCurrentStep("navigating-to-login-page");
|
||
TL.browser.navigateTo(BINANCE.page.getLoginUrl());
|
||
break;
|
||
}
|
||
},
|
||
};
|
||
|
||
// ====== APP SERVICE ======
|
||
const AppServices = {
|
||
initialize: async () => {
|
||
try {
|
||
await AppState.initialize();
|
||
await AppSession.initialize();
|
||
|
||
// Khởi tạo timer
|
||
AppServices.Timer.initialize();
|
||
|
||
await AppUi.menu.create();
|
||
AppUi.dashboardOverlay.init();
|
||
await AppServices.registerSession();
|
||
|
||
const status = AppSession.getSessionStatus();
|
||
if (status === AppEnums.BOT_STATUS.INITIALIZING) {
|
||
const startupMode = AppSettings.getBotStartupMode();
|
||
if (startupMode === AppEnums.BOT_STARTUP_MODE.AUTO) {
|
||
AppSession.setSessionStatus(AppEnums.BOT_STATUS.WAITING_FOR_TASK);
|
||
} else {
|
||
AppSession.setSessionStatus(AppEnums.BOT_STATUS.STOP);
|
||
}
|
||
}
|
||
|
||
AppServices.initInterval();
|
||
} catch (error) {
|
||
TL.error('APP-SERVICES', 'Error during initialization:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
registerSession: async () => {
|
||
try {
|
||
const msg = await BAF.register({ session_id: AppSession.SESSION_ID });
|
||
|
||
if (msg.ok) {
|
||
AppSession.setSessionToken(msg.data.token);
|
||
AppSession.setSessionBinded(true);
|
||
} else {
|
||
AppSession.setSessionBinded(false);
|
||
TL.error('APP-SERVICES', `1.5.3. Failed to register session: ${msg.error}`);
|
||
}
|
||
} catch (error) {
|
||
TL.error('APP-SERVICES', '1.5.3. Error during session registration:', error);
|
||
AppSession.setSessionBinded(false);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
initInterval: () => {
|
||
try {
|
||
setInterval(() => AppServices.heartbeat(), CONFIG.HEARTBEAT_INTERVAL);
|
||
setInterval(() => AppUi.dashboardOverlay.updateContent(), CONFIG.DASHBOARD_UPDATE_INTERVAL);
|
||
|
||
// Timer update interval - cập nhật timer mỗi giây
|
||
setInterval(() => {
|
||
const timerDisplay = document.querySelector('#timer-display-ohr1Youj');
|
||
if (timerDisplay) {
|
||
timerDisplay.textContent = `⏱️ ${AppServices.Timer.getDisplay()}`;
|
||
}
|
||
}, 1000);
|
||
} catch (error) {
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
isConnected: () => {
|
||
const serverLastSeen = AppState.getServerLastSeen();
|
||
return serverLastSeen && new Date() - serverLastSeen < 60000;
|
||
},
|
||
|
||
isReadyToRun: () => {
|
||
return AppSession.getSessionBinded() && AppSettings.getSafetyGuard() && AppSession.getSessionStatus() === AppEnums.BOT_STATUS.WAITING_FOR_TASK;
|
||
},
|
||
|
||
// ====== TIMER SYSTEM ======
|
||
Timer: {
|
||
// Timer state management - Quản lý trạng thái timer
|
||
getStartTime: () => {
|
||
const time = sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_START_TIME);
|
||
return time ? parseInt(time) : null;
|
||
},
|
||
setStartTime: time => {
|
||
if (time === null || time === undefined) {
|
||
sessionStorage.removeItem(SESSION_STORAGE_KEYS.TIMER_START_TIME);
|
||
} else {
|
||
sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_START_TIME, time.toString());
|
||
}
|
||
},
|
||
|
||
isRunning: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_IS_RUNNING) === 'true',
|
||
setRunning: running => sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_IS_RUNNING, running ? 'true' : 'false'),
|
||
|
||
getAccumulated: () => {
|
||
const time = sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_ACCUMULATED);
|
||
return time ? parseInt(time) : 0;
|
||
},
|
||
setAccumulated: time => sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_ACCUMULATED, time.toString()),
|
||
|
||
// Timer control methods
|
||
start: () => {
|
||
if (!AppServices.Timer.isRunning()) {
|
||
AppServices.Timer.setStartTime(Date.now());
|
||
AppServices.Timer.setRunning(true);
|
||
TL.debug('TIMER', 'Timer started');
|
||
}
|
||
},
|
||
|
||
pause: () => {
|
||
if (AppServices.Timer.isRunning()) {
|
||
const startTime = AppServices.Timer.getStartTime();
|
||
const currentTime = Date.now();
|
||
const elapsed = currentTime - startTime;
|
||
const accumulated = AppServices.Timer.getAccumulated();
|
||
AppServices.Timer.setAccumulated(accumulated + elapsed);
|
||
AppServices.Timer.setRunning(false);
|
||
AppServices.Timer.setStartTime(null);
|
||
}
|
||
},
|
||
|
||
stop: () => {
|
||
AppServices.Timer.pause();
|
||
AppServices.Timer.setAccumulated(0);
|
||
},
|
||
|
||
getDisplay: () => {
|
||
let totalTime = AppServices.Timer.getAccumulated();
|
||
|
||
if (AppServices.Timer.isRunning()) {
|
||
const startTime = AppServices.Timer.getStartTime();
|
||
if (startTime) {
|
||
const currentTime = Date.now();
|
||
totalTime += (currentTime - startTime);
|
||
}
|
||
}
|
||
|
||
const hours = Math.floor(totalTime / 3600000);
|
||
const minutes = Math.floor((totalTime % 3600000) / 60000);
|
||
const seconds = Math.floor((totalTime % 60000) / 1000);
|
||
|
||
const display = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
TL.debug('TIMER', `Display: ${display}, Running: ${AppServices.Timer.isRunning()}, Accumulated: ${totalTime}`);
|
||
return display;
|
||
},
|
||
|
||
initialize: () => {
|
||
AppServices.Timer.setAccumulated(0);
|
||
AppServices.Timer.setRunning(false);
|
||
AppServices.Timer.setStartTime(null);
|
||
TL.debug('TIMER', 'Timer initialized');
|
||
}
|
||
},
|
||
|
||
// ====== HEARTBEAT SYSTEM ======
|
||
heartbeat: async () => {
|
||
if (!AppServices.isReadyToRun()) return;
|
||
|
||
try {
|
||
const status = {
|
||
logged_in: AppState.getIsLoggedIn(),
|
||
current_page: AppState.getCurrentPage(),
|
||
bot_status: AppSession.getSessionStatus(),
|
||
current_task: AppSession.getCurrentTask(),
|
||
current_task_data: AppSession.getCurrentTaskData(),
|
||
current_step: AppSession.getCurrentStep(),
|
||
};
|
||
|
||
// TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`);
|
||
const start = performance.now();
|
||
const res = await BAF.ping(status);
|
||
const end = performance.now();
|
||
const duration = Math.round(end - start);
|
||
|
||
if (res.ok) {
|
||
AppState.update({ server_last_seen: new Date(), server_latency: duration });
|
||
}
|
||
|
||
return status;
|
||
} catch (e) {
|
||
TL.error('HEARTBEAT', e.message);
|
||
return null;
|
||
}
|
||
},
|
||
|
||
handleNewTask: async () => {
|
||
const data = {
|
||
logged_in: AppState.getIsLoggedIn(),
|
||
current_page: AppState.getCurrentPage(),
|
||
bot_status: AppSession.getSessionStatus(),
|
||
current_task: AppSession.getCurrentTask(),
|
||
current_task_data: AppSession.getCurrentTaskData(),
|
||
current_step: AppSession.getCurrentStep(),
|
||
};
|
||
|
||
const taskResponse = await BAF.getTask(data);
|
||
TL.debug('APP-SERVICES', `Task: ${JSON.stringify(taskResponse, null, 2)}`);
|
||
|
||
if (taskResponse.ok) {
|
||
const task = taskResponse.data;
|
||
AppSession.setCurrentTask(task.type);
|
||
AppSession.setCurrentTaskData(task.data || null);
|
||
|
||
switch (task.type) {
|
||
case BAF_TASKS.NO_TASK:
|
||
TL.debug('APP-TASK', "No task. Sleep now");
|
||
break;
|
||
case BAF_TASKS.LOGIN:
|
||
AppTasks.login("start", task.data);
|
||
break;
|
||
default:
|
||
TL.debug('APP-TASK', `Unknown task 🛑`);
|
||
AppSession.setSessionStatus(AppEnums.BOT_STATUS.ERROR);
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
resumeTask: async () => {
|
||
TL.debug('APP-SERVICES', 'Resume task');
|
||
|
||
const task = AppSession.getCurrentTask();
|
||
const data = AppSession.getCurrentTaskData();
|
||
const step = AppSession.getCurrentStep();
|
||
|
||
switch (task) {
|
||
case BAF_TASKS.LOGIN:
|
||
AppTasks.login(step, data);
|
||
break;
|
||
};
|
||
},
|
||
|
||
// ====== BOT STATUS CONTROL ======
|
||
Status: {
|
||
play: () => {
|
||
AppServices.Timer.start();
|
||
AppSession.setSessionStatus(AppEnums.BOT_STATUS.WAITING_FOR_TASK);
|
||
},
|
||
|
||
pause: () => {
|
||
AppServices.Timer.pause();
|
||
AppSession.setSessionStatus(AppEnums.BOT_STATUS.PAUSE);
|
||
},
|
||
|
||
stop: () => {
|
||
AppServices.Timer.stop();
|
||
AppSession.setCurrentTask(BAF_TASKS.NO_TASK);
|
||
AppSession.setCurrentTaskData(null);
|
||
AppSession.setCurrentStep(BAF_TASKS.NO_STEP);
|
||
AppSession.setSessionStatus(AppEnums.BOT_STATUS.STOP);
|
||
}
|
||
},
|
||
|
||
start: async () => {
|
||
while (true) {
|
||
switch (AppSession.getSessionStatus()) {
|
||
case AppEnums.BOT_STATUS.INITIALIZING:
|
||
TL.debug('APP-SERVICES', 'Bot is initializing, waiting...');
|
||
break;
|
||
case AppEnums.BOT_STATUS.STOP:
|
||
TL.debug('APP-SERVICES', 'Bot is stopped, skipping task check');
|
||
break;
|
||
case AppEnums.BOT_STATUS.PAUSE:
|
||
TL.debug('APP-SERVICES', 'Bot is paused, skipping task check');
|
||
break;
|
||
case AppEnums.BOT_STATUS.WAITING_FOR_TASK:
|
||
await AppServices.handleNewTask();
|
||
break;
|
||
case AppEnums.BOT_STATUS.PERFORMING_TASK:
|
||
await AppServices.resumeTask();
|
||
break;
|
||
case AppEnums.BOT_STATUS.ERROR:
|
||
TL.debug('APP-SERVICES', 'Bot is in error state, skipping task check');
|
||
break;
|
||
default:
|
||
TL.debug('APP-SERVICES', 'Bot is in unknown state, skipping task check');
|
||
break;
|
||
}
|
||
|
||
await TL.delay(CONFIG.TASK_INTERVAL);
|
||
}
|
||
}
|
||
};
|
||
|
||
// Start the application
|
||
try {
|
||
|
||
await AppServices.initialize();
|
||
|
||
} catch (error) {
|
||
TL.error('MAIN', 'Failed to initialize app:', error);
|
||
TL.error('MAIN', 'Error stack:', error.stack);
|
||
}
|
||
|
||
TL.log('MAIN', 'App started');
|
||
await AppServices.start();
|
||
})(); |