diff --git a/.darglint b/.darglint index cdc16584..5b972259 100644 --- a/.darglint +++ b/.darglint @@ -1,4 +1,4 @@ [darglint] # Disable darglint checks for private functions. ignore_regex=^_(.*) -ignore=DAR402 +ignore=DAR402,DAR101 diff --git a/.flake8 b/.flake8 index 87bd1ef4..24e791bb 100644 --- a/.flake8 +++ b/.flake8 @@ -5,7 +5,8 @@ docstring-convention=google # D212 and D213 are mutually exclusive, so we're only allowing D213. # D205 and D415 don't like multi-line summary statements, which is annoying. # E203 and W503 don't interact well with black -ignore = D205,D212,D415,E203,W503 +# D417 asks for params in the docstring - it's not necessary because of Click +ignore = D205,D212,D415,E203,W503,D417 # This is the line length that black uses # https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length diff --git a/xbridge_cli/server/config/__init__.py b/xbridge_cli/server/config/__init__.py index acace85f..c328525b 100644 --- a/xbridge_cli/server/config/__init__.py +++ b/xbridge_cli/server/config/__init__.py @@ -7,6 +7,7 @@ generate_bootstrap, generate_witness_config, ) +from xbridge_cli.server.config.prod import create_prod_server_configs @click.group(name="create-config") @@ -18,5 +19,6 @@ def create_server_configs() -> None: create_server_configs.add_command(generate_all_configs, name="all") create_server_configs.add_command(generate_bootstrap, name="bootstrap") create_server_configs.add_command(generate_witness_config, name="witness") +create_server_configs.add_command(create_prod_server_configs, name="prod") __all__ = ["create_server_configs"] diff --git a/xbridge_cli/server/config/config.py b/xbridge_cli/server/config/config.py index 6d551729..c4193ccb 100644 --- a/xbridge_cli/server/config/config.py +++ b/xbridge_cli/server/config/config.py @@ -128,6 +128,13 @@ def _generate_rippled_configs(config_dir: str, docker: bool = False) -> Tuple[in is_flag=True, help="Whether the config files are for a docker setup.", ) +@click.option( + "--locking-ip", + "locking_chain_ip", + default="127.0.0.1", + type=str, + help="The IP address of the locking chain node.", +) @click.option( "--locking-port", "locking_chain_port", @@ -136,6 +143,13 @@ def _generate_rippled_configs(config_dir: str, docker: bool = False) -> Tuple[in type=int, help="The port used by the locking chain.", ) +@click.option( + "--issuing-ip", + "issuing_chain_ip", + default="127.0.0.1", + type=str, + help="The IP address of the issuing chain node.", +) @click.option( "--issuing-port", "issuing_chain_port", @@ -227,7 +241,9 @@ def _generate_rippled_configs(config_dir: str, docker: bool = False) -> Tuple[in def generate_witness_config( config_dir: str, name: str, + locking_chain_ip: str, locking_chain_port: int, + issuing_chain_ip: str, issuing_chain_port: int, witness_port: int, locking_reward_seed: str, @@ -249,6 +265,8 @@ def generate_witness_config( Args: config_dir: The folder in which to store config files. name: The name of the witness server. + locking_chain_ip: The IP address of the locking chain node. + issuing_chain_ip: The IP address of the issuing chain node. locking_chain_port: The port used by the locking chain. issuing_chain_port: The port used by the issuing chain. witness_port: The port that will be used by the witness server. @@ -293,7 +311,9 @@ def generate_witness_config( log_file = os.path.join(sub_dir, "witness.log") template_data = { + "locking_chain_ip": locking_chain_ip, "locking_chain_port": locking_chain_port, + "issuing_chain_ip": issuing_chain_ip, "issuing_chain_port": issuing_chain_port, "witness_port": witness_port, "db_dir": os.path.join(sub_dir, "db"), diff --git a/xbridge_cli/server/config/prod/__init__.py b/xbridge_cli/server/config/prod/__init__.py new file mode 100644 index 00000000..84d18973 --- /dev/null +++ b/xbridge_cli/server/config/prod/__init__.py @@ -0,0 +1,24 @@ +"""Subcommand for all commands dealing with production server config file generation.""" + +import click + +from xbridge_cli.server.config.prod.bootstrap import ( + combine_bootstrap_pieces, + get_bootstrap_piece_from_witness, +) +from xbridge_cli.server.config.prod.witness import generate_prod_witness_config + + +@click.group(name="prod") +def create_prod_server_configs() -> None: + """Subcommand for production server config file generation.""" + pass + + +create_prod_server_configs.add_command(generate_prod_witness_config, name="witness") +create_prod_server_configs.add_command( + get_bootstrap_piece_from_witness, name="bootstrap" +) +create_prod_server_configs.add_command(combine_bootstrap_pieces, name="combine") + +__all__ = ["create_prod_server_configs"] diff --git a/xbridge_cli/server/config/prod/bootstrap.py b/xbridge_cli/server/config/prod/bootstrap.py new file mode 100644 index 00000000..a142360a --- /dev/null +++ b/xbridge_cli/server/config/prod/bootstrap.py @@ -0,0 +1,157 @@ +"""Generate files related to a production bootstrap file.""" + +import json +import os +from sys import platform +from typing import List, Optional + +import click +from xrpl import CryptoAlgorithm +from xrpl.wallet import Wallet + +from xbridge_cli.server.config.config import _generate_template + + +@click.command(name="bootstrap") +@click.option( + "-w", + "--witness_file", + required=True, + prompt=True, + type=click.Path(exists=True), + help="The location of the witness config file.", +) +@click.option( + "-o", + "--output_file", + prompt=True, + default="./bootstrap-piece.json", + type=click.Path(), + help=( + "The location of the witness config file. The default is " + "'./bootstrap-piece.json'." + ), +) +def get_bootstrap_piece_from_witness( + witness_file: str, output_file: Optional[str] = "./bootstrap-piece.json" +) -> None: + """ + Extract the info needed for the bootstrap file from a witness config file, without + revealing any secret information. + """ + with open(witness_file) as f: + witness_config = json.load(f) + + locking_config = witness_config["LockingChain"] + issuing_config = witness_config["IssuingChain"] + + locking_reward_account = locking_config["RewardAccount"] + locking_submit_account = locking_config["TxnSubmit"]["SubmittingAccount"] + issuing_reward_account = issuing_config["RewardAccount"] + issuing_submit_account = issuing_config["TxnSubmit"]["SubmittingAccount"] + signing_key_seed = witness_config["SigningKeySeed"] + signing_key_algo = CryptoAlgorithm(witness_config["SigningKeyType"].upper()) + signing_key_account = Wallet.from_seed( + signing_key_seed, algorithm=signing_key_algo + ).classic_address + + template_data = { + "locking_reward_account": locking_reward_account, + "locking_submit_account": locking_submit_account, + "issuing_reward_account": issuing_reward_account, + "issuing_submit_account": issuing_submit_account, + "signing_key_type": signing_key_algo.value, + "signing_key_account": signing_key_account, + } + + # add the rippled.cfg file + _generate_template( + "bootstrap-witness.jinja", + template_data, + output_file or os.path.join(os.getcwd(), "bootstrap-witness.json"), + ) + + +@click.command(name="combine") +@click.option( + "-l", + "--locking_seed", + "locking_door_seed", + required=True, + prompt=True, + help="The seed of the locking chain's door account.", +) +@click.option( + "-i", + "--issuing_seed", + "issuing_door_seed", + required=True, + prompt=True, + help="The seed of the issuing chain's door account.", +) +@click.option( + "-b", + "--bootstrap_piece", + "bootstrap_pieces", + required=True, + multiple=True, + type=click.Path(exists=True), + help="One of the bootstrap pieces. Must include all of them here.", +) +def combine_bootstrap_pieces( + locking_door_seed: str, + issuing_door_seed: str, + bootstrap_pieces: List[str], +) -> None: + """Combine the bootstrap witness files into the bridge bootstrap file.""" + locking_reward_accounts = [] + locking_submit_accounts = [] + issuing_reward_accounts = [] + issuing_submit_accounts = [] + signing_accounts = [] + for bootstrap_piece in bootstrap_pieces: + with open(os.path.abspath(bootstrap_piece)) as f: + bootstrap_config = json.load(f) + locking_reward_accounts.append( + bootstrap_config["LockingChain"]["RewardAccount"] + ) + locking_submit_accounts.append( + bootstrap_config["LockingChain"]["SubmitAccount"] + ) + issuing_reward_accounts.append( + bootstrap_config["IssuingChain"]["RewardAccount"] + ) + issuing_submit_accounts.append( + bootstrap_config["IssuingChain"]["SubmitAccount"] + ) + signing_accounts.append(bootstrap_config["SigningKeyAccount"]) + + locking_door = Wallet.from_seed(locking_door_seed) + issuing_door = Wallet.from_seed(issuing_door_seed) + locking_issue = "XRP" + issuing_issue = "XRP" + + template_data = { + "is_linux": platform == "linux" or platform == "linux2", + "locking_node_port": 5005, + "locking_door_account": locking_door.classic_address, + "locking_door_seed": locking_door.seed, + "locking_issue": repr(locking_issue).replace("'", '"'), + "locking_reward_accounts": locking_reward_accounts, + "locking_submit_accounts": locking_submit_accounts, + "issuing_node_port": 5006, + "issuing_door_account": issuing_door.classic_address, + "issuing_door_seed": issuing_door.seed, + "issuing_issue": repr(issuing_issue).replace("'", '"'), + "issuing_reward_accounts": issuing_reward_accounts, + "issuing_submit_accounts": issuing_reward_accounts, + "signing_accounts": signing_accounts, + } + # if verbose: + # click.echo(pformat(template_data)) + + _generate_template( + "bootstrap.jinja", + template_data, + os.path.join(os.getcwd(), "bridge_bootstrap.json"), + ) diff --git a/xbridge_cli/server/config/prod/witness.py b/xbridge_cli/server/config/prod/witness.py new file mode 100644 index 00000000..26e1021c --- /dev/null +++ b/xbridge_cli/server/config/prod/witness.py @@ -0,0 +1,167 @@ +"""Prod config generation.""" +import os +from sys import platform + +import click +from xrpl import CryptoAlgorithm +from xrpl.wallet import Wallet + +from xbridge_cli.server.config.config import _generate_template, _get_currency + + +@click.command(name="witness") +@click.option( + "-c", + "--config_path", + default="./witness.json", + prompt=True, + type=click.Path(), + help="The location in which to store this config file.", +) +@click.option( + "--locking", + "locking_chain", + required=True, + prompt="Locking Chain (IP:WS Port)", + type=str, + help=( + "The address of the locking chain node's Websocket port, of the form " + "`IP:Port`." + ), +) +@click.option( + "--issuing", + "issuing_chain", + required=True, + prompt="Issuing Chain (IP:WS Port)", + type=str, + help=( + "The address of the issuing chain node's Websocket port, of the form " + "`IP:Port`." + ), +) +@click.option( + "--rpc_port", + default="6006", + prompt=True, + type=int, + help="The port that will be used by the witness server for RPC commands.", +) +@click.option( + "--locking_door", + required=True, + prompt=True, + help="The door account on the locking chain.", +) +@click.option( + "--locking_currency", + default="XRP", + prompt=True, + help=( + "The currency on the locking chain. Defaults to XRP. An issued currency is of " + "the form `{{currency}}.{{issue}}`" + ), +) +@click.option( + "--issuing_door", + default="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + prompt=True, + help="The door account on the issuing chain. Defaults to the genesis account.", +) +@click.option( + "--signing_seed", + "signing_seed_param", + prompt="Signing Seed (leave blank to auto-generate)", + default="", + help="The seed to use for signing attestations.", +) +@click.option( + "--locking_reward_seed", + required=True, + prompt=True, + help="The seed for the reward account for the witness on the locking chain.", +) +@click.option( + "--locking_reward_account", + required=True, + prompt=True, + help="The reward account for the witness on the locking chain.", +) +@click.option( + "--issuing_reward_seed", + required=True, + prompt=True, + help="The seed for the reward account for the witness on the issuing chain.", +) +@click.option( + "--issuing_reward_account", + required=True, + prompt=True, + help="The reward account for the witness on the issuing chain.", +) +@click.option( + "-v", + "--verbose", + is_flag=True, + help="Whether or not to print more verbose information.", +) +def generate_prod_witness_config( + config_path: str, + locking_chain: str, + issuing_chain: str, + rpc_port: int, + locking_reward_seed: str, + locking_reward_account: str, + issuing_reward_seed: str, + issuing_reward_account: str, + locking_door: str, + signing_seed_param: str = "", + locking_currency: str = "XRP", + issuing_door: str = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + verbose: bool = False, +) -> None: + """Generate the config file for a production witness server.""" + abs_config_path = os.path.abspath(config_path) + sub_dir = "/opt/witness" + + locking_chain_ip, locking_chain_port = locking_chain.split(":") + issuing_chain_ip, issuing_chain_port = issuing_chain.split(":") + + if signing_seed_param == "": + signing_seed = Wallet.create(algorithm=CryptoAlgorithm.SECP256K1).seed + else: + signing_seed = signing_seed_param + + locking_issue = _get_currency(locking_currency) + issuing_issue = locking_issue.copy() + if issuing_issue["currency"] != "XRP": + issuing_issue["issuer"] = issuing_door + + log_file = os.path.join(os.path.dirname(abs_config_path), "witness.log") + + template_data = { + "locking_chain_ip": locking_chain_ip, + "locking_chain_port": locking_chain_port, + "issuing_chain_ip": issuing_chain_ip, + "issuing_chain_port": issuing_chain_port, + "witness_port": rpc_port, + "db_dir": os.path.join(sub_dir, "db"), + "seed": signing_seed, + "locking_reward_seed": locking_reward_seed, + "locking_reward_account": locking_reward_account, + "issuing_reward_seed": issuing_reward_seed, + "issuing_reward_account": issuing_reward_account, + "src_door": locking_door, + "src_issue": repr(locking_issue).replace("'", '"'), + "dst_door": issuing_door, + "dst_issue": repr(issuing_issue).replace("'", '"'), + "is_linux": platform == "linux" or platform == "linux2", + "is_docker": False, + "log_file": log_file, + } + + if verbose: + click.echo(template_data) + + # add the witness.json file + _generate_template("witness.jinja", template_data, abs_config_path) diff --git a/xbridge_cli/server/config/templates/bootstrap-witness.jinja b/xbridge_cli/server/config/templates/bootstrap-witness.jinja new file mode 100644 index 00000000..82420d27 --- /dev/null +++ b/xbridge_cli/server/config/templates/bootstrap-witness.jinja @@ -0,0 +1,11 @@ +{ + "LockingChain": { + "RewardAccount": "{{ locking_reward_account }}", + "SubmitAccount": "{{ locking_submit_account }}" + }, + "IssuingChain": { + "RewardAccount": "{{ issuing_reward_account }}", + "SubmitAccount": "{{ issuing_submit_account }}" + }, + "SigningKeyAccount": "{{ signing_key_account }}" +} diff --git a/xbridge_cli/server/config/templates/witness.jinja b/xbridge_cli/server/config/templates/witness.jinja index 8884a235..f6f12b14 100644 --- a/xbridge_cli/server/config/templates/witness.jinja +++ b/xbridge_cli/server/config/templates/witness.jinja @@ -3,6 +3,8 @@ "Endpoint": { {% if is_docker %} "Host": "192.168.176.2", + {% elif locking_chain_ip %} + "Host": "{{ locking_chain_ip }}", {% else %} "Host": "127.0.0.1", {% endif %} @@ -20,6 +22,8 @@ "Endpoint": { {% if is_docker %} "Host": "192.168.176.3", + {% elif issuing_chain_ip %} + "Host": "{{ issuing_chain_ip }}", {% elif is_linux %} "Host": "127.0.0.2", {% else %}