2022-06-17 04:34:41 +00:00
|
|
|
#!/bin/env python3
|
2022-07-07 13:38:59 +00:00
|
|
|
import sys
|
2022-06-17 04:34:41 +00:00
|
|
|
import time
|
|
|
|
import json
|
|
|
|
from jinja2 import Template
|
|
|
|
import npyscreen
|
|
|
|
|
2022-07-07 13:38:59 +00:00
|
|
|
# Help text in a box at the bottom of the screen
|
|
|
|
class HelpBoxBase(npyscreen.BoxTitle):
|
|
|
|
def splitlines(self):
|
|
|
|
all_help = []
|
|
|
|
for fld, msg in self.help_msgs.items():
|
|
|
|
if fld[-1] != ":":
|
|
|
|
fld += ":"
|
|
|
|
message = [m.lstrip() for m in msg if m != ""]
|
|
|
|
helpline = f"{fld:<28}{'. '.join(message)}"
|
|
|
|
all_help.append(helpline)
|
|
|
|
all_help.append("")
|
|
|
|
return all_help
|
2022-06-17 04:34:41 +00:00
|
|
|
|
2022-07-07 13:38:59 +00:00
|
|
|
def display_help_message(self, field):
|
|
|
|
self.set_values(
|
|
|
|
self.help_msgs.get(field, "No help availabe for {}".format(field))
|
|
|
|
)
|
|
|
|
self.clear()
|
|
|
|
self.display()
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
|
2022-07-07 13:38:59 +00:00
|
|
|
class P2PoolHelpBox(HelpBoxBase):
|
|
|
|
help_msgs = {
|
|
|
|
"Wallet Address:": [
|
|
|
|
"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!",
|
|
|
|
],
|
|
|
|
"P2Pool Sidechain:": [
|
|
|
|
"Which P2Pool sidechain to mine on",
|
|
|
|
" use main for faster miners",
|
|
|
|
" use mini for slower miners",
|
|
|
|
],
|
|
|
|
"Enable Server Statistics": [
|
|
|
|
"Provide access to your P2Pool server statictics via a web interface",
|
|
|
|
],
|
|
|
|
"Statistics Port:": [
|
|
|
|
"Port number (or IP:Port) to expose web access to your P2Pool server",
|
|
|
|
"statictics",
|
|
|
|
],
|
|
|
|
"Expose Stratum Port": [
|
|
|
|
"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",
|
|
|
|
],
|
|
|
|
"Stratum Port:": [
|
|
|
|
"Port number (or IP:Port) to expose P2Pool stratum to your network to",
|
|
|
|
"allow external miners to connect",
|
|
|
|
"Note: You may choose to open this port in your hosts firewall and/or",
|
|
|
|
" router to allow miners outside your network to connect",
|
|
|
|
],
|
|
|
|
"P2Pool Log Level:": [
|
|
|
|
"Verbosity of the log; (Less) 0 - 6 (More)",
|
|
|
|
],
|
|
|
|
"Enable Autodiff": [
|
|
|
|
"Use automatic difficulty adjustment for miners connected to stratum",
|
|
|
|
],
|
|
|
|
"Enable Light Mode": [
|
|
|
|
"Don't allocate RandomX dataset, saves 2GB of RAM",
|
|
|
|
],
|
|
|
|
"Disable Cache": [
|
|
|
|
"Disable p2pool.cache (not recommended)",
|
|
|
|
],
|
|
|
|
"Additional P2Pool Options:": [
|
|
|
|
"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",
|
|
|
|
],
|
|
|
|
"Next": [
|
|
|
|
"Next configuration menu",
|
|
|
|
],
|
|
|
|
"Save": [
|
|
|
|
"Save current configuration and exit",
|
|
|
|
],
|
|
|
|
"Cancel": [
|
|
|
|
"Exit without saving",
|
|
|
|
],
|
|
|
|
}
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
|
2022-07-07 13:38:59 +00:00
|
|
|
class MoneroHelpBox(HelpBoxBase):
|
|
|
|
help_msgs = {
|
|
|
|
"Configure Monero Node": [
|
|
|
|
"Configure and run a Monero Node",
|
|
|
|
"",
|
|
|
|
"Note: You must either configure a local node or specify a public node",
|
|
|
|
],
|
|
|
|
"Monero Version:": [
|
|
|
|
"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",
|
|
|
|
],
|
|
|
|
"Prune Blockchain": [
|
|
|
|
"Prune the Monero node to limit the size of the blockchain on disk",
|
|
|
|
],
|
|
|
|
"Monero Log Level:": [
|
|
|
|
"Verbosity of the log; (Less) 0 - 4 (More)",
|
|
|
|
"",
|
|
|
|
"Note: settings above 0 are very noisy",
|
|
|
|
],
|
|
|
|
"Expose RPC Port": [
|
|
|
|
"Expose restricted RPC API port to your network so external services",
|
|
|
|
"(wallets for example) can connect",
|
|
|
|
"Note: You may choose to open this port in your hosts firewall and/or",
|
|
|
|
" router to allow services outside your network to connect",
|
|
|
|
],
|
|
|
|
"RPC Port:": [
|
|
|
|
"TCP port to listen on for RPC connections",
|
|
|
|
],
|
|
|
|
"RPC Login:": [
|
|
|
|
"Specify username[:password] required to connect to the RPC API",
|
|
|
|
],
|
|
|
|
"Limit Data Rates": [
|
|
|
|
"Set a limit value for incoming and outgoing data transfer",
|
|
|
|
],
|
|
|
|
"Rate Limit Up:": [
|
|
|
|
"Set outgoing data transfer limit [kB/s]",
|
|
|
|
],
|
|
|
|
"Rate Limit Down:": [
|
|
|
|
"Set incoming data transfer limit [kB/s]",
|
|
|
|
],
|
|
|
|
"Sync Pruned Blocks": [
|
|
|
|
"Accept pruned blocks instead of pruning yourself to save",
|
|
|
|
"network transfer",
|
|
|
|
],
|
|
|
|
"Fast Block Sync": [
|
|
|
|
'Sync up most of the way by using embedded, "known" (old) block',
|
|
|
|
"hashes without calculating the block hash to verify the proof of work",
|
|
|
|
"",
|
|
|
|
"Note: Faster initial sync by trusting the monerod binary",
|
|
|
|
],
|
|
|
|
"Public Node:": [
|
|
|
|
"Public Monero Node to Use",
|
|
|
|
"",
|
|
|
|
"Note: The public node must have both Monero RPC and zmq-pub ports",
|
|
|
|
" available",
|
|
|
|
],
|
|
|
|
"Node Login:": [
|
|
|
|
"Specify username[:password] required to connect to public monero",
|
|
|
|
"node RPC API (if required)",
|
|
|
|
],
|
|
|
|
"Additional monerod Options:": [
|
|
|
|
"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/'",
|
|
|
|
],
|
|
|
|
"Prev": [
|
|
|
|
"Previous configuration menu",
|
|
|
|
],
|
|
|
|
"Next": [
|
|
|
|
"Next configuration menu",
|
|
|
|
],
|
|
|
|
"Save": [
|
|
|
|
"Save current configuration and exit",
|
|
|
|
],
|
|
|
|
"Cancel": [
|
|
|
|
"Exit without saving",
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class XMRigHelpBox(HelpBoxBase):
|
|
|
|
help_msgs = {
|
|
|
|
"Configure XMRig CPU Miner": [
|
|
|
|
"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",
|
|
|
|
],
|
|
|
|
"Username:": [
|
|
|
|
"Set a username for the miner",
|
|
|
|
],
|
|
|
|
"Use Fixed Difficulty": [
|
|
|
|
"Used a fixed minig difficulty",
|
|
|
|
"",
|
|
|
|
"Note: Allows you to see XMRig submitting shares below P2Pool threshold",
|
|
|
|
],
|
|
|
|
"Fixed Difficulty:": [
|
|
|
|
"Set a fixed mining difficulty",
|
|
|
|
"",
|
|
|
|
"Note: Allows you to see XMRig submitting shares below P2Pool threshold",
|
|
|
|
],
|
|
|
|
"CPU Use %:": [
|
|
|
|
"Maximum CPU threads count (in percentage) hint for autoconfig",
|
|
|
|
"Note: Applies to cores only. If you have HyperThreading enabled you",
|
|
|
|
" should divide this value by 2 (use 0-50%). Reference:",
|
|
|
|
" https://github.com/xmrig/xmrig/issues/1670#issuecomment-644433778",
|
|
|
|
],
|
|
|
|
"CPU Priority:": [
|
|
|
|
"Set process priority (0 idle, 2 normal to 5 highest)",
|
|
|
|
],
|
|
|
|
"Additional XMRig Options:": [
|
|
|
|
"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'",
|
|
|
|
],
|
|
|
|
"Prev": [
|
|
|
|
"Previous configuration menu",
|
|
|
|
],
|
|
|
|
"Save": [
|
|
|
|
"Save current configuration and exit",
|
|
|
|
],
|
|
|
|
"Cancel": [
|
|
|
|
"Exit without saving",
|
|
|
|
],
|
|
|
|
}
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
2022-07-07 13:38:59 +00:00
|
|
|
class PrevButton(npyscreen.Button):
|
|
|
|
def whenToggled(self):
|
|
|
|
self.value = False
|
|
|
|
self.parent.prev_form()
|
|
|
|
|
|
|
|
|
|
|
|
class NextButton(npyscreen.Button):
|
|
|
|
def whenToggled(self):
|
|
|
|
self.value = False
|
|
|
|
self.parent.next_form()
|
|
|
|
|
|
|
|
|
2022-06-17 04:34:41 +00:00
|
|
|
class SaveButton(npyscreen.Button):
|
|
|
|
def whenToggled(self):
|
2022-07-07 13:38:59 +00:00
|
|
|
self.find_parent_app().save_and_exit()
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CancelButton(npyscreen.Button):
|
|
|
|
def whenToggled(self):
|
2022-07-07 13:38:59 +00:00
|
|
|
self.find_parent_app().cancel_and_exit()
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
##
|
2022-07-07 13:38:59 +00:00
|
|
|
# Config Forms Base Class
|
|
|
|
class ConfigFormBase(npyscreen.FormBaseNew):
|
2022-06-17 04:34:41 +00:00
|
|
|
name_size = 20
|
|
|
|
indent = 5
|
2022-07-07 13:38:59 +00:00
|
|
|
current_config = None
|
|
|
|
defaults = None
|
|
|
|
ALLOW_RESIZE = False
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
def while_editing(self, arg):
|
|
|
|
self.help.display_help_message(arg.name)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.display()
|
|
|
|
|
|
|
|
def get_default_config(self):
|
|
|
|
# Return defaults
|
|
|
|
if self.defaults is None:
|
|
|
|
with open("defaults") as defaults_file:
|
|
|
|
self.defaults = json.load(defaults_file)
|
|
|
|
return self.defaults
|
|
|
|
|
|
|
|
def get_current_config(self):
|
|
|
|
# Return current config
|
|
|
|
if self.current_config is None:
|
|
|
|
# Read current config
|
|
|
|
with open("/docker-compose/current_config") as current_config:
|
|
|
|
self.current_config = json.load(current_config)
|
|
|
|
return self.current_config
|
|
|
|
|
|
|
|
def reset_defaults(self, arg):
|
|
|
|
# Set config to default values
|
|
|
|
ok = npyscreen.notify_ok_cancel(
|
|
|
|
"Set current form values to defaults", title="Reset to Defaults"
|
|
|
|
)
|
|
|
|
if not ok:
|
|
|
|
return
|
|
|
|
defaults = self.get_default_config()
|
|
|
|
self.set_config(defaults)
|
|
|
|
|
2022-06-17 04:34:41 +00:00
|
|
|
|
2022-07-07 13:38:59 +00:00
|
|
|
##
|
|
|
|
# P2Pool Configuration Form
|
|
|
|
class P2PoolConfigForm(ConfigFormBase):
|
2022-06-17 04:34:41 +00:00
|
|
|
def create(self):
|
|
|
|
# Add Hot-Key Controls
|
2022-07-07 13:38:59 +00:00
|
|
|
self.add_handlers({"^D": self.reset_defaults})
|
2022-06-17 04:34:41 +00:00
|
|
|
# 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,
|
|
|
|
)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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,
|
|
|
|
)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
|
|
|
self.enable_statistics = self.add(
|
|
|
|
PatchedFormControlCheckbox,
|
|
|
|
name="Enable Server Statistics",
|
|
|
|
value=True,
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.statistics_port = self.add(
|
|
|
|
npyscreen.TitleText,
|
|
|
|
name="Statistics Port:",
|
|
|
|
value="3334",
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.enable_statistics.addVisibleWhenSelected(self.statistics_port)
|
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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,
|
|
|
|
)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
self.autodiff = self.add(
|
|
|
|
PatchedFormControlCheckbox,
|
|
|
|
name="Enable Autodiff",
|
|
|
|
value=True,
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
|
|
|
self.light_mode = self.add(
|
|
|
|
PatchedFormControlCheckbox,
|
|
|
|
name="Enable Light Mode",
|
|
|
|
value=False,
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.nextrely += 1
|
|
|
|
self.no_cache = self.add(
|
|
|
|
npyscreen.Checkbox,
|
|
|
|
name="Disable Cache",
|
|
|
|
value=False,
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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
|
2022-07-07 13:38:59 +00:00
|
|
|
# Add "Next", "Save", and "Cancel" buttons
|
|
|
|
self.next_button = self.add(NextButton, name="Next", relx=1)
|
|
|
|
self.nextrely -= 1
|
|
|
|
self.save_button = self.add(SaveButton, name="Save", relx=8)
|
|
|
|
self.nextrely -= 1
|
|
|
|
self.cancel_button = self.add(CancelButton, name="Cancel", relx=15)
|
|
|
|
self.nextrely += 1
|
|
|
|
# Add Help Box
|
|
|
|
self.help = self.add(
|
|
|
|
P2PoolHelpBox,
|
|
|
|
name="Commands: ^D: Load Defaults - ^C: Exit Without Saving",
|
|
|
|
values=[""],
|
|
|
|
editable=False,
|
|
|
|
)
|
|
|
|
# Start with current config
|
|
|
|
self.set_config(self.get_current_config())
|
|
|
|
|
|
|
|
def next_form(self):
|
|
|
|
self.find_parent_app().switchForm("MONERO")
|
|
|
|
|
|
|
|
def set_config(self, config):
|
|
|
|
self.wallet_address.set_value(config["wallet_address"])
|
|
|
|
self.sidechain.set_value(config["sidechain"])
|
|
|
|
self.enable_statistics.set_value(config["enable_statistics"])
|
|
|
|
self.statistics_port.set_value(config["statistics_port"])
|
|
|
|
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.light_mode.set_value(config["light_mode"])
|
|
|
|
self.no_cache.value = config["no_cache"]
|
|
|
|
self.p2pool_extra.set_value(config["p2pool_options"])
|
|
|
|
self.DISPLAY()
|
|
|
|
|
|
|
|
def get_config(self):
|
|
|
|
config = {
|
|
|
|
"wallet_address": self.wallet_address.value,
|
|
|
|
"sidechain": self.sidechain.value,
|
|
|
|
"enable_statistics": self.enable_statistics.value,
|
|
|
|
"statistics_port": self.statistics_port.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,
|
|
|
|
"light_mode": self.light_mode.value,
|
|
|
|
"no_cache": self.no_cache.value,
|
|
|
|
"p2pool_options": self.p2pool_extra.value,
|
|
|
|
}
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Monero Configuration Form
|
|
|
|
class MoneroConfigForm(ConfigFormBase):
|
|
|
|
def create(self):
|
|
|
|
# Add Hot-Key Controls
|
|
|
|
self.add_handlers({"^D": self.reset_defaults})
|
2022-06-17 04:34:41 +00:00
|
|
|
# Add Monero Configuration
|
|
|
|
self.add(
|
|
|
|
npyscreen.TitleText,
|
|
|
|
name="## Monero Node Configuration",
|
|
|
|
editable=False,
|
|
|
|
begin_entry_at=50,
|
|
|
|
)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.configure_monero_node = self.add(
|
|
|
|
PatchedFormControlCheckbox,
|
|
|
|
name="Configure Monero Node",
|
|
|
|
value=True,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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(
|
2022-07-07 13:38:59 +00:00
|
|
|
PatchedFormControlCheckbox,
|
2022-06-17 04:34:41 +00:00
|
|
|
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)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
|
|
|
self.expose_rpc_port = self.add(
|
|
|
|
PatchedFormControlCheckbox,
|
|
|
|
name="Expose RPC Port",
|
|
|
|
value=False,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.configure_monero_node.addVisibleWhenSelected(self.expose_rpc_port)
|
|
|
|
self.rpc_port = self.add(
|
2022-06-17 04:34:41 +00:00
|
|
|
npyscreen.TitleText,
|
2022-07-07 13:38:59 +00:00
|
|
|
name="RPC Port:",
|
|
|
|
value="18081",
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.configure_monero_node.addVisibleWhenSelected(self.rpc_port)
|
|
|
|
self.expose_rpc_port.addVisibleWhenSelected(self.rpc_port)
|
|
|
|
self.rpc_login = self.add(
|
|
|
|
npyscreen.TitleText,
|
|
|
|
name="RPC Login:",
|
2022-06-17 04:34:41 +00:00
|
|
|
value="",
|
2022-07-07 13:38:59 +00:00
|
|
|
begin_entry_at=self.name_size,
|
2022-06-17 04:34:41 +00:00
|
|
|
relx=self.indent,
|
|
|
|
)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.expose_rpc_port.addVisibleWhenSelected(self.rpc_login)
|
|
|
|
self.configure_monero_node.addVisibleWhenSelected(self.rpc_login)
|
|
|
|
self.nextrely += 1
|
|
|
|
self.limit_data_rates = self.add(
|
|
|
|
PatchedFormControlCheckbox,
|
|
|
|
name="Limit Data Rates",
|
|
|
|
value=False,
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.configure_monero_node.addVisibleWhenSelected(self.limit_data_rates)
|
|
|
|
self.rate_limit_up = self.add(
|
|
|
|
npyscreen.TitleText,
|
|
|
|
name="Rate Limit Up:",
|
|
|
|
value="2048",
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.limit_data_rates.addVisibleWhenSelected(self.rate_limit_up)
|
|
|
|
self.configure_monero_node.addVisibleWhenSelected(self.rate_limit_up)
|
|
|
|
self.rate_limit_down = self.add(
|
|
|
|
npyscreen.TitleText,
|
|
|
|
name="Rate Limit Down:",
|
|
|
|
value="8192",
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.limit_data_rates.addVisibleWhenSelected(self.rate_limit_down)
|
|
|
|
self.configure_monero_node.addVisibleWhenSelected(self.rate_limit_down)
|
|
|
|
self.nextrely += 1
|
|
|
|
self.sync_pruned_blocks = self.add(
|
|
|
|
npyscreen.Checkbox,
|
|
|
|
name="Sync Pruned Blocks",
|
|
|
|
value=False,
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.configure_monero_node.addVisibleWhenSelected(self.sync_pruned_blocks)
|
|
|
|
self.prune_node.addVisibleWhenSelected(self.sync_pruned_blocks)
|
|
|
|
self.fast_sync = self.add(
|
|
|
|
npyscreen.Checkbox,
|
|
|
|
name="Fast Block Sync",
|
2023-04-22 20:54:32 +00:00
|
|
|
value=True,
|
2022-07-07 13:38:59 +00:00
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.configure_monero_node.addVisibleWhenSelected(self.fast_sync)
|
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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
|
2022-07-07 13:38:59 +00:00
|
|
|
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)
|
2022-06-17 04:34:41 +00:00
|
|
|
self.nextrely += 1
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
|
|
|
# Add "Prev", "Next", "Save", and "Cancel" buttons
|
|
|
|
self.prev_button = self.add(PrevButton, name="Prev", relx=1)
|
|
|
|
self.nextrely -= 1
|
|
|
|
self.next_button = self.add(NextButton, name="Next", relx=8)
|
|
|
|
self.nextrely -= 1
|
|
|
|
self.save_button = self.add(SaveButton, name="Save", relx=15)
|
|
|
|
self.nextrely -= 1
|
|
|
|
self.cancel_button = self.add(CancelButton, name="Cancel", relx=22)
|
|
|
|
self.nextrely += 1
|
|
|
|
# Add Help Box
|
|
|
|
self.help = self.add(
|
|
|
|
MoneroHelpBox,
|
|
|
|
name="Commands: ^D: Load Defaults - ^C: Exit Without Saving",
|
|
|
|
values=[""],
|
|
|
|
editable=False,
|
|
|
|
)
|
|
|
|
# Start with current config
|
|
|
|
self.set_config(self.get_current_config())
|
|
|
|
|
|
|
|
def prev_form(self):
|
|
|
|
self.find_parent_app().switchForm("MAIN")
|
|
|
|
|
|
|
|
def next_form(self):
|
|
|
|
self.find_parent_app().switchForm("XMRIG")
|
|
|
|
|
|
|
|
def set_config(self, config):
|
|
|
|
self.configure_monero_node.set_value(config["configure_monero"])
|
|
|
|
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.expose_rpc_port.set_value(config["expose_rpc_port"])
|
|
|
|
self.rpc_port.set_value(config["rpc_port"])
|
|
|
|
self.rpc_login.set_value(config["rpc_login"])
|
|
|
|
self.limit_data_rates.set_value(config["limit_data_rates"])
|
|
|
|
self.rate_limit_up.set_value(config["rate_limit_up"])
|
|
|
|
self.rate_limit_down.set_value(config["rate_limit_down"])
|
|
|
|
self.sync_pruned_blocks.value = config["sync_pruned_blocks"]
|
|
|
|
self.fast_sync.value = config["fast_sync"]
|
|
|
|
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.DISPLAY()
|
|
|
|
|
|
|
|
def get_config(self):
|
|
|
|
config = {
|
|
|
|
"configure_monero": self.configure_monero_node.value,
|
|
|
|
"monero_version": self.monero_git_tag.value,
|
|
|
|
"prune_blockchain": self.prune_node.value,
|
|
|
|
"monero_log_level": self.monero_log_level.value,
|
|
|
|
"expose_rpc_port": self.expose_rpc_port.value,
|
|
|
|
"rpc_port": self.rpc_port.value,
|
|
|
|
"rpc_login": self.rpc_login.value,
|
|
|
|
"limit_data_rates": self.limit_data_rates.value,
|
|
|
|
"rate_limit_up": self.rate_limit_up.value,
|
|
|
|
"rate_limit_down": self.rate_limit_down.value,
|
|
|
|
"sync_pruned_blocks": self.sync_pruned_blocks.value,
|
|
|
|
"fast_sync": self.fast_sync.value,
|
|
|
|
"monero_options": self.monero_extra.value,
|
|
|
|
"public_monero_node": self.public_node.value,
|
|
|
|
"monero_node_login": self.node_login.value,
|
|
|
|
}
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# XMRig Configuration Form
|
|
|
|
class XMRigConfigForm(ConfigFormBase):
|
|
|
|
def create(self):
|
|
|
|
# Add Hot-Key Controls
|
|
|
|
self.add_handlers({"^D": self.reset_defaults})
|
|
|
|
# Hidden caryover from P2Pool form
|
|
|
|
self.autodiff = self.add(
|
|
|
|
PatchedFormControlCheckbox,
|
|
|
|
name="Enable Autodiff",
|
|
|
|
value=True,
|
|
|
|
begin_entry_at=self.name_size,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.autodiff.hide = True
|
|
|
|
self.autodiff.editable = False
|
|
|
|
self.nextrely -= 1
|
2022-06-17 04:34:41 +00:00
|
|
|
# Add XMRig Configuration
|
|
|
|
self.add(
|
|
|
|
npyscreen.TitleText,
|
|
|
|
name="## XMRig Miner Configuration",
|
|
|
|
editable=False,
|
|
|
|
begin_entry_at=50,
|
|
|
|
)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.configure_xmrig_miner = self.add(
|
|
|
|
PatchedFormControlCheckbox,
|
|
|
|
name="Configure XMRig CPU Miner",
|
|
|
|
value=True,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.nextrely += 1
|
|
|
|
self.cpu_priority = self.add(
|
|
|
|
TitleIntegerSlider,
|
|
|
|
name="CPU Priority:",
|
|
|
|
out_of=5,
|
|
|
|
value=2,
|
|
|
|
lowest=0,
|
|
|
|
step=1,
|
|
|
|
width=48,
|
|
|
|
begin_entry_at=20,
|
|
|
|
label=True,
|
|
|
|
relx=self.indent,
|
|
|
|
)
|
|
|
|
self.configure_xmrig_miner.addVisibleWhenSelected(self.cpu_priority)
|
|
|
|
self.nextrely += 1
|
2022-06-17 04:34:41 +00:00
|
|
|
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
|
2022-07-07 13:38:59 +00:00
|
|
|
# Add "Prev", "Save", and "Cancel" buttons
|
|
|
|
self.prev_button = self.add(PrevButton, name="Prev", relx=1)
|
2022-06-17 04:34:41 +00:00
|
|
|
self.nextrely -= 1
|
2022-07-07 13:38:59 +00:00
|
|
|
self.save_button = self.add(SaveButton, name="Save", relx=8)
|
|
|
|
self.nextrely -= 1
|
|
|
|
self.cancel_button = self.add(CancelButton, name="Cancel", relx=15)
|
2022-06-17 04:34:41 +00:00
|
|
|
self.nextrely += 1
|
|
|
|
# Add Help Box
|
|
|
|
self.help = self.add(
|
2022-07-07 13:38:59 +00:00
|
|
|
XMRigHelpBox,
|
2022-06-17 04:34:41 +00:00
|
|
|
name="Commands: ^D: Load Defaults - ^C: Exit Without Saving",
|
|
|
|
values=[""],
|
|
|
|
editable=False,
|
|
|
|
)
|
|
|
|
# Start with current config
|
2022-07-07 13:38:59 +00:00
|
|
|
self.set_config(self.get_current_config())
|
|
|
|
|
|
|
|
def prev_form(self):
|
|
|
|
self.find_parent_app().switchForm("MONERO")
|
|
|
|
|
|
|
|
def next_form(self):
|
|
|
|
self.find_parent_app().switchForm(None)
|
|
|
|
|
|
|
|
def beforeEditing(self):
|
|
|
|
# Cary autodiff value from P2Pool config form
|
|
|
|
enable_autodiff = self.find_parent_app().get_p2pool_config()["enable_autodiff"]
|
|
|
|
self.autodiff.set_value(enable_autodiff)
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
def set_config(self, config):
|
|
|
|
self.configure_xmrig_miner.set_value(config["configure_xmrig"])
|
|
|
|
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"])
|
2022-07-07 13:38:59 +00:00
|
|
|
self.cpu_priority.set_value(config["cpu_priority"])
|
2022-06-17 04:34:41 +00:00
|
|
|
self.xmrig_extra.set_value(config["xmrig_options"])
|
|
|
|
self.DISPLAY()
|
|
|
|
|
|
|
|
def get_config(self):
|
|
|
|
config = {
|
|
|
|
"configure_xmrig": self.configure_xmrig_miner.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,
|
2022-07-07 13:38:59 +00:00
|
|
|
"cpu_priority": self.cpu_priority.value,
|
2022-06-17 04:34:41 +00:00
|
|
|
"xmrig_options": self.xmrig_extra.value,
|
|
|
|
}
|
|
|
|
return config
|
|
|
|
|
2022-07-07 13:38:59 +00:00
|
|
|
|
|
|
|
##
|
|
|
|
# Our P2Pool configuration App
|
|
|
|
class ConfigApp(npyscreen.NPSAppManaged):
|
|
|
|
current_config = None
|
|
|
|
defaults = None
|
|
|
|
|
|
|
|
def onStart(self):
|
|
|
|
self.p2pool_form = self.addForm(
|
|
|
|
"MAIN",
|
|
|
|
P2PoolConfigForm,
|
|
|
|
name="P2Pool for docker-compose: P2Pool Configuration",
|
|
|
|
minimum_lines=35,
|
|
|
|
minimum_columns=80,
|
2022-06-17 04:34:41 +00:00
|
|
|
)
|
2022-07-07 13:38:59 +00:00
|
|
|
self.monero_form = self.addForm(
|
|
|
|
"MONERO",
|
|
|
|
MoneroConfigForm,
|
|
|
|
name="P2Pool for docker-compose: Monero Configuration",
|
|
|
|
minimum_lines=35,
|
|
|
|
minimum_columns=80,
|
|
|
|
)
|
|
|
|
self.xmrig_form = self.addForm(
|
|
|
|
"XMRIG",
|
|
|
|
XMRigConfigForm,
|
|
|
|
name="P2Pool for docker-compose: XMRig Configuration",
|
|
|
|
minimum_lines=35,
|
|
|
|
minimum_columns=80,
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_p2pool_config(self):
|
|
|
|
return self.p2pool_form.get_config()
|
|
|
|
|
|
|
|
def get_config(self):
|
|
|
|
p2pool_config = self.p2pool_form.get_config()
|
|
|
|
monero_config = self.monero_form.get_config()
|
|
|
|
xmrig_config = self.xmrig_form.get_config()
|
|
|
|
return p2pool_config | monero_config | xmrig_config
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
def save_and_exit(self):
|
2022-07-07 13:38:59 +00:00
|
|
|
# Get config from all forms
|
2022-06-17 04:34:41 +00:00
|
|
|
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")
|
2022-07-07 13:38:59 +00:00
|
|
|
self.switchForm(None)
|
|
|
|
self.saved = True
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
def cancel_and_exit(self):
|
2022-07-07 13:38:59 +00:00
|
|
|
self.switchForm(None)
|
|
|
|
self.saved = False
|
2022-06-17 04:34:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
else:
|
|
|
|
print("Configuration Aborted")
|
2022-07-07 13:38:59 +00:00
|
|
|
sys.exit(1)
|
2022-06-17 04:34:41 +00:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
print("Configuration Aborted")
|
2022-07-07 13:38:59 +00:00
|
|
|
sys.exit(1)
|