// ==UserScript== // @name Binance Alpha Farm Agent // @namespace http://baf.thuanle.me // @version 2025.07.31 // @author TL // @description Automated trading agent for Binance Alpha Farm // @match https://www.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== (async () => { 'use strict'; // ====== Servers ====== const BAF_SERVERS = { local: { label: '🏠 Local', url: 'http://localhost:3000' }, prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' }, }; const CONFIG = { heartbeat_interval: 10000, is_debug: false, } // ====== Storage ====== const STORAGE = { key_token: 'baf-agent-token', 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), }; // ====== Utility ====== 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), error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${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}`); // fallback return false; }, delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)), }; // ====== DOM helpers ====== TL.dom = { isVisible: (el) => { if (!el) return false; const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; return (el.offsetWidth + el.offsetHeight) > 0; }, isDisabled: (el) => { return el?.disabled === true || el?.getAttribute('aria-disabled') === 'true'; }, isInViewport: (el) => { if (!el) return false; const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }, click: (el) => { if (el && typeof el.click === 'function') el.click(); }, scrollToView: (el, behavior = 'smooth') => { el?.scrollIntoView?.({ behavior, block: 'center' }); }, }; // ====== Network helpers (GM_xmlhttpRequest) ====== TL.net = { gmRequest(url, init = {}) { const headersToObject = (h) => { const o = {}; (h || new Headers()).forEach((v, k) => { o[k] = v; }); return o; }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url, method: (init.method || 'GET').toUpperCase(), headers: headersToObject(init.headers), data: init.body, onload: (resp) => { const text = resp.responseText || ''; let data = text; const isJSON = /content-type:\s*application\/json/i.test(resp.responseHeaders || ''); if (isJSON) { try { data = JSON.parse(text); } catch { } } TL.debug(`net`,`${init.method} ${resp.status} ${url}`, 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 ====== 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; }, // 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); if (params) for (const [k, v] of Object.entries(params)) url.searchParams.append(k, v); const h = new Headers(headers || {}); const token = await STORAGE.getToken(); 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); }, get: (path, params, init = {}) => BAF.request('GET', path, { params, headers: init.headers }), post: (path, body, init = {}) => BAF.request('POST', path, { body, headers: init.headers }), ping: (status) => BAF.post('/agent/ping', status), }; // ====== 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); 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 = { detectLoginState: () => { const loginBtn = document.querySelector('#toLoginPage'); const regBtn = document.querySelector('#toRegisterPage'); if (TL.dom.isVisible?.(loginBtn) || TL.ui.isVisible?.(regBtn)) return false; if (!loginBtn && !regBtn) return true; return null; // đang load hoặc chưa rõ }, isLoggedIn: async (timeoutMs = 6000, pollMs = 200) => { const deadline = Date.now() + timeoutMs; TL.debug(`BINANCE`,`isLoggedIn: Checking login state...`); while (Date.now() < deadline) { const state = BINANCE.detectLoginState(); if (state !== null) { return state; } TL.debug(`BINANCE`,`isLoggedIn: not found. Sleeping...`); await TL.delay(pollMs); } const fallback = BINANCE.detectLoginState() ?? false; TL.debug(`BINANCE`,`isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`); return fallback; }, }; // ====== Main ====== async function welcome_message() { 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})\n`; TL.log(`BAF`,resStr); } // ====== Heartbeat ====== async function heartbeat_report() { try { const isLoggedIn = await BINANCE.isLoggedIn(); const status = { logged_in: isLoggedIn }; // Log heartbeat TL.debug(`HEARTBEAT`,`${JSON.stringify(status, null, 2)}`); await BAF.ping(status); return status; } catch (e) { TL.error('HEARTBEAT', e.message); return null; } } // ====== Khởi tạo ====== await welcome_message(); await createGM_Menu(); setInterval(heartbeat_report, CONFIG.heartbeat_interval); })();