def add(plugin, version, skip_confirmation): if plugin.startswith("ape"): raise Abort(f"Namespace 'ape' in '{plugin}' is not required") # NOTE: Add namespace prefix (prevents arbitrary installs) plugin = f"ape_{clean_plugin_name(plugin)}" if version: plugin = f"{plugin}=={version}" if plugin in FIRST_CLASS_PLUGINS: raise Abort(f"Cannot add 1st class plugin '{plugin}'") elif is_plugin_installed(plugin): raise Abort(f"Plugin '{plugin}' already installed") elif ( plugin in SECOND_CLASS_PLUGINS or skip_confirmation or click.confirm(f"Install unknown 3rd party plugin '{plugin}'?") ): notify("INFO", f"Installing {plugin}...") # NOTE: Be *extremely careful* with this command, as it modifies the user's # installed packages, to potentially catastrophic results # NOTE: This is not abstracted into another function *on purpose* subprocess.call([sys.executable, "-m", "pip", "install", "--quiet", plugin])
def compile(self, contract_filepaths: List[Path]) -> Dict[str, ContractType]: extensions = set(path.suffix for path in contract_filepaths) if extensions > set(self.registered_compilers): raise Exception("No compiler found for extension") contract_types = {} for extension in extensions: paths_to_compile = [ path for path in contract_filepaths if path.suffix == extension ] for path in paths_to_compile: try: notify( "INFO", f"Compiling '{path.relative_to(self.config.PROJECT_FOLDER)}'" ) except ValueError: notify("INFO", f"Compiling '{path}'") for contract_type in self.registered_compilers[extension].compile( paths_to_compile): if contract_type.contractName in contract_types: raise Exception( "ContractType collision across compiler plugins") contract_types[contract_type.contractName] = contract_type return contract_types
def _list(): if len(accounts) == 0: notify("WARNING", "No accounts found.") return elif len(accounts) > 1: click.echo(f"Found {len(accounts)} accounts:") else: click.echo("Found 1 account:") for account in accounts: alias_display = f" (alias: '{account.alias}')" if account.alias else "" click.echo(f" {account.address}{alias_display}")
def install(skip_confirmation): for plugin, version in config.get_config("plugins").items(): if not plugin.startswith("ape-"): raise Abort(f"Namespace 'ape' required in config item '{plugin}'") if not is_plugin_installed(plugin.replace("-", "_")) and ( plugin.replace("-", "_") in SECOND_CLASS_PLUGINS or skip_confirmation or click.confirm(f"Install unknown 3rd party plugin '{plugin}'?") ): notify("INFO", f"Installing {plugin}...") # NOTE: Be *extremely careful* with this command, as it modifies the user's # installed packages, to potentially catastrophic results # NOTE: This is not abstracted into another function *on purpose* subprocess.call( [sys.executable, "-m", "pip", "install", "--quiet", f"{plugin}=={version}"] )
def _Contract( address: Union[str, AddressAPI, AddressType], networks: "NetworkManager", converters: "ConversionManager", contract_type: Optional[ContractType] = None, ) -> AddressAPI: """ Function used to triage whether we have a contract type available for the given address/network combo, or explicitly provided. If none are found, returns a simple `Address` instance instead of throwing (provides a warning) """ # Check contract cache (e.g. previously deployed/downloaded contracts) # TODO: Add `contract_cache` dict-like object to `NetworkAPI` # network = provider.network # if not contract_type and address in network.contract_cache: # contract_type = network.contract_cache[address] # Check explorer API/cache (e.g. publicly published contracts) # TODO: Add `get_contract_type` to `ExplorerAPI` # TODO: Store in `NetworkAPI.contract_cache` to reduce API calls # explorer = provider.network.explorer # if not contract_type and explorer: # contract_type = explorer.get_contract_type(address) # We have a contract type either: # 1) explicity provided, # 2) from network cache, or # 3) from explorer if contract_type: return ContractInstance( # type: ignore _address=converters.convert(address, AddressType), _provider=networks.active_provider, _contract_type=contract_type, ) else: # We don't have a contract type from any source, provide raw address instead notify("WARNING", f"No contract type found for {address}") return Address( # type: ignore _address=converters.convert(address, AddressType), _provider=networks.active_provider, )
def generate(alias): if alias in accounts.aliases: notify("ERROR", f"Account with alias '{alias}' already exists") return path = container.data_folder.joinpath(f"{alias}.json") extra_entropy = click.prompt( "Add extra entropy for key generation...", hide_input=True, ) account = EthAccount.create(extra_entropy) passphrase = click.prompt( "Create Passphrase", hide_input=True, confirmation_prompt=True, ) path.write_text(json.dumps(EthAccount.encrypt(account.key, passphrase))) notify( "SUCCESS", f"A new account '{account.address}' has been added with the id '{alias}'" )
def _list(display_all): plugins = [] for name, plugin in plugin_manager.list_name_plugin(): version_str = "" version = get_package_version(name) if name in FIRST_CLASS_PLUGINS: if not display_all: continue # NOTE: Skip 1st class plugins unless specified version_str = " (core)" elif version: version_str = f" ({version})" plugins.append(f"{name}{version_str}") if plugins: click.echo("Installed plugins:") click.echo(" " + "\n ".join(plugins)) else: notify("INFO", "No plugins installed")
def _import(alias): if alias in accounts.aliases: notify("ERROR", f"Account with alias '{alias}' already exists") return path = container.data_folder.joinpath(f"{alias}.json") key = click.prompt("Enter Private Key", hide_input=True) try: account = EthAccount.from_key(to_bytes(hexstr=key)) except Exception as error: notify("ERROR", f"Key can't be imported {error}") return passphrase = click.prompt( "Create Passphrase", hide_input=True, confirmation_prompt=True, ) path.write_text(json.dumps(EthAccount.encrypt(account.key, passphrase))) notify( "SUCCESS", f"A new account '{account.address}' has been added with the id '{alias}'" )
def convert(self, value: str) -> AddressType: notify("WARNING", f"'{value}' is not in checksummed form") return to_checksum_address(value)
def __post_init__(self): notify("INFO", f"Submitted {self.txn_hash.hex()}")
def delete(alias): account = accounts.load(alias) account.delete() notify("SUCCESS", f"Account '{alias}' has been deleted")
def change_password(alias): account = accounts.load(alias) account.change_password() notify("SUCCESS", f"Password has been changed for account '{alias}'")
} # Plugins maintained OSS by ApeWorX (and trusted) SECOND_CLASS_PLUGINS: Set[str] = set() # TODO: Should the github client be in core? # TODO: Handle failures with connecting to github (potentially cached on disk?) if "GITHUB_ACCESS_TOKEN" in os.environ: author = Github(os.environ["GITHUB_ACCESS_TOKEN"]).get_organization("ApeWorX") SECOND_CLASS_PLUGINS = { repo.name.replace("-", "_") for repo in author.get_repos() if repo.name.startswith("ape-") } else: notify("WARNING", "$GITHUB_ACCESS_TOKEN not set, skipping 2nd class plugins") def is_plugin_installed(plugin: str) -> bool: try: __import__(plugin) return True except ImportError: return False @click.group(short_help="Manage ape plugins") def cli(): """ Command-line helper for managing installed plugins. """
def cli(filepaths, use_cache, display_size): """ Compiles the manifest for this project and saves the results back to the manifest. Note that ape automatically recompiles any changed contracts each time a project is loaded. You do not have to manually trigger a recompile. """ # NOTE: Lazy load so that testing works properly from ape import project # Expand source tree based on selection if not filepaths: if not (project.path / "contracts").exists(): notify("WARNING", "No `contracts/` directory detected") return # If no paths are specified, use all local project sources contract_filepaths = project.sources else: # Expand any folder paths expanded_filepaths = flatten( [d.rglob("*.*") if d.is_dir() else [d] for d in filepaths]) # Filter by what's in our project's source tree # NOTE: Make the paths absolute like `project.sources` contract_filepaths = [ c.resolve() for c in expanded_filepaths if c.resolve() in project.sources ] if not contract_filepaths: if filepaths: selected_paths = "', '".join(str(p.resolve()) for p in filepaths) else: selected_paths = str(project.path / "contracts") extensions = ", ".join(set(f.suffix for f in selected_paths)) notify( "WARNING", f"No compilers detected for the following extensions: {extensions}" ) return # TODO: only compile selected contracts contract_types = project.load_contracts(use_cache) # Display bytecode size for *all* contract types (not just ones we compiled) if display_size: codesize = [] for contract in contract_types.values(): if not contract.deploymentBytecode: continue # Skip if not bytecode to display bytecode = contract.deploymentBytecode.bytecode if bytecode: codesize.append((contract.contractName, len(bytecode) // 2)) if not codesize: notify("INFO", "No contracts with bytecode to display") return click.echo() click.echo("============ Deployment Bytecode Sizes ============") indent = max(len(i[0]) for i in codesize) for name, size in sorted(codesize, key=lambda k: k[1], reverse=True): pct = size / 24577 # pct_color = color(next((i[1] for i in CODESIZE_COLORS if pct >= i[0]), "")) # TODO Get colors fixed for bytecode size output # click.echo(f" {name:<{indent}} - {size:>6,}B ({pct_color}{pct:.2%}{color})") click.echo(f" {name:<{indent}} - {size:>6,}B ({pct:.2%})") click.echo()