class Plugin(AbstractPlugin): # ---------- Environment Variables ---------- # BITCOIN_NETWORK = os.getenv("BITCOIN_NETWORK", "regtest") # For documentation purposes only (these env vars will be detected by electrumsv too) ELECTRUMSV_ACCOUNT_XPRV = os.getenv("ELECTRUMSV_ACCOUNT_XPRV") BITCOIN_NODE_HOST = os.environ.get("BITCOIN_NODE_HOST") or "127.0.0.1" BITCOIN_NODE_PORT = os.environ.get("BITCOIN_NODE_PORT") or 18332 BITCOIN_NODE_RPCUSER = os.environ.get("BITCOIN_NODE_RPCUSER") or "rpcuser" BITCOIN_NODE_RPCPASSWORD = os.environ.get( "BITCOIN_NODE_RPCPASSWORD") or "rpcpassword" RESTAPI_HOST = os.environ.get("RESTAPI_HOST") DEFAULT_PORT = 9999 RESERVED_PORTS: Set[int] = {DEFAULT_PORT} COMPONENT_NAME = get_directory_name(__file__) DEFAULT_REMOTE_REPO = "https://github.com/electrumsv/electrumsv.git" DEFAULT_BRANCH = "develop" def __init__(self, cli_inputs: CLIInputs): self.cli_inputs = cli_inputs self.config = Config() self.plugin_tools = PluginTools(self, self.cli_inputs) self.tools = LocalTools(self) self.logger = logging.getLogger(self.COMPONENT_NAME) self.src = self.plugin_tools.get_source_dir(dirname="electrumsv") self.datadir = None # dynamically allocated self.id = None # dynamically allocated self.port = None # dynamically allocated self.component_info: Optional[Component] = None self.network = self.BITCOIN_NETWORK def install(self) -> None: """required state: source_dir - which are derivable from 'repo' and 'branch' flags""" self.plugin_tools.modify_pythonpath_for_portability(self.src) repo = self.cli_inputs.repo if self.cli_inputs.repo == "": repo = self.DEFAULT_REMOTE_REPO branch = self.cli_inputs.branch if self.cli_inputs.branch == "": branch = self.DEFAULT_BRANCH if is_remote_repo(repo): self.tools.fetch_electrumsv(repo, branch) self.tools.packages_electrumsv(repo, branch) self.logger.debug(f"Installed {self.COMPONENT_NAME}") def start(self) -> None: """plugin datadir, id, port are allocated dynamically""" self.plugin_tools.modify_pythonpath_for_portability(self.src) self.logger.debug(f"Starting RegTest electrumsv daemon...") assert self.src is not None if not self.src.exists(): self.logger.error( f"source code directory does not exist - try 'electrumsv-sdk install " f"{self.COMPONENT_NAME}' to install the plugin first") sys.exit(1) self.tools.process_cli_args() self.datadir, self.id = self.plugin_tools.allocate_datadir_and_id() self.port = self.plugin_tools.allocate_port() logfile = self.plugin_tools.get_logfile_path(self.id) metadata = ComponentMetadata(config_path=str( self.datadir.joinpath("regtest/cli_inputs")), datadir=str(self.datadir)) status_endpoint = f"http://127.0.0.1:{self.port}" os.makedirs(self.datadir.joinpath("regtest/wallets"), exist_ok=True) if self.tools.is_offline_cli_mode(): # 'reset' recurses into here... command, env_vars = self.tools.generate_command() self.plugin_tools.spawn_process(command, env_vars=env_vars, id=self.id, component_name=self.COMPONENT_NAME, src=self.src, logfile=logfile, status_endpoint=status_endpoint, metadata=metadata) if self.tools.wallet_db_exists(): if self.cli_inputs.cli_extension_args['deterministic_seed']: if self.tools.wallet_db_exists(): raise ValueError( f"Cannot set a deterministic seed. This wallet: '{self.id}' " f"already exists. Please try 'electrumsv-sdk reset --deterministic-seed " f"--id={self.id}' or create a new wallet with a new --id." ) # If daemon or gui mode continue... elif not self.tools.wallet_db_exists(): if self.cli_inputs.cli_extension_args['deterministic_seed']: set_deterministic_electrumsv_seed( self.cli_inputs.selected_component, self.cli_inputs.component_id) # reset wallet self.tools.delete_wallet(datadir=self.datadir) self.tools.create_wallet(datadir=self.datadir, wallet_name='worker1.sqlite') if not self.tools.wallet_db_exists(): self.logger.exception("wallet db creation failed unexpectedly") command, env_vars = self.tools.generate_command() self.plugin_tools.spawn_process(command, env_vars=env_vars, id=self.id, component_name=self.COMPONENT_NAME, src=self.src, logfile=logfile, status_endpoint=status_endpoint, metadata=metadata) def stop(self) -> None: """some components require graceful shutdown via a REST API or RPC API but most can use the generic 'plugin_tools.kill_component()' function.""" is_new_terminal = not (self.cli_inputs.inline_flag or self.cli_inputs.background_flag) self.plugin_tools.call_for_component_id_or_type( self.COMPONENT_NAME, callable=partial(kill_process, graceful_wait_period=5.0, is_new_terminal=is_new_terminal)) self.logger.info( f"stopped selected {self.COMPONENT_NAME} instance (if running)") def reset(self) -> None: """ reset_electrumsv will be called many times for different component ids if applicable. - the reset entrypoint is only relevant for RegTest """ self.plugin_tools.modify_pythonpath_for_portability(self.src) def reset_electrumsv(component_dict: ComponentTypedDict) -> None: self.logger.debug( "Resetting state of RegTest electrumsv server...") # reset is sometimes used with no args and so the --deterministic-seed extension # doesn't take effect if hasattr(self.cli_inputs, 'deterministic_seed'): if self.cli_inputs.cli_extension_args['deterministic_seed']: set_deterministic_electrumsv_seed( self.cli_inputs.selected_component, self.id) metadata = component_dict.get('metadata', {}) assert metadata is not None # typing bug self.datadir = Path(metadata["datadir"]) self.id = component_dict.get('id') self.tools.delete_wallet(datadir=self.datadir) self.tools.create_wallet(datadir=self.datadir, wallet_name='worker1.sqlite') self.plugin_tools.call_for_component_id_or_type( self.COMPONENT_NAME, callable=reset_electrumsv) self.logger.info( "Reset of RegTest electrumsv wallet completed successfully")
class Plugin(AbstractPlugin): DEFAULT_PORT = 49241 RESERVED_PORTS: Set[int] = {DEFAULT_PORT} COMPONENT_NAME = get_directory_name(__file__) DEFAULT_REMOTE_REPO = "https://github.com/electrumsv/simple-indexer" SIMPLE_INDEX_RESET = os.environ.get("SIMPLE_INDEX_RESET", '1') def __init__(self, cli_inputs: CLIInputs) -> None: self.cli_inputs = cli_inputs self.config = Config() self.plugin_tools = PluginTools(self, self.cli_inputs) self.tools = LocalTools(self) self.logger = logging.getLogger(self.COMPONENT_NAME) self.src = self.plugin_tools.get_source_dir(dirname="simple-indexer") self.datadir = None # dynamically allocated self.id = None # dynamically allocated self.port = None # dynamically allocated self.component_info: Optional[Component] = None def install(self) -> None: """required state: source_dir - which are derivable from 'repo' and 'branch' flags""" self.plugin_tools.modify_pythonpath_for_portability(self.src) repo = self.cli_inputs.repo if self.cli_inputs.repo == "": repo = self.DEFAULT_REMOTE_REPO if is_remote_repo(repo): self.tools.fetch_simple_indexer(repo, self.cli_inputs.branch) self.tools.packages_simple_indexer(repo, self.cli_inputs.branch) def start(self) -> None: """plugin datadir, id, port are allocated dynamically""" self.logger.debug(f"Starting RegTest simple_indexer daemon...") assert self.src is not None # typing bug if not self.src.exists(): self.logger.error(f"source code directory does not exist - try 'electrumsv-sdk install " f"{self.COMPONENT_NAME}' to install the plugin first") sys.exit(1) self.plugin_tools.modify_pythonpath_for_portability(self.src) self.datadir, self.id = self.plugin_tools.allocate_datadir_and_id() self.port = self.plugin_tools.allocate_port() command = f"{sys.executable} {self.src.joinpath('server.py')}" env_vars = { "PYTHONUNBUFFERED": "1", "SIMPLE_INDEX_RESET": self.SIMPLE_INDEX_RESET, } logfile = self.plugin_tools.get_logfile_path(self.id) self.plugin_tools.spawn_process(command, env_vars=env_vars, id=self.id, component_name=self.COMPONENT_NAME, src=self.src, logfile=logfile, status_endpoint=f"http://127.0.0.1:{self.port}", metadata={"datadir": str(self.datadir)}) def stop(self) -> None: """some components require graceful shutdown via a REST API or RPC API but most can use the generic 'app_state.kill_component()' function to track down the pid and kill the process.""" self.plugin_tools.call_for_component_id_or_type(self.COMPONENT_NAME, callable=kill_process) self.logger.info(f"stopped selected {self.COMPONENT_NAME} instance (if running)") def reset(self) -> None: self.logger.debug("Resetting state of RegTest simple_indexer server...") assert self.src is not None db_path = self.src.joinpath("simple_indexer.db") node_headers_path = self.src.joinpath("node_headers.mmap") local_headers_path = self.src.joinpath("local_headers.mmap") if db_path.exists(): os.remove(db_path) if node_headers_path.exists(): os.remove(node_headers_path) if local_headers_path.exists(): os.remove(local_headers_path) self.logger.info("Reset of RegTest simple_indexer completed successfully")
class Plugin(AbstractPlugin): BITCOIN_NETWORK = os.getenv("BITCOIN_NETWORK", "regtest") DEFAULT_PORT = 18332 DEFAULT_P2P_PORT = 18444 DEFAULT_ZMQ_PORT = 28332 # if ports == None -> set by deterministic port allocation NODE_PORT: int = int(os.environ.get("NODE_PORT") or DEFAULT_PORT) NODE_P2P_PORT: int = int( os.environ.get("NODE_P2P_PORT") or DEFAULT_P2P_PORT) NODE_ZMQ_PORT: int = int( os.environ.get("NODE_ZMQ_PORT") or DEFAULT_ZMQ_PORT) NODE_RPCALLOWIP = os.environ.get("NODE_RPCALLOWIP") # else 127.0.0.1 NODE_RPCBIND = os.environ.get("NODE_RPCBIND") RESERVED_PORTS: Set[int] = {DEFAULT_PORT, DEFAULT_P2P_PORT} COMPONENT_NAME = get_directory_name(__file__) def __init__(self, cli_inputs: CLIInputs): self.cli_inputs = cli_inputs self.config = Config() self.plugin_tools = PluginTools(self, self.cli_inputs) self.tools = LocalTools(self) self.logger = logging.getLogger(self.COMPONENT_NAME) self.src = Path(electrumsv_node.FILE_PATH).parent self.datadir: Optional[Path] = None # dynamically allocated self.id: Optional[str] = None # dynamically allocated self.port: Optional[int] = None # dynamically allocated self.p2p_port: Optional[int] = None # dynamically allocated self.zmq_port: Optional[int] = None # dynamically allocated self.component_info: Optional[Component] = None self.network = self.BITCOIN_NETWORK def install(self) -> None: """The node component has a pip installer at https://pypi.org/project/electrumsv-node/ and only official releases from pypi are supported""" self.plugin_tools.modify_pythonpath_for_portability(self.src) self.tools.fetch_node() self.logger.debug(f"Installed {self.COMPONENT_NAME}") def start(self) -> None: self.plugin_tools.modify_pythonpath_for_portability(self.src) # env vars take precedence for port and dbdir self.datadir, self.id = self.plugin_tools.allocate_datadir_and_id() self.tools.process_cli_args( ) # cli args may override network in env vars if self.NODE_PORT: self.port = self.NODE_PORT else: self.port = self.plugin_tools.allocate_port() if self.NODE_P2P_PORT: self.p2p_port = self.NODE_P2P_PORT else: self.p2p_port = self.plugin_tools.get_component_port( self.DEFAULT_P2P_PORT, self.COMPONENT_NAME, self.id) if self.NODE_ZMQ_PORT: self.zmq_port = self.NODE_ZMQ_PORT else: self.zmq_port = self.plugin_tools.get_component_port( self.DEFAULT_ZMQ_PORT, self.COMPONENT_NAME, self.id) extra_params = [] # TODO: NODE_RPCALLOWIP and NODE_RPCBIND can be removed after the next release of # electrumsv-node as they now default to 0.0.0.0/0 and 0.0.0.0 respectively if self.NODE_RPCALLOWIP: extra_params.append(f"-rpcallowip={self.NODE_RPCALLOWIP}") if self.NODE_RPCBIND: extra_params.append(f"-rpcbind={self.NODE_RPCBIND}") shell_command = electrumsv_node.shell_command( data_path=str(self.datadir), rpcport=self.port, p2p_port=self.p2p_port, zmq_port=self.zmq_port, network=self.network, print_to_console=True, extra_params=extra_params) command = " ".join(shell_command) logfile = self.plugin_tools.get_logfile_path(self.id) self.plugin_tools.spawn_process( command, env_vars=os.environ.copy(), id=self.id, component_name=self.COMPONENT_NAME, src=self.src, logfile=logfile, status_endpoint=f"http://*****:*****@127.0.0.1:{self.port}", metadata=ComponentMetadata(datadir=str(self.datadir), rpcport=self.port, p2p_port=self.p2p_port)) if electrumsv_node.is_node_running(): return else: self.logger.exception("node failed to start") def stop(self) -> None: """The bitcoin node requires graceful shutdown via the RPC API - a good example of why this entrypoint is provided for user customizations (rather than always killing the process).""" def stop_node(component_dict: ComponentTypedDict) -> None: metadata = component_dict.get("metadata", {}) assert metadata is not None # typing bug rpcport = metadata.get("rpcport") if not rpcport: raise Exception("rpcport data not found") electrumsv_node.stop(rpcport=rpcport) self.plugin_tools.call_for_component_id_or_type(self.COMPONENT_NAME, callable=stop_node) self.logger.info( f"stopped selected {self.COMPONENT_NAME} instance (if running)") def reset(self) -> None: def reset_node(component_dict: ComponentTypedDict) -> None: metadata = component_dict.get("metadata", {}) assert metadata is not None # typing bug rpcport = metadata.get('rpcport') datadir = metadata.get("datadir") if not rpcport: raise Exception("rpcport data not found") electrumsv_node.reset(data_path=datadir, rpcport=rpcport) self.plugin_tools.call_for_component_id_or_type(self.COMPONENT_NAME, callable=reset_node) self.logger.info( "Reset of RegTest bitcoin daemon completed successfully.")