#!/bin/env python3 import time import json from jinja2 import Template import npyscreen def load_default_config(): # Read defaults with open("defaults") as defaults_file: defaults = json.load(defaults_file) return defaults def load_current_config(): # Read current config with open("/docker-compose/current_config") as current_config: config = json.load(current_config) return config ## # Help text in a box at the bottom of the screen class HelpBox(npyscreen.BoxTitle): def display_help_message(self, field): if field == "Configure Monero Node": self.set_values( [ "Configure and run a Monero Node", "", "Note: You must either configure a local node or specify a public node", ] ) elif field == "Configure XMRig CPU Miner": self.set_values( [ "Configure and run an XMRig CPU Miner", "", "Note: You must either configure am XMRig CPU Miner or expose the", " P2Pool stratum port and connect an external miner, or both", ] ) elif field == "Wallet Address:": self.set_values( [ "Your monero wallet address for receiving mining reqards", "", "Note: You have to use a primary wallet address for mining", " Subaddresses and integrated addresses are not supported!", ] ) elif field == "P2Pool Sidechain:": self.set_values( [ "Which P2Pool sidechain to mine on", " use main for faster miners", " use mini for slower miners", ] ) elif field == "Expose Stratum Port": self.set_values( [ "Expose the P2Pool stratum port to your network so external miners", "can connect", "Note: You may choose to open this port in your hosts firewall and/or", " router to allow miners outside your network to connect", ] ) elif field == "Stratum Port:": self.set_values( [ "Port number to expose P2Pool stratum to your network to allow", "external miners to connect", ] ) elif field == "P2Pool Log Level:": self.set_values(["Verbosity of the log; (Less) 0 - 6 (More)"]) elif field == "Enable Autodiff": self.set_values( ["Use automatic difficulty adjustment for miners connected to stratum"] ) elif field == "Additional P2Pool Options:": self.set_values( [ "Additional options to pass to p2pool commandline", "", "Note: Advanced - Only add options if you know what you are doing", " See ouput of 'p2pool --help' for available options", ] ) elif field == "Monero Version:": self.set_values( [ "Version of Monero to build; 'latest' for the most recent release", "", "Note: Must be v0.17.3.0 or later for p2pool support", " See: https://github.com/monero-project/monero/tags", ] ) elif field == "Prune Blockchain": self.set_values( ["Prune the Monero node to limit the size of the blockchain on disk"] ) elif field == "Monero Log Level:": self.set_values(["Verbosity of the log; (Less) 0 - 4 (More)"]) elif field == "Additional monerod Options:": self.set_values( [ "Additional options to pass to monerod commandline", "", "Note: Advanced - Only add options if you know what you are doing", " See 'https://monerodocs.org/interacting/monerod-reference/'", ] ) elif field == "Public Node:": self.set_values( [ "Public Monero Node to Use", "", "Note: The public node must have both Monero RPC and zmq-pub ports", " available", ] ) elif field == "Node Login:": self.set_values( [ "Specify username[:password] required to connect to public monero", "node RPC API (if required)" ] ) elif field == "Username:": self.set_values(["Set a username for the miner"]) elif field == "Use Fixed Difficulty": self.set_values( [ "Used a fixed minig difficulty", "", "Note: Allows you to see XMRig submitting shares below P2Pool threshold", ] ) elif field == "Fixed Difficulty:": self.set_values( [ "Set a fixed mining difficulty", "", "Note: Allows you to see XMRig submitting shares below P2Pool threshold", ] ) elif field == "CPU Use %:": self.set_values( ["maximum CPU threads count (in percentage) hint for autoconfig"] ) elif field == "Additional XMRig Options:": self.set_values( [ "Additional options to pass to xmrig", "", "Note: Advanced - Only add options if you know what you are doing", " See 'https://xmrig.com/docs/miner/command-line-options'", ] ) elif field == "Save": self.set_values(["Save current configuration"]) elif field == "Cancel": self.set_values(["Exit without saving"]) self.clear() self.display() ## # Custom (integer) values for title slider class IntegerSlider(npyscreen.Slider): def translate_value(self): from_val = int(str(self.value).split(".")[0]) out_of_val = int(str(self.out_of).split(".")[0]) if from_val >= 1000: from_val = str(from_val / 1000).split(".")[0] + "K" out_of_val = str(out_of_val / 1000).split(".")[0] + "K" return "{}/{}".format(from_val, out_of_val) class TitleIntegerSlider(npyscreen.TitleSlider): _entry_type = IntegerSlider ## # Patched updateDependents for FormControlCheckbox class PatchedFormControlCheckbox(npyscreen.FormControlCheckbox): def updateDependents(self): if self.value: for w in self._visibleWhenSelected: try: w.fc_visible except AttributeError: w.fc_visible = {} w.fc_visible[self.name] = True for w in self._notVisibleWhenSelected: try: w.fc_visible except AttributeError: w.fc_visible = {} w.fc_visible[self.name] = False else: for w in self._visibleWhenSelected: try: w.fc_visible except AttributeError: w.fc_visible = {} w.fc_visible[self.name] = False for w in self._notVisibleWhenSelected: try: w.fc_visible except AttributeError: w.fc_visible = {} w.fc_visible[self.name] = True for w in self._visibleWhenSelected + self._notVisibleWhenSelected: w.hidden = False in w.fc_visible.values() w.editable = not False in w.fc_visible.values() self.parent.display() def set_value(self, value): self.value = value self.display() def display(self): self.updateDependents() super() class SaveButton(npyscreen.Button): def whenToggled(self): self.parent.save_and_exit() class CancelButton(npyscreen.Button): def whenToggled(self): self.parent.cancel_and_exit() ## # Configuration Form class ConfigForm(npyscreen.FormBaseNew): ALLOW_RESIZE = False name_size = 20 indent = 5 def while_editing(self, arg): self.help.display_help_message(arg.name) def create(self): # Add Hot-Key Controls self.add_handlers({"^D": self.load_defaults}) # Add Global Configuration self.add( npyscreen.TitleText, name="## General Configuration", editable=False, begin_entry_at=50, ) self.configure_monero_node = self.add( PatchedFormControlCheckbox, name="Configure Monero Node", value=True, relx=self.indent, ) self.configure_xmrig_miner = self.add( PatchedFormControlCheckbox, name="Configure XMRig CPU Miner", value=True, relx=self.indent, ) self.nextrely += 1 self.nextrely += 1 # Add P2Pool Configuration self.add( npyscreen.TitleText, name="## P2Pool Configuration", editable=False, begin_entry_at=50, ) self.wallet_address = self.add( npyscreen.TitleText, name="Wallet Address:", value="", begin_entry_at=self.name_size, relx=self.indent, ) self.sidechain = self.add( npyscreen.TitleSelectOne, name="P2Pool Sidechain:", values=["main", "mini"], scroll_exit=True, max_height=2, value=[ 0, ], begin_entry_at=self.name_size, relx=self.indent, ) self.expose_stratum_port = self.add( PatchedFormControlCheckbox, name="Expose Stratum Port", value=True, begin_entry_at=self.name_size, relx=self.indent, ) self.stratum_port = self.add( npyscreen.TitleText, name="Stratum Port:", value="3333", begin_entry_at=self.name_size, relx=self.indent, ) self.expose_stratum_port.addVisibleWhenSelected(self.stratum_port) self.p2pool_log_level = self.add( TitleIntegerSlider, name="P2Pool Log Level:", out_of=6, value=3, lowest=0, step=1, width=43, begin_entry_at=20, label=True, block_color=None, relx=self.indent, ) self.autodiff = self.add( PatchedFormControlCheckbox, name="Enable Autodiff", value=True, begin_entry_at=self.name_size, relx=self.indent, ) self.p2pool_extra = self.add( npyscreen.TitleText, name="Additional P2Pool Options:", value="", begin_entry_at=self.name_size + 10, relx=self.indent, ) self.nextrely += 1 self.nextrely += 1 # Add Monero Configuration self.add( npyscreen.TitleText, name="## Monero Node Configuration", editable=False, begin_entry_at=50, ) self.monero_git_tag = self.add( npyscreen.TitleText, name="Monero Version:", value="latest", begin_entry_at=self.name_size, relx=self.indent, ) self.configure_monero_node.addVisibleWhenSelected(self.monero_git_tag) self.prune_node = self.add( npyscreen.Checkbox, name="Prune Blockchain", value=True, begin_entry_at=self.name_size, relx=self.indent, ) self.configure_monero_node.addVisibleWhenSelected(self.prune_node) self.monero_log_level = self.add( TitleIntegerSlider, name="Monero Log Level:", out_of=4, value=0, lowest=0, step=1, width=43, begin_entry_at=20, label=True, block_color=None, relx=self.indent, ) self.configure_monero_node.addVisibleWhenSelected(self.monero_log_level) self.monero_extra = self.add( npyscreen.TitleText, name="Additional monerod Options:", value="", begin_entry_at=self.name_size + 10, relx=self.indent, ) self.configure_monero_node.addVisibleWhenSelected(self.monero_extra) self.public_node = self.add( npyscreen.TitleText, name="Public Node:", value="", begin_entry_at=self.name_size, relx=self.indent, ) self.configure_monero_node.addInvisibleWhenSelected(self.public_node) self.node_login = self.add( npyscreen.TitleText, name="Node Login:", value="", begin_entry_at=self.name_size, relx=self.indent, ) self.configure_monero_node.addInvisibleWhenSelected(self.node_login) self.nextrely += 1 self.nextrely += 1 # Add XMRig Configuration self.add( npyscreen.TitleText, name="## XMRig Miner Configuration", editable=False, begin_entry_at=50, ) self.username = self.add( npyscreen.TitleText, name="Username:", value="", begin_entry_at=self.name_size, relx=self.indent, ) self.configure_xmrig_miner.addVisibleWhenSelected(self.username) self.use_fixed_difficulty = self.add( PatchedFormControlCheckbox, name="Use Fixed Difficulty", value=True, relx=self.indent, ) self.configure_xmrig_miner.addVisibleWhenSelected(self.use_fixed_difficulty) self.autodiff.addInvisibleWhenSelected(self.use_fixed_difficulty) self.fixed_difficulty = self.add( TitleIntegerSlider, name="Fixed Difficulty:", out_of=2000000, value=500000, lowest=50000, step=50000, width=51, begin_entry_at=20, label=True, relx=self.indent, ) self.configure_xmrig_miner.addVisibleWhenSelected(self.fixed_difficulty) self.use_fixed_difficulty.addVisibleWhenSelected(self.fixed_difficulty) self.autodiff.addInvisibleWhenSelected(self.fixed_difficulty) self.cpu_threads = self.add( TitleIntegerSlider, name="CPU Use %:", out_of=100, value=100, lowest=1, step=10, width=48, begin_entry_at=20, label=True, relx=self.indent, ) self.configure_xmrig_miner.addVisibleWhenSelected(self.cpu_threads) self.xmrig_extra = self.add( npyscreen.TitleText, name="Additional XMRig Options:", value="", begin_entry_at=self.name_size + 10, relx=self.indent, ) self.configure_xmrig_miner.addVisibleWhenSelected(self.xmrig_extra) self.nextrely += 1 self.nextrely += 1 # Add "Save" button self.save_button = self.add(SaveButton, name="Save", relx=1) self.nextrely -= 1 self.cancel_button = self.add(CancelButton, name="Cancel", relx=8) self.nextrely += 1 # Add Help Box self.help = self.add( HelpBox, name="Commands: ^D: Load Defaults - ^C: Exit Without Saving", values=[""], rely=-8, editable=False, ) # Start with current config self.set_config(load_current_config()) def set_config(self, config): self.configure_monero_node.set_value(config["configure_monero"]) self.configure_xmrig_miner.set_value(config["configure_xmrig"]) self.wallet_address.set_value(config["wallet_address"]) self.sidechain.set_value(config["sidechain"]) self.expose_stratum_port.set_value(config["expose_stratum_port"]) self.stratum_port.set_value(config["stratum_port"]) self.p2pool_log_level.set_value(config["p2pool_log_level"]) self.autodiff.set_value(config["enable_autodiff"]) self.p2pool_extra.set_value(config["p2pool_options"]) self.monero_git_tag.set_value(config["monero_version"]) self.prune_node.value = config["prune_blockchain"] self.monero_log_level.set_value(config["monero_log_level"]) self.monero_extra.set_value(config["monero_options"]) self.public_node.set_value(config["public_monero_node"]) self.node_login.set_value(config["monero_node_login"]) self.username.set_value(config["xmrig_username"]) self.use_fixed_difficulty.set_value(config["use_fixed_difficulty"]) self.fixed_difficulty.set_value(config["fixed_difficulty"]) self.cpu_threads.set_value(config["cpu_percent"]) self.xmrig_extra.set_value(config["xmrig_options"]) self.DISPLAY() def get_config(self): config = { "configure_monero": self.configure_monero_node.value, "configure_xmrig": self.configure_xmrig_miner.value, "wallet_address": self.wallet_address.value, "sidechain": self.sidechain.value, "expose_stratum_port": self.expose_stratum_port.value, "stratum_port": self.stratum_port.value, "p2pool_log_level": self.p2pool_log_level.value, "enable_autodiff": self.autodiff.value, "p2pool_options": self.p2pool_extra.value, "monero_version": self.monero_git_tag.value, "prune_blockchain": self.prune_node.value, "monero_log_level": self.monero_log_level.value, "monero_options": self.monero_extra.value, "public_monero_node": self.public_node.value, "monero_node_login": self.node_login.value, "xmrig_username": self.username.value, "use_fixed_difficulty": self.use_fixed_difficulty.value, "fixed_difficulty": self.fixed_difficulty.value, "cpu_percent": self.cpu_threads.value, "xmrig_options": self.xmrig_extra.value, } return config # Control methods def load_defaults(self, arg): ok = npyscreen.notify_ok_cancel( "Set all values to defaults", title="Load Defaults" ) if not ok: return defaults = load_default_config() self.set_config(defaults) def save_and_exit(self): config = self.get_config() # Save "current config" values file with open("current_config.jinja2", "r") as current_config: template = current_config.read() rendered = Template(template).render(config) with open("/docker-compose/current_config", "w") as current_config: current_config.write(rendered) # Render and save docker-compose file with open("docker-compose.jinja2", "r") as compose_file: template = compose_file.read() rendered = Template(template).render(config, trim_blocks=True) with open("/docker-compose/docker-compose.yml", "w") as compose_file: compose_file.write(rendered) npyscreen.notify("Saved current settings", title="Saved") self.find_parent_app().switchForm(None) self.find_parent_app().saved = True def cancel_and_exit(self): self.find_parent_app().switchForm(None) self.find_parent_app().saved = False ## # Our P2Pool configuration App class ConfigApp(npyscreen.NPSAppManaged): def onStart(self): self.f = self.addForm( "MAIN", ConfigForm, name="P2Pool for docker-compose: Global Configuration", lines=45, columns=80, minimum_lines=45, minimum_columns=80, ) if __name__ == "__main__": try: time.sleep(1) # Give docker a second to initialize the terminal App = ConfigApp() App.run() print("\n\n") if App.saved: print("Configuration Saved") print( 'Run "docker compose up -d" to start (if you are using the docker-compose plugin' ) print( 'or, "docker-compose up -d" (if you are using pip installed docker-compose)' ) else: print("Configuration Aborted") except KeyboardInterrupt: print("Configuration Aborted")