From 0dc8024ceb5b3ba89f31069486fb866615173a72 Mon Sep 17 00:00:00 2001 From: direkturcrypto Date: Sun, 1 Mar 2026 19:07:57 +0700 Subject: [PATCH] fix: proxy uses undici ProxyAgent for fetch, schedule moved to .env, verbose redeem logging - Fix proxy: native fetch needs undici ProxyAgent with dispatcher option (https-proxy-agent only works with axios, not Node's built-in fetch) - Move schedule from hardcoded to .env: SNIPER_SCHEDULE_BTC=19:40-22:40,03:40-06:10 - Add verbose logging to redeemSniperPositions for debugging - Config dynamically reads all SNIPER_SCHEDULE_* env vars --- .env.example | 8 ++++ src/config/index.js | 17 ++++++- src/services/ctf.js | 12 +++-- src/services/schedule.js | 99 +++++++++++++++++++++------------------- src/utils/proxy.js | 81 ++++++++++++-------------------- 5 files changed, 114 insertions(+), 103 deletions(-) diff --git a/.env.example b/.env.example index 51403cd..8d3dfc3 100644 --- a/.env.example +++ b/.env.example @@ -148,6 +148,14 @@ SNIPER_PRICE=0.01 # At $0.01/share: 5 shares = $0.05 per side, $0.10 per market SNIPER_SHARES=5 +# ── Sniper Session Schedule (all times UTC+8) ────────────── +# Format: HH:MM-HH:MM,HH:MM-HH:MM (comma-separated sessions) +# Assets without a schedule entry are always active. +SNIPER_SCHEDULE_BTC=19:40-22:40,03:40-06:10 +SNIPER_SCHEDULE_ETH=11:40-15:40,16:40-19:40 +SNIPER_SCHEDULE_SOL=09:40-12:40,21:40-23:40 +SNIPER_SCHEDULE_XRP=18:40-20:40,08:40-09:50 + # ───────────────────────────────────────────── # PROXY (Polymarket API only, NOT Polygon RPC) # Supports HTTP/HTTPS/SOCKS5 proxies diff --git a/src/config/index.js b/src/config/index.js index 7ad80ca..0f1cb4d 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -82,8 +82,23 @@ const config = { sniperPrice: parseFloat(process.env.SNIPER_PRICE || '0.01'), // $ per share sniperShares: parseFloat(process.env.SNIPER_SHARES || '5'), // shares per side + // ── Sniper Schedule (UTC+8) ──────────────────────────────────── + // Per-asset session windows. Format: SNIPER_SCHEDULE_{ASSET}=HH:MM-HH:MM,HH:MM-HH:MM + // Assets without a schedule are always active. + sniperSchedule: (() => { + const schedule = {}; + const prefix = 'SNIPER_SCHEDULE_'; + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith(prefix) && value) { + const asset = key.slice(prefix.length).toLowerCase(); + schedule[asset] = value; + } + } + return schedule; + })(), + // ── Proxy (Polymarket API only, NOT Polygon RPC) ────────────── - // Supports HTTP/HTTPS/SOCKS5. Example: http://user:pass@host:port + // Supports HTTP/HTTPS. Example: http://user:pass@host:port proxyUrl: process.env.PROXY_URL || '', }; diff --git a/src/services/ctf.js b/src/services/ctf.js index 8482752..6ceba2c 100644 --- a/src/services/ctf.js +++ b/src/services/ctf.js @@ -560,13 +560,19 @@ export async function redeemSniperPositions() { let dataPositions = []; try { const resp = await proxyFetch(`${config.dataHost}/positions?user=${config.proxyWallet}`); - if (resp.ok) dataPositions = await resp.json(); + if (!resp.ok) { + logger.warn(`SNIPER redeemer: Data API returned ${resp.status} — will retry`); + return; + } + dataPositions = await resp.json(); if (!Array.isArray(dataPositions)) dataPositions = []; - } catch { - return; // silent — will retry next interval + } catch (err) { + logger.warn(`SNIPER redeemer: Data API fetch failed — ${err.message}`); + return; } if (dataPositions.length === 0) return; + logger.info(`SNIPER redeemer: checking ${dataPositions.length} position(s)...`); const provider = await getPolygonProvider(); const ctf = new ethers.Contract(CTF_ADDRESS, CTF_ABI, provider); diff --git a/src/services/schedule.js b/src/services/schedule.js index 1549285..2b1fda7 100644 --- a/src/services/schedule.js +++ b/src/services/schedule.js @@ -1,45 +1,50 @@ /** * schedule.js - * Hardcoded trading session schedule for the sniper bot. + * Trading session schedule for the sniper bot. + * Reads schedule from .env via config (SNIPER_SCHEDULE_*). * - * All times are defined in UTC+8 and converted to UTC internally. + * All times are in UTC+8 and converted to UTC internally. * Assets outside their session window are skipped by the sniper detector. * - * Schedule (UTC+8): - * BTC: 19:40–22:40, 03:40–06:10 - * ETH: 11:40–15:40, 16:40–19:40 - * SOL: 09:40–12:40, 21:40–23:40 - * XRP: 18:40–20:40, 08:40–09:50 + * .env format per asset: + * SNIPER_SCHEDULE_BTC=19:40-22:40,03:40-06:10 + * SNIPER_SCHEDULE_ETH=11:40-15:40,16:40-19:40 */ +import config from '../config/index.js'; import logger from '../utils/logger.js'; // UTC+8 offset in hours const UTC8_OFFSET = 8; /** - * Schedule definition in UTC+8 times. - * Each asset has an array of sessions with { startUtc8, endUtc8 } strings (HH:MM). - * Internally we store startMinUTC and endMinUTC for fast comparison. + * Parse schedule string from .env. + * Format: "HH:MM-HH:MM,HH:MM-HH:MM" + * Returns: [{ startUtc8, endUtc8, startMin, endMin }] */ -const SCHEDULE_UTC8 = { - btc: [ - { startUtc8: '19:40', endUtc8: '22:40' }, - { startUtc8: '03:40', endUtc8: '06:10' }, - ], - eth: [ - { startUtc8: '11:40', endUtc8: '15:40' }, - { startUtc8: '16:40', endUtc8: '19:40' }, - ], - sol: [ - { startUtc8: '09:40', endUtc8: '12:40' }, - { startUtc8: '21:40', endUtc8: '23:40' }, - ], - xrp: [ - { startUtc8: '18:40', endUtc8: '20:40' }, - { startUtc8: '08:40', endUtc8: '09:50' }, - ], -}; +function parseScheduleString(str) { + if (!str || !str.trim()) return null; + + const sessions = []; + const parts = str.split(',').map((s) => s.trim()).filter(Boolean); + + for (const part of parts) { + const match = part.match(/^(\d{1,2}:\d{2})\s*[-–]\s*(\d{1,2}:\d{2})$/); + if (!match) { + logger.warn(`SCHEDULE: invalid session format "${part}" — expected HH:MM-HH:MM`); + continue; + } + const [, startUtc8, endUtc8] = match; + sessions.push({ + startUtc8, + endUtc8, + startMin: utc8ToUtcMinutes(startUtc8), + endMin: utc8ToUtcMinutes(endUtc8), + }); + } + + return sessions.length > 0 ? sessions : null; +} /** * Convert HH:MM in UTC+8 to minutes-since-midnight in UTC. @@ -53,15 +58,19 @@ function utc8ToUtcMinutes(hhmm) { return totalMin; } -// Pre-compute UTC ranges for fast lookup -const SCHEDULE_UTC = {}; -for (const [asset, sessions] of Object.entries(SCHEDULE_UTC8)) { - SCHEDULE_UTC[asset] = sessions.map((s) => ({ - startUtc8: s.startUtc8, - endUtc8: s.endUtc8, - startMin: utc8ToUtcMinutes(s.startUtc8), - endMin: utc8ToUtcMinutes(s.endUtc8), - })); +// Build schedule from config (reads SNIPER_SCHEDULE_* from .env) +const SCHEDULE = {}; +const SCHEDULE_DISPLAY = {}; + +for (const [asset, raw] of Object.entries(config.sniperSchedule || {})) { + const sessions = parseScheduleString(raw); + if (sessions) { + SCHEDULE[asset] = sessions; + SCHEDULE_DISPLAY[asset] = sessions.map((s) => ({ + startUtc8: s.startUtc8, + endUtc8: s.endUtc8, + })); + } } /** @@ -78,10 +87,8 @@ function nowMinutesUTC() { */ function inRange(nowMin, startMin, endMin) { if (startMin <= endMin) { - // Normal range (e.g. 11:40 – 14:40) return nowMin >= startMin && nowMin < endMin; } else { - // Overnight wrap (e.g. 19:40 – 22:10 where UTC wraps past midnight) return nowMin >= startMin || nowMin < endMin; } } @@ -91,7 +98,7 @@ function inRange(nowMin, startMin, endMin) { * Returns true if asset has no schedule (always active). */ export function isAssetInSession(asset) { - const sessions = SCHEDULE_UTC[asset.toLowerCase()]; + const sessions = SCHEDULE[asset.toLowerCase()]; if (!sessions) return true; // no schedule = always active const now = nowMinutesUTC(); @@ -103,19 +110,16 @@ export function isAssetInSession(asset) { * Returns string like "2h 15m" or null if currently in session. */ export function getNextSessionInfo(asset) { - const sessions = SCHEDULE_UTC[asset.toLowerCase()]; + const sessions = SCHEDULE[asset.toLowerCase()]; if (!sessions) return null; const now = nowMinutesUTC(); - - // If currently in session, return null if (sessions.some((s) => inRange(now, s.startMin, s.endMin))) return null; - // Find the nearest upcoming session start let minWait = Infinity; for (const s of sessions) { let wait = s.startMin - now; - if (wait <= 0) wait += 1440; // wrap to next day + if (wait <= 0) wait += 1440; if (wait < minWait) minWait = wait; } @@ -128,9 +132,8 @@ export function getNextSessionInfo(asset) { } /** - * Get the full schedule definition (for display in TUI / console). - * Returns the SCHEDULE_UTC8 object with startUtc8 and endUtc8 strings. + * Get the schedule for display (UTC+8 strings). */ export function getSchedule() { - return SCHEDULE_UTC8; + return SCHEDULE_DISPLAY; } diff --git a/src/utils/proxy.js b/src/utils/proxy.js index bc1fdb5..908a9ea 100644 --- a/src/utils/proxy.js +++ b/src/utils/proxy.js @@ -3,35 +3,20 @@ * Proxy support for Polymarket API calls only. * * - CLOB API: uses axios internally (via @polymarket/clob-client) → - * we set axios.defaults to use the proxy agent globally. - * - Gamma / Data API: uses native fetch → we provide proxyFetch() wrapper. + * we set axios.defaults.httpAgent/httpsAgent via https-proxy-agent. + * - Gamma / Data API: uses native fetch (undici) → + * we use undici.ProxyAgent with the `dispatcher` option. * - Polygon RPC: NOT proxied (separate ethers.js provider). * - * Set PROXY_URL in .env to enable. Supports HTTP/HTTPS/SOCKS5 proxies. + * Set PROXY_URL in .env to enable. Supports HTTP/HTTPS proxies. * Example: PROXY_URL=http://user:pass@proxy.example.com:8080 */ import config from '../config/index.js'; import logger from './logger.js'; -let proxyAgent = null; - -/** - * Initialize the proxy agent from config.proxyUrl. - * Returns the agent or null if no proxy is configured. - */ -async function createAgent() { - if (!config.proxyUrl) return null; - - try { - const { HttpsProxyAgent } = await import('https-proxy-agent'); - return new HttpsProxyAgent(config.proxyUrl); - } catch (err) { - logger.error(`Failed to create proxy agent: ${err.message}`); - logger.error('Make sure https-proxy-agent is installed: npm i https-proxy-agent'); - return null; - } -} +let axiosAgent = null; // https-proxy-agent for axios (CLOB client) +let fetchDispatcher = null; // undici ProxyAgent for native fetch /** * Set up axios defaults so that the @polymarket/clob-client's @@ -44,43 +29,41 @@ export async function setupAxiosProxy() { return; } - proxyAgent = await createAgent(); - if (!proxyAgent) return; - try { + // 1. Setup axios proxy (for CLOB client) + const { HttpsProxyAgent } = await import('https-proxy-agent'); + axiosAgent = new HttpsProxyAgent(config.proxyUrl); + const axiosModule = await import('axios'); const axios = axiosModule.default || axiosModule; - - // Disable axios built-in proxy (env vars) and use our agent instead axios.defaults.proxy = false; - axios.defaults.httpAgent = proxyAgent; - axios.defaults.httpsAgent = proxyAgent; - + axios.defaults.httpAgent = axiosAgent; + axios.defaults.httpsAgent = axiosAgent; logger.info(`Axios proxy configured → ${maskProxyUrl(config.proxyUrl)}`); } catch (err) { logger.error(`Failed to configure axios proxy: ${err.message}`); + logger.error('Make sure https-proxy-agent is installed: npm i https-proxy-agent'); + } + + try { + // 2. Setup undici ProxyAgent (for native fetch) + const undici = await import('undici'); + fetchDispatcher = new undici.ProxyAgent(config.proxyUrl); + logger.info(`Fetch proxy configured → ${maskProxyUrl(config.proxyUrl)}`); + } catch (err) { + logger.error(`Failed to configure fetch proxy: ${err.message}`); } } -/** - * Get the proxy agent for use with native fetch(). - */ -export function getProxyAgent() { - return proxyAgent; -} - /** * Proxy-aware fetch wrapper. - * Drop-in replacement for global fetch() — injects the proxy agent - * when PROXY_URL is configured. + * Drop-in replacement for global fetch() — uses undici ProxyAgent + * as dispatcher when PROXY_URL is configured. * Use this for Gamma API and Data API calls. */ export async function proxyFetch(url, opts = {}) { - if (proxyAgent) { - // Node 18+ fetch supports the 'dispatcher' option for undici, - // but the standard approach for http/https agent is via the agent option. - // We use the node-fetch compatible approach via the agent option. - opts.agent = proxyAgent; + if (fetchDispatcher) { + opts.dispatcher = fetchDispatcher; } return fetch(url, opts); } @@ -95,17 +78,13 @@ export async function testProxy() { logger.info(`Testing proxy connection → ${maskProxyUrl(config.proxyUrl)} ...`); try { - // Ensure agent is created - if (!proxyAgent) { - proxyAgent = await createAgent(); - } - if (!proxyAgent) { - throw new Error('Proxy agent creation failed'); + if (!fetchDispatcher) { + throw new Error('Proxy dispatcher not initialized'); } - // Test with a simple GET to the CLOB time endpoint + // Test with a simple GET to the CLOB time endpoint via proxied fetch const resp = await fetch(`${config.clobHost}/time`, { - agent: proxyAgent, + dispatcher: fetchDispatcher, signal: AbortSignal.timeout(15000), });