Files
binance-alpha-farm-agent/agent.user.js

291 lines
10 KiB
JavaScript

// ==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);
})();