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
|
# At $0.01/share: 5 shares = $0.05 per side, $0.10 per market
|
||||||
SNIPER_SHARES=5
|
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)
|
# PROXY (Polymarket API only, NOT Polygon RPC)
|
||||||
# Supports HTTP/HTTPS/SOCKS5 proxies
|
# Supports HTTP/HTTPS/SOCKS5 proxies
|
||||||
|
|||||||
+16
-1
@@ -82,8 +82,23 @@ const config = {
|
|||||||
sniperPrice: parseFloat(process.env.SNIPER_PRICE || '0.01'), // $ per share
|
sniperPrice: parseFloat(process.env.SNIPER_PRICE || '0.01'), // $ per share
|
||||||
sniperShares: parseFloat(process.env.SNIPER_SHARES || '5'), // shares per side
|
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) ──────────────
|
// ── 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 || '',
|
proxyUrl: process.env.PROXY_URL || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -560,13 +560,19 @@ export async function redeemSniperPositions() {
|
|||||||
let dataPositions = [];
|
let dataPositions = [];
|
||||||
try {
|
try {
|
||||||
const resp = await proxyFetch(`${config.dataHost}/positions?user=${config.proxyWallet}`);
|
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 = [];
|
if (!Array.isArray(dataPositions)) dataPositions = [];
|
||||||
} catch {
|
} catch (err) {
|
||||||
return; // silent — will retry next interval
|
logger.warn(`SNIPER redeemer: Data API fetch failed — ${err.message}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataPositions.length === 0) return;
|
if (dataPositions.length === 0) return;
|
||||||
|
logger.info(`SNIPER redeemer: checking ${dataPositions.length} position(s)...`);
|
||||||
|
|
||||||
const provider = await getPolygonProvider();
|
const provider = await getPolygonProvider();
|
||||||
const ctf = new ethers.Contract(CTF_ADDRESS, CTF_ABI, provider);
|
const ctf = new ethers.Contract(CTF_ADDRESS, CTF_ABI, provider);
|
||||||
|
|||||||
+51
-48
@@ -1,45 +1,50 @@
|
|||||||
/**
|
/**
|
||||||
* schedule.js
|
* 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.
|
* Assets outside their session window are skipped by the sniper detector.
|
||||||
*
|
*
|
||||||
* Schedule (UTC+8):
|
* .env format per asset:
|
||||||
* BTC: 19:40–22:40, 03:40–06:10
|
* SNIPER_SCHEDULE_BTC=19:40-22:40,03:40-06:10
|
||||||
* ETH: 11:40–15:40, 16:40–19:40
|
* SNIPER_SCHEDULE_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
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import config from '../config/index.js';
|
||||||
import logger from '../utils/logger.js';
|
import logger from '../utils/logger.js';
|
||||||
|
|
||||||
// UTC+8 offset in hours
|
// UTC+8 offset in hours
|
||||||
const UTC8_OFFSET = 8;
|
const UTC8_OFFSET = 8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule definition in UTC+8 times.
|
* Parse schedule string from .env.
|
||||||
* Each asset has an array of sessions with { startUtc8, endUtc8 } strings (HH:MM).
|
* Format: "HH:MM-HH:MM,HH:MM-HH:MM"
|
||||||
* Internally we store startMinUTC and endMinUTC for fast comparison.
|
* Returns: [{ startUtc8, endUtc8, startMin, endMin }]
|
||||||
*/
|
*/
|
||||||
const SCHEDULE_UTC8 = {
|
function parseScheduleString(str) {
|
||||||
btc: [
|
if (!str || !str.trim()) return null;
|
||||||
{ startUtc8: '19:40', endUtc8: '22:40' },
|
|
||||||
{ startUtc8: '03:40', endUtc8: '06:10' },
|
const sessions = [];
|
||||||
],
|
const parts = str.split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
eth: [
|
|
||||||
{ startUtc8: '11:40', endUtc8: '15:40' },
|
for (const part of parts) {
|
||||||
{ startUtc8: '16:40', endUtc8: '19:40' },
|
const match = part.match(/^(\d{1,2}:\d{2})\s*[-–]\s*(\d{1,2}:\d{2})$/);
|
||||||
],
|
if (!match) {
|
||||||
sol: [
|
logger.warn(`SCHEDULE: invalid session format "${part}" — expected HH:MM-HH:MM`);
|
||||||
{ startUtc8: '09:40', endUtc8: '12:40' },
|
continue;
|
||||||
{ startUtc8: '21:40', endUtc8: '23:40' },
|
}
|
||||||
],
|
const [, startUtc8, endUtc8] = match;
|
||||||
xrp: [
|
sessions.push({
|
||||||
{ startUtc8: '18:40', endUtc8: '20:40' },
|
startUtc8,
|
||||||
{ startUtc8: '08:40', endUtc8: '09:50' },
|
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.
|
* Convert HH:MM in UTC+8 to minutes-since-midnight in UTC.
|
||||||
@@ -53,15 +58,19 @@ function utc8ToUtcMinutes(hhmm) {
|
|||||||
return totalMin;
|
return totalMin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-compute UTC ranges for fast lookup
|
// Build schedule from config (reads SNIPER_SCHEDULE_* from .env)
|
||||||
const SCHEDULE_UTC = {};
|
const SCHEDULE = {};
|
||||||
for (const [asset, sessions] of Object.entries(SCHEDULE_UTC8)) {
|
const SCHEDULE_DISPLAY = {};
|
||||||
SCHEDULE_UTC[asset] = sessions.map((s) => ({
|
|
||||||
startUtc8: s.startUtc8,
|
for (const [asset, raw] of Object.entries(config.sniperSchedule || {})) {
|
||||||
endUtc8: s.endUtc8,
|
const sessions = parseScheduleString(raw);
|
||||||
startMin: utc8ToUtcMinutes(s.startUtc8),
|
if (sessions) {
|
||||||
endMin: utc8ToUtcMinutes(s.endUtc8),
|
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) {
|
function inRange(nowMin, startMin, endMin) {
|
||||||
if (startMin <= endMin) {
|
if (startMin <= endMin) {
|
||||||
// Normal range (e.g. 11:40 – 14:40)
|
|
||||||
return nowMin >= startMin && nowMin < endMin;
|
return nowMin >= startMin && nowMin < endMin;
|
||||||
} else {
|
} else {
|
||||||
// Overnight wrap (e.g. 19:40 – 22:10 where UTC wraps past midnight)
|
|
||||||
return nowMin >= startMin || nowMin < endMin;
|
return nowMin >= startMin || nowMin < endMin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +98,7 @@ function inRange(nowMin, startMin, endMin) {
|
|||||||
* Returns true if asset has no schedule (always active).
|
* Returns true if asset has no schedule (always active).
|
||||||
*/
|
*/
|
||||||
export function isAssetInSession(asset) {
|
export function isAssetInSession(asset) {
|
||||||
const sessions = SCHEDULE_UTC[asset.toLowerCase()];
|
const sessions = SCHEDULE[asset.toLowerCase()];
|
||||||
if (!sessions) return true; // no schedule = always active
|
if (!sessions) return true; // no schedule = always active
|
||||||
|
|
||||||
const now = nowMinutesUTC();
|
const now = nowMinutesUTC();
|
||||||
@@ -103,19 +110,16 @@ export function isAssetInSession(asset) {
|
|||||||
* Returns string like "2h 15m" or null if currently in session.
|
* Returns string like "2h 15m" or null if currently in session.
|
||||||
*/
|
*/
|
||||||
export function getNextSessionInfo(asset) {
|
export function getNextSessionInfo(asset) {
|
||||||
const sessions = SCHEDULE_UTC[asset.toLowerCase()];
|
const sessions = SCHEDULE[asset.toLowerCase()];
|
||||||
if (!sessions) return null;
|
if (!sessions) return null;
|
||||||
|
|
||||||
const now = nowMinutesUTC();
|
const now = nowMinutesUTC();
|
||||||
|
|
||||||
// If currently in session, return null
|
|
||||||
if (sessions.some((s) => inRange(now, s.startMin, s.endMin))) return null;
|
if (sessions.some((s) => inRange(now, s.startMin, s.endMin))) return null;
|
||||||
|
|
||||||
// Find the nearest upcoming session start
|
|
||||||
let minWait = Infinity;
|
let minWait = Infinity;
|
||||||
for (const s of sessions) {
|
for (const s of sessions) {
|
||||||
let wait = s.startMin - now;
|
let wait = s.startMin - now;
|
||||||
if (wait <= 0) wait += 1440; // wrap to next day
|
if (wait <= 0) wait += 1440;
|
||||||
if (wait < minWait) minWait = wait;
|
if (wait < minWait) minWait = wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,9 +132,8 @@ export function getNextSessionInfo(asset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full schedule definition (for display in TUI / console).
|
* Get the schedule for display (UTC+8 strings).
|
||||||
* Returns the SCHEDULE_UTC8 object with startUtc8 and endUtc8 strings.
|
|
||||||
*/
|
*/
|
||||||
export function getSchedule() {
|
export function getSchedule() {
|
||||||
return SCHEDULE_UTC8;
|
return SCHEDULE_DISPLAY;
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-51
@@ -3,35 +3,20 @@
|
|||||||
* Proxy support for Polymarket API calls only.
|
* Proxy support for Polymarket API calls only.
|
||||||
*
|
*
|
||||||
* - CLOB API: uses axios internally (via @polymarket/clob-client) →
|
* - CLOB API: uses axios internally (via @polymarket/clob-client) →
|
||||||
* we set axios.defaults to use the proxy agent globally.
|
* we set axios.defaults.httpAgent/httpsAgent via https-proxy-agent.
|
||||||
* - Gamma / Data API: uses native fetch → we provide proxyFetch() wrapper.
|
* - Gamma / Data API: uses native fetch (undici) →
|
||||||
|
* we use undici.ProxyAgent with the `dispatcher` option.
|
||||||
* - Polygon RPC: NOT proxied (separate ethers.js provider).
|
* - 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
|
* Example: PROXY_URL=http://user:pass@proxy.example.com:8080
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import config from '../config/index.js';
|
import config from '../config/index.js';
|
||||||
import logger from './logger.js';
|
import logger from './logger.js';
|
||||||
|
|
||||||
let proxyAgent = null;
|
let axiosAgent = null; // https-proxy-agent for axios (CLOB client)
|
||||||
|
let fetchDispatcher = null; // undici ProxyAgent for native fetch
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up axios defaults so that the @polymarket/clob-client's
|
* Set up axios defaults so that the @polymarket/clob-client's
|
||||||
@@ -44,43 +29,41 @@ export async function setupAxiosProxy() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyAgent = await createAgent();
|
|
||||||
if (!proxyAgent) return;
|
|
||||||
|
|
||||||
try {
|
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 axiosModule = await import('axios');
|
||||||
const axios = axiosModule.default || axiosModule;
|
const axios = axiosModule.default || axiosModule;
|
||||||
|
|
||||||
// Disable axios built-in proxy (env vars) and use our agent instead
|
|
||||||
axios.defaults.proxy = false;
|
axios.defaults.proxy = false;
|
||||||
axios.defaults.httpAgent = proxyAgent;
|
axios.defaults.httpAgent = axiosAgent;
|
||||||
axios.defaults.httpsAgent = proxyAgent;
|
axios.defaults.httpsAgent = axiosAgent;
|
||||||
|
|
||||||
logger.info(`Axios proxy configured → ${maskProxyUrl(config.proxyUrl)}`);
|
logger.info(`Axios proxy configured → ${maskProxyUrl(config.proxyUrl)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to configure axios proxy: ${err.message}`);
|
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.
|
* Proxy-aware fetch wrapper.
|
||||||
* Drop-in replacement for global fetch() — injects the proxy agent
|
* Drop-in replacement for global fetch() — uses undici ProxyAgent
|
||||||
* when PROXY_URL is configured.
|
* as dispatcher when PROXY_URL is configured.
|
||||||
* Use this for Gamma API and Data API calls.
|
* Use this for Gamma API and Data API calls.
|
||||||
*/
|
*/
|
||||||
export async function proxyFetch(url, opts = {}) {
|
export async function proxyFetch(url, opts = {}) {
|
||||||
if (proxyAgent) {
|
if (fetchDispatcher) {
|
||||||
// Node 18+ fetch supports the 'dispatcher' option for undici,
|
opts.dispatcher = fetchDispatcher;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
return fetch(url, opts);
|
return fetch(url, opts);
|
||||||
}
|
}
|
||||||
@@ -95,17 +78,13 @@ export async function testProxy() {
|
|||||||
logger.info(`Testing proxy connection → ${maskProxyUrl(config.proxyUrl)} ...`);
|
logger.info(`Testing proxy connection → ${maskProxyUrl(config.proxyUrl)} ...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure agent is created
|
if (!fetchDispatcher) {
|
||||||
if (!proxyAgent) {
|
throw new Error('Proxy dispatcher not initialized');
|
||||||
proxyAgent = await createAgent();
|
|
||||||
}
|
|
||||||
if (!proxyAgent) {
|
|
||||||
throw new Error('Proxy agent creation failed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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`, {
|
const resp = await fetch(`${config.clobHost}/time`, {
|
||||||
agent: proxyAgent,
|
dispatcher: fetchDispatcher,
|
||||||
signal: AbortSignal.timeout(15000),
|
signal: AbortSignal.timeout(15000),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user