#!/usr/bin/env python3 """2026-10-28 client update: miners + v3 wallet (a) default node port 8770 -> 8773 in normalize_server (+ help texts) (b) multi-node failover: eddycoin_chain.SERVERS = chain.json "server" + optional "servers" list + local node http://127.0.0.1:8773; miners: CLI default list = pool + SERVERS, GUI always lists SERVERS; wallet: api() fails over to the next node on the same network (c) hash-only denylist (eddycoin_old_keys from tools/hashdeny: SHA-256 of addresses only) Client protocol (API paths, headers, work/submit/tx formats) unchanged. Counted exact replacements; inputs are read-only.""" import hashlib, os, re, shutil H = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MIN = os.environ.get("EDDY_MINER_SRC", "miners-prev") WAL = os.environ.get("EDDY_WALLET_SRC", "wallet-prev") HD = os.environ.get("EDDY_HASHED_DIR", "hashed") OUT = os.path.join(H, "src") for d in ("miner", "wallet"): os.makedirs(os.path.join(OUT, d), exist_ok=True) def rep(t, old, new, n=1, name=""): c = t.count(old) assert c == n, "%s: expected %d x %r, found %d" % (name, n, old[:80], c) return t.replace(old, new) def port(t, name, n): assert t.count("8773") == 0, name return rep(t, "8770", "8773", n, name) # ---------- eddycoin_chain.py (shared, identical for miners + wallet) ---------- t = open(os.path.join(MIN, "eddycoin_chain.py")).read() assert t == open(os.path.join(WAL, "eddycoin_chain.py")).read() t = rep(t, ' {"chain": "", "server": "https://", "pool": "https://", "explorer": "https:///eddycoin.html"}\n', ' {"chain": "", "server": "https://", "pool": "https://", "explorer": "https:///eddycoin.html"}\n' 'Optional "servers": ["https://", "http://:8773", ...] = more nodes of the same chain for automatic failover\n' '(the programs always add the local node http://127.0.0.1:8773 at the end of the list).\n', name="chain") t = rep(t, 'PLACEHOLDER_SERVER = "https://new-chain-not-active.invalid" # RFC 2606: never resolves, so nothing is contacted\n', 'PLACEHOLDER_SERVER = "https://new-chain-not-active.invalid" # RFC 2606: never resolves, so nothing is contacted\n' 'NODE_PORT = 8773 # default port of an Eddycoin mainnet node (eddycoin-node/2.x)\n' 'LOCAL_NODE = "http://127.0.0.1:%d" % NODE_PORT # Eddycoin Node running on this computer (last failover choice)\n', name="chain") t = rep(t, ' c.update({k: j[k] for k in ("chain", "label", "server", "pool", "explorer") if isinstance(j.get(k), str)})\n', ' c.update({k: j[k] for k in ("chain", "label", "server", "pool", "explorer") if isinstance(j.get(k), str)})\n' ' if isinstance(j.get("servers"), list):\n' ' c["servers"] = [s for s in j["servers"] if isinstance(s, str) and s.strip()]\n', name="chain") t = rep(t, ' if not c["active"]:\n c["server"] = PLACEHOLDER_SERVER; c["pool"] = ""\n return c\n', ' if not c["active"]:\n c["server"] = PLACEHOLDER_SERVER; c["pool"] = ""\n' ' # failover list: main server first, then the extra nodes from chain.json, then a node on this computer\n' ' lst = []\n' ' for s in ([c["server"]] + list(c.get("servers") or []) + [LOCAL_NODE]) if c["active"] else [c["server"]]:\n' ' s = s.strip().rstrip("/")\n' ' if s and s not in lst and s != c["pool"]:\n' ' lst.append(s)\n' ' c["servers"] = lst\n' ' return c\n', name="chain") t = rep(t, 'POOL = CHAIN["pool"] or None\n', 'POOL = CHAIN["pool"] or None\nSERVERS = list(CHAIN["servers"]) # failover order (solo nodes only, never the pool)\n', name="chain") compile(t, "eddycoin_chain.py", "exec") for d in ("miner", "wallet"): open(os.path.join(OUT, d, "eddycoin_chain.py"), "w").write(t) # ---------- hashed denylist modules ---------- shutil.copyfile(os.path.join(HD, "old_keys_miner_hashed.py"), os.path.join(OUT, "miner", "eddycoin_old_keys.py")) shutil.copyfile(os.path.join(HD, "old_keys_wallet_hashed.py"), os.path.join(OUT, "wallet", "eddycoin_old_keys.py")) for d in ("miner", "wallet"): s = open(os.path.join(OUT, d, "eddycoin_old_keys.py")).read() assert not re.search(r"eddy1[0-9a-f]{40}", s), d # ---------- miners ---------- for fn in ("eddycoin_echain_cpu_miner.py", "eddycoin_dchain_gpu_miner.py"): t = port(open(os.path.join(MIN, fn)).read(), fn, 12) t = rep(t, "DEFAULT_SERVERS = [x for x in (DEFAULT_POOL, LIVE_SERVER) if x] # pool first; if the pool is unreachable for 60 s the miner solo-mines on the live server\n", "DEFAULT_SERVERS = [] # pool first; if it is unreachable for 60 s the miner solo-mines on the next reachable node (eddycoin_chain.json server + servers + local node)\n" "for _x in [DEFAULT_POOL, LIVE_SERVER] + list(_CH.SERVERS):\n" " if _x and _x not in DEFAULT_SERVERS:\n" " DEFAULT_SERVERS.append(_x)\n", name=fn) open(os.path.join(OUT, "miner", fn), "w").write(t) t = port(open(os.path.join(MIN, "eddycoin_miner_gui.py")).read(), "gui", 16) t = rep(t, ''' if first_run and pool_default in srv_list: # new install: pool first''', ''' for x in _CH.SERVERS: # failover nodes of this chain (eddycoin_chain.json + local node) are always in the list, after the user's own entries if x and x not in srv_list and check_server(x)[0]: srv_list.append(x) if first_run and pool_default in srv_list: # new install: pool first''', name="gui") t = rep(t, ''' if v in (default_server, pool_default): messagebox.showinfo(title, "%s always stays in the list." % v); return''', ''' if v in (default_server, pool_default) or v in _CH.SERVERS: messagebox.showinfo(title, "%s always stays in the list." % v); return''', name="gui") open(os.path.join(OUT, "miner", "eddycoin_miner_gui.py"), "w").write(t) # ---------- wallet ---------- t = port(open(os.path.join(WAL, "eddycoin_wallet.py")).read(), "wallet", 16) OLD_API = t[t.index("def api(server, path, body=None, timeout=20):\n"):t.index("\n\ndef tx_status(")] t = rep(t, OLD_API, OLD_API.replace("def api(server, path, body=None, timeout=20):\n", "def _api1(server, path, body=None, timeout=20):\n", 1) + ''' FAILOVER = [] # ordered fallback nodes; the GUI fills it from its server list. Empty: requests to the default server fall back to eddycoin_chain.SERVERS _NET_OK = set() # fallback nodes that answered /api/state with exactly this network _DOWN = ("HTTP 502", "HTTP 503", "HTTP 504", "HTTP 521", "HTTP 522", "HTTP 523", "HTTP 530") def api(server, path, body=None, timeout=20, failover=True): """Request to the selected node. If it is unreachable (network error, or 502-504 from its proxy), the SAME request goes to the next fallback node that reports exactly this network in /api/state. A signed transaction is identical on every node (same txid), so sending it to another node of the same chain is safe.""" err = None try: r = _api1(server, path, body, timeout) if not failover or not (isinstance(r, dict) and str(r.get("error", "")).startswith(_DOWN)): return r except (urllib.error.URLError, OSError) as ex: if not failover: raise r, err = None, ex for alt in [s for s in (FAILOVER or (_CH.SERVERS if server == DEFAULT_SERVER else [])) if s != server]: try: if alt not in _NET_OK: st = _api1(alt, "/api/state", timeout=8) if not (_CH.network_ok(st.get("network")) and isinstance(st.get("tips"), dict)): continue _NET_OK.add(alt) r2 = _api1(alt, path, body, timeout) if isinstance(r2, dict) and str(r2.get("error", "")).startswith(_DOWN): continue return r2 except (urllib.error.URLError, OSError, ValueError): continue if err is not None: raise err return r''', name="wallet") t = rep(t, ''' srv_list.insert(0, x) if x == DEFAULT_SERVER else srv_list.append(x) ''', ''' srv_list.insert(0, x) if x == DEFAULT_SERVER else srv_list.append(x) for x in _CH.SERVERS: # failover nodes of this chain (eddycoin_chain.json + local node) are always in the list if x and x not in srv_list and check_server(x)[0]: srv_list.append(x) FAILOVER[:] = srv_list # same list object order; api() uses it when the selected node is unreachable ''', name="wallet") t = rep(t, ' r = api(url, "/api/state", timeout=8)\n', ' r = api(url, "/api/state", timeout=8, failover=False) # testing one node: never answer from another\n', name="wallet") open(os.path.join(OUT, "wallet", "eddycoin_wallet.py"), "w").write(t) shutil.copyfile(os.path.join(WAL, "eddycoin_seed.py"), os.path.join(OUT, "wallet", "eddycoin_seed.py")) for d in ("miner", "wallet"): for f in sorted(os.listdir(os.path.join(OUT, d))): if f.endswith(".py"): p = os.path.join(OUT, d, f) compile(open(p).read(), f, "exec") print(d, hashlib.sha256(open(p, "rb").read()).hexdigest()[:12], f)