From f67970c34eb502faa98e00863507533192cae59a Mon Sep 17 00:00:00 2001 From: FrondEnt Date: Fri, 30 Jan 2026 00:22:45 +0200 Subject: [PATCH] Add proxy support (HTTP + WebSocket) --- README.md | 28 ++++++++++ package-lock.json | 104 +++++++++++++++++++++++++++++++++++ package.json | 3 + src/data/binanceWs.js | 3 +- src/data/chainlinkWs.js | 3 +- src/data/polymarketLiveWs.js | 4 +- src/index.js | 3 + src/net/proxy.js | 45 +++++++++++++++ 8 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 src/net/proxy.js diff --git a/README.md b/README.md index de2e017..0a89792 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,34 @@ WSS RPC (optional but recommended for more real-time fallback): - `POLYGON_WSS_URL` (optional) - `POLYGON_WSS_URLS` (optional, comma-separated) +### Proxy support + +The bot supports HTTP(S) proxies for both HTTP requests (fetch) and WebSocket connections. + +Supported env vars (standard): + +- `HTTPS_PROXY` / `https_proxy` +- `HTTP_PROXY` / `http_proxy` +- `ALL_PROXY` / `all_proxy` + +Examples: + +PowerShell: + +```powershell +$env:HTTPS_PROXY = "http://127.0.0.1:8080" +# or +$env:ALL_PROXY = "socks5://127.0.0.1:1080" +``` + +CMD: + +```cmd +set HTTPS_PROXY=http://127.0.0.1:8080 +REM or +set ALL_PROXY=socks5://127.0.0.1:1080 +``` + ## Run ```bash diff --git a/package-lock.json b/package-lock.json index 954b78a..bad5fcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.1.0", "dependencies": { "ethers": "^6.11.1", + "https-proxy-agent": "^7.0.6", + "socks-proxy-agent": "^8.0.5", + "undici": "^6.21.3", "ws": "^8.18.0" } }, @@ -57,6 +60,32 @@ "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "license": "MIT" }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/ethers": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", @@ -106,12 +135,87 @@ } } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "license": "0BSD" }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index cd2dde5..796676e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ }, "dependencies": { "ethers": "^6.11.1", + "https-proxy-agent": "^7.0.6", + "socks-proxy-agent": "^8.0.5", + "undici": "^6.21.3", "ws": "^8.18.0" } } diff --git a/src/data/binanceWs.js b/src/data/binanceWs.js index 3aee0a9..aa4d8d0 100644 --- a/src/data/binanceWs.js +++ b/src/data/binanceWs.js @@ -1,5 +1,6 @@ import WebSocket from "ws"; import { CONFIG } from "../config.js"; +import { wsAgentForUrl } from "../net/proxy.js"; function toNumber(x) { const n = Number(x); @@ -22,7 +23,7 @@ export function startBinanceTradeStream({ symbol = CONFIG.symbol, onUpdate } = { if (closed) return; const url = buildWsUrl(symbol); - ws = new WebSocket(url); + ws = new WebSocket(url, { agent: wsAgentForUrl(url) }); ws.on("open", () => { reconnectMs = 500; diff --git a/src/data/chainlinkWs.js b/src/data/chainlinkWs.js index 4413c49..8a64690 100644 --- a/src/data/chainlinkWs.js +++ b/src/data/chainlinkWs.js @@ -1,6 +1,7 @@ import WebSocket from "ws"; import { ethers } from "ethers"; import { CONFIG } from "../config.js"; +import { wsAgentForUrl } from "../net/proxy.js"; const ANSWER_UPDATED_TOPIC0 = ethers.id("AnswerUpdated(int256,uint256,uint256)"); @@ -55,7 +56,7 @@ export function startChainlinkPriceStream({ const url = wssUrls[urlIndex % wssUrls.length]; urlIndex += 1; - ws = new WebSocket(url); + ws = new WebSocket(url, { agent: wsAgentForUrl(url) }); const send = (obj) => { try { diff --git a/src/data/polymarketLiveWs.js b/src/data/polymarketLiveWs.js index 02bfec0..58a4403 100644 --- a/src/data/polymarketLiveWs.js +++ b/src/data/polymarketLiveWs.js @@ -1,5 +1,6 @@ import WebSocket from "ws"; import { CONFIG } from "../config.js"; +import { wsAgentForUrl } from "../net/proxy.js"; function safeJsonParse(s) { try { @@ -46,7 +47,8 @@ export function startPolymarketChainlinkPriceStream({ if (closed) return; ws = new WebSocket(wsUrl, { - handshakeTimeout: 10_000 + handshakeTimeout: 10_000, + agent: wsAgentForUrl(wsUrl) }); const scheduleReconnect = () => { diff --git a/src/index.js b/src/index.js index cea8f07..4c8b8a9 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ import { startBinanceTradeStream } from "./data/binanceWs.js"; import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; +import { applyGlobalProxyFromEnv } from "./net/proxy.js"; function countVwapCrosses(closes, vwapSeries, lookback) { if (closes.length < lookback || vwapSeries.length < lookback) return null; @@ -37,6 +38,8 @@ function countVwapCrosses(closes, vwapSeries, lookback) { return crosses; } +applyGlobalProxyFromEnv(); + function fmtTimeLeft(mins) { const totalSeconds = Math.max(0, Math.floor(mins * 60)); const m = Math.floor(totalSeconds / 60); diff --git a/src/net/proxy.js b/src/net/proxy.js new file mode 100644 index 0000000..94004ed --- /dev/null +++ b/src/net/proxy.js @@ -0,0 +1,45 @@ +import { ProxyAgent, setGlobalDispatcher } from "undici"; +import { HttpsProxyAgent } from "https-proxy-agent"; +import { SocksProxyAgent } from "socks-proxy-agent"; + +function readEnv(name) { + const v = process.env[name]; + return v && String(v).trim() ? String(v).trim() : ""; +} + +export function getProxyUrlFor(targetUrl) { + const u = String(targetUrl || ""); + const isHttps = u.startsWith("https://") || u.startsWith("wss://"); + const isHttp = u.startsWith("http://") || u.startsWith("ws://"); + + const all = readEnv("ALL_PROXY") || readEnv("all_proxy"); + const https = readEnv("HTTPS_PROXY") || readEnv("https_proxy"); + const http = readEnv("HTTP_PROXY") || readEnv("http_proxy"); + + if (isHttps) return https || all || ""; + if (isHttp) return http || all || ""; + return all || https || http || ""; +} + +export function applyGlobalProxyFromEnv() { + const proxyUrl = getProxyUrlFor("https://example.com"); + if (!proxyUrl) return false; + + try { + setGlobalDispatcher(new ProxyAgent(proxyUrl)); + return true; + } catch { + return false; + } +} + +export function wsAgentForUrl(wsUrl) { + const proxyUrl = getProxyUrlFor(wsUrl); + if (!proxyUrl) return undefined; + + const lower = proxyUrl.toLowerCase(); + if (lower.startsWith("socks://") || lower.startsWith("socks5://") || lower.startsWith("socks4://")) { + return new SocksProxyAgent(proxyUrl); + } + return new HttpsProxyAgent(proxyUrl); +}