bryceweiner commited on
Commit
f2e8fc9
·
verified ·
1 Parent(s): 2f99a73

release 4 (75796a8c89)

Browse files
CHANGELOG.md CHANGED
@@ -35,6 +35,11 @@ The Streamlit console is replaced by a web dashboard served by the same single p
35
  - **Fixed:** `tools/publish_release.py` read the current release over TLS without root certificates (python.org's macOS
36
  Python) and counted the failure as "no release", so it would have published release 1 again. It now uses certifi and
37
  stops on anything but a 404.
 
 
 
 
 
38
  - **Fixed:** a SIGTERM killed the worker threads mid-step: uvicorn re-raises the signal once the server stops. The
39
  console now stops them in the app's shutdown and waits for them to flush.
40
 
 
35
  - **Fixed:** `tools/publish_release.py` read the current release over TLS without root certificates (python.org's macOS
36
  Python) and counted the failure as "no release", so it would have published release 1 again. It now uses certifi and
37
  stops on anything but a 404.
38
+ - **Fixed, after it burned 0.06 ETH:** a test run could spend real money. While `.env` had `RH_LIVE_ENABLED=1` and the
39
+ real RH key, a vault test that expected signing to be off paid two test claims of 0.03 ETH from the live RH wallet to its
40
+ placeholder address `0x1111…1111` (transactions `0x063cbd4f…` and `0x5fd55d3d…`). Now `fly_trader/netguard.py` refuses
41
+ every Solana or EVM broadcast and every Kalshi order, cancel or transfer from a test run unless it goes to a local test
42
+ chain. Tests never load the operator's signing keys or live switches.
43
  - **Fixed:** a SIGTERM killed the worker threads mid-step: uvicorn re-raises the signal once the server stops. The
44
  console now stops them in the app's shutdown and waits for them to flush.
45
 
fly_trader/chain/rpc.py CHANGED
@@ -137,11 +137,17 @@ class HttpSolanaRpc:
137
 
138
  # ---- generic ----
139
  def call(self, method: str, params: list | None = None):
 
 
 
140
  payload = {"jsonrpc": "2.0", "id": self._next_id(), "method": method, "params": params or []}
141
  return self._post(payload, method, _request_summary(method, params))
142
 
143
  def batch(self, calls: list[tuple[str, list | None]]) -> list:
144
  """Send several calls in one HTTP request; results in call order. Raises on the first error."""
 
 
 
145
  if not calls:
146
  return []
147
  payload = [{"jsonrpc": "2.0", "id": self._next_id(), "method": m, "params": p or []} for m, p in calls]
 
137
 
138
  # ---- generic ----
139
  def call(self, method: str, params: list | None = None):
140
+ if method == "sendTransaction":
141
+ from ..netguard import refuse_broadcast
142
+ refuse_broadcast(self._url, "a Solana transaction") # never from a test run (fly_trader/netguard.py)
143
  payload = {"jsonrpc": "2.0", "id": self._next_id(), "method": method, "params": params or []}
144
  return self._post(payload, method, _request_summary(method, params))
145
 
146
  def batch(self, calls: list[tuple[str, list | None]]) -> list:
147
  """Send several calls in one HTTP request; results in call order. Raises on the first error."""
148
+ if any(m == "sendTransaction" for m, _ in calls):
149
+ from ..netguard import refuse_broadcast
150
+ refuse_broadcast(self._url, "a Solana transaction") # never from a test run (fly_trader/netguard.py)
151
  if not calls:
152
  return []
153
  payload = [{"jsonrpc": "2.0", "id": self._next_id(), "method": m, "params": p or []} for m, p in calls]
fly_trader/kalshi/client.py CHANGED
@@ -122,6 +122,9 @@ class KalshiRest:
122
 
123
  def request(self, method: str, path: str, params: dict | None = None, body: dict | None = None, timeout: float = 60.0) -> dict:
124
  """One paced, logged call. Raises ``KalshiApiError`` on HTTP >= 400 (429 is retried with backoff first)."""
 
 
 
