diff --git a/package-lock.json b/package-lock.json index 636a1c5..8b443fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "blessed": "^0.1.81", "dotenv": "^16.4.7", "ethers": "^5.8.0", + "global-agent": "^4.1.2", "https-proxy-agent": "^7.0.6", "undici": "^7.22.0", "ws": "^8.19.0" @@ -1107,6 +1108,40 @@ } } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1208,6 +1243,18 @@ "node": ">= 0.4" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ethereum-cryptography": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", @@ -1405,6 +1452,37 @@ "node": ">= 6" } }, + "node_modules/global-agent": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.2.tgz", + "integrity": "sha512-CvUwIRwmDHu33Y71P8v1Pr8yo5/U2inzrdRNEtPdiYKBd8J4oQ6aDkt9WqZhMQDtAcTAl+akDTHssRYaBTTlhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1427,6 +1505,18 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1575,6 +1665,21 @@ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", "license": "MIT" }, + "node_modules/matcher": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1684,6 +1789,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1733,7 +1847,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1742,6 +1855,21 @@ "node": ">=10" } }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1822,6 +1950,18 @@ "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", "license": "Unlicense" }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/package.json b/package.json index 9986f31..0d7238c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "blessed": "^0.1.81", "dotenv": "^16.4.7", "ethers": "^5.8.0", + "global-agent": "^4.1.2", "https-proxy-agent": "^7.0.6", "undici": "^7.22.0", "ws": "^8.19.0" diff --git a/src/sniper.js b/src/sniper.js index f9958bb..e4b4378 100644 --- a/src/sniper.js +++ b/src/sniper.js @@ -9,9 +9,6 @@ * For the TUI dashboard version, use: npm run sniper-tui */ -// Load proxy patch BEFORE any other imports (must patch https before axios is loaded) -import './utils/proxy-patch.cjs'; - import { validateMMConfig } from './config/index.js'; import config from './config/index.js'; import logger from './utils/logger.js'; @@ -21,6 +18,9 @@ import { executeSnipe } from './services/sniperExecutor.js'; import { redeemSniperPositions } from './services/ctf.js'; import { getSchedule, isAssetInSession, getNextSessionInfo } from './services/schedule.js'; +// Set proxy before any network calls +import './utils/proxy-patch.cjs'; + // ── Validate config ──────────────────────────────────────────────────────────── try { diff --git a/src/utils/proxy-patch.cjs b/src/utils/proxy-patch.cjs index 4f1cc7d..5ce8d05 100644 --- a/src/utils/proxy-patch.cjs +++ b/src/utils/proxy-patch.cjs @@ -1,261 +1,129 @@ /** * proxy-patch.cjs * - * Strategy: Patch https.request at the lowest level with domain filtering. - * This affects ALL HTTPS requests, but we only route Polymarket domains through proxy. - * - * RPC Polygon calls are NOT affected because: - * - They go to polygon-rpc.com (not in whitelist) - * - They use ethers.js JsonRpcProvider which uses fetch or its own transport + * Simple proxy patch using https-proxy-agent. + * Only routes Polymarket domains through proxy. */ const PROXY_URL = process.env.PROXY_URL || ''; if (PROXY_URL) { + const { HttpsProxyAgent } = require('https-proxy-agent'); const https = require('https'); const http = require('http'); - const { URL } = require('url'); - const net = require('net'); - const tls = require('tls'); - const proxyUrl = new URL(PROXY_URL); - const proxyHost = proxyUrl.hostname; - const proxyPort = parseInt(proxyUrl.port) || (proxyUrl.protocol === 'https:' ? 443 : 80); - const proxyAuth = (proxyUrl.username || proxyUrl.password) - ? Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`).toString('base64') - : null; + const agent = new HttpsProxyAgent(PROXY_URL); - // Polymarket domains ONLY - const POLY_HOSTS = new Set([ + // Polymarket domains + const POLY_DOMAINS = [ 'polymarket.com', - 'www.polymarket.com', 'clob.polymarket.com', 'gamma-api.polymarket.com', 'data-api.polymarket.com', - ]); + ]; function shouldProxy(hostname) { - // Check exact match or subdomain - if (POLY_HOSTS.has(hostname)) return true; - for (const host of POLY_HOSTS) { - if (hostname.endsWith('.' + host)) return true; + if (!hostname) return false; + for (const domain of POLY_DOMAINS) { + if (hostname === domain || hostname.endsWith('.' + domain)) { + return true; + } } return false; } - // Store original request + // Store originals const originalHttpsRequest = https.request; const originalHttpRequest = http.request; - // Create proxy-aware https.request + // Patch https.request https.request = function(options, callback) { let hostname; if (typeof options === 'string') { try { - hostname = new URL(options).hostname; + const url = new URL(options); + hostname = url.hostname; } catch { return originalHttpsRequest.apply(https, arguments); } - } else if (options.hostname || options.host) { - hostname = options.hostname || (typeof options.host === 'string' ? options.host.split(':')[0] : options.host); + } else { + hostname = options.hostname || options.host; + if (typeof hostname === 'string' && hostname.includes(':')) { + hostname = hostname.split(':')[0]; + } } - // If not Polymarket, use direct connection - if (!hostname || !shouldProxy(hostname)) { + // Only proxy Polymarket + if (!shouldProxy(hostname)) { return originalHttpsRequest.apply(https, arguments); } - // For Polymarket: create CONNECT tunnel through proxy - const targetHost = hostname; - const targetPort = 443; - - // Build CONNECT request headers - const connectHeaders = { - 'Host': `${targetHost}:${targetPort}`, - }; - if (proxyAuth) { - connectHeaders['Proxy-Authorization'] = `Basic ${proxyAuth}`; - } - - // Create socket connection through proxy - const connectOptions = { - host: proxyHost, - port: proxyPort, - method: 'CONNECT', - path: `${targetHost}:${targetPort}`, - headers: connectHeaders, - }; - - // Make CONNECT request to proxy - const connectReq = originalHttpRequest(connectOptions); - - // Return a promise-like request that waits for tunnel - const deferredReq = { - on: function(event, handler) { - if (event === 'response' || event === 'error' || event === 'timeout') { - this._deferredHandlers = this._deferredHandlers || {}; - this._deferredHandlers[event] = handler; - } - return this; - }, - once: function(event, handler) { - return this.on(event, handler); - }, - write: function(chunk) { - if (this._realRequest) { - this._realRequest.write(chunk); - } else { - this._buffer = this._buffer || []; - this._buffer.push(chunk); - } - return true; - }, - end: function(chunk) { - if (chunk) this.write(chunk); - if (this._realRequest) { - this._realRequest.end(); - } else { - this._ended = true; - } - return this; - }, - setTimeout: function(ms) { - this._timeout = ms; - if (this._realRequest) { - this._realRequest.setTimeout(ms); - } - return this; - }, - setHeader: function(name, value) { - this._headers = this._headers || {}; - this._headers[name] = value; - return this; - }, - abort: function() { - if (this._realRequest) { - this._realRequest.abort(); - } - if (this._connectReq) { - this._connectReq.abort(); - } - } - }; - - connectReq.on('connect', (res, socket) => { - if (res.statusCode !== 200) { - const err = new Error(`Proxy CONNECT failed: ${res.statusCode}`); - if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.error) { - deferredReq._deferredHandlers.error(err); - } - return; - } - - // Wrap socket in TLS - const tlsOptions = { - socket: socket, - servername: targetHost, - rejectUnauthorized: options.rejectUnauthorized !== false, + // Apply proxy agent + if (typeof options === 'string') { + // Convert string URL to options object with agent + const url = new URL(options); + const newOptions = { + protocol: url.protocol, + hostname: url.hostname, + port: url.port || 443, + path: url.pathname + url.search, + method: 'GET', + headers: {}, + agent: agent, }; - const tlsSocket = tls.connect(tlsOptions); - - // Now make actual HTTPS request through tunnel - const requestOptions = typeof options === 'string' - ? { ...new URL(options), createConnection: () => tlsSocket } - : { ...options, createConnection: () => tlsSocket }; - - const realReq = originalHttpsRequest(requestOptions, (res) => { - if (callback) callback(res); - if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.response) { - deferredReq._deferredHandlers.response(res); - } - }); - - // Apply deferred operations - if (deferredReq._headers) { - for (const [name, value] of Object.entries(deferredReq._headers)) { - realReq.setHeader(name, value); - } + if (callback) { + return originalHttpsRequest.call(https, newOptions, callback); } - if (deferredReq._timeout) { - realReq.setTimeout(deferredReq._timeout); - } - if (deferredReq._buffer) { - for (const chunk of deferredReq._buffer) { - realReq.write(chunk); - } - } - if (deferredReq._ended) { - realReq.end(); - } - - // Forward events - realReq.on('error', (err) => { - if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.error) { - deferredReq._deferredHandlers.error(err); - } - }); - realReq.on('timeout', () => { - if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.timeout) { - deferredReq._deferredHandlers.timeout(); - } - }); - - deferredReq._realRequest = realReq; - deferredReq._connectReq = connectReq; - }); - - connectReq.on('error', (err) => { - if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.error) { - deferredReq._deferredHandlers.error(err); - } - }); - - connectReq.end(); - - return deferredReq; + return originalHttpsRequest.call(https, newOptions); + } else { + // Options object - just add agent + options.agent = agent; + return originalHttpsRequest.apply(https, arguments); + } }; - // Also patch http.request for completeness (though CLOB uses HTTPS) + // Patch http.request (for HTTP proxy connections) http.request = function(options, callback) { let hostname; if (typeof options === 'string') { try { - hostname = new URL(options).hostname; + const url = new URL(options); + hostname = url.hostname; } catch { return originalHttpRequest.apply(http, arguments); } - } else if (options.hostname || options.host) { - hostname = options.hostname || (typeof options.host === 'string' ? options.host.split(':')[0] : options.host); + } else { + hostname = options.hostname || options.host; } - // Only proxy Polymarket domains - if (!hostname || !shouldProxy(hostname)) { + // Only proxy Polymarket + if (!shouldProxy(hostname)) { return originalHttpRequest.apply(http, arguments); } - // For HTTP, route through proxy directly - const targetUrl = typeof options === 'string' ? new URL(options) : null; - const path = targetUrl ? targetUrl.pathname + targetUrl.search : (options.path || '/'); - - const proxyOptions = { - host: proxyHost, - port: proxyPort, - method: options.method || 'GET', - path: path, - headers: { ...(options.headers || {}) }, - }; - - if (proxyAuth) { - proxyOptions.headers['Proxy-Authorization'] = `Basic ${proxyAuth}`; + // Apply same logic for HTTP + if (typeof options === 'string') { + const url = new URL(options); + const newOptions = { + protocol: url.protocol, + hostname: url.hostname, + port: url.port || 80, + path: url.pathname + url.search, + method: 'GET', + headers: {}, + agent: agent, + }; + if (callback) { + return originalHttpRequest.call(http, newOptions, callback); + } + return originalHttpRequest.call(http, newOptions); + } else { + options.agent = agent; + return originalHttpRequest.apply(http, arguments); } - - return originalHttpRequest.call(http, proxyOptions, callback); }; - console.log(`[proxy-patch] HTTPS/HTTP CONNECT tunnel active for Polymarket only`); - console.log(`[proxy-patch] RPC Polygon and other domains use direct connection`); -} else { - console.log('[proxy-patch] No PROXY_URL set, skipping patch'); + console.log('[proxy-patch] Proxy active for Polymarket domains'); }