976 lines
41 KiB
JavaScript
976 lines
41 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 KEYS = {
|
|
SESSION_ID: 'aRah9OhHeijee6sho3baequu9phoovah',
|
|
SESSION_TOKEN: 'ThiegiecohViuZ1Iecio7gahphiechub',
|
|
|
|
AGENT_TOKEN: 'baf-agent-token',
|
|
}
|
|
if (!sessionStorage.getItem(KEYS.SESSION_ID)) {
|
|
sessionStorage.setItem(KEYS.SESSION_ID, uuid());
|
|
}
|
|
|
|
// ====== CONFIGURATION ======
|
|
const CONFIG = {
|
|
HEARTBEAT_INTERVAL: 10000, // Send heartbeat every 10 seconds
|
|
DASHBOARD_UPDATE_INTERVAL: 500, // Update dashboard overlay every 0.5 seconds
|
|
TASK_INTERVAL: 1000, // Check for new task every 1 second
|
|
SERVERS: {
|
|
local: { label: '🏠 Local', url: 'http://localhost:3000' },
|
|
prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' },
|
|
},
|
|
};
|
|
|
|
|
|
const AppSession = {
|
|
SESSION_ID: sessionStorage.getItem(KEYS.SESSION_ID),
|
|
|
|
getSessionToken: () => sessionStorage.getItem(KEYS.SESSION_TOKEN),
|
|
setSessionToken: token => sessionStorage.setItem(KEYS.SESSION_TOKEN, token),
|
|
}
|
|
|
|
// ====== APP ENUMS ======
|
|
const AppEnums = {
|
|
BOT_STATUS: {
|
|
IDLE: 'idle',
|
|
RUNNING: 'running',
|
|
}
|
|
};
|
|
|
|
// ====== 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
|
|
|
|
// UI state - Trạng thái UI
|
|
is_loading: false, // Đang loading
|
|
error_message: null, // Thông báo lỗi
|
|
|
|
// Session flags - Flags cho session hiện tại (reset khi reload)
|
|
menuCreated: false, // Menu đã được tạo
|
|
popupInitialized: false, // Popup đã được khởi tạo
|
|
appInitialized: false // App đã được khởi tạo hoàn toàn
|
|
};
|
|
|
|
this.observers = new Map(); // 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; }
|
|
getIsLoading() { return this.data.is_loading; }
|
|
getMenuCreated() { return this.data.menuCreated; }
|
|
getPopupInitialized() { return this.data.popupInitialized; }
|
|
getAppInitialized() { return this.data.appInitialized; }
|
|
|
|
/**
|
|
* Cập nhật state với partial update
|
|
* @param {Object} updates - Object chứa cặp key-value cần cập nhật vào state
|
|
* Ví dụ: { current_task: task, is_loading: true }
|
|
*/
|
|
async update(updates) {
|
|
const oldData = { ...this.data };
|
|
Object.assign(this.data, updates);
|
|
await this.notifyObservers(oldData, this.data);
|
|
// TL.debug('STATE', 'Updated', updates);
|
|
}
|
|
|
|
/**
|
|
* Đăng ký observer cho state change
|
|
* @param {string} key - Key của state cần observe
|
|
* @param {Function} callback - Callback function khi state thay đổi
|
|
*/
|
|
subscribe(key, callback) {
|
|
if (!this.observers.has(key)) {
|
|
this.observers.set(key, []);
|
|
}
|
|
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_status: 'baf-bot-status',
|
|
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 });
|
|
},
|
|
|
|
getBotStatus: () => GM_getValue(AppSettings.key_bot_status, AppEnums.BOT_STATUS.IDLE),
|
|
setBotStatus: (status) => GM_setValue(AppSettings.key_bot_status, status),
|
|
|
|
// 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),
|
|
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
|
|
*/
|
|
navigate: (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,
|
|
onload: (resp) => {
|
|
const text = resp.responseText || '';
|
|
let data = text;
|
|
const isJSON = /content-type:\s*application\/json/i.test(resp.responseHeaders || '');
|
|
if (isJSON) { try { data = JSON.parse(text); } catch { } }
|
|
TL.debug(`net`, `${init.method} ${resp.status} ${url}\n${init.body}`, data);
|
|
resolve({
|
|
status: resp.status,
|
|
ok: resp.status >= 200 && resp.status < 300,
|
|
data,
|
|
rawText: text,
|
|
headers: null
|
|
});
|
|
},
|
|
onerror: reject,
|
|
ontimeout: () => reject(new Error('GM_xmlhttpRequest timeout')),
|
|
});
|
|
});
|
|
},
|
|
}
|
|
};
|
|
|
|
// ====== BAF API MODULE ======
|
|
const BAF = {
|
|
_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 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.statusOverlay.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: () => {
|
|
// Get status display
|
|
let statusDisplay = '';
|
|
switch (AppSettings.getBotStatus()) {
|
|
case AppEnums.BOT_STATUS.IDLE:
|
|
statusDisplay = '💤 Idle';
|
|
break;
|
|
case AppEnums.BOT_STATUS.RUNNING:
|
|
statusDisplay = '▶️ Running';
|
|
break;
|
|
default:
|
|
statusDisplay = AppSettings.getBotStatus();
|
|
}
|
|
|
|
// 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" 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 id="bot-status-toggle-btn" style="color: #007bff; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid #007bff; background: rgba(0, 123, 255, 0.1); transition: all 0.2s ease;" title="Click to toggle bot status between idle and running">🤖 Bot Status: ${statusDisplay}</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" 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" 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>
|
|
`;
|
|
|
|
// Update content (excluding position button)
|
|
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');
|
|
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');
|
|
if (debugToggleBtn) {
|
|
debugToggleBtn.onclick = () => {
|
|
const newDebugState = !AppSettings.getDebug();
|
|
AppSettings.setDebug(newDebugState);
|
|
// Update the content to reflect the change
|
|
AppUi.dashboardOverlay.updateContent();
|
|
};
|
|
}
|
|
|
|
// Add event listener for bot status toggle button
|
|
const botStatusToggleBtn = contentDiv.querySelector('#bot-status-toggle-btn');
|
|
if (botStatusToggleBtn) {
|
|
botStatusToggleBtn.onclick = () => {
|
|
const currentBotStatus = AppSettings.getBotStatus();
|
|
const newBotStatus = currentBotStatus === AppEnums.BOT_STATUS.IDLE ? AppEnums.BOT_STATUS.RUNNING : AppEnums.BOT_STATUS.IDLE;
|
|
AppSettings.setBotStatus(newBotStatus);
|
|
// Update the content to reflect the change
|
|
AppUi.dashboardOverlay.updateContent();
|
|
};
|
|
}
|
|
|
|
// Add event listener for server toggle button
|
|
const serverToggleBtn = contentDiv.querySelector('#server-toggle-btn');
|
|
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();
|
|
};
|
|
}
|
|
},
|
|
|
|
// 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: () => {
|
|
if (AppSettings.getPopupVisible()) {
|
|
AppUi.dashboardOverlay.show();
|
|
}
|
|
}
|
|
},
|
|
|
|
// ====== 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', 'Failed to create menu:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
// ====== APP SERVICE ======
|
|
const AppServices = {
|
|
initialize: async () => {
|
|
await AppState.initialize();
|
|
await AppUi.menu.create();
|
|
AppUi.dashboardOverlay.init();
|
|
|
|
// Setup observers for dashboard overlay updates
|
|
AppServices.initObservers();
|
|
|
|
await AppServices.registerSession();
|
|
|
|
// Setup interval services
|
|
AppServices.initInterval();
|
|
|
|
TL.debug('APP-SERVICES', 'AppServices initialized');
|
|
},
|
|
|
|
initObservers: () => {
|
|
// AppState.subscribe('server_last_seen', () => {
|
|
// if (AppSettings.getPopupVisible()) {
|
|
// AppUi.dashboardOverlay.updateContent();
|
|
// }
|
|
// });
|
|
|
|
// AppState.subscribe('is_logged_in', () => {
|
|
// if (AppSettings.getPopupVisible()) {
|
|
// AppUi.dashboardOverlay.updateContent();
|
|
// }
|
|
// });
|
|
|
|
// AppState.subscribe('current_page', () => {
|
|
// if (AppSettings.getPopupVisible()) {
|
|
// AppUi.dashboardOverlay.updateContent();
|
|
// }
|
|
// });
|
|
},
|
|
registerSession: async () => {
|
|
const msg = await BAF.register({ session_id: AppSession.SESSION_ID });
|
|
AppSession.setSessionToken(msg.data.token);
|
|
TL.debug('APP-SERVICES', `Session token: ${AppSession.getSessionToken()}`);
|
|
},
|
|
|
|
// Setup interval services
|
|
initInterval: () => {
|
|
setInterval(() => AppServices.heartbeat(), CONFIG.HEARTBEAT_INTERVAL);
|
|
setInterval(() => AppUi.dashboardOverlay.updateContent(), CONFIG.DASHBOARD_UPDATE_INTERVAL);
|
|
|
|
TL.debug('APP-SERVICES', 'Interval services started');
|
|
},
|
|
|
|
isConnected: () => {
|
|
const serverLastSeen = AppState.getServerLastSeen();
|
|
return serverLastSeen && new Date() - serverLastSeen < 60000;
|
|
},
|
|
|
|
// ====== HEARTBEAT SYSTEM ======
|
|
heartbeat: async () => {
|
|
if (!AppSettings.getSafetyGuard()) return null;
|
|
|
|
try {
|
|
const status = {
|
|
logged_in: AppState.getIsLoggedIn(),
|
|
current_page: AppState.getCurrentPage(),
|
|
bot_status: AppSettings.getBotStatus(),
|
|
current_task: null, // TODO: Add to AppState if needed
|
|
current_task_data: null, // TODO: Add to AppState if needed
|
|
current_step: null, // TODO: Add to AppState if needed
|
|
};
|
|
|
|
// TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`);
|
|
const 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;
|
|
}
|
|
},
|
|
|
|
handleTask: (task) => {
|
|
TL.debug('APP-SERVICES', `Task: ${JSON.stringify(task, null, 2)}`);
|
|
},
|
|
|
|
start: async () => {
|
|
while (true) {
|
|
const task = await BAF.getTask();
|
|
if (task.ok) {
|
|
AppServices.handleTask(task.data);
|
|
}
|
|
|
|
await TL.sleep(CONFIG.TASK_INTERVAL);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Start the application
|
|
try {
|
|
await AppServices.initialize();
|
|
} catch (error) {
|
|
TL.error('MAIN', 'Failed to initialize app:', error);
|
|
}
|
|
|
|
TL.log('MAIN', 'App started');
|
|
await AppServices.start();
|
|
})(); |