Files
siloqy/prod/bingx/dns_cache.py
Codex 84e4a50e3f repo hygiene: track the PINK launcher import closure
67 production .py modules that the running PINK service imports but which
were never committed: prod/bingx/ (HTTP client, market/user streams,
journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2
production modules and their co-located tests. Rule going forward: every
module imported by launch_dolphin_pink.py / pink_direct.py must appear in
git ls-files. Excludes _backup dirs, __pycache__, and non-code files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:09:32 +02:00

129 lines
4.2 KiB
Python

from __future__ import annotations
import socket
import time
from dataclasses import dataclass, field
from typing import Mapping
_STATIC_ENDPOINT_IPS: dict[str, tuple[str, ...]] = {
"open-api.bingx.com": (
"18.66.122.103",
"18.66.122.71",
"18.66.122.3",
"18.66.122.36",
"2600:9000:2250:4800:1a:4f2d:b440:93a1",
"2600:9000:2250:c400:1a:4f2d:b440:93a1",
"2600:9000:2250:6c00:1a:4f2d:b440:93a1",
"2600:9000:2250:7200:1a:4f2d:b440:93a1",
"2600:9000:2250:4600:1a:4f2d:b440:93a1",
"2600:9000:2250:ee00:1a:4f2d:b440:93a1",
"2600:9000:2250:fc00:1a:4f2d:b440:93a1",
"2600:9000:2250:6800:1a:4f2d:b440:93a1",
),
"open-api-vst.bingx.com": (
"18.165.122.31",
"18.165.122.22",
"18.165.122.47",
"18.165.122.63",
"2600:9000:2375:a200:14:7788:7980:93a1",
"2600:9000:2375:9000:14:7788:7980:93a1",
"2600:9000:2375:2c00:14:7788:7980:93a1",
"2600:9000:2375:7800:14:7788:7980:93a1",
"2600:9000:2375:f600:14:7788:7980:93a1",
"2600:9000:2375:ce00:14:7788:7980:93a1",
"2600:9000:2375:e800:14:7788:7980:93a1",
"2600:9000:2375:e200:14:7788:7980:93a1",
),
"open-api-swap.bingx.com": (
"3.164.68.61",
"3.164.68.42",
"3.164.68.75",
"3.164.68.125",
"2600:9000:278c:7400:1f:3dec:7600:93a1",
"2600:9000:278c:3e00:1f:3dec:7600:93a1",
"2600:9000:278c:5800:1f:3dec:7600:93a1",
"2600:9000:278c:3400:1f:3dec:7600:93a1",
"2600:9000:278c:ce00:1f:3dec:7600:93a1",
"2600:9000:278c:9a00:1f:3dec:7600:93a1",
"2600:9000:278c:9000:1f:3dec:7600:93a1",
"2600:9000:278c:c800:1f:3dec:7600:93a1",
),
"open-api.bingx.pro": (
"2606:4700:4403::ac40:9313",
"2a06:98c1:310d::6812:28ed",
),
"open-api-vst.bingx.pro": (
"2a06:98c1:310d::6812:28ed",
"2606:4700:4403::ac40:9313",
),
"open-api-swap.bingx.pro": (
"2a06:98c1:310d::6812:28ed",
"2606:4700:4403::ac40:9313",
),
}
@dataclass(slots=True)
class BingxDnsRecord:
hostname: str
ips: tuple[str, ...]
source: str
updated_at_ns: int
@dataclass(slots=True)
class BingxDnsFallbackCache:
default_ips: Mapping[str, tuple[str, ...]] = field(default_factory=lambda: dict(_STATIC_ENDPOINT_IPS))
_records: dict[str, BingxDnsRecord] = field(default_factory=dict, init=False, repr=False)
def resolve(self, hostname: str) -> tuple[str, ...]:
record = self._records.get(hostname)
if record is not None:
return record.ips
return tuple(self.default_ips.get(hostname, ()))
def record_static(self, hostname: str) -> tuple[str, ...]:
ips = self.resolve(hostname)
if ips:
self._records[hostname] = BingxDnsRecord(
hostname=hostname,
ips=ips,
source="static",
updated_at_ns=time.monotonic_ns(),
)
return ips
def refresh_from_dns(self, hostname: str) -> tuple[str, ...]:
seen: list[str] = []
for family in (socket.AF_UNSPEC,):
infos = socket.getaddrinfo(hostname, None, family=family, type=socket.SOCK_STREAM)
for info in infos:
address = info[4][0]
if address not in seen:
seen.append(address)
ips = tuple(seen)
if ips:
self._records[hostname] = BingxDnsRecord(
hostname=hostname,
ips=ips,
source="dns",
updated_at_ns=time.monotonic_ns(),
)
return ips
def maybe_refresh_from_dns(self, hostname: str, *, min_interval_secs: int = 300) -> tuple[str, ...] | None:
record = self._records.get(hostname)
if record is not None:
age_secs = (time.monotonic_ns() - record.updated_at_ns) / 1_000_000_000
if age_secs < min_interval_secs:
return record.ips
try:
return self.refresh_from_dns(hostname)
except Exception:
return None
def is_bingx_hostname(hostname: str | None) -> bool:
return hostname in _STATIC_ENDPOINT_IPS