# -*- coding: utf-8 -*- """Shared tkinter GUI for the Eddycoin CPU (E-Chain) and GPU (D-Chain) miners - new Eddycoin mainnet build. MIT License, Copyright (c) 2026 Nakamoto Vein.""" import json, math, os, queue, re, sys, threading, time, types import eddycoin_chain as _CH # single switch: eddycoin_chain.json import eddycoin_old_keys as _OLD HTTP_WARNING = "plain http:// is unencrypted - fine for your own PC or home network, but others on the network could read or change the traffic" def check_server(url): """Return (ok, message). Allowed: any https:// URL; http:// only for this PC (localhost / 127.x), private LAN IPs (10.x, 172.16-31.x, 192.168.x) or an explicit node URL with a port (e.g. http://name.duckdns.org:8773). For http:// to another computer the message is a warning (unencrypted).""" import ipaddress, urllib.parse try: u = urllib.parse.urlsplit(url.strip()) host = (u.hostname or "").lower(); port = u.port except ValueError: return False, "invalid server URL" if u.scheme == "https" and host: return True, "" if u.scheme != "http" or not host: return False, "server must be an https:// URL or an http:// node URL (e.g. http://192.168.1.20:8773)" if host == "localhost": return True, "" try: ip = ipaddress.ip_address(host) except ValueError: ip = None if ip is not None and ip.is_loopback: return True, "" lan = ip is not None and ((ip.version == 4 and any(ip in ipaddress.ip_network(n) for n in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"))) or (ip.version == 6 and (ip.is_private or ip.is_link_local))) if lan or port is not None: return True, HTTP_WARNING return False, "http:// is allowed only for this PC, a LAN IP (10.x / 172.16-31.x / 192.168.x) or a node URL with a port, e.g. http://name.duckdns.org:8773 - otherwise use https://" def normalize_server(text, default_port=8773): """Turn what the user typed into a server URL: 192.168.1.5:8773 -> http://192.168.1.5:8773 host.example:8773 -> http://host.example:8773 [fd00::5]:8773 -> http://[fd00::5]:8773 192.168.1.5 / raspberrypi / localhost -> http://...:8773 eddycoin-demo.pages.dev -> https://eddycoin-demo.pages.dev (plain domain without port = https) Full URLs (http:// or https://) are kept; a trailing slash / path is removed.""" import ipaddress, re as _re, urllib.parse t = _re.sub(r"\s+", "", str(text or "")) if not t: return "" if "://" in t: try: u = urllib.parse.urlsplit(t) except ValueError: return t return "%s://%s" % (u.scheme.lower(), u.netloc.lower()) if u.netloc else t hp = t.split("/")[0] m = _re.match(r"^\[([0-9A-Fa-f:.]+)\](?::(\d{1,5}))?$", hp) if m: return "http://[%s]:%s" % (m.group(1).lower(), m.group(2) or default_port) try: ip = ipaddress.ip_address(hp) return ("http://[%s]:%d" if ip.version == 6 else "http://%s:%d") % (str(ip), default_port) except ValueError: pass m = _re.match(r"^([A-Za-z0-9.\-]+):(\d{1,5})$", hp) if m: return "http://%s:%s" % (m.group(1).lower(), m.group(2)) if _re.match(r"^[A-Za-z0-9\-]+$", hp): return "http://%s:%d" % (hp.lower(), default_port) if _re.match(r"^[A-Za-z0-9.\-]+$", hp): return "https://" + hp.lower() return t MONO = ["Consolas"] ADDR_RE = re.compile(r"^eddy1[0-9a-f]{40}$") BG, PANEL, FG, MUTED, ACC, WARN = "#0b1020", "#121a30", "#e6ebf5", "#9aa6c0", "#10b981", "#f59e0b" def default_threads(): """Sensible default worker count: on Macs the performance cores (Apple Silicon) or the physical cores (Intel), elsewhere half of the logical cores. Always at least 1 and leaves one logical core free.""" logical = os.cpu_count() or 2 n = 0 if sys.platform == "darwin": import subprocess for key in ("hw.perflevel0.physicalcpu", "hw.physicalcpu"): try: n = int(subprocess.run(["sysctl", "-n", key], capture_output=True, text=True, timeout=3).stdout.strip() or 0) except Exception: n = 0 if n > 0: break if n <= 0: n = logical // 2 return max(1, min(n, logical - 1 if logical > 1 else 1)) def app_dir(): return os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else sys.argv[0])) def fmt(m, dec=8): m = int(m) # int or decimal string (server amounts are decimal strings) q, r = divmod(abs(m), 10 ** dec) return ("-" if m < 0 else "") + "{:,}".format(q) + "." + str(r).rjust(dec, "0") EXE_GPU_HELP = ("This .exe already contains PyOpenCL and numpy - you only need a graphics driver with OpenCL\n" "(the normal driver install is enough, CUDA Toolkit NOT required):\n" " NVIDIA: GeForce/Studio driver from nvidia.com\n AMD: Adrenalin driver from amd.com\n" " Intel: Arc/Iris Xe graphics driver from intel.com\nThen restart Eddycoin GPU Miner.") def mix(c1, c2, t): """blend two #rrggbb colours (t = 0 -> c1, 1 -> c2)""" t = max(0.0, min(1.0, t)); a = [int(c1[i:i + 2], 16) for i in (1, 3, 5)]; b = [int(c2[i:i + 2], 16) for i in (1, 3, 5)] return "#%02x%02x%02x" % tuple(int(round(x + (y - x) * t)) for x, y in zip(a, b)) class MinerAnim: """purely visual, driven by real miner events: two puzzle pieces drift together while the miner hashes (speed follows the measured hashrate); an accepted share / block snaps them together with a short spark burst, an EDDY coin forms and slides to the end of a short chain of earlier coins (7 visible, the oldest fades out). No text. ~25 fps while something moves, ~6 fps idle, nothing while minimized or switched off. Only moves existing canvas items.""" KEEP = 7 PIECE_L, PIECE_R, SPARK, GOLD, GOLD_HI, GOLD_LO, LINK = "#10b981", "#60a5fa", "#fde68a", "#f59e0b", "#fde68a", "#b45309", "#3a4563" def __init__(self, root, cv): self.root, self.cv = root, cv; self.on = True; self.job = None; self.t0 = time.time() self.running = self.hashing = False; self.rate = 0.0; self.phase = 0.0; self.last_t = time.time() self.queue = []; self.ev = None; self.coins = []; self.shift = None; self.size = (0, 0) self.links = [] self.pl = cv.create_polygon(0, 0, 0, 0, fill=self.PIECE_L, outline="#065f46", width=2) self.pr = cv.create_polygon(0, 0, 0, 0, fill=self.PIECE_R, outline="#1e3a8a", width=2) self.glow = cv.create_oval(0, 0, 0, 0, outline="", fill="") self.sparks = [cv.create_line(0, 0, 0, 0, fill=self.SPARK, width=2, state="hidden") for _ in range(12)] self.newc = self._coin_items(); self._coin_state(self.newc, "hidden") cv.bind("", lambda e: self.layout()) # ---------- shapes ---------- def _coin_items(self): cv = self.cv return (cv.create_oval(0, 0, 0, 0, fill=self.GOLD, outline=self.GOLD_HI, width=2), cv.create_oval(0, 0, 0, 0, fill="", outline=self.GOLD_LO, width=2), cv.create_arc(0, 0, 0, 0, start=100, extent=70, style="arc", outline=self.GOLD_HI, width=2)) def _coin_state(self, c, st): for i in c: self.cv.itemconfigure(i, state=st) def _coin_place(self, c, x, y, r, fade=0.0, big=False): cv = self.cv; cv.coords(c[0], x - r, y - r, x + r, y + r); ri = r * 0.62 cv.coords(c[1], x - ri, y - ri, x + ri, y + ri); rh = r * 0.8; cv.coords(c[2], x - rh, y - rh, x + rh, y + rh) bg = "#0b1020"; base = "#fbbf24" if big else self.GOLD cv.itemconfigure(c[0], fill=mix(base, bg, fade), outline=mix(self.GOLD_HI, bg, fade)) cv.itemconfigure(c[1], outline=mix(self.GOLD_LO, bg, fade)); cv.itemconfigure(c[2], outline=mix("#fff7d6", bg, fade)) @staticmethod def piece(x, y, s, side): """square puzzle piece (size s, centre x,y). side 'L': tab sticking out to the right; 'R': matching notch on the left""" h = s / 2; tr = s * 0.22; pts = [] if side == "L": pts += [x - h, y - h, x + h, y - h, x + h, y - tr] for k in range(9): # tab = half circle outwards a = -math.pi / 2 + k * math.pi / 8; pts += [x + h + tr * math.cos(a), y + tr * math.sin(a)] pts += [x + h, y + tr, x + h, y + h, x - h, y + h] else: pts += [x - h, y - h, x + h, y - h, x + h, y + h, x - h, y + h, x - h, y + tr] for k in range(9): # notch = half circle inwards a = math.pi / 2 - k * math.pi / 8; pts += [x - h + tr * math.cos(a), y + tr * math.sin(a)] pts += [x - h, y - tr] return pts # ---------- layout ---------- def layout(self): self.size = (max(self.cv.winfo_width(), 200), max(self.cv.winfo_height(), 60)) def geo(self): w, h = self.size; s = min(44, h * 0.42) px = max(110, w * 0.2); cy = h / 2; r = min(17, h * 0.17); gap = r * 2 + 16 chain_end = w - 40 return w, h, s, px, cy, r, gap, chain_end # ---------- inputs from the GUI (real events) ---------- def update(self, running, hashing, rate): self.running, self.hashing, self.rate = running, hashing and running, float(rate or 0) def event(self, kind): if len(self.queue) < 3: self.queue.append(kind) def start(self): if not self.job: self.job = self.root.after(60, self.frame) def frame(self): self.job = None try: busy = self.draw() except Exception: busy = False hidden = self.root.state() in ("iconic", "withdrawn") self.job = self.root.after(500 if (hidden or not self.on) else (40 if busy else 160), self.frame) def draw(self): if not self.on or self.root.state() in ("iconic", "withdrawn"): self.last_t = time.time(); return False if self.size[0] < 10: self.layout() cv = self.cv; now = time.time(); dt = min(0.2, now - self.last_t); self.last_t = now w, h, s, px, cy, r, gap, chain_end = self.geo() # hashing speed: 0.3 cycles/s at <=256 kH/s .. 1.6 cycles/s at >=4 GH/s (log scale) if self.hashing: lg = math.log2(max(self.rate, 1.0)); spd = 0.3 + 1.3 * max(0.0, min(1.0, (lg - 18) / 14)) self.phase += dt * spd * 2 * math.pi if self.ev is None and self.queue: self.ev = [self.queue.pop(0), 0.0]; self.newc_big = self.ev[0] == "block" if self.ev: # event clock; runs faster when more events are waiting so the picture never lags behind self.ev[1] += dt * (1.8 if self.queue else 1.0) busy = self.hashing or self.ev is not None or self.shift is not None # pieces far = s * 1.15; near = s * 0.5 + s * 0.02 e = self.ev[1] if self.ev else None if e is not None and e < 0.18: # snap together k = e / 0.18; d = (self.dl if hasattr(self, "dl") else far) * (1 - k) + (s * 0.5) * k elif e is not None: d = s * 0.5 elif self.hashing: d = near + (far - near) * (0.55 + 0.45 * math.cos(self.phase)); self.dl = d else: d = far + 6; self.dl = d dim = 0.0 if self.running else 0.55 fade_p = 0.0 if e is None or e < 0.4 else min(0.7, (e - 0.4) / 0.2) # pieces dim while the coin forms, then come back if e is not None and e > 0.8: fade_p = max(0.0, 0.7 - (e - 0.8) / 0.4 * 0.7) d = s * 0.5 + (far - s * 0.5) * min(1.0, (e - 0.8) / 0.6); self.dl = d # pieces separate again for the next round a = max(dim, fade_p) cv.coords(self.pl, *self.piece(px - d, cy, s, "L")); cv.coords(self.pr, *self.piece(px + d, cy, s, "R")) cv.itemconfigure(self.pl, fill=mix(self.PIECE_L, "#0b1020", a), outline=mix("#065f46", "#0b1020", a)) cv.itemconfigure(self.pr, fill=mix(self.PIECE_R, "#0b1020", a), outline=mix("#1e3a8a", "#0b1020", a)) if e is not None and e > 1.4 and not self.hashing: pass # glow while hashing (soft breathing) if e is not None and 0.15 <= e < 0.45: # short flash where the pieces meet k = (e - 0.15) / 0.3; g = s * (0.2 + 0.5 * k) cv.coords(self.glow, px - g, cy - g, px + g, cy + g); cv.itemconfigure(self.glow, outline=mix(self.SPARK, "#0b1020", k), width=3); cv.tag_raise(self.glow) elif self.hashing and e is None: g = s * 0.9 + 3 * math.sin(self.phase * 0.5) cv.coords(self.glow, px - g, cy - g, px + g, cy + g); cv.itemconfigure(self.glow, outline=mix("#0b1020", "#10b981", 0.25), width=1) else: cv.itemconfigure(self.glow, outline="") # sparks 0.18 .. 0.6 s big = getattr(self, "newc_big", False) if e is not None and 0.18 <= e < 0.6: k = (e - 0.18) / 0.42; n = len(self.sparks) if big else 8 for i, ln in enumerate(self.sparks): if i >= n: cv.itemconfigure(ln, state="hidden"); continue ang = i * 2 * math.pi / n + 0.3; r0 = s * 0.62 + s * 0.8 * k; r1 = r0 + s * (0.55 if big else 0.4) * (1 - k * 0.7) cv.coords(ln, px + r0 * math.cos(ang), cy + r0 * math.sin(ang), px + r1 * math.cos(ang), cy + r1 * math.sin(ang)) cv.itemconfigure(ln, state="normal", fill=mix(self.SPARK if big else "#a7f3d0", "#0b1020", k * k), width=3 if big else 2); cv.tag_raise(ln) else: for ln in self.sparks: cv.itemconfigure(ln, state="hidden") # chain: newest coin at chain_end, older ones to the left, linked; the oldest fades out sh = 0.0 if self.shift is not None: k = min(1.0, (now - self.shift) / 0.35); sh = gap * (1 - (1 - k) ** 2) if k >= 1.0: self.shift = None; sh = 0.0 while len(self.coins) > self.KEEP: old = self.coins.pop(0) for i in old[0]: cv.delete(i) if self.links: cv.delete(self.links.pop(0)) n = len(self.coins); xs = [] for i, (items, cbig) in enumerate(self.coins): pos = n - 1 - i # 0 = newest x = chain_end - pos * gap + ((gap - sh) if self.shift is not None else 0.0) vis = (pos - (1 if self.shift is not None else 0)) + (sh / gap if self.shift is not None else 0.0) # slot after the shift, fractional fade = 0.0 if vis <= self.KEEP - 3 else min(1.0, (vis - (self.KEEP - 3)) * 0.33) xs.append(x); self._coin_place(items, x, cy, r * (1.12 if cbig else 1.0), fade, cbig) while len(self.links) < max(0, len(xs) - 1): l = cv.create_line(0, 0, 0, 0, fill=self.LINK, width=4); cv.tag_lower(l); self.links.append(l) for i, l in enumerate(self.links): if i + 1 < len(xs): cv.coords(l, xs[i] + r * 0.9, cy, xs[i + 1] - r * 0.9, cy) # new coin: forms 0.45..0.75 s at the puzzle, slides 0.75..1.4 s to the end of the chain if e is not None and e >= 0.45: self._coin_state(self.newc, "normal") rr = r * (1.12 if big else 1.0) if e < 0.75: k = (e - 0.45) / 0.3; self._coin_place(self.newc, px, cy, rr * (0.3 + 0.7 * k) * (1 + 0.15 * math.sin(k * math.pi)), 0.0, big) elif e < 1.4: k = (e - 0.75) / 0.65; k = 1 - (1 - k) ** 3 tx = chain_end + gap if self.coins else chain_end x = px + (tx - px) * k; y = cy - math.sin(k * math.pi) * h * 0.12 self._coin_place(self.newc, x, y, rr, 0.0, big) else: # arrives: becomes part of the chain, chain shifts left by one slot self._coin_state(self.newc, "hidden") c = self._coin_items(); self.coins.append((c, big)) if len(self.coins) > 1: self.shift = now self.ev = None busy = True else: self._coin_state(self.newc, "hidden") return busy def run(kind, title, default_server, api, start_fn, list_devices=None, install_help="", pool_default=None): """kind 'cpu' or 'gpu'. start_fn(cfg, out, stop_ev, st) runs the live mining loop (blocking, in a worker thread).""" import tkinter as tk from tkinter import ttk, filedialog, messagebox sp = os.path.join(app_dir(), "eddycoin_miner_settings.json") first_run = not os.path.exists(sp) # only a NEW install starts on the pool; saved settings are never switched silently try: with open(sp, encoding="utf-8") as f: S = json.load(f) except Exception: S = {} if first_run: try: with open(os.path.join(app_dir(), "eddycoin_miner_defaults.json"), encoding="utf-8") as f: S = dict(json.load(f)) except Exception: pass root = tk.Tk(); root.title(title + " - " + _CH.LABEL); root.geometry("820x760"); root.minsize(680, 640); root.configure(bg=BG) import tkinter.font as tkf MONO[0] = "Consolas" if "Consolas" in tkf.families() else "DejaVu Sans Mono" if "DejaVu Sans Mono" in tkf.families() else "Courier" sty = ttk.Style() try: sty.theme_use("clam") except Exception: pass sty.configure(".", background=BG, foreground=FG, fieldbackground=PANEL); sty.configure("Muted.TLabel", foreground=MUTED) sty.map("TCombobox", fieldbackground=[("readonly", PANEL)], foreground=[("readonly", FG)], selectbackground=[("readonly", PANEL)], selectforeground=[("readonly", FG)]) sty.configure("Big.TLabel", font=("Segoe UI", 20, "bold"), foreground=ACC) sty.configure("Start.TButton", font=("Segoe UI", 14, "bold"), padding=10, background=ACC, foreground="#04120c") sty.configure("Stop.TButton", font=("Segoe UI", 14, "bold"), padding=10, background="#ef4444", foreground="white") sty.map("Start.TButton", background=[("disabled", "#2b3a4f")]); sty.map("Stop.TButton", background=[("disabled", "#2b3a4f")]) fr = ttk.Frame(root, padding=12); fr.pack(fill="both", expand=True) ttk.Label(fr, text=title, font=("Segoe UI", 15, "bold")).grid(row=0, column=0, columnspan=4, sticky="w") ttk.Label(fr, text="Wallet address").grid(row=1, column=0, sticky="w", pady=(10, 2)) addr = tk.StringVar(value=S.get("address", "")) ttk.Entry(fr, textvariable=addr, width=52, font=(MONO[0], 10)).grid(row=2, column=0, columnspan=2, sticky="we") def paste(): try: addr.set(root.clipboard_get().strip()) except Exception: pass def load_wallet(): p = filedialog.askopenfilename(initialdir=app_dir(), filetypes=[("Eddycoin wallet", "*.json"), ("All files", "*.*")]) if not p: return try: with open(p, encoding="utf-8") as f: w = json.load(f) if w.get("format") != "eddycoin-wallet" or not ADDR_RE.match(w.get("address", "")): raise ValueError("not an Eddycoin wallet file") addr.set(w["address"]); w.clear() # only the public address is used; no password or key needed except Exception as ex: messagebox.showerror(title, "Cannot read wallet file: %s" % ex) ttk.Button(fr, text="Paste", command=paste).grid(row=2, column=2, padx=4) ttk.Button(fr, text="Load wallet file", command=load_wallet).grid(row=2, column=3) ttk.Label(fr, text="Server (your list - if the selected one is unreachable for 60 s, the next one is used)").grid(row=3, column=0, columnspan=4, sticky="w", pady=(8, 2)) warned = set() def probe(url): r = api(url, "/api/state", timeout=8) if _CH.network_ok(r.get("network")) and isinstance(r.get("tips"), dict): t = r["tips"]; p = r.get("pool") if isinstance(r.get("pool"), dict) else None if p and url not in S.setdefault("pools", []): S["pools"].append(url) return True, "%sY #%s, E #%s, D #%s" % (("POOL (fee %.2f %%) - " % (p.get("fee_bps", 0) / 100)) if p else "solo - ", t.get("Y"), t.get("E"), t.get("D")) return False, str(r.get("error") or r.get("reason") or "not an Eddycoin server")[:120] srv_list = [normalize_server(x) for x in (S.get("servers") or []) if check_server(normalize_server(x))[0]] for x in (S.get("server"), default_server, pool_default): # older settings had a single "server"; the live server and the pool are always in the list x = str(x or "").strip().rstrip("/") if x and check_server(x)[0] and x not in srv_list: if x == default_server or (x == pool_default and first_run): srv_list.insert(0, x) 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, 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 srv_list.remove(pool_default); srv_list.insert(0, pool_default) start_default = pool_default if (first_run and pool_default) else default_server server = tk.StringVar(value=(S.get("server") or start_default).strip().rstrip("/")) if server.get() not in srv_list: server.set(start_default) srow = ttk.Frame(fr); srow.grid(row=4, column=0, columnspan=4, sticky="we") srv_cb = ttk.Combobox(srow, textvariable=server, values=srv_list, state="readonly", width=46); srv_cb.pack(side="left", fill="x", expand=True) in_use = ttk.Label(fr, text="", style="Muted.TLabel"); in_use.grid(row=5, column=0, columnspan=4, sticky="w") def add_server(): from tkinter import simpledialog v = simpledialog.askstring(title, "Server or node address - with or without http://, port included, e.g.\n 127.0.0.1:8773 (Eddycoin Node on this PC)\n 192.168.1.20:8773 (node in your home network)\n name.duckdns.org:8773 (a friend's node)\n [fd00::20]:8773 (IPv6)\nNo port: an IP / PC name gets :8773, a plain domain uses https://", parent=root) if not v: return v = normalize_server(v) ok, msg = check_server(v) if not ok: messagebox.showerror(title, msg[0].upper() + msg[1:] + "."); return if msg and not messagebox.askokcancel(title, "Note: " + msg + ".\n\nAdd %s anyway?" % v): return warned.add(v) in_use.config(text="checking %s …" % v); root.update_idletasks() ok2, info = probe(v) if not ok2: in_use.config(text=""); messagebox.showerror(title, "%s did not answer like an Eddycoin server/node (/api/state):\n%s\n\nNot added." % (v, info)); return if v not in srv_list: srv_list.append(v) srv_cb.config(values=srv_list); server.set(v); save_settings() in_use.config(text="added %s: %s" % (v, info)) def remove_server(): v = server.get() if v in (default_server, pool_default) or v in _CH.SERVERS: messagebox.showinfo(title, "%s always stays in the list." % v); return if v in srv_list: srv_list.remove(v) srv_cb.config(values=srv_list); server.set(default_server); save_settings(); in_use.config(text="removed %s" % v) def test_servers(): in_use.config(text="testing %d server(s) …" % len(srv_list)) def job(): res = [] for v in list(srv_list): ok2, info = probe(v) res.append("%s %s %s" % ("OK " if ok2 else "---", v, info if ok2 else "unreachable (%s)" % info)) for line in res: out("test: " + line) root.after(0, lambda: in_use.config(text="test: %d of %d reachable - details in the log below" % (sum(r.startswith("OK") for r in res), len(res)))) threading.Thread(target=job, daemon=True).start() ttk.Button(srow, text="Add", command=add_server).pack(side="left", padx=(6, 0)) ttk.Button(srow, text="Remove", command=remove_server).pack(side="left", padx=(4, 0)) ttk.Button(srow, text="Test", command=test_servers).pack(side="left", padx=(4, 0)) opt = ttk.Frame(fr); opt.grid(row=6, column=0, columnspan=4, sticky="we", pady=8) intensity = tk.IntVar(value=int(S.get(kind + "_intensity", S.get("intensity", 100)))) threads = tk.IntVar(value=int(S.get("threads", default_threads()))) dev_v = tk.StringVar(value="") devs = [] if kind == "cpu": ttk.Label(opt, text="Threads").grid(row=0, column=0, sticky="w") tl = ttk.Label(opt, text=""); tl.grid(row=0, column=2, sticky="w") ttk.Scale(opt, from_=1, to=max(1, os.cpu_count() or 1), variable=threads, length=260, command=lambda v: (threads.set(round(float(v))), tl.config(text="%d of %d logical cores" % (threads.get(), os.cpu_count() or 1)))).grid(row=0, column=1, padx=8) tl.config(text="%d of %d logical cores" % (threads.get(), os.cpu_count() or 1)) else: ttk.Label(opt, text="GPU device").grid(row=0, column=0, sticky="w") err = None; devs = [] try: devs = list_devices() or [] except SystemExit as ex: err = ex.code if isinstance(ex.code, str) and ex.code else ("No OpenCL platform/driver was found.\n" + (EXE_GPU_HELP if getattr(sys, "frozen", False) else install_help)) except Exception as ex: err = "%s\n%s" % (ex, install_help) cb = ttk.Combobox(opt, textvariable=dev_v, values=[d["label"] for d in devs], state="readonly", width=60); cb.grid(row=0, column=1, columnspan=2, padx=8, sticky="w") if devs: want = S.get("device"); pick = next((d for d in devs if d["label"] == want), None) or next((d for d in devs if d["gpu"]), devs[0]); dev_v.set(pick["label"]) nogpu = not any(d["gpu"] for d in devs) if err or nogpu: msg = (err or "No OpenCL GPU was found (only CPU/other OpenCL devices).") + ("" if err else ("\nApple has deprecated OpenCL; use the CPU miner." if sys.platform == "darwin" else "\nInstall/update your graphics driver (NVIDIA, AMD or Intel; CUDA Toolkit not needed).")) tk.Label(opt, text=msg.strip(), bg="#2a1f05", fg="#fde68a", justify="left", wraplength=700).grid(row=2, column=0, columnspan=3, sticky="we", pady=4) ttk.Label(opt, text="Intensity").grid(row=1, column=0, sticky="w") il = ttk.Label(opt, text="%d %%" % intensity.get()); il.grid(row=1, column=2, sticky="w") ttk.Scale(opt, from_=10, to=100, variable=intensity, length=260, command=lambda v: (intensity.set(round(float(v))), il.config(text="%d %% (lower = less heat)" % intensity.get()))).grid(row=1, column=1, padx=8) bf = ttk.Frame(fr); bf.grid(row=7, column=0, columnspan=4, sticky="we", pady=6) start_b = ttk.Button(bf, text="START", style="Start.TButton"); start_b.pack(side="left", expand=True, fill="x", padx=(0, 6)) stop_b = ttk.Button(bf, text="STOP", style="Stop.TButton"); stop_b.pack(side="left", expand=True, fill="x"); stop_b.state(["disabled"]) stf = ttk.Frame(fr); stf.grid(row=8, column=0, columnspan=4, sticky="we") labels = {} for i, (k, t) in enumerate([("hashrate", "Hashrate (measured)"), ("bits", "Difficulty"), ("found", "Blocks / shares"), ("accrej", "Accepted / rejected"), ("pending", "Pending reward"), ("final", "Finalized EDDY"), ("elapsed", "Elapsed"), ("last", "Last result")]): c = ttk.Frame(stf, padding=(0, 4, 18, 4)); c.grid(row=i // 4, column=i % 4, sticky="w") labels[k] = ttk.Label(c, text="-", style="Big.TLabel" if k in ("hashrate", "final") else "TLabel", font=None if k in ("hashrate", "final") else ("Segoe UI", 12, "bold")) labels[k].pack(anchor="w"); ttk.Label(c, text=t, style="Muted.TLabel").pack(anchor="w") anim_cv = tk.Canvas(fr, height=110, bg=BG, highlightthickness=0, borderwidth=0) anim = MinerAnim(root, anim_cv) anim_v = tk.BooleanVar(value=bool(S.get("animation", True))) def show_anim(*a): anim.on = bool(anim_v.get()) if anim.on: anim_cv.grid(row=9, column=0, columnspan=4, sticky="we", pady=(6, 0)) else: anim_cv.grid_remove() ttk.Checkbutton(opt, text="Animation", variable=anim_v).grid(row=3, column=0, columnspan=2, sticky="w", pady=(4, 0)) anim_v.trace_add("write", lambda *a: (show_anim(), save_settings())) auto_v = tk.BooleanVar(value=bool(S.get("autostart", False))) ttk.Checkbutton(opt, text="Start mining automatically when opened", variable=auto_v).grid(row=4, column=0, columnspan=3, sticky="w", pady=(4, 0)) auto_v.trace_add("write", lambda *a: save_settings()) show_anim(); anim.start() log = tk.Text(fr, height=10, bg="#0a0f1d", fg=FG, insertbackground=FG, font=(MONO[0], 9), relief="flat"); log.grid(row=10, column=0, columnspan=4, sticky="nsew", pady=(8, 0)) fr.columnconfigure(1, weight=1); fr.rowconfigure(10, weight=1) q = queue.Queue(); run_state = {"th": None, "stop": None, "st": {}, "t0": None, "wallet": None, "last_poll": 0} def out(msg): q.put(time.strftime("%H:%M:%S ") + str(msg).strip()) def save_settings(): S.update(address=addr.get().strip(), server=server.get().strip(), servers=list(srv_list), threads=threads.get(), device=dev_v.get(), animation=bool(anim_v.get()), autostart=bool(auto_v.get())); S[kind + "_intensity"] = intensity.get() try: with open(sp, "w", encoding="utf-8") as f: json.dump(S, f, indent=1) except Exception: pass def start(): a = addr.get().strip(); sv = server.get().strip().rstrip("/") if ADDR_RE.match(a) and _OLD.is_old(a): messagebox.showerror(title, _OLD.refusal(a) or "Refused payout address."); return if not ADDR_RE.match(a): messagebox.showerror(title, "Enter your payout wallet address (eddy1 + 40 hex characters). Create a wallet in Eddycoin Wallet and copy its address."); return if not _CH.ACTIVE: messagebox.showinfo(title, "The new Eddycoin chain is not live yet, so there is nothing to mine.\n\nYour address is saved. Mining starts once eddycoin_chain.json holds the new chain's values (see README.txt)."); save_settings(); return ok, msg = check_server(sv) if not ok: messagebox.showerror(title, msg[0].upper() + msg[1:] + "."); return if msg and sv not in warned and not messagebox.askokcancel(title, "Note: " + msg + ".\n\nMine via %s anyway?" % sv): return if msg: warned.add(sv) servers = [sv] + [x for x in srv_list[srv_list.index(sv) + 1:] + srv_list[:srv_list.index(sv)] if x != sv] if sv in srv_list else [sv] pools = set([pool_default] + list(S.get("pools") or [])) if sv not in pools: # solo selected: never fail over into a pool (pool selected: may fall back to solo servers) servers = [x for x in servers if x not in pools or x == sv] cfg = types.SimpleNamespace(wallet=a, server=sv, servers=servers, intensity=intensity.get(), threads=threads.get(), minutes=None, offline=False, device=next((d for d in devs if d["label"] == dev_v.get()), None)) if kind == "gpu" and not cfg.device: messagebox.showerror(title, "No OpenCL device selected.\n" + install_help); return save_settings() run_state.update(stop=threading.Event(), st={}, t0=time.time(), wallet=a, last_poll=0, anim_seen=0) def work(): try: start_fn(cfg, out, run_state["stop"], run_state["st"]) except Exception as ex: out("ERROR: %s" % ex) q.put(None) run_state["th"] = threading.Thread(target=work, daemon=True); run_state["th"].start() start_b.state(["disabled"]); stop_b.state(["!disabled"]); out("started") def stop(): if run_state["stop"]: run_state["stop"].set(); out("stopping…"); stop_b.state(["disabled"]) start_b.config(command=start); stop_b.config(command=stop) def poll_wallet(a, sv): try: r = api(sv, "/api/wallet?addr=" + a) if "eddy" in r: run_state["st"].update(pending=r["pendingReward"], final=r["eddy"]) except Exception: pass def tick(): try: while True: m = q.get_nowait() if m is None: start_b.state(["!disabled"]); stop_b.state(["disabled"]); log.insert("end", time.strftime("%H:%M:%S ") + "stopped\n"); log.see("end"); continue log.insert("end", m + "\n"); log.see("end") if int(log.index("end-1c").split(".")[0]) > 600: log.delete("1.0", "100.0") except queue.Empty: pass st = run_state["st"] alive = bool(run_state["th"] and run_state["th"].is_alive()) anim.update(alive, bool(st.get("hashing")), st.get("hashrate", 0)) seen = run_state.get("anim_seen", 0); acc = st.get("accepted", 0) if acc > seen: # real accepted share / block -> snap + coin (at most 3 queued) last = str(st.get("last", "")) for _ in range(min(3, acc - seen)): anim.event("block" if ("BLOCK" in last or last.startswith("accepted")) else "share") run_state["anim_seen"] = acc if run_state["t0"]: hr = st.get("hashrate", 0) labels["hashrate"].config(text=("%.2f MH/s" % (hr / 1e6)) if hr < 1e9 else ("%.2f GH/s" % (hr / 1e9))) labels["bits"].config(text="%s bits" % st.get("bits", "-")); labels["found"].config(text=str(st.get("found", 0))) labels["accrej"].config(text="%d / %d" % (st.get("accepted", 0), st.get("rejected", 0))); labels["last"].config(text=str(st.get("last", "-"))[:40]) if "pending" in st: labels["pending"].config(text=fmt(st["pending"]) + " EDDY"); labels["final"].config(text=fmt(st["final"]) + " EDDY") if run_state["th"] and run_state["th"].is_alive(): if st.get("server"): in_use.config(text="in use: %s%s" % (st["server"], " (switched from %s)" % server.get() if st["server"] != server.get() else "")) e = int(time.time() - run_state["t0"]); labels["elapsed"].config(text="%d:%02d:%02d" % (e // 3600, e // 60 % 60, e % 60)) if time.time() - run_state["last_poll"] > 15: run_state["last_poll"] = time.time(); threading.Thread(target=poll_wallet, args=(run_state["wallet"], st.get("server") or server.get().strip().rstrip("/")), daemon=True).start() root.after(500, tick) def on_close(): save_settings() if run_state["stop"]: run_state["stop"].set() if run_state["th"]: run_state["th"].join(timeout=5) root.destroy() root.protocol("WM_DELETE_WINDOW", on_close) root.after(500, tick) out("ready - enter your address and press START") if pool_default and first_run: out("Server: the official Eddycoin pool %s (default). For solo mining pick or add a normal server, e.g. %s" % (pool_default, default_server)) elif pool_default and not S.get("pool_offered"): out("NEW: the official Eddycoin pool %s is now in the Server list - select it to mine in the pool. Your saved server stays selected." % pool_default) S["pool_offered"] = True if auto_v.get() and os.environ.get("EDDY_GUI_AUTOSTART") != "1": if not _CH.ACTIVE: out("Automatic start is on - the new Eddycoin chain is not live yet, so nothing is mined until it is activated.") elif kind == "gpu" and not any(d["gpu"] for d in devs): out("Automatic start skipped: no OpenCL graphics card was found (see the note above). The CPU miner can still mine.") elif ADDR_RE.match(addr.get().strip()): out("automatic start: %s at %d %% intensity%s" % (kind.upper(), intensity.get(), (", %d threads" % threads.get()) if kind == "cpu" else "")) root.after(2000, start) if os.environ.get("EDDY_GUI_AUTOSTART") == "1": # automated tests only root.after(1500, start) root.mainloop()