125
  client = self._ensure()
126
  q = {k: v for k, v in (params or {}).items() if v is not None}
127
  url = self.base + path + (("?" + urlencode(q)) if q else "")
 
122
 
123
  def request(self, method: str, path: str, params: dict | None = None, body: dict | None = None, timeout: float = 60.0) -> dict:
124
  """One paced, logged call. Raises ``KalshiApiError`` on HTTP >= 400 (429 is retried with backoff first)."""
125
+ if method.upper() != "GET":
126
+ from ..netguard import refuse_broadcast
127
+ refuse_broadcast(self.base, f"Kalshi {method} {path}") # never from a test run (fly_trader/netguard.py)
128
  client = self._ensure()
129
  q = {k: v for k, v in (params or {}).items() if v is not None}
130
  url = self.base + path + (("?" + urlencode(q)) if q else "")
fly_trader/netguard.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """No money moves from a test run: every place that broadcasts a transaction or changes a live account refuses while
2
+ pytest runs, unless it talks to a local test chain (anvil, a fake on 127.0.0.1).
3
+
4
+ Why it exists: on 2026-10-17 the test suite ran while the operator's .env had RH_LIVE_ENABLED=1 and the real RH bot key.
5
+ A vault test expected Robinhood Chain signing to be off; it was on, so the claim processor built the real wallet
6
+ (RhWallet(RhRpc())) and paid two test claims of 0.03 ETH to the test's placeholder address 0x1111…1111 on mainnet —
7
+ 0.06 ETH burned. The database already refuses production under pytest (db/connection.py); this does the same for chains
8
+ and exchanges, at the lowest layer, so no configuration or code path above it can get around it.
9
+
10
+ A test run is recognised by PYTEST_CURRENT_TEST (set by pytest for every test's setup, call and teardown) or
11
+ FLY_TRADER_TESTING=1 (set by tests/conftest.py before anything is imported). Neither is ever set in production.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from urllib.parse import urlsplit
17
+
18
+ LOOPBACK = {"127.0.0.1", "localhost", "::1"}
19
+
20
+
21
+ class BroadcastUnderTest(RuntimeError):
22
+ """A test tried to move real money."""
23
+
24
+
25
+ def under_test() -> bool:
26
+ return bool(os.environ.get("PYTEST_CURRENT_TEST")) or os.environ.get("FLY_TRADER_TESTING") == "1"
27
+
28
+
29
+ def refuse_broadcast(url: str | None, what: str) -> None:
30
+ """Raise when a test is about to send ``what`` to a real network (anything but a loopback host)."""
31
+ if not under_test():
32
+ return
33
+ host = urlsplit(url or "").hostname or ""
34
+ if host not in LOOPBACK:
35
+ raise BroadcastUnderTest(f"refusing to send {what} to {host or 'an unknown host'} from a test run: tests never move real money")
fly_trader/rh/rpc.py CHANGED
@@ -53,6 +53,9 @@ class RhRpc(EvmRpc):
53
  def _batch(self, calls: list[tuple[str, list]]) -> list[Any]:
54
  if not calls:
55
  return []
 
 
 
56
  with self._lock:
57
  base = self._id + 1; self._id += len(calls)
58
  body = [{"jsonrpc": "2.0", "id": base + i, "method": m, "params": p} for i, (m, p) in enumerate(calls)]
 
53
  def _batch(self, calls: list[tuple[str, list]]) -> list[Any]:
54
  if not calls:
55
  return []
56
+ if any(m == "eth_sendRawTransaction" for m, _ in calls):
57
+ from ..netguard import refuse_broadcast
58
+ refuse_broadcast(self.url, "a transaction") # never from a test run (fly_trader/netguard.py)
59
  with self._lock:
60
  base = self._id + 1; self._id += len(calls)
61
  body = [{"jsonrpc": "2.0", "id": base + i, "method": m, "params": p} for i, (m, p) in enumerate(calls)]