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:
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
+30
-51
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user