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
This commit is contained in:
direkturcrypto
2026-03-01 19:07:57 +07:00
parent f31f257da5
commit 0dc8024ceb
5 changed files with 114 additions and 103 deletions
+8
View File
@@ -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
+16 -1
View File
@@ -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 || '',
};
+9 -3
View File
@@ -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);
+51 -48
View File
@@ -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:4022:40, 03:4006:10
* ETH: 11:4015:40, 16:4019:40
* SOL: 09:4012:40, 21:4023:40
* XRP: 18:4020:40, 08:4009: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;
}
+30 -51
View File
@@ -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),
});