diff --git a/deployment/constants.py b/deployment/constants.py index 92964597..cb8808cf 100644 --- a/deployment/constants.py +++ b/deployment/constants.py @@ -4,10 +4,13 @@ import deployment +# +# Filesystem +# + DEPLOYMENT_DIR = Path(deployment.__file__).parent CONSTRUCTOR_PARAMS_DIR = DEPLOYMENT_DIR / "constructor_params" ARTIFACTS_DIR = DEPLOYMENT_DIR / "artifacts" -OZ_DEPENDENCY = project.dependencies["openzeppelin"]["5.0.0"] # # Domains @@ -20,7 +23,7 @@ SUPPORTED_TACO_DOMAINS = [LYNX, TAPIR, MAINNET] # -# Nodes +# Testnet # LYNX_NODES = { @@ -39,18 +42,25 @@ "0xcbE2F626d84c556AbA674FABBbBDdbED6B39d87b": "0xb057B982fB575509047e90cf5087c9B863a2022d", } -# EIP1967 +# +# Contracts +# + +OZ_DEPENDENCY = project.dependencies["openzeppelin"]["5.0.0"] + +# EIP1967 Admin slot - https://eips.ethereum.org/EIPS/eip-1967#admin-address +EIP1967_ADMIN_SLOT = 0xB53127684A568B3173AE13B9F8A6016E243E63B6E8EE1178D6A717850B5D6103 -# Admin slot - https://eips.ethereum.org/EIPS/eip-1967#admin-address -EIP1967_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 +ACCESS_CONTROLLERS = ["GlobalAllowList", "OpenAccessAuthorizer", "ManagedAllowList"] +FEE_MODELS = ["FreeFeeModel", "BqETHSubscription"] # -# Contracts +# Sampling # -ACCESS_CONTROLLERS = [ - "GlobalAllowList", - "OpenAccessAuthorizer", - "ManagedAllowList" -] +PORTER_SAMPLING_ENDPOINTS = { + MAINNET: "https://porter.nucypher.io/bucket_sampling", + LYNX: "https://porter-lynx.nucypher.io/get_ursulas", + TAPIR: "https://porter-tapir.nucypher.io/get_ursulas", +} diff --git a/deployment/types.py b/deployment/types.py new file mode 100644 index 00000000..cbdc61a2 --- /dev/null +++ b/deployment/types.py @@ -0,0 +1,32 @@ +import click +from eth_utils import to_checksum_address + + +class MinInt(click.ParamType): + name = "minint" + + def __init__(self, min_value): + self.min_value = min_value + + def convert(self, value, param, ctx): + try: + ivalue = int(value) + except ValueError: + self.fail(f"{value} is not a valid integer", param, ctx) + if ivalue < self.min_value: + self.fail( + f"{value} is less than the minimum allowed value of {self.min_value}", param, ctx + ) + return ivalue + + +class ChecksumAddress(click.ParamType): + name = "checksum_address" + + def convert(self, value, param, ctx): + try: + value = to_checksum_address(value=value) + except ValueError: + self.fail("Invalid ethereum address") + else: + return value diff --git a/deployment/utils.py b/deployment/utils.py index 7c957a92..5575baa0 100644 --- a/deployment/utils.py +++ b/deployment/utils.py @@ -1,14 +1,15 @@ import json import os from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Optional +import requests import yaml from ape import networks, project from ape.contracts import ContractContainer, ContractInstance from ape_etherscan.utils import API_KEY_ENV_KEY_MAP -from deployment.constants import ARTIFACTS_DIR +from deployment.constants import ARTIFACTS_DIR, LYNX, MAINNET, PORTER_SAMPLING_ENDPOINTS, TAPIR from deployment.networks import is_local_network @@ -48,7 +49,7 @@ def validate_config(config: Dict) -> Path: config_chain_id = deployment.get("chain_id") if not config_chain_id: raise ValueError("chain_id is not set in params file.") - + contracts = config.get("contracts") if not contracts: raise ValueError("Constructor parameters file missing 'contracts' field.") @@ -156,3 +157,27 @@ def registry_filepath_from_domain(domain: str) -> Path: raise ValueError(f"No registry found for domain '{domain}'") return p + + +def sample_nodes( + domain: str, num_nodes: int, random_seed: Optional[int] = None, duration: Optional[int] = None +): + porter_endpoint = PORTER_SAMPLING_ENDPOINTS.get(domain) + if not porter_endpoint: + raise ValueError(f"Porter endpoint not found for domain '{domain}'") + + params = { + "quantity": num_nodes, + } + if duration: + params["duration"] = duration + if random_seed: + if domain != MAINNET: + raise ValueError("'random_seed' is only a valid parameter for mainnet") + params["random_seed"] = random_seed + + response = requests.get(porter_endpoint, params=params) + data = response.json() + result = sorted(data["result"]["ursulas"], key=lambda x: x.lower()) + + return result diff --git a/scripts/initiate_ritual.py b/scripts/initiate_ritual.py index 64d23338..9dd80e65 100644 --- a/scripts/initiate_ritual.py +++ b/scripts/initiate_ritual.py @@ -1,18 +1,18 @@ #!/usr/bin/python3 import click -from ape import project from ape.cli import ConnectedProviderCommand, account_option, network_option -from deployment.constants import LYNX, LYNX_NODES, SUPPORTED_TACO_DOMAINS, TAPIR, TAPIR_NODES +from deployment import registry +from deployment.constants import ACCESS_CONTROLLERS, FEE_MODELS, SUPPORTED_TACO_DOMAINS from deployment.params import Transactor -from deployment.registry import contracts_from_registry -from deployment.utils import check_plugins, registry_filepath_from_domain +from deployment.types import ChecksumAddress, MinInt +from deployment.utils import check_plugins, sample_nodes -@click.command(cls=ConnectedProviderCommand) -@network_option(required=True) +@click.command(cls=ConnectedProviderCommand, name="initiate-ritual") @account_option() +@network_option(required=True) @click.option( "--domain", "-d", @@ -23,48 +23,101 @@ @click.option( "--duration", "-t", - help="Duration of the ritual", - type=int, - default=86400, - show_default=True, + help="Duration of the ritual in seconds. Must be at least 24h.", + type=MinInt(86400), + required=True, ) @click.option( "--access-controller", + "-c", + help="The registry name of an access controller contract.", + type=click.Choice(ACCESS_CONTROLLERS), + required=True, +) +@click.option( + "--fee-model", + "-f", + help="The name of a fee model contract.", + type=click.Choice(FEE_MODELS), + required=True, +) +@click.option( + "--authority", "-a", - help="global allow list or open access authorizer.", - type=click.Choice(["GlobalAllowList", "OpenAccessAuthorizer", "ManagedAllowList"]), + help="The ethereum address of the ritual authority.", required=True, + type=ChecksumAddress(), ) -def cli(domain, duration, network, account, access_controller): +@click.option( + "--num-nodes", + "-n", + help="Number of nodes to use for the ritual.", + type=int, +) +@click.option( + "--random-seed", + "-r", + help="Random seed integer for bucket sampling on mainnet.", + type=int, +) +@click.option( + "--handpicked", + help="The filepath of a file containing newline separated staking provider addresses.", + type=click.File("r"), +) +def cli( + domain, + account, + network, + duration, + access_controller, + fee_model, + authority, + num_nodes, + random_seed, + handpicked, +): + """Initiate a ritual for a TACo domain.""" + + # Setup check_plugins() - print(f"Using network: {network}") - print(f"Using domain: {domain}") - print(f"Using account: {account}") - transactor = Transactor(account=account) + click.echo(f"Connected to {network.name} network.") + if not (bool(handpicked) ^ (num_nodes is not None)): + raise click.BadOptionUsage( + option_name="--num-nodes", + message=f"Specify either --num-nodes or --handpicked; got {num_nodes}, {handpicked}.", + ) + if handpicked and random_seed: + raise click.BadOptionUsage( + option_name="--random-seed", + message="Cannot specify --random-seed when using --handpicked.", + ) - if domain == LYNX: - providers = list(sorted(LYNX_NODES.keys())) - elif domain == TAPIR: - providers = list(sorted(TAPIR_NODES.keys())) + # Get the staking providers in the ritual cohort + if handpicked: + providers = sorted(line.lower() for line in handpicked) + if not providers: + raise ValueError(f"No staking providers found in the handpicked file {handpicked.name}") else: - # mainnet sampling not currently supported - raise ValueError(f"Sampling of providers not supported for domain '{domain}'") - - registry_filepath = registry_filepath_from_domain(domain=domain) - - chain_id = project.chain_manager.chain_id - deployments = contracts_from_registry(filepath=registry_filepath, chain_id=chain_id) - coordinator = deployments[project.Coordinator.contract_type.name] + providers = sample_nodes( + domain=domain, num_nodes=num_nodes, duration=duration, random_seed=random_seed + ) - access_controller = deployments[getattr(project, access_controller).contract_type.name] - authority = transactor.get_account().address + # Get the contracts from the registry + coordinator = registry.get_contract(domain=domain, contract_name="Coordinator") + access_controller = registry.get_contract(domain=domain, contract_name=access_controller) + fee_model = registry.get_contract(domain=domain, contract_name=fee_model) - while True: - transactor.transact( - coordinator.initiateRitual, providers, authority, duration, access_controller.address - ) - if not input("Another? [y/n] ").lower().startswith("y"): - break + # Initiate the ritual + transactor = Transactor(account=account) + transactor.transact( + coordinator.initiateRitual, + fee_model.address, + providers, + authority, + duration, + access_controller.address, + ) if __name__ == "__main__": diff --git a/scripts/manage_subscription.py b/scripts/manage_subscription.py index 53815a1a..48ada8de 100644 --- a/scripts/manage_subscription.py +++ b/scripts/manage_subscription.py @@ -1,6 +1,6 @@ import click from ape import Contract -from ape.cli import account_option, ConnectedProviderCommand +from ape.cli import account_option, ConnectedProviderCommand, network_option from deployment import registry from deployment.options import ( @@ -51,6 +51,7 @@ def cli(): @cli.command(cls=ConnectedProviderCommand) @account_option() +@network_option(required=True) @domain_option @subscription_contract_option @encryptor_slots_option @@ -59,9 +60,10 @@ def cli(): default=0, help="Subscription billing period number to pay for.", ) -def pay_subscription(account, domain, subscription_contract, encryptor_slots, period): +def pay_subscription(account, network, domain, subscription_contract, encryptor_slots, period): """Pay for a new subscription period and initial encryptor slots.""" check_plugins() + click.echo(f"Connected to {network.name} network.") transactor = Transactor(account=account) subscription_contract = registry.get_contract( contract_name=subscription_contract, @@ -92,12 +94,14 @@ def pay_subscription(account, domain, subscription_contract, encryptor_slots, pe @cli.command(cls=ConnectedProviderCommand) @account_option() +@network_option(required=True) @domain_option @subscription_contract_option @encryptor_slots_option -def pay_slots(account, domain, subscription_contract, encryptor_slots): +def pay_slots(account, network, domain, subscription_contract, encryptor_slots): """Pay for additional encryptor slots in the current billing period.""" check_plugins() + click.echo(f"Connected to {network.name} network.") transactor = Transactor(account=account) subscription_contract = registry.get_contract( contract_name=subscription_contract, @@ -123,12 +127,14 @@ def pay_slots(account, domain, subscription_contract, encryptor_slots): @cli.command(cls=ConnectedProviderCommand) @account_option() +@network_option(required=True) @domain_option @ritual_id_option @access_controller_option @encryptors_option -def add_encryptors(account, domain, ritual_id, access_controller, encryptors): +def add_encryptors(account, network, domain, ritual_id, access_controller, encryptors): """Authorize encryptors to the access control contract for a ritual.""" + click.echo(f"Connected to {network.name} network.") access_controller = registry.get_contract( contract_name=access_controller, domain=domain