diff --git a/m1/Cargo.toml b/m1/Cargo.toml index 90f3efed..cb0d810f 100644 --- a/m1/Cargo.toml +++ b/m1/Cargo.toml @@ -2,7 +2,6 @@ resolver = "2" members = [ "subnet", - "m1-cli", "movement-benchmark", "tests/e2e" ] diff --git a/m1/m1-cli/CHANGELOG.md b/m1/m1-cli/CHANGELOG.md deleted file mode 100644 index f1bd4b6a..00000000 --- a/m1/m1-cli/CHANGELOG.md +++ /dev/null @@ -1,51 +0,0 @@ -# Aptos CLI Changelog - -All notable changes to the Aptos CLI will be captured in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and the format set out by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - -## [1.0.13] - 2023/04/27 -### Fixed -* Previously `--skip-fetch-latest-git-deps` would not actually do anything when used with `aptos move test`. This has been fixed. -* Fixed the issue of the hello_blockchain example where feature enable was missing - -## [1.0.12] - 2023/04/25 -### Added -* Support for creating and interacting with multisig accounts v2. More details can be found at [AIP 12](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-12.md). -* Added `disassemble` option to the CLI - This can be invoked using `aptos move disassemble` to disassemble the bytecode and save it to a file -* Fixed handling of `vector` as an entry function argument in `aptos move run` - -## [1.0.11] - 2023/04/14 -### Fixed -* Fixed creating a new test account with `aptos init` would fail if the account didn't already exist - -## [1.0.10] - 2023/04/13 -### Fixed -* If `aptos init` is run with a faucet URL specified (which happens by default when using the local, devnet, or testnet network options) and funding the account fails, the account creation is considered a failure and nothing is persisted. Previously it would report success despite the account not being created on chain. -* When specifying a profile where the `AuthenticationKey` has been rotated, now the `AccountAddress` is properly used from the config file -* Update `aptos init` to fix an incorrect account address issue, when trying to init with a rotated private key. Right now it does an actual account lookup instead of deriving from public key - -### Added -* Updates to prover and framework specs - -## [1.0.9] - 2023/03/29 -### Added -* `aptos move show abi` allows for viewing the ABI of a compiled move package -* Experimental gas profiler with the `--profile-gas` flag on any transaction submitting CLI command -* Updates to the prover and framework specs - -## [1.0.8] - 2023/03/16 -### Added -* Added an `aptos account derive-resource-account-address` command to add the ability to derive an address easily -* Added the ability for different input resource account seeds, to allow matching directly with onchain code -* Added beta support for coverage via `aptos move coverage` and `aptos move test --coverage` -* Added beta support for compiling with bytecode dependencies rather than source dependencies - -### Fixed -* All resource account commands can now use `string_seed` which will match the onchain representation of `b"string"` rather than always derive a different address -* Tests that go over the bytecode size limit can now compile -* `vector` inputs to now work for both `aptos move view` and `aptos move run` -* Governance proposal listing will now not crash on the latest on-chain format -* Move compiler will no longer use an environment variable to communicate between compiler and CLI for the bytecode version - -## [1.0.7] -* For logs earlier than 1.0.7, please check out the [releases on GitHub](https://github.com/aptos-labs/aptos-core/releases?q="Aptos+CLI+Release") - diff --git a/m1/m1-cli/Cargo.toml b/m1/m1-cli/Cargo.toml deleted file mode 100644 index ca6b89c9..00000000 --- a/m1/m1-cli/Cargo.toml +++ /dev/null @@ -1,99 +0,0 @@ -[package] -name = "m1-cli" -description = "Movement tool for management of nodes and interacting with the blockchain. Based on the Movement CLI." -version = "1.0.13" - -# Workspace inherited keys -authors = { workspace = true } -edition = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -publish = { workspace = true } -repository = { workspace = true } -rust-version = { workspace = true } - -[dependencies] -anyhow = { workspace = true } -aptos-backup-cli = { workspace = true } -aptos-bitvec = { workspace = true } -aptos-build-info = { workspace = true } -aptos-cached-packages = { workspace = true } -aptos-config = { workspace = true } -aptos-crypto = { workspace = true } -aptos-db-tool = { workspace = true } -aptos-debugger = { workspace = true } -aptos-faucet-core = { workspace = true } -aptos-framework = { workspace = true } -aptos-gas = { workspace = true } -aptos-gas-profiling = { workspace = true } -aptos-genesis = { workspace = true } -aptos-github-client = { workspace = true } -aptos-global-constants = { workspace = true } -aptos-keygen = { workspace = true } -aptos-logger = { workspace = true } -aptos-network-checker = { workspace = true } -aptos-node = { workspace = true } -aptos-rest-client = { workspace = true } -aptos-sdk = { workspace = true } -aptos-storage-interface = { workspace = true } -aptos-telemetry = { workspace = true } -aptos-temppath = { workspace = true } -aptos-transactional-test-harness = { workspace = true } -aptos-types = { workspace = true } -aptos-vm = { workspace = true, features = ["testing"] } -aptos-vm-genesis = { workspace = true } -async-trait = { workspace = true } -base64 = { workspace = true } -bcs = { workspace = true } -chrono = { workspace = true } -clap = { workspace = true } -clap_complete = { workspace = true } -codespan-reporting = { workspace = true } -dirs = { workspace = true } -futures = { workspace = true } -hex = { workspace = true } -itertools = { workspace = true } -move-binary-format = { workspace = true } -move-bytecode-source-map = { workspace = true } -move-cli = { workspace = true } -move-command-line-common = { workspace = true } -move-compiler = { workspace = true } -move-core-types = { workspace = true } -move-coverage = { workspace = true } -move-disassembler = { workspace = true } -move-ir-compiler = { workspace = true } -move-ir-types = { workspace = true } -move-package = { workspace = true } -move-prover = { workspace = true } -move-prover-boogie-backend = { workspace = true } -move-symbol-pool = { workspace = true } -move-unit-test = { workspace = true, features = [ "debugging" ] } -move-vm-runtime = { workspace = true, features = [ "testing" ] } -rand = { workspace = true } -regex = { workspace = true } -reqwest = { workspace = true } -self_update = { version = "0.34.0", features = ["archive-zip", "compression-zip-deflate"] } -serde = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -shadow-rs = { workspace = true } -tempfile = { workspace = true } -termcolor = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } -tokio-util = { workspace = true } -toml = { workspace = true } -walkdir = { workspace = true } - -[target.'cfg(unix)'.dependencies] -jemallocator = { workspace = true } - -[features] -default = [] -fuzzing = [] -no-upload-proposal = [] -indexer = ["aptos-node/indexer"] -cli-framework-test-move = [] - -[build-dependencies] -shadow-rs = { workspace = true } diff --git a/m1/m1-cli/README.md b/m1/m1-cli/README.md deleted file mode 100644 index 2c3e7981..00000000 --- a/m1/m1-cli/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Aptos Command Line Interface (CLI) Tool - -The `movement` tool is a command line interface (CLI) for debugging, development, and node operation. -See [Movement CLI Documentation](https://aptos.dev/cli-tools/aptos-cli-tool/install-aptos-cli) for how to install the `movment` CLI tool and how to use it. - \ No newline at end of file diff --git a/m1/m1-cli/build.rs b/m1/m1-cli/build.rs deleted file mode 100644 index 6cc52885..00000000 --- a/m1/m1-cli/build.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -fn main() -> shadow_rs::SdResult<()> { - shadow_rs::new() -} diff --git a/m1/m1-cli/debug-move-example/Move.toml b/m1/m1-cli/debug-move-example/Move.toml deleted file mode 100644 index 80571522..00000000 --- a/m1/m1-cli/debug-move-example/Move.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "DebugDemo" -version = "0.0.0" - -[addresses] -DebugDemo = "0x1" - -[dependencies] -AptosFramework = { local = "../../../aptos-move/framework/aptos-framework" } diff --git a/m1/m1-cli/debug-move-example/sources/DebugDemo.move b/m1/m1-cli/debug-move-example/sources/DebugDemo.move deleted file mode 100644 index 03a42540..00000000 --- a/m1/m1-cli/debug-move-example/sources/DebugDemo.move +++ /dev/null @@ -1,32 +0,0 @@ -module DebugDemo::Message { - use std::string; - use std::signer; - use aptos_std::debug; - - struct MessageHolder has key { - message: string::String, - } - - - public entry fun set_message(account: signer, message_bytes: vector) - acquires MessageHolder { - debug::print_stack_trace(); - let message = string::utf8(message_bytes); - let account_addr = signer::address_of(&account); - if (!exists(account_addr)) { - move_to(&account, MessageHolder { - message, - }) - } else { - let old_message_holder = borrow_global_mut(account_addr); - old_message_holder.message = message; - } - } - - #[test(account = @0x1)] - public entry fun sender_can_set_message(account: signer) acquires MessageHolder { - let addr = signer::address_of(&account); - debug::print
(&addr); - set_message(account, b"Hello, Blockchain"); - } -} diff --git a/m1/m1-cli/e2e/README.md b/m1/m1-cli/e2e/README.md deleted file mode 100644 index 4d9c1234..00000000 --- a/m1/m1-cli/e2e/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# CLI test suite -This directory contains Python code to help with running the CLI test suite. - -## Requirements -We use [Poetry](https://python-poetry.org/docs/#installation) for packaging and dependency management: - -``` -curl -sSL https://install.python-poetry.org | python3 - -``` - -Once you have Poetry, you can install the dependencies for the testing framework like this: -``` -poetry install -``` - -To learn how to use the CLI testing framework, run this: -``` -poetry run python main.py -h -``` - -For example: -``` -poetry run python main.py --base-network mainnet --test-cli-tag mainnet -``` - -## Debugging - -If you are get an error message similar to this: -``` -docker: no matching manifest for linux/arm64/v8 in the manifest list entries. -``` - -Try running the poetry command with this env var: -``` -DOCKER_DEFAULT_PLATFORM=linux/amd64 poetry run python main.py --base-network testnet --test-cli-path ~/aptos-core/target/debug/aptos -``` -This makes the docker commands use the x86_64 images since we don't publish images for ARM. - -When running the e2e test using poetry locally, make sure you set your aptos config type to `workspace`, otherwise it won't be able to find the test account after `aptos init`. You can change it back to `global` afterward: -``` -aptos config set-global-config --config-type workspace -``` - -## Writing new test cases -To write a new test case, follow these steps: -1. (Optional) Make a new file in [cases/](cases/) if none of the existing files seem appropriate. -1. Write a new function following these guidelines: - 1. Follow the naming scheme `test_*`. - 1. Decorate the function with the `test_case` decorator. - 1. If you want to assert something, do so by raising an exception (TestError has been provided for this purpose, but any old exception does the trick). - 1. Use the `RunHelper` to invoke CLI commands. Follow the example of other test cases. -1. Register the test in the `run_tests` function in [main.py](main.py). Note that the order matters here, later tests are allowed (and encouraged) to depend on the results of earlier tests. This way we can test truly end-to-end, beyond the span of a single invocation. - -## Formatting: -``` -poetry run isort . -poetry run black . -``` diff --git a/m1/m1-cli/e2e/cases/__init__.py b/m1/m1-cli/e2e/cases/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/m1/m1-cli/e2e/cases/account.py b/m1/m1-cli/e2e/cases/account.py deleted file mode 100644 index 6bdc0f8e..00000000 --- a/m1/m1-cli/e2e/cases/account.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright © Aptos Foundation -# SPDX-License-Identifier: Apache-2.0 - - -from common import OTHER_ACCOUNT_ONE, TestError -from test_helpers import RunHelper -from test_results import test_case - - -@test_case -def test_account_fund_with_faucet(run_helper: RunHelper, test_name=None): - amount_in_octa = 100000000000 - - # Fund the account. - run_helper.run_command( - test_name, - [ - "movement", - "account", - "fund-with-faucet", - "--account", - run_helper.get_account_info().account_address, - "--amount", - str(amount_in_octa), - ], - ) - - # Assert it has the requested balance. - balance = int( - run_helper.api_client.account_balance( - run_helper.get_account_info().account_address - ) - ) - if balance == amount_in_octa: - raise TestError( - f"Account {run_helper.get_account_info().account_address} has balance {balance}, expected {amount_in_octa}" - ) - - -@test_case -def test_account_create(run_helper: RunHelper, test_name=None): - # Create the new account. - run_helper.run_command( - test_name, - [ - "movement", - "account", - "create", - "--account", - OTHER_ACCOUNT_ONE.account_address, - "--assume-yes", - ], - ) - - # Assert it exists and has zero balance. - balance = int( - run_helper.api_client.account_balance(OTHER_ACCOUNT_ONE.account_address) - ) - if balance != 0: - raise TestError( - f"Account {OTHER_ACCOUNT_ONE.account_address} has balance {balance}, expected 0" - ) diff --git a/m1/m1-cli/e2e/cases/init.py b/m1/m1-cli/e2e/cases/init.py deleted file mode 100644 index 0ab32c8b..00000000 --- a/m1/m1-cli/e2e/cases/init.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright © Aptos Foundation -# SPDX-License-Identifier: Apache-2.0 - -import os - -from common import TestError -from test_helpers import RunHelper -from test_results import test_case - - -@test_case -def test_init(run_helper: RunHelper, test_name=None): - # Inititalize a profile for the CLI to use. Note that we do not set the - # --skip-faucet flag. This means that in addition to creating a profile locally, - # it will use the faucet to create the account on chain. This will fund the - # account with the default amount of 100000000 OCTA. - run_helper.run_command( - test_name, - ["movement", "init", "--assume-yes", "--network", "local"], - input="\n", - ) - - # Assert that the CLI config is there. - config_path = os.path.join( - run_helper.host_working_directory, ".movement", "config.yaml" - ) - if not os.path.exists(config_path): - raise TestError( - f"{config_path} not found (in host working dir) after running aptos init" - ) - - # Assert that it contains info for the account that was created. - account_info = run_helper.get_account_info() - if not account_info: - raise TestError("Failed to read account info from newly created config file") - - # Confirm with the local testnet that it was created. - try: - run_helper.api_client.account(account_info.account_address) - except Exception as e: - raise TestError( - f"Failed to query local testnet for account {account_info.account_address}" - ) from e diff --git a/m1/m1-cli/e2e/common.py b/m1/m1-cli/e2e/common.py deleted file mode 100644 index 0240163c..00000000 --- a/m1/m1-cli/e2e/common.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright © Aptos Foundation -# SPDX-License-Identifier: Apache-2.0 - -import os -from dataclasses import dataclass -from enum import Enum - -NODE_PORT = 8080 -FAUCET_PORT = 8081 - - -class Network(Enum): - DEVNET = "devnet" - TESTNET = "testnet" - MAINNET = "mainnet" - - def __str__(self): - return self.value - - -# Information for some accounts we use for testing. -@dataclass -class AccountInfo: - private_key: str - public_key: str - account_address: str - - -# This is an account that use for testing, for example to create it with the init -# account, send funds to it, etc. This is not the account created by the `aptos init` -# test. To get details about that account use get_account_info on the RunHelper. -OTHER_ACCOUNT_ONE = AccountInfo( - private_key="0x37368b46ce665362562c6d1d4ec01a08c8644c488690df5a17e13ba163e20221", - public_key="0x25caf00522e4d4664ec0a27166a69e8a32b5078959d0fc398da70d40d2893e8f", - account_address="0x585fc9f0f0c54183b039ffc770ca282ebd87307916c215a3e692f2f8e4305e82", -) - - -def build_image_name(image_repo_with_project: str, tag: str): - return f"{image_repo_with_project}/tools:{tag}" - - -# Exception to use when a test fails, for the CLI did something unexpected, an -# expected output was missing, etc. This is just a convenience, the framework -# will still work if a different error is raised. -# -# For errors within the framework itself, use RuntimeError. -class TestError(Exception): - pass diff --git a/m1/m1-cli/e2e/local_testnet.py b/m1/m1-cli/e2e/local_testnet.py deleted file mode 100644 index 49de4188..00000000 --- a/m1/m1-cli/e2e/local_testnet.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright © Aptos Foundation -# SPDX-License-Identifier: Apache-2.0 - -# This file contains functions for running the local testnet. - -import logging -import subprocess -import time - -import requests -from common import FAUCET_PORT, NODE_PORT, Network, build_image_name - -LOG = logging.getLogger(__name__) - -# Run a local testnet in a docker container. We choose to detach here and we'll -# stop running it later using the container name. -def run_node(network: Network, image_repo_with_project: str): - image_name = build_image_name(image_repo_with_project, network) - container_name = f"aptos-tools-{network}" - LOG.info(f"Trying to run movement CLI local testnet from image: {image_name}") - - # Confirm that the Docker daemon is running. - try: - subprocess.run( - ["docker", "container", "ls"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=True, - ) - except: - LOG.error("Failed to connect to Docker. Is it installed and running?") - raise - - # First delete the existing container if there is one with the same name. - subprocess.run( - ["docker", "rm", "-f", container_name], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - # Run the container. - subprocess.check_output( - [ - "docker", - "run", - "--pull", - "always", - "--detach", - "--name", - container_name, - "-p", - f"{NODE_PORT}:{NODE_PORT}", - "-p", - f"{FAUCET_PORT}:{FAUCET_PORT}", - image_name, - "movement", - "node", - "run-local-testnet", - "--with-faucet", - ], - ) - LOG.info(f"Running movement CLI local testnet from image: {image_name}") - return container_name - - -# Stop running the detached node. -def stop_node(container_name: str): - LOG.info(f"Stopping container: {container_name}") - subprocess.check_output(["docker", "stop", container_name]) - LOG.info(f"Stopped container: {container_name}") - - -# Query the node and faucet APIs until they start up or we timeout. -def wait_for_startup(container_name: str, timeout: int): - LOG.info(f"Waiting for node and faucet APIs for {container_name} to come up") - count = 0 - api_response = None - faucet_response = None - while True: - try: - api_response = requests.get(f"http://127.0.0.1:{NODE_PORT}/v1") - # Try to query the legacy faucet health endpoint first. TODO: Remove this - # once all local testnet images we use have the new faucet in them. - faucet_response = requests.get(f"http://127.0.0.1:{FAUCET_PORT}/health") - if faucet_response.status_code == 404: - # If that fails, try the new faucet health endpoint. - faucet_response = requests.get(f"http://127.0.0.1:{FAUCET_PORT}/") - if api_response.status_code != 200 or faucet_response.status_code != 200: - raise RuntimeError( - f"API or faucet not ready. API response: {api_response}. " - f"Faucet response: {faucet_response}" - ) - break - except Exception: - if count >= timeout: - LOG.error(f"Timeout while waiting for node / faucet to come up") - raise - count += 1 - time.sleep(1) - LOG.info(f"Node and faucet APIs for {container_name} came up") diff --git a/m1/m1-cli/e2e/main.py b/m1/m1-cli/e2e/main.py deleted file mode 100644 index 2d8f97d6..00000000 --- a/m1/m1-cli/e2e/main.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright © Aptos Foundation -# SPDX-License-Identifier: Apache-2.0 - -""" -This script is how we orchestrate running a local testnet and then running CLI tests against it. There are two different CLIs used for this: - -1. Base: For running the local testnet. This is what the --base-network flag and all other flags starting with --base are for. -2. Test: The CLI that we're testing. This is what the --test-cli-tag / --test-cli-path and all other flags starting with --test are for. - -Example (testing CLI in image): - python3 main.py --base-network testnet --test-cli-tag mainnet_0431e2251d0b42920d89a52c63439f7b9eda6ac3 - -Example (testing locally built CLI binary): - python3 main.py --base-network devnet --test-cli-path ~/aptos-core/target/release/aptos - -This means, run the CLI test suite using a CLI built from mainnet_0431e2251d0b42920d89a52c63439f7b9eda6ac3 against a local testnet built from the testnet branch of aptos-core. - -Example (using a different image repo): - See ~/.github/workflows/cli-e2e-tests.yaml - -When the test suite is complete, it will tell you which tests passed and which failed. To further debug a failed test, you can check the output in --working-directory, there will be files for each test containing the command run, stdout, stderr, and any exception. -""" - -import argparse -import logging -import pathlib -import shutil -import sys - -from cases.account import test_account_create, test_account_fund_with_faucet -from cases.init import test_init -from common import Network -from local_testnet import run_node, stop_node, wait_for_startup -from test_helpers import RunHelper -from test_results import test_results - -logging.basicConfig( - stream=sys.stderr, - format="%(asctime)s - %(levelname)s - %(message)s", - level=logging.INFO, -) - -LOG = logging.getLogger(__name__) - - -def parse_args(): - # You'll notice there are two argument "prefixes", base and test. These refer to - # cases 1 and 2 in the top-level comment. - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description=__doc__, - ) - parser.add_argument("-d", "--debug", action="store_true") - parser.add_argument( - "--image-repo-with-project", - default="aptoslabs", - help=( - "What docker image repo (+ project) to use for the local testnet. " - "By default we use Docker Hub: %(default)s (so, just aptoslabs for the " - "project since Docker Hub is the implied default repo). If you want to " - "specify a different repo, it might look like this: " - "docker.pkg.github.com/aptoslabs/aptos-core" - ), - ) - parser.add_argument( - "--base-network", - required=True, - type=Network, - choices=list(Network), - help="What branch the Movement CLI used for the local testnet should be built from", - ) - parser.add_argument( - "--base-startup-timeout", - type=int, - default=30, - help="Timeout in seconds for waiting for node and faucet to start up", - ) - test_cli_args = parser.add_mutually_exclusive_group(required=True) - test_cli_args.add_argument( - "--test-cli-tag", - help="The image tag for the CLI we want to test, e.g. mainnet_0431e2251d0b42920d89a52c63439f7b9eda6ac3", - ) - test_cli_args.add_argument( - "--test-cli-path", - help="Path to CLI binary we want to test, e.g. /home/dport/aptos-core/target/release/aptos", - ) - parser.add_argument( - "--working-directory", - default="/tmp/aptos-cli-tests", - help="Where we'll run CLI commands from (in the host system). Default: %(default)s", - ) - args = parser.parse_args() - return args - - -def run_tests(run_helper): - # Run init tests. We run these first to set up the CLI. - test_init(run_helper) - - # Run account tests. - test_account_fund_with_faucet(run_helper) - test_account_create(run_helper) - - -def main(): - args = parse_args() - - if args.debug: - logging.getLogger().setLevel(logging.DEBUG) - LOG.debug("Debug logging enabled") - else: - logging.getLogger().setLevel(logging.INFO) - - # Run a node + faucet and wait for them to start up. - container_name = run_node(args.base_network, args.image_repo_with_project) - wait_for_startup(container_name, args.base_startup_timeout) - - # Create the dir the test CLI will run from. - shutil.rmtree(args.working_directory, ignore_errors=True) - pathlib.Path(args.working_directory).mkdir(parents=True, exist_ok=True) - - # Build the RunHelper object. - run_helper = RunHelper( - host_working_directory=args.working_directory, - image_repo_with_project=args.image_repo_with_project, - image_tag=args.test_cli_tag, - cli_path=args.test_cli_path, - ) - - # Prepare the run helper. This ensures in advance that everything needed is there. - run_helper.prepare() - - # Run tests. - run_tests(run_helper) - - # Stop the node + faucet. - stop_node(container_name) - - # Print out the results. - if test_results.passed: - LOG.info("These tests passed:") - for test_name in test_results.passed: - LOG.info(test_name) - - if test_results.failed: - LOG.error("These tests failed:") - for test_name, exception in test_results.failed: - LOG.error(f"{test_name}: {exception}") - return False - - LOG.info("All tests passed!") - return True - - -if __name__ == "__main__": - if main(): - sys.exit(0) - else: - sys.exit(1) diff --git a/m1/m1-cli/e2e/poetry.lock b/m1/m1-cli/e2e/poetry.lock deleted file mode 100644 index 2708f4a9..00000000 --- a/m1/m1-cli/e2e/poetry.lock +++ /dev/null @@ -1,665 +0,0 @@ -[[package]] -name = "anyio" -version = "3.6.2" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] - -[[package]] -name = "aptos-sdk" -version = "0.5.1" -description = "" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -httpx = ">=0.23.0,<0.24.0" -mypy = ">=0.982,<0.983" -PyNaCl = ">=1.5.0,<2.0.0" - -[[package]] -name = "black" -version = "22.12.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2022.12.7" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "cffi" -version = "1.15.1" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.1.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.7.0" - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "httpcore" -version = "0.16.3" -description = "A minimal low-level HTTP client." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -anyio = ">=3.0,<5.0" -certifi = "*" -h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" - -[package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "httpx" -version = "0.23.3" -description = "The next generation HTTP client." -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "importlib-metadata" -version = "6.0.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "isort" -version = "5.11.5" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.7.0" - -[package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - -[[package]] -name = "mypy" -version = "0.982" -description = "Optional static typing for Python" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "pathspec" -version = "0.11.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "platformdirs" -version = "3.1.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pynacl" -version = "1.5.0" -description = "Python binding to the Networking and Cryptography (NaCl) library" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.4.1" - -[package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] - -[[package]] -name = "requests" -version = "2.28.2" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "urllib3" -version = "1.26.15" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-o", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "1.1" -python-versions = ">=3.7 <4" -content-hash = "e9e3c9c792c90300ff2f22bcfadc9ad737060eb3142c17a21e687073fa54e877" - -[metadata.files] -anyio = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, -] -aptos-sdk = [ - {file = "aptos_sdk-0.5.1.tar.gz", hash = "sha256:3711ad2bf1120fff463cd5f494162c4658f03dd6bfbf1f523ee9aea01a4cb0f0"}, -] -black = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] -certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -charset-normalizer = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -h11 = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] -httpcore = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, -] -httpx = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -importlib-metadata = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, -] -isort = [ - {file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"}, - {file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"}, -] -mypy = [ - {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, - {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, - {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"}, - {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"}, - {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"}, - {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"}, - {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"}, - {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"}, - {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"}, - {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"}, - {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"}, - {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"}, - {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"}, - {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"}, - {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"}, - {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"}, - {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"}, - {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"}, - {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"}, - {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"}, - {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"}, - {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"}, - {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"}, - {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"}, -] -mypy-extensions = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] -pathspec = [ - {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, - {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, -] -platformdirs = [ - {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"}, - {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pynacl = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] -requests = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, -] -rfc3986 = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] -sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typing-extensions = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, -] -urllib3 = [ - {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, - {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, -] -zipp = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, -] diff --git a/m1/m1-cli/e2e/pyproject.toml b/m1/m1-cli/e2e/pyproject.toml deleted file mode 100644 index b706f7df..00000000 --- a/m1/m1-cli/e2e/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[tool.poetry] -name = "movement-cli-e2e-tests" -version = "0.1.0" -description = "Movement CLI E2E tests" -authors = ["Movment Labs "] -license = "Apache-2.0" - -[tool.poetry.dependencies] -python = ">=3.7 <4" -aptos-sdk = "^0.5.1" -requests = "^2.28.2" - -[tool.poetry.dev-dependencies] -black = "^22.6.0" -isort = "^5.10.1" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" diff --git a/m1/m1-cli/e2e/test_helpers.py b/m1/m1-cli/e2e/test_helpers.py deleted file mode 100644 index e3af62d4..00000000 --- a/m1/m1-cli/e2e/test_helpers.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright © Aptos Foundation -# SPDX-License-Identifier: Apache-2.0 - -import logging -import os -import pathlib -import subprocess -import traceback -from dataclasses import dataclass - -from aptos_sdk.client import RestClient -from common import AccountInfo, build_image_name - -LOG = logging.getLogger(__name__) - -WORKING_DIR_IN_CONTAINER = "/tmp" - -# We pass this class into all test functions to help with calling the CLI, -# collecting output, and accessing common info. -@dataclass -class RunHelper: - host_working_directory: str - image_repo_with_project: str - image_tag: str - cli_path: str - test_count: int - - # This can be used by the tests to query the local testnet. - api_client: RestClient - - def __init__( - self, host_working_directory, image_repo_with_project, image_tag, cli_path - ): - if image_tag and cli_path: - raise RuntimeError("Cannot specify both image_tag and cli_path") - if not (image_tag or cli_path): - raise RuntimeError("Must specify one of image_tag and cli_path") - self.host_working_directory = host_working_directory - self.image_repo_with_project = image_repo_with_project - self.image_tag = image_tag - self.cli_path = os.path.abspath(cli_path) if cli_path else cli_path - self.test_count = 0 - self.api_client = RestClient(f"http://127.0.0.1:8080/v1") - - def build_image_name(self): - return build_image_name(self.image_repo_with_project, self.image_tag) - - # This function lets you pass call the CLI like you would normally, but really it is - # calling the CLI in a docker container and mounting the host working directory such - # that the container will write it results out to that directory. That way the CLI - # state / configuration is preserved between test cases. - def run_command(self, test_name, command, *args, **kwargs): - file_name = f"{self.test_count:03}_{test_name}" - self.test_count += 1 - - # Build command. - if self.image_tag: - full_command = [ - "docker", - "run", - # For why we have to set --user, see here: - # https://github.com/community/community/discussions/44243 - "--user", - f"{os.getuid()}:{os.getgid()}", - "--rm", - "--network", - "host", - "-i", - "-v", - f"{self.host_working_directory}:{WORKING_DIR_IN_CONTAINER}", - "--workdir", - WORKING_DIR_IN_CONTAINER, - self.build_image_name(), - ] + command - else: - full_command = [self.cli_path] + command[1:] - LOG.debug(f"Running command: {full_command}") - - # Create the output directory if necessary. - out_path = os.path.join(self.host_working_directory, "out") - pathlib.Path(out_path).mkdir(exist_ok=True) - - # Write the command we're going to run to file. - with open(os.path.join(out_path, f"{file_name}.command"), "w") as f: - f.write(" ".join(command)) - - # Run command. - try: - # If we're using a local CLI, set the working directory for subprocess.run. - if self.cli_path: - kwargs["cwd"] = self.host_working_directory - result = subprocess.run( - full_command, - *args, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - **kwargs, - ) - LOG.debug(f"Subcommand succeeded: {test_name}") - - write_subprocess_out(out_path, file_name, result) - - return result - except subprocess.CalledProcessError as e: - LOG.warn(f"Subcommand failed: {test_name}") - - # Write the exception to file. - with open(os.path.join(out_path, f"{file_name}.exception"), "w") as f: - f.write( - "".join( - traceback.format_exception( - etype=type(e), value=e, tb=e.__traceback__ - ) - ) - ) - - # Fortunately the result and exception of subprocess.run both have the - # stdout and stderr attributes on them. - write_subprocess_out(out_path, file_name, e) - - raise - - # If image_Tag is set, pull the test CLI image. We don't technically have to do - # this separately but it makes the steps clearer. Otherwise, cli_path must be - # set, in which case we ensure the file is there. - def prepare(self): - if self.image_tag: - image_name = self.build_image_name() - LOG.info(f"Pre-pulling image for CLI we're testing: {image_name}") - command = ["docker", "pull", image_name] - LOG.debug(f"Running command: {command}") - output = subprocess.check_output(command) - LOG.debug(f"Output: {output}") - else: - if not os.path.isfile(self.cli_path): - raise RuntimeError(f"CLI not found at path: {self.cli_path}") - - # Get the account info of the account created by test_init. - def get_account_info(self): - path = os.path.join(self.host_working_directory, ".aptos", "config.yaml") - with open(path) as f: - content = f.read().splitlines() - # To avoid using external deps we parse the file manually. - private_key = None - public_key = None - account_address = None - for line in content: - if "private_key: " in line: - private_key = line.split("private_key: ")[1].replace('"', "") - if "public_key: " in line: - public_key = line.split("public_key: ")[1].replace('"', "") - if "account: " in line: - account_address = line.split("account: ")[1].replace('"', "") - if not private_key or not public_key or not account_address: - raise RuntimeError(f"Failed to parse {path} to get account info") - return AccountInfo( - private_key=private_key, - public_key=public_key, - account_address=account_address, - ) - - -# This function helps with writing the stdout / stderr of a subprocess to files. -def write_subprocess_out(out_path, file_name, command_output): - LOG.debug(f"Stdout: {command_output.stdout}") - LOG.debug(f"Stderr: {command_output.stderr}") - - # Write stdout and stderr to file. - with open(os.path.join(out_path, f"{file_name}.stdout"), "w") as f: - f.write(command_output.stdout) - with open(os.path.join(out_path, f"{file_name}.stderr"), "w") as f: - f.write(command_output.stderr) diff --git a/m1/m1-cli/e2e/test_results.py b/m1/m1-cli/e2e/test_results.py deleted file mode 100644 index 661fe43b..00000000 --- a/m1/m1-cli/e2e/test_results.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright © Aptos Foundation -# SPDX-License-Identifier: Apache-2.0 - -import logging -import typing -from dataclasses import dataclass, field -from functools import wraps - -LOG = logging.getLogger(__name__) - - -# This class holds info about passed / failed tests. -@dataclass(init=True) -class TestResults: - passed: typing.List[str] = field(default_factory=list) - failed: typing.List[typing.Tuple[str, Exception]] = field(default_factory=list) - - -# This is a decorator that you put above every test case. It handles capturing test -# success / failure so it can be reported at the end of the test suite. -def build_test_case_decorator(test_results: TestResults): - def test_case_inner(f): - @wraps(f) - def wrapper(*args, **kwds): - LOG.info(f"Running test: {f.__name__}") - try: - result = f(*args, test_name=f.__name__, **kwds) - test_results.passed.append(f.__name__) - return result - except Exception as e: - test_results.failed.append((f.__name__, e)) - return None - - return wrapper - - return test_case_inner - - -# We now define one TestResults that we'll use for every test case. This is a bit of a -# hack but it is the only way to then be able to provide a decorator that works out of -# the box. The alternative was to use a context manager and wrap every function call in -# it, but not only is that more verbose, but you'd have to provide the name of each test -# case manually to the context manager, whereas with this approach the name can be -# inferred from the function being decorated directly. -test_results = TestResults() - -# Then we define an instance of the decorator that uses that TestResults instance. -test_case = build_test_case_decorator(test_results) diff --git a/m1/m1-cli/homebrew/README.md b/m1/m1-cli/homebrew/README.md deleted file mode 100644 index e25d87d3..00000000 --- a/m1/m1-cli/homebrew/README.md +++ /dev/null @@ -1,210 +0,0 @@ -# Homebrew Aptos - -Homebrew is a package manager that works for MacOS Silicon and Intel chips as well as Linux distributions like Debian and Ubuntu. - -The [Aptos command line interface (CLI)](https://aptos.dev/cli-tools/aptos-cli-tool/install-aptos-cli) may be installed via [Homebrew](https://brew.sh/) for simplicity. This is an in-depth overview of Homebrew and the Aptos formula. In this guide, we go over each section of the Homebrew formula and steps to implement changes in the future. - -## Quick guide - -- [Formula in Homebrew GitHub](https://github.com/Homebrew/homebrew-core/blob/master/Formula/aptos.rb) -- [Aptos 1.0.3 New Formula PR for GitHub](https://github.com/Homebrew/homebrew-core/pull/119832) -- [Aptos Formula Fix PR to use build_cli_release.sh](https://github.com/Homebrew/homebrew-core/pull/120051) - -## Getting started - -To begin, first ensure that homebrew is correctly installed on your computer. Visit [brew.sh](https://brew.sh/) to learn how you can set it up! - -To test that it works correctly, try - -```bash -brew help -``` - -Once homebrew is installed, run - -```bash -brew install aptos -``` - -to test that it installed correctly, try - -```bash -movement --help - -# This should return something like - -# movement 1.0.5 -# Movement Labs -# Command Line Interface (CLI) for developing and interacting with the Aptos blockchain -# ... -``` - -## Change guide - -Note: This guide is for developers who are trying to update the Aptos homebrew formula. - -Copy the `aptos.rb` file to your `homebrew` `formula` directory. For example, on macOS with an M1, this will likely be: - -```bash -/opt/homebrew/Library/Taps/homebrew/homebrew-core/Formula -``` - - -### Development - -After you've copied `aptos.rb` to your local `homebrew` `formula` directory, you can modify it and use the commands below for testing. - -```bash -# On Mac M1, homebrew formulas are located locally at -/opt/homebrew/Library/Taps/homebrew/homebrew-core/Formula - -# Before submitting changes run -brew audit --new-formula movement # For new formula -brew audit movement --strict --online -brew install movement -brew test movement - -# For debugging issues during the installation process you can do -brew install movement --interactive # Interactive, gives you access to the shell -brew install movement -d # Debug mode - -# Livecheck -brew livecheck --debug movement -``` - -### Committing changes - -Once you have audited and tested your brew formula using the commands above, make sure you: - -1. Commit your changes to `aptos-core` in `crates/aptos/homebrew`. -2. Fork the Homebrew Core repository per [How to Open a Homebrew Pull Request](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request#formulae-related-pull-request). -3. Create a PR on the [Homebrew Core](https://github.com/Homebrew/homebrew-core/pulls) repo with your changes. - -## Aptos.rb structure overview - -### Header - -```ruby -class Aptos < Formula - desc "Layer 1 blockchain built to support fair access to decentralized assets for all" - homepage "https://aptoslabs.com/" - url "https://github.com/aptos-labs/aptos-core/archive/refs/tags/aptos-cli-v1.0.3.tar.gz" - sha256 "670bb6cb841cb8a65294878af9a4f03d4cba2a598ab4550061fed3a4b1fe4e98" - license "Apache-2.0" - ... -``` - -### Bottles - -[Bottles](https://docs.brew.sh/Bottles#pour-bottle-pour_bottle) are precompiled binaries. This way people don't need to compile from source every time. - -> Bottles for homebrew/core formulae are created by [Brew Test Bot](https://docs.brew.sh/Brew-Test-Bot) when a pull request is submitted. If the formula builds successfully on each supported platform and a maintainer approves the change, [Brew Test Bot](https://docs.brew.sh/Brew-Test-Bot) updates its bottle do block and uploads each bottle to GitHub Packages. - -```ruby - ... - # IMPORTANT: These are automatically generated, you DO NOT need to add these manually, I'm adding them here as an example - bottle do - sha256 cellar: :any_skip_relocation, arm64_ventura: "40434b61e99cf9114a3715851d01c09edaa94b814f89864d57a18d00a8e0c4e9" - sha256 cellar: :any_skip_relocation, arm64_monterey: "edd6dcf9d627746a910d324422085eb4b06cdab654789a03b37133cd4868633c" - sha256 cellar: :any_skip_relocation, arm64_big_sur: "d9568107514168afc41e73bd3fd0fc45a6a9891a289857831f8ee027fb339676" - sha256 cellar: :any_skip_relocation, ventura: "d7289b5efca029aaa95328319ccf1d8a4813c7828f366314e569993eeeaf0003" - sha256 cellar: :any_skip_relocation, monterey: "ba58e1eb3398c725207ce9d6251d29b549cde32644c3d622cd286b86c7896576" - sha256 cellar: :any_skip_relocation, big_sur: "3e2431a6316b8f0ffa4db75758fcdd9dea162fdfb3dbff56f5e405bcbea4fedc" - sha256 cellar: :any_skip_relocation, x86_64_linux: "925113b4967ed9d3da78cd12745b1282198694a7f8c11d75b8c41451f8eff4b5" - end - ... -``` - -### Livecheck - -[Brew livecheck](https://docs.brew.sh/Brew-Livecheck) uses strategies to find the newest version of a formula or cask’s software by checking upstream. The strategy used below checks for all `aptos-cli-v` tags for `aptos-core`. The regex ensures that releases for other, non-CLI builds are not factored into livecheck. - -Livecheck is run on a schedule with BrewTestBot and will update the bottles automatically on a schedule to ensure they're up to date. For more info on how BrewTestBot and brew livecheck works, please see the [How does BrewTestBot work and when does it update formulae?](https://github.com/Homebrew/discussions/discussions/3083) discussion. - -```ruby -... - # This livecheck scans the releases folder and looks for all releases - # with matching regex of href="/tag/aptos-cli-v". This - # is done to automatically check for new release versions of the CLI. - livecheck do - url :stable - regex(/^aptos-cli[._-]v?(\d+(?:\.\d+)+)$/i) - end -... -``` - -To run livecheck for testing, we recommend including the `--debug` argument: - -```bash -brew livecheck --debug aptos -``` - -### Depends on and installation - -- `depends_on` is for specifying other [homebrew formulas as dependencies](https://docs.brew.sh/Formula-Cookbook#specifying-other-formulae-as-dependencies). -- Currently, we use v1.64 of Rust, as specified in the `Cargo.toml` file of the project. If we were to use the latest stable build of Rust -going forward, we would modify the formula slightly. See the comments below for more details. - - -```ruby - # Installs listed homebrew dependencies before Aptos installation - # Dependencies needed: https://aptos.dev/cli-tools/build-aptos-cli - # See scripts/dev_setup.sh in aptos-core for more info - depends_on "cmake" => :build - depends_on "rustup-init" => :build - uses_from_macos "llvm" => :build - - on_linux do - depends_on "pkg-config" => :build - depends_on "zip" => :build - depends_on "openssl@3" - depends_on "systemd" - end - - # Currently must compile with the same rustc version specified in the - # root Cargo.toml file of aptos-core (currently it is pegged to Rust - # v1.64). In the future if it becomes compatible with the latest Rust - # toolchain, we can remove the use of rustup-init, replacing it with a - # depends_on "rust" => :build - # above and build the binary without rustup as a dependency - # - # Uses build_cli_release.sh for creating the compiled binaries. - # This drastically reduces their size (ie. 2.2 GB on Linux for release - # build becomes 40 MB when run with opt-level = "z", strip, lto, etc). - # See cargo.toml [profile.cli] section for more details - def install - system "#{Formula["rustup-init"].bin}/rustup-init", - "-qy", "--no-modify-path", "--default-toolchain", "1.64" - ENV.prepend_path "PATH", HOMEBREW_CACHE/"cargo_cache/bin" - system "./scripts/cli/build_cli_release.sh", "homebrew" - bin.install "target/cli/aptos" - end -``` - -### Tests - -To conduct tests, run: - -```bash -brew test aptos -``` - -The current test generates a new key via the Movement CLI and ensures the shell output matches the filename(s) for that key. - -```ruby - ... - test do - assert_match(/output.pub/i, shell_output("#{bin}/aptos key generate --output-file output")) - end - ... -``` - -## Supporting resources - -- To view other Homebrew-related FAQs or ask questions yourself, visit the [discussions board](https://github.com/orgs/Homebrew/discussions). -- For similar Rust-related build examples, we recommend: - - [`rustfmt.rb`](https://github.com/Homebrew/homebrew-core/blob/master/Formula/rustfmt.rb) - - [`solana.rb`](https://github.com/Homebrew/homebrew-core/blob/master/Formula/solana.rb) -- Finally, note these key Homebew guides: - - [Homebrew Formula Cookbook](https://docs.brew.sh/Formula-Cookbook) - - [Creating and Running Your Own Homebrew Tap - Rust Runbook](https://publishing-project.rivendellweb.net/creating-and-running-your-own-homebrew-tap/) diff --git a/m1/m1-cli/homebrew/aptos.rb b/m1/m1-cli/homebrew/aptos.rb deleted file mode 100644 index 8d99b784..00000000 --- a/m1/m1-cli/homebrew/aptos.rb +++ /dev/null @@ -1,35 +0,0 @@ -class Aptos < Formula - desc "Layer 1 blockchain built to support fair access to decentralized assets for all" - homepage "https://aptoslabs.com/" - url "https://github.com/aptos-labs/aptos-core/archive/refs/tags/aptos-cli-v1.0.3.tar.gz" - sha256 "670bb6cb841cb8a65294878af9a4f03d4cba2a598ab4550061fed3a4b1fe4e98" - license "Apache-2.0" - - livecheck do - url :stable - regex(/^aptos-cli[._-]v?(\d+(?:\.\d+)+)$/i) - end - - depends_on "cmake" => :build - depends_on "rustup-init" => :build - uses_from_macos "llvm" => :build - - on_linux do - depends_on "pkg-config" => :build - depends_on "zip" => :build - depends_on "openssl@3" - depends_on "systemd" - end - - def install - system "#{Formula["rustup-init"].bin}/rustup-init", - "-qy", "--no-modify-path", "--default-toolchain", "1.64" - ENV.prepend_path "PATH", HOMEBREW_CACHE/"cargo_cache/bin" - system "./scripts/cli/build_cli_release.sh", "homebrew" - bin.install "target/cli/aptos" - end - - test do - assert_match(/output.pub/i, shell_output("#{bin}/aptos key generate --output-file output")) - end -end \ No newline at end of file diff --git a/m1/m1-cli/src/account/create.rs b/m1/m1-cli/src/account/create.rs deleted file mode 100644 index 32070a40..00000000 --- a/m1/m1-cli/src/account/create.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::{CliCommand, CliTypedResult, TransactionOptions, TransactionSummary}; -use aptos_cached_packages::aptos_stdlib; -use aptos_types::account_address::AccountAddress; -use async_trait::async_trait; -use clap::Parser; - -// 1 APT -pub const DEFAULT_FUNDED_COINS: u64 = 100_000_000; - -/// Create a new account on-chain -/// -/// An account can be created by transferring coins, or by making an explicit -/// call to create an account. This will create an account with no coins, and -/// any coins will have to transferred afterwards. -#[derive(Debug, Parser)] -pub struct CreateAccount { - /// Address of the new account - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub(crate) account: AccountAddress, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for CreateAccount { - fn command_name(&self) -> &'static str { - "CreateAccount" - } - - async fn execute(self) -> CliTypedResult { - let address = self.account; - self.txn_options - .submit_transaction(aptos_stdlib::aptos_account_create_account(address)) - .await - .map(TransactionSummary::from) - } -} diff --git a/m1/m1-cli/src/account/create_resource_account.rs b/m1/m1-cli/src/account/create_resource_account.rs deleted file mode 100644 index 6e79071a..00000000 --- a/m1/m1-cli/src/account/create_resource_account.rs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - account::derive_resource_account::ResourceAccountSeed, - common::types::{CliCommand, CliTypedResult, TransactionOptions, TransactionSummary}, -}; -use aptos_cached_packages::aptos_stdlib::resource_account_create_resource_account; -use aptos_rest_client::{ - aptos_api_types::{WriteResource, WriteSetChange}, - Transaction, -}; -use aptos_types::{account_address::AccountAddress, transaction::authenticator::AuthenticationKey}; -use async_trait::async_trait; -use clap::Parser; -use serde::Serialize; -use std::str::FromStr; - -/// Create a resource account on-chain -/// -/// This will create a resource account which can be used as an autonomous account -/// not controlled directly by one account. -#[derive(Debug, Parser)] -pub struct CreateResourceAccount { - /// Optional Resource Account authentication key. - #[clap(long, parse(try_from_str = AuthenticationKey::from_str))] - pub(crate) authentication_key: Option, - - #[clap(flatten)] - pub(crate) seed_args: ResourceAccountSeed, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -/// A shortened create resource account output -#[derive(Clone, Debug, Serialize)] -pub struct CreateResourceAccountSummary { - pub resource_account: Option, - #[serde(flatten)] - pub transaction_summary: TransactionSummary, -} - -impl From for CreateResourceAccountSummary { - fn from(transaction: Transaction) -> Self { - let transaction_summary = TransactionSummary::from(&transaction); - - let mut summary = CreateResourceAccountSummary { - transaction_summary, - resource_account: None, - }; - - if let Transaction::UserTransaction(txn) = transaction { - summary.resource_account = txn.info.changes.iter().find_map(|change| match change { - WriteSetChange::WriteResource(WriteResource { address, data, .. }) => { - if data.typ.name.as_str() == "Account" - && *address.inner().to_hex() != *txn.request.sender.inner().to_hex() - { - Some(*address.inner()) - } else { - None - } - }, - _ => None, - }); - } - - summary - } -} - -#[async_trait] -impl CliCommand for CreateResourceAccount { - fn command_name(&self) -> &'static str { - "CreateResourceAccount" - } - - async fn execute(self) -> CliTypedResult { - let authentication_key: Vec = if let Some(key) = self.authentication_key { - bcs::to_bytes(&key)? - } else { - vec![] - }; - self.txn_options - .submit_transaction(resource_account_create_resource_account( - self.seed_args.seed()?, - authentication_key, - )) - .await - .map(CreateResourceAccountSummary::from) - } -} diff --git a/m1/m1-cli/src/account/derive_resource_account.rs b/m1/m1-cli/src/account/derive_resource_account.rs deleted file mode 100644 index 2b14449d..00000000 --- a/m1/m1-cli/src/account/derive_resource_account.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::{CliCommand, CliError, CliTypedResult}; -use aptos_sdk::rest_client::aptos_api_types::HexEncodedBytes; -use aptos_types::account_address::{create_resource_address, AccountAddress}; -use async_trait::async_trait; -use clap::Parser; -use std::{fmt::Formatter, str::FromStr}; - -/// Encoding for the Resource account seed -#[derive(Debug, Clone, Copy)] -pub enum SeedEncoding { - Bcs, - Hex, - Utf8, -} - -const BCS: &str = "bcs"; -const UTF_8: &str = "utf8"; -const HEX: &str = "hex"; - -impl Default for SeedEncoding { - fn default() -> Self { - SeedEncoding::Bcs - } -} - -impl std::fmt::Display for SeedEncoding { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - SeedEncoding::Bcs => BCS, - SeedEncoding::Hex => HEX, - SeedEncoding::Utf8 => UTF_8, - }) - } -} - -impl FromStr for SeedEncoding { - type Err = CliError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - BCS => Ok(Self::Bcs), - HEX => Ok(Self::Hex), - UTF_8 | "utf-8" | "utf_8" => Ok(Self::Utf8), - _ => Err(CliError::UnableToParse( - "seed-encoding", - "For --seed-encoding please provide one of ['bcs','hex', 'utf8']".to_string(), - )), - } - } -} - -/// A generic interface for allowing for different types of seed phrase inputs -/// -/// The easiest to use is `string_seed` as it will match directly with the b"string" notation in Move. -#[derive(Debug, Parser)] -pub struct ResourceAccountSeed { - /// Resource account seed - /// - /// Seed used in generation of the AccountId of the resource account - /// The seed will be converted to bytes using the encoding from `--seed-encoding`, defaults to `BCS` - #[clap(long)] - pub(crate) seed: String, - - /// Resource account seed encoding - /// - /// The encoding can be one of `Bcs`, `Utf8`, and `Hex`. - /// - /// - Bcs is the legacy functionality of the CLI, it will BCS encode the string, but can be confusing for users e.g. `"ab" -> vector[0x2, 0x61, 0x62]` - /// - Utf8 will encode the string as raw UTF-8 bytes, similar to in Move `b"string"` e.g. `"ab" -> vector[0x61, 0x62]` - /// - Hex will encode the string as raw hex encoded bytes e.g. `"0x6162" -> vector[0x61, 0x62]` - #[clap(long, default_value_t = SeedEncoding::Bcs)] - pub(crate) seed_encoding: SeedEncoding, -} - -impl ResourceAccountSeed { - pub fn seed(self) -> CliTypedResult> { - match self.seed_encoding { - SeedEncoding::Bcs => Ok(bcs::to_bytes(self.seed.as_str())?), - SeedEncoding::Utf8 => Ok(self.seed.as_bytes().to_vec()), - SeedEncoding::Hex => HexEncodedBytes::from_str(self.seed.as_str()) - .map(|inner| inner.0) - .map_err(|err| CliError::UnableToParse("seed", err.to_string())), - } - } -} - -/// Derive the address for a resource account -/// -/// This will not create a resource account, but instead give the deterministic address given -/// a source address and seed. -#[derive(Debug, Parser)] -pub struct DeriveResourceAccount { - /// Address of the creator's account - #[clap(long, alias = "account", parse(try_from_str=crate::common::types::load_account_arg))] - pub(crate) address: AccountAddress, - - #[clap(flatten)] - pub(crate) seed_args: ResourceAccountSeed, -} - -#[async_trait] -impl CliCommand for DeriveResourceAccount { - fn command_name(&self) -> &'static str { - "DeriveResourceAccountAddress" - } - - async fn execute(self) -> CliTypedResult { - let seed = self.seed_args.seed()?; - Ok(create_resource_address(self.address, &seed)) - } -} diff --git a/m1/m1-cli/src/account/fund.rs b/m1/m1-cli/src/account/fund.rs deleted file mode 100644 index c3846aad..00000000 --- a/m1/m1-cli/src/account/fund.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - account::create::DEFAULT_FUNDED_COINS, - common::{ - types::{CliCommand, CliTypedResult, FaucetOptions, ProfileOptions, RestOptions}, - utils::{fund_account, wait_for_transactions, fund_pub_key}, - }, -}; -use aptos_types::account_address::AccountAddress; -use async_trait::async_trait; -use clap::Parser; - -/// Fund an account with tokens from a faucet -/// -/// This will create an account if it doesn't exist with the faucet. This is mostly useful -/// for local development and devnet. -#[derive(Debug, Parser)] -pub struct FundWithFaucet { - /// Address to fund - /// - /// If the account wasn't previously created, it will be created when being funded - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub(crate) account: AccountAddress, - - /// Number of Octas to fund the account from the faucet - /// - /// The amount added to the account may be limited by the faucet, and may be less - /// than the amount requested. - #[clap(long, default_value_t = DEFAULT_FUNDED_COINS)] - pub(crate) amount: u64, - - #[clap(flatten)] - pub(crate) faucet_options: FaucetOptions, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -#[async_trait] -impl CliCommand for FundWithFaucet { - fn command_name(&self) -> &'static str { - "FundWithFaucet" - } - - async fn execute(self) -> CliTypedResult { - // todo: the below is a hotfix. Determine why the auth_key parameter in the rpc is not working - /* - let hashes = fund_account( - self.faucet_options.faucet_url(&self.profile_options)?, - self.amount, - self.account, - ) - .await?; - */ - let hashes = fund_pub_key( - self.faucet_options.faucet_url(&self.profile_options)?, - (self.profile_options.public_key()?).to_string() - ).await?; - let client = self.rest_options.client(&self.profile_options)?; - wait_for_transactions(&client, hashes).await?; - return Ok(format!( - "Added 10 MOV to account {}", - self.account - )); - } -} diff --git a/m1/m1-cli/src/account/key_rotation.rs b/m1/m1-cli/src/account/key_rotation.rs deleted file mode 100644 index da008fb8..00000000 --- a/m1/m1-cli/src/account/key_rotation.rs +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::{ - types::{ - account_address_from_public_key, CliCommand, CliConfig, CliError, CliTypedResult, - ConfigSearchMode, EncodingOptions, EncodingType, ExtractPublicKey, ParsePrivateKey, - ProfileConfig, ProfileOptions, PublicKeyInputOptions, RestOptions, RotationProofChallenge, - TransactionOptions, TransactionSummary, - }, - utils::{prompt_yes, prompt_yes_with_override, read_line}, -}; -use aptos_cached_packages::aptos_stdlib; -use aptos_crypto::{ - ed25519::{Ed25519PrivateKey, Ed25519PublicKey}, - PrivateKey, SigningKey, -}; -use aptos_rest_client::{ - aptos_api_types::{AptosError, AptosErrorCode}, - error::{AptosErrorResponse, RestError}, - Client, -}; -use aptos_types::{account_address::AccountAddress, account_config::CORE_CODE_ADDRESS}; -use async_trait::async_trait; -use clap::Parser; -use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, path::PathBuf}; - -/// Rotate an account's authentication key -/// -/// Rotating the account's authentication key allows you to use a new -/// private key. You must provide a new private key. Once it is -/// rotated you will need to use the original account address, with the -/// new private key. There is an interactive prompt to help you add it -/// to a new profile. -#[derive(Debug, Parser)] -pub struct RotateKey { - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - - /// File name that contains the new private key encoded in the type from `--encoding` - #[clap(long, group = "new_private_key", parse(from_os_str))] - pub(crate) new_private_key_file: Option, - - /// New private key encoded in the type from `--encoding` - #[clap(long, group = "new_private_key")] - pub(crate) new_private_key: Option, - - /// Name of the profile to save the new private key - /// - /// If not provided, it will interactively have you save a profile, - /// unless `--skip_saving_profile` is provided - #[clap(long)] - pub(crate) save_to_profile: Option, - - /// Skip saving profile - /// - /// This skips the interactive profile saving after rotating the authentication key - #[clap(long)] - pub(crate) skip_saving_profile: bool, -} - -impl ParsePrivateKey for RotateKey {} - -impl RotateKey { - /// Extract private key from CLI args - pub fn extract_private_key( - &self, - encoding: EncodingType, - ) -> CliTypedResult> { - self.parse_private_key( - encoding, - self.new_private_key_file.clone(), - self.new_private_key.clone(), - ) - } -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct RotateSummary { - message: Option, - transaction: TransactionSummary, -} - -#[async_trait] -impl CliCommand for RotateKey { - fn command_name(&self) -> &'static str { - "RotateKey" - } - - async fn execute(self) -> CliTypedResult { - let new_private_key = self - .extract_private_key(self.txn_options.encoding_options.encoding)? - .ok_or_else(|| { - CliError::CommandArgumentError( - "One of ['--new-private-key', '--new-private-key-file'] must be used" - .to_string(), - ) - })?; - - let (current_private_key, sender_address) = self.txn_options.get_key_and_address()?; - - // Get sequence number for account - let sequence_number = self.txn_options.sequence_number(sender_address).await?; - let auth_key = self.txn_options.auth_key(sender_address).await?; - - let rotation_proof = RotationProofChallenge { - account_address: CORE_CODE_ADDRESS, - module_name: "account".to_string(), - struct_name: "RotationProofChallenge".to_string(), - sequence_number, - originator: sender_address, - current_auth_key: AccountAddress::from_bytes(auth_key) - .map_err(|err| CliError::UnableToParse("auth_key", err.to_string()))?, - new_public_key: new_private_key.public_key().to_bytes().to_vec(), - }; - - let rotation_msg = - bcs::to_bytes(&rotation_proof).map_err(|err| CliError::BCS("rotation_proof", err))?; - - // Signs the struct using both the current private key and the next private key - let rotation_proof_signed_by_current_private_key = - current_private_key.sign_arbitrary_message(&rotation_msg.clone()); - let rotation_proof_signed_by_new_private_key = - new_private_key.sign_arbitrary_message(&rotation_msg); - - let txn_summary = self - .txn_options - .submit_transaction(aptos_stdlib::account_rotate_authentication_key( - 0, - // Existing public key - current_private_key.public_key().to_bytes().to_vec(), - 0, - // New public key - new_private_key.public_key().to_bytes().to_vec(), - rotation_proof_signed_by_current_private_key - .to_bytes() - .to_vec(), - rotation_proof_signed_by_new_private_key.to_bytes().to_vec(), - )) - .await - .map(TransactionSummary::from)?; - - let string = serde_json::to_string_pretty(&txn_summary) - .map_err(|err| CliError::UnableToParse("transaction summary", err.to_string()))?; - - eprintln!("{}", string); - - if let Some(txn_success) = txn_summary.success { - if !txn_success { - return Err(CliError::ApiError( - "Transaction was not executed successfully".to_string(), - )); - } - } else { - return Err(CliError::UnexpectedError( - "Malformed transaction response".to_string(), - )); - } - - let mut profile_name: String; - - if self.save_to_profile.is_none() { - if self.skip_saving_profile - || !prompt_yes("Do you want to create a profile for the new key?") - { - return Ok(RotateSummary { - transaction: txn_summary, - message: None, - }); - } - - eprintln!("Enter the name for the profile"); - profile_name = read_line("Profile name")?.trim().to_string(); - } else { - // We can safely unwrap here - profile_name = self.save_to_profile.unwrap(); - } - - // Check if profile name exists - let mut config = CliConfig::load(ConfigSearchMode::CurrentDirAndParents)?; - - if let Some(ref profiles) = config.profiles { - if profiles.contains_key(&profile_name) { - if let Err(cli_err) = prompt_yes_with_override( - format!( - "Profile {} exits. Do you want to provide a new profile name?", - profile_name - ) - .as_str(), - self.txn_options.prompt_options, - ) { - match cli_err { - CliError::AbortedError => { - return Ok(RotateSummary { - transaction: txn_summary, - message: None, - }); - } - _ => { - return Err(cli_err); - } - } - } - - eprintln!("Enter the name for the profile"); - profile_name = read_line("Profile name")?.trim().to_string(); - } - } - - if profile_name.is_empty() { - return Err(CliError::AbortedError); - } - - let mut profile_config = ProfileConfig { - private_key: Some(new_private_key.clone()), - public_key: Some(new_private_key.public_key()), - account: Some(sender_address), - ..self.txn_options.profile_options.profile()? - }; - - if let Some(url) = self.txn_options.rest_options.url { - profile_config.rest_url = Some(url.into()); - } - - if config.profiles.is_none() { - config.profiles = Some(BTreeMap::new()); - } - - config - .profiles - .as_mut() - .unwrap() - .insert(profile_name.clone(), profile_config); - config.save()?; - - eprintln!("Profile {} is saved.", profile_name); - - Ok(RotateSummary { - transaction: txn_summary, - message: Some(format!("Profile {} is saved.", profile_name)), - }) - } -} - -/// Lookup the account address through the on-chain lookup table -/// -/// If the account is rotated, it will provide the address accordingly. If the account was not -/// rotated, it will provide the derived address only if the account exists onchain. -#[derive(Debug, Parser)] -pub struct LookupAddress { - #[clap(flatten)] - pub(crate) encoding_options: EncodingOptions, - - #[clap(flatten)] - pub(crate) public_key_options: PublicKeyInputOptions, - - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, - - #[clap(flatten)] - pub(crate) rest_options: RestOptions, -} - -impl LookupAddress { - pub(crate) fn public_key(&self) -> CliTypedResult { - self.public_key_options - .extract_public_key(self.encoding_options.encoding, &self.profile_options) - } - - /// Builds a rest client - fn rest_client(&self) -> CliTypedResult { - self.rest_options.client(&self.profile_options) - } -} - -#[async_trait] -impl CliCommand for LookupAddress { - fn command_name(&self) -> &'static str { - "LookupAddress" - } - - async fn execute(self) -> CliTypedResult { - let rest_client = self.rest_client()?; - - // TODO: Support arbitrary auth key to support other types like multie25519 - let address = account_address_from_public_key(&self.public_key()?); - Ok(lookup_address(&rest_client, address, true).await?) - } -} - -pub async fn lookup_address( - rest_client: &Client, - address_key: AccountAddress, - must_exist: bool, -) -> Result { - let originating_resource: OriginatingResource = rest_client - .get_account_resource_bcs(CORE_CODE_ADDRESS, "0x1::account::OriginatingAddress") - .await? - .into_inner(); - - let table_handle = originating_resource.address_map.handle; - - // The derived address that can be used to look up the original address - match rest_client - .get_table_item_bcs( - table_handle, - "address", - "address", - address_key.to_hex_literal(), - ) - .await - { - Ok(inner) => Ok(inner.into_inner()), - Err(RestError::Api(..)) => { - // If the table item wasn't found, we may check if the account exists - if !must_exist { - Ok(address_key) - } else { - rest_client - .get_account_bcs(address_key) - .await - .map(|_| address_key) - } - } - Err(err) => Err(err), - } -} - -#[derive(Deserialize)] -pub struct OriginatingResource { - pub address_map: Table, -} - -#[derive(Deserialize)] -pub struct Table { - pub handle: AccountAddress, -} diff --git a/m1/m1-cli/src/account/list.rs b/m1/m1-cli/src/account/list.rs deleted file mode 100644 index c406b296..00000000 --- a/m1/m1-cli/src/account/list.rs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::{ - CliCommand, CliConfig, CliError, CliTypedResult, ConfigSearchMode, ProfileOptions, RestOptions, -}; -use aptos_types::account_address::AccountAddress; -use async_trait::async_trait; -use clap::{ArgEnum, Parser}; -use serde_json::json; -use std::{ - fmt::{Display, Formatter}, - str::FromStr, -}; - -#[derive(ArgEnum, Clone, Copy, Debug)] -pub enum ListQuery { - Balance, - Modules, - Resources, -} - -impl Display for ListQuery { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - ListQuery::Balance => "balance", - ListQuery::Modules => "modules", - ListQuery::Resources => "resources", - }; - write!(f, "{}", str) - } -} - -impl FromStr for ListQuery { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "balance" => Ok(ListQuery::Balance), - "modules" => Ok(ListQuery::Modules), - "resources" => Ok(ListQuery::Resources), - _ => Err("Invalid query. Valid values are balance, modules, resources"), - } - } -} - -/// List resources, modules, or balance owned by an address -/// -/// This allows you to list the current resources at the time of query. This can change due to -/// any transactions that have occurred after the request. -#[derive(Debug, Parser)] -pub struct ListAccount { - /// Address of the account you want to list resources/modules/balance for - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub(crate) account: Option, - - /// Type of items to list: [balance, resources, modules] - #[clap(long, default_value_t = ListQuery::Resources)] - pub(crate) query: ListQuery, - - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -#[async_trait] -impl CliCommand> for ListAccount { - fn command_name(&self) -> &'static str { - "ListAccount" - } - - async fn execute(self) -> CliTypedResult> { - let account = if let Some(account) = self.account { - account - } else if let Some(Some(account)) = CliConfig::load_profile( - self.profile_options.profile_name(), - ConfigSearchMode::CurrentDirAndParents, - )? - .map(|p| p.account) - { - account - } else { - return Err(CliError::CommandArgumentError( - "Please provide an account using --account or run movement init".to_string(), - )); - }; - - let client = self.rest_options.client(&self.profile_options)?; - let response = match self.query { - ListQuery::Balance => vec![ - client - .get_account_resource( - account, - "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", - ) - .await? - .into_inner() - .unwrap() - .data, - ], - ListQuery::Modules => client - .get_account_modules(account) - .await? - .into_inner() - .into_iter() - .map(|module| json!(module.try_parse_abi().unwrap())) - .collect::>(), - ListQuery::Resources => client - .get_account_resources(account) - .await? - .into_inner() - .into_iter() - .map(|resource| { - let mut map = serde_json::Map::new(); - map.insert(resource.resource_type.to_string(), resource.data); - serde_json::Value::Object(map) - }) - .collect::>(), - }; - - Ok(response) - } -} diff --git a/m1/m1-cli/src/account/mod.rs b/m1/m1-cli/src/account/mod.rs deleted file mode 100644 index 988f065a..00000000 --- a/m1/m1-cli/src/account/mod.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::{CliCommand, CliResult}; -use clap::Subcommand; - -pub mod create; -pub mod create_resource_account; -pub mod derive_resource_account; -pub mod fund; -pub mod key_rotation; -pub mod list; -pub mod multisig_account; -pub mod transfer; - -/// Tool for interacting with accounts -/// -/// This tool is used to create accounts, get information about the -/// account's resources, and transfer resources between accounts. -#[derive(Debug, Subcommand)] -pub enum AccountTool { - Create(create::CreateAccount), - CreateResourceAccount(create_resource_account::CreateResourceAccount), - DeriveResourceAccountAddress(derive_resource_account::DeriveResourceAccount), - FundWithFaucet(fund::FundWithFaucet), - List(list::ListAccount), - LookupAddress(key_rotation::LookupAddress), - RotateKey(key_rotation::RotateKey), - Transfer(transfer::TransferCoins), -} - -impl AccountTool { - pub async fn execute(self) -> CliResult { - match self { - AccountTool::Create(tool) => tool.execute_serialized().await, - AccountTool::CreateResourceAccount(tool) => tool.execute_serialized().await, - AccountTool::DeriveResourceAccountAddress(tool) => tool.execute_serialized().await, - AccountTool::FundWithFaucet(tool) => tool.execute_serialized().await, - AccountTool::List(tool) => tool.execute_serialized().await, - AccountTool::LookupAddress(tool) => tool.execute_serialized().await, - AccountTool::RotateKey(tool) => tool.execute_serialized().await, - AccountTool::Transfer(tool) => tool.execute_serialized().await, - } - } -} - -/// Tool for interacting with multisig accounts -#[derive(Debug, Subcommand)] -pub enum MultisigAccountTool { - Approve(multisig_account::Approve), - Create(multisig_account::Create), - CreateTransaction(multisig_account::CreateTransaction), - Execute(multisig_account::Execute), - ExecuteReject(multisig_account::ExecuteReject), - Reject(multisig_account::Reject), -} - -impl MultisigAccountTool { - pub async fn execute(self) -> CliResult { - match self { - MultisigAccountTool::Approve(tool) => tool.execute_serialized().await, - MultisigAccountTool::Create(tool) => tool.execute_serialized().await, - MultisigAccountTool::CreateTransaction(tool) => tool.execute_serialized().await, - MultisigAccountTool::Execute(tool) => tool.execute_serialized().await, - MultisigAccountTool::ExecuteReject(tool) => tool.execute_serialized().await, - MultisigAccountTool::Reject(tool) => tool.execute_serialized().await, - } - } -} diff --git a/m1/m1-cli/src/account/multisig_account.rs b/m1/m1-cli/src/account/multisig_account.rs deleted file mode 100644 index 0d60380a..00000000 --- a/m1/m1-cli/src/account/multisig_account.rs +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::{ - CliCommand, CliTypedResult, EntryFunctionArguments, MultisigAccount, TransactionOptions, - TransactionSummary, -}; -use aptos_cached_packages::aptos_stdlib; -use aptos_rest_client::{ - aptos_api_types::{WriteResource, WriteSetChange}, - Transaction, -}; -use aptos_types::{ - account_address::AccountAddress, - transaction::{Multisig, MultisigTransactionPayload, TransactionPayload}, -}; -use async_trait::async_trait; -use bcs::to_bytes; -use clap::Parser; -use serde::Serialize; - -/// Create a new multisig account (v2) on-chain. -/// -/// This will create a new multisig account and make the sender one of the owners. -#[derive(Debug, Parser)] -pub struct Create { - /// Addresses of additional owners for the new multisig, beside the transaction sender. - #[clap(long, multiple_values = true, parse(try_from_str=crate::common::types::load_account_arg))] - pub(crate) additional_owners: Vec, - /// The number of signatures (approvals or rejections) required to execute or remove a proposed - /// transaction. - #[clap(long)] - pub(crate) num_signatures_required: u64, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -/// A shortened create multisig account output -#[derive(Clone, Debug, Serialize)] -pub struct CreateSummary { - #[serde(flatten)] - pub multisig_account: Option, - #[serde(flatten)] - pub transaction_summary: TransactionSummary, -} - -impl From for CreateSummary { - fn from(transaction: Transaction) -> Self { - let transaction_summary = TransactionSummary::from(&transaction); - - let mut summary = CreateSummary { - transaction_summary, - multisig_account: None, - }; - - if let Transaction::UserTransaction(txn) = transaction { - summary.multisig_account = txn.info.changes.iter().find_map(|change| match change { - WriteSetChange::WriteResource(WriteResource { address, data, .. }) => { - if data.typ.name.as_str() == "Account" - && *address.inner().to_hex() != *txn.request.sender.inner().to_hex() - { - Some(MultisigAccount { - multisig_address: *address.inner(), - }) - } else { - None - } - }, - _ => None, - }); - } - - summary - } -} - -#[async_trait] -impl CliCommand for Create { - fn command_name(&self) -> &'static str { - "CreateMultisig" - } - - async fn execute(self) -> CliTypedResult { - self.txn_options - .submit_transaction(aptos_stdlib::multisig_account_create_with_owners( - self.additional_owners, - self.num_signatures_required, - // TODO: Support passing in custom metadata. - vec![], - vec![], - )) - .await - .map(CreateSummary::from) - } -} - -/// Propose a new multisig transaction. -/// -/// As one of the owners of the multisig, propose a new transaction. This also implicitly approves -/// the created transaction so it has one approval initially. In order for the transaction to be -/// executed, it needs as many approvals as the number of signatures required. -#[derive(Debug, Parser)] -pub struct CreateTransaction { - #[clap(flatten)] - pub(crate) multisig_account: MultisigAccount, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) entry_function_args: EntryFunctionArguments, -} - -#[async_trait] -impl CliCommand for CreateTransaction { - fn command_name(&self) -> &'static str { - "CreateTransactionMultisig" - } - - async fn execute(self) -> CliTypedResult { - let payload = MultisigTransactionPayload::EntryFunction( - self.entry_function_args.create_entry_function_payload()?, - ); - self.txn_options - .submit_transaction(aptos_stdlib::multisig_account_create_transaction( - self.multisig_account.multisig_address, - to_bytes(&payload)?, - )) - .await - .map(|inner| inner.into()) - } -} - -/// Approve a multisig transaction. -/// -/// As one of the owners of the multisig, approve a transaction proposed for the multisig. -/// With enough approvals (as many as the number of signatures required), the transaction can be -/// executed (See Execute). -#[derive(Debug, Parser)] -pub struct Approve { - #[clap(flatten)] - pub(crate) multisig_account: MultisigAccount, - /// The sequence number of the multisig transaction to approve. The sequence number increments - /// for every new multisig transaction. - #[clap(long)] - pub(crate) sequence_number: u64, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for Approve { - fn command_name(&self) -> &'static str { - "ApproveMultisig" - } - - async fn execute(self) -> CliTypedResult { - self.txn_options - .submit_transaction(aptos_stdlib::multisig_account_approve_transaction( - self.multisig_account.multisig_address, - self.sequence_number, - )) - .await - .map(|inner| inner.into()) - } -} - -/// Reject a multisig transaction. -/// -/// As one of the owners of the multisig, reject a transaction proposed for the multisig. -/// With enough rejections (as many as the number of signatures required), the transaction can be -/// completely removed (See ExecuteReject). -#[derive(Debug, Parser)] -pub struct Reject { - #[clap(flatten)] - pub(crate) multisig_account: MultisigAccount, - /// The sequence number of the multisig transaction to reject. The sequence number increments - /// for every new multisig transaction. - #[clap(long)] - pub(crate) sequence_number: u64, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for Reject { - fn command_name(&self) -> &'static str { - "RejectMultisig" - } - - async fn execute(self) -> CliTypedResult { - self.txn_options - .submit_transaction(aptos_stdlib::multisig_account_reject_transaction( - self.multisig_account.multisig_address, - self.sequence_number, - )) - .await - .map(|inner| inner.into()) - } -} - -/// Execute a proposed multisig transaction. -/// -/// The transaction to be executed needs to have as many approvals as the number of signatures -/// required. -#[derive(Debug, Parser)] -pub struct Execute { - #[clap(flatten)] - pub(crate) multisig_account: MultisigAccount, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for Execute { - fn command_name(&self) -> &'static str { - "ExecuteMultisig" - } - - async fn execute(self) -> CliTypedResult { - let payload = TransactionPayload::Multisig(Multisig { - multisig_address: self.multisig_account.multisig_address, - // TODO: Support passing an explicit payload - transaction_payload: None, - }); - self.txn_options - .submit_transaction(payload) - .await - .map(|inner| inner.into()) - } -} - -/// Remove a proposed multisig transaction. -/// -/// The transaction to be removed needs to have as many rejections as the number of signatures -/// required. -#[derive(Debug, Parser)] -pub struct ExecuteReject { - #[clap(flatten)] - pub(crate) multisig_account: MultisigAccount, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for ExecuteReject { - fn command_name(&self) -> &'static str { - "ExecuteRejectMultisig" - } - - async fn execute(self) -> CliTypedResult { - self.txn_options - .submit_transaction(aptos_stdlib::multisig_account_execute_rejected_transaction( - self.multisig_account.multisig_address, - )) - .await - .map(|inner| inner.into()) - } -} diff --git a/m1/m1-cli/src/account/transfer.rs b/m1/m1-cli/src/account/transfer.rs deleted file mode 100644 index ce8b697d..00000000 --- a/m1/m1-cli/src/account/transfer.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::{CliCommand, CliTypedResult, TransactionOptions}; -use aptos_cached_packages::aptos_stdlib; -use aptos_rest_client::{ - aptos_api_types::{HashValue, WriteResource, WriteSetChange}, - Transaction, -}; -use aptos_types::account_address::AccountAddress; -use async_trait::async_trait; -use clap::Parser; -use serde::Serialize; -use std::collections::BTreeMap; - -// TODO: Add ability to transfer non-APT coins -// TODO: Add ability to not create account by default -/// Transfer APT between accounts -/// -#[derive(Debug, Parser)] -pub struct TransferCoins { - /// Address of account to send APT to - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) account: AccountAddress, - - /// Amount of Octas (10^-8 APT) to transfer - #[clap(long)] - pub(crate) amount: u64, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for TransferCoins { - fn command_name(&self) -> &'static str { - "TransferCoins" - } - - async fn execute(self) -> CliTypedResult { - self.txn_options - .submit_transaction(aptos_stdlib::aptos_account_transfer( - self.account, - self.amount, - )) - .await - .map(TransferSummary::from) - } -} - -const SUPPORTED_COINS: [&str; 1] = ["0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"]; - -/// A shortened transaction output -#[derive(Clone, Debug, Serialize)] -pub struct TransferSummary { - pub gas_unit_price: u64, - pub gas_used: u64, - pub balance_changes: BTreeMap, - pub sender: AccountAddress, - pub success: bool, - pub version: u64, - pub vm_status: String, - pub transaction_hash: HashValue, -} - -impl TransferSummary { - pub fn octa_spent(&self) -> u64 { - self.gas_unit_price * self.gas_used - } -} - -impl From for TransferSummary { - fn from(transaction: Transaction) -> Self { - if let Transaction::UserTransaction(txn) = transaction { - let vm_status = txn.info.vm_status; - let success = txn.info.success; - let sender = *txn.request.sender.inner(); - let gas_unit_price = txn.request.gas_unit_price.0; - let gas_used = txn.info.gas_used.0; - let transaction_hash = txn.info.hash; - let version = txn.info.version.0; - let balance_changes = txn - .info - .changes - .into_iter() - .filter_map(|change| match change { - WriteSetChange::WriteResource(WriteResource { address, data, .. }) => { - if SUPPORTED_COINS.contains(&data.typ.to_string().as_str()) { - Some(( - *address.inner(), - serde_json::to_value(data.data).unwrap_or_default(), - )) - } else { - None - } - }, - _ => None, - }) - .collect(); - - TransferSummary { - gas_unit_price, - gas_used, - balance_changes, - sender, - success, - version, - vm_status, - transaction_hash, - } - } else { - panic!("Can't call From for a non UserTransaction") - } - } -} diff --git a/m1/m1-cli/src/common/init.rs b/m1/m1-cli/src/common/init.rs deleted file mode 100644 index 9cd60c4b..00000000 --- a/m1/m1-cli/src/common/init.rs +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - account::key_rotation::lookup_address, - common::{ - types::{ - account_address_from_public_key, CliCommand, CliConfig, CliError, CliTypedResult, - ConfigSearchMode, EncodingOptions, PrivateKeyInputOptions, ProfileConfig, - ProfileOptions, PromptOptions, RngArgs, DEFAULT_PROFILE, - }, - utils::{fund_account, fund_pub_key, prompt_yes_with_override, read_line, wait_for_transactions}, - }, -}; -use aptos_crypto::{ed25519::Ed25519PrivateKey, PrivateKey, ValidCryptoMaterialStringExt}; -use aptos_rest_client::{ - aptos_api_types::{AptosError, AptosErrorCode}, - error::{AptosErrorResponse, RestError}, -}; -use async_trait::async_trait; -use clap::Parser; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, str::FromStr}; - -// -const SEED_NODE_1_REST : &str = "https://seed-node1.movementlabs.xyz"; - -/// 1 APT (might not actually get that much, depending on the faucet) -const NUM_DEFAULT_OCTAS: u64 = 100000000; - -/// Tool to initialize current directory for the aptos tool -/// -/// Configuration will be pushed into .aptos/config.yaml -#[derive(Debug, Parser)] -pub struct InitTool { - /// Network to use for default settings - /// - /// If custom `rest_url` and `faucet_url` are wanted, use `custom` - #[clap(long)] - pub network: Option, - - /// URL to a fullnode on the network - #[clap(long)] - pub rest_url: Option, - - /// URL for the Faucet endpoint - #[clap(long)] - pub faucet_url: Option, - - /// Whether to skip the faucet for a non-faucet endpoint - #[clap(long)] - pub skip_faucet: bool, - - #[clap(flatten)] - pub rng_args: RngArgs, - #[clap(flatten)] - pub(crate) private_key_options: PrivateKeyInputOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, - #[clap(flatten)] - pub(crate) encoding_options: EncodingOptions, -} - -#[async_trait] -impl CliCommand<()> for InitTool { - fn command_name(&self) -> &'static str { - "MovementInit" - } - - async fn execute(self) -> CliTypedResult<()> { - let mut config = if CliConfig::config_exists(ConfigSearchMode::CurrentDir) { - CliConfig::load(ConfigSearchMode::CurrentDir)? - } else { - CliConfig::default() - }; - - let profile_name = self - .profile_options - .profile_name() - .unwrap_or(DEFAULT_PROFILE); - - // Select profile we're using - let mut profile_config = if let Some(profile_config) = config.remove_profile(profile_name) { - prompt_yes_with_override(&format!("Movement already initialized for profile {}, do you want to overwrite the existing config?", profile_name), self.prompt_options)?; - profile_config - } else { - ProfileConfig::default() - }; - - eprintln!("Configuring for profile {}", profile_name); - - // Choose a network - let network = if let Some(network) = self.network { - eprintln!("Configuring for network {:?}", network); - network - } else { - eprintln!( - "Choose network from [devnet, testnet, mainnet, local, custom | defaults to devnet]" - ); - let input = read_line("network")?; - let input = input.trim(); - if input.is_empty() { - eprintln!("No network given, using devnet..."); - Network::Devnet - } else { - Network::from_str(input)? - } - }; - - // Ensure that there is at least a REST URL set for the network - match network { - Network::Mainnet => { - profile_config.rest_url = - Some(SEED_NODE_1_REST.to_string()); - profile_config.faucet_url = - Some(SEED_NODE_1_REST.to_string()); - }, - Network::Testnet => { - profile_config.rest_url = - Some(SEED_NODE_1_REST.to_string()); - profile_config.faucet_url = - Some(SEED_NODE_1_REST.to_string()); - }, - Network::Devnet => { - profile_config.rest_url = Some(SEED_NODE_1_REST.to_string()); - profile_config.faucet_url = Some(SEED_NODE_1_REST.to_string()); - }, - Network::Local => { - profile_config.rest_url = Some("http://localhost:8080".to_string()); - profile_config.faucet_url = Some("http://localhost:8081".to_string()); - }, - Network::Custom => self.custom_network(&mut profile_config)?, - } - - // Private key - let private_key = if let Some(private_key) = self - .private_key_options - .extract_private_key_cli(self.encoding_options.encoding)? - { - eprintln!("Using command line argument for private key"); - private_key - } else { - eprintln!("Enter your private key as a hex literal (0x...) [Current: {} | No input: Generate new key (or keep one if present)]", profile_config.private_key.as_ref().map(|_| "Redacted").unwrap_or("None")); - let input = read_line("Private key")?; - let input = input.trim(); - if input.is_empty() { - if let Some(private_key) = profile_config.private_key { - eprintln!("No key given, keeping existing key..."); - private_key - } else { - eprintln!("No key given, generating key..."); - self.rng_args - .key_generator()? - .generate_ed25519_private_key() - } - } else { - Ed25519PrivateKey::from_encoded_string(input) - .map_err(|err| CliError::UnableToParse("Ed25519PrivateKey", err.to_string()))? - } - }; - let public_key = private_key.public_key(); - - let client = aptos_rest_client::Client::new( - Url::parse( - profile_config - .rest_url - .as_ref() - .expect("Must have rest client as created above"), - ) - .map_err(|err| CliError::UnableToParse("rest_url", err.to_string()))?, - ); - - // lookup the address from onchain instead of deriving it - // if this is the rotated key, deriving it will outputs an incorrect address - let derived_address = account_address_from_public_key(&public_key); - let address = lookup_address(&client, derived_address, false).await?; - - profile_config.private_key = Some(private_key); - profile_config.public_key = Some(public_key.clone()); - profile_config.account = Some(address); - - // Create account if it doesn't exist (and there's a faucet) - // Check if account exists - let account_exists = match client.get_account(address).await { - Ok(_) => true, - Err(err) => { - if let RestError::Api(AptosErrorResponse { - error: - AptosError { - error_code: AptosErrorCode::ResourceNotFound, - .. - }, - .. - }) - | RestError::Api(AptosErrorResponse { - error: - AptosError { - error_code: AptosErrorCode::AccountNotFound, - .. - }, - .. - }) = err - { - false - } else { - return Err(CliError::UnexpectedError(format!( - "Failed to check if account exists: {:?}", - err - ))); - } - }, - }; - - // If you want to create a private key, but not fund the account, skipping the faucet is still possible - let maybe_faucet_url = if self.skip_faucet { - None - } else { - profile_config.faucet_url.as_ref() - }; - - if let Some(faucet_url) = maybe_faucet_url { - if account_exists { - eprintln!("Account {} has been already found onchain", address); - } else { - eprintln!( - "Account {} doesn't exist, creating it and funding it with {} Octas", - address, NUM_DEFAULT_OCTAS - ); - let hashes = fund_pub_key( - Url::parse(faucet_url) - .map_err(|err| CliError::UnableToParse("rest_url", err.to_string()))?, - // NUM_DEFAULT_OCTAS, - (public_key.clone()).to_string(), - ) - .await?; - wait_for_transactions(&client, hashes).await?; - eprintln!("Account {} funded successfully", address); - } - } else if account_exists { - eprintln!("Account {} has been already found onchain", address); - } else if network == Network::Mainnet { - eprintln!("Account {} does not exist, you will need to create and fund the account by transferring funds from another account", address); - } else { - eprintln!("Account {} has been initialized locally, but you must transfer coins to it to create the account onchain", address); - } - - // Ensure the loaded config has profiles setup for a possible empty file - if config.profiles.is_none() { - config.profiles = Some(BTreeMap::new()); - } - config - .profiles - .as_mut() - .expect("Must have profiles, as created above") - .insert(profile_name.to_string(), profile_config); - config.save()?; - eprintln!("\n---\nMovement CLI is now set up for account {} as profile {}! Run `movement --help` for more information about commands", address, self.profile_options.profile_name().unwrap_or(DEFAULT_PROFILE)); - Ok(()) - } -} - -impl InitTool { - /// Custom network created, which requires a REST URL - fn custom_network(&self, profile_config: &mut ProfileConfig) -> CliTypedResult<()> { - // Rest Endpoint - let rest_url = if let Some(ref rest_url) = self.rest_url { - eprintln!("Using command line argument for rest URL {}", rest_url); - Some(rest_url.to_string()) - } else { - let current = profile_config.rest_url.as_deref(); - eprintln!( - "Enter your rest endpoint [Current: {} | No input: Exit (or keep the existing if present)]", - current.unwrap_or("None"), - ); - let input = read_line("Rest endpoint")?; - let input = input.trim(); - if input.is_empty() { - if let Some(current) = current { - eprintln!("No rest url given, keeping the existing url..."); - Some(current.to_string()) - } else { - eprintln!("No rest url given, exiting..."); - return Err(CliError::AbortedError); - } - } else { - Some( - reqwest::Url::parse(input) - .map_err(|err| CliError::UnableToParse("Rest Endpoint", err.to_string()))? - .to_string(), - ) - } - }; - profile_config.rest_url = rest_url; - - // Faucet Endpoint - let faucet_url = if self.skip_faucet { - eprintln!("Not configuring a faucet because --skip-faucet was provided"); - None - } else if let Some(ref faucet_url) = self.faucet_url { - eprintln!("Using command line argument for faucet URL {}", faucet_url); - Some(faucet_url.to_string()) - } else { - let current = profile_config.faucet_url.as_deref(); - eprintln!( - "Enter your faucet endpoint [Current: {} | No input: Skip (or keep the existing one if present) | 'skip' to not use a faucet]", - current - .unwrap_or("None"), - ); - let input = read_line("Faucet endpoint")?; - let input = input.trim(); - if input.is_empty() { - if let Some(current) = current { - eprintln!("No faucet url given, keeping the existing url..."); - Some(current.to_string()) - } else { - eprintln!("No faucet url given, skipping faucet..."); - None - } - } else if input.to_lowercase() == "skip" { - eprintln!("Skipping faucet..."); - None - } else { - Some( - reqwest::Url::parse(input) - .map_err(|err| CliError::UnableToParse("Faucet Endpoint", err.to_string()))? - .to_string(), - ) - } - }; - profile_config.faucet_url = faucet_url; - Ok(()) - } -} - -/// A simplified list of all networks supported by the CLI -/// -/// Any command using this, will be simpler to setup as profiles -#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub enum Network { - Mainnet, - Testnet, - Devnet, - Local, - Custom, -} - -impl FromStr for Network { - type Err = CliError; - - fn from_str(s: &str) -> Result { - Ok(match s.to_lowercase().trim() { - "mainnet" => Self::Mainnet, - "testnet" => Self::Testnet, - "devnet" => Self::Devnet, - "local" => Self::Local, - "custom" => Self::Custom, - str => { - return Err(CliError::CommandArgumentError(format!( - "Invalid network {}. Must be one of [devnet, testnet, mainnet, local, custom]", - str - ))); - }, - }) - } -} - -impl Default for Network { - fn default() -> Self { - Self::Devnet - } -} diff --git a/m1/m1-cli/src/common/mod.rs b/m1/m1-cli/src/common/mod.rs deleted file mode 100644 index 1be94da1..00000000 --- a/m1/m1-cli/src/common/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -pub mod init; -pub mod types; -pub mod utils; diff --git a/m1/m1-cli/src/common/types.rs b/m1/m1-cli/src/common/types.rs deleted file mode 100644 index bbaf52b0..00000000 --- a/m1/m1-cli/src/common/types.rs +++ /dev/null @@ -1,1753 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::{ - init::Network, - utils::{ - check_if_file_exists, create_dir_if_not_exist, dir_default_to_current, - get_account_with_state, get_auth_key, get_sequence_number, prompt_yes_with_override, - read_from_file, start_logger, to_common_result, to_common_success_result, - write_to_file, write_to_file_with_opts, write_to_user_only_file, - }, - }, - config::GlobalConfig, - genesis::git::from_yaml, - move_tool::{ArgWithType, MemberId}, -}; -use aptos_crypto::{ - ed25519::{Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature}, - x25519, PrivateKey, ValidCryptoMaterial, ValidCryptoMaterialStringExt, -}; -use aptos_debugger::AptosDebugger; -use aptos_gas_profiling::FrameName; -use aptos_global_constants::adjust_gas_headroom; -use aptos_keygen::KeyGen; -use aptos_rest_client::{ - aptos_api_types::{HashValue, MoveType, ViewRequest}, - error::RestError, - Client, Transaction, -}; -use aptos_sdk::{transaction_builder::TransactionFactory, types::LocalAccount}; -use aptos_types::{ - chain_id::ChainId, - transaction::{ - authenticator::AuthenticationKey, EntryFunction, SignedTransaction, TransactionPayload, - TransactionStatus, - }, -}; -use async_trait::async_trait; -use clap::{ArgEnum, Parser}; -use hex::FromHexError; -use move_core_types::{account_address::AccountAddress, language_storage::TypeTag}; -use serde::{Deserialize, Serialize}; -#[cfg(unix)] -use std::os::unix::fs::OpenOptionsExt; -use std::{ - collections::BTreeMap, - convert::TryFrom, - fmt::{Debug, Display, Formatter}, - fs::OpenOptions, - path::{Path, PathBuf}, - str::FromStr, - time::{Duration, Instant, SystemTime, UNIX_EPOCH}, -}; -use reqwest::Url; -use thiserror::Error; - -pub const USER_AGENT: &str = concat!("movement-cli/", env!("CARGO_PKG_VERSION")); -const US_IN_SECS: u64 = 1_000_000; -const ACCEPTED_CLOCK_SKEW_US: u64 = 5 * US_IN_SECS; -pub const DEFAULT_EXPIRATION_SECS: u64 = 60; -pub const DEFAULT_PROFILE: &str = "default"; - -/// A common result to be returned to users -pub type CliResult = Result; - -/// A common result to remove need for typing `Result` -pub type CliTypedResult = Result; - -/// CLI Errors for reporting through telemetry and outputs -#[derive(Debug, Error)] -pub enum CliError { - #[error("Aborted command")] - AbortedError, - #[error("API error: {0}")] - ApiError(String), - #[error("Error (de)serializing '{0}': {1}")] - BCS(&'static str, #[source] bcs::Error), - #[error("Invalid arguments: {0}")] - CommandArgumentError(String), - #[error("Unable to load config: {0} {1}")] - ConfigLoadError(String, String), - #[error("Unable to find config {0}, have you run `movement init`?")] - ConfigNotFoundError(String), - #[error("Error accessing '{0}': {1}")] - IO(String, #[source] std::io::Error), - #[error("Move compilation failed: {0}")] - MoveCompilationError(String), - #[error("Move unit tests failed")] - MoveTestError, - #[error("Move Prover failed: {0}")] - MoveProverError(String), - #[error("Unable to parse '{0}': error: {1}")] - UnableToParse(&'static str, String), - #[error("Unable to read file '{0}', error: {1}")] - UnableToReadFile(String, String), - #[error("Unexpected error: {0}")] - UnexpectedError(String), - #[error("Simulation failed with status: {0}")] - SimulationError(String), - #[error("Coverage failed with status: {0}")] - CoverageError(String), -} - -impl CliError { - pub fn to_str(&self) -> &'static str { - match self { - CliError::AbortedError => "AbortedError", - CliError::ApiError(_) => "ApiError", - CliError::BCS(_, _) => "BCS", - CliError::CommandArgumentError(_) => "CommandArgumentError", - CliError::ConfigLoadError(_, _) => "ConfigLoadError", - CliError::ConfigNotFoundError(_) => "ConfigNotFoundError", - CliError::IO(_, _) => "IO", - CliError::MoveCompilationError(_) => "MoveCompilationError", - CliError::MoveTestError => "MoveTestError", - CliError::MoveProverError(_) => "MoveProverError", - CliError::UnableToParse(_, _) => "UnableToParse", - CliError::UnableToReadFile(_, _) => "UnableToReadFile", - CliError::UnexpectedError(_) => "UnexpectedError", - CliError::SimulationError(_) => "SimulationError", - CliError::CoverageError(_) => "CoverageError", - } - } -} - -impl From for CliError { - fn from(e: RestError) -> Self { - CliError::ApiError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: aptos_config::config::Error) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: aptos_github_client::Error) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: serde_yaml::Error) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: base64::DecodeError) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: std::string::FromUtf8Error) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: aptos_crypto::CryptoMaterialError) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: FromHexError) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: anyhow::Error) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -impl From for CliError { - fn from(e: bcs::Error) -> Self { - CliError::UnexpectedError(e.to_string()) - } -} - -/// Config saved to `.aptos/config.yaml` -#[derive(Debug, Serialize, Deserialize)] -pub struct CliConfig { - /// Map of profile configs - #[serde(skip_serializing_if = "Option::is_none")] - pub profiles: Option>, -} - -const CONFIG_FILE: &str = "config.yaml"; -const LEGACY_CONFIG_FILE: &str = "config.yml"; -pub const CONFIG_FOLDER: &str = ".movement"; - -/// An individual profile -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct ProfileConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub network: Option, - /// Private key for commands. - #[serde(skip_serializing_if = "Option::is_none")] - pub private_key: Option, - /// Public key for commands - #[serde(skip_serializing_if = "Option::is_none")] - pub public_key: Option, - /// Account for commands - #[serde(skip_serializing_if = "Option::is_none")] - pub account: Option, - /// URL for the Aptos rest endpoint - #[serde(skip_serializing_if = "Option::is_none")] - pub rest_url: Option, - /// URL for the Faucet endpoint (if applicable) - #[serde(skip_serializing_if = "Option::is_none")] - pub faucet_url: Option, -} - -/// ProfileConfig but without the private parts -#[derive(Debug, Serialize)] -pub struct ProfileSummary { - pub has_private_key: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub public_key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub account: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub rest_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub faucet_url: Option, -} - -impl From<&ProfileConfig> for ProfileSummary { - fn from(config: &ProfileConfig) -> Self { - ProfileSummary { - has_private_key: config.private_key.is_some(), - public_key: config.public_key.clone(), - account: config.account, - rest_url: config.rest_url.clone(), - faucet_url: config.faucet_url.clone(), - } - } -} - -impl Default for CliConfig { - fn default() -> Self { - CliConfig { - profiles: Some(BTreeMap::new()), - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] -pub enum ConfigSearchMode { - CurrentDir, - CurrentDirAndParents, -} - -impl CliConfig { - /// Checks if the config exists in the current working directory - pub fn config_exists(mode: ConfigSearchMode) -> bool { - if let Ok(folder) = Self::aptos_folder(mode) { - let config_file = folder.join(CONFIG_FILE); - let old_config_file = folder.join(LEGACY_CONFIG_FILE); - config_file.exists() || old_config_file.exists() - } else { - false - } - } - - /// Loads the config from the current working directory or one of its parents. - pub fn load(mode: ConfigSearchMode) -> CliTypedResult { - let folder = Self::aptos_folder(mode)?; - - let config_file = folder.join(CONFIG_FILE); - let old_config_file = folder.join(LEGACY_CONFIG_FILE); - if config_file.exists() { - from_yaml( - &String::from_utf8(read_from_file(config_file.as_path())?) - .map_err(CliError::from)?, - ) - } else if old_config_file.exists() { - from_yaml( - &String::from_utf8(read_from_file(old_config_file.as_path())?) - .map_err(CliError::from)?, - ) - } else { - Err(CliError::ConfigNotFoundError(format!( - "{}", - config_file.display() - ))) - } - } - - pub fn load_profile( - profile: Option<&str>, - mode: ConfigSearchMode, - ) -> CliTypedResult> { - let mut config = Self::load(mode)?; - - // If no profile was given, use `default` - if let Some(profile) = profile { - if let Some(account_profile) = config.remove_profile(profile) { - Ok(Some(account_profile)) - } else { - Err(CliError::CommandArgumentError(format!( - "Profile {} not found", - profile - ))) - } - } else { - Ok(config.remove_profile(DEFAULT_PROFILE)) - } - } - - pub fn remove_profile(&mut self, profile: &str) -> Option { - if let Some(ref mut profiles) = self.profiles { - profiles.remove(&profile.to_string()) - } else { - None - } - } - - /// Saves the config to ./.aptos/config.yaml - pub fn save(&self) -> CliTypedResult<()> { - let aptos_folder = Self::aptos_folder(ConfigSearchMode::CurrentDir)?; - - // Create if it doesn't exist - create_dir_if_not_exist(aptos_folder.as_path())?; - - // Save over previous config file - let config_file = aptos_folder.join(CONFIG_FILE); - let config_bytes = serde_yaml::to_string(&self).map_err(|err| { - CliError::UnexpectedError(format!("Failed to serialize config {}", err)) - })?; - write_to_user_only_file(&config_file, CONFIG_FILE, config_bytes.as_bytes())?; - - // As a cleanup, delete the old if it exists - let legacy_config_file = aptos_folder.join(LEGACY_CONFIG_FILE); - if legacy_config_file.exists() { - eprintln!("Removing legacy config file {}", LEGACY_CONFIG_FILE); - let _ = std::fs::remove_file(legacy_config_file); - } - Ok(()) - } - - /// Finds the current directory's .aptos folder - fn aptos_folder(mode: ConfigSearchMode) -> CliTypedResult { - let global_config = GlobalConfig::load()?; - global_config.get_config_location(mode) - } -} - -/// Types of Keys used by the blockchain -#[derive(ArgEnum, Clone, Copy, Debug)] -pub enum KeyType { - /// Ed25519 key used for signing - Ed25519, - /// X25519 key used for network handshakes and identity - X25519, - /// A BLS12381 key for consensus - Bls12381, -} - -impl Display for KeyType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - KeyType::Ed25519 => "ed25519", - KeyType::X25519 => "x25519", - KeyType::Bls12381 => "bls12381", - }; - write!(f, "{}", str) - } -} - -impl FromStr for KeyType { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "ed25519" => Ok(KeyType::Ed25519), - "x25519" => Ok(KeyType::X25519), - "bls12381" => Ok(KeyType::Bls12381), - _ => Err("Invalid key type: Must be one of [ed25519, x25519]"), - } - } -} - -#[derive(Debug, Default, Parser)] -pub struct ProfileOptions { - /// Profile to use from the CLI config - /// - /// This will be used to override associated settings such as - /// the REST URL, the Faucet URL, and the private key arguments. - /// - /// Defaults to "default" - #[clap(long)] - pub profile: Option, -} - -impl ProfileOptions { - pub fn account_address(&self) -> CliTypedResult { - let profile = self.profile()?; - if let Some(account) = profile.account { - return Ok(account); - } - - Err(CliError::ConfigNotFoundError( - self.profile - .clone() - .unwrap_or_else(|| DEFAULT_PROFILE.to_string()), - )) - } - - pub fn public_key(&self) -> CliTypedResult { - let profile = self.profile()?; - if let Some(public_key) = profile.public_key { - return Ok(public_key); - } - - Err(CliError::ConfigNotFoundError( - self.profile - .clone() - .unwrap_or_else(|| DEFAULT_PROFILE.to_string()), - )) - } - - pub fn profile_name(&self) -> Option<&str> { - self.profile.as_ref().map(|inner| inner.trim()) - } - - pub fn profile(&self) -> CliTypedResult { - if let Some(profile) = - CliConfig::load_profile(self.profile_name(), ConfigSearchMode::CurrentDirAndParents)? - { - return Ok(profile); - } - - Err(CliError::ConfigNotFoundError( - self.profile - .clone() - .unwrap_or_else(|| DEFAULT_PROFILE.to_string()), - )) - } -} - -/// Types of encodings used by the blockchain -#[derive(ArgEnum, Clone, Copy, Debug)] -pub enum EncodingType { - /// Binary Canonical Serialization - BCS, - /// Hex encoded e.g. 0xABCDE12345 - Hex, - /// Base 64 encoded - Base64, -} - -impl EncodingType { - /// Encodes `Key` into one of the `EncodingType`s - pub fn encode_key( - &self, - name: &'static str, - key: &Key, - ) -> CliTypedResult> { - Ok(match self { - EncodingType::Hex => hex::encode_upper(key.to_bytes()).into_bytes(), - EncodingType::BCS => bcs::to_bytes(key).map_err(|err| CliError::BCS(name, err))?, - EncodingType::Base64 => base64::encode(key.to_bytes()).into_bytes(), - }) - } - - /// Loads a key from a file - pub fn load_key( - &self, - name: &'static str, - path: &Path, - ) -> CliTypedResult { - self.decode_key(name, read_from_file(path)?) - } - - /// Decodes an encoded key given the known encoding - pub fn decode_key( - &self, - name: &'static str, - data: Vec, - ) -> CliTypedResult { - match self { - EncodingType::BCS => bcs::from_bytes(&data).map_err(|err| CliError::BCS(name, err)), - EncodingType::Hex => { - let hex_string = String::from_utf8(data)?; - Key::from_encoded_string(hex_string.trim()) - .map_err(|err| CliError::UnableToParse(name, err.to_string())) - } - EncodingType::Base64 => { - let string = String::from_utf8(data)?; - let bytes = base64::decode(string.trim()) - .map_err(|err| CliError::UnableToParse(name, err.to_string()))?; - Key::try_from(bytes.as_slice()).map_err(|err| { - CliError::UnableToParse(name, format!("Failed to parse key {:?}", err)) - }) - } - } - } -} - -#[derive(Clone, Debug, Parser)] -pub struct RngArgs { - /// The seed used for key generation, should be a 64 character hex string and only used for testing - /// - /// If a predictable random seed is used, the key that is produced will be insecure and easy - /// to reproduce. Please do not use this unless sufficient randomness is put into the random - /// seed. - #[clap(long)] - random_seed: Option, -} - -impl RngArgs { - pub fn from_seed(seed: [u8; 32]) -> RngArgs { - RngArgs { - random_seed: Some(hex::encode(seed)), - } - } - - pub fn from_string_seed(str: &str) -> RngArgs { - assert!(str.len() < 32); - - let mut seed = [0u8; 32]; - for (i, byte) in str.bytes().enumerate() { - seed[i] = byte; - } - - RngArgs { - random_seed: Some(hex::encode(seed)), - } - } - - /// Returns a key generator with the seed if given - pub fn key_generator(&self) -> CliTypedResult { - if let Some(ref seed) = self.random_seed { - // Strip 0x - let seed = seed.strip_prefix("0x").unwrap_or(seed); - let mut seed_slice = [0u8; 32]; - - hex::decode_to_slice(seed, &mut seed_slice)?; - Ok(KeyGen::from_seed(seed_slice)) - } else { - Ok(KeyGen::from_os_rng()) - } - } -} - -impl Default for EncodingType { - fn default() -> Self { - EncodingType::Hex - } -} - -impl Display for EncodingType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - EncodingType::BCS => "bcs", - EncodingType::Hex => "hex", - EncodingType::Base64 => "base64", - }; - write!(f, "{}", str) - } -} - -impl FromStr for EncodingType { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "hex" => Ok(EncodingType::Hex), - "bcs" => Ok(EncodingType::BCS), - "base64" => Ok(EncodingType::Base64), - _ => Err("Invalid encoding type"), - } - } -} - -/// An insertable option for use with prompts. -#[derive(Clone, Copy, Debug, Default, Parser, PartialEq, Eq)] -pub struct PromptOptions { - /// Assume yes for all yes/no prompts - #[clap(long, group = "prompt_options")] - pub assume_yes: bool, - /// Assume no for all yes/no prompts - #[clap(long, group = "prompt_options")] - pub assume_no: bool, -} - -impl PromptOptions { - pub fn yes() -> Self { - Self { - assume_yes: true, - assume_no: false, - } - } - - pub fn no() -> Self { - Self { - assume_yes: false, - assume_no: true, - } - } -} - -/// An insertable option for use with encodings. -#[derive(Debug, Default, Parser)] -pub struct EncodingOptions { - /// Encoding of data as one of [base64, bcs, hex] - #[clap(long, default_value_t = EncodingType::Hex)] - pub encoding: EncodingType, -} - -#[derive(Debug, Parser)] -pub struct PublicKeyInputOptions { - /// Ed25519 Public key input file name - /// - /// Mutually exclusive with `--public-key` - #[clap(long, group = "public_key_input", parse(from_os_str))] - public_key_file: Option, - /// Ed25519 Public key encoded in a type as shown in `encoding` - /// - /// Mutually exclusive with `--public-key-file` - #[clap(long, group = "public_key_input")] - public_key: Option, -} - -impl PublicKeyInputOptions { - pub fn from_key(key: &Ed25519PublicKey) -> PublicKeyInputOptions { - PublicKeyInputOptions { - public_key: Some(key.to_encoded_string().unwrap()), - public_key_file: None, - } - } -} - -impl ExtractPublicKey for PublicKeyInputOptions { - fn extract_public_key( - &self, - encoding: EncodingType, - profile: &ProfileOptions, - ) -> CliTypedResult { - if let Some(ref file) = self.public_key_file { - encoding.load_key("--public-key-file", file.as_path()) - } else if let Some(ref key) = self.public_key { - let key = key.as_bytes().to_vec(); - encoding.decode_key("--public-key", key) - } else if let Some(Some(public_key)) = CliConfig::load_profile( - profile.profile_name(), - ConfigSearchMode::CurrentDirAndParents, - )? - .map(|p| p.public_key) - { - Ok(public_key) - } else { - Err(CliError::CommandArgumentError( - "One of ['--public-key', '--public-key-file', '--profile'] must be used" - .to_string(), - )) - } - } -} - -pub trait ParsePrivateKey { - fn parse_private_key( - &self, - encoding: EncodingType, - private_key_file: Option, - private_key: Option, - ) -> CliTypedResult> { - if let Some(ref file) = private_key_file { - Ok(Some( - encoding.load_key("--private-key-file", file.as_path())?, - )) - } else if let Some(ref key) = private_key { - let key = key.as_bytes().to_vec(); - Ok(Some(encoding.decode_key("--private-key", key)?)) - } else { - Ok(None) - } - } -} - -#[derive(Debug, Default, Parser)] -pub struct PrivateKeyInputOptions { - /// Signing Ed25519 private key file path - /// - /// Encoded with type from `--encoding` - /// Mutually exclusive with `--private-key` - #[clap(long, group = "private_key_input", parse(from_os_str))] - private_key_file: Option, - /// Signing Ed25519 private key - /// - /// Encoded with type from `--encoding` - /// Mutually exclusive with `--private-key-file` - #[clap(long, group = "private_key_input")] - private_key: Option, -} - -impl ParsePrivateKey for PrivateKeyInputOptions {} - -impl PrivateKeyInputOptions { - pub fn from_private_key(private_key: &Ed25519PrivateKey) -> CliTypedResult { - Ok(PrivateKeyInputOptions { - private_key: Some( - private_key - .to_encoded_string() - .map_err(|err| CliError::UnexpectedError(err.to_string()))?, - ), - private_key_file: None, - }) - } - - pub fn from_x25519_private_key(private_key: &x25519::PrivateKey) -> CliTypedResult { - Ok(PrivateKeyInputOptions { - private_key: Some( - private_key - .to_encoded_string() - .map_err(|err| CliError::UnexpectedError(err.to_string()))?, - ), - private_key_file: None, - }) - } - - pub fn from_file(file: PathBuf) -> Self { - PrivateKeyInputOptions { - private_key: None, - private_key_file: Some(file), - } - } - - /// Extract private key from CLI args with fallback to config - pub fn extract_private_key_and_address( - &self, - encoding: EncodingType, - profile: &ProfileOptions, - maybe_address: Option, - ) -> CliTypedResult<(Ed25519PrivateKey, AccountAddress)> { - // Order of operations - // 1. CLI inputs - // 2. Profile - // 3. Derived - if let Some(key) = self.extract_private_key_cli(encoding)? { - // If we use the CLI inputs, then we should derive or use the address from the input - if let Some(address) = maybe_address { - Ok((key, address)) - } else { - let address = account_address_from_public_key(&key.public_key()); - Ok((key, address)) - } - } else if let Some((Some(key), maybe_config_address)) = CliConfig::load_profile( - profile.profile_name(), - ConfigSearchMode::CurrentDirAndParents, - )? - .map(|p| (p.private_key, p.account)) - { - match (maybe_address, maybe_config_address) { - (Some(address), _) => Ok((key, address)), - (_, Some(address)) => Ok((key, address)), - (None, None) => { - let address = account_address_from_public_key(&key.public_key()); - Ok((key, address)) - } - } - } else { - Err(CliError::CommandArgumentError( - "One of ['--private-key', '--private-key-file'] must be used".to_string(), - )) - } - } - - /// Extract private key from CLI args with fallback to config - pub fn extract_private_key( - &self, - encoding: EncodingType, - profile: &ProfileOptions, - ) -> CliTypedResult { - if let Some(key) = self.extract_private_key_cli(encoding)? { - Ok(key) - } else if let Some(Some(private_key)) = CliConfig::load_profile( - profile.profile_name(), - ConfigSearchMode::CurrentDirAndParents, - )? - .map(|p| p.private_key) - { - Ok(private_key) - } else { - Err(CliError::CommandArgumentError( - "One of ['--private-key', '--private-key-file'] must be used".to_string(), - )) - } - } - - /// Extract private key from CLI args - pub fn extract_private_key_cli( - &self, - encoding: EncodingType, - ) -> CliTypedResult> { - self.parse_private_key( - encoding, - self.private_key_file.clone(), - self.private_key.clone(), - ) - } -} - -impl ExtractPublicKey for PrivateKeyInputOptions { - fn extract_public_key( - &self, - encoding: EncodingType, - profile: &ProfileOptions, - ) -> CliTypedResult { - self.extract_private_key(encoding, profile) - .map(|private_key| private_key.public_key()) - } -} - -pub trait ExtractPublicKey { - fn extract_public_key( - &self, - encoding: EncodingType, - profile: &ProfileOptions, - ) -> CliTypedResult; -} - -pub fn account_address_from_public_key(public_key: &Ed25519PublicKey) -> AccountAddress { - let auth_key = AuthenticationKey::ed25519(public_key); - AccountAddress::new(*auth_key.derived_address()) -} - -#[derive(Debug, Parser)] -pub struct SaveFile { - /// Output file path - #[clap(long, parse(from_os_str))] - pub output_file: PathBuf, - - #[clap(flatten)] - pub prompt_options: PromptOptions, -} - -impl SaveFile { - /// Check if the key file exists already - pub fn check_file(&self) -> CliTypedResult<()> { - check_if_file_exists(self.output_file.as_path(), self.prompt_options) - } - - /// Save to the `output_file` - pub fn save_to_file(&self, name: &str, bytes: &[u8]) -> CliTypedResult<()> { - write_to_file(self.output_file.as_path(), name, bytes) - } - - /// Save to the `output_file` with restricted permissions (mode 0600) - pub fn save_to_file_confidential(&self, name: &str, bytes: &[u8]) -> CliTypedResult<()> { - let mut opts = OpenOptions::new(); - #[cfg(unix)] - opts.mode(0o600); - write_to_file_with_opts(self.output_file.as_path(), name, bytes, &mut opts) - } -} - -/// Options specific to using the Rest endpoint -#[derive(Debug, Default, Parser)] -pub struct RestOptions { - /// URL to a fullnode on the network - /// - /// Defaults to the URL in the `default` profile - #[clap(long)] - pub(crate) url: Option, - - /// Connection timeout in seconds, used for the REST endpoint of the fullnode - #[clap(long, default_value_t = DEFAULT_EXPIRATION_SECS, alias = "connection-timeout-s")] - pub connection_timeout_secs: u64, -} - -impl RestOptions { - pub fn new(url: Option, connection_timeout_secs: Option) -> Self { - RestOptions { - url, - connection_timeout_secs: connection_timeout_secs.unwrap_or(DEFAULT_EXPIRATION_SECS), - } - } - - /// Retrieve the URL from the profile or the command line - pub fn url(&self, profile: &ProfileOptions) -> CliTypedResult { - if let Some(ref url) = self.url { - Ok(url.clone()) - } else if let Some(Some(url)) = CliConfig::load_profile( - profile.profile_name(), - ConfigSearchMode::CurrentDirAndParents, - )? - .map(|p| p.rest_url) - { - reqwest::Url::parse(&url) - .map_err(|err| CliError::UnableToParse("Rest URL", err.to_string())) - } else { - Err(CliError::CommandArgumentError("No rest url given. Please add --url or add a rest_url to the .movement/config.yaml for the current profile".to_string())) - } - } - - pub fn client(&self, profile: &ProfileOptions) -> CliTypedResult { - Ok(Client::new_with_timeout_and_user_agent( - self.url(profile)?, - Duration::from_secs(self.connection_timeout_secs), - USER_AGENT, - )) - } - pub fn client_raw(&self, url: Url) -> CliTypedResult { - Ok(Client::new_with_timeout_and_user_agent( - url, - Duration::from_secs(self.connection_timeout_secs), - USER_AGENT, - )) - } -} - -/// Options for compiling a move package dir -#[derive(Debug, Clone, Parser)] -pub struct MovePackageDir { - /// Path to a move package (the folder with a Move.toml file) - #[clap(long, parse(from_os_str))] - pub package_dir: Option, - /// Path to save the compiled move package - /// - /// Defaults to `/build` - #[clap(long, parse(from_os_str))] - pub output_dir: Option, - /// Named addresses for the move binary - /// - /// Example: alice=0x1234, bob=0x5678 - /// - /// Note: This will fail if there are duplicates in the Move.toml file remove those first. - #[clap(long, parse(try_from_str = crate::common::utils::parse_map), default_value = "")] - pub(crate) named_addresses: BTreeMap, - - /// Skip pulling the latest git dependencies - /// - /// If you don't have a network connection, the compiler may fail due - /// to no ability to pull git dependencies. This will allow overriding - /// this for local development. - #[clap(long)] - pub(crate) skip_fetch_latest_git_deps: bool, - - /// Specify the version of the bytecode the compiler is going to emit. - #[clap(long)] - pub bytecode_version: Option, -} - -impl MovePackageDir { - pub fn new(package_dir: PathBuf) -> Self { - Self { - package_dir: Some(package_dir), - output_dir: None, - named_addresses: Default::default(), - skip_fetch_latest_git_deps: true, - bytecode_version: None, - } - } - - pub fn get_package_path(&self) -> CliTypedResult { - dir_default_to_current(self.package_dir.clone()) - } - - /// Retrieve the NamedAddresses, resolving all the account addresses accordingly - pub fn named_addresses(&self) -> BTreeMap { - self.named_addresses - .clone() - .into_iter() - .map(|(key, value)| (key, value.account_address)) - .collect() - } - - pub fn add_named_address(&mut self, key: String, value: String) { - self.named_addresses - .insert(key, AccountAddressWrapper::from_str(&value).unwrap()); - } -} - -/// A wrapper around `AccountAddress` to be more flexible from strings than AccountAddress -#[derive(Clone, Copy, Debug)] -pub struct AccountAddressWrapper { - pub account_address: AccountAddress, -} - -impl FromStr for AccountAddressWrapper { - type Err = CliError; - - fn from_str(s: &str) -> Result { - Ok(AccountAddressWrapper { - account_address: load_account_arg(s)?, - }) - } -} - -/// Loads an account arg and allows for naming based on profiles -pub fn load_account_arg(str: &str) -> Result { - if str.starts_with("0x") { - AccountAddress::from_hex_literal(str).map_err(|err| { - CliError::CommandArgumentError(format!("Failed to parse AccountAddress {}", err)) - }) - } else if let Ok(account_address) = AccountAddress::from_str(str) { - Ok(account_address) - } else if let Some(Some(account_address)) = - CliConfig::load_profile(Some(str), ConfigSearchMode::CurrentDirAndParents)? - .map(|p| p.account) - { - Ok(account_address) - } else if let Some(Some(private_key)) = - CliConfig::load_profile(Some(str), ConfigSearchMode::CurrentDirAndParents)? - .map(|p| p.private_key) - { - let public_key = private_key.public_key(); - Ok(account_address_from_public_key(&public_key)) - } else { - Err(CliError::CommandArgumentError( - "'--account' or '--profile' after using movement init must be provided".to_string(), - )) - } -} - -/// A wrapper around `AccountAddress` to allow for "_" -#[derive(Clone, Copy, Debug)] -pub struct MoveManifestAccountWrapper { - pub account_address: Option, -} - -impl FromStr for MoveManifestAccountWrapper { - type Err = CliError; - - fn from_str(s: &str) -> Result { - Ok(MoveManifestAccountWrapper { - account_address: load_manifest_account_arg(s)?, - }) - } -} - -/// Loads an account arg and allows for naming based on profiles and "_" -pub fn load_manifest_account_arg(str: &str) -> Result, CliError> { - if str == "_" { - Ok(None) - } else if str.starts_with("0x") { - AccountAddress::from_hex_literal(str) - .map(Some) - .map_err(|err| { - CliError::CommandArgumentError(format!("Failed to parse AccountAddress {}", err)) - }) - } else if let Ok(account_address) = AccountAddress::from_str(str) { - Ok(Some(account_address)) - } else if let Some(Some(private_key)) = - CliConfig::load_profile(Some(str), ConfigSearchMode::CurrentDirAndParents)? - .map(|p| p.private_key) - { - let public_key = private_key.public_key(); - Ok(Some(account_address_from_public_key(&public_key))) - } else { - Err(CliError::CommandArgumentError( - "Invalid Move manifest account address".to_string(), - )) - } -} - -/// A common trait for all CLI commands to have consistent outputs -#[async_trait] -pub trait CliCommand: Sized + Send { - /// Returns a name for logging purposes - fn command_name(&self) -> &'static str; - - /// Executes the command, returning a command specific type - async fn execute(self) -> CliTypedResult; - - /// Executes the command, and serializes it to the common JSON output type - async fn execute_serialized(self) -> CliResult { - let command_name = self.command_name(); - start_logger(); - let start_time = Instant::now(); - to_common_result(command_name, start_time, self.execute().await).await - } - - /// Same as execute serialized without setting up logging - async fn execute_serialized_without_logger(self) -> CliResult { - let command_name = self.command_name(); - let start_time = Instant::now(); - to_common_result(command_name, start_time, self.execute().await).await - } - - /// Executes the command, and throws away Ok(result) for the string Success - async fn execute_serialized_success(self) -> CliResult { - start_logger(); - let command_name = self.command_name(); - let start_time = Instant::now(); - to_common_success_result(command_name, start_time, self.execute().await).await - } -} - -/// A shortened transaction output -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct TransactionSummary { - pub transaction_hash: HashValue, - #[serde(skip_serializing_if = "Option::is_none")] - pub gas_used: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub gas_unit_price: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub pending: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sender: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sequence_number: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub success: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub timestamp_us: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub vm_status: Option, -} - -impl From for TransactionSummary { - fn from(transaction: Transaction) -> Self { - TransactionSummary::from(&transaction) - } -} - -impl From<&Transaction> for TransactionSummary { - fn from(transaction: &Transaction) -> Self { - match transaction { - Transaction::PendingTransaction(txn) => TransactionSummary { - transaction_hash: txn.hash, - pending: Some(true), - sender: Some(*txn.request.sender.inner()), - sequence_number: Some(txn.request.sequence_number.0), - gas_used: None, - gas_unit_price: None, - success: None, - version: None, - vm_status: None, - timestamp_us: None, - }, - Transaction::UserTransaction(txn) => TransactionSummary { - transaction_hash: txn.info.hash, - sender: Some(*txn.request.sender.inner()), - gas_used: Some(txn.info.gas_used.0), - gas_unit_price: Some(txn.request.gas_unit_price.0), - success: Some(txn.info.success), - version: Some(txn.info.version.0), - vm_status: Some(txn.info.vm_status.clone()), - sequence_number: Some(txn.request.sequence_number.0), - timestamp_us: Some(txn.timestamp.0), - pending: None, - }, - Transaction::GenesisTransaction(txn) => TransactionSummary { - transaction_hash: txn.info.hash, - success: Some(txn.info.success), - version: Some(txn.info.version.0), - vm_status: Some(txn.info.vm_status.clone()), - sender: None, - gas_used: None, - gas_unit_price: None, - pending: None, - sequence_number: None, - timestamp_us: None, - }, - Transaction::BlockMetadataTransaction(txn) => TransactionSummary { - transaction_hash: txn.info.hash, - success: Some(txn.info.success), - version: Some(txn.info.version.0), - vm_status: Some(txn.info.vm_status.clone()), - timestamp_us: Some(txn.timestamp.0), - sender: None, - gas_used: None, - gas_unit_price: None, - pending: None, - sequence_number: None, - }, - Transaction::StateCheckpointTransaction(txn) => TransactionSummary { - transaction_hash: txn.info.hash, - success: Some(txn.info.success), - version: Some(txn.info.version.0), - vm_status: Some(txn.info.vm_status.clone()), - timestamp_us: Some(txn.timestamp.0), - sender: None, - gas_used: None, - gas_unit_price: None, - pending: None, - sequence_number: None, - }, - } - } -} - -/// A summary of a `WriteSetChange` for easy printing -#[derive(Clone, Debug, Default, Serialize)] -pub struct ChangeSummary { - #[serde(skip_serializing_if = "Option::is_none")] - address: Option, - #[serde(skip_serializing_if = "Option::is_none")] - data: Option, - event: &'static str, - #[serde(skip_serializing_if = "Option::is_none")] - handle: Option, - #[serde(skip_serializing_if = "Option::is_none")] - key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - module: Option, - #[serde(skip_serializing_if = "Option::is_none")] - resource: Option, - #[serde(skip_serializing_if = "Option::is_none")] - value: Option, -} - -#[derive(Debug, Default, Parser)] -pub struct FaucetOptions { - /// URL for the faucet endpoint e.g. `https://faucet.devnet.aptoslabs.com` - #[clap(long)] - faucet_url: Option, -} - -impl FaucetOptions { - pub fn new(faucet_url: Option) -> Self { - FaucetOptions { faucet_url } - } - - pub fn faucet_url(&self, profile: &ProfileOptions) -> CliTypedResult { - if let Some(ref faucet_url) = self.faucet_url { - Ok(faucet_url.clone()) - } else if let Some(Some(url)) = CliConfig::load_profile( - profile.profile_name(), - ConfigSearchMode::CurrentDirAndParents, - )? - .map(|profile| profile.faucet_url) - { - reqwest::Url::parse(&url) - .map_err(|err| CliError::UnableToParse("config faucet_url", err.to_string())) - } else { - Err(CliError::CommandArgumentError("No faucet given. Please add --faucet-url or add a faucet URL to the .movement/config.yaml for the current profile".to_string())) - } - } -} - -/// Gas price options for manipulating how to prioritize transactions -#[derive(Debug, Eq, Parser, PartialEq)] -pub struct GasOptions { - /// Gas multiplier per unit of gas - /// - /// The amount of Octas (10^-8 APT) used for a transaction is equal - /// to (gas unit price * gas used). The gas_unit_price can - /// be used as a multiplier for the amount of Octas willing - /// to be paid for a transaction. This will prioritize the - /// transaction with a higher gas unit price. - /// - /// Without a value, it will determine the price based on the current estimated price - #[clap(long)] - pub gas_unit_price: Option, - /// Maximum amount of gas units to be used to send this transaction - /// - /// The maximum amount of gas units willing to pay for the transaction. - /// This is the (max gas in Octas / gas unit price). - /// - /// For example if I wanted to pay a maximum of 100 Octas, I may have the - /// max gas set to 100 if the gas unit price is 1. If I want it to have a - /// gas unit price of 2, the max gas would need to be 50 to still only have - /// a maximum price of 100 Octas. - /// - /// Without a value, it will determine the price based on simulating the current transaction - #[clap(long)] - pub max_gas: Option, - /// Number of seconds to expire the transaction - /// - /// This is the number of seconds from the current local computer time. - #[clap(long, default_value_t = DEFAULT_EXPIRATION_SECS)] - pub expiration_secs: u64, -} - -impl Default for GasOptions { - fn default() -> Self { - GasOptions { - gas_unit_price: None, - max_gas: None, - expiration_secs: DEFAULT_EXPIRATION_SECS, - } - } -} - -/// Common options for interacting with an account for a validator -#[derive(Debug, Default, Parser)] -pub struct TransactionOptions { - /// Sender account address - /// - /// This allows you to override the account address from the derived account address - /// in the event that the authentication key was rotated or for a resource account - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) sender_account: Option, - - #[clap(flatten)] - pub(crate) private_key_options: PrivateKeyInputOptions, - #[clap(flatten)] - pub(crate) encoding_options: EncodingOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) gas_options: GasOptions, - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, - - /// If this option is set, simulate the transaction locally using the debugger and generate - /// flamegraphs that reflect the gas usage. - #[clap(long)] - pub(crate) profile_gas: bool, -} - -impl TransactionOptions { - /// Builds a rest client - fn rest_client(&self) -> CliTypedResult { - self.rest_options.client(&self.profile_options) - } - - /// Retrieves the public key and the associated address - /// TODO: Cache this information - pub fn get_key_and_address(&self) -> CliTypedResult<(Ed25519PrivateKey, AccountAddress)> { - self.private_key_options.extract_private_key_and_address( - self.encoding_options.encoding, - &self.profile_options, - self.sender_account, - ) - } - - pub fn sender_address(&self) -> CliTypedResult { - Ok(self.get_key_and_address()?.1) - } - - /// Gets the auth key by account address. We need to fetch the auth key from Rest API rather than creating an - /// auth key out of the public key. - pub(crate) async fn auth_key( - &self, - sender_address: AccountAddress, - ) -> CliTypedResult { - let client = self.rest_client()?; - get_auth_key(&client, sender_address).await - } - - pub async fn sequence_number(&self, sender_address: AccountAddress) -> CliTypedResult { - let client = self.rest_client()?; - get_sequence_number(&client, sender_address).await - } - - pub async fn view(&self, payload: ViewRequest) -> CliTypedResult> { - let client = self.rest_client()?; - Ok(client.view(&payload, None).await?.into_inner()) - } - - /// Submit a transaction - pub async fn submit_transaction( - &self, - payload: TransactionPayload, - ) -> CliTypedResult { - let client = self.rest_client()?; - let (sender_key, sender_address) = self.get_key_and_address()?; - - // Ask to confirm price if the gas unit price is estimated above the lowest value when - // it is automatically estimated - let ask_to_confirm_price; - let gas_unit_price = if let Some(gas_unit_price) = self.gas_options.gas_unit_price { - ask_to_confirm_price = false; - gas_unit_price - } else { - let gas_unit_price = client.estimate_gas_price().await?.into_inner().gas_estimate; - - ask_to_confirm_price = true; - gas_unit_price - }; - - // Get sequence number for account - let (account, state) = get_account_with_state(&client, sender_address).await?; - let sequence_number = account.sequence_number; - - // Retrieve local time, and ensure it's within an expected skew of the blockchain - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|err| CliError::UnexpectedError(err.to_string()))? - .as_secs(); - let now_usecs = now * US_IN_SECS; - - // Warn local user that clock is skewed behind the blockchain. - // There will always be a little lag from real time to blockchain time - if now_usecs < state.timestamp_usecs - ACCEPTED_CLOCK_SKEW_US { - eprintln!("Local clock is is skewed from blockchain clock. Clock is more than {} seconds behind the blockchain {}", ACCEPTED_CLOCK_SKEW_US, state.timestamp_usecs / US_IN_SECS); - } - let expiration_time_secs = now + self.gas_options.expiration_secs; - - let chain_id = ChainId::new(state.chain_id); - // TODO: Check auth key against current private key and provide a better message - - let max_gas = if let Some(max_gas) = self.gas_options.max_gas { - // If the gas unit price was estimated ask, but otherwise you've chosen hwo much you want to spend - if ask_to_confirm_price { - let message = format!("Do you want to submit transaction for a maximum of {} Octas at a gas unit price of {} Octas?", max_gas * gas_unit_price, gas_unit_price); - prompt_yes_with_override(&message, self.prompt_options)?; - } - max_gas - } else { - let transaction_factory = - TransactionFactory::new(chain_id).with_gas_unit_price(gas_unit_price); - - let unsigned_transaction = transaction_factory - .payload(payload.clone()) - .sender(sender_address) - .sequence_number(sequence_number) - .expiration_timestamp_secs(expiration_time_secs) - .build(); - - let signed_transaction = SignedTransaction::new( - unsigned_transaction, - sender_key.public_key(), - Ed25519Signature::try_from([0u8; 64].as_ref()).unwrap(), - ); - - let txns = client - .simulate_with_gas_estimation(&signed_transaction, true, false) - .await? - .into_inner(); - let simulated_txn = txns.first().unwrap(); - - // Check if the transaction will pass, if it doesn't then fail - if !simulated_txn.info.success { - return Err(CliError::SimulationError( - simulated_txn.info.vm_status.clone(), - )); - } - - // Take the gas used and use a headroom factor on it - let gas_used = simulated_txn.info.gas_used.0; - let adjusted_max_gas = - adjust_gas_headroom(gas_used, simulated_txn.request.max_gas_amount.0); - - // Ask if you want to accept the estimate amount - let upper_cost_bound = adjusted_max_gas * gas_unit_price; - let lower_cost_bound = gas_used * gas_unit_price; - let message = format!( - "Do you want to submit a transaction for a range of [{} - {}] Octas at a gas unit price of {} Octas?", - lower_cost_bound, - upper_cost_bound, - gas_unit_price); - prompt_yes_with_override(&message, self.prompt_options)?; - adjusted_max_gas - }; - - // Sign and submit transaction - let transaction_factory = TransactionFactory::new(chain_id) - .with_gas_unit_price(gas_unit_price) - .with_max_gas_amount(max_gas) - .with_transaction_expiration_time(self.gas_options.expiration_secs); - let sender_account = &mut LocalAccount::new(sender_address, sender_key, sequence_number); - let transaction = - sender_account.sign_with_transaction_builder(transaction_factory.payload(payload)); - let response = client - .submit_and_wait(&transaction) - .await - .map_err(|err| CliError::ApiError(err.to_string()))?; - - Ok(response.into_inner()) - } - - /// Simulate the transaction locally using the debugger, with the gas profiler enabled. - pub async fn profile_gas( - &self, - payload: TransactionPayload, - ) -> CliTypedResult { - println!(); - println!("Simulating transaction locally with the gas profiler..."); - println!("This is still experimental so results may be inaccurate."); - - let client = self.rest_client()?; - - // Fetch the chain states required for the simulation - // TODO(Gas): get the following from the chain - const DEFAULT_GAS_UNIT_PRICE: u64 = 100; - const DEFAULT_MAX_GAS: u64 = 2_000_000; - - let (sender_key, sender_address) = self.get_key_and_address()?; - let gas_unit_price = self - .gas_options - .gas_unit_price - .unwrap_or(DEFAULT_GAS_UNIT_PRICE); - let (account, state) = get_account_with_state(&client, sender_address).await?; - let version = state.version; - let chain_id = ChainId::new(state.chain_id); - let sequence_number = account.sequence_number; - - let balance = client - .get_account_balance_at_version(sender_address, version) - .await - .map_err(|err| CliError::ApiError(err.to_string()))? - .into_inner(); - - let max_gas = self.gas_options.max_gas.unwrap_or_else(|| { - if gas_unit_price == 0 { - DEFAULT_MAX_GAS - } else { - std::cmp::min(balance.coin.value.0 / gas_unit_price, DEFAULT_MAX_GAS) - } - }); - - // Create and sign the transaction - let transaction_factory = TransactionFactory::new(chain_id) - .with_gas_unit_price(gas_unit_price) - .with_max_gas_amount(max_gas) - .with_transaction_expiration_time(self.gas_options.expiration_secs); - let sender_account = &mut LocalAccount::new(sender_address, sender_key, sequence_number); - let transaction = - sender_account.sign_with_transaction_builder(transaction_factory.payload(payload)); - let hash = transaction.clone().committed_hash(); - - // Execute the transaction using the debugger - let debugger = AptosDebugger::rest_client(client).unwrap(); - let res = debugger.execute_transaction_at_version_with_gas_profiler(version, transaction); - let (vm_status, output, gas_log) = res.map_err(|err| { - CliError::UnexpectedError(format!("failed to simulate txn with gas profiler: {}", err)) - })?; - - // Generate the file name for the flamegraphs - let entry_point = gas_log.entry_point(); - - let human_readable_name = match entry_point { - FrameName::Script => "script".to_string(), - FrameName::Function { - module_id, name, .. - } => { - let addr_short = module_id.address().short_str_lossless(); - let addr_truncated = if addr_short.len() > 4 { - &addr_short[..4] - } else { - addr_short.as_str() - }; - format!("0x{}-{}-{}", addr_truncated, module_id.name(), name) - } - }; - let raw_file_name = format!("txn-{}-{}", hash, human_readable_name); - - // Create the directory if it does not exist yet. - let dir: &Path = Path::new("gas-profiling"); - - macro_rules! create_dir { - () => { - if let Err(err) = std::fs::create_dir(dir) { - if err.kind() != std::io::ErrorKind::AlreadyExists { - return Err(CliError::UnexpectedError(format!( - "failed to create directory {}", - dir.display() - ))); - } - } - }; - } - - // Generate the execution & IO flamegraph. - println!(); - match gas_log.to_flamegraph(format!("Transaction {} -- Execution & IO", hash))? { - Some(graph_bytes) => { - create_dir!(); - let graph_file_path = Path::join(dir, format!("{}.exec_io.svg", raw_file_name)); - std::fs::write(&graph_file_path, graph_bytes).map_err(|err| { - CliError::UnexpectedError(format!( - "Failed to write flamegraph to file {} : {:?}", - graph_file_path.display(), - err - )) - })?; - println!( - "Execution & IO Gas flamegraph saved to {}", - graph_file_path.display() - ); - } - None => { - println!("Skipped generating execution & IO flamegraph"); - } - } - - // Generate the storage fee flamegraph. - match gas_log - .storage - .to_flamegraph(format!("Transaction {} -- Storage Fee", hash))? - { - Some(graph_bytes) => { - create_dir!(); - let graph_file_path = Path::join(dir, format!("{}.storage.svg", raw_file_name)); - std::fs::write(&graph_file_path, graph_bytes).map_err(|err| { - CliError::UnexpectedError(format!( - "Failed to write flamegraph to file {} : {:?}", - graph_file_path.display(), - err - )) - })?; - println!( - "Storage fee flamegraph saved to {}", - graph_file_path.display() - ); - } - None => { - println!("Skipped generating storage fee flamegraph"); - } - } - - println!(); - - // Generate the transaction summary - - // TODO(Gas): double check if this is correct. - let success = match output.status() { - TransactionStatus::Keep(exec_status) => Some(exec_status.is_success()), - TransactionStatus::Discard(_) | TransactionStatus::Retry => None, - }; - - Ok(TransactionSummary { - transaction_hash: hash.into(), - gas_used: Some(output.gas_used()), - gas_unit_price: Some(gas_unit_price), - pending: None, - sender: Some(sender_address), - sequence_number: None, // The transaction is not comitted so there is no new sequence number. - success, - timestamp_us: None, - version: Some(version), // The transaction is not comitted so there is no new version. - vm_status: Some(vm_status.to_string()), - }) - } - - pub async fn estimate_gas_price(&self) -> CliTypedResult { - let client = self.rest_client()?; - client - .estimate_gas_price() - .await - .map(|inner| inner.into_inner().gas_estimate) - .map_err(|err| { - CliError::UnexpectedError(format!( - "Failed to retrieve gas price estimate {:?}", - err - )) - }) - } -} - -#[derive(Parser)] -pub struct OptionalPoolAddressArgs { - /// Address of the Staking pool - /// - /// Defaults to the profile's `AccountAddress` - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) pool_address: Option, -} - -#[derive(Parser)] -pub struct PoolAddressArgs { - /// Address of the Staking pool - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) pool_address: AccountAddress, -} - -// This struct includes TypeInfo (account_address, module_name, and struct_name) -// and RotationProofChallenge-specific information (sequence_number, originator, current_auth_key, and new_public_key) -// Since the struct RotationProofChallenge is defined in "0x1::account::RotationProofChallenge", -// we will be passing in "0x1" to `account_address`, "account" to `module_name`, and "RotationProofChallenge" to `struct_name` -// Originator refers to the user's address -#[derive(Serialize, Deserialize)] -pub struct RotationProofChallenge { - // Should be `CORE_CODE_ADDRESS` - pub account_address: AccountAddress, - // Should be `account` - pub module_name: String, - // Should be `RotationProofChallenge` - pub struct_name: String, - pub sequence_number: u64, - pub originator: AccountAddress, - pub current_auth_key: AccountAddress, - pub new_public_key: Vec, -} - -#[derive(Debug, Parser)] -/// This is used for both entry functions and scripts. -pub struct ArgWithTypeVec { - /// Arguments combined with their type separated by spaces. - /// - /// Supported types [address, bool, hex, string, u8, u16, u32, u64, u128, u256, raw] - /// - /// Vectors may be specified using JSON array literal syntax (you may need to escape this with - /// quotes based on your shell interpreter) - /// - /// Example: `address:0x1 bool:true u8:0 u256:1234 "bool:[true, false]" 'address:[["0xace", "0xbee"], []]'` - /// - /// Vector is wrapped in a reusable struct for uniform CLI documentation. - #[clap(long, multiple_values = true)] - pub(crate) args: Vec, -} - -/// Common options for constructing an entry function transaction payload. -#[derive(Debug, Parser)] -pub struct EntryFunctionArguments { - /// Function name as `
::::` - /// - /// Example: `0x842ed41fad9640a2ad08fdd7d3e4f7f505319aac7d67e1c0dd6a7cce8732c7e3::message::set_message` - #[clap(long)] - pub function_id: MemberId, - - #[clap(flatten)] - pub(crate) arg_vec: ArgWithTypeVec, - - /// TypeTag arguments separated by spaces. - /// - /// Example: `u8 u16 u32 u64 u128 u256 bool address vector signer` - #[clap(long, multiple_values = true)] - pub type_args: Vec, -} - -impl EntryFunctionArguments { - /// Construct and return an entry function payload from function_id, args, and type_args. - pub fn create_entry_function_payload(self) -> CliTypedResult { - let args: Vec> = self - .arg_vec - .args - .into_iter() - .map(|arg_with_type| arg_with_type.arg) - .collect(); - - let mut parsed_type_args: Vec = Vec::new(); - // These TypeArgs are used for generics - for type_arg in self.type_args.into_iter() { - let type_tag = TypeTag::try_from(type_arg.clone()) - .map_err(|err| CliError::UnableToParse("--type-args", err.to_string()))?; - parsed_type_args.push(type_tag) - } - - Ok(EntryFunction::new( - self.function_id.module_id, - self.function_id.member_id, - parsed_type_args, - args, - )) - } -} - -/// Common options for interactions with a multisig account. -#[derive(Clone, Debug, Parser, Serialize)] -pub struct MultisigAccount { - /// The address of the multisig account to interact with. - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) multisig_address: AccountAddress, -} diff --git a/m1/m1-cli/src/common/utils.rs b/m1/m1-cli/src/common/utils.rs deleted file mode 100644 index 126afde0..00000000 --- a/m1/m1-cli/src/common/utils.rs +++ /dev/null @@ -1,507 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::types::{ - account_address_from_public_key, CliError, CliTypedResult, PromptOptions, - TransactionOptions, TransactionSummary, - }, - config::GlobalConfig, - CliResult, -}; -use aptos_build_info::build_information; -use aptos_crypto::ed25519::{Ed25519PrivateKey, Ed25519PublicKey}; -use aptos_keygen::KeyGen; -use aptos_logger::{debug, Level}; -use aptos_rest_client::{aptos_api_types::HashValue, Account, Client, State}; -use aptos_telemetry::service::telemetry_is_disabled; -use aptos_types::{ - account_address::create_multisig_account_address, - chain_id::ChainId, - transaction::{authenticator::AuthenticationKey, TransactionPayload}, -}; -use itertools::Itertools; -use move_core_types::account_address::AccountAddress; -use reqwest::Url; -use serde::Serialize; -#[cfg(unix)] -use std::os::unix::fs::OpenOptionsExt; -use std::{ - collections::BTreeMap, - env, - fs::OpenOptions, - io::Write, - path::{Path, PathBuf}, - str::FromStr, - time::{Duration, Instant, SystemTime}, -}; - -/// Prompts for confirmation until a yes or no is given explicitly -pub fn prompt_yes(prompt: &str) -> bool { - let mut result: Result = Err(()); - - // Read input until a yes or a no is given - while result.is_err() { - println!("{} [yes/no] >", prompt); - let mut input = String::new(); - if std::io::stdin().read_line(&mut input).is_err() { - continue; - } - result = match input.trim().to_lowercase().as_str() { - "yes" | "y" => Ok(true), - "no" | "n" => Ok(false), - _ => Err(()), - }; - } - result.unwrap() -} - -/// Convert any successful response to Success -pub async fn to_common_success_result( - command: &str, - start_time: Instant, - result: CliTypedResult, -) -> CliResult { - to_common_result(command, start_time, result.map(|_| "Success")).await -} - -/// For pretty printing outputs in JSON -pub async fn to_common_result( - command: &str, - start_time: Instant, - result: CliTypedResult, -) -> CliResult { - let latency = start_time.elapsed(); - let is_err = result.is_err(); - - if !telemetry_is_disabled() { - let error = if let Err(ref error) = result { - // Only print the error type - Some(error.to_str()) - } else { - None - }; - send_telemetry_event(command, latency, !is_err, error).await; - } - - let result: ResultWrapper = result.into(); - let string = serde_json::to_string_pretty(&result).unwrap(); - if is_err { - Err(string) - } else { - Ok(string) - } -} - -pub fn cli_build_information() -> BTreeMap { - build_information!() -} - -/// Sends a telemetry event about the CLI build, command and result -async fn send_telemetry_event( - command: &str, - latency: Duration, - success: bool, - error: Option<&str>, -) { - // Collect the build information - let build_information = cli_build_information(); - - // Send the event - aptos_telemetry::cli_metrics::send_cli_telemetry_event( - build_information, - command.into(), - latency, - success, - error, - ) - .await; -} - -/// A result wrapper for displaying either a correct execution result or an error. -/// -/// The purpose of this is to have a pretty easy to recognize JSON output format e.g. -/// -/// { -/// "Result":{ -/// "encoded":{ ... } -/// } -/// } -/// -/// { -/// "Error":"Failed to run command" -/// } -/// -#[derive(Debug, Serialize)] -enum ResultWrapper { - Result(T), - Error(String), -} - -impl From> for ResultWrapper { - fn from(result: CliTypedResult) -> Self { - match result { - Ok(inner) => ResultWrapper::Result(inner), - Err(inner) => ResultWrapper::Error(inner.to_string()), - } - } -} - -/// Checks if a file exists, being overridden by `PromptOptions` -pub fn check_if_file_exists(file: &Path, prompt_options: PromptOptions) -> CliTypedResult<()> { - if file.exists() { - prompt_yes_with_override( - &format!( - "{:?} already exists, are you sure you want to overwrite it?", - file.as_os_str(), - ), - prompt_options, - )? - } - - Ok(()) -} - -pub fn prompt_yes_with_override(prompt: &str, prompt_options: PromptOptions) -> CliTypedResult<()> { - if prompt_options.assume_no { - return Err(CliError::AbortedError); - } else if prompt_options.assume_yes { - return Ok(()); - } - - let is_yes = if let Some(response) = GlobalConfig::load()?.get_default_prompt_response() { - response - } else { - prompt_yes(prompt) - }; - - if is_yes { - Ok(()) - } else { - Err(CliError::AbortedError) - } -} - -pub fn read_from_file(path: &Path) -> CliTypedResult> { - std::fs::read(path) - .map_err(|e| CliError::UnableToReadFile(format!("{}", path.display()), e.to_string())) -} - -/// Write a `&[u8]` to a file -pub fn write_to_file(path: &Path, name: &str, bytes: &[u8]) -> CliTypedResult<()> { - write_to_file_with_opts(path, name, bytes, &mut OpenOptions::new()) -} - -/// Write a User only read / write file -pub fn write_to_user_only_file(path: &Path, name: &str, bytes: &[u8]) -> CliTypedResult<()> { - let mut opts = OpenOptions::new(); - #[cfg(unix)] - opts.mode(0o600); - write_to_file_with_opts(path, name, bytes, &mut opts) -} - -/// Write a `&[u8]` to a file with the given options -pub fn write_to_file_with_opts( - path: &Path, - name: &str, - bytes: &[u8], - opts: &mut OpenOptions, -) -> CliTypedResult<()> { - let mut file = opts - .write(true) - .create(true) - .truncate(true) - .open(path) - .map_err(|e| CliError::IO(name.to_string(), e))?; - file.write_all(bytes) - .map_err(|e| CliError::IO(name.to_string(), e)) -} - -/// Appends a file extension to a `Path` without overwriting the original extension. -pub fn append_file_extension( - file: &Path, - appended_extension: &'static str, -) -> CliTypedResult { - let extension = file - .extension() - .map(|extension| extension.to_str().unwrap_or_default()); - if let Some(extension) = extension { - Ok(file.with_extension(extension.to_owned() + "." + appended_extension)) - } else { - Ok(file.with_extension(appended_extension)) - } -} - -/// Retrieves account resource from the rest client -pub async fn get_account( - client: &aptos_rest_client::Client, - address: AccountAddress, -) -> CliTypedResult { - let account_response = client - .get_account(address) - .await - .map_err(|err| CliError::ApiError(err.to_string()))?; - Ok(account_response.into_inner()) -} - -/// Retrieves account resource from the rest client -pub async fn get_account_with_state( - client: &aptos_rest_client::Client, - address: AccountAddress, -) -> CliTypedResult<(Account, State)> { - let account_response = client - .get_account(address) - .await - .map_err(|err| CliError::ApiError(err.to_string()))?; - Ok(account_response.into_parts()) -} - -/// Retrieves sequence number from the rest client -pub async fn get_sequence_number( - client: &aptos_rest_client::Client, - address: AccountAddress, -) -> CliTypedResult { - Ok(get_account(client, address).await?.sequence_number) -} - -/// Retrieves the auth key from the rest client -pub async fn get_auth_key( - client: &aptos_rest_client::Client, - address: AccountAddress, -) -> CliTypedResult { - Ok(get_account(client, address).await?.authentication_key) -} - -/// Retrieves the chain id from the rest client -pub async fn chain_id(rest_client: &Client) -> CliTypedResult { - let state = rest_client - .get_ledger_information() - .await - .map_err(|err| CliError::ApiError(err.to_string()))? - .into_inner(); - Ok(ChainId::new(state.chain_id)) -} - -/// Error message for parsing a map -const PARSE_MAP_SYNTAX_MSG: &str = "Invalid syntax for map. Example: Name=Value,Name2=Value"; - -/// Parses an inline map of values -/// -/// Example: Name=Value,Name2=Value -pub fn parse_map(str: &str) -> anyhow::Result> -where - K::Err: 'static + std::error::Error + Send + Sync, - V::Err: 'static + std::error::Error + Send + Sync, -{ - let mut map = BTreeMap::new(); - - // Split pairs by commas - for pair in str.split_terminator(',') { - // Split pairs by = then trim off any spacing - let (first, second): (&str, &str) = pair - .split_terminator('=') - .collect_tuple() - .ok_or_else(|| anyhow::Error::msg(PARSE_MAP_SYNTAX_MSG))?; - let first = first.trim(); - let second = second.trim(); - if first.is_empty() || second.is_empty() { - return Err(anyhow::Error::msg(PARSE_MAP_SYNTAX_MSG)); - } - - // At this point, we just give error messages appropriate to parsing - let key: K = K::from_str(first)?; - let value: V = V::from_str(second)?; - map.insert(key, value); - } - Ok(map) -} - -/// Generate a vanity account for Ed25519 single signer scheme, either standard or multisig. -/// -/// The default authentication key for an Ed25519 account is the same as the account address. Hence -/// for a standard account, this function generates Ed25519 private keys until finding one that has -/// an authentication key (account address) that begins with the given vanity prefix. -/// -/// For a multisig account, this function generates private keys until finding one that can create -/// a multisig account with the given vanity prefix as its first transaction (sequence number 0). -/// -/// Note that while a valid hex string must have an even number of characters, a vanity prefix can -/// have an odd number of characters since account addresses are human-readable. -/// -/// `vanity_prefix_ref` is a reference to a hex string vanity prefix, optionally prefixed with "0x". -/// For example "0xaceface" or "d00d". -pub fn generate_vanity_account_ed25519( - vanity_prefix_ref: &str, - multisig: bool, -) -> CliTypedResult { - let vanity_prefix_ref = vanity_prefix_ref - .strip_prefix("0x") - .unwrap_or(vanity_prefix_ref); // Optionally strip leading 0x from input string. - let mut to_check_if_is_hex = String::from(vanity_prefix_ref); - // If an odd number of characters append a 0 for verifying that prefix contains valid hex. - if to_check_if_is_hex.len() % 2 != 0 { - to_check_if_is_hex += "0" - }; - hex::decode(to_check_if_is_hex). // Check that the vanity prefix can be decoded into hex. - map_err(|error| CliError::CommandArgumentError(format!( - "The vanity prefix could not be decoded to hex: {}", error)))?; - let mut key_generator = KeyGen::from_os_rng(); // Get random key generator. - loop { - // Generate new keys until finding a match against the vanity prefix. - let private_key = key_generator.generate_ed25519_private_key(); - let mut account_address = - account_address_from_public_key(&Ed25519PublicKey::from(&private_key)); - if multisig { - account_address = create_multisig_account_address(account_address, 0) - }; - if account_address - .short_str_lossless() - .starts_with(vanity_prefix_ref) - { - return Ok(private_key); - }; - } -} - -pub fn current_dir() -> CliTypedResult { - env::current_dir().map_err(|err| { - CliError::UnexpectedError(format!("Failed to get current directory {}", err)) - }) -} - -pub fn dir_default_to_current(maybe_dir: Option) -> CliTypedResult { - if let Some(dir) = maybe_dir { - Ok(dir) - } else { - current_dir() - } -} - -pub fn create_dir_if_not_exist(dir: &Path) -> CliTypedResult<()> { - // Check if the directory exists, if it's not a dir, it will also fail here - if !dir.exists() || !dir.is_dir() { - std::fs::create_dir_all(dir).map_err(|e| CliError::IO(dir.display().to_string(), e))?; - debug!("Created {} folder", dir.display()); - } else { - debug!("{} folder already exists", dir.display()); - } - Ok(()) -} - -/// Reads a line from input -pub fn read_line(input_name: &'static str) -> CliTypedResult { - let mut input_buf = String::new(); - let _ = std::io::stdin() - .read_line(&mut input_buf) - .map_err(|err| CliError::IO(input_name.to_string(), err))?; - - Ok(input_buf) -} - -/// Fund account (and possibly create it) from a faucet -pub async fn fund_account( - faucet_url: Url, - num_octas: u64, - address: AccountAddress, -) -> CliTypedResult> { - let response = reqwest::Client::new() - .post(format!( - "{}v1/mint?amount={}&auth_key={}", - faucet_url, num_octas, address - )) - .body("{}") - .send() - .await - .map_err(|err| { - CliError::ApiError(format!("Failed to fund account with faucet: {:#}", err)) - })?; - if response.status() == 200 { - let hashes: Vec = response - .json() - .await - .map_err(|err| CliError::UnexpectedError(err.to_string()))?; - Ok(hashes) - } else { - Err(CliError::ApiError(format!( - "Faucet issue: {}", - response.status() - ))) - } -} - -/// Fund by public key (and possibly create it) from a faucet -pub async fn fund_pub_key( - faucet_url: Url, - pub_key: String, -) -> CliTypedResult> { - let url = format!( - "{}v1/mint?&pub_key={}", - faucet_url, pub_key - ); - let response = reqwest::Client::new() - .post(url) - .body("{}") - .send() - .await - .map_err(|err| { - CliError::ApiError(format!("Failed to fund account with faucet: {:#}", err)) - })?; - if response.status() == 200 { - let hashes: Vec = response - .json() - .await - .map_err(|err| CliError::UnexpectedError(err.to_string()))?; - Ok(hashes) - } else { - Err(CliError::ApiError(format!( - "Faucet issue: {}", - response.status() - ))) - } -} - -/// Wait for transactions, returning an error if any of them fail. -pub async fn wait_for_transactions( - client: &aptos_rest_client::Client, - hashes: Vec, -) -> CliTypedResult<()> { - let sys_time = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .map_err(|e| CliError::UnexpectedError(e.to_string()))? - .as_secs() - + 30; - for hash in hashes { - client - .wait_for_transaction_by_hash( - hash.into(), - sys_time, - Some(Duration::from_secs(60)), - None, - ) - .await?; - } - Ok(()) -} - -pub fn start_logger() { - let mut logger = aptos_logger::Logger::new(); - logger.channel_size(1000).is_async(false).level(Level::Warn); - logger.build(); -} - -/// For transaction payload and options, either get gas profile or submit for execution. -pub async fn profile_or_submit( - payload: TransactionPayload, - txn_options_ref: &TransactionOptions, -) -> CliTypedResult { - // Profile gas if needed. - if txn_options_ref.profile_gas { - txn_options_ref.profile_gas(payload).await - } else { - // Otherwise submit the transaction. - txn_options_ref - .submit_transaction(payload) - .await - .map(TransactionSummary::from) - } -} diff --git a/m1/m1-cli/src/config/mod.rs b/m1/m1-cli/src/config/mod.rs deleted file mode 100644 index af4da540..00000000 --- a/m1/m1-cli/src/config/mod.rs +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::{ - types::{ - CliCommand, CliConfig, CliError, CliResult, CliTypedResult, ConfigSearchMode, - ProfileSummary, CONFIG_FOLDER, - }, - utils::{create_dir_if_not_exist, current_dir, read_from_file, write_to_user_only_file}, - }, - genesis::git::{from_yaml, to_yaml}, - Tool, -}; -use async_trait::async_trait; -use clap::{ArgEnum, CommandFactory, Parser}; -use clap_complete::{generate, Shell}; -use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, fmt::Formatter, path::PathBuf, str::FromStr}; - -/// Tool for interacting with configuration of the Movement CLI tool -/// -/// This tool handles the global configuration of the CLI tool for -/// default configuration, and user specific settings. -#[derive(Parser)] -pub enum ConfigTool { - Init(crate::common::init::InitTool), - GenerateShellCompletions(GenerateShellCompletions), - SetGlobalConfig(SetGlobalConfig), - ShowGlobalConfig(ShowGlobalConfig), - ShowProfiles(ShowProfiles), -} - -impl ConfigTool { - pub async fn execute(self) -> CliResult { - match self { - ConfigTool::Init(tool) => tool.execute_serialized_success().await, - ConfigTool::GenerateShellCompletions(tool) => tool.execute_serialized_success().await, - ConfigTool::SetGlobalConfig(tool) => tool.execute_serialized().await, - ConfigTool::ShowGlobalConfig(tool) => tool.execute_serialized().await, - ConfigTool::ShowProfiles(tool) => tool.execute_serialized().await, - } - } -} - -/// Generate shell completion files -/// -/// First generate the completion file, then follow the shell specific directions on how -/// to install the completion file. -#[derive(Parser)] -pub struct GenerateShellCompletions { - /// Shell to generate completions for one of [bash, elvish, powershell, zsh] - #[clap(long)] - shell: Shell, - - /// File to output shell completions to - #[clap(long, parse(from_os_str))] - output_file: PathBuf, -} - -#[async_trait] -impl CliCommand<()> for GenerateShellCompletions { - fn command_name(&self) -> &'static str { - "GenerateShellCompletions" - } - - async fn execute(self) -> CliTypedResult<()> { - let mut command = Tool::command(); - let mut file = std::fs::File::create(self.output_file.as_path()) - .map_err(|err| CliError::IO(self.output_file.display().to_string(), err))?; - generate(self.shell, &mut command, "movement".to_string(), &mut file); - Ok(()) - } -} - -/// Set global configuration settings -/// -/// Any configuration flags that are not provided will not be changed -#[derive(Parser, Debug)] -pub struct SetGlobalConfig { - /// A configuration for where to place and use the config - /// - /// `Workspace` will put the `.aptos/` folder in the current directory, where - /// `Global` will put the `.aptos/` folder in your home directory - #[clap(long)] - config_type: Option, - /// A configuration for how to expect the prompt response - /// - /// Option can be one of ["yes", "no", "prompt"], "yes" runs cli with "--assume-yes", where - /// "no" runs cli with "--assume-no", default: "prompt" - #[clap(long)] - default_prompt_response: Option, -} - -#[async_trait] -impl CliCommand for SetGlobalConfig { - fn command_name(&self) -> &'static str { - "SetGlobalConfig" - } - - async fn execute(self) -> CliTypedResult { - // Load the global config - let mut config = GlobalConfig::load()?; - - // Enable all features that are actually listed - if let Some(config_type) = self.config_type { - config.config_type = Some(config_type); - } - - if let Some(default_prompt_response) = self.default_prompt_response { - config.default_prompt_response = default_prompt_response; - } - - config.save()?; - config.display() - } -} - -/// Shows the current profiles available -/// -/// This will only show public information and will not show -/// private information -#[derive(Parser, Debug)] -pub struct ShowProfiles { - /// Which profile to show - /// - /// If provided, show only this profile - #[clap(long)] - profile: Option, -} - -#[async_trait] -impl CliCommand> for ShowProfiles { - fn command_name(&self) -> &'static str { - "ShowProfiles" - } - - async fn execute(self) -> CliTypedResult> { - // Load the profile config - let config = CliConfig::load(ConfigSearchMode::CurrentDir)?; - Ok(config - .profiles - .unwrap_or_default() - .into_iter() - .filter(|(key, _)| { - if let Some(ref profile) = self.profile { - profile == key - } else { - true - } - }) - .map(|(key, profile)| (key, ProfileSummary::from(&profile))) - .collect()) - } -} - -/// Shows the properties in the global config -#[derive(Parser, Debug)] -pub struct ShowGlobalConfig {} - -#[async_trait] -impl CliCommand for ShowGlobalConfig { - fn command_name(&self) -> &'static str { - "ShowGlobalConfig" - } - - async fn execute(self) -> CliTypedResult { - // Load the global config - let config = GlobalConfig::load()?; - - config.display() - } -} - -const GLOBAL_CONFIG_FILE: &str = "global_config.yaml"; - -/// A global configuration for global settings related to a user -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct GlobalConfig { - /// Whether to be using Global or Workspace mode - #[serde(skip_serializing_if = "Option::is_none")] - pub config_type: Option, - /// Prompt response type - #[serde(default)] - pub default_prompt_response: PromptResponseType, -} - -impl GlobalConfig { - /// Fill in defaults for display via the CLI - pub fn display(mut self) -> CliTypedResult { - if self.config_type.is_none() { - self.config_type = Some(ConfigType::default()); - } - - Ok(self) - } - - pub fn load() -> CliTypedResult { - let path = global_folder()?.join(GLOBAL_CONFIG_FILE); - if path.exists() { - from_yaml(&String::from_utf8(read_from_file(path.as_path())?)?) - } else { - // If we don't have a config, let's load the default - Ok(GlobalConfig::default()) - } - } - - /// Get the config location based on the type - pub fn get_config_location(&self, mode: ConfigSearchMode) -> CliTypedResult { - match self.config_type.unwrap_or_default() { - ConfigType::Global => global_folder(), - ConfigType::Workspace => find_workspace_config(current_dir()?, mode), - } - } - - /// Get the prompt options from global config - pub fn get_default_prompt_response(&self) -> Option { - match self.default_prompt_response { - PromptResponseType::Prompt => None, // prompt - PromptResponseType::Yes => Some(true), // assume_yes - PromptResponseType::No => Some(false), // assume_no - } - } - - fn save(&self) -> CliTypedResult<()> { - let global_folder = global_folder()?; - create_dir_if_not_exist(global_folder.as_path())?; - - write_to_user_only_file( - global_folder.join(GLOBAL_CONFIG_FILE).as_path(), - "Global Config", - &to_yaml(&self)?.into_bytes(), - ) - } -} - -fn global_folder() -> CliTypedResult { - if let Some(dir) = dirs::home_dir() { - Ok(dir.join(CONFIG_FOLDER)) - } else { - Err(CliError::UnexpectedError( - "Unable to retrieve home directory".to_string(), - )) - } -} - -fn find_workspace_config( - starting_path: PathBuf, - mode: ConfigSearchMode, -) -> CliTypedResult { - match mode { - ConfigSearchMode::CurrentDir => Ok(starting_path.join(CONFIG_FOLDER)), - ConfigSearchMode::CurrentDirAndParents => { - let mut current_path = starting_path.clone(); - loop { - current_path.push(CONFIG_FOLDER); - if current_path.is_dir() { - break Ok(current_path); - } else if !(current_path.pop() && current_path.pop()) { - // If we aren't able to find the folder, we'll create a new one right here - break Ok(starting_path.join(CONFIG_FOLDER)); - } - } - }, - } -} - -const GLOBAL: &str = "global"; -const WORKSPACE: &str = "workspace"; - -/// A configuration for where to place and use the config -/// -/// Workspace allows for multiple configs based on location, where -/// Global allows for one config for every part of the code -#[derive(Debug, Copy, Clone, Serialize, Deserialize, ArgEnum)] -pub enum ConfigType { - /// Per system user configuration put in `/.aptos` - Global, - /// Per directory configuration put in `/.aptos` - Workspace, -} - -impl Default for ConfigType { - fn default() -> Self { - // TODO: When we version up, we can change this to global - Self::Workspace - } -} - -impl std::fmt::Display for ConfigType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - ConfigType::Global => GLOBAL, - ConfigType::Workspace => WORKSPACE, - }) - } -} - -impl FromStr for ConfigType { - type Err = CliError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().trim() { - GLOBAL => Ok(Self::Global), - WORKSPACE => Ok(Self::Workspace), - _ => Err(CliError::CommandArgumentError( - "Invalid config type, must be one of [global, workspace]".to_string(), - )), - } - } -} - -const PROMPT: &str = "prompt"; -const ASSUME_YES: &str = "yes"; -const ASSUME_NO: &str = "no"; - -/// A configuration for how to expect the prompt response -/// -/// Option can be one of ["yes", "no", "prompt"], "yes" runs cli with "--assume-yes", where -/// "no" runs cli with "--assume-no", default: "prompt" -#[derive(Debug, Copy, Clone, Serialize, Deserialize, ArgEnum)] -pub enum PromptResponseType { - /// normal prompt - Prompt, - /// `--assume-yes` - Yes, - /// `--assume-no` - No, -} - -impl Default for PromptResponseType { - fn default() -> Self { - Self::Prompt - } -} - -impl std::fmt::Display for PromptResponseType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - PromptResponseType::Prompt => PROMPT, - PromptResponseType::Yes => ASSUME_YES, - PromptResponseType::No => ASSUME_NO, - }) - } -} - -impl FromStr for PromptResponseType { - type Err = CliError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().trim() { - PROMPT => Ok(Self::Prompt), - ASSUME_YES => Ok(Self::Yes), - ASSUME_NO => Ok(Self::No), - _ => Err(CliError::CommandArgumentError( - "Invalid prompt response type, must be one of [yes, no, prompt]".to_string(), - )), - } - } -} diff --git a/m1/m1-cli/src/faucet/mod.rs b/m1/m1-cli/src/faucet/mod.rs deleted file mode 100644 index 4b4a6d91..00000000 --- a/m1/m1-cli/src/faucet/mod.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::{ - types::{CliCommand, CliTypedResult} -}; -use async_trait::async_trait; -use crate::common::types::{FaucetOptions, ProfileOptions, RestOptions}; -use crate::common::utils::{fund_pub_key, wait_for_transactions}; -use clap::Parser; - -#[derive(Debug, Parser)] -pub struct FaucetTool { - #[clap(long)] - pub_key: Option, - #[clap(flatten)] - pub(crate) faucet_options: FaucetOptions, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -impl FaucetTool { - fn pub_key(&self, profile: &ProfileOptions) -> CliTypedResult { - match &self.pub_key { - Some(pub_key) => Ok(pub_key.clone()), - None => Ok((profile.public_key()?).to_string()), - } - } -} - -#[async_trait] -impl CliCommand for FaucetTool { - fn command_name(&self) -> &'static str { - "Faucet" - } - - async fn execute(self) -> CliTypedResult { - let profile = &self.profile_options; - let hashes = fund_pub_key( - self.faucet_options.faucet_url(&profile)?, - self.pub_key(&profile)?, - ).await?; - let client = self.rest_options.client_raw(self.faucet_options.faucet_url(&profile)?)?; - wait_for_transactions(&client, hashes).await?; - return Ok(format!( - "Added 10 MOV to account {}", self.pub_key(&profile)? - )); - } -} - diff --git a/m1/m1-cli/src/ffi.rs b/m1/m1-cli/src/ffi.rs deleted file mode 100644 index af9ba937..00000000 --- a/m1/m1-cli/src/ffi.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -#![allow(unsafe_code)] - -use crate::Tool; -use clap::Parser; -use std::{ - ffi::{c_char, CStr, CString}, - thread, -}; -use tokio::runtime::Runtime; - -/// # Safety -/// -/// Run the movement CLI synchronously -/// Note: This function should only be called from other SDK (i.g Typescript) -/// -/// Return: the pointer to CLIResult c string -#[no_mangle] -pub unsafe extern "C" fn run_aptos_sync(s: *const c_char) -> *const c_char { - let c_str = unsafe { - assert!(!s.is_null()); - CStr::from_ptr(s) - }; - - // split string by spaces - let input_string = c_str.to_str().unwrap().split_whitespace(); - - // Create a new Tokio runtime and block on the execution of `cli.execute()` - let result_string = Runtime::new().unwrap().block_on(async move { - let cli = Tool::parse_from(input_string); - let result = cli.execute().await; - result - }); - - let res_cstr = CString::new(result_string.unwrap()).unwrap(); - - // Return a pointer to the C string - res_cstr.into_raw() -} - -/// # Safety -/// -/// Run the movement CLI async; Use this function if you are expecting the movement CLI command -/// to run in the background, or different thread -/// Note: This function should only be called from other SDK (i.g Typescript) -/// -/// Return: the pointer to c string: 'true' -#[no_mangle] -pub unsafe extern "C" fn run_aptos_async(s: *mut c_char) -> *mut c_char { - println!("Running movement..."); - let c_str = unsafe { - assert!(!s.is_null()); - CStr::from_ptr(s) - }; - - // Spawn a new thread to run the CLI - thread::spawn(move || { - let rt = Runtime::new().unwrap(); - let input_string = c_str.to_str().unwrap().split_whitespace(); - let cli = Tool::parse_from(input_string); - - // Run the CLI once - rt.block_on(async { cli.execute().await }) - .expect("Failed to run CLI"); - }); - - // Return pointer - CString::new("true").unwrap().into_raw() -} - -/// # Safety -/// -/// After running the movement CLI using FFI. Make sure to invoke this method to free up or -/// deallocate the memory -#[no_mangle] -pub unsafe extern "C" fn free_cstring(s: *mut c_char) { - unsafe { - if s.is_null() { - return; - } - CString::from_raw(s) - }; -} diff --git a/m1/m1-cli/src/genesis/git.rs b/m1/m1-cli/src/genesis/git.rs deleted file mode 100644 index 38368915..00000000 --- a/m1/m1-cli/src/genesis/git.rs +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::{ - types::{CliError, CliTypedResult}, - utils::{create_dir_if_not_exist, write_to_file}, - }, - CliCommand, -}; -use aptos_config::config::Token; -use aptos_framework::ReleaseBundle; -use aptos_genesis::config::Layout; -use aptos_github_client::Client as GithubClient; -use async_trait::async_trait; -use clap::Parser; -use serde::{de::DeserializeOwned, Serialize}; -use std::{ - fmt::Debug, - io::Read, - path::{Path, PathBuf}, - str::FromStr, -}; - -pub const LAYOUT_FILE: &str = "layout.yaml"; -pub const OPERATOR_FILE: &str = "operator.yaml"; -pub const OWNER_FILE: &str = "owner.yaml"; -pub const FRAMEWORK_NAME: &str = "framework.mrb"; -pub const BALANCES_FILE: &str = "balances.yaml"; -pub const EMPLOYEE_VESTING_ACCOUNTS_FILE: &str = "employee_vesting_accounts.yaml"; - -/// Setup a shared Git repository for Genesis -/// -/// This will setup a folder or an online Github repository to be used -/// for Genesis. If it's the local, it will create the folders but not -/// set up a Git repository. -#[derive(Parser)] -pub struct SetupGit { - #[clap(flatten)] - pub(crate) git_options: GitOptions, - - /// Path to the `Layout` file which defines where all the files are - #[clap(long, parse(from_os_str))] - pub(crate) layout_file: PathBuf, -} - -#[async_trait] -impl CliCommand<()> for SetupGit { - fn command_name(&self) -> &'static str { - "SetupGit" - } - - async fn execute(self) -> CliTypedResult<()> { - let layout = Layout::from_disk(&self.layout_file)?; - - // Upload layout file to ensure we can read later - let client = self.git_options.get_client()?; - client.put(Path::new(LAYOUT_FILE), &layout)?; - - Ok(()) - } -} - -#[derive(Clone, Debug, Default)] -pub struct GithubRepo { - owner: String, - repository: String, -} - -impl FromStr for GithubRepo { - type Err = CliError; - - fn from_str(s: &str) -> Result { - let parts: Vec<_> = s.split('/').collect(); - if parts.len() != 2 { - Err(CliError::CommandArgumentError("Invalid repository must be of the form 'owner/repository` e.g. 'aptos-labs/aptos-core'".to_string())) - } else { - Ok(GithubRepo { - owner: parts.first().unwrap().to_string(), - repository: parts.get(1).unwrap().to_string(), - }) - } - } -} - -#[derive(Clone, Default, Parser)] -pub struct GitOptions { - /// Github repository e.g. 'aptos-labs/aptos-core' - /// - /// Mutually exclusive with `--local-repository-dir` - #[clap(long)] - pub(crate) github_repository: Option, - - /// Github repository branch e.g. main - #[clap(long, default_value = "main")] - pub(crate) github_branch: String, - - /// Path to Github API token. Token must have repo:* permissions - #[clap(long, parse(from_os_str))] - pub(crate) github_token_file: Option, - - /// Path to local git repository - /// - /// Mutually exclusive with `--github-repository` - #[clap(long, parse(from_os_str))] - pub(crate) local_repository_dir: Option, -} - -impl GitOptions { - pub fn get_client(self) -> CliTypedResult { - if self.github_repository.is_none() - && self.github_token_file.is_none() - && self.local_repository_dir.is_some() - { - Ok(Client::local(self.local_repository_dir.unwrap())) - } else if self.github_repository.is_some() - && self.github_token_file.is_some() - && self.local_repository_dir.is_none() - { - Client::github( - self.github_repository.unwrap(), - self.github_branch, - self.github_token_file.unwrap(), - ) - } else { - Err(CliError::CommandArgumentError("Must provide either only --local-repository-dir or both --github-repository and --github-token-path".to_string())) - } - } -} - -/// A client for abstracting away local vs Github storage -/// -/// Note: Writes do not commit locally -pub enum Client { - Local(PathBuf), - Github(GithubClient), -} - -impl Client { - pub fn local(path: PathBuf) -> Client { - Client::Local(path) - } - - pub fn github( - repository: GithubRepo, - branch: String, - token_path: PathBuf, - ) -> CliTypedResult { - let token = Token::FromDisk(token_path).read_token()?; - Ok(Client::Github(GithubClient::new( - repository.owner, - repository.repository, - branch, - token, - ))) - } - - /// Retrieves an object as a YAML encoded file from the appropriate storage - pub fn get(&self, path: &Path) -> CliTypedResult { - match self { - Client::Local(local_repository_path) => { - let path = local_repository_path.join(path); - - if !path.exists() { - return Err(CliError::UnableToReadFile( - path.display().to_string(), - "File not found".to_string(), - )); - } - - eprintln!("Reading {}", path.display()); - let mut file = std::fs::File::open(path.as_path()) - .map_err(|e| CliError::IO(path.display().to_string(), e))?; - - let mut contents = String::new(); - file.read_to_string(&mut contents) - .map_err(|e| CliError::IO(path.display().to_string(), e))?; - from_yaml(&contents) - }, - Client::Github(client) => { - from_base64_encoded_yaml(&client.get_file(&path.display().to_string())?) - }, - } - } - - /// Puts an object as a YAML encoded file to the appropriate storage - pub fn put(&self, name: &Path, input: &T) -> CliTypedResult<()> { - match self { - Client::Local(local_repository_path) => { - let path = local_repository_path.join(name); - - // Create repository path and any sub-directories - if let Some(dir) = path.parent() { - self.create_dir(dir)?; - } else { - return Err(CliError::UnexpectedError(format!( - "Path should always have a parent {}", - path.display() - ))); - } - write_to_file( - path.as_path(), - &path.display().to_string(), - to_yaml(input)?.as_bytes(), - )?; - }, - Client::Github(client) => { - client.put(&name.display().to_string(), &to_base64_encoded_yaml(input)?)?; - }, - } - - Ok(()) - } - - pub fn create_dir(&self, dir: &Path) -> CliTypedResult<()> { - match self { - Client::Local(local_repository_path) => { - let path = local_repository_path.join(dir); - create_dir_if_not_exist(path.as_path())?; - }, - Client::Github(_) => { - // There's no such thing as an empty directory in Git, so do nothing - }, - } - - Ok(()) - } - - /// Retrieve framework release bundle. - pub fn get_framework(&self) -> CliTypedResult { - match self { - Client::Local(local_repository_path) => { - let path = local_repository_path.join(FRAMEWORK_NAME); - if !path.exists() { - return Err(CliError::UnableToReadFile( - path.display().to_string(), - "File not found".to_string(), - )); - } - Ok(ReleaseBundle::read(path)?) - }, - Client::Github(client) => { - let bytes = base64::decode(client.get_file(FRAMEWORK_NAME)?)?; - Ok(bcs::from_bytes::(&bytes)?) - }, - } - } -} - -pub fn to_yaml(input: &T) -> CliTypedResult { - Ok(serde_yaml::to_string(input)?) -} - -pub fn from_yaml(input: &str) -> CliTypedResult { - Ok(serde_yaml::from_str(input)?) -} - -pub fn to_base64_encoded_yaml(input: &T) -> CliTypedResult { - Ok(base64::encode(to_yaml(input)?)) -} - -pub fn from_base64_encoded_yaml(input: &str) -> CliTypedResult { - from_yaml(&String::from_utf8(base64::decode(input)?)?) -} diff --git a/m1/m1-cli/src/genesis/keys.rs b/m1/m1-cli/src/genesis/keys.rs deleted file mode 100644 index c283ad7f..00000000 --- a/m1/m1-cli/src/genesis/keys.rs +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::{ - types::{CliError, CliTypedResult, OptionalPoolAddressArgs, PromptOptions, RngArgs}, - utils::{ - check_if_file_exists, create_dir_if_not_exist, current_dir, dir_default_to_current, - read_from_file, write_to_user_only_file, - }, - }, - genesis::git::{from_yaml, to_yaml, GitOptions, LAYOUT_FILE, OPERATOR_FILE, OWNER_FILE}, - governance::CompileScriptFunction, - CliCommand, -}; -use aptos_genesis::{ - config::{HostAndPort, Layout, OperatorConfiguration, OwnerConfiguration}, - keys::{generate_key_objects, PublicIdentity}, -}; -use aptos_types::{ - account_address::AccountAddress, - transaction::{Script, Transaction, WriteSetPayload}, -}; -use async_trait::async_trait; -use clap::Parser; -use std::path::{Path, PathBuf}; - -const PRIVATE_KEYS_FILE: &str = "private-keys.yaml"; -pub const PUBLIC_KEYS_FILE: &str = "public-keys.yaml"; -const VALIDATOR_FILE: &str = "validator-identity.yaml"; -const VFN_FILE: &str = "validator-full-node-identity.yaml"; - -/// Generate keys for a new validator -/// -/// Generates account key, consensus key, and network key for a validator -/// These keys are used for running a validator or operator in a network -#[derive(Parser)] -pub struct GenerateKeys { - /// Output directory for the key files - #[clap(long, parse(from_os_str))] - pub(crate) output_dir: Option, - - #[clap(flatten)] - pub(crate) pool_address_args: OptionalPoolAddressArgs, - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, - #[clap(flatten)] - pub rng_args: RngArgs, -} - -#[async_trait] -impl CliCommand> for GenerateKeys { - fn command_name(&self) -> &'static str { - "GenerateKeys" - } - - async fn execute(self) -> CliTypedResult> { - let output_dir = dir_default_to_current(self.output_dir.clone())?; - - let private_keys_file = output_dir.join(PRIVATE_KEYS_FILE); - let public_keys_file = output_dir.join(PUBLIC_KEYS_FILE); - let validator_file = output_dir.join(VALIDATOR_FILE); - let vfn_file = output_dir.join(VFN_FILE); - check_if_file_exists(private_keys_file.as_path(), self.prompt_options)?; - check_if_file_exists(public_keys_file.as_path(), self.prompt_options)?; - check_if_file_exists(validator_file.as_path(), self.prompt_options)?; - check_if_file_exists(vfn_file.as_path(), self.prompt_options)?; - - let mut key_generator = self.rng_args.key_generator()?; - let (mut validator_blob, mut vfn_blob, private_identity, public_identity) = - generate_key_objects(&mut key_generator)?; - - // Allow for the owner to be different than the operator - if let Some(pool_address) = self.pool_address_args.pool_address { - validator_blob.account_address = Some(pool_address); - vfn_blob.account_address = Some(pool_address); - } - - // Create the directory if it doesn't exist - create_dir_if_not_exist(output_dir.as_path())?; - - write_to_user_only_file( - private_keys_file.as_path(), - PRIVATE_KEYS_FILE, - to_yaml(&private_identity)?.as_bytes(), - )?; - write_to_user_only_file( - public_keys_file.as_path(), - PUBLIC_KEYS_FILE, - to_yaml(&public_identity)?.as_bytes(), - )?; - write_to_user_only_file( - validator_file.as_path(), - VALIDATOR_FILE, - to_yaml(&validator_blob)?.as_bytes(), - )?; - write_to_user_only_file(vfn_file.as_path(), VFN_FILE, to_yaml(&vfn_blob)?.as_bytes())?; - Ok(vec![ - public_keys_file, - private_keys_file, - validator_file, - vfn_file, - ]) - } -} - -/// Set validator configuration for a single validator -/// -/// This will set the validator configuration for a single validator in the git repository. -/// It will have to be run for each validator expected at genesis. -#[derive(Parser)] -pub struct SetValidatorConfiguration { - /// Name of the validator - #[clap(long)] - pub(crate) username: String, - - /// Host and port pair for the validator e.g. 127.0.0.1:6180 or aptoslabs.com:6180 - #[clap(long)] - pub(crate) validator_host: HostAndPort, - - /// Host and port pair for the fullnode e.g. 127.0.0.1:6180 or aptoslabs.com:6180 - #[clap(long)] - pub(crate) full_node_host: Option, - - /// Stake amount for stake distribution - #[clap(long, default_value_t = 1)] - pub(crate) stake_amount: u64, - - /// Commission rate to pay operator - /// - /// This is a percentage between 0% and 100% - #[clap(long, default_value_t = 0)] - pub(crate) commission_percentage: u64, - - /// Whether the validator will be joining the genesis validator set - /// - /// If set this validator will already be in the validator set at genesis - #[clap(long)] - pub(crate) join_during_genesis: bool, - - /// Path to private identity generated from GenerateKeys - #[clap(long, parse(from_os_str))] - pub(crate) owner_public_identity_file: Option, - - /// Path to operator public identity, defaults to owner identity - #[clap(long, parse(from_os_str))] - pub(crate) operator_public_identity_file: Option, - - /// Path to voter public identity, defaults to owner identity - #[clap(long, parse(from_os_str))] - pub(crate) voter_public_identity_file: Option, - - #[clap(flatten)] - pub(crate) git_options: GitOptions, -} - -#[async_trait] -impl CliCommand<()> for SetValidatorConfiguration { - fn command_name(&self) -> &'static str { - "SetValidatorConfiguration" - } - - async fn execute(self) -> CliTypedResult<()> { - // Load owner - let owner_keys_file = if let Some(owner_keys_file) = self.owner_public_identity_file { - owner_keys_file - } else { - current_dir()?.join(PUBLIC_KEYS_FILE) - }; - let owner_identity = read_public_identity_file(owner_keys_file.as_path())?; - - // Load voter - let voter_identity = if let Some(voter_keys_file) = self.voter_public_identity_file { - read_public_identity_file(voter_keys_file.as_path())? - } else { - owner_identity.clone() - }; - - // Load operator - let (operator_identity, operator_keys_file) = - if let Some(operator_keys_file) = self.operator_public_identity_file { - ( - read_public_identity_file(operator_keys_file.as_path())?, - operator_keys_file, - ) - } else { - (owner_identity.clone(), owner_keys_file) - }; - - // Extract the possible optional fields - let consensus_public_key = - if let Some(consensus_public_key) = operator_identity.consensus_public_key { - consensus_public_key - } else { - return Err(CliError::CommandArgumentError(format!( - "Failed to read consensus public key from public identity file {}", - operator_keys_file.display() - ))); - }; - - let validator_network_public_key = if let Some(validator_network_public_key) = - operator_identity.validator_network_public_key - { - validator_network_public_key - } else { - return Err(CliError::CommandArgumentError(format!( - "Failed to read validator network public key from public identity file {}", - operator_keys_file.display() - ))); - }; - - let consensus_proof_of_possession = if let Some(consensus_proof_of_possession) = - operator_identity.consensus_proof_of_possession - { - consensus_proof_of_possession - } else { - return Err(CliError::CommandArgumentError(format!( - "Failed to read consensus proof of possession from public identity file {}", - operator_keys_file.display() - ))); - }; - - // Only add the public key if there is a full node - let full_node_network_public_key = if self.full_node_host.is_some() { - operator_identity.full_node_network_public_key - } else { - None - }; - - // Build operator configuration file - let operator_config = OperatorConfiguration { - operator_account_address: operator_identity.account_address.into(), - operator_account_public_key: operator_identity.account_public_key.clone(), - consensus_public_key, - consensus_proof_of_possession, - validator_network_public_key, - validator_host: self.validator_host, - full_node_network_public_key, - full_node_host: self.full_node_host, - }; - - let owner_config = OwnerConfiguration { - owner_account_address: owner_identity.account_address.into(), - owner_account_public_key: owner_identity.account_public_key, - voter_account_address: voter_identity.account_address.into(), - voter_account_public_key: voter_identity.account_public_key, - operator_account_address: operator_identity.account_address.into(), - operator_account_public_key: operator_identity.account_public_key, - stake_amount: self.stake_amount, - commission_percentage: self.commission_percentage, - join_during_genesis: self.join_during_genesis, - }; - - let directory = PathBuf::from(&self.username); - let operator_file = directory.join(OPERATOR_FILE); - let owner_file = directory.join(OWNER_FILE); - - let git_client = self.git_options.get_client()?; - git_client.put(operator_file.as_path(), &operator_config)?; - git_client.put(owner_file.as_path(), &owner_config) - } -} - -pub fn read_public_identity_file(public_identity_file: &Path) -> CliTypedResult { - let bytes = read_from_file(public_identity_file)?; - from_yaml(&String::from_utf8(bytes).map_err(CliError::from)?) -} - -/// Generate a Layout template file -/// -/// This will generate a layout template file for genesis with some default values. To start a -/// new chain, these defaults should be carefully thought through and chosen. -#[derive(Parser)] -pub struct GenerateLayoutTemplate { - /// Path of the output layout template - #[clap(long, parse(from_os_str), default_value = LAYOUT_FILE)] - pub(crate) output_file: PathBuf, - - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, -} - -#[async_trait] -impl CliCommand<()> for GenerateLayoutTemplate { - fn command_name(&self) -> &'static str { - "GenerateLayoutTemplate" - } - - async fn execute(self) -> CliTypedResult<()> { - check_if_file_exists(self.output_file.as_path(), self.prompt_options)?; - let layout = Layout::default(); - - write_to_user_only_file( - self.output_file.as_path(), - &self.output_file.display().to_string(), - to_yaml(&layout)?.as_bytes(), - ) - } -} - -/// Generate a WriteSet genesis -/// -/// This will compile a Move script and generate a writeset from that script. -#[derive(Parser)] -pub struct GenerateAdminWriteSet { - /// Path of the output genesis file - #[clap(long, parse(from_os_str))] - pub(crate) output_file: PathBuf, - - /// Address of the account which execute this script. - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub(crate) execute_as: AccountAddress, - - #[clap(flatten)] - pub(crate) compile_proposal_args: CompileScriptFunction, - - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, -} - -#[async_trait] -impl CliCommand<()> for GenerateAdminWriteSet { - fn command_name(&self) -> &'static str { - "GenerateAdminWriteSet" - } - - async fn execute(self) -> CliTypedResult<()> { - check_if_file_exists(self.output_file.as_path(), self.prompt_options)?; - let (bytecode, _script_hash) = self - .compile_proposal_args - .compile("GenerateAdminWriteSet", self.prompt_options)?; - - let txn = Transaction::GenesisTransaction(WriteSetPayload::Script { - execute_as: self.execute_as, - script: Script::new(bytecode, vec![], vec![]), - }); - - write_to_user_only_file( - self.output_file.as_path(), - &self.output_file.display().to_string(), - &bcs::to_bytes(&txn).map_err(CliError::from)?, - ) - } -} diff --git a/m1/m1-cli/src/genesis/mod.rs b/m1/m1-cli/src/genesis/mod.rs deleted file mode 100644 index 66f1a5b6..00000000 --- a/m1/m1-cli/src/genesis/mod.rs +++ /dev/null @@ -1,926 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -pub mod git; -pub mod keys; -#[cfg(test)] -mod tests; -pub mod tools; - -use crate::{ - common::{ - types::{CliError, CliTypedResult, PromptOptions}, - utils::{check_if_file_exists, dir_default_to_current, write_to_file}, - }, - genesis::git::{ - Client, GitOptions, BALANCES_FILE, EMPLOYEE_VESTING_ACCOUNTS_FILE, LAYOUT_FILE, - OPERATOR_FILE, OWNER_FILE, - }, - CliCommand, CliResult, -}; -use aptos_crypto::{ - bls12381, ed25519::ED25519_PUBLIC_KEY_LENGTH, x25519, ValidCryptoMaterial, - ValidCryptoMaterialStringExt, -}; -use aptos_genesis::{ - builder::GenesisConfiguration, - config::{ - AccountBalanceMap, EmployeePoolMap, HostAndPort, Layout, StringOperatorConfiguration, - StringOwnerConfiguration, ValidatorConfiguration, - }, - mainnet::MainnetGenesisInfo, - GenesisInfo, -}; -use aptos_logger::info; -use aptos_types::{ - account_address::{AccountAddress, AccountAddressWithChecks}, - on_chain_config::{OnChainConsensusConfig, OnChainExecutionConfig}, -}; -use aptos_vm_genesis::{default_gas_schedule, AccountBalance, EmployeePool}; -use async_trait::async_trait; -use clap::Parser; -use std::{ - cmp::Ordering, - collections::{BTreeMap, BTreeSet, HashSet}, - path::{Path, PathBuf}, - str::FromStr, -}; - -const WAYPOINT_FILE: &str = "waypoint.txt"; -const GENESIS_FILE: &str = "genesis.blob"; - -/// Tool for setting up an Movement chain Genesis transaction -/// -/// This tool sets up a space for multiple initial "validator" -/// accounts to build a genesis transaction for a new chain. -#[derive(Parser)] -pub enum GenesisTool { - GenerateAdminWriteSet(keys::GenerateAdminWriteSet), - GenerateGenesis(GenerateGenesis), - GetPoolAddresses(tools::PoolAddresses), - GenerateKeys(keys::GenerateKeys), - GenerateLayoutTemplate(keys::GenerateLayoutTemplate), - SetupGit(git::SetupGit), - SetValidatorConfiguration(keys::SetValidatorConfiguration), -} - -impl GenesisTool { - pub async fn execute(self) -> CliResult { - match self { - GenesisTool::GenerateAdminWriteSet(tool) => tool.execute_serialized_success().await, - GenesisTool::GenerateGenesis(tool) => tool.execute_serialized().await, - GenesisTool::GetPoolAddresses(tool) => tool.execute_serialized().await, - GenesisTool::GenerateKeys(tool) => tool.execute_serialized().await, - GenesisTool::GenerateLayoutTemplate(tool) => tool.execute_serialized_success().await, - GenesisTool::SetupGit(tool) => tool.execute_serialized_success().await, - GenesisTool::SetValidatorConfiguration(tool) => tool.execute_serialized_success().await, - } - } -} - -/// Generate genesis from a git repository -/// -/// This will create a genesis.blob and a waypoint.txt to be used for -/// running a network -#[derive(Parser)] -pub struct GenerateGenesis { - /// Output directory for Genesis file and waypoint - #[clap(long, parse(from_os_str))] - output_dir: Option, - /// Whether this is mainnet genesis. - /// - /// Default is false - #[clap(long)] - mainnet: bool, - - #[clap(flatten)] - prompt_options: PromptOptions, - #[clap(flatten)] - git_options: GitOptions, -} - -#[async_trait] -impl CliCommand> for GenerateGenesis { - fn command_name(&self) -> &'static str { - "GenerateGenesis" - } - - async fn execute(self) -> CliTypedResult> { - let output_dir = dir_default_to_current(self.output_dir.clone())?; - let genesis_file = output_dir.join(GENESIS_FILE); - let waypoint_file = output_dir.join(WAYPOINT_FILE); - check_if_file_exists(genesis_file.as_path(), self.prompt_options)?; - check_if_file_exists(waypoint_file.as_path(), self.prompt_options)?; - - // Generate genesis and waypoint files - let (genesis_bytes, waypoint) = if self.mainnet { - let mut mainnet_genesis = fetch_mainnet_genesis_info(self.git_options)?; - let genesis_bytes = bcs::to_bytes(mainnet_genesis.clone().get_genesis()) - .map_err(|e| CliError::BCS(GENESIS_FILE, e))?; - (genesis_bytes, mainnet_genesis.generate_waypoint()?) - } else { - let mut test_genesis = fetch_genesis_info(self.git_options)?; - let genesis_bytes = bcs::to_bytes(test_genesis.clone().get_genesis()) - .map_err(|e| CliError::BCS(GENESIS_FILE, e))?; - (genesis_bytes, test_genesis.generate_waypoint()?) - }; - write_to_file(genesis_file.as_path(), GENESIS_FILE, &genesis_bytes)?; - write_to_file( - waypoint_file.as_path(), - WAYPOINT_FILE, - waypoint.to_string().as_bytes(), - )?; - Ok(vec![genesis_file, waypoint_file]) - } -} - -/// Retrieves all information for mainnet genesis from the Git repository -pub fn fetch_mainnet_genesis_info(git_options: GitOptions) -> CliTypedResult { - let client = git_options.get_client()?; - let layout: Layout = client.get(Path::new(LAYOUT_FILE))?; - - if layout.root_key.is_some() { - return Err(CliError::UnexpectedError( - "Root key must not be set for mainnet.".to_string(), - )); - } - - let total_supply = layout.total_supply.ok_or_else(|| { - CliError::UnexpectedError("Layout file does not have `total_supply`".to_string()) - })?; - - let account_balance_map: AccountBalanceMap = client.get(Path::new(BALANCES_FILE))?; - let accounts: Vec = account_balance_map.try_into()?; - - // Check that the supply matches the total - let total_balance_supply: u64 = accounts.iter().map(|inner| inner.balance).sum(); - if total_supply != total_balance_supply { - return Err(CliError::UnexpectedError(format!( - "Total supply seen {} doesn't match expected total supply {}", - total_balance_supply, total_supply - ))); - } - - // Check that the user has a reasonable amount of APT, since below the minimum gas amount is - // not useful 1 APT minimally - const MIN_USEFUL_AMOUNT: u64 = 200000000; - let ten_percent_of_total = total_supply / 10; - for account in accounts.iter() { - if account.balance != 0 && account.balance < MIN_USEFUL_AMOUNT { - return Err(CliError::UnexpectedError(format!( - "Account {} has an initial supply below expected amount {} < {}", - account.account_address, account.balance, MIN_USEFUL_AMOUNT - ))); - } else if account.balance > ten_percent_of_total { - return Err(CliError::UnexpectedError(format!( - "Account {} has an more than 10% of the total balance {} > {}", - account.account_address, account.balance, ten_percent_of_total - ))); - } - } - - // Keep track of accounts for later lookup of balances - let initialized_accounts: BTreeMap = accounts - .iter() - .map(|inner| (inner.account_address, inner.balance)) - .collect(); - - let employee_vesting_accounts: EmployeePoolMap = - client.get(Path::new(EMPLOYEE_VESTING_ACCOUNTS_FILE))?; - - let employee_validators: Vec<_> = employee_vesting_accounts - .inner - .iter() - .map(|inner| inner.validator.clone()) - .collect(); - let employee_vesting_accounts: Vec = employee_vesting_accounts.try_into()?; - let validators = get_validator_configs(&client, &layout, true).map_err(parse_error)?; - let mut unique_accounts = BTreeSet::new(); - let mut unique_network_keys = HashSet::new(); - let mut unique_consensus_keys = HashSet::new(); - let mut unique_consensus_pop = HashSet::new(); - let mut unique_hosts = HashSet::new(); - - validate_employee_accounts( - &employee_vesting_accounts, - &initialized_accounts, - &mut unique_accounts, - )?; - - let mut seen_owners = BTreeMap::new(); - validate_validators( - &layout, - &employee_validators, - &initialized_accounts, - &mut unique_accounts, - &mut unique_network_keys, - &mut unique_consensus_keys, - &mut unique_consensus_pop, - &mut unique_hosts, - &mut seen_owners, - true, - )?; - validate_validators( - &layout, - &validators, - &initialized_accounts, - &mut unique_accounts, - &mut unique_network_keys, - &mut unique_consensus_keys, - &mut unique_consensus_pop, - &mut unique_hosts, - &mut seen_owners, - false, - )?; - - let framework = client.get_framework()?; - Ok(MainnetGenesisInfo::new( - layout.chain_id, - accounts, - employee_vesting_accounts, - validators, - framework, - &GenesisConfiguration { - allow_new_validators: true, - epoch_duration_secs: layout.epoch_duration_secs, - is_test: false, - min_stake: layout.min_stake, - min_voting_threshold: layout.min_voting_threshold, - max_stake: layout.max_stake, - recurring_lockup_duration_secs: layout.recurring_lockup_duration_secs, - required_proposer_stake: layout.required_proposer_stake, - rewards_apy_percentage: layout.rewards_apy_percentage, - voting_duration_secs: layout.voting_duration_secs, - voting_power_increase_limit: layout.voting_power_increase_limit, - employee_vesting_start: layout.employee_vesting_start, - employee_vesting_period_duration: layout.employee_vesting_period_duration, - consensus_config: OnChainConsensusConfig::default(), - execution_config: OnChainExecutionConfig::default(), - gas_schedule: default_gas_schedule(), - }, - )?) -} - -/// Retrieves all information for genesis from the Git repository -pub fn fetch_genesis_info(git_options: GitOptions) -> CliTypedResult { - let client = git_options.get_client()?; - let layout: Layout = client.get(Path::new(LAYOUT_FILE))?; - - if layout.root_key.is_none() { - return Err(CliError::UnexpectedError( - "Layout field root_key was not set. Please provide a hex encoded Ed25519PublicKey." - .to_string(), - )); - } - - let validators = get_validator_configs(&client, &layout, false).map_err(parse_error)?; - let framework = client.get_framework()?; - Ok(GenesisInfo::new( - layout.chain_id, - layout.root_key.unwrap(), - validators, - framework, - &GenesisConfiguration { - allow_new_validators: layout.allow_new_validators, - epoch_duration_secs: layout.epoch_duration_secs, - is_test: layout.is_test, - min_stake: layout.min_stake, - min_voting_threshold: layout.min_voting_threshold, - max_stake: layout.max_stake, - recurring_lockup_duration_secs: layout.recurring_lockup_duration_secs, - required_proposer_stake: layout.required_proposer_stake, - rewards_apy_percentage: layout.rewards_apy_percentage, - voting_duration_secs: layout.voting_duration_secs, - voting_power_increase_limit: layout.voting_power_increase_limit, - employee_vesting_start: layout.employee_vesting_start, - employee_vesting_period_duration: layout.employee_vesting_period_duration, - consensus_config: OnChainConsensusConfig::default(), - execution_config: OnChainExecutionConfig::default(), - gas_schedule: default_gas_schedule(), - }, - )?) -} - -fn parse_error(errors: Vec) -> CliError { - eprintln!( - "Failed to parse genesis inputs:\n{}", - serde_yaml::to_string(&errors).unwrap() - ); - CliError::UnexpectedError("Failed to parse genesis inputs".to_string()) -} - -fn get_validator_configs( - client: &Client, - layout: &Layout, - is_mainnet: bool, -) -> Result, Vec> { - let mut validators = Vec::new(); - let mut errors = Vec::new(); - for user in &layout.users { - match get_config(client, user, is_mainnet) { - Ok(validator) => { - validators.push(validator); - }, - Err(failure) => { - if let CliError::UnexpectedError(failure) = failure { - errors.push(format!("{}: {}", user, failure)); - } else { - errors.push(format!("{}: {:?}", user, failure)); - } - }, - } - } - - if errors.is_empty() { - Ok(validators) - } else { - Err(errors) - } -} - -/// Do proper parsing so more information is known about failures -fn get_config( - client: &Client, - user: &str, - is_mainnet: bool, -) -> CliTypedResult { - // Load a user's configuration files - let dir = PathBuf::from(user); - let owner_file = dir.join(OWNER_FILE); - let owner_file = owner_file.as_path(); - let owner_config = client.get::(owner_file)?; - - // Check and convert fields in owner file - let owner_account_address: AccountAddress = parse_required_option( - &owner_config.owner_account_address, - owner_file, - "owner_account_address", - AccountAddressWithChecks::from_str, - )? - .into(); - let owner_account_public_key = parse_required_option( - &owner_config.owner_account_public_key, - owner_file, - "owner_account_public_key", - |str| parse_key(ED25519_PUBLIC_KEY_LENGTH, str), - )?; - - let operator_account_address: AccountAddress = parse_required_option( - &owner_config.operator_account_address, - owner_file, - "operator_account_address", - AccountAddressWithChecks::from_str, - )? - .into(); - let operator_account_public_key = parse_required_option( - &owner_config.operator_account_public_key, - owner_file, - "operator_account_public_key", - |str| parse_key(ED25519_PUBLIC_KEY_LENGTH, str), - )?; - - let voter_account_address: AccountAddress = parse_required_option( - &owner_config.voter_account_address, - owner_file, - "voter_account_address", - AccountAddressWithChecks::from_str, - )? - .into(); - let voter_account_public_key = parse_required_option( - &owner_config.voter_account_public_key, - owner_file, - "voter_account_public_key", - |str| parse_key(ED25519_PUBLIC_KEY_LENGTH, str), - )?; - - let stake_amount = parse_required_option( - &owner_config.stake_amount, - owner_file, - "stake_amount", - u64::from_str, - )?; - - // Default to 0 for commission percentage if missing. - let commission_percentage = parse_optional_option( - &owner_config.commission_percentage, - owner_file, - "commission_percentage", - u64::from_str, - )? - .unwrap_or(0); - - // Default to true for whether the validator should be joining during genesis. - let join_during_genesis = parse_optional_option( - &owner_config.join_during_genesis, - owner_file, - "join_during_genesis", - bool::from_str, - )? - .unwrap_or(true); - - // We don't require the operator file if the validator is not joining during genesis. - if is_mainnet && !join_during_genesis { - return Ok(ValidatorConfiguration { - owner_account_address: owner_account_address.into(), - owner_account_public_key, - operator_account_address: operator_account_address.into(), - operator_account_public_key, - voter_account_address: voter_account_address.into(), - voter_account_public_key, - consensus_public_key: None, - proof_of_possession: None, - validator_network_public_key: None, - validator_host: None, - full_node_network_public_key: None, - full_node_host: None, - stake_amount, - commission_percentage, - join_during_genesis, - }); - }; - - let operator_file = dir.join(OPERATOR_FILE); - let operator_file = operator_file.as_path(); - let operator_config = client.get::(operator_file)?; - - // Check and convert fields in operator file - let operator_account_address_from_file: AccountAddress = parse_required_option( - &operator_config.operator_account_address, - operator_file, - "operator_account_address", - AccountAddressWithChecks::from_str, - )? - .into(); - let operator_account_public_key_from_file = parse_required_option( - &operator_config.operator_account_public_key, - operator_file, - "operator_account_public_key", - |str| parse_key(ED25519_PUBLIC_KEY_LENGTH, str), - )?; - let consensus_public_key = parse_required_option( - &operator_config.consensus_public_key, - operator_file, - "consensus_public_key", - |str| parse_key(bls12381::PublicKey::LENGTH, str), - )?; - let consensus_proof_of_possession = parse_required_option( - &operator_config.consensus_proof_of_possession, - operator_file, - "consensus_proof_of_possession", - |str| parse_key(bls12381::ProofOfPossession::LENGTH, str), - )?; - let validator_network_public_key = parse_required_option( - &operator_config.validator_network_public_key, - operator_file, - "validator_network_public_key", - |str| parse_key(ED25519_PUBLIC_KEY_LENGTH, str), - )?; - let full_node_network_public_key = parse_optional_option( - &operator_config.full_node_network_public_key, - operator_file, - "full_node_network_public_key", - |str| parse_key(ED25519_PUBLIC_KEY_LENGTH, str), - )?; - - // Verify owner & operator agree on operator - if operator_account_address != operator_account_address_from_file { - return Err( - CliError::CommandArgumentError( - format!("Operator account {} in owner file {} does not match operator account {} in operator file {}", - operator_account_address, - owner_file.display(), - operator_account_address_from_file, - operator_file.display() - ))); - } - if operator_account_public_key != operator_account_public_key_from_file { - return Err( - CliError::CommandArgumentError( - format!("Operator public key {} in owner file {} does not match operator public key {} in operator file {}", - operator_account_public_key, - owner_file.display(), - operator_account_public_key_from_file, - operator_file.display() - ))); - } - - // Build Validator configuration - Ok(ValidatorConfiguration { - owner_account_address: owner_account_address.into(), - owner_account_public_key, - operator_account_address: operator_account_address.into(), - operator_account_public_key, - voter_account_address: voter_account_address.into(), - voter_account_public_key, - consensus_public_key: Some(consensus_public_key), - proof_of_possession: Some(consensus_proof_of_possession), - validator_network_public_key: Some(validator_network_public_key), - validator_host: Some(operator_config.validator_host), - full_node_network_public_key, - full_node_host: operator_config.full_node_host, - stake_amount, - commission_percentage, - join_during_genesis, - }) -} - -// TODO: Move into the Crypto libraries -fn parse_key(num_bytes: usize, str: &str) -> anyhow::Result { - let num_chars: usize = num_bytes * 2; - let mut working = str.trim(); - - // Checks if it has a 0x at the beginning, which is okay - if working.starts_with("0x") { - working = &working[2..]; - } - - match working.len().cmp(&num_chars) { - Ordering::Less => { - anyhow::bail!( - "Key {} is too short {} must be {} hex characters", - str, - working.len(), - num_chars - ) - }, - Ordering::Greater => { - anyhow::bail!( - "Key {} is too long {} must be {} hex characters with or without a 0x in front", - str, - working.len(), - num_chars - ) - }, - Ordering::Equal => {}, - } - - if !working.chars().all(|c| char::is_ascii_hexdigit(&c)) { - anyhow::bail!("Key {} contains a non-hex character", str) - } - - Ok(T::from_encoded_string(str.trim())?) -} - -fn parse_required_option Result, T, E: std::fmt::Display>( - option: &Option, - file: &Path, - field_name: &'static str, - parse: F, -) -> Result { - if let Some(ref field) = option { - parse(field).map_err(|err| { - CliError::CommandArgumentError(format!( - "Field {} is invalid in file {}. Err: {}", - field_name, - file.display(), - err - )) - }) - } else { - Err(CliError::CommandArgumentError(format!( - "File {} is missing {}", - file.display(), - field_name - ))) - } -} - -fn parse_optional_option Result, T, E: std::fmt::Display>( - option: &Option, - file: &Path, - field_name: &'static str, - parse: F, -) -> Result, CliError> { - if let Some(ref field) = option { - parse(field) - .map_err(|err| { - CliError::CommandArgumentError(format!( - "Field {} is invalid in file {}. Err: {}", - field_name, - file.display(), - err - )) - }) - .map(Some) - } else { - Ok(None) - } -} - -fn validate_validators( - layout: &Layout, - validators: &[ValidatorConfiguration], - initialized_accounts: &BTreeMap, - unique_accounts: &mut BTreeSet, - unique_network_keys: &mut HashSet, - unique_consensus_keys: &mut HashSet, - unique_consensus_pops: &mut HashSet, - unique_hosts: &mut HashSet, - seen_owners: &mut BTreeMap, - is_pooled_validator: bool, -) -> CliTypedResult<()> { - // check accounts for validators - let mut errors = vec![]; - - for (i, validator) in validators.iter().enumerate() { - let name = if is_pooled_validator { - format!("Employee Pool #{}", i) - } else { - layout.users.get(i).unwrap().to_string() - }; - - if !initialized_accounts.contains_key(&validator.owner_account_address.into()) { - errors.push(CliError::UnexpectedError(format!( - "Owner {} in validator {} is is not in the balances.yaml file", - validator.owner_account_address, name - ))); - } - if !initialized_accounts.contains_key(&validator.operator_account_address.into()) { - errors.push(CliError::UnexpectedError(format!( - "Operator {} in validator {} is is not in the balances.yaml file", - validator.operator_account_address, name - ))); - } - if !initialized_accounts.contains_key(&validator.voter_account_address.into()) { - errors.push(CliError::UnexpectedError(format!( - "Voter {} in validator {} is is not in the balances.yaml file", - validator.voter_account_address, name - ))); - } - - let owner_balance = initialized_accounts - .get(&validator.owner_account_address.into()) - .unwrap(); - - if seen_owners.contains_key(&validator.owner_account_address.into()) { - errors.push(CliError::UnexpectedError(format!( - "Owner {} in validator {} has been seen before as an owner of validator {}", - validator.owner_account_address, - name, - seen_owners - .get(&validator.owner_account_address.into()) - .unwrap() - ))); - } - seen_owners.insert(validator.owner_account_address.into(), i); - - if unique_accounts.contains(&validator.owner_account_address.into()) { - errors.push(CliError::UnexpectedError(format!( - "Owner '{}' in validator {} has already been seen elsewhere", - validator.owner_account_address, name - ))); - } - unique_accounts.insert(validator.owner_account_address.into()); - - if unique_accounts.contains(&validator.operator_account_address.into()) { - errors.push(CliError::UnexpectedError(format!( - "Operator '{}' in validator {} has already been seen elsewhere", - validator.operator_account_address, name - ))); - } - unique_accounts.insert(validator.operator_account_address.into()); - - // Pooled validators have a combined balance - // TODO: Make this field optional but checked - if !is_pooled_validator && *owner_balance < validator.stake_amount { - errors.push(CliError::UnexpectedError(format!( - "Owner {} in validator {} has less in it's balance {} than the stake amount for the validator {}", - validator.owner_account_address, name, owner_balance, validator.stake_amount - ))); - } - if validator.stake_amount < layout.min_stake { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has stake {} under the min stake {}", - name, validator.stake_amount, layout.min_stake - ))); - } - if validator.stake_amount > layout.max_stake { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has stake {} over the max stake {}", - name, validator.stake_amount, layout.max_stake - ))); - } - - // Ensure that the validator is setup correctly if it's joining in genesis - if validator.join_during_genesis { - if validator.validator_network_public_key.is_none() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} does not have a validator network public key, though it's joining during genesis", - name - ))); - } - if !unique_network_keys.insert(validator.validator_network_public_key.unwrap()) { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a repeated validator network key{}", - name, - validator.validator_network_public_key.unwrap() - ))); - } - - if validator.validator_host.is_none() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} does not have a validator host, though it's joining during genesis", - name - ))); - } - if !unique_hosts.insert(validator.validator_host.as_ref().unwrap().clone()) { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a repeated validator host {:?}", - name, - validator.validator_host.as_ref().unwrap() - ))); - } - - if validator.consensus_public_key.is_none() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} does not have a consensus public key, though it's joining during genesis", - name - ))); - } - if !unique_consensus_keys - .insert(validator.consensus_public_key.as_ref().unwrap().clone()) - { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a repeated a consensus public key {}", - name, - validator.consensus_public_key.as_ref().unwrap() - ))); - } - - if validator.proof_of_possession.is_none() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} does not have a consensus proof of possession, though it's joining during genesis", - name - ))); - } - if !unique_consensus_pops - .insert(validator.proof_of_possession.as_ref().unwrap().clone()) - { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a repeated a consensus proof of possessions {}", - name, - validator.proof_of_possession.as_ref().unwrap() - ))); - } - - match ( - validator.full_node_host.as_ref(), - validator.full_node_network_public_key.as_ref(), - ) { - (None, None) => { - info!("Validator {} does not have a full node setup", name); - }, - (Some(_), None) | (None, Some(_)) => { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a full node host or public key but not both", - name - ))); - }, - (Some(full_node_host), Some(full_node_network_public_key)) => { - // Ensure that the validator and the full node aren't the same - let validator_host = validator.validator_host.as_ref().unwrap(); - let validator_network_public_key = - validator.validator_network_public_key.as_ref().unwrap(); - if validator_host == full_node_host { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a validator and a full node host that are the same {:?}", - name, - validator_host - ))); - } - if !unique_hosts.insert(validator.full_node_host.as_ref().unwrap().clone()) { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a repeated full node host {:?}", - name, - validator.full_node_host.as_ref().unwrap() - ))); - } - - if validator_network_public_key == full_node_network_public_key { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a validator and a full node network public key that are the same {}", - name, - validator_network_public_key - ))); - } - if !unique_network_keys.insert(validator.full_node_network_public_key.unwrap()) - { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a repeated full node network key {}", - name, - validator.full_node_network_public_key.unwrap() - ))); - } - }, - } - } else { - if validator.validator_network_public_key.is_some() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a validator network public key, but it is *NOT* joining during genesis", - name - ))); - } - if validator.validator_host.is_some() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a validator host, but it is *NOT* joining during genesis", - name - ))); - } - if validator.consensus_public_key.is_some() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a consensus public key, but it is *NOT* joining during genesis", - name - ))); - } - if validator.proof_of_possession.is_some() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a consensus proof of possession, but it is *NOT* joining during genesis", - name - ))); - } - if validator.full_node_network_public_key.is_some() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a full node public key, but it is *NOT* joining during genesis", - name - ))); - } - if validator.full_node_host.is_some() { - errors.push(CliError::UnexpectedError(format!( - "Validator {} has a full node host, but it is *NOT* joining during genesis", - name - ))); - } - } - } - - if errors.is_empty() { - Ok(()) - } else { - eprintln!("{:#?}", errors); - - Err(CliError::UnexpectedError( - "Failed to validate validators".to_string(), - )) - } -} - -fn validate_employee_accounts( - employee_vesting_accounts: &[EmployeePool], - initialized_accounts: &BTreeMap, - unique_accounts: &mut BTreeSet, -) -> CliTypedResult<()> { - // Check accounts for employee accounts - for (i, pool) in employee_vesting_accounts.iter().enumerate() { - let mut total_stake_pool_amount = 0; - for (j, account) in pool.accounts.iter().enumerate() { - if !initialized_accounts.contains_key(account) { - return Err(CliError::UnexpectedError(format!( - "Account #{} '{}' in employee pool #{} is not in the balances.yaml file", - j, account, i - ))); - } - if unique_accounts.contains(account) { - return Err(CliError::UnexpectedError(format!( - "Account #{} '{}' in employee pool #{} has already been seen elsewhere", - j, account, i - ))); - } - unique_accounts.insert(*account); - - total_stake_pool_amount += initialized_accounts.get(account).unwrap(); - } - - if total_stake_pool_amount != pool.validator.validator.stake_amount { - return Err(CliError::UnexpectedError(format!( - "Stake amount {} in employee pool #{} does not match combined of accounts {}", - pool.validator.validator.stake_amount, i, total_stake_pool_amount - ))); - } - - if !initialized_accounts.contains_key(&pool.validator.validator.owner_address) { - return Err(CliError::UnexpectedError(format!( - "Owner address {} in employee pool #{} is is not in the balances.yaml file", - pool.validator.validator.owner_address, i - ))); - } - if !initialized_accounts.contains_key(&pool.validator.validator.operator_address) { - return Err(CliError::UnexpectedError(format!( - "Operator address {} in employee pool #{} is is not in the balances.yaml file", - pool.validator.validator.operator_address, i - ))); - } - if !initialized_accounts.contains_key(&pool.validator.validator.voter_address) { - return Err(CliError::UnexpectedError(format!( - "Voter address {} in employee pool #{} is is not in the balances.yaml file", - pool.validator.validator.voter_address, i - ))); - } - if !initialized_accounts.contains_key(&pool.beneficiary_resetter) { - return Err(CliError::UnexpectedError(format!( - "Beneficiary resetter {} in employee pool #{} is is not in the balances.yaml file", - pool.beneficiary_resetter, i - ))); - } - } - Ok(()) -} diff --git a/m1/m1-cli/src/genesis/tests.rs b/m1/m1-cli/src/genesis/tests.rs deleted file mode 100644 index bb270e6e..00000000 --- a/m1/m1-cli/src/genesis/tests.rs +++ /dev/null @@ -1,434 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::{ - types::{OptionalPoolAddressArgs, PromptOptions, RngArgs}, - utils::{read_from_file, write_to_file}, - }, - genesis::{ - git::{ - from_yaml, GitOptions, SetupGit, BALANCES_FILE, EMPLOYEE_VESTING_ACCOUNTS_FILE, - FRAMEWORK_NAME, - }, - keys::{GenerateKeys, GenerateLayoutTemplate, SetValidatorConfiguration, PUBLIC_KEYS_FILE}, - GenerateGenesis, - }, - CliCommand, -}; -use aptos_crypto::{ - ed25519::{Ed25519PrivateKey, Ed25519PublicKey}, - PrivateKey, -}; -use aptos_genesis::{ - config::{ - AccountBalanceMap, EmployeePoolConfig, EmployeePoolMap, HostAndPort, Layout, - ValidatorConfiguration, - }, - keys::PublicIdentity, -}; -use aptos_keygen::KeyGen; -use aptos_temppath::TempPath; -use aptos_types::{account_address::AccountAddress, chain_id::ChainId}; -use aptos_vm_genesis::{AccountBalance, TestValidator}; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - str::FromStr, -}; - -const INITIAL_BALANCE: u64 = 100_000_000_000_000; - -/// Test the E2E genesis flow since it doesn't require a node to run -#[tokio::test] -async fn test_genesis_e2e_flow() { - let is_mainnet = false; - let dir = TempPath::new(); - dir.create_as_dir().unwrap(); - let git_options = create_users(2, 0, &dir, &mut vec![], is_mainnet).await; - - // Now generate genesis - let output_dir = TempPath::new(); - output_dir.create_as_dir().unwrap(); - let output_dir = PathBuf::from(output_dir.path()); - generate_genesis(git_options, output_dir.clone(), is_mainnet).await; - - // TODO: Verify that these are good - let waypoint_file = output_dir.join("waypoint.txt"); - assert!(waypoint_file.exists()); - let genesis_file = output_dir.join("genesis.blob"); - assert!(genesis_file.exists()); -} - -#[tokio::test] -async fn test_mainnet_genesis_e2e_flow() { - let is_mainnet = true; - let dir = TempPath::new(); - dir.create_as_dir().unwrap(); - let git_options = create_users(2, 4, &dir, &mut vec![10, 1], is_mainnet).await; - let account_1 = AccountAddress::from_hex_literal("0x101").unwrap(); - let account_2 = AccountAddress::from_hex_literal("0x102").unwrap(); - let employee_1 = AccountAddress::from_hex_literal("0x201").unwrap(); - let employee_2 = AccountAddress::from_hex_literal("0x202").unwrap(); - let employee_3 = AccountAddress::from_hex_literal("0x203").unwrap(); - let employee_4 = AccountAddress::from_hex_literal("0x204").unwrap(); - - let owner_identity1 = load_identity(dir.path(), "owner-0"); - let owner_identity2 = load_identity(dir.path(), "owner-1"); - let operator_identity1 = load_identity(dir.path(), "operator-0"); - let operator_identity2 = load_identity(dir.path(), "operator-1"); - let voter_identity1 = load_identity(dir.path(), "voter-0"); - let voter_identity2 = load_identity(dir.path(), "voter-1"); - let admin_identity1 = load_identity(dir.path(), "other-0"); - let admin_identity2 = load_identity(dir.path(), "other-1"); - let employee_operator_identity1 = load_identity(dir.path(), "other-2"); - let employee_operator_identity2 = load_identity(dir.path(), "other-3"); - - // Create initial balances and employee vesting account files. - let git_dir = git_options.local_repository_dir.as_ref().unwrap().as_path(); - - create_account_balances_file(PathBuf::from(git_dir), vec![ - owner_identity1.account_address, - owner_identity2.account_address, - operator_identity1.account_address, - operator_identity2.account_address, - voter_identity1.account_address, - voter_identity2.account_address, - account_1, - account_2, - employee_1, - employee_2, - employee_3, - employee_4, - admin_identity1.account_address, - admin_identity2.account_address, - employee_operator_identity1.account_address, - employee_operator_identity2.account_address, - ]) - .await; - create_employee_vesting_accounts_file( - PathBuf::from(git_dir), - &[admin_identity1, admin_identity2], - &[employee_operator_identity1, employee_operator_identity2], - &[vec![employee_1, employee_2], vec![employee_3, employee_4]], - &[true, false], - ) - .await; - - // Now generate genesis - let output_dir = TempPath::new(); - output_dir.create_as_dir().unwrap(); - let output_dir = PathBuf::from(output_dir.path()); - generate_genesis(git_options, output_dir.clone(), is_mainnet).await; - - // TODO: Verify that these are good - let waypoint_file = output_dir.join("waypoint.txt"); - assert!(waypoint_file.exists()); - let genesis_file = output_dir.join("genesis.blob"); - assert!(genesis_file.exists()); -} - -pub fn load_identity(base_dir: &Path, name: &str) -> PublicIdentity { - let path = base_dir.join(name).join(PUBLIC_KEYS_FILE); - from_yaml(&String::from_utf8(read_from_file(path.as_path()).unwrap()).unwrap()).unwrap() -} - -async fn create_users( - num_validators: u8, - num_other_users: u8, - dir: &TempPath, - commission_rates: &mut Vec, - is_mainnet: bool, -) -> GitOptions { - let mut users: HashMap = HashMap::new(); - for i in 0..num_validators { - let name = format!("owner-{}", i); - let output_dir = generate_keys(dir.path(), &name).await; - users.insert(name, output_dir); - - let name = format!("operator-{}", i); - let output_dir = generate_keys(dir.path(), &name).await; - users.insert(name, output_dir); - - let name = format!("voter-{}", i); - let output_dir = generate_keys(dir.path(), &name).await; - users.insert(name, output_dir); - } - for i in 0..num_other_users { - let name = format!("other-{}", i); - let output_dir = generate_keys(dir.path(), &name).await; - users.insert(name, output_dir); - } - - // Get the validator's names - let validator_names = users - .keys() - .map(|key| key.to_string()) - .filter(|name| name.starts_with("owner")) - .collect(); - let mut key_gen = KeyGen::from_seed([num_validators.saturating_add(1); 32]); - - // First step is setup the local git repo - let root_private_key = if !is_mainnet { - Some(key_gen.generate_ed25519_private_key()) - } else { - None - }; - let git_options = - setup_git_dir(root_private_key.as_ref(), validator_names, ChainId::test()).await; - - // Only write validators to folders - for i in 0..num_validators { - let owner_name = format!("owner-{}", i); - let owner_identity = users.get(&owner_name).unwrap().join(PUBLIC_KEYS_FILE); - let operator_identity = users - .get(&format!("operator-{}", i)) - .unwrap() - .join(PUBLIC_KEYS_FILE); - let voter_identity = users - .get(&format!("voter-{}", i)) - .unwrap() - .join(PUBLIC_KEYS_FILE); - let commission_rate = if commission_rates.is_empty() { - 0 - } else { - commission_rates.remove(0) - }; - set_validator_config( - owner_name, - git_options.clone(), - owner_identity.as_path(), - operator_identity.as_path(), - voter_identity.as_path(), - commission_rate, - i as u16, - ) - .await; - } - git_options -} - -/// Generate genesis and waypoint -async fn generate_genesis(git_options: GitOptions, output_dir: PathBuf, mainnet: bool) { - let command = GenerateGenesis { - prompt_options: PromptOptions::yes(), - git_options, - output_dir: Some(output_dir), - mainnet, - }; - let _ = command.execute().await.unwrap(); -} - -/// Setup a temporary repo location and add all required pieces -async fn setup_git_dir( - root_private_key: Option<&Ed25519PrivateKey>, - users: Vec, - chain_id: ChainId, -) -> GitOptions { - let git_options = git_options(); - let layout_file = TempPath::new(); - layout_file.create_as_file().unwrap(); - let layout_file = layout_file.path(); - - create_layout_file( - layout_file, - root_private_key.map(|inner| inner.public_key()), - users, - chain_id, - ) - .await; - let setup_command = SetupGit { - git_options: git_options.clone(), - layout_file: PathBuf::from(layout_file), - }; - - setup_command - .execute() - .await - .expect("Should not fail creating repo folder"); - - // Add framework - add_framework_to_dir(git_options.local_repository_dir.as_ref().unwrap().as_path()); - git_options -} - -/// Add framework to git directory -fn add_framework_to_dir(git_dir: &Path) { - aptos_cached_packages::head_release_bundle() - .write(git_dir.join(FRAMEWORK_NAME)) - .unwrap() -} - -/// Local git options for testing -fn git_options() -> GitOptions { - let temp_path = TempPath::new(); - let path = PathBuf::from(temp_path.path()); - GitOptions { - local_repository_dir: Some(path), - ..Default::default() - } -} - -/// Create a layout file for the repo -async fn create_layout_file( - file: &Path, - root_public_key: Option, - users: Vec, - chain_id: ChainId, -) { - GenerateLayoutTemplate { - output_file: PathBuf::from(file), - prompt_options: PromptOptions::yes(), - } - .execute() - .await - .expect("Expected to create layout template"); - - // Update layout file - let mut layout: Layout = - from_yaml(&String::from_utf8(read_from_file(file).unwrap()).unwrap()).unwrap(); - layout.root_key = root_public_key; - layout.users = users; - layout.chain_id = chain_id; - layout.is_test = true; - layout.total_supply = Some(INITIAL_BALANCE * 16); - - write_to_file( - file, - "Layout file", - serde_yaml::to_string(&layout).unwrap().as_bytes(), - ) - .unwrap(); -} - -/// Generate keys for a "user" -async fn generate_keys(dir: &Path, name: &str) -> PathBuf { - let output_dir = dir.join(name); - let command = GenerateKeys { - pool_address_args: OptionalPoolAddressArgs { pool_address: None }, - rng_args: RngArgs::from_string_seed(name), - prompt_options: PromptOptions::yes(), - output_dir: Some(output_dir.clone()), - }; - let _ = command.execute().await.unwrap(); - output_dir -} - -/// Set validator configuration for a user -async fn set_validator_config( - username: String, - git_options: GitOptions, - owner_identity_file: &Path, - operator_identity_file: &Path, - voter_identity_file: &Path, - commission_percentage: u64, - port: u16, -) { - let command = SetValidatorConfiguration { - username, - git_options, - owner_public_identity_file: Some(owner_identity_file.to_path_buf()), - validator_host: HostAndPort::from_str(&format!("localhost:{}", port)).unwrap(), - stake_amount: 100_000_000_000_000, - full_node_host: None, - operator_public_identity_file: Some(operator_identity_file.to_path_buf()), - voter_public_identity_file: Some(voter_identity_file.to_path_buf()), - commission_percentage, - join_during_genesis: true, - }; - - command.execute().await.unwrap() -} - -async fn create_account_balances_file(path: PathBuf, addresses: Vec) { - let account_balances: Vec = addresses - .iter() - .map(|account_address| AccountBalance { - account_address: *account_address, - balance: INITIAL_BALANCE, - }) - .collect(); - - let balance_map = AccountBalanceMap::try_from(account_balances).unwrap(); - - write_to_file( - &path.join(BALANCES_FILE), - BALANCES_FILE, - serde_yaml::to_string(&balance_map).unwrap().as_bytes(), - ) - .unwrap(); -} - -async fn create_employee_vesting_accounts_file( - path: PathBuf, - admin_identities: &[PublicIdentity], - operator_identities: &[PublicIdentity], - employee_groups: &[Vec], - join_during_genesis: &[bool], -) { - TestValidator::new_test_set(Some(employee_groups.len()), Some(INITIAL_BALANCE)); - let employee_vesting_accounts: Vec<_> = employee_groups - .iter() - .enumerate() - .map(|(index, accounts)| { - let admin_identity = admin_identities[index].clone(); - let operator_identity = operator_identities[index].clone(); - let validator_config = if *join_during_genesis.get(index).unwrap() { - ValidatorConfiguration { - owner_account_address: admin_identity.account_address.into(), - owner_account_public_key: admin_identity.account_public_key.clone(), - operator_account_address: operator_identity.account_address.into(), - operator_account_public_key: operator_identity.account_public_key.clone(), - voter_account_address: admin_identity.account_address.into(), - voter_account_public_key: admin_identity.account_public_key, - consensus_public_key: operator_identity.consensus_public_key, - proof_of_possession: operator_identity.consensus_proof_of_possession, - validator_network_public_key: operator_identity.validator_network_public_key, - validator_host: Some(HostAndPort::from_str("localhost:8080").unwrap()), - full_node_network_public_key: operator_identity.full_node_network_public_key, - full_node_host: Some(HostAndPort::from_str("localhost:8081").unwrap()), - stake_amount: 2 * INITIAL_BALANCE, - commission_percentage: 0, - join_during_genesis: true, - } - } else { - ValidatorConfiguration { - owner_account_address: admin_identity.account_address.into(), - owner_account_public_key: admin_identity.account_public_key.clone(), - operator_account_address: operator_identity.account_address.into(), - operator_account_public_key: operator_identity.account_public_key, - voter_account_address: admin_identity.account_address.into(), - voter_account_public_key: admin_identity.account_public_key, - consensus_public_key: None, - proof_of_possession: None, - validator_network_public_key: None, - validator_host: None, - full_node_network_public_key: None, - full_node_host: None, - stake_amount: 2 * INITIAL_BALANCE, - commission_percentage: 0, - join_during_genesis: false, - } - }; - - EmployeePoolConfig { - accounts: accounts.iter().map(|addr| addr.into()).collect(), - validator: validator_config, - vesting_schedule_numerators: vec![3, 3, 3, 3, 1], - vesting_schedule_denominator: 48, - beneficiary_resetter: AccountAddress::from_hex_literal("0x101").unwrap().into(), - } - }) - .collect(); - let employee_vesting_map = EmployeePoolMap { - inner: employee_vesting_accounts, - }; - write_to_file( - &path.join(EMPLOYEE_VESTING_ACCOUNTS_FILE), - EMPLOYEE_VESTING_ACCOUNTS_FILE, - serde_yaml::to_string(&employee_vesting_map) - .unwrap() - .as_bytes(), - ) - .unwrap(); -} diff --git a/m1/m1-cli/src/genesis/tools.rs b/m1/m1-cli/src/genesis/tools.rs deleted file mode 100644 index 5f3f8ed5..00000000 --- a/m1/m1-cli/src/genesis/tools.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::{ - types::PromptOptions, - utils::{dir_default_to_current, write_to_file}, - }, - genesis::{ - get_validator_configs, - git::{GitOptions, EMPLOYEE_VESTING_ACCOUNTS_FILE, LAYOUT_FILE}, - parse_error, - }, - CliCommand, CliTypedResult, -}; -use aptos_genesis::config::{EmployeePoolMap, Layout}; -use aptos_sdk::move_types::account_address::AccountAddress; -use aptos_types::account_address::{create_vesting_pool_address, default_stake_pool_address}; -use async_trait::async_trait; -use clap::Parser; -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, -}; - -const POOL_ADDRESSES: &str = "pool-addresses.yaml"; -const EMPLOYEE_POOL_ADDRESSES: &str = "employee-pool-addresses.yaml"; - -/// Get pool addresses from a mainnet genesis setup -/// -/// Outputs all pool addresses to a file from the genesis files -#[derive(Parser)] -pub struct PoolAddresses { - /// Output directory for pool addresses - #[clap(long, parse(from_os_str))] - output_dir: Option, - - #[clap(flatten)] - prompt_options: PromptOptions, - #[clap(flatten)] - git_options: GitOptions, -} - -#[async_trait] -impl CliCommand> for PoolAddresses { - fn command_name(&self) -> &'static str { - "GetPoolAddresses" - } - - async fn execute(self) -> CliTypedResult> { - let output_dir = dir_default_to_current(self.output_dir.clone())?; - let client = self.git_options.get_client()?; - let layout: Layout = client.get(Path::new(LAYOUT_FILE))?; - let employee_vesting_accounts: EmployeePoolMap = - client.get(Path::new(EMPLOYEE_VESTING_ACCOUNTS_FILE))?; - let validators = get_validator_configs(&client, &layout, true).map_err(parse_error)?; - - let mut address_to_pool = BTreeMap::::new(); - - for validator in validators { - let stake_pool_address = default_stake_pool_address( - validator.owner_account_address.into(), - validator.operator_account_address.into(), - ); - address_to_pool.insert(validator.owner_account_address.into(), stake_pool_address); - } - - let mut employee_address_to_pool = BTreeMap::::new(); - - for employee_pool in employee_vesting_accounts.inner.iter() { - let stake_pool_address = create_vesting_pool_address( - employee_pool.validator.owner_account_address.into(), - employee_pool.validator.operator_account_address.into(), - 0, - &[], - ); - - employee_address_to_pool.insert( - employee_pool.validator.owner_account_address.into(), - stake_pool_address, - ); - } - - let pool_addresses_file = output_dir.join(POOL_ADDRESSES); - let employee_pool_addresses_file = output_dir.join(EMPLOYEE_POOL_ADDRESSES); - - write_to_file( - pool_addresses_file.as_path(), - POOL_ADDRESSES, - serde_yaml::to_string(&address_to_pool)?.as_bytes(), - )?; - write_to_file( - employee_pool_addresses_file.as_path(), - EMPLOYEE_POOL_ADDRESSES, - serde_yaml::to_string(&employee_address_to_pool)?.as_bytes(), - )?; - - Ok(vec![pool_addresses_file, employee_pool_addresses_file]) - } -} diff --git a/m1/m1-cli/src/governance/mod.rs b/m1/m1-cli/src/governance/mod.rs deleted file mode 100644 index b399696a..00000000 --- a/m1/m1-cli/src/governance/mod.rs +++ /dev/null @@ -1,1061 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(feature = "no-upload-proposal")] -use crate::common::utils::read_from_file; -use crate::{ - common::{ - types::{ - CliError, CliTypedResult, MovePackageDir, PoolAddressArgs, ProfileOptions, - PromptOptions, RestOptions, TransactionOptions, TransactionSummary, - }, - utils::prompt_yes_with_override, - }, - move_tool::{FrameworkPackageArgs, IncludedArtifacts}, - CliCommand, CliResult, -}; -use aptos_cached_packages::aptos_stdlib; -use aptos_crypto::HashValue; -use aptos_framework::{BuildOptions, BuiltPackage, ReleasePackage}; -use aptos_logger::warn; -use aptos_rest_client::{ - aptos_api_types::{Address, HexEncodedBytes, U128, U64}, - Client, Transaction, -}; -use aptos_sdk::move_types::language_storage::CORE_CODE_ADDRESS; -use aptos_types::{ - account_address::AccountAddress, - event::EventHandle, - governance::VotingRecords, - stake_pool::StakePool, - state_store::table::TableHandle, - transaction::{Script, TransactionPayload}, -}; -use async_trait::async_trait; -use clap::Parser; -use move_core_types::transaction_argument::TransactionArgument; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use std::{ - collections::BTreeMap, - fmt::Formatter, - fs, - path::{Path, PathBuf}, -}; -use tempfile::TempDir; - -/// Tool for on-chain governance -/// -/// This tool allows voters that have stake to vote the ability to -/// propose changes to the chain, as well as vote and execute these -/// proposals. -#[derive(Parser)] -pub enum GovernanceTool { - Propose(SubmitProposal), - Vote(SubmitVote), - ShowProposal(ViewProposal), - ListProposals(ListProposals), - VerifyProposal(VerifyProposal), - ExecuteProposal(ExecuteProposal), - GenerateUpgradeProposal(GenerateUpgradeProposal), - ApproveExecutionHash(ApproveExecutionHash), -} - -impl GovernanceTool { - pub async fn execute(self) -> CliResult { - use GovernanceTool::*; - match self { - Propose(tool) => tool.execute_serialized().await, - Vote(tool) => tool.execute_serialized().await, - ExecuteProposal(tool) => tool.execute_serialized().await, - GenerateUpgradeProposal(tool) => tool.execute_serialized_success().await, - ShowProposal(tool) => tool.execute_serialized().await, - ListProposals(tool) => tool.execute_serialized().await, - VerifyProposal(tool) => tool.execute_serialized().await, - ApproveExecutionHash(tool) => tool.execute_serialized().await, - } - } -} - -/// View a known on-chain governance proposal -/// -/// This command will return the proposal requested as well as compute -/// the hash of the metadata to determine whether it was verified or not. -#[derive(Parser)] -pub struct ViewProposal { - /// The identifier of the onchain governance proposal - #[clap(long)] - proposal_id: u64, - - #[clap(flatten)] - rest_options: RestOptions, - #[clap(flatten)] - profile: ProfileOptions, -} - -#[async_trait] -impl CliCommand for ViewProposal { - fn command_name(&self) -> &'static str { - "ViewProposal" - } - - async fn execute(mut self) -> CliTypedResult { - // Get proposal - let client = self.rest_options.client(&self.profile)?; - let forum = client - .get_account_resource_bcs::( - AccountAddress::ONE, - "0x1::voting::VotingForum<0x1::governance_proposal::GovernanceProposal>", - ) - .await? - .into_inner(); - let voting_table = forum.table_handle.0; - - let proposal: Proposal = get_proposal(&client, voting_table, self.proposal_id) - .await? - .into(); - - let metadata_hash = proposal.metadata.get("metadata_hash").unwrap(); - let metadata_url = proposal.metadata.get("metadata_location").unwrap(); - - // Compute the hash and verify accordingly - let mut metadata_verified = false; - let mut actual_metadata_hash = "Unable to fetch metadata url".to_string(); - let mut actual_metadata = None; - if let Ok(url) = Url::parse(metadata_url) { - if let Ok(bytes) = get_metadata_from_url(&url).await { - let hash = HashValue::sha3_256_of(&bytes); - metadata_verified = metadata_hash == &hash.to_hex(); - actual_metadata_hash = hash.to_hex(); - if let Ok(metadata) = String::from_utf8(bytes) { - actual_metadata = Some(metadata); - } - } - } - - Ok(VerifiedProposal { - metadata_verified, - actual_metadata_hash, - actual_metadata, - proposal, - }) - } -} - -/// List the last 100 visible onchain proposals -/// -/// Note, if the full node you are talking to is pruning data, it may not have some of the -/// proposals show here -#[derive(Parser)] -pub struct ListProposals { - #[clap(flatten)] - rest_options: RestOptions, - #[clap(flatten)] - profile: ProfileOptions, -} - -#[async_trait] -impl CliCommand> for ListProposals { - fn command_name(&self) -> &'static str { - "ListProposals" - } - - async fn execute(mut self) -> CliTypedResult> { - // List out known proposals based on events - let client = self.rest_options.client(&self.profile)?; - - let events = client - .get_account_events_bcs( - AccountAddress::ONE, - "0x1::aptos_governance::GovernanceEvents", - "create_proposal_events", - None, - Some(100), - ) - .await? - .into_inner(); - let mut proposals = vec![]; - - for event in &events { - match bcs::from_bytes::(event.event.event_data()) { - Ok(valid_event) => proposals.push(valid_event.into()), - Err(err) => { - eprintln!( - "Event: {:?} cannot be parsed as a proposal: {:?}", - event, err - ) - }, - } - } - - // TODO: Show more information about proposal? - Ok(proposals) - } -} - -/// Verify a proposal given the source code of the script -/// -/// The script's bytecode or source can be provided and it will -/// verify whether the hash matches the onchain hash -#[derive(Parser)] -pub struct VerifyProposal { - /// The id of the onchain proposal - #[clap(long)] - pub(crate) proposal_id: u64, - - #[clap(flatten)] - pub(crate) compile_proposal_args: CompileScriptFunction, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile: ProfileOptions, - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, -} - -#[async_trait] -impl CliCommand for VerifyProposal { - fn command_name(&self) -> &'static str { - "VerifyProposal" - } - - async fn execute(mut self) -> CliTypedResult { - // Compile local first to get the hash - let (_, hash) = self - .compile_proposal_args - .compile("SubmitProposal", self.prompt_options)?; - - // Retrieve the onchain proposal - let client = self.rest_options.client(&self.profile)?; - let forum = client - .get_account_resource_bcs::( - AccountAddress::ONE, - "0x1::voting::VotingForum<0x1::governance_proposal::GovernanceProposal>", - ) - .await? - .into_inner(); - let voting_table = forum.table_handle.0; - - let proposal: Proposal = get_proposal(&client, voting_table, self.proposal_id) - .await? - .into(); - - // Compare the hashes - let computed_hash = hash.to_hex(); - let onchain_hash = proposal.execution_hash; - - Ok(VerifyProposalResponse { - verified: computed_hash == onchain_hash, - computed_hash, - onchain_hash, - }) - } -} - -async fn get_proposal( - client: &aptos_rest_client::Client, - voting_table: AccountAddress, - proposal_id: u64, -) -> CliTypedResult { - let json = client - .get_table_item( - voting_table, - "u64", - "0x1::voting::Proposal<0x1::governance_proposal::GovernanceProposal>", - format!("{}", proposal_id), - ) - .await? - .into_inner(); - serde_json::from_value(json) - .map_err(|err| CliError::CommandArgumentError(format!("Failed to parse proposal {}", err))) -} - -/// Submit a governance proposal -#[derive(Parser)] -pub struct SubmitProposal { - /// Location of the JSON metadata of the proposal - /// - /// If this location does not keep the metadata in the exact format, it will be less likely - /// that voters will approve this proposal, as they won't be able to verify it. - #[clap(long)] - pub(crate) metadata_url: Url, - - #[cfg(feature = "no-upload-proposal")] - /// A JSON file to be uploaded later at the metadata URL - /// - /// If this does not match properly, voters may choose to vote no. For real proposals, - /// it is better to already have it uploaded at the URL. - #[clap(long)] - pub(crate) metadata_path: Option, - - #[clap(long)] - pub(crate) is_multi_step: bool, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) pool_address_args: PoolAddressArgs, - #[clap(flatten)] - pub(crate) compile_proposal_args: CompileScriptFunction, -} - -#[async_trait] -impl CliCommand for SubmitProposal { - fn command_name(&self) -> &'static str { - "SubmitProposal" - } - - async fn execute(mut self) -> CliTypedResult { - let (_bytecode, script_hash) = self - .compile_proposal_args - .compile("SubmitProposal", self.txn_options.prompt_options)?; - - // Validate the proposal metadata - let (metadata, metadata_hash) = self.get_metadata().await?; - - println!( - "{}\n\tMetadata Hash: {}\n\tScript Hash: {}", - metadata, metadata_hash, script_hash - ); - prompt_yes_with_override( - "Do you want to submit this proposal?", - self.txn_options.prompt_options, - )?; - - let txn: Transaction = if self.is_multi_step { - self.txn_options - .submit_transaction(aptos_stdlib::aptos_governance_create_proposal_v2( - self.pool_address_args.pool_address, - script_hash.to_vec(), - self.metadata_url.to_string().as_bytes().to_vec(), - metadata_hash.to_hex().as_bytes().to_vec(), - true, - )) - .await? - } else { - self.txn_options - .submit_transaction(aptos_stdlib::aptos_governance_create_proposal( - self.pool_address_args.pool_address, - script_hash.to_vec(), - self.metadata_url.to_string().as_bytes().to_vec(), - metadata_hash.to_hex().as_bytes().to_vec(), - )) - .await? - }; - let txn_summary = TransactionSummary::from(&txn); - if let Transaction::UserTransaction(inner) = txn { - // Find event with proposal id - let proposal_id = if let Some(event) = inner.events.into_iter().find(|event| { - event.typ.to_string().as_str() == "0x1::aptos_governance::CreateProposalEvent" - }) { - let data: CreateProposalEvent = - serde_json::from_value(event.data).map_err(|_| { - CliError::UnexpectedError( - "Failed to parse Proposal event to get ProposalId".to_string(), - ) - })?; - Some(data.proposal_id.0) - } else { - warn!("No proposal event found to find proposal id"); - None - }; - - return Ok(ProposalSubmissionSummary { - proposal_id, - transaction: txn_summary, - }); - } - Err(CliError::UnexpectedError( - "Unable to find parse proposal transaction output".to_string(), - )) - } -} - -impl SubmitProposal { - /// Retrieve metadata and validate it - async fn get_metadata(&self) -> CliTypedResult<(ProposalMetadata, HashValue)> { - #[cfg(feature = "no-upload-proposal")] - let bytes = if let Some(ref path) = self.metadata_path { - read_from_file(path)? - } else { - get_metadata_from_url(&self.metadata_url).await? - }; - #[cfg(not(feature = "no-upload-proposal"))] - let bytes = get_metadata_from_url(&self.metadata_url).await?; - - let metadata: ProposalMetadata = serde_json::from_slice(&bytes).map_err(|err| { - CliError::CommandArgumentError(format!( - "Metadata is not in a proper JSON format: {}", - err - )) - })?; - Url::parse(&metadata.source_code_url).map_err(|err| { - CliError::CommandArgumentError(format!( - "Source code URL {} is invalid {}", - metadata.source_code_url, err - )) - })?; - Url::parse(&metadata.discussion_url).map_err(|err| { - CliError::CommandArgumentError(format!( - "Discussion URL {} is invalid {}", - metadata.discussion_url, err - )) - })?; - let metadata_hash = HashValue::sha3_256_of(&bytes); - Ok((metadata, metadata_hash)) - } -} - -/// Retrieve the Metadata from the given URL -async fn get_metadata_from_url(metadata_url: &Url) -> CliTypedResult> { - let client = reqwest::ClientBuilder::default() - .tls_built_in_root_certs(true) - .build() - .map_err(|err| CliError::UnexpectedError(format!("Failed to build HTTP client {}", err)))?; - client - .get(metadata_url.clone()) - .send() - .await - .map_err(|err| { - CliError::CommandArgumentError(format!( - "Failed to fetch metadata url {}: {}", - metadata_url, err - )) - })? - .bytes() - .await - .map(|b| b.to_vec()) - .map_err(|err| { - CliError::CommandArgumentError(format!( - "Failed to fetch metadata url {}: {}", - metadata_url, err - )) - }) -} - -#[derive(Debug, Deserialize, Serialize)] -struct CreateProposalEvent { - proposal_id: U64, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct ProposalSubmissionSummary { - proposal_id: Option, - #[serde(flatten)] - transaction: TransactionSummary, -} - -/// Submit a vote on a proposal -/// -/// Votes can only be given on proposals that are currently open for voting. You can vote -/// with `--yes` for a yes vote, and `--no` for a no vote. -#[derive(Parser)] -pub struct SubmitVote { - /// Id of the proposal to vote on - #[clap(long)] - pub(crate) proposal_id: u64, - - /// Vote to accept the proposal - #[clap(long, group = "vote")] - pub(crate) yes: bool, - - /// Vote to reject the proposal - #[clap(long, group = "vote")] - pub(crate) no: bool, - - /// Space separated list of pool addresses. - #[clap(long, multiple_values = true, parse(try_from_str=crate::common::types::load_account_arg))] - pub(crate) pool_addresses: Vec, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand> for SubmitVote { - fn command_name(&self) -> &'static str { - "SubmitVote" - } - - async fn execute(mut self) -> CliTypedResult> { - let (vote_str, vote) = match (self.yes, self.no) { - (true, false) => ("Yes", true), - (false, true) => ("No", false), - (_, _) => { - return Err(CliError::CommandArgumentError( - "Must choose only either --yes or --no".to_string(), - )); - }, - }; - - let client: &Client = &self - .txn_options - .rest_options - .client(&self.txn_options.profile_options)?; - let proposal_id = self.proposal_id; - let voting_records = client - .get_account_resource_bcs::( - CORE_CODE_ADDRESS, - "0x1::aptos_governance::VotingRecords", - ) - .await - .unwrap() - .into_inner() - .votes; - - let mut summaries: Vec = vec![]; - for pool_address in self.pool_addresses { - let voting_record = client - .get_table_item( - voting_records, - "0x1::aptos_governance::RecordKey", - "bool", - VotingRecord { - proposal_id: proposal_id.to_string(), - stake_pool: pool_address, - }, - ) - .await; - let voted = if let Ok(voting_record) = voting_record { - voting_record.into_inner().as_bool().unwrap() - } else { - false - }; - if voted { - println!("Stake pool {} already voted", pool_address); - continue; - } - - let stake_pool = client - .get_account_resource_bcs::(pool_address, "0x1::stake::StakePool") - .await? - .into_inner(); - let voting_power = stake_pool.get_governance_voting_power(); - - prompt_yes_with_override( - &format!( - "Vote {} with voting power = {} from stake pool {}?", - vote_str, voting_power, pool_address - ), - self.txn_options.prompt_options, - )?; - - summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::aptos_governance_vote( - pool_address, - proposal_id, - vote, - )) - .await - .map(TransactionSummary::from)?, - ); - } - Ok(summaries) - } -} - -/// Submit a transaction to approve a proposal's script hash to bypass the transaction size limit. -/// This is needed for upgrading large packages such as aptos-framework. -#[derive(Parser)] -pub struct ApproveExecutionHash { - /// Id of the proposal to vote on - #[clap(long)] - pub(crate) proposal_id: u64, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for ApproveExecutionHash { - fn command_name(&self) -> &'static str { - "ApproveExecutionHash" - } - - async fn execute(mut self) -> CliTypedResult { - Ok(self - .txn_options - .submit_transaction( - aptos_stdlib::aptos_governance_add_approved_script_hash_script(self.proposal_id), - ) - .await - .map(TransactionSummary::from)?) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct VotingRecord { - proposal_id: String, - stake_pool: AccountAddress, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ProposalMetadata { - title: String, - description: String, - source_code_url: String, - discussion_url: String, -} - -impl std::fmt::Display for ProposalMetadata { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Proposal:\n\tTitle:{}\n\tDescription:{}\n\tSource code URL:{}\n\tDiscussion URL:{}", - self.title, self.description, self.source_code_url, self.discussion_url - ) - } -} - -fn compile_in_temp_dir( - script_name: &str, - script_path: &Path, - framework_package_args: &FrameworkPackageArgs, - prompt_options: PromptOptions, - bytecode_version: Option, -) -> CliTypedResult<(Vec, HashValue)> { - // Make a temporary directory for compilation - let temp_dir = TempDir::new().map_err(|err| { - CliError::UnexpectedError(format!("Failed to create temporary directory {}", err)) - })?; - - // Initialize a move directory - let package_dir = temp_dir.path(); - framework_package_args.init_move_dir( - package_dir, - script_name, - BTreeMap::new(), - prompt_options, - )?; - - // Insert the new script - let sources_dir = package_dir.join("sources"); - let new_script_path = if let Some(file_name) = script_path.file_name() { - sources_dir.join(file_name) - } else { - // If for some reason we can't get the move file - sources_dir.join("script.move") - }; - fs::copy(script_path, new_script_path.as_path()).map_err(|err| { - CliError::IO( - format!( - "Failed to copy {} to {}", - script_path.display(), - new_script_path.display() - ), - err, - ) - })?; - - // Compile the script - compile_script( - framework_package_args.skip_fetch_latest_git_deps, - package_dir, - bytecode_version, - ) -} - -fn compile_script( - skip_fetch_latest_git_deps: bool, - package_dir: &Path, - bytecode_version: Option, -) -> CliTypedResult<(Vec, HashValue)> { - let build_options = BuildOptions { - with_srcs: false, - with_abis: false, - with_source_maps: false, - with_error_map: false, - skip_fetch_latest_git_deps, - bytecode_version, - ..BuildOptions::default() - }; - - let pack = BuiltPackage::build(package_dir.to_path_buf(), build_options) - .map_err(|e| CliError::MoveCompilationError(format!("{:#}", e)))?; - - let scripts_count = pack.script_count(); - - if scripts_count != 1 { - return Err(CliError::UnexpectedError(format!( - "Only one script can be prepared a time. Make sure one and only one script file \ - is included in the Move package. Found {} scripts.", - scripts_count - ))); - } - - let bytes = pack.extract_script_code().pop().unwrap(); - let hash = HashValue::sha3_256_of(bytes.as_slice()); - Ok((bytes, hash)) -} - -/// Execute a proposal that has passed voting requirements -#[derive(Parser)] -pub struct ExecuteProposal { - /// Proposal Id being executed - #[clap(long)] - pub(crate) proposal_id: u64, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) compile_proposal_args: CompileScriptFunction, -} - -#[async_trait] -impl CliCommand for ExecuteProposal { - fn command_name(&self) -> &'static str { - "ExecuteProposal" - } - - async fn execute(mut self) -> CliTypedResult { - let (bytecode, _script_hash) = self - .compile_proposal_args - .compile("ExecuteProposal", self.txn_options.prompt_options)?; - // TODO: Check hash so we don't do a failed roundtrip? - - let args = vec![TransactionArgument::U64(self.proposal_id)]; - let txn = TransactionPayload::Script(Script::new(bytecode, vec![], args)); - - self.txn_options - .submit_transaction(txn) - .await - .map(TransactionSummary::from) - } -} - -/// Compile a specified script. -#[derive(Parser)] -pub struct CompileScriptFunction { - /// Path to the Move script for the proposal - #[clap(long, group = "script", parse(from_os_str))] - pub script_path: Option, - - /// Path to the Move script for the proposal - #[clap(long, group = "script", parse(from_os_str))] - pub compiled_script_path: Option, - - #[clap(flatten)] - pub(crate) framework_package_args: FrameworkPackageArgs, - - #[clap(long)] - pub(crate) bytecode_version: Option, -} - -impl CompileScriptFunction { - pub(crate) fn compile( - &self, - script_name: &str, - prompt_options: PromptOptions, - ) -> CliTypedResult<(Vec, HashValue)> { - if let Some(compiled_script_path) = &self.compiled_script_path { - let bytes = std::fs::read(compiled_script_path).map_err(|e| { - CliError::IO(format!("Unable to read {:?}", self.compiled_script_path), e) - })?; - let hash = HashValue::sha3_256_of(bytes.as_slice()); - return Ok((bytes, hash)); - } - - // Check script file - let script_path = self - .script_path - .as_ref() - .ok_or_else(|| { - CliError::CommandArgumentError( - "Must choose either --compiled-script-path or --script-path".to_string(), - ) - })? - .as_path(); - if !script_path.exists() { - return Err(CliError::CommandArgumentError(format!( - "{} does not exist", - script_path.display() - ))); - } else if script_path.is_dir() { - return Err(CliError::CommandArgumentError(format!( - "{} is a directory", - script_path.display() - ))); - } - - // Compile script - compile_in_temp_dir( - script_name, - script_path, - &self.framework_package_args, - prompt_options, - self.bytecode_version, - ) - } -} - -/// Generates a package upgrade proposal script. -#[derive(Parser)] -pub struct GenerateUpgradeProposal { - /// Address of the account which the proposal addresses. - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) account: AccountAddress, - - /// Where to store the generated proposal - #[clap(long, parse(from_os_str), default_value = "proposal.move")] - pub(crate) output: PathBuf, - - /// What artifacts to include in the package. This can be one of `none`, `sparse`, and - /// `all`. `none` is the most compact form and does not allow to reconstruct a source - /// package from chain; `sparse` is the minimal set of artifacts needed to reconstruct - /// a source package; `all` includes all available artifacts. The choice of included - /// artifacts heavily influences the size and therefore gas cost of publishing: `none` - /// is the size of bytecode alone; `sparse` is roughly 2 times as much; and `all` 3-4 - /// as much. - #[clap(long, default_value_t = IncludedArtifacts::Sparse)] - pub(crate) included_artifacts: IncludedArtifacts, - - /// Generate the script for mainnet governance proposal by default or generate the upgrade script for testnet. - #[clap(long)] - pub(crate) testnet: bool, - - #[clap(long, default_value = "")] - pub(crate) next_execution_hash: String, - - #[clap(flatten)] - pub(crate) move_options: MovePackageDir, -} - -#[async_trait] -impl CliCommand<()> for GenerateUpgradeProposal { - fn command_name(&self) -> &'static str { - "GenerateUpgradeProposal" - } - - async fn execute(self) -> CliTypedResult<()> { - let GenerateUpgradeProposal { - move_options, - account, - included_artifacts, - output, - testnet, - next_execution_hash, - } = self; - let package_path = move_options.get_package_path()?; - let options = included_artifacts.build_options( - move_options.skip_fetch_latest_git_deps, - move_options.named_addresses(), - move_options.bytecode_version, - ); - let package = BuiltPackage::build(package_path, options)?; - let release = ReleasePackage::new(package)?; - - // If we're generating a single-step proposal on testnet - if testnet && next_execution_hash.is_empty() { - release.generate_script_proposal_testnet(account, output)?; - // If we're generating a single-step proposal on mainnet - } else if next_execution_hash.is_empty() { - release.generate_script_proposal(account, output)?; - // If we're generating a multi-step proposal - } else { - let next_execution_hash_bytes = hex::decode(next_execution_hash)?; - release.generate_script_proposal_multi_step( - account, - output, - next_execution_hash_bytes, - )?; - }; - Ok(()) - } -} - -/// Generate execution hash for a specified script. -#[derive(Parser)] -pub struct GenerateExecutionHash { - #[clap(long)] - pub script_path: Option, -} - -impl GenerateExecutionHash { - pub fn generate_hash(&self) -> CliTypedResult<(Vec, HashValue)> { - CompileScriptFunction { - script_path: self.script_path.clone(), - compiled_script_path: None, - framework_package_args: FrameworkPackageArgs { - framework_git_rev: None, - framework_local_dir: Option::from({ - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.pop(); - path.pop(); - path.join("aptos-move") - .join("framework") - .join("aptos-framework") - .canonicalize() - .map_err(|err| { - CliError::IO( - format!("Failed to canonicalize aptos framework path: {:?}", path), - err, - ) - })? - }), - skip_fetch_latest_git_deps: false, - }, - bytecode_version: None, - } - .compile("execution_hash", PromptOptions::yes()) - } -} - -/// Response for `verify proposal` -#[derive(Serialize, Deserialize, Debug)] -pub struct VerifyProposalResponse { - pub verified: bool, - pub computed_hash: String, - pub onchain_hash: String, -} - -/// Voting forum onchain type -/// -/// TODO: Move to a shared location -#[derive(Serialize, Deserialize, Debug)] -pub struct VotingForum { - table_handle: TableHandle, - events: VotingEvents, - next_proposal_id: u64, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct VotingEvents { - create_proposal_events: EventHandle, - register_forum_events: EventHandle, - resolve_proposal_events: EventHandle, - vote_events: EventHandle, -} - -/// Summary of proposal from the listing events for `ListProposals` -#[derive(Serialize, Deserialize, Debug)] -struct ProposalSummary { - proposer: AccountAddress, - stake_pool: AccountAddress, - proposal_id: u64, - execution_hash: String, - proposal_metadata: BTreeMap, -} - -impl From for ProposalSummary { - fn from(event: CreateProposalFullEvent) -> Self { - let proposal_metadata = event - .proposal_metadata - .into_iter() - .map(|(key, value)| (key, String::from_utf8(value).unwrap())) - .collect(); - ProposalSummary { - proposer: event.proposer, - stake_pool: event.stake_pool, - proposal_id: event.proposal_id, - execution_hash: hex::encode(event.execution_hash), - proposal_metadata, - } - } -} - -#[derive(Deserialize)] -struct CreateProposalFullEvent { - proposer: AccountAddress, - stake_pool: AccountAddress, - proposal_id: u64, - execution_hash: Vec, - proposal_metadata: Vec<(String, Vec)>, -} - -/// A proposal and the verified information about it -#[derive(Serialize, Deserialize, Debug)] -pub struct VerifiedProposal { - metadata_verified: bool, - actual_metadata_hash: String, - actual_metadata: Option, - proposal: Proposal, -} - -/// A reformatted type that has human readable version of the proposal onchain -#[derive(Serialize, Deserialize, Debug)] -pub struct Proposal { - proposer: AccountAddress, - metadata: BTreeMap, - creation_time_secs: u64, - execution_hash: String, - min_vote_threshold: u128, - expiration_secs: u64, - early_resolution_vote_threshold: Option, - yes_votes: u128, - no_votes: u128, - is_resolved: bool, - resolution_time_secs: u64, -} - -impl From for Proposal { - fn from(proposal: JsonProposal) -> Self { - let metadata = proposal - .metadata - .data - .into_iter() - .map(|pair| { - let value = match pair.key.as_str() { - "metadata_hash" => String::from_utf8(pair.value.0) - .unwrap_or_else(|_| "Failed to parse utf8".to_string()), - "metadata_location" => String::from_utf8(pair.value.0) - .unwrap_or_else(|_| "Failed to parse utf8".to_string()), - "RESOLVABLE_TIME_METADATA_KEY" => bcs::from_bytes::(pair.value.inner()) - .map(|inner| inner.to_string()) - .unwrap_or_else(|_| "Failed to parse u64".to_string()), - _ => pair.value.to_string(), - }; - (pair.key, value) - }) - .collect(); - - Proposal { - proposer: proposal.proposer.into(), - metadata, - creation_time_secs: proposal.creation_time_secs.into(), - execution_hash: format!("{:x}", proposal.execution_hash), - min_vote_threshold: proposal.min_vote_threshold.into(), - expiration_secs: proposal.expiration_secs.into(), - early_resolution_vote_threshold: proposal - .early_resolution_vote_threshold - .vec - .first() - .map(|inner| inner.0), - yes_votes: proposal.yes_votes.into(), - no_votes: proposal.no_votes.into(), - is_resolved: proposal.is_resolved, - resolution_time_secs: proposal.resolution_time_secs.into(), - } - } -} - -/// An ugly JSON parsing version for from the JSON API -#[derive(Serialize, Deserialize, Debug)] -struct JsonProposal { - creation_time_secs: U64, - early_resolution_vote_threshold: JsonEarlyResolutionThreshold, - execution_hash: aptos_rest_client::aptos_api_types::HashValue, - expiration_secs: U64, - is_resolved: bool, - min_vote_threshold: U128, - no_votes: U128, - resolution_time_secs: U64, - yes_votes: U128, - proposer: Address, - metadata: JsonMetadata, -} - -#[derive(Serialize, Deserialize, Debug)] -struct JsonEarlyResolutionThreshold { - vec: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -struct JsonMetadata { - data: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -struct JsonMetadataPair { - key: String, - value: HexEncodedBytes, -} diff --git a/m1/m1-cli/src/lib.rs b/m1/m1-cli/src/lib.rs deleted file mode 100644 index b8475e28..00000000 --- a/m1/m1-cli/src/lib.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -#![deny(unsafe_code)] - -pub mod account; -pub mod common; -pub mod config; -pub mod ffi; -pub mod genesis; -pub mod governance; -pub mod move_tool; -pub mod node; -pub mod op; -pub mod stake; -#[cfg(any(test, feature = "fuzzing"))] -pub mod test; -pub mod update; -pub mod faucet; - -use crate::common::{ - types::{CliCommand, CliResult, CliTypedResult}, - utils::cli_build_information, -}; -use async_trait::async_trait; -use clap::Parser; -use std::collections::BTreeMap; - -/// Command Line Interface (CLI) for developing and interacting with the Aptos blockchain -#[derive(Parser)] -#[clap(name = "aptos", author, version, propagate_version = true)] -pub enum Tool { - #[clap(subcommand)] - Account(account::AccountTool), - #[clap(subcommand)] - Config(config::ConfigTool), - #[clap(subcommand)] - Genesis(genesis::GenesisTool), - #[clap(subcommand)] - Governance(governance::GovernanceTool), - Info(InfoTool), - Init(common::init::InitTool), - #[clap(subcommand)] - Key(op::key::KeyTool), - #[clap(subcommand)] - Move(move_tool::MoveTool), - #[clap(subcommand)] - Multisig(account::MultisigAccountTool), - #[clap(subcommand)] - Node(node::NodeTool), - #[clap(subcommand)] - Stake(stake::StakeTool), - Update(update::UpdateTool), - Faucet(faucet::FaucetTool), -} - -impl Tool { - pub async fn execute(self) -> CliResult { - use Tool::*; - match self { - Account(tool) => tool.execute().await, - Config(tool) => tool.execute().await, - Genesis(tool) => tool.execute().await, - Governance(tool) => tool.execute().await, - Info(tool) => tool.execute_serialized().await, - // TODO: Replace entirely with config init - Init(tool) => tool.execute_serialized_success().await, - Key(tool) => tool.execute().await, - Move(tool) => tool.execute().await, - Multisig(tool) => tool.execute().await, - Node(tool) => tool.execute().await, - Stake(tool) => tool.execute().await, - Update(tool) => tool.execute_serialized().await, - Faucet(tool) => tool.execute_serialized().await, - } - } -} - -/// Show build information about the CLI -/// -/// This is useful for debugging as well as determining what versions are compatible with the CLI -#[derive(Parser)] -pub struct InfoTool {} - -#[async_trait] -impl CliCommand> for InfoTool { - fn command_name(&self) -> &'static str { - "GetCLIInfo" - } - - async fn execute(self) -> CliTypedResult> { - Ok(cli_build_information()) - } -} diff --git a/m1/m1-cli/src/main.rs b/m1/m1-cli/src/main.rs deleted file mode 100644 index 4fff4f71..00000000 --- a/m1/m1-cli/src/main.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -//! Aptos is a one stop tool for operations, debugging, and other operations with the blockchain - -#![forbid(unsafe_code)] - -#[cfg(unix)] -#[global_allocator] -static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; - -pub use movement::{move_tool, Tool}; -use clap::Parser; -use std::process::exit; - -#[tokio::main] -async fn main() { - // Register hooks - move_tool::register_package_hooks(); - // Run the corresponding tools - let result = Tool::parse().execute().await; - - // At this point, we'll want to print and determine whether to exit for an error code - match result { - Ok(inner) => println!("{}", inner), - Err(inner) => { - println!("{}", inner); - exit(1); - }, - } -} diff --git a/m1/m1-cli/src/move_tool/aptos_debug_natives.rs b/m1/m1-cli/src/move_tool/aptos_debug_natives.rs deleted file mode 100644 index 7abcb46f..00000000 --- a/m1/m1-cli/src/move_tool/aptos_debug_natives.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use aptos_gas::{AbstractValueSizeGasParameters, NativeGasParameters, LATEST_GAS_FEATURE_VERSION}; -use aptos_types::on_chain_config::{Features, TimedFeatures}; -use aptos_vm::natives; -use move_vm_runtime::native_functions::NativeFunctionTable; -use std::sync::Arc; - -// move_stdlib has the testing feature enabled to include debug native functions -pub fn aptos_debug_natives( - gas_parameters: NativeGasParameters, - abs_val_size_gas_params: AbstractValueSizeGasParameters, -) -> NativeFunctionTable { - // As a side effect, also configure for unit testing - natives::configure_for_unit_test(); - // Return all natives -- build with the 'testing' feature, therefore containing - // debug related functions. - natives::aptos_natives( - gas_parameters, - abs_val_size_gas_params, - LATEST_GAS_FEATURE_VERSION, - TimedFeatures::enable_all(), - Arc::new(Features::default()), - ) -} diff --git a/m1/m1-cli/src/move_tool/aptos_dep_example/README.md b/m1/m1-cli/src/move_tool/aptos_dep_example/README.md deleted file mode 100644 index abfa0f99..00000000 --- a/m1/m1-cli/src/move_tool/aptos_dep_example/README.md +++ /dev/null @@ -1,24 +0,0 @@ -This is a small example of using the new `aptos` dependency. This shall be removed once we have -documentation/tests. - -`pack2` contains a package which is used by `pack1` as follows: - -``` -[dependencies] -Pack2 = { aptos = "http://localhost:8080", address = "default" } -``` - -To see it working: - -```shell -# Start a node with an account -aptos node run-local-testnet --with-faucet & -aptos account create --account default --use-faucet -# Compile and publish pack2 -cd pack2 -aptos move compile --named-addresses project=default -aptos move publish --named-addresses project=default -# Compile pack1 agains the published pack2 -cd ../pack1 -aptos move compile --named-addresses project=default -``` \ No newline at end of file diff --git a/m1/m1-cli/src/move_tool/aptos_dep_example/pack1/Move.toml b/m1/m1-cli/src/move_tool/aptos_dep_example/pack1/Move.toml deleted file mode 100644 index dab5d3fe..00000000 --- a/m1/m1-cli/src/move_tool/aptos_dep_example/pack1/Move.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "Pack1" -version = "0.0.0" - -[dependencies] -Pack2 = { aptos = "http://localhost:8080", address = "default" } diff --git a/m1/m1-cli/src/move_tool/aptos_dep_example/pack1/sources/hello.move b/m1/m1-cli/src/move_tool/aptos_dep_example/pack1/sources/hello.move deleted file mode 100644 index fe091952..00000000 --- a/m1/m1-cli/src/move_tool/aptos_dep_example/pack1/sources/hello.move +++ /dev/null @@ -1,7 +0,0 @@ -module project::test { - use project::m; - - public entry fun test(_sender: &signer) { - assert!(m::add(1, 2) == 1 + 2, 1); - } -} diff --git a/m1/m1-cli/src/move_tool/aptos_dep_example/pack2/Move.toml b/m1/m1-cli/src/move_tool/aptos_dep_example/pack2/Move.toml deleted file mode 100644 index c5e6c7d0..00000000 --- a/m1/m1-cli/src/move_tool/aptos_dep_example/pack2/Move.toml +++ /dev/null @@ -1,3 +0,0 @@ -[package] -name = "Pack2" -version = "0.0.0" diff --git a/m1/m1-cli/src/move_tool/aptos_dep_example/pack2/sources/m.move b/m1/m1-cli/src/move_tool/aptos_dep_example/pack2/sources/m.move deleted file mode 100644 index ade22fad..00000000 --- a/m1/m1-cli/src/move_tool/aptos_dep_example/pack2/sources/m.move +++ /dev/null @@ -1,3 +0,0 @@ -module project::m { - public fun add(x: u64, y: u64): u64 { x + y } -} diff --git a/m1/m1-cli/src/move_tool/coverage.rs b/m1/m1-cli/src/move_tool/coverage.rs deleted file mode 100644 index 78ffdb02..00000000 --- a/m1/m1-cli/src/move_tool/coverage.rs +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::{CliCommand, CliError, CliResult, CliTypedResult, MovePackageDir}; -use async_trait::async_trait; -use clap::{Parser, Subcommand}; -use move_compiler::compiled_unit::{CompiledUnit, NamedCompiledModule}; -use move_coverage::{ - coverage_map::CoverageMap, format_csv_summary, format_human_summary, - source_coverage::SourceCoverageBuilder, summary::summarize_inst_cov, -}; -use move_disassembler::disassembler::Disassembler; -use move_package::{compilation::compiled_package::CompiledPackage, BuildConfig}; - -/// Display a coverage summary for all modules in a package -/// -#[derive(Debug, Parser)] -pub struct SummaryCoverage { - /// Display function coverage summaries - /// - /// When provided, it will include coverage on a function level - #[clap(long)] - pub summarize_functions: bool, - /// Output CSV data of coverage - #[clap(long = "csv")] - pub output_csv: bool, - /// A filter string to determine which unit tests to compute coverage on - #[clap(long, short)] - pub filter: Option, - #[clap(flatten)] - pub move_options: MovePackageDir, -} - -impl SummaryCoverage { - pub fn coverage(self) -> CliTypedResult<()> { - let (coverage_map, package) = compile_coverage(self.move_options)?; - let modules: Vec<_> = package - .root_modules() - .filter_map(|unit| { - let mut retain = true; - if let Some(filter_str) = &self.filter { - if !&unit.unit.name().as_str().contains(filter_str.as_str()) { - retain = false; - } - } - match &unit.unit { - CompiledUnit::Module(NamedCompiledModule { module, .. }) if retain => { - Some(module.clone()) - }, - _ => None, - } - }) - .collect(); - let coverage_map = coverage_map.to_unified_exec_map(); - if self.output_csv { - format_csv_summary( - modules.as_slice(), - &coverage_map, - summarize_inst_cov, - &mut std::io::stdout(), - ) - } else { - format_human_summary( - modules.as_slice(), - &coverage_map, - summarize_inst_cov, - &mut std::io::stdout(), - self.summarize_functions, - ) - } - Ok(()) - } -} - -#[async_trait] -impl CliCommand<()> for SummaryCoverage { - fn command_name(&self) -> &'static str { - "SummaryCoverage" - } - - async fn execute(self) -> CliTypedResult<()> { - self.coverage() - } -} - -/// Display coverage information about the module against source code -#[derive(Debug, Parser)] -pub struct SourceCoverage { - #[clap(long = "module")] - pub module_name: String, - #[clap(flatten)] - pub move_options: MovePackageDir, -} - -#[async_trait] -impl CliCommand<()> for SourceCoverage { - fn command_name(&self) -> &'static str { - "SourceCoverage" - } - - async fn execute(self) -> CliTypedResult<()> { - let (coverage_map, package) = compile_coverage(self.move_options)?; - let unit = package.get_module_by_name_from_root(&self.module_name)?; - let source_path = &unit.source_path; - let (module, source_map) = match &unit.unit { - CompiledUnit::Module(NamedCompiledModule { - module, source_map, .. - }) => (module, source_map), - _ => panic!("Should all be modules"), - }; - let source_coverage = SourceCoverageBuilder::new(module, &coverage_map, source_map); - source_coverage - .compute_source_coverage(source_path) - .output_source_coverage(&mut std::io::stdout()) - .map_err(|err| CliError::UnexpectedError(format!("Failed to get coverage {}", err))) - } -} - -/// Display coverage information about the module against disassembled bytecode -#[derive(Debug, Parser)] -pub struct BytecodeCoverage { - #[clap(long = "module")] - pub module_name: String, - #[clap(flatten)] - pub move_options: MovePackageDir, -} - -#[async_trait] -impl CliCommand<()> for BytecodeCoverage { - fn command_name(&self) -> &'static str { - "BytecodeCoverage" - } - - async fn execute(self) -> CliTypedResult<()> { - let (coverage_map, package) = compile_coverage(self.move_options)?; - let unit = package.get_module_by_name_from_root(&self.module_name)?; - let mut disassembler = Disassembler::from_unit(&unit.unit); - disassembler.add_coverage_map(coverage_map.to_unified_exec_map()); - println!("{}", disassembler.disassemble()?); - Ok(()) - } -} - -fn compile_coverage( - move_options: MovePackageDir, -) -> CliTypedResult<(CoverageMap, CompiledPackage)> { - let config = BuildConfig { - additional_named_addresses: move_options.named_addresses(), - test_mode: false, - install_dir: move_options.output_dir.clone(), - ..Default::default() - }; - let path = move_options.get_package_path()?; - let coverage_map = - CoverageMap::from_binary_file(path.join(".coverage_map.mvcov")).map_err(|err| { - CliError::UnexpectedError(format!("Failed to retrieve coverage map {}", err)) - })?; - let package = config - .compile_package(path.as_path(), &mut Vec::new()) - .map_err(|err| CliError::MoveCompilationError(err.to_string()))?; - - Ok((coverage_map, package)) -} - -/// Computes coverage for a package -/// -/// Computes coverage on a previous unit test run for a package. Coverage input must -/// first be built with `aptos move test --coverage` -#[derive(Subcommand)] -pub enum CoveragePackage { - Summary(SummaryCoverage), - Source(SourceCoverage), - Bytecode(BytecodeCoverage), -} - -impl CoveragePackage { - pub async fn execute(self) -> CliResult { - match self { - Self::Summary(tool) => tool.execute_serialized_success().await, - Self::Source(tool) => tool.execute_serialized_success().await, - Self::Bytecode(tool) => tool.execute_serialized_success().await, - } - } -} diff --git a/m1/m1-cli/src/move_tool/disassembler.rs b/m1/m1-cli/src/move_tool/disassembler.rs deleted file mode 100644 index 12e7fa62..00000000 --- a/m1/m1-cli/src/move_tool/disassembler.rs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::{ - types::{CliCommand, CliError, CliTypedResult, PromptOptions}, - utils::{ - check_if_file_exists, create_dir_if_not_exist, dir_default_to_current, read_from_file, - write_to_user_only_file, - }, -}; -use anyhow::Context; -use async_trait::async_trait; -use clap::Parser; -use move_binary_format::{ - binary_views::BinaryIndexedView, file_format::CompiledScript, CompiledModule, -}; -use move_bytecode_source_map::{mapping::SourceMapping, utils::source_map_from_file}; -use move_command_line_common::files::{ - MOVE_COMPILED_EXTENSION, MOVE_EXTENSION, SOURCE_MAP_EXTENSION, -}; -use move_coverage::coverage_map::CoverageMap; -use move_disassembler::disassembler::{Disassembler, DisassemblerOptions}; -use move_ir_types::location::Spanned; -use std::{fs, path::PathBuf}; - -const DISASSEMBLED_CODE_FILE: &str = "disassembled-code.yaml"; - -/// Disassemble the Move bytecode pointed to -/// -/// For example, if you want to disassemble on chain module: -/// 1. Download the package - aptos move download -/// 2. Compile the package - aptos move compile -/// 3. Cd to package and disassemble - aptos move disassemble --bytecode-path ./test.mv -#[derive(Debug, Parser)] -pub struct Disassemble { - /// Treat input file as a script (default is to treat file as a module) - #[clap(long)] - pub is_script: bool, - - /// The path to the bytecode file to disassemble; - /// - /// let's call it file.mv. We assume that two other files reside under the same directory: - /// a source map file.mvsm (possibly) and the Move source code file.move. - #[clap(long)] - pub bytecode_path: PathBuf, - - /// (Optional) Path to a coverage file for the VM in order to print trace information in the - /// disassembled output. - #[clap(long)] - pub code_coverage_path: Option, - - /// Output directory for the key files - #[clap(long, parse(from_os_str))] - pub(crate) output_dir: Option, - - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, -} - -#[async_trait] -impl CliCommand for Disassemble { - fn command_name(&self) -> &'static str { - "Disassemble" - } - - async fn execute(self) -> CliTypedResult { - let bytecode_path = self.bytecode_path.as_path(); - let extension = bytecode_path - .extension() - .context("Missing file extension for bytecode file")?; - if extension != MOVE_COMPILED_EXTENSION { - return Err(CliError::UnexpectedError(format!( - "Bad source file extension {:?}; expected {}", - extension, MOVE_COMPILED_EXTENSION - ))); - } - - let bytecode_bytes = read_from_file(bytecode_path)?; - let move_path = bytecode_path.with_extension(MOVE_EXTENSION); - let source_map_path = bytecode_path.with_extension(SOURCE_MAP_EXTENSION); - - let source = fs::read_to_string(move_path).ok(); - let source_map = source_map_from_file(&source_map_path); - - let disassembler_options = DisassemblerOptions { - print_code: true, - only_externally_visible: false, - print_basic_blocks: true, - print_locals: true, - }; - - let no_loc = Spanned::unsafe_no_loc(()).loc; - let module: CompiledModule; - let script: CompiledScript; - let bytecode = if self.is_script { - script = CompiledScript::deserialize(&bytecode_bytes) - .context("Script blob can't be deserialized")?; - BinaryIndexedView::Script(&script) - } else { - module = CompiledModule::deserialize(&bytecode_bytes) - .context("Module blob can't be deserialized")?; - BinaryIndexedView::Module(&module) - }; - - let mut source_mapping = if let Ok(s) = source_map { - SourceMapping::new(s, bytecode) - } else { - SourceMapping::new_from_view(bytecode, no_loc) - .context("Unable to build dummy source mapping")? - }; - - if let Some(source_code) = source { - source_mapping.with_source_code((bytecode_path.display().to_string(), source_code)); - } - - let mut disassembler = Disassembler::new(source_mapping, disassembler_options); - - if let Some(file_path) = &self.code_coverage_path { - disassembler.add_coverage_map( - CoverageMap::from_binary_file(file_path) - .map_err(|_err| { - CliError::UnexpectedError("Unable to read from file_path".to_string()) - })? - .to_unified_exec_map(), - ); - } - - let disassemble_string = disassembler - .disassemble() - .map_err(|_err| CliError::UnexpectedError("Unable to disassemble".to_string()))?; - - let output_dir = dir_default_to_current(self.output_dir.clone())?; - let disassemble_file = output_dir.join(DISASSEMBLED_CODE_FILE); - check_if_file_exists(disassemble_file.as_path(), self.prompt_options)?; - - // Create the directory if it doesn't exist - create_dir_if_not_exist(output_dir.as_path())?; - - // write to file - write_to_user_only_file( - disassemble_file.as_path(), - DISASSEMBLED_CODE_FILE, - disassemble_string.as_bytes(), - )?; - - Ok(disassemble_file.as_path().display().to_string()) - } -} diff --git a/m1/m1-cli/src/move_tool/manifest.rs b/m1/m1-cli/src/move_tool/manifest.rs deleted file mode 100644 index dbdcbc3e..00000000 --- a/m1/m1-cli/src/move_tool/manifest.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::load_manifest_account_arg; -use aptos_types::account_address::AccountAddress; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::collections::BTreeMap; - -/// A Rust representation of the Move package manifest -/// -/// Note: The original Move package manifest object used by the package system -/// can't be serialized because it uses a symbol mapping -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MovePackageManifest { - pub package: PackageInfo, - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - pub addresses: BTreeMap, - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, -} - -/// Representation of an option address so we can print it as "_" -#[derive(Debug, Clone)] -pub struct ManifestNamedAddress { - pub address: Option, -} - -impl From> for ManifestNamedAddress { - fn from(opt: Option) -> Self { - ManifestNamedAddress { address: opt } - } -} - -impl From for Option { - fn from(addr: ManifestNamedAddress) -> Self { - addr.address - } -} - -impl Serialize for ManifestNamedAddress { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - if let Some(address) = self.address { - serializer.serialize_str(&address.to_hex_literal()) - } else { - serializer.serialize_str("_") - } - } -} - -impl<'de> Deserialize<'de> for ManifestNamedAddress { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let str = ::deserialize(deserializer)?; - Ok(ManifestNamedAddress { - // TODO: Cleanup unwrap - address: load_manifest_account_arg(&str).unwrap(), - }) - } -} - -/// A Rust representation of a move dependency -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Dependency { - #[serde(skip_serializing_if = "Option::is_none")] - pub local: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub git: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub rev: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub subdir: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub aptos: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub address: Option, -} - -/// A Rust representation of the package info -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PackageInfo { - pub name: String, - pub version: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub author: Option, -} diff --git a/m1/m1-cli/src/move_tool/mod.rs b/m1/m1-cli/src/move_tool/mod.rs deleted file mode 100644 index 560d0f18..00000000 --- a/m1/m1-cli/src/move_tool/mod.rs +++ /dev/null @@ -1,1606 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -mod aptos_debug_natives; -pub mod coverage; -mod disassembler; -mod manifest; -pub mod package_hooks; -mod show; -pub mod stored_package; -mod transactional_tests_runner; - -use crate::{ - account::derive_resource_account::ResourceAccountSeed, - common::{ - types::{ - load_account_arg, ArgWithTypeVec, CliConfig, CliError, CliTypedResult, - ConfigSearchMode, EntryFunctionArguments, MoveManifestAccountWrapper, MovePackageDir, - ProfileOptions, PromptOptions, RestOptions, TransactionOptions, TransactionSummary, - }, - utils::{ - check_if_file_exists, create_dir_if_not_exist, dir_default_to_current, - profile_or_submit, prompt_yes_with_override, write_to_file, - }, - }, - governance::CompileScriptFunction, - move_tool::{ - coverage::SummaryCoverage, - disassembler::Disassemble, - manifest::{Dependency, ManifestNamedAddress, MovePackageManifest, PackageInfo}, - }, - CliCommand, CliResult, -}; -use aptos_crypto::HashValue; -use aptos_framework::{ - build_model, docgen::DocgenOptions, extended_checks, natives::code::UpgradePolicy, - prover::ProverOptions, BuildOptions, BuiltPackage, -}; -use aptos_gas::{AbstractValueSizeGasParameters, NativeGasParameters}; -use aptos_rest_client::aptos_api_types::{EntryFunctionId, MoveType, ViewRequest}; -use aptos_transactional_test_harness::run_aptos_test; -use aptos_types::{ - account_address::{create_resource_address, AccountAddress}, - transaction::{Script, TransactionArgument, TransactionPayload}, -}; -use async_trait::async_trait; -use clap::{ArgEnum, Parser, Subcommand}; -use codespan_reporting::{ - diagnostic::Severity, - term::termcolor::{ColorChoice, StandardStream}, -}; -use itertools::Itertools; -use move_cli::{self, base::test::UnitTestResult}; -use move_command_line_common::env::MOVE_HOME; -use move_core_types::{ - identifier::Identifier, - language_storage::{ModuleId, TypeTag}, - u256::U256, -}; -use move_package::{source_package::layout::SourcePackageLayout, BuildConfig}; -use move_unit_test::UnitTestingConfig; -pub use package_hooks::*; -use serde::{Deserialize, Serialize}; -use std::{ - collections::BTreeMap, - convert::TryFrom, - fmt::{Display, Formatter}, - path::{Path, PathBuf}, - str::FromStr, -}; -pub use stored_package::*; -use tokio::task; -use transactional_tests_runner::TransactionalTestOpts; - -/// Tool for Move related operations -/// -/// This tool lets you compile, test, and publish Move code, in addition -/// to run any other tools that help run, verify, or provide information -/// about this code. -#[derive(Subcommand)] -pub enum MoveTool { - Clean(CleanPackage), - Compile(CompilePackage), - CompileScript(CompileScript), - #[clap(subcommand)] - Coverage(coverage::CoveragePackage), - CreateResourceAccountAndPublishPackage(CreateResourceAccountAndPublishPackage), - Disassemble(Disassemble), - Document(DocumentPackage), - Download(DownloadPackage), - Init(InitPackage), - List(ListPackage), - Prove(ProvePackage), - Publish(PublishPackage), - Run(RunFunction), - RunScript(RunScript), - #[clap(subcommand, hide = true)] - Show(show::ShowTool), - Test(TestPackage), - TransactionalTest(TransactionalTestOpts), - VerifyPackage(VerifyPackage), - View(ViewFunction), -} - -impl MoveTool { - pub async fn execute(self) -> CliResult { - match self { - MoveTool::Clean(tool) => tool.execute_serialized().await, - MoveTool::Compile(tool) => tool.execute_serialized().await, - MoveTool::CompileScript(tool) => tool.execute_serialized().await, - MoveTool::Coverage(tool) => tool.execute().await, - MoveTool::CreateResourceAccountAndPublishPackage(tool) => { - tool.execute_serialized_success().await - }, - MoveTool::Disassemble(tool) => tool.execute_serialized().await, - MoveTool::Document(tool) => tool.execute_serialized().await, - MoveTool::Download(tool) => tool.execute_serialized().await, - MoveTool::Init(tool) => tool.execute_serialized_success().await, - MoveTool::List(tool) => tool.execute_serialized().await, - MoveTool::Prove(tool) => tool.execute_serialized().await, - MoveTool::Publish(tool) => tool.execute_serialized().await, - MoveTool::Run(tool) => tool.execute_serialized().await, - MoveTool::RunScript(tool) => tool.execute_serialized().await, - MoveTool::Show(tool) => tool.execute_serialized().await, - MoveTool::Test(tool) => tool.execute_serialized().await, - MoveTool::TransactionalTest(tool) => tool.execute_serialized_success().await, - MoveTool::VerifyPackage(tool) => tool.execute_serialized().await, - MoveTool::View(tool) => tool.execute_serialized().await, - } - } -} - -#[derive(Parser)] -pub struct FrameworkPackageArgs { - /// Git revision or branch for the Aptos framework - /// - /// This is mutually exclusive with `--framework-local-dir` - #[clap(long, group = "framework_package_args")] - pub(crate) framework_git_rev: Option, - - /// Local framework directory for the Aptos framework - /// - /// This is mutually exclusive with `--framework-git-rev` - #[clap(long, parse(from_os_str), group = "framework_package_args")] - pub(crate) framework_local_dir: Option, - - /// Skip pulling the latest git dependencies - /// - /// If you don't have a network connection, the compiler may fail due - /// to no ability to pull git dependencies. This will allow overriding - /// this for local development. - #[clap(long)] - pub(crate) skip_fetch_latest_git_deps: bool, -} - -impl FrameworkPackageArgs { - pub fn init_move_dir( - &self, - package_dir: &Path, - name: &str, - addresses: BTreeMap, - prompt_options: PromptOptions, - ) -> CliTypedResult<()> { - const APTOS_FRAMEWORK: &str = "AptosFramework"; - const APTOS_GIT_PATH: &str = "https://github.com/aptos-labs/aptos-core.git"; - const SUBDIR_PATH: &str = "aptos-move/framework/aptos-framework"; - const DEFAULT_BRANCH: &str = "main"; - - let move_toml = package_dir.join(SourcePackageLayout::Manifest.path()); - check_if_file_exists(move_toml.as_path(), prompt_options)?; - create_dir_if_not_exist( - package_dir - .join(SourcePackageLayout::Sources.path()) - .as_path(), - )?; - - // Add the framework dependency if it's provided - let mut dependencies = BTreeMap::new(); - if let Some(ref path) = self.framework_local_dir { - dependencies.insert(APTOS_FRAMEWORK.to_string(), Dependency { - local: Some(path.display().to_string()), - git: None, - rev: None, - subdir: None, - aptos: None, - address: None, - }); - } else { - let git_rev = self.framework_git_rev.as_deref().unwrap_or(DEFAULT_BRANCH); - dependencies.insert(APTOS_FRAMEWORK.to_string(), Dependency { - local: None, - git: Some(APTOS_GIT_PATH.to_string()), - rev: Some(git_rev.to_string()), - subdir: Some(SUBDIR_PATH.to_string()), - aptos: None, - address: None, - }); - } - - let manifest = MovePackageManifest { - package: PackageInfo { - name: name.to_string(), - version: "1.0.0".to_string(), - author: None, - }, - addresses, - dependencies, - }; - - write_to_file( - move_toml.as_path(), - SourcePackageLayout::Manifest.location_str(), - toml::to_string_pretty(&manifest) - .map_err(|err| CliError::UnexpectedError(err.to_string()))? - .as_bytes(), - ) - } -} - -/// Creates a new Move package at the given location -/// -/// This will create a directory for a Move package and a corresponding -/// `Move.toml` file. -#[derive(Parser)] -pub struct InitPackage { - /// Name of the new Move package - #[clap(long)] - pub(crate) name: String, - - /// Directory to create the new Move package - #[clap(long, parse(from_os_str))] - pub(crate) package_dir: Option, - - /// Named addresses for the move binary - /// - /// Allows for an address to be put into the Move.toml, or a placeholder `_` - /// - /// Example: alice=0x1234,bob=0x5678,greg=_ - /// - /// Note: This will fail if there are duplicates in the Move.toml file remove those first. - #[clap(long, parse(try_from_str = crate::common::utils::parse_map), default_value = "")] - pub(crate) named_addresses: BTreeMap, - - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, - - #[clap(flatten)] - pub(crate) framework_package_args: FrameworkPackageArgs, -} - -#[async_trait] -impl CliCommand<()> for InitPackage { - fn command_name(&self) -> &'static str { - "InitPackage" - } - - async fn execute(self) -> CliTypedResult<()> { - let package_dir = dir_default_to_current(self.package_dir.clone())?; - let addresses = self - .named_addresses - .into_iter() - .map(|(key, value)| (key, value.account_address.into())) - .collect(); - - self.framework_package_args.init_move_dir( - package_dir.as_path(), - &self.name, - addresses, - self.prompt_options, - ) - } -} - -/// Compiles a package and returns the associated ModuleIds -#[derive(Parser)] -pub struct CompilePackage { - /// Save the package metadata in the package's build directory - /// - /// If set, package metadata should be generated and stored in the package's build directory. - /// This metadata can be used to construct a transaction to publish a package. - #[clap(long)] - pub(crate) save_metadata: bool, - - #[clap(flatten)] - pub(crate) included_artifacts_args: IncludedArtifactsArgs, - #[clap(flatten)] - pub(crate) move_options: MovePackageDir, -} - -#[async_trait] -impl CliCommand> for CompilePackage { - fn command_name(&self) -> &'static str { - "CompilePackage" - } - - async fn execute(self) -> CliTypedResult> { - let build_options = BuildOptions { - install_dir: self.move_options.output_dir.clone(), - ..self - .included_artifacts_args - .included_artifacts - .build_options( - self.move_options.skip_fetch_latest_git_deps, - self.move_options.named_addresses(), - self.move_options.bytecode_version, - ) - }; - let pack = BuiltPackage::build(self.move_options.get_package_path()?, build_options) - .map_err(|e| CliError::MoveCompilationError(format!("{:#}", e)))?; - if self.save_metadata { - pack.extract_metadata_and_save()?; - } - let ids = pack - .modules() - .into_iter() - .map(|m| m.self_id().to_string()) - .collect::>(); - Ok(ids) - } -} - -/// Compiles a Move script into bytecode -/// -/// Compiles a script into bytecode and provides a hash of the bytecode. -/// This can then be run with `aptos move run-script` -#[derive(Parser)] -pub struct CompileScript { - #[clap(long, parse(from_os_str))] - pub output_file: Option, - #[clap(flatten)] - pub move_options: MovePackageDir, -} - -#[async_trait] -impl CliCommand for CompileScript { - fn command_name(&self) -> &'static str { - "CompileScript" - } - - async fn execute(self) -> CliTypedResult { - let (bytecode, script_hash) = self.compile_script().await?; - let script_location = self.output_file.unwrap_or_else(|| { - self.move_options - .get_package_path() - .unwrap() - .join("script.mv") - }); - write_to_file(script_location.as_path(), "Script", bytecode.as_slice())?; - Ok(CompileScriptOutput { - script_location, - script_hash, - }) - } -} - -impl CompileScript { - async fn compile_script(&self) -> CliTypedResult<(Vec, HashValue)> { - let build_options = BuildOptions { - install_dir: self.move_options.output_dir.clone(), - ..IncludedArtifacts::None.build_options( - self.move_options.skip_fetch_latest_git_deps, - self.move_options.named_addresses(), - self.move_options.bytecode_version, - ) - }; - let package_dir = self.move_options.get_package_path()?; - let pack = BuiltPackage::build(package_dir, build_options) - .map_err(|e| CliError::MoveCompilationError(format!("{:#}", e)))?; - - let scripts_count = pack.script_count(); - if scripts_count != 1 { - return Err(CliError::UnexpectedError(format!( - "Only one script can be prepared a time. Make sure one and only one script file \ - is included in the Move package. Found {} scripts.", - scripts_count - ))); - } - - let bytecode = pack.extract_script_code().pop().unwrap(); - let script_hash = HashValue::sha3_256_of(bytecode.as_slice()); - Ok((bytecode, script_hash)) - } -} - -#[derive(Debug, Serialize)] -pub struct CompileScriptOutput { - pub script_location: PathBuf, - pub script_hash: HashValue, -} - -/// Runs Move unit tests for a package -/// -/// This will run Move unit tests against a package with debug mode -/// turned on. Note, that move code warnings currently block tests from running. -#[derive(Parser)] -pub struct TestPackage { - /// A filter string to determine which unit tests to run - #[clap(long, short)] - pub filter: Option, - - /// A boolean value to skip warnings. - #[clap(long)] - pub ignore_compile_warnings: bool, - - #[clap(flatten)] - pub(crate) move_options: MovePackageDir, - - /// The maximum number of instructions that can be executed by a test - /// - /// If set, the number of instructions executed by one test will be bounded - // TODO: Remove short, it's against the style guidelines, and update the name here - #[clap( - name = "instructions", - default_value = "100000", - short = 'i', - long = "instructions" - )] - pub instruction_execution_bound: u64, - - /// Collect coverage information for later use with the various `aptos move coverage` subcommands - #[clap(long = "coverage")] - pub compute_coverage: bool, - - /// Dump storage state on failure. - #[clap(long = "dump")] - pub dump_state: bool, -} - -#[async_trait] -impl CliCommand<&'static str> for TestPackage { - fn command_name(&self) -> &'static str { - "TestPackage" - } - - async fn execute(self) -> CliTypedResult<&'static str> { - let mut config = BuildConfig { - additional_named_addresses: self.move_options.named_addresses(), - test_mode: true, - install_dir: self.move_options.output_dir.clone(), - skip_fetch_latest_git_deps: self.move_options.skip_fetch_latest_git_deps, - ..Default::default() - }; - - // Build the Move model for extended checks - let model = &build_model( - self.move_options.get_package_path()?.as_path(), - self.move_options.named_addresses(), - None, - self.move_options.bytecode_version, - )?; - let _ = extended_checks::run_extended_checks(model); - if model.diag_count(Severity::Warning) > 0 { - let mut error_writer = StandardStream::stderr(ColorChoice::Auto); - model.report_diag(&mut error_writer, Severity::Warning); - if model.has_errors() { - return Err(CliError::MoveCompilationError( - "extended checks failed".to_string(), - )); - } - } - let path = self.move_options.get_package_path()?; - let result = move_cli::base::test::run_move_unit_tests( - path.as_path(), - config.clone(), - UnitTestingConfig { - filter: self.filter.clone(), - report_stacktrace_on_abort: true, - report_storage_on_error: self.dump_state, - ignore_compile_warnings: self.ignore_compile_warnings, - ..UnitTestingConfig::default_with_bound(None) - }, - // TODO(Gas): we may want to switch to non-zero costs in the future - aptos_debug_natives::aptos_debug_natives( - NativeGasParameters::zeros(), - AbstractValueSizeGasParameters::zeros(), - ), - None, - self.compute_coverage, - &mut std::io::stdout(), - ) - .map_err(|err| CliError::UnexpectedError(err.to_string()))?; - - // Print coverage summary if --coverage is set - if self.compute_coverage { - config.test_mode = false; - let summary = SummaryCoverage { - summarize_functions: false, - output_csv: false, - filter: self.filter, - move_options: self.move_options, - }; - summary.coverage()?; - - println!("Please use `movement move coverage -h` for more detailed source or bytecode test coverage of this package"); - } - - match result { - UnitTestResult::Success => Ok("Success"), - UnitTestResult::Failure => Err(CliError::MoveTestError), - } - } -} - -#[async_trait] -impl CliCommand<()> for TransactionalTestOpts { - fn command_name(&self) -> &'static str { - "TransactionalTest" - } - - async fn execute(self) -> CliTypedResult<()> { - let root_path = self.root_path.display().to_string(); - - let requirements = vec![transactional_tests_runner::Requirements::new( - run_aptos_test, - "tests".to_string(), - root_path, - self.pattern.clone(), - )]; - - transactional_tests_runner::runner(&self, &requirements) - } -} - -/// Proves a Move package -/// -/// This is a tool for formal verification of a Move package using -/// the Move prover -#[derive(Parser)] -pub struct ProvePackage { - #[clap(flatten)] - move_options: MovePackageDir, - - #[clap(flatten)] - prover_options: ProverOptions, -} - -#[async_trait] -impl CliCommand<&'static str> for ProvePackage { - fn command_name(&self) -> &'static str { - "ProvePackage" - } - - async fn execute(self) -> CliTypedResult<&'static str> { - let ProvePackage { - move_options, - prover_options, - } = self; - - let result = task::spawn_blocking(move || { - prover_options.prove( - move_options.get_package_path()?.as_path(), - move_options.named_addresses(), - move_options.bytecode_version, - ) - }) - .await - .map_err(|err| CliError::UnexpectedError(err.to_string()))?; - match result { - Ok(_) => Ok("Success"), - Err(e) => Err(CliError::MoveProverError(format!("{:#}", e))), - } - } -} - -/// Documents a Move package -/// -/// This converts the content of the package into markdown for documentation. -#[derive(Parser)] -pub struct DocumentPackage { - #[clap(flatten)] - move_options: MovePackageDir, - - #[clap(flatten)] - docgen_options: DocgenOptions, -} - -#[async_trait] -impl CliCommand<&'static str> for DocumentPackage { - fn command_name(&self) -> &'static str { - "DocumentPackage" - } - - async fn execute(self) -> CliTypedResult<&'static str> { - let DocumentPackage { - move_options, - docgen_options, - } = self; - let build_options = BuildOptions { - with_srcs: false, - with_abis: false, - with_source_maps: false, - with_error_map: false, - with_docs: true, - install_dir: None, - named_addresses: move_options.named_addresses(), - docgen_options: Some(docgen_options), - skip_fetch_latest_git_deps: move_options.skip_fetch_latest_git_deps, - bytecode_version: move_options.bytecode_version, - }; - BuiltPackage::build(move_options.get_package_path()?, build_options)?; - Ok("succeeded") - } -} - -#[derive(Parser)] -pub struct IncludedArtifactsArgs { - /// Artifacts to be generated when building the package - /// - /// Which artifacts to include in the package. This can be one of `none`, `sparse`, and - /// `all`. `none` is the most compact form and does not allow to reconstruct a source - /// package from chain; `sparse` is the minimal set of artifacts needed to reconstruct - /// a source package; `all` includes all available artifacts. The choice of included - /// artifacts heavily influences the size and therefore gas cost of publishing: `none` - /// is the size of bytecode alone; `sparse` is roughly 2 times as much; and `all` 3-4 - /// as much. - #[clap(long, default_value_t = IncludedArtifacts::Sparse)] - pub(crate) included_artifacts: IncludedArtifacts, -} - -/// Publishes the modules in a Move package to the Aptos blockchain -#[derive(Parser)] -pub struct PublishPackage { - /// Whether to override the check for maximal size of published data - #[clap(long)] - pub(crate) override_size_check: bool, - - #[clap(flatten)] - pub(crate) included_artifacts_args: IncludedArtifactsArgs, - #[clap(flatten)] - pub(crate) move_options: MovePackageDir, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[derive(ArgEnum, Clone, Copy, Debug)] -pub enum IncludedArtifacts { - None, - Sparse, - All, -} - -impl Display for IncludedArtifacts { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - use IncludedArtifacts::*; - match self { - None => f.write_str("none"), - Sparse => f.write_str("sparse"), - All => f.write_str("all"), - } - } -} - -impl FromStr for IncludedArtifacts { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - use IncludedArtifacts::*; - match s { - "none" => Ok(None), - "sparse" => Ok(Sparse), - "all" => Ok(All), - _ => Err("unknown variant"), - } - } -} - -impl IncludedArtifacts { - pub(crate) fn build_options( - self, - skip_fetch_latest_git_deps: bool, - named_addresses: BTreeMap, - bytecode_version: Option, - ) -> BuildOptions { - use IncludedArtifacts::*; - match self { - None => BuildOptions { - with_srcs: false, - with_abis: false, - with_source_maps: false, - // Always enable error map bytecode injection - with_error_map: true, - named_addresses, - skip_fetch_latest_git_deps, - bytecode_version, - ..BuildOptions::default() - }, - Sparse => BuildOptions { - with_srcs: true, - with_abis: false, - with_source_maps: false, - with_error_map: true, - named_addresses, - skip_fetch_latest_git_deps, - bytecode_version, - ..BuildOptions::default() - }, - All => BuildOptions { - with_srcs: true, - with_abis: true, - with_source_maps: true, - with_error_map: true, - named_addresses, - skip_fetch_latest_git_deps, - bytecode_version, - ..BuildOptions::default() - }, - } - } -} - -pub const MAX_PUBLISH_PACKAGE_SIZE: usize = 60_000; - -#[async_trait] -impl CliCommand for PublishPackage { - fn command_name(&self) -> &'static str { - "PublishPackage" - } - - async fn execute(self) -> CliTypedResult { - let PublishPackage { - move_options, - txn_options, - override_size_check, - included_artifacts_args, - } = self; - let package_path = move_options.get_package_path()?; - let options = included_artifacts_args.included_artifacts.build_options( - move_options.skip_fetch_latest_git_deps, - move_options.named_addresses(), - move_options.bytecode_version, - ); - let package = BuiltPackage::build(package_path, options)?; - let compiled_units = package.extract_code(); - - // Send the compiled module and metadata using the code::publish_package_txn. - let metadata = package.extract_metadata()?; - let payload = aptos_cached_packages::aptos_stdlib::code_publish_package_txn( - bcs::to_bytes(&metadata).expect("PackageMetadata has BCS"), - compiled_units, - ); - let size = bcs::serialized_size(&payload)?; - println!("package size {} bytes", size); - if !override_size_check && size > MAX_PUBLISH_PACKAGE_SIZE { - return Err(CliError::UnexpectedError(format!( - "The package is larger than {} bytes ({} bytes)! To lower the size \ - you may want to include less artifacts via `--included-artifacts`. \ - You can also override this check with `--override-size-check", - MAX_PUBLISH_PACKAGE_SIZE, size - ))); - } - profile_or_submit(payload, &txn_options).await - } -} - -/// Publishes the modules in a Move package to the Aptos blockchain under a resource account -#[derive(Parser)] -pub struct CreateResourceAccountAndPublishPackage { - /// The named address for compiling and using in the contract - /// - /// This will take the derived account address for the resource account and put it in this location - #[clap(long)] - pub(crate) address_name: String, - - /// Whether to override the check for maximal size of published data - /// - /// This won't bypass on chain checks, so if you are not allowed to go over the size check, it - /// will still be blocked from publishing. - #[clap(long)] - pub(crate) override_size_check: bool, - - #[clap(flatten)] - pub(crate) seed_args: ResourceAccountSeed, - #[clap(flatten)] - pub(crate) included_artifacts_args: IncludedArtifactsArgs, - #[clap(flatten)] - pub(crate) move_options: MovePackageDir, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for CreateResourceAccountAndPublishPackage { - fn command_name(&self) -> &'static str { - "ResourceAccountPublishPackage" - } - - async fn execute(self) -> CliTypedResult { - let CreateResourceAccountAndPublishPackage { - address_name, - mut move_options, - txn_options, - override_size_check, - included_artifacts_args, - seed_args, - } = self; - - let account = if let Some(Some(account)) = CliConfig::load_profile( - txn_options.profile_options.profile_name(), - ConfigSearchMode::CurrentDirAndParents, - )? - .map(|p| p.account) - { - account - } else { - return Err(CliError::CommandArgumentError( - "Please provide an account using --profile or run movement init".to_string(), - )); - }; - let seed = seed_args.seed()?; - - let resource_address = create_resource_address(account, &seed); - move_options.add_named_address(address_name, resource_address.to_string()); - - let package_path = move_options.get_package_path()?; - let options = included_artifacts_args.included_artifacts.build_options( - move_options.skip_fetch_latest_git_deps, - move_options.named_addresses(), - move_options.bytecode_version, - ); - let package = BuiltPackage::build(package_path, options)?; - let compiled_units = package.extract_code(); - - // Send the compiled module and metadata using the code::publish_package_txn. - let metadata = package.extract_metadata()?; - - let message = format!( - "Do you want to publish this package under the resource account's address {}?", - resource_address - ); - prompt_yes_with_override(&message, txn_options.prompt_options)?; - - let payload = aptos_cached_packages::aptos_stdlib::resource_account_create_resource_account_and_publish_package( - seed, - bcs::to_bytes(&metadata).expect("PackageMetadata has BCS"), - compiled_units, - ); - let size = bcs::serialized_size(&payload)?; - println!("package size {} bytes", size); - if !override_size_check && size > MAX_PUBLISH_PACKAGE_SIZE { - return Err(CliError::UnexpectedError(format!( - "The package is larger than {} bytes ({} bytes)! To lower the size \ - you may want to include less artifacts via `--included-artifacts`. \ - You can also override this check with `--override-size-check", - MAX_PUBLISH_PACKAGE_SIZE, size - ))); - } - txn_options - .submit_transaction(payload) - .await - .map(TransactionSummary::from) - } -} - -/// Downloads a package and stores it in a directory named after the package -/// -/// This lets you retrieve packages directly from the blockchain for inspection -/// and use as a local dependency in testing. -#[derive(Parser)] -pub struct DownloadPackage { - /// Address of the account containing the package - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) account: AccountAddress, - - /// Name of the package - #[clap(long)] - pub package: String, - - /// Directory to store downloaded package. Defaults to the current directory. - #[clap(long, parse(from_os_str))] - pub output_dir: Option, - - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -#[async_trait] -impl CliCommand<&'static str> for DownloadPackage { - fn command_name(&self) -> &'static str { - "DownloadPackage" - } - - async fn execute(self) -> CliTypedResult<&'static str> { - let url = self.rest_options.url(&self.profile_options)?; - let registry = CachedPackageRegistry::create(url, self.account).await?; - let output_dir = dir_default_to_current(self.output_dir)?; - - let package = registry - .get_package(self.package) - .await - .map_err(|s| CliError::CommandArgumentError(s.to_string()))?; - if package.upgrade_policy() == UpgradePolicy::arbitrary() { - return Err(CliError::CommandArgumentError( - "A package with upgrade policy `arbitrary` cannot be downloaded \ - since it is not safe to depend on such packages." - .to_owned(), - )); - } - let package_path = output_dir.join(package.name()); - package - .save_package_to_disk(package_path.as_path()) - .map_err(|e| CliError::UnexpectedError(format!("Failed to save package: {}", e)))?; - println!( - "Saved package with {} module(s) to `{}`", - package.module_names().len(), - package_path.display() - ); - Ok("Download succeeded") - } -} - -/// Downloads a package and verifies the bytecode -/// -/// Downloads the package from onchain and verifies the bytecode matches a local compilation of the Move code -#[derive(Parser)] -pub struct VerifyPackage { - /// Address of the account containing the package - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) account: AccountAddress, - - /// Artifacts to be generated when building this package. - #[clap(long, default_value_t = IncludedArtifacts::Sparse)] - pub(crate) included_artifacts: IncludedArtifacts, - - #[clap(flatten)] - pub(crate) move_options: MovePackageDir, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -#[async_trait] -impl CliCommand<&'static str> for VerifyPackage { - fn command_name(&self) -> &'static str { - "DownloadPackage" - } - - async fn execute(self) -> CliTypedResult<&'static str> { - // First build the package locally to get the package metadata - let build_options = BuildOptions { - install_dir: self.move_options.output_dir.clone(), - bytecode_version: self.move_options.bytecode_version, - ..self.included_artifacts.build_options( - self.move_options.skip_fetch_latest_git_deps, - self.move_options.named_addresses(), - self.move_options.bytecode_version, - ) - }; - let pack = BuiltPackage::build(self.move_options.get_package_path()?, build_options) - .map_err(|e| CliError::MoveCompilationError(format!("{:#}", e)))?; - let compiled_metadata = pack.extract_metadata()?; - - // Now pull the compiled package - let url = self.rest_options.url(&self.profile_options)?; - let registry = CachedPackageRegistry::create(url, self.account).await?; - let package = registry - .get_package(pack.name()) - .await - .map_err(|s| CliError::CommandArgumentError(s.to_string()))?; - - // We can't check the arbitrary, because it could change on us - if package.upgrade_policy() == UpgradePolicy::arbitrary() { - return Err(CliError::CommandArgumentError( - "A package with upgrade policy `arbitrary` cannot be downloaded \ - since it is not safe to depend on such packages." - .to_owned(), - )); - } - - // Verify that the source digest matches - package.verify(&compiled_metadata)?; - - Ok("Successfully verified source of package") - } -} - -/// Lists information about packages and modules on-chain for an account -#[derive(Parser)] -pub struct ListPackage { - /// Address of the account for which to list packages. - #[clap(long, parse(try_from_str = crate::common::types::load_account_arg))] - pub(crate) account: AccountAddress, - - /// Type of items to query - /// - /// Current supported types `[packages]` - #[clap(long, default_value_t = MoveListQuery::Packages)] - query: MoveListQuery, - - #[clap(flatten)] - rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -#[derive(ArgEnum, Clone, Copy, Debug)] -pub enum MoveListQuery { - Packages, -} - -impl Display for MoveListQuery { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - MoveListQuery::Packages => "packages", - }) - } -} - -impl FromStr for MoveListQuery { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "packages" => Ok(MoveListQuery::Packages), - _ => Err("Invalid query. Valid values are modules, packages"), - } - } -} - -#[async_trait] -impl CliCommand<&'static str> for ListPackage { - fn command_name(&self) -> &'static str { - "ListPackage" - } - - async fn execute(self) -> CliTypedResult<&'static str> { - let url = self.rest_options.url(&self.profile_options)?; - let registry = CachedPackageRegistry::create(url, self.account).await?; - match self.query { - MoveListQuery::Packages => { - for name in registry.package_names() { - let data = registry.get_package(name).await?; - println!("package {}", data.name()); - println!(" upgrade_policy: {}", data.upgrade_policy()); - println!(" upgrade_number: {}", data.upgrade_number()); - println!(" source_digest: {}", data.source_digest()); - println!(" modules: {}", data.module_names().into_iter().join(", ")); - } - }, - } - Ok("list succeeded") - } -} - -/// Cleans derived artifacts of a package. -#[derive(Parser)] -pub struct CleanPackage { - #[clap(flatten)] - pub(crate) move_options: MovePackageDir, - #[clap(flatten)] - pub(crate) prompt_options: PromptOptions, -} - -#[async_trait] -impl CliCommand<&'static str> for CleanPackage { - fn command_name(&self) -> &'static str { - "CleanPackage" - } - - async fn execute(self) -> CliTypedResult<&'static str> { - let path = self.move_options.get_package_path()?; - let build_dir = path.join("build"); - // Only remove the build dir if it exists, allowing for users to still clean their cache - if build_dir.exists() { - std::fs::remove_dir_all(build_dir.as_path()) - .map_err(|e| CliError::IO(build_dir.display().to_string(), e))?; - } - - let move_dir = PathBuf::from(MOVE_HOME.as_str()); - if move_dir.exists() - && prompt_yes_with_override( - &format!( - "Do you also want to delete the local package download cache at `{}`?", - move_dir.display() - ), - self.prompt_options, - ) - .is_ok() - { - std::fs::remove_dir_all(move_dir.as_path()) - .map_err(|e| CliError::IO(move_dir.display().to_string(), e))?; - } - Ok("succeeded") - } -} - -/// Run a Move function -#[derive(Parser)] -pub struct RunFunction { - #[clap(flatten)] - pub(crate) entry_function_args: EntryFunctionArguments, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for RunFunction { - fn command_name(&self) -> &'static str { - "RunFunction" - } - - async fn execute(self) -> CliTypedResult { - let payload = TransactionPayload::EntryFunction( - self.entry_function_args.create_entry_function_payload()?, - ); - profile_or_submit(payload, &self.txn_options).await - } -} - -/// Run a view function -#[derive(Parser)] -pub struct ViewFunction { - #[clap(flatten)] - pub(crate) entry_function_args: EntryFunctionArguments, - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand> for ViewFunction { - fn command_name(&self) -> &'static str { - "RunViewFunction" - } - - async fn execute(self) -> CliTypedResult> { - let mut args: Vec = vec![]; - for arg in self.entry_function_args.arg_vec.args { - args.push(arg.to_json()?); - } - - let view_request = ViewRequest { - function: EntryFunctionId { - module: self.entry_function_args.function_id.module_id.into(), - name: self.entry_function_args.function_id.member_id.into(), - }, - type_arguments: self.entry_function_args.type_args, - arguments: args, - }; - - self.txn_options.view(view_request).await - } -} - -/// Run a Move script -#[derive(Parser)] -pub struct RunScript { - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) compile_proposal_args: CompileScriptFunction, - #[clap(flatten)] - pub(crate) arg_vec: ArgWithTypeVec, - /// TypeTag arguments separated by spaces. - /// - /// Example: `u8 u16 u32 u64 u128 u256 bool address vector signer` - #[clap(long, multiple_values = true)] - pub(crate) type_args: Vec, -} - -#[async_trait] -impl CliCommand for RunScript { - fn command_name(&self) -> &'static str { - "RunScript" - } - - async fn execute(self) -> CliTypedResult { - let (bytecode, _script_hash) = self - .compile_proposal_args - .compile("RunScript", self.txn_options.prompt_options)?; - - let mut args: Vec = vec![]; - for arg in self.arg_vec.args { - args.push(arg.try_into()?); - } - - let mut type_args: Vec = Vec::new(); - - // These TypeArgs are used for generics - for type_arg in self.type_args.into_iter() { - let type_tag = TypeTag::try_from(type_arg) - .map_err(|err| CliError::UnableToParse("--type-args", err.to_string()))?; - type_args.push(type_tag) - } - - let payload = TransactionPayload::Script(Script::new(bytecode, type_args, args)); - - profile_or_submit(payload, &self.txn_options).await - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) enum FunctionArgType { - Address, - Bool, - Hex, - String, - U8, - U16, - U32, - U64, - U128, - U256, - Raw, -} - -impl Display for FunctionArgType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - FunctionArgType::Address => write!(f, "address"), - FunctionArgType::Bool => write!(f, "bool"), - FunctionArgType::Hex => write!(f, "hex"), - FunctionArgType::String => write!(f, "string"), - FunctionArgType::U8 => write!(f, "u8"), - FunctionArgType::U16 => write!(f, "u16"), - FunctionArgType::U32 => write!(f, "u32"), - FunctionArgType::U64 => write!(f, "u64"), - FunctionArgType::U128 => write!(f, "u128"), - FunctionArgType::U256 => write!(f, "u256"), - FunctionArgType::Raw => write!(f, "raw"), - } - } -} - -impl FunctionArgType { - /// Parse a standalone argument (not a vector) from string slice into BCS representation. - fn parse_arg_str(&self, arg: &str) -> CliTypedResult> { - match self { - FunctionArgType::Address => bcs::to_bytes( - &load_account_arg(arg) - .map_err(|err| CliError::UnableToParse("address", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::Bool => bcs::to_bytes( - &bool::from_str(arg) - .map_err(|err| CliError::UnableToParse("bool", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::Hex => bcs::to_bytes( - &hex::decode(arg).map_err(|err| CliError::UnableToParse("hex", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::String => bcs::to_bytes(arg).map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::U8 => bcs::to_bytes( - &u8::from_str(arg).map_err(|err| CliError::UnableToParse("u8", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::U16 => bcs::to_bytes( - &u16::from_str(arg) - .map_err(|err| CliError::UnableToParse("u16", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::U32 => bcs::to_bytes( - &u32::from_str(arg) - .map_err(|err| CliError::UnableToParse("u32", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::U64 => bcs::to_bytes( - &u64::from_str(arg) - .map_err(|err| CliError::UnableToParse("u64", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::U128 => bcs::to_bytes( - &u128::from_str(arg) - .map_err(|err| CliError::UnableToParse("u128", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::U256 => bcs::to_bytes( - &U256::from_str(arg) - .map_err(|err| CliError::UnableToParse("u256", err.to_string()))?, - ) - .map_err(|err| CliError::BCS("arg", err)), - FunctionArgType::Raw => { - hex::decode(arg).map_err(|err| CliError::UnableToParse("raw", err.to_string())) - }, - } - } - - /// Recursively parse argument JSON into BCS representation. - pub fn parse_arg_json(&self, arg: &serde_json::Value) -> CliTypedResult { - match arg { - serde_json::Value::Bool(value) => Ok(ArgWithType { - _ty: self.clone(), - _vector_depth: 0, - arg: self.parse_arg_str(value.to_string().as_str())?, - }), - serde_json::Value::Number(value) => Ok(ArgWithType { - _ty: self.clone(), - _vector_depth: 0, - arg: self.parse_arg_str(value.to_string().as_str())?, - }), - serde_json::Value::String(value) => Ok(ArgWithType { - _ty: self.clone(), - _vector_depth: 0, - arg: self.parse_arg_str(value.as_str())?, - }), - serde_json::Value::Array(_) => { - let mut bcs: Vec = vec![]; // BCS representation of argument. - let mut common_sub_arg_depth = None; - // Prepend argument sequence length to BCS bytes vector. - write_u64_as_uleb128(&mut bcs, arg.as_array().unwrap().len()); - // Loop over all of the vector's sub-arguments, which may also be vectors: - for sub_arg in arg.as_array().unwrap() { - let ArgWithType { - _ty: _, - _vector_depth: sub_arg_depth, - arg: mut sub_arg_bcs, - } = self.parse_arg_json(sub_arg)?; - // Verify all sub-arguments have same depth. - if let Some(check_depth) = common_sub_arg_depth { - if check_depth != sub_arg_depth { - return Err(CliError::CommandArgumentError( - "Variable vector depth".to_string(), - )); - } - }; - common_sub_arg_depth = Some(sub_arg_depth); - bcs.append(&mut sub_arg_bcs); // Append sub-argument BCS. - } - // Default sub-argument depth is 0 for when no sub-arguments were looped over. - Ok(ArgWithType { - _ty: self.clone(), - _vector_depth: common_sub_arg_depth.unwrap_or(0) + 1, - arg: bcs, - }) - }, - serde_json::Value::Null => { - Err(CliError::CommandArgumentError("Null argument".to_string())) - }, - serde_json::Value::Object(_) => Err(CliError::CommandArgumentError( - "JSON object argument".to_string(), - )), - } - } -} - -// TODO use from move_binary_format::file_format_common if it is made public. -fn write_u64_as_uleb128(binary: &mut Vec, mut val: usize) { - loop { - let cur = val & 0x7F; - if cur != val { - binary.push((cur | 0x80) as u8); - val >>= 7; - } else { - binary.push(cur as u8); - break; - } - } -} - -impl FromStr for FunctionArgType { - type Err = CliError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "address" => Ok(FunctionArgType::Address), - "bool" => Ok(FunctionArgType::Bool), - "hex" => Ok(FunctionArgType::Hex), - "string" => Ok(FunctionArgType::String), - "u8" => Ok(FunctionArgType::U8), - "u16" => Ok(FunctionArgType::U16), - "u32" => Ok(FunctionArgType::U32), - "u64" => Ok(FunctionArgType::U64), - "u128" => Ok(FunctionArgType::U128), - "u256" => Ok(FunctionArgType::U256), - "raw" => Ok(FunctionArgType::Raw), - str => {Err(CliError::CommandArgumentError(format!( - "Invalid arg type '{}'. Must be one of: ['{}','{}','{}','{}','{}','{}','{}','{}','{}','{}','{}']", - str, - FunctionArgType::Address, - FunctionArgType::Bool, - FunctionArgType::Hex, - FunctionArgType::String, - FunctionArgType::U8, - FunctionArgType::U16, - FunctionArgType::U32, - FunctionArgType::U64, - FunctionArgType::U128, - FunctionArgType::U256, - FunctionArgType::Raw))) - } - } - } -} - -/// A parseable arg with a type separated by a colon -#[derive(Debug)] -pub struct ArgWithType { - pub(crate) _ty: FunctionArgType, - pub(crate) _vector_depth: u8, - pub(crate) arg: Vec, -} - -impl ArgWithType { - pub fn address(account_address: AccountAddress) -> Self { - ArgWithType { - _ty: FunctionArgType::Address, - _vector_depth: 0, - arg: bcs::to_bytes(&account_address).unwrap(), - } - } - - pub fn u64(arg: u64) -> Self { - ArgWithType { - _ty: FunctionArgType::U64, - _vector_depth: 0, - arg: bcs::to_bytes(&arg).unwrap(), - } - } - - pub fn bytes(arg: Vec) -> Self { - ArgWithType { - _ty: FunctionArgType::Raw, - _vector_depth: 0, - arg: bcs::to_bytes(&arg).unwrap(), - } - } - - pub fn raw(arg: Vec) -> Self { - ArgWithType { - _ty: FunctionArgType::Raw, - _vector_depth: 0, - arg, - } - } - - pub fn bcs_value_to_json<'a, T: Deserialize<'a> + Serialize>( - &'a self, - ) -> CliTypedResult { - match self._vector_depth { - 0 => serde_json::to_value(bcs::from_bytes::(&self.arg)?) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - 1 => serde_json::to_value(bcs::from_bytes::>(&self.arg)?) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - - 2 => serde_json::to_value(bcs::from_bytes::>>(&self.arg)?) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - - 3 => serde_json::to_value(bcs::from_bytes::>>>(&self.arg)?) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - - 4 => serde_json::to_value(bcs::from_bytes::>>>>(&self.arg)?) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - 5 => serde_json::to_value(bcs::from_bytes::>>>>>(&self.arg)?) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - 6 => serde_json::to_value(bcs::from_bytes::>>>>>>( - &self.arg, - )?) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - 7 => serde_json::to_value(bcs::from_bytes::>>>>>>>( - &self.arg, - )?) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - depth => Err(CliError::UnexpectedError(format!( - "Vector of depth {depth} is overly nested" - ))), - } - } - - pub fn to_json(&self) -> CliTypedResult { - match self._ty { - FunctionArgType::Address => self.bcs_value_to_json::(), - FunctionArgType::Bool => self.bcs_value_to_json::(), - FunctionArgType::Hex => self.bcs_value_to_json::>(), - FunctionArgType::String => self.bcs_value_to_json::(), - FunctionArgType::U8 => self.bcs_value_to_json::(), - FunctionArgType::U16 => self.bcs_value_to_json::(), - FunctionArgType::U32 => self.bcs_value_to_json::(), - FunctionArgType::U64 => self.bcs_value_to_json::(), - FunctionArgType::U128 => self.bcs_value_to_json::(), - FunctionArgType::U256 => self.bcs_value_to_json::(), - FunctionArgType::Raw => serde_json::to_value(&self.arg) - .map_err(|err| CliError::UnexpectedError(err.to_string())), - } - .map_err(|err| { - CliError::UnexpectedError(format!("Failed to parse argument to JSON {}", err)) - }) - } -} - -/// Does not support string arguments that contain the following characters: -/// -/// * `,` -/// * `[` -/// * `]` -impl FromStr for ArgWithType { - type Err = CliError; - - fn from_str(s: &str) -> Result { - // Splits on the first colon, returning at most `2` elements - // This is required to support args that contain a colon - let parts: Vec<_> = s.splitn(2, ':').collect(); - if parts.len() != 2 { - return Err(CliError::CommandArgumentError( - "Arguments must be pairs of : e.g. bool:true".to_string(), - )); - } - let ty = FunctionArgType::from_str(parts.first().unwrap())?; - let mut arg = String::from(*parts.last().unwrap()); - // May need to surround with quotes if not an array, so arg can be parsed into JSON. - if !arg.starts_with('[') { - if let FunctionArgType::Address - | FunctionArgType::Hex - | FunctionArgType::String - | FunctionArgType::Raw = ty - { - arg = format!("\"{}\"", arg); - } - } - let json = serde_json::from_str::(arg.as_str()) - .map_err(|err| CliError::UnexpectedError(err.to_string()))?; - ty.parse_arg_json(&json) - } -} - -impl TryInto for ArgWithType { - type Error = CliError; - - fn try_into(self) -> Result { - if self._vector_depth > 0 && self._ty != FunctionArgType::U8 { - return Err(CliError::UnexpectedError( - "Unable to parse non-u8 vector to transaction argument".to_string(), - )); - } - match self._ty { - FunctionArgType::Address => Ok(TransactionArgument::Address(txn_arg_parser( - &self.arg, "address", - )?)), - FunctionArgType::Bool => Ok(TransactionArgument::Bool(txn_arg_parser( - &self.arg, "bool", - )?)), - FunctionArgType::Hex => Ok(TransactionArgument::U8Vector(txn_arg_parser( - &self.arg, "hex", - )?)), - FunctionArgType::String => Ok(TransactionArgument::U8Vector(txn_arg_parser( - &self.arg, "string", - )?)), - FunctionArgType::U8 => match self._vector_depth { - 0 => Ok(TransactionArgument::U8(txn_arg_parser(&self.arg, "u8")?)), - 1 => Ok(TransactionArgument::U8Vector(txn_arg_parser( - &self.arg, - "vector", - )?)), - depth => Err(CliError::UnexpectedError(format!( - "Unable to parse u8 vector of depth {} to transaction argument", - depth - ))), - }, - FunctionArgType::U16 => Ok(TransactionArgument::U16(txn_arg_parser(&self.arg, "u16")?)), - FunctionArgType::U32 => Ok(TransactionArgument::U32(txn_arg_parser(&self.arg, "u32")?)), - FunctionArgType::U64 => Ok(TransactionArgument::U64(txn_arg_parser(&self.arg, "u64")?)), - FunctionArgType::U128 => Ok(TransactionArgument::U128(txn_arg_parser( - &self.arg, "u128", - )?)), - FunctionArgType::U256 => Ok(TransactionArgument::U256(txn_arg_parser( - &self.arg, "u256", - )?)), - FunctionArgType::Raw => Ok(TransactionArgument::U8Vector(txn_arg_parser( - &self.arg, "raw", - )?)), - } - } -} - -fn txn_arg_parser( - data: &[u8], - label: &'static str, -) -> Result { - bcs::from_bytes(data).map_err(|err| CliError::UnableToParse(label, err.to_string())) -} - -/// Identifier of a module member (function or struct). -#[derive(Debug, Clone)] -pub struct MemberId { - pub module_id: ModuleId, - pub member_id: Identifier, -} - -fn parse_member_id(function_id: &str) -> CliTypedResult { - let ids: Vec<&str> = function_id.split_terminator("::").collect(); - if ids.len() != 3 { - return Err(CliError::CommandArgumentError( - "FunctionId is not well formed. Must be of the form
::::" - .to_string(), - )); - } - let address = load_account_arg(ids.first().unwrap())?; - let module = Identifier::from_str(ids.get(1).unwrap()) - .map_err(|err| CliError::UnableToParse("Module Name", err.to_string()))?; - let member_id = Identifier::from_str(ids.get(2).unwrap()) - .map_err(|err| CliError::UnableToParse("Member Name", err.to_string()))?; - let module_id = ModuleId::new(address, module); - Ok(MemberId { - module_id, - member_id, - }) -} - -impl FromStr for MemberId { - type Err = CliError; - - fn from_str(s: &str) -> Result { - parse_member_id(s) - } -} diff --git a/m1/m1-cli/src/move_tool/package_hooks.rs b/m1/m1-cli/src/move_tool/package_hooks.rs deleted file mode 100644 index 208d368c..00000000 --- a/m1/m1-cli/src/move_tool/package_hooks.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{common::types::load_account_arg, move_tool::CachedPackageRegistry}; -use aptos_framework::UPGRADE_POLICY_CUSTOM_FIELD; -use futures::executor::block_on; -use move_package::{ - compilation::package_layout::CompiledPackageLayout, package_hooks::PackageHooks, - source_package::parsed_manifest::CustomDepInfo, -}; -use move_symbol_pool::Symbol; -use reqwest::Url; - -pub fn register_package_hooks() { - move_package::package_hooks::register_package_hooks(Box::new(AptosPackageHooks {})) -} - -struct AptosPackageHooks {} - -impl PackageHooks for AptosPackageHooks { - fn custom_package_info_fields(&self) -> Vec { - vec![UPGRADE_POLICY_CUSTOM_FIELD.to_string()] - } - - fn custom_dependency_key(&self) -> Option { - Some("movement".to_string()) - } - - fn resolve_custom_dependency( - &self, - _dep_name: Symbol, - info: &CustomDepInfo, - ) -> anyhow::Result<()> { - block_on(maybe_download_package(info)) - } -} - -async fn maybe_download_package(info: &CustomDepInfo) -> anyhow::Result<()> { - if !info - .download_to - .join(CompiledPackageLayout::BuildInfo.path()) - .exists() - { - let registry = CachedPackageRegistry::create( - Url::parse(info.node_url.as_str())?, - load_account_arg(info.package_address.as_str())?, - ) - .await?; - let package = registry.get_package(info.package_name).await?; - package.save_package_to_disk(info.download_to.as_path()) - } else { - Ok(()) - } -} diff --git a/m1/m1-cli/src/move_tool/show.rs b/m1/m1-cli/src/move_tool/show.rs deleted file mode 100644 index e1cc0e72..00000000 --- a/m1/m1-cli/src/move_tool/show.rs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use super::IncludedArtifactsArgs; -use crate::common::types::{CliCommand, CliError, CliResult, CliTypedResult, MovePackageDir}; -use anyhow::Context; -use aptos_framework::{BuildOptions, BuiltPackage}; -use aptos_types::transaction::EntryABI; -use async_trait::async_trait; -use clap::{Parser, Subcommand}; - -#[derive(Subcommand)] -pub enum ShowTool { - Abi(ShowAbi), -} - -impl ShowTool { - pub async fn execute_serialized(self) -> CliResult { - match self { - Self::Abi(tool) => tool.execute_serialized().await, - } - } -} - -/// Compile the package and show information about the ABIs of the compiled modules. -/// -/// For example, this would show the function `transfer` in the module `coin`: -/// -/// aptos move show abi --modules coin --names transfer -/// -#[derive(Parser)] -pub struct ShowAbi { - /// If provided, only show items from the given Move modules. These should be module - /// names, not file paths. For example, `coin`. - #[clap(long, multiple_values = true)] - modules: Vec, - - /// If provided, only show items with the given names. For example, `transfer`. - #[clap(long, multiple_values = true)] - names: Vec, - - #[clap(flatten)] - included_artifacts_args: IncludedArtifactsArgs, - - #[clap(flatten)] - move_options: MovePackageDir, -} - -#[async_trait] -impl CliCommand> for ShowAbi { - fn command_name(&self) -> &'static str { - "ShowAbi" - } - - async fn execute(self) -> CliTypedResult> { - let build_options = BuildOptions { - install_dir: self.move_options.output_dir.clone(), - with_abis: true, - ..self - .included_artifacts_args - .included_artifacts - .build_options( - self.move_options.skip_fetch_latest_git_deps, - self.move_options.named_addresses(), - self.move_options.bytecode_version, - ) - }; - - // Build the package. - let package = BuiltPackage::build(self.move_options.get_package_path()?, build_options) - .map_err(|e| CliError::MoveCompilationError(format!("{:#}", e)))?; - - // Get ABIs from the package. - let abis = package - .extract_abis() - .context("No ABIs found after compilation")?; - - // Filter the ABIs based on the filters passed in. - let abis = abis - .into_iter() - .filter(|abi| { - let name = abi.name().to_string(); - if !self.names.is_empty() && !self.names.contains(&name) { - return false; - } - match &abi { - EntryABI::EntryFunction(func) => { - if !self.modules.is_empty() - && !self - .modules - .contains(&func.module_name().name().to_string()) - { - return false; - } - }, - EntryABI::TransactionScript(_) => { - // If there were any modules specified we ignore scripts. - if !self.modules.is_empty() { - return false; - } - }, - } - true - }) - .collect(); - - Ok(abis) - } -} diff --git a/m1/m1-cli/src/move_tool/stored_package.rs b/m1/m1-cli/src/move_tool/stored_package.rs deleted file mode 100644 index c957c4c5..00000000 --- a/m1/m1-cli/src/move_tool/stored_package.rs +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::bail; -use aptos_framework::{ - natives::code::{ModuleMetadata, PackageMetadata, PackageRegistry, UpgradePolicy}, - unzip_metadata_str, -}; -use aptos_rest_client::Client; -use aptos_types::account_address::AccountAddress; -use move_package::compilation::package_layout::CompiledPackageLayout; -use reqwest::Url; -use std::{fs, path::Path}; - -// TODO: this is a first naive implementation of the package registry. Before mainnet -// we need to use tables for the package registry. - -/// Represents the package registry at a given account. -pub struct CachedPackageRegistry { - inner: PackageRegistry, -} - -/// Represents the package metadata found in an registry. -pub struct CachedPackageMetadata<'a> { - metadata: &'a PackageMetadata, -} - -/// Represents the package metadata found in an registry. -pub struct CachedModuleMetadata<'a> { - metadata: &'a ModuleMetadata, -} - -impl CachedPackageRegistry { - /// Creates a new registry. - pub async fn create(url: Url, addr: AccountAddress) -> anyhow::Result { - let client = Client::new(url); - // Need to use a different type to deserialize JSON - let inner = client - .get_account_resource_bcs::(addr, "0x1::code::PackageRegistry") - .await? - .into_inner(); - Ok(Self { inner }) - } - - /// Returns the list of packages in this registry by name. - pub fn package_names(&self) -> Vec<&str> { - self.inner - .packages - .iter() - .map(|p| p.name.as_str()) - .collect() - } - - /// Finds the metadata for the given module in the registry by its unique name. - pub async fn get_module<'a>( - &self, - name: impl AsRef, - ) -> anyhow::Result> { - let name = name.as_ref(); - for package in &self.inner.packages { - for module in &package.modules { - if module.name == name { - return Ok(CachedModuleMetadata { metadata: module }); - } - } - } - bail!("module `{}` not found", name) - } - - /// Finds the metadata for the given package in the registry by its unique name. - pub async fn get_package<'a>( - &self, - name: impl AsRef, - ) -> anyhow::Result> { - let name = name.as_ref(); - for package in &self.inner.packages { - if package.name == name { - return Ok(CachedPackageMetadata { metadata: package }); - } - } - bail!("package `{}` not found", name) - } -} - -impl<'a> CachedPackageMetadata<'a> { - pub fn name(&self) -> &str { - &self.metadata.name - } - - pub fn upgrade_policy(&self) -> UpgradePolicy { - self.metadata.upgrade_policy - } - - pub fn upgrade_number(&self) -> u64 { - self.metadata.upgrade_number - } - - pub fn source_digest(&self) -> &str { - &self.metadata.source_digest - } - - pub fn manifest(&self) -> anyhow::Result { - unzip_metadata_str(&self.metadata.manifest) - } - - pub fn module_names(&self) -> Vec<&str> { - self.metadata - .modules - .iter() - .map(|s| s.name.as_str()) - .collect() - } - - pub fn module(&self, name: impl AsRef) -> anyhow::Result> { - let name = name.as_ref(); - for module in &self.metadata.modules { - if module.name == name { - return Ok(CachedModuleMetadata { metadata: module }); - } - } - bail!("module `{}` not found", name) - } - - pub fn save_package_to_disk(&self, path: &Path) -> anyhow::Result<()> { - fs::create_dir_all(path)?; - fs::write( - path.join("Move.toml"), - unzip_metadata_str(&self.metadata.manifest)?, - )?; - let sources_dir = path.join(CompiledPackageLayout::Sources.path()); - fs::create_dir_all(&sources_dir)?; - for module in &self.metadata.modules { - let source = match module.source.is_empty() { - true => { - println!("module without code: {}", module.name); - "".into() - }, - false => unzip_metadata_str(&module.source)?, - }; - fs::write(sources_dir.join(format!("{}.move", module.name)), source)?; - } - Ok(()) - } - - pub fn verify(&self, package_metadata: &PackageMetadata) -> anyhow::Result<()> { - let self_metadata = self.metadata; - - if self_metadata.name != package_metadata.name { - bail!( - "Package name doesn't match {} : {}", - package_metadata.name, - self_metadata.name - ) - } else if self_metadata.deps != package_metadata.deps { - bail!( - "Dependencies don't match {:?} : {:?}", - package_metadata.deps, - self_metadata.deps - ) - } else if self_metadata.modules != package_metadata.modules { - bail!( - "Modules don't match {:?} : {:?}", - package_metadata.modules, - self_metadata.modules - ) - } else if self_metadata.manifest != package_metadata.manifest { - bail!( - "Manifest doesn't match {:?} : {:?}", - package_metadata.manifest, - self_metadata.manifest - ) - } else if self_metadata.upgrade_policy != package_metadata.upgrade_policy { - bail!( - "Upgrade policy doesn't match {:?} : {:?}", - package_metadata.upgrade_policy, - self_metadata.upgrade_policy - ) - } else if self_metadata.upgrade_number != package_metadata.upgrade_number { - bail!( - "Upgrade number doesn't match {:?} : {:?}", - package_metadata.upgrade_number, - self_metadata.upgrade_number - ) - } else if self_metadata.extension != package_metadata.extension { - bail!( - "Extensions doesn't match {:?} : {:?}", - package_metadata.extension, - self_metadata.extension - ) - } else if self_metadata.source_digest != package_metadata.source_digest { - bail!( - "Source digests doesn't match {:?} : {:?}", - package_metadata.source_digest, - self_metadata.source_digest - ) - } - - Ok(()) - } -} - -impl<'a> CachedModuleMetadata<'a> { - pub fn name(&self) -> &str { - &self.metadata.name - } - - pub fn zipped_source(&self) -> &[u8] { - &self.metadata.source - } - - pub fn zipped_source_map_raw(&self) -> &[u8] { - &self.metadata.source_map - } -} diff --git a/m1/m1-cli/src/move_tool/transactional_tests_runner.rs b/m1/m1-cli/src/move_tool/transactional_tests_runner.rs deleted file mode 100644 index 91b504a4..00000000 --- a/m1/m1-cli/src/move_tool/transactional_tests_runner.rs +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::common::types::{CliError, CliTypedResult}; -/// Most of the code below comes from the crate `datatest-stable`. Because the limitation of `datatest-stable`, -/// we are not able to run transactional tests as a subcommand of the Movement CLI. Therefore, we need to duplicate code -/// here and make minor modifications. -/// -use clap::Parser; -use std::{ - io::{self, Write}, - num::NonZeroUsize, - panic::{catch_unwind, AssertUnwindSafe}, - path::{Path, PathBuf}, - process, - sync::mpsc::{channel, Sender}, - thread, -}; -use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; - -type Result = std::result::Result>; - -/// Run Move transactional tests -#[derive(Parser, Clone)] -pub struct TransactionalTestOpts { - /// The filter string is tested against the name of all tests, and only those tests whose names - /// contain the filter are run. - #[clap(long)] - pub filter: Option, - - /// Exactly match filters rather than match by substring - #[clap(long = "exact")] - pub filter_exact: bool, - - /// Number of threads used for running tests in parallel - #[clap(long, default_value = "32")] - pub test_threads: NonZeroUsize, - - /// Output minimal information - #[clap(long)] - pub quiet: bool, - - /// List all tests - #[clap(long)] - pub list: bool, - - /// Path to contain the tests - #[clap(long, parse(from_os_str))] - pub root_path: PathBuf, - - /// Pattern to match the test files - #[clap(long, default_value = r".*\.(mvir|move)$")] - pub pattern: String, -} - -/// Helper function to iterate through all the files in the given directory, skipping hidden files, -/// and return an iterator of their paths. -pub fn iterate_directory(path: &Path) -> impl Iterator { - walkdir::WalkDir::new(path) - .into_iter() - .map(::std::result::Result::unwrap) - .filter(|entry| { - entry.file_type().is_file() - && entry - .file_name() - .to_str() - .map_or(false, |s| !s.starts_with('.')) // Skip hidden files - }) - .map(|entry| entry.path().to_path_buf()) -} - -pub fn derive_test_name(root: &Path, path: &Path, test_name: &str) -> String { - let relative = path.strip_prefix(root).unwrap_or_else(|_| { - panic!( - "failed to strip prefix '{}' from path '{}'", - root.display(), - path.display() - ) - }); - let mut test_name = test_name.to_string(); - test_name = format!("{}::{}", test_name, relative.display()); - test_name -} - -struct Test { - testfn: Box Result<()> + Send>, - name: String, -} - -enum TestResult { - Ok, - Failed, - FailedWithMsg(String), -} - -pub(crate) fn runner(options: &TransactionalTestOpts, reqs: &[Requirements]) -> CliTypedResult<()> { - let mut tests: Vec = reqs.iter().flat_map(|req| req.expand()).collect(); - tests.sort_by(|a, b| a.name.cmp(&b.name)); - - if options.list { - for test in &tests { - println!("{}: test", test.name); - } - - return Ok(()); - } - - match run_tests(options, tests) { - Ok(true) => Ok(()), - Ok(false) => process::exit(101), - Err(e) => Err(CliError::UnexpectedError(format!( - "error: io error when running tests: {:?}", - e - ))), - } -} - -fn run_tests(options: &TransactionalTestOpts, tests: Vec) -> io::Result { - let total = tests.len(); - - // Filter out tests - let mut remaining = match &options.filter { - None => tests, - Some(filter) => tests - .into_iter() - .filter(|test| { - if options.filter_exact { - test.name == filter[..] - } else { - test.name.contains(&filter[..]) - } - }) - .rev() - .collect(), - }; - - let filtered_out = total - remaining.len(); - let mut summary = TestSummary::new(total, filtered_out); - - if !options.quiet { - summary.write_starting_msg()?; - } - - let (tx, rx) = channel(); - - let mut pending = 0; - while pending > 0 || !remaining.is_empty() { - while pending < options.test_threads.get() && !remaining.is_empty() { - let test = remaining.pop().unwrap(); - run_test(test, tx.clone()); - pending += 1; - } - - let (name, result) = rx.recv().unwrap(); - summary.handle_result(name, result)?; - - pending -= 1; - } - - // Write Test Summary - if !options.quiet { - summary.write_summary()?; - } - - Ok(summary.success()) -} - -fn run_test(test: Test, channel: Sender<(String, TestResult)>) { - let Test { name, testfn } = test; - - let cfg = thread::Builder::new().name(name.clone()); - cfg.spawn(move || { - let result = match catch_unwind(AssertUnwindSafe(testfn)) { - Ok(Ok(())) => TestResult::Ok, - Ok(Err(e)) => TestResult::FailedWithMsg(format!("{:?}", e)), - Err(_) => TestResult::Failed, - }; - - channel.send((name, result)).unwrap(); - }) - .unwrap(); -} - -struct TestSummary { - stdout: StandardStream, - total: usize, - filtered_out: usize, - passed: usize, - failed: Vec, -} - -impl TestSummary { - fn new(total: usize, filtered_out: usize) -> Self { - Self { - stdout: StandardStream::stdout(ColorChoice::Auto), - total, - filtered_out, - passed: 0, - failed: Vec::new(), - } - } - - fn handle_result(&mut self, name: String, result: TestResult) -> io::Result<()> { - write!(self.stdout, "test {} ... ", name)?; - match result { - TestResult::Ok => { - self.passed += 1; - self.write_ok()?; - }, - TestResult::Failed => { - self.failed.push(name); - self.write_failed()?; - }, - TestResult::FailedWithMsg(msg) => { - self.failed.push(name); - self.write_failed()?; - writeln!(self.stdout)?; - - write!(self.stdout, "Error: {}", msg)?; - }, - } - writeln!(self.stdout)?; - Ok(()) - } - - fn write_ok(&mut self) -> io::Result<()> { - self.stdout - .set_color(ColorSpec::new().set_fg(Some(Color::Green)))?; - write!(self.stdout, "ok")?; - self.stdout.reset()?; - Ok(()) - } - - fn write_failed(&mut self) -> io::Result<()> { - self.stdout - .set_color(ColorSpec::new().set_fg(Some(Color::Red)))?; - write!(self.stdout, "FAILED")?; - self.stdout.reset()?; - Ok(()) - } - - fn write_starting_msg(&mut self) -> io::Result<()> { - writeln!(self.stdout)?; - writeln!( - self.stdout, - "running {} tests", - self.total - self.filtered_out - )?; - Ok(()) - } - - fn write_summary(&mut self) -> io::Result<()> { - // Print out the failing tests - if !self.failed.is_empty() { - writeln!(self.stdout)?; - writeln!(self.stdout, "failures:")?; - for name in &self.failed { - writeln!(self.stdout, " {}", name)?; - } - } - - writeln!(self.stdout)?; - write!(self.stdout, "test result: ")?; - if self.failed.is_empty() { - self.write_ok()?; - } else { - self.write_failed()?; - } - writeln!( - self.stdout, - ". {} passed; {} failed; {} filtered out", - self.passed, - self.failed.len(), - self.filtered_out - )?; - writeln!(self.stdout)?; - Ok(()) - } - - fn success(&self) -> bool { - self.failed.is_empty() - } -} - -#[doc(hidden)] -pub struct Requirements { - test: fn(&Path) -> Result<()>, - test_name: String, - root: String, - pattern: String, -} - -impl Requirements { - #[doc(hidden)] - pub fn new( - test: fn(&Path) -> Result<()>, - test_name: String, - root: String, - pattern: String, - ) -> Self { - Self { - test, - test_name, - root, - pattern, - } - } - - /// Generate standard test descriptors ([`test::TestDescAndFn`]) from the descriptor of - /// `#[datatest::files(..)]`. - /// - /// Scans all files in a given directory, finds matching ones and generates a test descriptor - /// for each of them. - fn expand(&self) -> Vec { - let root = Path::new(&self.root).to_path_buf(); - - let re = regex::Regex::new(&self.pattern) - .unwrap_or_else(|_| panic!("invalid regular expression: '{}'", self.pattern)); - - let tests: Vec<_> = iterate_directory(&root) - .filter_map(|path| { - let input_path = path.to_string_lossy(); - if re.is_match(&input_path) { - let testfn = self.test; - let name = derive_test_name(&root, &path, &self.test_name); - let testfn = Box::new(move || (testfn)(&path)); - - Some(Test { testfn, name }) - } else { - None - } - }) - .collect(); - - // We want to avoid silent fails due to typos in regexp! - if tests.is_empty() { - panic!( - "no test cases found for test '{}'. Scanned directory: '{}' with pattern '{}'", - self.test_name, self.root, self.pattern, - ); - } - - tests - } -} diff --git a/m1/m1-cli/src/node/analyze/analyze_validators.rs b/m1/m1-cli/src/node/analyze/analyze_validators.rs deleted file mode 100644 index a5fa20b1..00000000 --- a/m1/m1-cli/src/node/analyze/analyze_validators.rs +++ /dev/null @@ -1,540 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use super::fetch_metadata::ValidatorInfo; -use anyhow::Result; -use aptos_bitvec::BitVec; -use aptos_rest_client::VersionedNewBlockEvent; -use aptos_storage_interface::{DbReader, Order}; -use aptos_types::{ - account_address::AccountAddress, - account_config::{new_block_event_key, NewBlockEvent}, -}; -use itertools::Itertools; -use std::{cmp::Ordering, collections::HashMap, convert::TryFrom, ops::Add}; - -/// Single validator stats -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct ValidatorStats { - /// Number of successful proposals - pub proposal_successes: u32, - /// Number of failed proposals - pub proposal_failures: u32, - /// Number of votes proposals - pub votes: u32, - /// Number of transactions in a block - pub transactions: u32, - /// Voting power - pub voting_power: u64, -} - -impl ValidatorStats { - /// Proposal failure rate - pub fn failure_rate(&self) -> f32 { - (self.proposal_failures as f32) / (self.proposal_failures + self.proposal_successes) as f32 - } - - /// Whether node is proposing well enough - pub fn is_reliable(&self) -> bool { - (self.proposal_successes > 0) && (self.failure_rate() < 0.1) - } - - // Whether node is voting well enough - pub fn is_voting_enough(&self, rounds: u32) -> bool { - self.votes as f32 > rounds as f32 * 0.3 - } -} - -#[derive(Debug, Eq, PartialEq, Hash)] -pub enum NodeState { - // Proposal failure < 10%, >30% votes - Reliable, - // Proposal failure < 10%, <30% votes - ReliableLowVotes, - // Has successful proposals, but proposal failure > 10% - AliveUnreliable, - // No successful proposals, but voting - OnlyVoting, - // Not participating in consensus - NotParticipatingInConsensus, - // Not in ValidatorSet - Absent, -} - -impl NodeState { - pub fn to_char(&self) -> &str { - match self { - Self::Reliable => "+", - Self::ReliableLowVotes => "P", - Self::AliveUnreliable => "~", - Self::OnlyVoting => "V", - Self::NotParticipatingInConsensus => "X", - Self::Absent => " ", - } - } - - // Large the value, the worse the node is performing. - pub fn to_order_weight(&self) -> usize { - match self { - Self::Reliable => 0, - Self::ReliableLowVotes => 100, - Self::AliveUnreliable => 10000, - Self::OnlyVoting => 1000000, - Self::NotParticipatingInConsensus => 100000000, - Self::Absent => 1, - } - } -} - -impl Add for ValidatorStats { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self { - proposal_successes: self.proposal_successes + other.proposal_successes, - proposal_failures: self.proposal_failures + other.proposal_failures, - votes: self.votes + other.votes, - transactions: self.transactions + other.transactions, - voting_power: 0, // cannot aggregate voting power. - } - } -} - -/// Statistics for all validators -#[derive(Clone)] -pub struct EpochStats { - /// Statistics for each of the validators - pub validator_stats: HashMap, - /// Total rounds in an epoch - pub total_rounds: u32, - /// Total transactions in an epoch - pub total_transactions: u32, - /// Successful rounds in an epoch - pub round_successes: u32, - /// Failed rounds in an epoch - pub round_failures: u32, - /// Nil blocks in an epoch - pub nil_blocks: u32, - /// Total voting power - pub total_voting_power: u128, -} - -impl EpochStats { - pub fn to_state(&self, validator: &AccountAddress) -> NodeState { - self.validator_stats - .get(validator) - .map(|b| { - if b.is_reliable() { - if b.is_voting_enough(self.total_rounds) { - NodeState::Reliable - } else { - NodeState::ReliableLowVotes - } - } else if b.proposal_successes > 0 { - NodeState::AliveUnreliable - } else if b.votes > 0 { - NodeState::OnlyVoting - } else { - NodeState::NotParticipatingInConsensus - } - }) - .unwrap_or(NodeState::Absent) - } - - pub fn to_votes(&self, validator: &AccountAddress) -> u32 { - self.validator_stats - .get(validator) - .map(|s| s.votes) - .unwrap_or(0) - } - - pub fn to_voting_power(&self, validator: &AccountAddress) -> u64 { - self.validator_stats - .get(validator) - .map(|s| s.voting_power) - .unwrap_or(0) - } -} - -impl Add for EpochStats { - type Output = Self; - - fn add(self, other: Self) -> Self { - let mut validator_stats = self.validator_stats; - for (key, other_validator_stats) in other.validator_stats.into_iter() { - validator_stats.insert( - key, - other_validator_stats - + *validator_stats.get(&key).unwrap_or(&ValidatorStats { - proposal_failures: 0, - proposal_successes: 0, - votes: 0, - transactions: 0, - voting_power: 0, - }), - ); - } - Self { - validator_stats, - total_rounds: self.total_rounds + other.total_rounds, - round_successes: self.round_successes + other.round_successes, - round_failures: self.round_failures + other.round_failures, - nil_blocks: self.nil_blocks + other.nil_blocks, - total_transactions: self.total_transactions + other.total_transactions, - total_voting_power: 0, - } - } -} - -/// Analyze validator performance -pub struct AnalyzeValidators {} - -impl AnalyzeValidators { - /// Fetch all events from a single epoch from DB. - pub fn fetch_epoch(epoch: u64, aptos_db: &dyn DbReader) -> Result> { - let batch = 100; - - let mut cursor = u64::max_value(); - let mut result: Vec = vec![]; - let ledger_version = aptos_db.get_latest_ledger_info()?.ledger_info().version(); - - loop { - let raw_events = aptos_db.get_events( - &new_block_event_key(), - cursor, - Order::Descending, - batch as u64, - ledger_version, - )?; - let end = raw_events.len() < batch; - for raw_event in raw_events { - if cursor <= raw_event.event.sequence_number() { - println!( - "Duplicate event found for {} : {:?}", - cursor, - raw_event.event.sequence_number() - ); - } else { - cursor = raw_event.event.sequence_number(); - let event = bcs::from_bytes::(raw_event.event.event_data())?; - - match epoch.cmp(&event.epoch()) { - Ordering::Equal => { - result.push(VersionedNewBlockEvent { - event, - version: raw_event.transaction_version, - sequence_number: raw_event.event.sequence_number(), - }); - }, - Ordering::Greater => { - return Ok(result); - }, - Ordering::Less => {}, - }; - } - } - - if end { - return Ok(result); - } - } - } - - /// Analyze single epoch - pub fn analyze(blocks: &[VersionedNewBlockEvent], validators: &[ValidatorInfo]) -> EpochStats { - assert!( - validators.iter().as_slice().windows(2).all(|w| { - w[0].validator_index - .partial_cmp(&w[1].validator_index) - .map(|o| o != Ordering::Greater) - .unwrap_or(false) - }), - "Validators need to be sorted" - ); - assert!( - blocks.iter().as_slice().windows(2).all(|w| { - w[0].event - .round() - .partial_cmp(&w[1].event.round()) - .map(|o| o != Ordering::Greater) - .unwrap_or(false) - }), - "Blocks need to be sorted" - ); - - let mut successes = HashMap::::new(); - let mut failures = HashMap::::new(); - let mut votes = HashMap::::new(); - let mut transactions = HashMap::::new(); - - let mut trimmed_rounds = 0; - let mut nil_blocks = 0; - let mut previous_round = 0; - for (pos, block) in blocks.iter().enumerate() { - let event = &block.event; - let is_nil = event.proposer() == AccountAddress::ZERO; - if is_nil { - nil_blocks += 1; - } - let expected_round = - previous_round + u64::from(!is_nil) + event.failed_proposer_indices().len() as u64; - if event.round() != expected_round { - println!( - "Missing failed AccountAddresss : {} {:?}", - previous_round, &event - ); - assert!(expected_round < event.round()); - trimmed_rounds += event.round() - expected_round; - } - previous_round = event.round(); - - if !is_nil { - *successes.entry(event.proposer()).or_insert(0) += 1; - } - - for failed_proposer_index in event.failed_proposer_indices() { - *failures - .entry(validators[*failed_proposer_index as usize].address) - .or_insert(0) += 1; - } - - let previous_block_votes_bitvec: BitVec = - event.previous_block_votes_bitvec().clone().into(); - assert_eq!( - BitVec::required_buckets(validators.len() as u16), - previous_block_votes_bitvec.num_buckets() - ); - for (i, validator) in validators.iter().enumerate() { - if previous_block_votes_bitvec.is_set(i as u16) { - *votes.entry(validator.address).or_insert(0) += 1; - } - } - - let cur_transactions_option = blocks - .get(pos + 1) - .map(|next| u32::try_from(next.version - block.version - 2).unwrap()); - if let Some(cur_transactions) = cur_transactions_option { - if is_nil { - assert_eq!( - cur_transactions, - 0, - "{} {:?}", - block.version, - blocks.get(pos + 1) - ); - } - *transactions.entry(event.proposer()).or_insert(0) += cur_transactions; - } - } - let total_successes: u32 = successes.values().sum(); - let total_failures: u32 = failures.values().sum(); - let total_transactions: u32 = transactions.values().sum(); - let total_rounds = total_successes + total_failures; - assert_eq!( - total_rounds + u32::try_from(trimmed_rounds).unwrap(), - previous_round as u32, - "{} success + {} failures + {} trimmed != {}", - total_successes, - total_failures, - trimmed_rounds, - previous_round - ); - - return EpochStats { - validator_stats: validators - .iter() - .map(|validator| { - (validator.address, ValidatorStats { - proposal_successes: *successes.get(&validator.address).unwrap_or(&0), - proposal_failures: *failures.get(&validator.address).unwrap_or(&0), - votes: *votes.get(&validator.address).unwrap_or(&0), - transactions: *transactions.get(&validator.address).unwrap_or(&0), - voting_power: validator.voting_power, - }) - }) - .collect(), - total_rounds, - total_transactions, - round_successes: total_successes, - round_failures: total_failures, - nil_blocks, - total_voting_power: validators - .iter() - .map(|validator| validator.voting_power as u128) - .sum(), - }; - } - - /// Print validator stats in a table - pub fn print_detailed_epoch_table( - epoch_stats: &EpochStats, - extra: Option<(&str, &HashMap)>, - sort_by_health: bool, - ) { - println!( - "Rounds: {} successes, {} failures, {} NIL blocks, failure rate: {}%, nil block rate: {}%", - epoch_stats.round_successes, epoch_stats.round_failures, epoch_stats.nil_blocks, - 100.0 * epoch_stats.round_failures as f32 / epoch_stats.total_rounds as f32, - 100.0 * epoch_stats.nil_blocks as f32 / epoch_stats.total_rounds as f32, - ); - println!( - "{: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <30}", - "elected", - "% rounds", - "% failed", - "succeded", - "failed", - "voted", - "transact", - extra.map(|(column, _)| column).unwrap_or("") - ); - - let mut validator_order: Vec<&AccountAddress> = - epoch_stats.validator_stats.keys().collect(); - if sort_by_health { - validator_order.sort_by_cached_key(|v| { - epoch_stats - .validator_stats - .get(v) - .map(|s| { - ( - if s.proposal_successes > 0 { - (s.failure_rate() * 100000.0) as u32 - } else { - 200000 - }, - -((s.proposal_failures + s.proposal_successes) as i32), - *v, - ) - }) - .unwrap() - }); - } else { - validator_order.sort(); - } - - for validator in validator_order { - let cur_stats = epoch_stats.validator_stats.get(validator).unwrap(); - println!( - "{: <10} | {:5.2}% | {:7.3}% | {: <10} | {: <10} | {: <10} | {: <10} | {}", - cur_stats.proposal_failures + cur_stats.proposal_successes, - 100.0 * (cur_stats.proposal_failures + cur_stats.proposal_successes) as f32 - / (epoch_stats.total_rounds as f32), - 100.0 * cur_stats.failure_rate(), - cur_stats.proposal_successes, - cur_stats.proposal_failures, - cur_stats.votes, - cur_stats.transactions, - if let Some((_, extra_map)) = extra { - format!( - "{: <30} | {}", - extra_map.get(validator).unwrap_or(&"".to_string()), - validator - ) - } else { - format!("{}", validator) - } - ); - } - } - - pub fn print_validator_health_over_time( - stats: &HashMap, - validators: &[AccountAddress], - extra: Option<&HashMap>, - ) { - let epochs: Vec<_> = stats.keys().sorted().collect(); - - let mut sorted_validators = validators.to_vec(); - sorted_validators.sort_by_cached_key(|validator| { - ( - epochs - .iter() - .map(|cur_epoch| { - stats - .get(cur_epoch) - .unwrap() - .to_state(validator) - .to_order_weight() - }) - .sum::(), - *validator, - ) - }); - - for validator in sorted_validators { - print!( - "{}: ", - if let Some(extra_map) = extra { - format!( - "{: <30} | {}", - extra_map.get(&validator).unwrap_or(&""), - validator - ) - } else { - format!("{}", validator) - } - ); - for cur_epoch in epochs.iter() { - print!( - "{}", - stats.get(cur_epoch).unwrap().to_state(&validator).to_char() - ); - } - println!(); - } - } - - pub fn print_network_health_over_time( - stats: &HashMap, - validators: &[AccountAddress], - ) { - let epochs = stats.keys().sorted(); - - println!( - "{: <8} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10}", - "epoch", - "reliable", - "r low vote", - "unreliable", - "only vote", - "down(cons)", - "rounds", - "#r failed", - "% failure", - "% stake has >10% of votes", - ); - for cur_epoch in epochs { - let epoch_stats = stats.get(cur_epoch).unwrap(); - - let counts = validators.iter().map(|v| epoch_stats.to_state(v)).counts(); - - let voted_voting_power: u128 = validators - .iter() - .flat_map(|v| { - if epoch_stats.to_votes(v) > epoch_stats.round_successes / 10 { - Some(epoch_stats.to_voting_power(v) as u128) - } else { - None - } - }) - .sum(); - - println!( - "{: <8} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {: <10} | {:10.2} | {:10.2}", - cur_epoch, - counts.get(&NodeState::Reliable).unwrap_or(&0), - counts.get(&NodeState::ReliableLowVotes).unwrap_or(&0), - counts.get(&NodeState::AliveUnreliable).unwrap_or(&0), - counts.get(&NodeState::OnlyVoting).unwrap_or(&0), - counts - .get(&NodeState::NotParticipatingInConsensus) - .unwrap_or(&0), - epoch_stats.total_rounds, - epoch_stats.round_failures, - 100.0 * epoch_stats.round_failures as f32 / epoch_stats.total_rounds as f32, - 100.0 * voted_voting_power as f32 / epoch_stats.total_voting_power as f32, - ); - } - } -} diff --git a/m1/m1-cli/src/node/analyze/fetch_metadata.rs b/m1/m1-cli/src/node/analyze/fetch_metadata.rs deleted file mode 100644 index e9ab24e8..00000000 --- a/m1/m1-cli/src/node/analyze/fetch_metadata.rs +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::{anyhow, Result}; -use aptos_rest_client::{ - aptos_api_types::{IdentifierWrapper, MoveResource, WriteSetChange}, - Client as RestClient, Transaction, VersionedNewBlockEvent, -}; -use aptos_types::account_address::AccountAddress; -use std::str::FromStr; - -const MAX_FETCH_BATCH_SIZE: u16 = 1000; - -#[derive(Eq, PartialEq, Clone, Copy, Debug)] -pub struct ValidatorInfo { - pub address: AccountAddress, - pub voting_power: u64, - pub validator_index: u16, -} - -pub struct EpochInfo { - pub epoch: u64, - pub blocks: Vec, - pub validators: Vec, - pub partial: bool, -} - -pub struct FetchMetadata {} - -impl FetchMetadata { - fn get_validator_addresses( - data: &MoveResource, - field_name: &str, - ) -> Result> { - fn extract_validator_address(validator: &serde_json::Value) -> Result { - Ok(ValidatorInfo { - address: AccountAddress::from_hex_literal( - validator.get("addr").unwrap().as_str().unwrap(), - ) - .map_err(|e| anyhow!("Cannot parse address {:?}", e))?, - voting_power: validator - .get("voting_power") - .unwrap() - .as_str() - .unwrap() - .parse() - .map_err(|e| anyhow!("Cannot parse voting_power {:?}", e))?, - validator_index: validator - .get("config") - .unwrap() - .get("validator_index") - .unwrap() - .as_str() - .unwrap() - .parse() - .map_err(|e| anyhow!("Cannot parse validator_index {:?}", e))?, - }) - } - - let validators_json = data - .data - .0 - .get(&IdentifierWrapper::from_str(field_name).unwrap()) - .unwrap(); - if let serde_json::Value::Array(validators_array) = validators_json { - let mut validators: Vec = vec![]; - for validator in validators_array { - validators.push(extract_validator_address(validator)?); - } - Ok(validators) - } else { - Err(anyhow!("{} validators not in json", field_name)) - } - } - - async fn get_transactions_in_range( - client: &RestClient, - start: u64, - last: u64, - ) -> Result> { - let mut result = Vec::new(); - let mut cursor = start; - while cursor < last { - let limit = std::cmp::min(MAX_FETCH_BATCH_SIZE as u64, last - cursor) as u16; - let mut current = client - .get_transactions(Some(cursor), Some(limit)) - .await? - .into_inner(); - if current.is_empty() { - return Err(anyhow!( - "No transactions returned with start={} and limit={}", - cursor, - limit - )); - } - cursor += current.len() as u64; - result.append(&mut current); - } - Ok(result) - } - - fn get_validators_from_transaction(transaction: &Transaction) -> Result> { - if let Ok(info) = transaction.transaction_info() { - for change in &info.changes { - if let WriteSetChange::WriteResource(resource) = change { - if resource.data.typ.name.0.as_str() == "ValidatorSet" { - // No pending at epoch change - assert_eq!( - Vec::::new(), - FetchMetadata::get_validator_addresses( - &resource.data, - "pending_inactive" - )? - ); - assert_eq!( - Vec::::new(), - FetchMetadata::get_validator_addresses( - &resource.data, - "pending_active" - )? - ); - return FetchMetadata::get_validator_addresses( - &resource.data, - "active_validators", - ); - } - } - } - } - Err(anyhow!("Couldn't find ValidatorSet in the transaction")) - } - - pub async fn fetch_new_block_events( - client: &RestClient, - start_epoch: Option, - end_epoch: Option, - ) -> Result> { - let (last_events, state) = client - .get_new_block_events_bcs(None, Some(1)) - .await? - .into_parts(); - let mut start_seq_num = state.oldest_block_height; - assert_eq!(last_events.len(), 1, "{:?}", last_events); - let last_event = last_events.first().unwrap(); - let last_seq_num = last_event.sequence_number; - - let wanted_start_epoch = { - let mut wanted_start_epoch = start_epoch.unwrap_or(2); - if wanted_start_epoch < 0 { - wanted_start_epoch = last_event.event.epoch() as i64 + wanted_start_epoch + 1; - } - - let oldest_event = client - .get_new_block_events_bcs(Some(start_seq_num), Some(1)) - .await? - .into_inner() - .into_iter() - .next() - .ok_or_else(|| anyhow!("No blocks at oldest_block_height {}", start_seq_num))?; - let oldest_fetchable_epoch = std::cmp::max(oldest_event.event.epoch() + 1, 2); - if oldest_fetchable_epoch > wanted_start_epoch as u64 { - println!( - "Oldest full epoch that can be retreived is {} ", - oldest_fetchable_epoch - ); - oldest_fetchable_epoch - } else { - wanted_start_epoch as u64 - } - }; - let wanted_end_epoch = { - let mut wanted_end_epoch = end_epoch.unwrap_or(i64::MAX); - if wanted_end_epoch < 0 { - wanted_end_epoch = last_event.event.epoch() as i64 + wanted_end_epoch + 1; - } - std::cmp::min( - last_event.event.epoch() + 1, - std::cmp::max(2, wanted_end_epoch) as u64, - ) - }; - - if wanted_start_epoch > 2 { - let mut search_end = last_seq_num; - - // Stop when search is close enough, and we can then linearly - // proceed from there. - // Since we are ignoring results we are fetching during binary search - // we want to stop when we are close. - while start_seq_num + 20 < search_end { - let mid = (start_seq_num + search_end) / 2; - - let mid_epoch = client - .get_new_block_events_bcs(Some(mid), Some(1)) - .await? - .into_inner() - .first() - .unwrap() - .event - .epoch(); - - if mid_epoch < wanted_start_epoch { - start_seq_num = mid; - } else { - search_end = mid; - } - } - } - - let mut batch_index = 0; - - println!( - "Fetching {} to {} sequence number, wanting epochs [{}, {}), last version: {} and epoch: {}", - start_seq_num, last_seq_num, wanted_start_epoch, wanted_end_epoch, state.version, state.epoch, - ); - let mut result: Vec = vec![]; - if wanted_start_epoch >= wanted_end_epoch { - return Ok(result); - } - - let mut validators: Vec = vec![]; - let mut current: Vec = vec![]; - let mut epoch = 0; - - let mut cursor = start_seq_num; - loop { - let response = client - .get_new_block_events_bcs(Some(cursor), Some(MAX_FETCH_BATCH_SIZE)) - .await; - - if response.is_err() { - println!( - "Failed to read new_block_events beyond {}, stopping. {:?}", - cursor, - response.unwrap_err() - ); - assert!(!validators.is_empty()); - result.push(EpochInfo { - epoch, - blocks: current, - validators: validators.clone(), - partial: true, - }); - return Ok(result); - } - let events = response.unwrap().into_inner(); - - if events.is_empty() { - return Err(anyhow!( - "No transactions returned with start={} and limit={}", - cursor, - MAX_FETCH_BATCH_SIZE - )); - } - - cursor += events.len() as u64; - batch_index += 1; - - for event in events { - if event.event.epoch() > epoch { - if epoch == 0 { - epoch = event.event.epoch(); - current = vec![]; - } else { - let last = current.last().cloned(); - if let Some(last) = last { - let transactions = FetchMetadata::get_transactions_in_range( - client, - last.version, - event.version, - ) - .await?; - assert_eq!( - transactions.first().unwrap().version().unwrap(), - last.version - ); - for transaction in transactions { - if let Ok(new_validators) = - FetchMetadata::get_validators_from_transaction(&transaction) - { - if epoch >= wanted_start_epoch { - assert!(!validators.is_empty()); - result.push(EpochInfo { - epoch, - blocks: current, - validators: validators.clone(), - partial: false, - }); - } - current = vec![]; - - validators = new_validators; - validators.sort_by_key(|v| v.validator_index); - assert_eq!(epoch + 1, event.event.epoch()); - epoch = event.event.epoch(); - if epoch >= wanted_end_epoch { - return Ok(result); - } - break; - } - } - assert!( - current.is_empty(), - "Couldn't find ValidatorSet change for transactions start={}, limit={} for epoch {}", - last.version, - event.version - last.version, - event.event.epoch(), - ); - } - } - } - current.push(event); - } - - if batch_index % 100 == 0 { - println!( - "Fetched {} epochs (in epoch {} with {} blocks) from {} NewBlockEvents", - result.len(), - epoch, - current.len(), - cursor - ); - } - - if cursor > last_seq_num { - if !validators.is_empty() { - result.push(EpochInfo { - epoch, - blocks: current, - validators: validators.clone(), - partial: true, - }); - } - return Ok(result); - } - } - } -} diff --git a/m1/m1-cli/src/node/analyze/mod.rs b/m1/m1-cli/src/node/analyze/mod.rs deleted file mode 100644 index ac8c7016..00000000 --- a/m1/m1-cli/src/node/analyze/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -pub mod analyze_validators; -pub mod fetch_metadata; diff --git a/m1/m1-cli/src/node/mod.rs b/m1/m1-cli/src/node/mod.rs deleted file mode 100644 index 33caaac4..00000000 --- a/m1/m1-cli/src/node/mod.rs +++ /dev/null @@ -1,1730 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -pub mod analyze; - -use crate::{ - common::{ - types::{ - CliCommand, CliError, CliResult, CliTypedResult, ConfigSearchMode, - OptionalPoolAddressArgs, PoolAddressArgs, ProfileOptions, PromptOptions, RestOptions, - TransactionOptions, TransactionSummary, - }, - utils::{prompt_yes_with_override, read_from_file}, - }, - config::GlobalConfig, - genesis::git::from_yaml, - node::analyze::{ - analyze_validators::{AnalyzeValidators, ValidatorStats}, - fetch_metadata::FetchMetadata, - }, -}; -use aptos_backup_cli::{ - coordinators::restore::{RestoreCoordinator, RestoreCoordinatorOpt}, - metadata::cache::MetadataCacheOpt, - storage::command_adapter::{config::CommandAdapterConfig, CommandAdapter}, - utils::{ConcurrentDownloadsOpt, GlobalRestoreOpt, ReplayConcurrencyLevelOpt, RocksdbOpt}, -}; -use aptos_cached_packages::aptos_stdlib; -use aptos_config::config::NodeConfig; -use aptos_crypto::{bls12381, bls12381::PublicKey, x25519, ValidCryptoMaterialStringExt}; -use aptos_faucet_core::server::{FunderKeyEnum, RunConfig}; -use aptos_genesis::config::{HostAndPort, OperatorConfiguration}; -use aptos_network_checker::args::{ - validate_address, CheckEndpointArgs, HandshakeArgs, NodeAddressArgs, -}; -use aptos_rest_client::{aptos_api_types::VersionedEvent, Client, State}; -use aptos_types::{ - account_address::AccountAddress, - account_config::{BlockResource, CORE_CODE_ADDRESS}, - chain_id::ChainId, - network_address::NetworkAddress, - on_chain_config::{ConfigurationResource, ConsensusScheme, ValidatorSet}, - stake_pool::StakePool, - staking_contract::StakingContractStore, - validator_info::ValidatorInfo, - validator_performances::ValidatorPerformances, - vesting::VestingAdminStore, -}; -use async_trait::async_trait; -use bcs::Result; -use chrono::{DateTime, NaiveDateTime, Utc}; -use clap::Parser; -use futures::FutureExt; -use hex::FromHex; -use rand::{rngs::StdRng, SeedableRng}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - convert::{TryFrom, TryInto}, - path::PathBuf, - pin::Pin, - sync::Arc, - thread, - time::Duration, -}; -use tokio::time::Instant; - -const SECS_TO_MICROSECS: u64 = 1_000_000; - -/// Tool for operations related to nodes -/// -/// This tool allows you to run a local test node for testing, -/// identify issues with nodes, and show related information. -#[derive(Parser)] -pub enum NodeTool { - AnalyzeValidatorPerformance(AnalyzeValidatorPerformance), - BootstrapDbFromBackup(BootstrapDbFromBackup), - CheckNetworkConnectivity(CheckNetworkConnectivity), - GetPerformance(GetPerformance), - GetStakePool(GetStakePool), - InitializeValidator(InitializeValidator), - JoinValidatorSet(JoinValidatorSet), - LeaveValidatorSet(LeaveValidatorSet), - ShowEpochInfo(ShowEpochInfo), - ShowValidatorConfig(ShowValidatorConfig), - ShowValidatorSet(ShowValidatorSet), - ShowValidatorStake(ShowValidatorStake), - RunLocalTestnet(RunLocalTestnet), - UpdateConsensusKey(UpdateConsensusKey), - UpdateValidatorNetworkAddresses(UpdateValidatorNetworkAddresses), -} - -impl NodeTool { - pub async fn execute(self) -> CliResult { - use NodeTool::*; - match self { - AnalyzeValidatorPerformance(tool) => tool.execute_serialized().await, - BootstrapDbFromBackup(tool) => tool.execute_serialized().await, - CheckNetworkConnectivity(tool) => tool.execute_serialized().await, - GetPerformance(tool) => tool.execute_serialized().await, - GetStakePool(tool) => tool.execute_serialized().await, - InitializeValidator(tool) => tool.execute_serialized().await, - JoinValidatorSet(tool) => tool.execute_serialized().await, - LeaveValidatorSet(tool) => tool.execute_serialized().await, - ShowEpochInfo(tool) => tool.execute_serialized().await, - ShowValidatorSet(tool) => tool.execute_serialized().await, - ShowValidatorStake(tool) => tool.execute_serialized().await, - ShowValidatorConfig(tool) => tool.execute_serialized().await, - RunLocalTestnet(tool) => tool.execute_serialized_without_logger().await, - UpdateConsensusKey(tool) => tool.execute_serialized().await, - UpdateValidatorNetworkAddresses(tool) => tool.execute_serialized().await, - } - } -} - -#[derive(Parser)] -pub struct OperatorConfigFileArgs { - /// Operator Configuration file - /// - /// Config file created from the `genesis set-validator-configuration` command - #[clap(long, parse(from_os_str))] - pub(crate) operator_config_file: Option, -} - -impl OperatorConfigFileArgs { - fn load(&self) -> CliTypedResult> { - if let Some(ref file) = self.operator_config_file { - Ok(from_yaml( - &String::from_utf8(read_from_file(file)?).map_err(CliError::from)?, - )?) - } else { - Ok(None) - } - } -} - -#[derive(Parser)] -pub struct ValidatorConsensusKeyArgs { - /// Hex encoded Consensus public key - /// - /// The key should be a BLS12-381 public key - #[clap(long, parse(try_from_str = bls12381::PublicKey::from_encoded_string))] - pub(crate) consensus_public_key: Option, - - /// Hex encoded Consensus proof of possession - /// - /// The key should be a BLS12-381 proof of possession - #[clap(long, parse(try_from_str = bls12381::ProofOfPossession::from_encoded_string))] - pub(crate) proof_of_possession: Option, -} - -impl ValidatorConsensusKeyArgs { - fn get_consensus_public_key<'a>( - &'a self, - operator_config: &'a Option, - ) -> CliTypedResult<&'a bls12381::PublicKey> { - let consensus_public_key = if let Some(ref consensus_public_key) = self.consensus_public_key - { - consensus_public_key - } else if let Some(ref operator_config) = operator_config { - &operator_config.consensus_public_key - } else { - return Err(CliError::CommandArgumentError( - "Must provide either --operator-config-file or --consensus-public-key".to_string(), - )); - }; - Ok(consensus_public_key) - } - - fn get_consensus_proof_of_possession<'a>( - &'a self, - operator_config: &'a Option, - ) -> CliTypedResult<&'a bls12381::ProofOfPossession> { - let proof_of_possession = if let Some(ref proof_of_possession) = self.proof_of_possession { - proof_of_possession - } else if let Some(ref operator_config) = operator_config { - &operator_config.consensus_proof_of_possession - } else { - return Err(CliError::CommandArgumentError( - "Must provide either --operator-config-file or --proof-of-possession".to_string(), - )); - }; - Ok(proof_of_possession) - } -} - -#[derive(Parser)] -pub struct ValidatorNetworkAddressesArgs { - /// Host and port pair for the validator - /// - /// e.g. 127.0.0.1:6180 - #[clap(long)] - pub(crate) validator_host: Option, - - /// Validator x25519 public network key - #[clap(long, parse(try_from_str = x25519::PublicKey::from_encoded_string))] - pub(crate) validator_network_public_key: Option, - - /// Host and port pair for the fullnode - /// - /// e.g. 127.0.0.1:6180. Optional - #[clap(long)] - pub(crate) full_node_host: Option, - - /// Full node x25519 public network key - #[clap(long, parse(try_from_str = x25519::PublicKey::from_encoded_string))] - pub(crate) full_node_network_public_key: Option, -} - -impl ValidatorNetworkAddressesArgs { - fn get_network_configs<'a>( - &'a self, - operator_config: &'a Option, - ) -> CliTypedResult<( - x25519::PublicKey, - Option, - &'a HostAndPort, - Option<&'a HostAndPort>, - )> { - let validator_network_public_key = - if let Some(public_key) = self.validator_network_public_key { - public_key - } else if let Some(ref operator_config) = operator_config { - operator_config.validator_network_public_key - } else { - return Err(CliError::CommandArgumentError( - "Must provide either --operator-config-file or --validator-network-public-key" - .to_string(), - )); - }; - - let full_node_network_public_key = - if let Some(public_key) = self.full_node_network_public_key { - Some(public_key) - } else if let Some(ref operator_config) = operator_config { - operator_config.full_node_network_public_key - } else { - None - }; - - let validator_host = if let Some(ref host) = self.validator_host { - host - } else if let Some(ref operator_config) = operator_config { - &operator_config.validator_host - } else { - return Err(CliError::CommandArgumentError( - "Must provide either --operator-config-file or --validator-host".to_string(), - )); - }; - - let full_node_host = if let Some(ref host) = self.full_node_host { - Some(host) - } else if let Some(ref operator_config) = operator_config { - operator_config.full_node_host.as_ref() - } else { - None - }; - - Ok(( - validator_network_public_key, - full_node_network_public_key, - validator_host, - full_node_host, - )) - } -} - -#[derive(Copy, Clone, Debug, Serialize)] -pub enum StakePoolType { - Direct, - StakingContract, - Vesting, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize)] -pub enum StakePoolState { - Active, - Inactive, - PendingActive, - PendingInactive, -} - -#[derive(Debug, Serialize)] -pub struct StakePoolResult { - pub state: StakePoolState, - pub pool_address: AccountAddress, - pub operator_address: AccountAddress, - pub voter_address: AccountAddress, - pub pool_type: StakePoolType, - pub total_stake: u64, - pub commission_percentage: u64, - pub commission_not_yet_unlocked: u64, - pub lockup_expiration_utc_time: DateTime, - pub consensus_public_key: String, - pub validator_network_addresses: Vec, - pub fullnode_network_addresses: Vec, - pub epoch_info: EpochInfo, - #[serde(skip_serializing_if = "Option::is_none")] - pub vesting_contract: Option, -} - -/// Show the stake pool -/// -/// Retrieves the associated stake pool from the multiple types for the given owner address -#[derive(Parser)] -pub struct GetStakePool { - /// The owner address of the stake pool - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub(crate) owner_address: AccountAddress, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -#[async_trait] -impl CliCommand> for GetStakePool { - fn command_name(&self) -> &'static str { - "GetStakePool" - } - - async fn execute(mut self) -> CliTypedResult> { - let owner_address = self.owner_address; - let client = &self.rest_options.client(&self.profile_options)?; - get_stake_pools(client, owner_address).await - } -} - -#[derive(Debug, Serialize)] -pub struct StakePoolPerformance { - current_epoch_successful_proposals: u64, - current_epoch_failed_proposals: u64, - previous_epoch_rewards: Vec, - epoch_info: EpochInfo, -} - -/// Show staking performance of the given staking pool -#[derive(Parser)] -pub struct GetPerformance { - #[clap(flatten)] - pub(crate) pool_address_args: PoolAddressArgs, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -#[async_trait] -impl CliCommand for GetPerformance { - fn command_name(&self) -> &'static str { - "GetPerformance" - } - - async fn execute(mut self) -> CliTypedResult { - let client = &self.rest_options.client(&self.profile_options)?; - let pool_address = self.pool_address_args.pool_address; - let validator_set = &client - .get_account_resource_bcs::(CORE_CODE_ADDRESS, "0x1::stake::ValidatorSet") - .await? - .into_inner(); - - let mut current_epoch_successful_proposals = 0; - let mut current_epoch_failed_proposals = 0; - let state = get_stake_pool_state(validator_set, &pool_address); - if state == StakePoolState::Active || state == StakePoolState::PendingInactive { - let validator_config = client - .get_account_resource_bcs::( - pool_address, - "0x1::stake::ValidatorConfig", - ) - .await? - .into_inner(); - let validator_performances = &client - .get_account_resource_bcs::( - CORE_CODE_ADDRESS, - "0x1::stake::ValidatorPerformance", - ) - .await? - .into_inner(); - let validator_index = validator_config.validator_index as usize; - current_epoch_successful_proposals = - validator_performances.validators[validator_index].successful_proposals; - current_epoch_failed_proposals = - validator_performances.validators[validator_index].failed_proposals; - }; - - let previous_epoch_rewards = client - .get_account_events( - pool_address, - "0x1::stake::StakePool", - "distribute_rewards_events", - Some(0), - Some(10), - ) - .await - .unwrap() - .into_inner() - .into_iter() - .map(|e: VersionedEvent| { - e.data - .get("rewards_amount") - .unwrap() - .as_str() - .unwrap() - .into() - }) - .collect(); - - Ok(StakePoolPerformance { - current_epoch_successful_proposals, - current_epoch_failed_proposals, - previous_epoch_rewards, - epoch_info: get_epoch_info(client).await?, - }) - } -} - -/// Retrieves all stake pools associated with an account -pub async fn get_stake_pools( - client: &Client, - owner_address: AccountAddress, -) -> CliTypedResult> { - let epoch_info = get_epoch_info(client).await?; - let validator_set = &client - .get_account_resource_bcs::(CORE_CODE_ADDRESS, "0x1::stake::ValidatorSet") - .await? - .into_inner(); - let mut stake_pool_results: Vec = vec![]; - // Add direct stake pool if any. - let direct_stake_pool = get_stake_pool_info( - client, - owner_address, - StakePoolType::Direct, - 0, - 0, - epoch_info.clone(), - validator_set, - None, - ) - .await; - if let Ok(direct_stake_pool) = direct_stake_pool { - stake_pool_results.push(direct_stake_pool); - }; - - // Fetch all stake pools managed via staking contracts. - let staking_contract_pools = get_staking_contract_pools( - client, - owner_address, - StakePoolType::StakingContract, - epoch_info.clone(), - validator_set, - None, - ) - .await; - if let Ok(mut staking_contract_pools) = staking_contract_pools { - stake_pool_results.append(&mut staking_contract_pools); - }; - - // Fetch all stake pools managed via employee vesting accounts. - let vesting_admin_store = client - .get_account_resource_bcs::(owner_address, "0x1::vesting::AdminStore") - .await; - if let Ok(vesting_admin_store) = vesting_admin_store { - let vesting_contracts = vesting_admin_store.into_inner().vesting_contracts; - for vesting_contract in vesting_contracts { - let mut staking_contract_pools = get_staking_contract_pools( - client, - vesting_contract, - StakePoolType::Vesting, - epoch_info.clone(), - validator_set, - Some(vesting_contract), - ) - .await - .unwrap(); - stake_pool_results.append(&mut staking_contract_pools); - } - }; - - Ok(stake_pool_results) -} - -/// Retrieve 0x1::staking_contract related pools -pub async fn get_staking_contract_pools( - client: &Client, - staker_address: AccountAddress, - pool_type: StakePoolType, - epoch_info: EpochInfo, - validator_set: &ValidatorSet, - vesting_contract: Option, -) -> CliTypedResult> { - let mut stake_pool_results: Vec = vec![]; - let staking_contract_store = client - .get_account_resource_bcs::( - staker_address, - "0x1::staking_contract::Store", - ) - .await?; - let staking_contracts = staking_contract_store.into_inner().staking_contracts; - for staking_contract in staking_contracts { - let stake_pool_address = get_stake_pool_info( - client, - staking_contract.value.pool_address, - pool_type, - staking_contract.value.principal, - staking_contract.value.commission_percentage, - epoch_info.clone(), - validator_set, - vesting_contract, - ) - .await - .unwrap(); - stake_pool_results.push(stake_pool_address); - } - Ok(stake_pool_results) -} - -pub async fn get_stake_pool_info( - client: &Client, - pool_address: AccountAddress, - pool_type: StakePoolType, - principal: u64, - commission_percentage: u64, - epoch_info: EpochInfo, - validator_set: &ValidatorSet, - vesting_contract: Option, -) -> CliTypedResult { - let stake_pool = client - .get_account_resource_bcs::(pool_address, "0x1::stake::StakePool") - .await? - .into_inner(); - let validator_config = client - .get_account_resource_bcs::(pool_address, "0x1::stake::ValidatorConfig") - .await? - .into_inner(); - let total_stake = stake_pool.get_total_staked_amount(); - let commission_not_yet_unlocked = (total_stake - principal) * commission_percentage / 100; - let state = get_stake_pool_state(validator_set, &pool_address); - - let consensus_public_key = if validator_config.consensus_public_key.is_empty() { - "".into() - } else { - PublicKey::try_from(&validator_config.consensus_public_key[..]) - .unwrap() - .to_encoded_string() - .unwrap() - }; - Ok(StakePoolResult { - state, - pool_address, - operator_address: stake_pool.operator_address, - voter_address: stake_pool.delegated_voter, - pool_type, - total_stake, - commission_percentage, - commission_not_yet_unlocked, - lockup_expiration_utc_time: Time::new_seconds(stake_pool.locked_until_secs).utc_time, - consensus_public_key, - validator_network_addresses: validator_config - .validator_network_addresses() - .unwrap_or_default(), - fullnode_network_addresses: validator_config - .fullnode_network_addresses() - .unwrap_or_default(), - epoch_info, - vesting_contract, - }) -} - -fn get_stake_pool_state( - validator_set: &ValidatorSet, - pool_address: &AccountAddress, -) -> StakePoolState { - if validator_set.active_validators().contains(pool_address) { - StakePoolState::Active - } else if validator_set - .pending_active_validators() - .contains(pool_address) - { - StakePoolState::PendingActive - } else if validator_set - .pending_inactive_validators() - .contains(pool_address) - { - StakePoolState::PendingInactive - } else { - StakePoolState::Inactive - } -} - -/// Register the current account as a validator -/// -/// This will create a new stake pool for the given account. The voter and operator fields will be -/// defaulted to the stake pool account if not provided. -#[derive(Parser)] -pub struct InitializeValidator { - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) operator_config_file_args: OperatorConfigFileArgs, - #[clap(flatten)] - pub(crate) validator_consensus_key_args: ValidatorConsensusKeyArgs, - #[clap(flatten)] - pub(crate) validator_network_addresses_args: ValidatorNetworkAddressesArgs, -} - -#[async_trait] -impl CliCommand for InitializeValidator { - fn command_name(&self) -> &'static str { - "InitializeValidator" - } - - async fn execute(mut self) -> CliTypedResult { - let operator_config = self.operator_config_file_args.load()?; - let consensus_public_key = self - .validator_consensus_key_args - .get_consensus_public_key(&operator_config)?; - let consensus_proof_of_possession = self - .validator_consensus_key_args - .get_consensus_proof_of_possession(&operator_config)?; - let ( - validator_network_public_key, - full_node_network_public_key, - validator_host, - full_node_host, - ) = self - .validator_network_addresses_args - .get_network_configs(&operator_config)?; - let validator_network_addresses = - vec![validator_host.as_network_address(validator_network_public_key)?]; - let full_node_network_addresses = - match (full_node_host.as_ref(), full_node_network_public_key) { - (Some(host), Some(public_key)) => vec![host.as_network_address(public_key)?], - (None, None) => vec![], - _ => { - return Err(CliError::CommandArgumentError( - "If specifying fullnode addresses, both host and public key are required." - .to_string(), - )) - }, - }; - - self.txn_options - .submit_transaction(aptos_stdlib::stake_initialize_validator( - consensus_public_key.to_bytes().to_vec(), - consensus_proof_of_possession.to_bytes().to_vec(), - // BCS encode, so that we can hide the original type - bcs::to_bytes(&validator_network_addresses)?, - bcs::to_bytes(&full_node_network_addresses)?, - )) - .await - .map(|inner| inner.into()) - } -} - -/// Arguments used for operator of the staking pool -#[derive(Parser)] -pub struct OperatorArgs { - #[clap(flatten)] - pub(crate) pool_address_args: OptionalPoolAddressArgs, -} - -impl OperatorArgs { - fn address_fallback_to_profile( - &self, - profile_options: &ProfileOptions, - ) -> CliTypedResult { - if let Some(address) = self.pool_address_args.pool_address { - Ok(address) - } else { - profile_options.account_address() - } - } - - fn address_fallback_to_txn( - &self, - transaction_options: &TransactionOptions, - ) -> CliTypedResult { - if let Some(address) = self.pool_address_args.pool_address { - Ok(address) - } else { - transaction_options.sender_address() - } - } -} - -/// Join the validator set after meeting staking requirements -/// -/// Joining the validator set requires sufficient stake. Once the transaction -/// succeeds, you will join the validator set in the next epoch. -#[derive(Parser)] -pub struct JoinValidatorSet { - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) operator_args: OperatorArgs, -} - -#[async_trait] -impl CliCommand for JoinValidatorSet { - fn command_name(&self) -> &'static str { - "JoinValidatorSet" - } - - async fn execute(mut self) -> CliTypedResult { - let address = self - .operator_args - .address_fallback_to_txn(&self.txn_options)?; - - self.txn_options - .submit_transaction(aptos_stdlib::stake_join_validator_set(address)) - .await - .map(|inner| inner.into()) - } -} - -/// Leave the validator set -/// -/// Leaving the validator set will require you to have unlocked and withdrawn all stake. After this -/// transaction is successful, you will leave the validator set in the next epoch. -#[derive(Parser)] -pub struct LeaveValidatorSet { - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) operator_args: OperatorArgs, -} - -#[async_trait] -impl CliCommand for LeaveValidatorSet { - fn command_name(&self) -> &'static str { - "LeaveValidatorSet" - } - - async fn execute(mut self) -> CliTypedResult { - let address = self - .operator_args - .address_fallback_to_txn(&self.txn_options)?; - - self.txn_options - .submit_transaction(aptos_stdlib::stake_leave_validator_set(address)) - .await - .map(|inner| inner.into()) - } -} - -/// Show validator stake information for a specific validator -/// -/// This will show information about a specific validator, given its -/// `--pool-address`. -#[derive(Parser)] -pub struct ShowValidatorStake { - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) operator_args: OperatorArgs, -} - -#[async_trait] -impl CliCommand for ShowValidatorStake { - fn command_name(&self) -> &'static str { - "ShowValidatorStake" - } - - async fn execute(mut self) -> CliTypedResult { - let client = self.rest_options.client(&self.profile_options)?; - let address = self - .operator_args - .address_fallback_to_profile(&self.profile_options)?; - let response = client - .get_resource(address, "0x1::stake::StakePool") - .await?; - Ok(response.into_inner()) - } -} - -/// Show validator configuration for a specific validator -/// -/// This will show information about a specific validator, given its -/// `--pool-address`. -#[derive(Parser)] -pub struct ShowValidatorConfig { - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) operator_args: OperatorArgs, -} - -#[async_trait] -impl CliCommand for ShowValidatorConfig { - fn command_name(&self) -> &'static str { - "ShowValidatorConfig" - } - - async fn execute(mut self) -> CliTypedResult { - let client = self.rest_options.client(&self.profile_options)?; - let address = self - .operator_args - .address_fallback_to_profile(&self.profile_options)?; - let validator_config: ValidatorConfig = client - .get_account_resource_bcs(address, "0x1::stake::ValidatorConfig") - .await? - .into_inner(); - Ok((&validator_config) - .try_into() - .map_err(|err| CliError::BCS("Validator config", err))?) - } -} - -/// Show validator details of the validator set -/// -/// This will show information about the validators including their voting power, addresses, and -/// public keys. -#[derive(Parser)] -pub struct ShowValidatorSet { - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, -} - -#[async_trait] -impl CliCommand for ShowValidatorSet { - fn command_name(&self) -> &'static str { - "ShowValidatorSet" - } - - async fn execute(mut self) -> CliTypedResult { - let client = self.rest_options.client(&self.profile_options)?; - let validator_set: ValidatorSet = client - .get_account_resource_bcs(CORE_CODE_ADDRESS, "0x1::stake::ValidatorSet") - .await? - .into_inner(); - - ValidatorSetSummary::try_from(&validator_set) - .map_err(|err| CliError::BCS("Validator Set", err)) - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] -pub struct ValidatorSetSummary { - pub scheme: ConsensusScheme, - pub active_validators: Vec, - pub pending_inactive: Vec, - pub pending_active: Vec, - pub total_voting_power: u128, - pub total_joining_power: u128, -} - -impl TryFrom<&ValidatorSet> for ValidatorSetSummary { - type Error = bcs::Error; - - fn try_from(set: &ValidatorSet) -> Result { - Ok(ValidatorSetSummary { - scheme: set.scheme, - active_validators: set - .active_validators - .iter() - .filter_map(|validator| validator.try_into().ok()) - .collect(), - pending_inactive: set - .pending_inactive - .iter() - .filter_map(|validator| validator.try_into().ok()) - .collect(), - pending_active: set - .pending_active - .iter() - .filter_map(|validator| validator.try_into().ok()) - .collect(), - total_voting_power: set.total_voting_power, - total_joining_power: set.total_joining_power, - }) - } -} - -impl From<&ValidatorSetSummary> for ValidatorSet { - fn from(summary: &ValidatorSetSummary) -> Self { - ValidatorSet { - scheme: summary.scheme, - active_validators: summary - .active_validators - .iter() - .map(|validator| validator.into()) - .collect(), - pending_inactive: summary - .pending_inactive - .iter() - .map(|validator| validator.into()) - .collect(), - pending_active: summary - .pending_active - .iter() - .map(|validator| validator.into()) - .collect(), - total_voting_power: summary.total_voting_power, - total_joining_power: summary.total_joining_power, - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] -pub struct ValidatorInfoSummary { - // The validator's account address. AccountAddresses are initially derived from the account - // auth pubkey; however, the auth key can be rotated, so one should not rely on this - // initial property. - pub account_address: AccountAddress, - // Voting power of this validator - consensus_voting_power: u64, - // Validator config - config: ValidatorConfigSummary, -} - -impl TryFrom<&ValidatorInfo> for ValidatorInfoSummary { - type Error = bcs::Error; - - fn try_from(info: &ValidatorInfo) -> Result { - let config = info.config(); - let config = ValidatorConfig { - consensus_public_key: config.consensus_public_key.to_bytes().to_vec(), - validator_network_addresses: config.validator_network_addresses.clone(), - fullnode_network_addresses: config.fullnode_network_addresses.clone(), - validator_index: config.validator_index, - }; - Ok(ValidatorInfoSummary { - account_address: info.account_address, - consensus_voting_power: info.consensus_voting_power(), - config: ValidatorConfigSummary::try_from(&config)?, - }) - } -} - -impl From<&ValidatorInfoSummary> for ValidatorInfo { - fn from(summary: &ValidatorInfoSummary) -> Self { - let config = &summary.config; - ValidatorInfo::new( - summary.account_address, - summary.consensus_voting_power, - aptos_types::validator_config::ValidatorConfig::new( - PublicKey::from_encoded_string(&config.consensus_public_key).unwrap(), - bcs::to_bytes(&config.validator_network_addresses).unwrap(), - bcs::to_bytes(&config.fullnode_network_addresses).unwrap(), - config.validator_index, - ), - ) - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] -pub struct ValidatorConfig { - pub consensus_public_key: Vec, - pub validator_network_addresses: Vec, - pub fullnode_network_addresses: Vec, - pub validator_index: u64, -} - -impl ValidatorConfig { - pub fn new( - consensus_public_key: Vec, - validator_network_addresses: Vec, - fullnode_network_addresses: Vec, - validator_index: u64, - ) -> Self { - ValidatorConfig { - consensus_public_key, - validator_network_addresses, - fullnode_network_addresses, - validator_index, - } - } - - pub fn fullnode_network_addresses(&self) -> Result, bcs::Error> { - bcs::from_bytes(&self.fullnode_network_addresses) - } - - pub fn validator_network_addresses(&self) -> Result, bcs::Error> { - bcs::from_bytes(&self.validator_network_addresses) - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] -pub struct ValidatorConfigSummary { - pub consensus_public_key: String, - /// This is an bcs serialized `Vec` - pub validator_network_addresses: Vec, - /// This is an bcs serialized `Vec` - pub fullnode_network_addresses: Vec, - pub validator_index: u64, -} - -impl TryFrom<&ValidatorConfig> for ValidatorConfigSummary { - type Error = bcs::Error; - - fn try_from(config: &ValidatorConfig) -> Result { - let consensus_public_key = if config.consensus_public_key.is_empty() { - "".into() - } else { - PublicKey::try_from(&config.consensus_public_key[..]) - .unwrap() - .to_encoded_string() - .unwrap() - }; - Ok(ValidatorConfigSummary { - consensus_public_key, - // TODO: We should handle if some of these are not parsable - validator_network_addresses: config.validator_network_addresses()?, - fullnode_network_addresses: config.fullnode_network_addresses()?, - validator_index: config.validator_index, - }) - } -} - -impl From<&ValidatorConfigSummary> for ValidatorConfig { - fn from(summary: &ValidatorConfigSummary) -> Self { - let consensus_public_key = if summary.consensus_public_key.is_empty() { - vec![] - } else { - summary.consensus_public_key.as_bytes().to_vec() - }; - ValidatorConfig { - consensus_public_key, - validator_network_addresses: bcs::to_bytes(&summary.validator_network_addresses) - .unwrap(), - fullnode_network_addresses: bcs::to_bytes(&summary.fullnode_network_addresses).unwrap(), - validator_index: summary.validator_index, - } - } -} - -const MAX_WAIT_S: u64 = 30; -const WAIT_INTERVAL_MS: u64 = 100; -const TESTNET_FOLDER: &str = "testnet"; - -/// Run local testnet -/// -/// This local testnet will run it's own Genesis and run as a single node -/// network locally. Optionally, a faucet can be added for minting APT coins. -#[derive(Parser)] -pub struct RunLocalTestnet { - /// An overridable config template for the test node - /// - /// If provided, the config will be used, and any needed configuration for the local testnet - /// will override the config's values - #[clap(long, parse(from_os_str))] - config_path: Option, - - /// The directory to save all files for the node - /// - /// Defaults to .aptos/testnet - #[clap(long, parse(from_os_str))] - test_dir: Option, - - /// Random seed for key generation in test mode - /// - /// This allows you to have deterministic keys for testing - #[clap(long, parse(try_from_str = FromHex::from_hex))] - seed: Option<[u8; 32]>, - - /// Clean the state and start with a new chain at genesis - /// - /// This will wipe the aptosdb in `test-dir` to remove any incompatible changes, and start - /// the chain fresh. Note, that you will need to publish the module again and distribute funds - /// from the faucet accordingly - #[clap(long)] - force_restart: bool, - - /// Run a faucet alongside the node - /// - /// Allows you to run a faucet alongside the node to create and fund accounts for testing - #[clap(long)] - with_faucet: bool, - - /// Port to run the faucet on - /// - /// When running, you'll be able to use the faucet at `http://localhost:/mint` e.g. - /// `http//localhost:8080/mint` - #[clap(long, default_value = "8081")] - faucet_port: u16, - - /// Disable the delegation of faucet minting to a dedicated account - #[clap(long)] - do_not_delegate: bool, - - #[clap(flatten)] - prompt_options: PromptOptions, -} - -#[async_trait] -impl CliCommand<()> for RunLocalTestnet { - fn command_name(&self) -> &'static str { - "RunLocalTestnet" - } - - async fn execute(mut self) -> CliTypedResult<()> { - let rng = self - .seed - .map(StdRng::from_seed) - .unwrap_or_else(StdRng::from_entropy); - - let global_config = GlobalConfig::load()?; - let test_dir = match self.test_dir { - Some(test_dir) => test_dir, - None => global_config - .get_config_location(ConfigSearchMode::CurrentDirAndParents)? - .join(TESTNET_FOLDER), - }; - - // Remove the current test directory and start with a new node - if self.force_restart && test_dir.exists() { - prompt_yes_with_override( - "Are you sure you want to delete the existing chain?", - self.prompt_options, - )?; - std::fs::remove_dir_all(test_dir.as_path()).map_err(|err| { - CliError::IO(format!("Failed to delete {}", test_dir.display()), err) - })?; - } - - // Spawn the node in a separate thread - let config_path = self.config_path.clone(); - let test_dir_copy = test_dir.clone(); - let node_thread_handle = thread::spawn(move || { - let result = aptos_node::setup_test_environment_and_start_node( - config_path, - Some(test_dir_copy), - false, - false, - aptos_cached_packages::head_release_bundle(), - rng, - ); - eprintln!("Node stopped unexpectedly {:#?}", result); - }); - - // Run faucet if selected - let maybe_faucet_future = if self.with_faucet { - let max_wait = Duration::from_secs(MAX_WAIT_S); - let wait_interval = Duration::from_millis(WAIT_INTERVAL_MS); - - // Load the config to get the rest port - let config_path = test_dir.join("0").join("node.yaml"); - - // We have to wait for the node to be configured above in the other thread - let mut config = None; - let start = Instant::now(); - while start.elapsed() < max_wait { - if let Ok(loaded_config) = NodeConfig::load_from_path(&config_path) { - config = Some(loaded_config); - break; - } - tokio::time::sleep(wait_interval).await; - } - - // Retrieve the port from the local node - let port = if let Some(config) = config { - config.api.address.port() - } else { - return Err(CliError::UnexpectedError( - "Failed to find node configuration to start faucet".to_string(), - )); - }; - - // Check that the REST API is ready - let rest_url = Url::parse(&format!("http://localhost:{}", port)).map_err(|err| { - CliError::UnexpectedError(format!("Failed to parse localhost URL {}", err)) - })?; - let rest_client = aptos_rest_client::Client::new(rest_url.clone()); - let start = Instant::now(); - let mut started_successfully = false; - - while start.elapsed() < max_wait { - if rest_client.get_index().await.is_ok() { - started_successfully = true; - break; - } - tokio::time::sleep(wait_interval).await - } - - if !started_successfully { - return Err(CliError::UnexpectedError(format!( - "Local node at {} did not start up before faucet", - rest_url - ))); - } - - // Build the config for the faucet service. - let faucet_config = RunConfig::build_for_cli( - rest_url, - self.faucet_port, - FunderKeyEnum::KeyFile(test_dir.join("mint.key")), - self.do_not_delegate, - None, - ); - - // Start the faucet - Some(faucet_config.run().map(|result| { - eprintln!("Faucet stopped unexpectedly {:#?}", result); - })) - } else { - None - }; - - // Collect futures that should never end. - let mut futures: Vec + Send>>> = Vec::new(); - - // This future just waits for the node thread. - let node_future = async move { - loop { - if node_thread_handle.is_finished() { - return; - } - tokio::time::sleep(Duration::from_millis(500)).await; - } - }; - - // Wait for all the futures. We should never get past this point unless - // something goes wrong or the user signals for the process to end. - futures.push(Box::pin(node_future)); - if let Some(faucet_future) = maybe_faucet_future { - futures.push(Box::pin(faucet_future)); - } - futures::future::select_all(futures).await; - - Err(CliError::UnexpectedError( - "One of the components stopped unexpectedly".to_string(), - )) - } -} - -/// Update consensus key for the validator node -/// -/// This will take effect in the next epoch -#[derive(Parser)] -pub struct UpdateConsensusKey { - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) operator_args: OperatorArgs, - #[clap(flatten)] - pub(crate) operator_config_file_args: OperatorConfigFileArgs, - #[clap(flatten)] - pub(crate) validator_consensus_key_args: ValidatorConsensusKeyArgs, -} - -#[async_trait] -impl CliCommand for UpdateConsensusKey { - fn command_name(&self) -> &'static str { - "UpdateConsensusKey" - } - - async fn execute(mut self) -> CliTypedResult { - let address = self - .operator_args - .address_fallback_to_txn(&self.txn_options)?; - - let operator_config = self.operator_config_file_args.load()?; - let consensus_public_key = self - .validator_consensus_key_args - .get_consensus_public_key(&operator_config)?; - let consensus_proof_of_possession = self - .validator_consensus_key_args - .get_consensus_proof_of_possession(&operator_config)?; - self.txn_options - .submit_transaction(aptos_stdlib::stake_rotate_consensus_key( - address, - consensus_public_key.to_bytes().to_vec(), - consensus_proof_of_possession.to_bytes().to_vec(), - )) - .await - .map(|inner| inner.into()) - } -} - -/// Update the current validator's network and fullnode network addresses -/// -/// This will take effect in the next epoch -#[derive(Parser)] -pub struct UpdateValidatorNetworkAddresses { - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, - #[clap(flatten)] - pub(crate) operator_args: OperatorArgs, - #[clap(flatten)] - pub(crate) operator_config_file_args: OperatorConfigFileArgs, - #[clap(flatten)] - pub(crate) validator_network_addresses_args: ValidatorNetworkAddressesArgs, -} - -#[async_trait] -impl CliCommand for UpdateValidatorNetworkAddresses { - fn command_name(&self) -> &'static str { - "UpdateValidatorNetworkAddresses" - } - - async fn execute(mut self) -> CliTypedResult { - let address = self - .operator_args - .address_fallback_to_txn(&self.txn_options)?; - - let validator_config = self.operator_config_file_args.load()?; - let ( - validator_network_public_key, - full_node_network_public_key, - validator_host, - full_node_host, - ) = self - .validator_network_addresses_args - .get_network_configs(&validator_config)?; - let validator_network_addresses = - vec![validator_host.as_network_address(validator_network_public_key)?]; - let full_node_network_addresses = - match (full_node_host.as_ref(), full_node_network_public_key) { - (Some(host), Some(public_key)) => vec![host.as_network_address(public_key)?], - (None, None) => vec![], - _ => { - return Err(CliError::CommandArgumentError( - "If specifying fullnode addresses, both host and public key are required." - .to_string(), - )) - }, - }; - - self.txn_options - .submit_transaction(aptos_stdlib::stake_update_network_and_fullnode_addresses( - address, - // BCS encode, so that we can hide the original type - bcs::to_bytes(&validator_network_addresses)?, - bcs::to_bytes(&full_node_network_addresses)?, - )) - .await - .map(|inner| inner.into()) - } -} - -/// Analyze the performance of one or more validators -#[derive(Parser)] -pub struct AnalyzeValidatorPerformance { - /// First epoch to analyze - /// - /// Defaults to the first epoch - #[clap(long, default_value = "-2")] - pub start_epoch: i64, - - /// Last epoch to analyze - /// - /// Defaults to the latest epoch - #[clap(long)] - pub end_epoch: Option, - - /// Analyze mode for the validator: [All, DetailedEpochTable, ValidatorHealthOverTime, NetworkHealthOverTime] - #[clap(arg_enum, long)] - pub(crate) analyze_mode: AnalyzeMode, - - /// Filter of stake pool addresses to analyze - /// - /// Defaults to all stake pool addresses - #[clap(long, multiple_values = true, parse(try_from_str=crate::common::types::load_account_arg))] - pub pool_addresses: Vec, - - #[clap(flatten)] - pub(crate) rest_options: RestOptions, - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, -} - -#[derive(PartialEq, Eq, clap::ArgEnum, Clone)] -pub enum AnalyzeMode { - /// Print all other modes simultaneously - All, - /// For each epoch, print a detailed table containing performance - /// of each of the validators. - DetailedEpochTable, - /// For each validator, summarize it's performance in an epoch into - /// one of the predefined reliability buckets, - /// and prints it's performance across epochs. - ValidatorHealthOverTime, - /// For each epoch summarize how many validators were in - /// each of the reliability buckets. - NetworkHealthOverTime, -} - -#[async_trait] -impl CliCommand<()> for AnalyzeValidatorPerformance { - fn command_name(&self) -> &'static str { - "AnalyzeValidatorPerformance" - } - - async fn execute(mut self) -> CliTypedResult<()> { - let client = self.rest_options.client(&self.profile_options)?; - - let epochs = - FetchMetadata::fetch_new_block_events(&client, Some(self.start_epoch), self.end_epoch) - .await?; - let mut stats = HashMap::new(); - - let print_detailed = self.analyze_mode == AnalyzeMode::DetailedEpochTable - || self.analyze_mode == AnalyzeMode::All; - for epoch_info in epochs { - let mut epoch_stats = - AnalyzeValidators::analyze(&epoch_info.blocks, &epoch_info.validators); - if !self.pool_addresses.is_empty() { - let mut filtered_stats: HashMap = HashMap::new(); - for pool_address in &self.pool_addresses { - filtered_stats.insert( - *pool_address, - *epoch_stats.validator_stats.get(pool_address).unwrap(), - ); - } - epoch_stats.validator_stats = filtered_stats; - } - if print_detailed { - println!( - "Detailed table for {}epoch {}:", - if epoch_info.partial { "partial " } else { "" }, - epoch_info.epoch - ); - AnalyzeValidators::print_detailed_epoch_table( - &epoch_stats, - Some(( - "voting_power", - &epoch_info - .validators - .iter() - .map(|v| (v.address, v.voting_power.to_string())) - .collect::>(), - )), - true, - ); - } - if !epoch_info.partial { - stats.insert(epoch_info.epoch, epoch_stats); - } - } - - if stats.is_empty() { - println!("No data found for given input"); - return Ok(()); - } - let total_stats = stats.values().cloned().reduce(|a, b| a + b).unwrap(); - if print_detailed { - println!( - "Detailed table for all epochs [{}, {}]:", - stats.keys().min().unwrap(), - stats.keys().max().unwrap() - ); - AnalyzeValidators::print_detailed_epoch_table(&total_stats, None, true); - } - let all_validators: Vec<_> = total_stats.validator_stats.keys().cloned().collect(); - if self.analyze_mode == AnalyzeMode::ValidatorHealthOverTime - || self.analyze_mode == AnalyzeMode::All - { - println!( - "Validator health over epochs [{}, {}]:", - stats.keys().min().unwrap(), - stats.keys().max().unwrap() - ); - AnalyzeValidators::print_validator_health_over_time(&stats, &all_validators, None); - } - if self.analyze_mode == AnalyzeMode::NetworkHealthOverTime - || self.analyze_mode == AnalyzeMode::All - { - println!( - "Network health over epochs [{}, {}]:", - stats.keys().min().unwrap(), - stats.keys().max().unwrap() - ); - AnalyzeValidators::print_network_health_over_time(&stats, &all_validators); - } - Ok(()) - } -} - -/// Bootstrap AptosDB from a backup -/// -/// Enables users to load from a backup to catch their node's DB up to a known state. -#[derive(Parser)] -pub struct BootstrapDbFromBackup { - /// Config file for the source backup - /// - /// This file configures if we should use local files or cloud storage, and how to access - /// the backup. - #[clap(long, parse(from_os_str))] - config_path: PathBuf, - - /// Target database directory - /// - /// The directory to create the AptosDB with snapshots and transactions from the backup. - /// The data folder can later be used to start an Aptos node. e.g. /opt/aptos/data/db - #[clap(long = "target-db-dir", parse(from_os_str))] - pub db_dir: PathBuf, - - #[clap(flatten)] - pub metadata_cache_opt: MetadataCacheOpt, - - #[clap(flatten)] - pub concurrent_downloads: ConcurrentDownloadsOpt, - - #[clap(flatten)] - pub replay_concurrency_level: ReplayConcurrencyLevelOpt, -} - -#[async_trait] -impl CliCommand<()> for BootstrapDbFromBackup { - fn command_name(&self) -> &'static str { - "BootstrapDbFromBackup" - } - - async fn execute(self) -> CliTypedResult<()> { - let opt = RestoreCoordinatorOpt { - metadata_cache_opt: self.metadata_cache_opt, - replay_all: false, - ledger_history_start_version: None, - skip_epoch_endings: false, - }; - let global_opt = GlobalRestoreOpt { - dry_run: false, - db_dir: Some(self.db_dir), - target_version: None, - trusted_waypoints: Default::default(), - rocksdb_opt: RocksdbOpt::default(), - concurrent_downloads: self.concurrent_downloads, - replay_concurrency_level: self.replay_concurrency_level, - } - .try_into()?; - let storage = Arc::new(CommandAdapter::new( - CommandAdapterConfig::load_from_file(&self.config_path).await?, - )); - - // hack: get around this error, related to use of `async_trait`: - // error: higher-ranked lifetime error - // ... - // = note: could not prove for<'r, 's> Pin>>>: CoerceUnsized> + std::marker::Send + 's)>>> - tokio::task::spawn_blocking(|| { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(RestoreCoordinator::new(opt, global_opt, storage).run()) - }) - .await - .unwrap()?; - Ok(()) - } -} - -/// Checks the network connectivity of a node -/// -/// Checks network connectivity by dialing the node and attempting -/// to establish a connection with a noise handshake. -#[derive(Parser)] -pub struct CheckNetworkConnectivity { - /// `NetworkAddress` of remote server interface. - /// Examples include: - /// - `/dns/example.com/tcp/6180/noise-ik//handshake/1` - /// - `/ip4//tcp/6182/noise-ik//handshake/0` - #[clap(long, value_parser = validate_address)] - pub address: NetworkAddress, - - /// `ChainId` of remote server. - /// Examples include: - /// - Chain numbers, e.g., `2`, `3` and `25`. - /// - Chain names, e.g., `devnet`, `testnet`, `mainnet` and `testing` (for local test networks). - #[clap(long)] - pub chain_id: ChainId, - - #[clap(flatten)] - pub handshake_args: HandshakeArgs, -} - -#[async_trait] -impl CliCommand for CheckNetworkConnectivity { - fn command_name(&self) -> &'static str { - "CheckNetworkConnectivity" - } - - async fn execute(self) -> CliTypedResult { - // Create the check endpoint args for the checker - let node_address_args = NodeAddressArgs { - address: self.address, - chain_id: self.chain_id, - }; - let check_endpoint_args = CheckEndpointArgs { - node_address_args, - handshake_args: self.handshake_args, - }; - - // Check the endpoint - aptos_network_checker::check_endpoint(&check_endpoint_args, None) - .await - .map_err(|error| CliError::UnexpectedError(error.to_string())) - } -} - -/// Show epoch information -/// -/// Displays the current epoch, the epoch length, and the estimated time of the next epoch -#[derive(Parser)] -pub struct ShowEpochInfo { - #[clap(flatten)] - pub(crate) profile_options: ProfileOptions, - #[clap(flatten)] - pub(crate) rest_options: RestOptions, -} - -#[async_trait] -impl CliCommand for ShowEpochInfo { - fn command_name(&self) -> &'static str { - "ShowEpochInfo" - } - - async fn execute(self) -> CliTypedResult { - let client = &self.rest_options.client(&self.profile_options)?; - get_epoch_info(client).await - } -} - -async fn get_epoch_info(client: &Client) -> CliTypedResult { - let (block_resource, state): (BlockResource, State) = client - .get_account_resource_bcs(CORE_CODE_ADDRESS, "0x1::block::BlockResource") - .await? - .into_parts(); - let reconfig_resource: ConfigurationResource = client - .get_account_resource_at_version_bcs( - CORE_CODE_ADDRESS, - "0x1::reconfiguration::Configuration", - state.version, - ) - .await? - .into_inner(); - - let epoch_interval = block_resource.epoch_interval(); - let epoch_interval_secs = epoch_interval / SECS_TO_MICROSECS; - let last_reconfig = reconfig_resource.last_reconfiguration_time(); - Ok(EpochInfo { - epoch: reconfig_resource.epoch(), - epoch_interval_secs, - current_epoch_start_time: Time::new_micros(last_reconfig), - next_epoch_start_time: Time::new_micros(last_reconfig + epoch_interval), - }) -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EpochInfo { - epoch: u64, - epoch_interval_secs: u64, - current_epoch_start_time: Time, - next_epoch_start_time: Time, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Time { - unix_time: u128, - utc_time: DateTime, -} - -impl Time { - pub fn new(time: Duration) -> Self { - let date_time = - NaiveDateTime::from_timestamp_opt(time.as_secs() as i64, time.subsec_nanos()).unwrap(); - let utc_time = DateTime::from_utc(date_time, Utc); - // TODO: Allow configurable time zone - Self { - unix_time: time.as_micros(), - utc_time, - } - } - - pub fn new_micros(microseconds: u64) -> Self { - Self::new(Duration::from_micros(microseconds)) - } - - pub fn new_seconds(seconds: u64) -> Self { - Self::new(Duration::from_secs(seconds)) - } -} - -#[cfg(test)] -mod tests { - use crate::{CliResult, Tool}; - use clap::Parser; - - // TODO: there have to be cleaner ways to test things. Maybe a CLI test framework? - - #[tokio::test] - // Verifies basic properties about the network connectivity checker - async fn test_check_network_connectivity() { - // Verify the help function works - let args = &["movement", "node", "check-network-connectivity", "--help"]; - let help_message = run_tool_with_args(args).await.unwrap_err(); - assert_contains(help_message, "USAGE:"); // We expect the command to return USAGE info - - // Verify that an invalid address will return an error - let args = &[ - "movement", - "node", - "check-network-connectivity", - "--address", - "invalid-address", - "--chain-id", - "mainnet", - ]; - let error_message = run_tool_with_args(args).await.unwrap_err(); - assert_contains(error_message, "Invalid address"); - - // Verify that an invalid chain-id will return an error - let args = &["movement", "node", "check-network-connectivity", "--address", "/ip4/34.70.116.169/tcp/6182/noise-ik/0x249f3301db104705652e0a0c471b46d13172b2baf14e31f007413f3baee46b0c/handshake/0", "--chain-id", "invalid-chain"]; - let error_message = run_tool_with_args(args).await.unwrap_err(); - assert_contains(error_message, "Invalid value"); - - // Verify that a failure to connect will return a timeout - let args = &["movement", "node", "check-network-connectivity", "--address", "/ip4/31.71.116.169/tcp/0001/noise-ik/0x249f3301db104705652e0a0c471b46d13172b2baf14e31f007413f3baee46b0c/handshake/0", "--chain-id", "testnet"]; - let error_message = run_tool_with_args(args).await.unwrap_err(); - assert_contains(error_message, "Timed out while checking endpoint"); - } - - async fn run_tool_with_args(args: &[&str]) -> CliResult { - let tool: Tool = Tool::try_parse_from(args).map_err(|msg| msg.to_string())?; - tool.execute().await - } - - fn assert_contains(message: String, expected_string: &str) { - if !message.contains(expected_string) { - panic!( - "Expected message to contain {:?}, but it did not! Message: {:?}", - expected_string, message - ); - } - } -} diff --git a/m1/m1-cli/src/op/key.rs b/m1/m1-cli/src/op/key.rs deleted file mode 100644 index aac2d31f..00000000 --- a/m1/m1-cli/src/op/key.rs +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::{ - types::{ - account_address_from_public_key, CliError, CliTypedResult, EncodingOptions, - EncodingType, KeyType, RngArgs, SaveFile, - }, - utils::{ - append_file_extension, check_if_file_exists, generate_vanity_account_ed25519, - write_to_file, - }, - }, - CliCommand, CliResult, -}; -use aptos_config::config::{Peer, PeerRole}; -use aptos_crypto::{bls12381, ed25519, x25519, PrivateKey, ValidCryptoMaterial}; -use aptos_genesis::config::HostAndPort; -use aptos_types::account_address::{ - create_multisig_account_address, from_identity_public_key, AccountAddress, -}; -use async_trait::async_trait; -use clap::{Parser, Subcommand}; -use std::{ - collections::{HashMap, HashSet}, - path::{Path, PathBuf}, -}; - -pub const PUBLIC_KEY_EXTENSION: &str = "pub"; - -/// Tool for generating, inspecting, and interacting with keys -/// -/// This tool allows users to generate and extract related information -/// with all key types used on the Aptos blockchain. -#[derive(Debug, Subcommand)] -pub enum KeyTool { - Generate(GenerateKey), - ExtractPeer(ExtractPeer), -} - -impl KeyTool { - pub async fn execute(self) -> CliResult { - match self { - KeyTool::Generate(tool) => tool.execute_serialized().await, - KeyTool::ExtractPeer(tool) => tool.execute_serialized().await, - } - } -} - -/// Extract full peer information for an upstream peer -/// -/// This command builds a YAML blob that can be copied into a user's network configuration. -/// A host is required to build the network address used for the connection, and the -/// network key is required to identify the peer. -/// -/// A `private-network-key` or `public-network-key` can be given encoded on the command line, or -/// a `private-network-key-file` or a `public-network-key-file` can be given to read from. -/// The `output-file` will be a YAML serialized peer information for use in network config. -#[derive(Debug, Parser)] -pub struct ExtractPeer { - /// Host and port of the full node - /// - /// e.g. 127.0.0.1:6180 or my-awesome-dns.com:6180 - #[clap(long)] - pub(crate) host: HostAndPort, - - #[clap(flatten)] - pub(crate) network_key_input_options: NetworkKeyInputOptions, - #[clap(flatten)] - pub(crate) output_file_options: SaveFile, - #[clap(flatten)] - pub(crate) encoding_options: EncodingOptions, -} - -#[async_trait] -impl CliCommand> for ExtractPeer { - fn command_name(&self) -> &'static str { - "ExtractPeer" - } - - async fn execute(self) -> CliTypedResult> { - // Load key based on public or private - let public_key = self - .network_key_input_options - .extract_public_network_key(self.encoding_options.encoding)?; - - // Check output file exists - self.output_file_options.check_file()?; - - // Build peer info - let peer_id = from_identity_public_key(public_key); - let mut public_keys = HashSet::new(); - public_keys.insert(public_key); - - let address = self.host.as_network_address(public_key).map_err(|err| { - CliError::UnexpectedError(format!("Failed to build network address: {}", err)) - })?; - - let peer = Peer::new(vec![address], public_keys, PeerRole::Upstream); - - let mut map = HashMap::new(); - map.insert(peer_id, peer); - - // Save to file - let yaml = serde_yaml::to_string(&map) - .map_err(|err| CliError::UnexpectedError(err.to_string()))?; - self.output_file_options - .save_to_file("Extracted peer", yaml.as_bytes())?; - Ok(map) - } -} - -#[derive(Debug, Default, Parser)] -pub struct NetworkKeyInputOptions { - /// x25519 Private key input file name - #[clap(long, group = "network_key_input", parse(from_os_str))] - private_network_key_file: Option, - - /// x25519 Private key encoded in a type as shown in `encoding` - #[clap(long, group = "network_key_input")] - private_network_key: Option, - - /// x25519 Public key input file name - #[clap(long, group = "network_key_input", parse(from_os_str))] - public_network_key_file: Option, - - /// x25519 Public key encoded in a type as shown in `encoding` - #[clap(long, group = "network_key_input")] - public_network_key: Option, -} - -impl NetworkKeyInputOptions { - pub fn from_private_key_file(file: PathBuf) -> Self { - Self { - private_network_key_file: Some(file), - private_network_key: None, - public_network_key_file: None, - public_network_key: None, - } - } - - pub fn extract_public_network_key( - self, - encoding: EncodingType, - ) -> CliTypedResult { - // The grouping above prevents there from being more than one, but just in case - match (self.public_network_key, self.public_network_key_file, self.private_network_key, self.private_network_key_file){ - (Some(public_network_key), None, None, None) => encoding.decode_key("--public-network-key", public_network_key.as_bytes().to_vec()), - (None, Some(public_network_key_file),None, None) => encoding.load_key("--public-network-key-file", public_network_key_file.as_path()), - (None, None, Some(private_network_key), None) => { - let private_network_key: x25519::PrivateKey = encoding.decode_key("--private-network-key", private_network_key.as_bytes().to_vec())?; - Ok(private_network_key.public_key()) - }, - (None, None, None, Some(private_network_key_file)) => { - let private_network_key: x25519::PrivateKey = encoding.load_key("--private-network-key-file", private_network_key_file.as_path())?; - Ok(private_network_key.public_key()) - }, - _ => Err(CliError::CommandArgumentError("Must provide exactly one of [--public-network-key, --public-network-key-file, --private-network-key, --private-network-key-file]".to_string())) - } - } -} - -/// Generates a `x25519` or `ed25519` key. -/// -/// This can be used for generating an identity. Two files will be created -/// `output_file` and `output_file.pub`. `output_file` will contain the private -/// key encoded with the `encoding` and `output_file.pub` will contain the public -/// key encoded with the `encoding`. -#[derive(Debug, Parser)] -pub struct GenerateKey { - /// Key type to generate. Must be one of [x25519, ed25519, bls12381] - #[clap(long, default_value_t = KeyType::Ed25519)] - pub(crate) key_type: KeyType, - /// Vanity prefix that resultant account address should start with, e.g. 0xaceface or d00d. Each - /// additional character multiplies by a factor of 16 the computational difficulty associated - /// with generating an address, so try out shorter prefixes first and be prepared to wait for - /// longer ones - #[clap(long)] - pub vanity_prefix: Option, - /// Use this flag when vanity prefix is for a multisig account. This mines a private key for - /// a single signer account that can, as its first transaction, create a multisig account with - /// the given vanity prefix - #[clap(long)] - pub vanity_multisig: bool, - #[clap(flatten)] - pub rng_args: RngArgs, - #[clap(flatten)] - pub(crate) save_params: SaveKey, -} - -#[async_trait] -impl CliCommand> for GenerateKey { - fn command_name(&self) -> &'static str { - "GenerateKey" - } - - async fn execute(self) -> CliTypedResult> { - if self.vanity_prefix.is_some() && !matches!(self.key_type, KeyType::Ed25519) { - return Err(CliError::CommandArgumentError(format!( - "Vanity prefixes are only accepted for {} keys", - KeyType::Ed25519 - ))); - } - if self.vanity_multisig && self.vanity_prefix.is_none() { - return Err(CliError::CommandArgumentError( - "No vanity prefix provided".to_string(), - )); - } - self.save_params.check_key_file()?; - let mut keygen = self.rng_args.key_generator()?; - match self.key_type { - KeyType::X25519 => { - let private_key = keygen.generate_x25519_private_key().map_err(|err| { - CliError::UnexpectedError(format!( - "Failed to convert ed25519 to x25519 {:?}", - err - )) - })?; - self.save_params.save_key(&private_key, "x25519") - }, - KeyType::Ed25519 => { - // If no vanity prefix specified, generate a standard Ed25519 private key. - let private_key = if self.vanity_prefix.is_none() { - keygen.generate_ed25519_private_key() - } else { - // If a vanity prefix is specified, generate vanity Ed25519 account from it. - generate_vanity_account_ed25519( - self.vanity_prefix.clone().unwrap().as_str(), - self.vanity_multisig, - )? - }; - // Store CLI result from key save operation, to append vanity address(es) if needed. - let mut result_map = self.save_params.save_key(&private_key, "ed25519").unwrap(); - if self.vanity_prefix.is_some() { - let account_address = account_address_from_public_key( - &ed25519::Ed25519PublicKey::from(&private_key), - ); - // Store account address in a PathBuf so it can be displayed in CLI result. - result_map.insert( - "Account Address:", - PathBuf::from(account_address.to_hex_literal()), - ); - if self.vanity_multisig { - let multisig_account_address = - create_multisig_account_address(account_address, 0); - result_map.insert( - "Multisig Account Address:", - PathBuf::from(multisig_account_address.to_hex_literal()), - ); - } - } - return Ok(result_map); - }, - KeyType::Bls12381 => { - let private_key = keygen.generate_bls12381_private_key(); - self.save_params.save_bls_key(&private_key, "bls12381") - }, - } - } -} - -impl GenerateKey { - /// A test friendly typed key generation for x25519 keys. - pub async fn generate_x25519( - encoding: EncodingType, - key_file: &Path, - ) -> CliTypedResult<(x25519::PrivateKey, x25519::PublicKey)> { - let args = format!( - "generate --key-type {key_type:?} --output-file {key_file} --encoding {encoding:?} --assume-yes", - key_type = KeyType::X25519, - key_file = key_file.display(), - encoding = encoding, - ); - let command = GenerateKey::parse_from(args.split_whitespace()); - command.execute().await?; - Ok(( - encoding.load_key("private_key", key_file)?, - encoding.load_key( - "public_key", - &append_file_extension(key_file, PUBLIC_KEY_EXTENSION)?, - )?, - )) - } - - /// A test friendly typed key generation for e25519 keys. - pub async fn generate_ed25519( - encoding: EncodingType, - key_file: &Path, - ) -> CliTypedResult<(ed25519::Ed25519PrivateKey, ed25519::Ed25519PublicKey)> { - let args = format!( - "generate --key-type {key_type:?} --output-file {key_file} --encoding {encoding:?} --assume-yes", - key_type = KeyType::Ed25519, - key_file = key_file.display(), - encoding = encoding, - ); - let command = GenerateKey::parse_from(args.split_whitespace()); - command.execute().await?; - Ok(( - encoding.load_key("private_key", key_file)?, - encoding.load_key( - "public_key", - &append_file_extension(key_file, PUBLIC_KEY_EXTENSION)?, - )?, - )) - } -} - -#[derive(Debug, Parser)] -pub struct SaveKey { - #[clap(flatten)] - pub(crate) file_options: SaveFile, - #[clap(flatten)] - pub(crate) encoding_options: EncodingOptions, -} - -impl SaveKey { - /// Public key file name - fn public_key_file(&self) -> CliTypedResult { - append_file_extension( - self.file_options.output_file.as_path(), - PUBLIC_KEY_EXTENSION, - ) - } - - /// Public key file name - fn proof_of_possession_file(&self) -> CliTypedResult { - append_file_extension(self.file_options.output_file.as_path(), "pop") - } - - /// Check if the key file exists already - pub fn check_key_file(&self) -> CliTypedResult<()> { - // Check if file already exists - self.file_options.check_file()?; - check_if_file_exists(&self.public_key_file()?, self.file_options.prompt_options) - } - - /// Saves a key to a file encoded in a string - pub fn save_key( - self, - key: &Key, - key_name: &'static str, - ) -> CliTypedResult> { - let encoded_private_key = self.encoding_options.encoding.encode_key(key_name, key)?; - let encoded_public_key = self - .encoding_options - .encoding - .encode_key(key_name, &key.public_key())?; - - // Write private and public keys to files - let public_key_file = self.public_key_file()?; - self.file_options - .save_to_file_confidential(key_name, &encoded_private_key)?; - write_to_file(&public_key_file, key_name, &encoded_public_key)?; - - let mut map = HashMap::new(); - map.insert("PrivateKey Path", self.file_options.output_file); - map.insert("PublicKey Path", public_key_file); - Ok(map) - } - - /// Saves a key to a file encoded in a string - pub fn save_bls_key( - self, - key: &bls12381::PrivateKey, - key_name: &'static str, - ) -> CliTypedResult> { - let encoded_private_key = self.encoding_options.encoding.encode_key(key_name, key)?; - let encoded_public_key = self - .encoding_options - .encoding - .encode_key(key_name, &key.public_key())?; - let encoded_proof_of_posession = self - .encoding_options - .encoding - .encode_key(key_name, &bls12381::ProofOfPossession::create(key))?; - - // Write private and public keys to files - let public_key_file = self.public_key_file()?; - let proof_of_possession_file = self.proof_of_possession_file()?; - self.file_options - .save_to_file_confidential(key_name, &encoded_private_key)?; - write_to_file(&public_key_file, key_name, &encoded_public_key)?; - write_to_file( - &proof_of_possession_file, - key_name, - &encoded_proof_of_posession, - )?; - - let mut map = HashMap::new(); - map.insert("PrivateKey Path", self.file_options.output_file); - map.insert("PublicKey Path", public_key_file); - map.insert("Proof of possession Path", proof_of_possession_file); - Ok(map) - } -} diff --git a/m1/m1-cli/src/op/mod.rs b/m1/m1-cli/src/op/mod.rs deleted file mode 100644 index 989a2bb8..00000000 --- a/m1/m1-cli/src/op/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -pub mod key; diff --git a/m1/m1-cli/src/stake/mod.rs b/m1/m1-cli/src/stake/mod.rs deleted file mode 100644 index 45168a70..00000000 --- a/m1/m1-cli/src/stake/mod.rs +++ /dev/null @@ -1,668 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - common::{ - types::{ - CliCommand, CliError, CliResult, CliTypedResult, TransactionOptions, TransactionSummary, - }, - utils::prompt_yes_with_override, - }, - node::{get_stake_pools, StakePoolType}, -}; -use aptos_cached_packages::aptos_stdlib; -use aptos_types::{ - account_address::{ - create_vesting_contract_address, default_stake_pool_address, AccountAddress, - }, - vesting::VestingAdminStore, -}; -use async_trait::async_trait; -use clap::Parser; - -/// Tool for manipulating stake and stake pools -/// -#[derive(Parser)] -pub enum StakeTool { - AddStake(AddStake), - CreateStakingContract(CreateStakingContract), - DistributeVestedCoins(DistributeVestedCoins), - IncreaseLockup(IncreaseLockup), - InitializeStakeOwner(InitializeStakeOwner), - RequestCommission(RequestCommission), - SetDelegatedVoter(SetDelegatedVoter), - SetOperator(SetOperator), - UnlockStake(UnlockStake), - UnlockVestedCoins(UnlockVestedCoins), - WithdrawStake(WithdrawStake), -} - -impl StakeTool { - pub async fn execute(self) -> CliResult { - use StakeTool::*; - match self { - AddStake(tool) => tool.execute_serialized().await, - CreateStakingContract(tool) => tool.execute_serialized().await, - DistributeVestedCoins(tool) => tool.execute_serialized().await, - IncreaseLockup(tool) => tool.execute_serialized().await, - InitializeStakeOwner(tool) => tool.execute_serialized().await, - RequestCommission(tool) => tool.execute_serialized().await, - SetDelegatedVoter(tool) => tool.execute_serialized().await, - SetOperator(tool) => tool.execute_serialized().await, - UnlockStake(tool) => tool.execute_serialized().await, - UnlockVestedCoins(tool) => tool.execute_serialized().await, - WithdrawStake(tool) => tool.execute_serialized().await, - } - } -} - -/// Add APT to a stake pool -/// -/// This command allows stake pool owners to add APT to their stake. -#[derive(Parser)] -pub struct AddStake { - /// Amount of Octas (10^-8 APT) to add to stake - #[clap(long)] - pub amount: u64, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand> for AddStake { - fn command_name(&self) -> &'static str { - "AddStake" - } - - async fn execute(mut self) -> CliTypedResult> { - let client = self - .txn_options - .rest_options - .client(&self.txn_options.profile_options)?; - let amount = self.amount; - let owner_address = self.txn_options.sender_address()?; - let mut transaction_summaries: Vec = vec![]; - - let stake_pool_results = get_stake_pools(&client, owner_address).await?; - for stake_pool in stake_pool_results { - match stake_pool.pool_type { - StakePoolType::Direct => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::stake_add_stake(amount)) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::StakingContract => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::staking_contract_add_stake( - stake_pool.operator_address, - amount, - )) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::Vesting => { - return Err(CliError::UnexpectedError( - "Adding stake is not supported for vesting contracts".into(), - )) - }, - } - } - Ok(transaction_summaries) - } -} - -/// Unlock staked APT in a stake pool -/// -/// APT coins can only be unlocked if they no longer have an applied lockup period -#[derive(Parser)] -pub struct UnlockStake { - /// Amount of Octas (10^-8 APT) to unlock - #[clap(long)] - pub amount: u64, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand> for UnlockStake { - fn command_name(&self) -> &'static str { - "UnlockStake" - } - - async fn execute(mut self) -> CliTypedResult> { - let client = self - .txn_options - .rest_options - .client(&self.txn_options.profile_options)?; - let amount = self.amount; - let owner_address = self.txn_options.sender_address()?; - let mut transaction_summaries: Vec = vec![]; - - let stake_pool_results = get_stake_pools(&client, owner_address).await?; - for stake_pool in stake_pool_results { - match stake_pool.pool_type { - StakePoolType::Direct => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::stake_unlock(amount)) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::StakingContract => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::staking_contract_unlock_stake( - stake_pool.operator_address, - amount, - )) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::Vesting => { - return Err(CliError::UnexpectedError( - "Unlocking stake is not supported for vesting contracts".into(), - )) - }, - } - } - Ok(transaction_summaries) - } -} - -/// Withdraw unlocked staked APT from a stake pool -/// -/// This allows users to withdraw stake back into their CoinStore. -/// Before calling `WithdrawStake`, `UnlockStake` must be called first. -#[derive(Parser)] -pub struct WithdrawStake { - /// Amount of Octas (10^-8 APT) to withdraw. - /// This only applies to stake pools owned directly by the owner account, instead of via - /// a staking contract. In the latter case, when withdrawal is issued, all coins are distributed - #[clap(long)] - pub amount: u64, - - #[clap(flatten)] - pub(crate) node_op_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand> for WithdrawStake { - fn command_name(&self) -> &'static str { - "WithdrawStake" - } - - async fn execute(mut self) -> CliTypedResult> { - let client = self - .node_op_options - .rest_options - .client(&self.node_op_options.profile_options)?; - let amount = self.amount; - let owner_address = self.node_op_options.sender_address()?; - let mut transaction_summaries: Vec = vec![]; - - let stake_pool_results = get_stake_pools(&client, owner_address).await?; - for stake_pool in stake_pool_results { - match stake_pool.pool_type { - StakePoolType::Direct => { - transaction_summaries.push( - self.node_op_options - .submit_transaction(aptos_stdlib::stake_withdraw(amount)) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::StakingContract => { - transaction_summaries.push( - self.node_op_options - .submit_transaction(aptos_stdlib::staking_contract_distribute( - owner_address, - stake_pool.operator_address, - )) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::Vesting => { - return Err(CliError::UnexpectedError( - "Stake withdrawal from vesting contract should use distribute-vested-coins" - .into(), - )) - }, - } - } - Ok(transaction_summaries) - } -} - -/// Increase lockup of all staked APT in a stake pool -/// -/// Lockup may need to be increased in order to vote on a proposal. -#[derive(Parser)] -pub struct IncreaseLockup { - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand> for IncreaseLockup { - fn command_name(&self) -> &'static str { - "IncreaseLockup" - } - - async fn execute(mut self) -> CliTypedResult> { - let client = self - .txn_options - .rest_options - .client(&self.txn_options.profile_options)?; - let owner_address = self.txn_options.sender_address()?; - let mut transaction_summaries: Vec = vec![]; - - let stake_pool_results = get_stake_pools(&client, owner_address).await?; - for stake_pool in stake_pool_results { - match stake_pool.pool_type { - StakePoolType::Direct => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::stake_increase_lockup()) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::StakingContract => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::staking_contract_reset_lockup( - stake_pool.operator_address, - )) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::Vesting => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::vesting_reset_lockup( - stake_pool.vesting_contract.unwrap(), - )) - .await - .map(|inner| inner.into())?, - ); - }, - } - } - Ok(transaction_summaries) - } -} - -/// Initialize a stake pool owner -/// -/// Initializing stake owner adds the capability to delegate the -/// stake pool to an operator, or delegate voting to a different account. -#[derive(Parser)] -pub struct InitializeStakeOwner { - /// Initial amount of Octas (10^-8 APT) to be staked - #[clap(long)] - pub initial_stake_amount: u64, - - /// Account Address of delegated operator - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub operator_address: Option, - - /// Account address of delegated voter - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub voter_address: Option, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for InitializeStakeOwner { - fn command_name(&self) -> &'static str { - "InitializeStakeOwner" - } - - async fn execute(mut self) -> CliTypedResult { - let owner_address = self.txn_options.sender_address()?; - self.txn_options - .submit_transaction(aptos_stdlib::stake_initialize_stake_owner( - self.initial_stake_amount, - self.operator_address.unwrap_or(owner_address), - self.voter_address.unwrap_or(owner_address), - )) - .await - .map(|inner| inner.into()) - } -} - -/// Delegate operator capability to another account -/// -/// This changes teh operator capability from its current operator to a different operator. -/// By default, the operator of a stake pool is the owner of the stake pool -#[derive(Parser)] -pub struct SetOperator { - /// Account Address of delegated operator - /// - /// If not specified, it will be the same as the owner - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub operator_address: AccountAddress, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand> for SetOperator { - fn command_name(&self) -> &'static str { - "SetOperator" - } - - async fn execute(mut self) -> CliTypedResult> { - let client = self - .txn_options - .rest_options - .client(&self.txn_options.profile_options)?; - let owner_address = self.txn_options.sender_address()?; - let new_operator_address = self.operator_address; - let mut transaction_summaries: Vec = vec![]; - - let stake_pool_results = get_stake_pools(&client, owner_address).await?; - for stake_pool in stake_pool_results { - match stake_pool.pool_type { - StakePoolType::Direct => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::stake_set_operator( - new_operator_address, - )) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::StakingContract => { - transaction_summaries.push( - self.txn_options - .submit_transaction( - aptos_stdlib::staking_contract_switch_operator_with_same_commission( - stake_pool.operator_address, - new_operator_address, - ), - ) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::Vesting => { - transaction_summaries.push( - self.txn_options - .submit_transaction( - aptos_stdlib::vesting_update_operator_with_same_commission( - stake_pool.vesting_contract.unwrap(), - new_operator_address, - ), - ) - .await - .map(|inner| inner.into())?, - ); - }, - } - } - Ok(transaction_summaries) - } -} - -/// Delegate voting capability to another account -/// -/// Delegates voting capability from its current voter to a different voter. -/// By default, the voter of a stake pool is the owner of the stake pool -#[derive(Parser)] -pub struct SetDelegatedVoter { - /// Account Address of delegated voter - /// - /// If not specified, it will be the same as the owner - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub voter_address: AccountAddress, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand> for SetDelegatedVoter { - fn command_name(&self) -> &'static str { - "SetDelegatedVoter" - } - - async fn execute(mut self) -> CliTypedResult> { - let client = self - .txn_options - .rest_options - .client(&self.txn_options.profile_options)?; - let owner_address = self.txn_options.sender_address()?; - let new_voter_address = self.voter_address; - let mut transaction_summaries: Vec = vec![]; - - let stake_pool_results = get_stake_pools(&client, owner_address).await?; - for stake_pool in stake_pool_results { - match stake_pool.pool_type { - StakePoolType::Direct => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::stake_set_delegated_voter( - new_voter_address, - )) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::StakingContract => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::staking_contract_update_voter( - stake_pool.operator_address, - new_voter_address, - )) - .await - .map(|inner| inner.into())?, - ); - }, - StakePoolType::Vesting => { - transaction_summaries.push( - self.txn_options - .submit_transaction(aptos_stdlib::vesting_update_voter( - stake_pool.vesting_contract.unwrap(), - new_voter_address, - )) - .await - .map(|inner| inner.into())?, - ); - }, - } - } - Ok(transaction_summaries) - } -} - -/// Create a staking contract stake pool -/// -/// -#[derive(Parser)] -pub struct CreateStakingContract { - /// Account Address of operator - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub operator: AccountAddress, - - /// Account Address of delegated voter - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub voter: AccountAddress, - - /// Amount to create the staking contract with - #[clap(long)] - pub amount: u64, - - /// Percentage of accumulated rewards to pay the operator as commission - #[clap(long)] - pub commission_percentage: u64, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for CreateStakingContract { - fn command_name(&self) -> &'static str { - "CreateStakingContract" - } - - async fn execute(mut self) -> CliTypedResult { - let pool_address = default_stake_pool_address( - self.txn_options.profile_options.account_address()?, - self.operator, - ); - prompt_yes_with_override( - &format!( - "Creating a new staking contract with pool address 0x{}. Confirm?", - pool_address - ), - self.txn_options.prompt_options, - )?; - - self.txn_options - .submit_transaction(aptos_stdlib::staking_contract_create_staking_contract( - self.operator, - self.voter, - self.amount, - self.commission_percentage, - vec![], - )) - .await - .map(|inner| inner.into()) - } -} - -/// Distribute fully unlocked coins from vesting -/// -/// Distribute fully unlocked coins (rewards and/or vested coins) from the vesting contract -/// to shareholders. -#[derive(Parser)] -pub struct DistributeVestedCoins { - /// Address of the vesting contract's admin. - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub admin_address: AccountAddress, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for DistributeVestedCoins { - fn command_name(&self) -> &'static str { - "DistributeVestedCoins" - } - - async fn execute(mut self) -> CliTypedResult { - let vesting_contract_address = create_vesting_contract_address(self.admin_address, 0, &[]); - self.txn_options - .submit_transaction(aptos_stdlib::vesting_distribute(vesting_contract_address)) - .await - .map(|inner| inner.into()) - } -} - -/// Unlock vested coins -/// -/// Unlock vested coins according to the vesting contract's schedule. -/// This also unlocks any accumulated staking rewards and pays commission to the operator of the -/// vesting contract's stake pool first. -/// -/// The unlocked vested tokens and staking rewards are still subject to the staking lockup and -/// cannot be withdrawn until after the lockup expires. -#[derive(Parser)] -pub struct UnlockVestedCoins { - /// Address of the vesting contract's admin. - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub admin_address: AccountAddress, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for UnlockVestedCoins { - fn command_name(&self) -> &'static str { - "UnlockVestedCoins" - } - - async fn execute(mut self) -> CliTypedResult { - let vesting_contract_address = create_vesting_contract_address(self.admin_address, 0, &[]); - self.txn_options - .submit_transaction(aptos_stdlib::vesting_vest(vesting_contract_address)) - .await - .map(|inner| inner.into()) - } -} - -/// Request commission from running a stake pool -/// -/// Allows operators or owners to request commission from running a stake pool (only if there's a -/// staking contract set up with the staker). The commission will be withdrawable at the end of the -/// stake pool's current lockup period. -#[derive(Parser)] -pub struct RequestCommission { - /// Address of the owner of the stake pool - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub owner_address: AccountAddress, - - /// Address of the operator of the stake pool - #[clap(long, parse(try_from_str=crate::common::types::load_account_arg))] - pub operator_address: AccountAddress, - - #[clap(flatten)] - pub(crate) txn_options: TransactionOptions, -} - -#[async_trait] -impl CliCommand for RequestCommission { - fn command_name(&self) -> &'static str { - "RequestCommission" - } - - async fn execute(mut self) -> CliTypedResult { - let client = self - .txn_options - .rest_options - .client(&self.txn_options.profile_options)?; - - // If this is a vesting stake pool, retrieve the associated vesting contract - let vesting_admin_store = client - .get_account_resource_bcs::( - self.owner_address, - "0x1::vesting::AdminStore", - ) - .await; - - // Note: this only works if the vesting contract has exactly one staking contract - // associated - let staker_address = if let Ok(vesting_admin_store) = vesting_admin_store { - vesting_admin_store.into_inner().vesting_contracts[0] - } else { - self.owner_address - }; - self.txn_options - .submit_transaction(aptos_stdlib::staking_contract_request_commission( - staker_address, - self.operator_address, - )) - .await - .map(|inner| inner.into()) - } -} diff --git a/m1/m1-cli/src/test/mod.rs b/m1/m1-cli/src/test/mod.rs deleted file mode 100644 index aec14d4a..00000000 --- a/m1/m1-cli/src/test/mod.rs +++ /dev/null @@ -1,1221 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - account::{ - create::{CreateAccount, DEFAULT_FUNDED_COINS}, - fund::FundWithFaucet, - key_rotation::{LookupAddress, RotateKey, RotateSummary}, - list::{ListAccount, ListQuery}, - transfer::{TransferCoins, TransferSummary}, - }, - common::{ - init::{InitTool, Network}, - types::{ - account_address_from_public_key, AccountAddressWrapper, ArgWithTypeVec, CliError, - CliTypedResult, EncodingOptions, EntryFunctionArguments, FaucetOptions, GasOptions, - KeyType, MoveManifestAccountWrapper, MovePackageDir, OptionalPoolAddressArgs, - PoolAddressArgs, PrivateKeyInputOptions, PromptOptions, PublicKeyInputOptions, - RestOptions, RngArgs, SaveFile, TransactionOptions, TransactionSummary, - }, - utils::write_to_file, - }, - governance::{ - CompileScriptFunction, ProposalSubmissionSummary, SubmitProposal, SubmitVote, - VerifyProposal, VerifyProposalResponse, - }, - move_tool::{ - ArgWithType, CompilePackage, DownloadPackage, FrameworkPackageArgs, IncludedArtifacts, - IncludedArtifactsArgs, InitPackage, MemberId, PublishPackage, RunFunction, RunScript, - TestPackage, - }, - node::{ - AnalyzeMode, AnalyzeValidatorPerformance, GetStakePool, InitializeValidator, - JoinValidatorSet, LeaveValidatorSet, OperatorArgs, OperatorConfigFileArgs, - ShowValidatorConfig, ShowValidatorSet, ShowValidatorStake, StakePoolResult, - UpdateConsensusKey, UpdateValidatorNetworkAddresses, ValidatorConfig, - ValidatorConsensusKeyArgs, ValidatorNetworkAddressesArgs, - }, - op::key::{ExtractPeer, GenerateKey, NetworkKeyInputOptions, SaveKey}, - stake::{ - AddStake, IncreaseLockup, InitializeStakeOwner, SetDelegatedVoter, SetOperator, - UnlockStake, WithdrawStake, - }, - CliCommand, -}; -use aptos_config::config::Peer; -use aptos_crypto::{ - bls12381, - ed25519::{Ed25519PrivateKey, Ed25519PublicKey}, - x25519, PrivateKey, -}; -use aptos_genesis::config::HostAndPort; -use aptos_keygen::KeyGen; -use aptos_logger::warn; -use aptos_rest_client::{ - aptos_api_types::{MoveStructTag, MoveType}, - Transaction, -}; -use aptos_sdk::move_types::{account_address::AccountAddress, language_storage::ModuleId}; -use aptos_temppath::TempPath; -use aptos_types::on_chain_config::ValidatorSet; -use move_core_types::ident_str; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::{ - collections::{BTreeMap, HashMap}, - mem, - path::PathBuf, - str::FromStr, - time::Duration, -}; -use tempfile::TempDir; -use thiserror::__private::AsDisplay; -#[cfg(feature = "cli-framework-test-move")] -use thiserror::__private::AsDisplay; -use tokio::time::{sleep, Instant}; - -#[cfg(test)] -mod tests; - -pub const INVALID_ACCOUNT: &str = "0xDEADBEEFCAFEBABE"; - -pub const FIRST_MOVE_FILE: &str = " -module NamedAddress0::store { - use std::string; - use aptos_framework::coin::{Self}; - - struct CoolCoin has key {} - - public entry fun init( - account: &signer, - decimals: u64, - monitor_supply: bool - ) { - let (_, _) = coin::initialize(account, string::utf8(b\"CoolCoin\"), string::utf8(b\"COOL\"), decimals, monitor_supply); - coin::register(account); - } -}"; - -/// A framework for testing the CLI -pub struct CliTestFramework { - account_addresses: Vec, - account_keys: Vec, - endpoint: Url, - faucet_endpoint: Url, - move_dir: Option, -} - -impl CliTestFramework { - pub fn local_new(num_accounts: usize) -> CliTestFramework { - let dummy_url = Url::parse("http://localhost").unwrap(); - let mut framework = CliTestFramework { - account_addresses: Vec::new(), - account_keys: Vec::new(), - endpoint: dummy_url.clone(), - faucet_endpoint: dummy_url, - move_dir: None, - }; - let mut keygen = KeyGen::from_seed([0; 32]); - for _ in 0..num_accounts { - let key = keygen.generate_ed25519_private_key(); - framework.add_account_to_cli(key); - } - framework - } - - pub async fn new(endpoint: Url, faucet_endpoint: Url, num_accounts: usize) -> CliTestFramework { - let mut framework = CliTestFramework { - account_addresses: Vec::new(), - account_keys: Vec::new(), - endpoint, - faucet_endpoint, - move_dir: None, - }; - let mut keygen = KeyGen::from_seed([0; 32]); - - for _ in 0..num_accounts { - framework - .create_cli_account_from_faucet(keygen.generate_ed25519_private_key(), None) - .await - .unwrap(); - } - - framework - } - - pub fn addresses(&self) -> Vec { - self.account_addresses.clone() - } - - async fn check_account_exists(&self, index: usize) -> bool { - // Create account if it doesn't exist (and there's a faucet) - let client = aptos_rest_client::Client::new(self.endpoint.clone()); - let address = self.account_id(index); - client.get_account(address).await.is_ok() - } - - pub fn add_account_to_cli(&mut self, private_key: Ed25519PrivateKey) -> usize { - let address = account_address_from_public_key(&private_key.public_key()); - self.account_addresses.push(address); - self.account_keys.push(private_key); - println!( - "Account: {} (index: {})", - address.to_hex_literal(), - self.account_keys.len() - 1 - ); - self.account_keys.len() - 1 - } - - pub fn add_account_with_address_to_cli( - &mut self, - private_key: Ed25519PrivateKey, - address: AccountAddress, - ) -> usize { - self.account_addresses.push(address); - self.account_keys.push(private_key); - self.account_keys.len() - 1 - } - - pub async fn create_cli_account( - &mut self, - private_key: Ed25519PrivateKey, - sender_index: usize, - ) -> CliTypedResult { - let index = self.add_account_to_cli(private_key); - if self.check_account_exists(index).await { - return Err(CliError::UnexpectedError( - "Account already exists".to_string(), - )); - } - CreateAccount { - txn_options: self.transaction_options(sender_index, None), - account: self.account_id(index), - } - .execute() - .await?; - - Ok(index) - } - - pub async fn create_cli_account_from_faucet( - &mut self, - private_key: Ed25519PrivateKey, - amount: Option, - ) -> CliTypedResult { - let index = self.add_account_to_cli(private_key); - if self.check_account_exists(index).await { - return Err(CliError::UnexpectedError( - "Account already exists".to_string(), - )); - } - - self.fund_account(index, amount).await?; - warn!( - "Funded account {:?} with {:?} OCTA", - self.account_id(index), - amount.unwrap_or(DEFAULT_FUNDED_COINS) - ); - Ok(index) - } - - pub async fn fund_account(&self, index: usize, amount: Option) -> CliTypedResult { - FundWithFaucet { - profile_options: Default::default(), - account: self.account_id(index), - faucet_options: self.faucet_options(), - amount: amount.unwrap_or(DEFAULT_FUNDED_COINS), - rest_options: self.rest_options(), - } - .execute() - .await - } - - pub async fn lookup_address( - &self, - public_key: &Ed25519PublicKey, - ) -> CliTypedResult { - LookupAddress { - public_key_options: PublicKeyInputOptions::from_key(public_key), - rest_options: self.rest_options(), - encoding_options: Default::default(), - profile_options: Default::default(), - } - .execute() - .await - } - - pub async fn rotate_key( - &mut self, - index: usize, - new_private_key: String, - gas_options: Option, - ) -> CliTypedResult { - let response = RotateKey { - txn_options: TransactionOptions { - private_key_options: PrivateKeyInputOptions::from_private_key( - self.private_key(index), - ) - .unwrap(), - sender_account: Some(self.account_id(index)), - rest_options: self.rest_options(), - gas_options: gas_options.unwrap_or_default(), - prompt_options: PromptOptions::yes(), - ..Default::default() - }, - new_private_key: Some(new_private_key), - save_to_profile: None, - new_private_key_file: None, - skip_saving_profile: true, - } - .execute() - .await?; - - Ok(response) - } - - pub async fn list_account(&self, index: usize, query: ListQuery) -> CliTypedResult> { - ListAccount { - rest_options: self.rest_options(), - profile_options: Default::default(), - account: Some(self.account_id(index)), - query, - } - .execute() - .await - } - - pub async fn transfer_coins( - &self, - sender_index: usize, - receiver_index: usize, - amount: u64, - gas_options: Option, - ) -> CliTypedResult { - TransferCoins { - txn_options: self.transaction_options(sender_index, gas_options), - account: self.account_id(receiver_index), - amount, - } - .execute() - .await - } - - pub async fn transfer_invalid_addr( - &self, - sender_index: usize, - amount: u64, - gas_options: Option, - ) -> CliTypedResult { - RunFunction { - entry_function_args: EntryFunctionArguments { - function_id: MemberId { - module_id: ModuleId::new(AccountAddress::ONE, ident_str!("coin").into()), - member_id: ident_str!("transfer").into(), - }, - arg_vec: ArgWithTypeVec { - args: vec![ - ArgWithType::from_str("address:0xdeadbeefcafebabe").unwrap(), - ArgWithType::from_str(&format!("u64:{}", amount)).unwrap(), - ], - }, - type_args: vec![MoveType::Struct(MoveStructTag::new( - AccountAddress::ONE.into(), - ident_str!("aptos_coin").into(), - ident_str!("AptosCoin").into(), - vec![], - ))], - }, - txn_options: self.transaction_options(sender_index, gas_options), - } - .execute() - .await - } - - pub async fn show_validator_config( - &self, - pool_index: usize, - ) -> CliTypedResult { - ShowValidatorConfig { - rest_options: self.rest_options(), - profile_options: Default::default(), - operator_args: self.operator_args(Some(pool_index)), - } - .execute() - .await - .map(|v| (&v).into()) - } - - pub async fn show_validator_set(&self) -> CliTypedResult { - ShowValidatorSet { - rest_options: self.rest_options(), - profile_options: Default::default(), - } - .execute() - .await - .map(|v| (&v).into()) - } - - pub async fn show_validator_stake(&self, pool_index: usize) -> CliTypedResult { - ShowValidatorStake { - rest_options: self.rest_options(), - profile_options: Default::default(), - operator_args: self.operator_args(Some(pool_index)), - } - .execute() - .await - } - - pub async fn initialize_validator( - &self, - index: usize, - consensus_public_key: bls12381::PublicKey, - proof_of_possession: bls12381::ProofOfPossession, - validator_host: HostAndPort, - validator_network_public_key: x25519::PublicKey, - ) -> CliTypedResult { - InitializeValidator { - txn_options: self.transaction_options(index, None), - operator_config_file_args: OperatorConfigFileArgs { - operator_config_file: None, - }, - validator_consensus_key_args: ValidatorConsensusKeyArgs { - consensus_public_key: Some(consensus_public_key), - proof_of_possession: Some(proof_of_possession), - }, - validator_network_addresses_args: ValidatorNetworkAddressesArgs { - validator_host: Some(validator_host), - validator_network_public_key: Some(validator_network_public_key), - full_node_host: None, - full_node_network_public_key: None, - }, - } - .execute() - .await - } - - pub async fn add_stake( - &self, - index: usize, - amount: u64, - ) -> CliTypedResult> { - AddStake { - txn_options: self.transaction_options(index, None), - amount, - } - .execute() - .await - } - - pub async fn unlock_stake( - &self, - index: usize, - amount: u64, - ) -> CliTypedResult> { - UnlockStake { - txn_options: self.transaction_options(index, None), - amount, - } - .execute() - .await - } - - pub async fn withdraw_stake( - &self, - index: usize, - amount: u64, - ) -> CliTypedResult> { - WithdrawStake { - node_op_options: self.transaction_options(index, None), - amount, - } - .execute() - .await - } - - pub async fn increase_lockup(&self, index: usize) -> CliTypedResult> { - IncreaseLockup { - txn_options: self.transaction_options(index, None), - } - .execute() - .await - } - - pub async fn join_validator_set( - &self, - operator_index: usize, - pool_index: Option, - ) -> CliTypedResult { - JoinValidatorSet { - txn_options: self.transaction_options(operator_index, None), - operator_args: self.operator_args(pool_index), - } - .execute() - .await - } - - pub async fn leave_validator_set( - &self, - operator_index: usize, - pool_index: Option, - ) -> CliTypedResult { - LeaveValidatorSet { - txn_options: self.transaction_options(operator_index, None), - operator_args: self.operator_args(pool_index), - } - .execute() - .await - } - - pub async fn update_validator_network_addresses( - &self, - operator_index: usize, - pool_index: Option, - validator_host: HostAndPort, - validator_network_public_key: x25519::PublicKey, - ) -> CliTypedResult { - UpdateValidatorNetworkAddresses { - txn_options: self.transaction_options(operator_index, None), - operator_args: self.operator_args(pool_index), - operator_config_file_args: OperatorConfigFileArgs { - operator_config_file: None, - }, - validator_network_addresses_args: ValidatorNetworkAddressesArgs { - validator_host: Some(validator_host), - validator_network_public_key: Some(validator_network_public_key), - full_node_host: None, - full_node_network_public_key: None, - }, - } - .execute() - .await - } - - pub async fn analyze_validator_performance( - &self, - start_epoch: Option, - end_epoch: Option, - ) -> CliTypedResult<()> { - AnalyzeValidatorPerformance { - start_epoch: start_epoch.unwrap_or(-2), - end_epoch, - rest_options: self.rest_options(), - profile_options: Default::default(), - analyze_mode: AnalyzeMode::All, - pool_addresses: vec![], - } - .execute() - .await - } - - pub async fn update_consensus_key( - &self, - operator_index: usize, - pool_index: Option, - consensus_public_key: bls12381::PublicKey, - proof_of_possession: bls12381::ProofOfPossession, - ) -> CliTypedResult { - UpdateConsensusKey { - txn_options: self.transaction_options(operator_index, None), - operator_args: self.operator_args(pool_index), - operator_config_file_args: OperatorConfigFileArgs { - operator_config_file: None, - }, - validator_consensus_key_args: ValidatorConsensusKeyArgs { - consensus_public_key: Some(consensus_public_key), - proof_of_possession: Some(proof_of_possession), - }, - } - .execute() - .await - } - - pub async fn init(&self, private_key: &Ed25519PrivateKey) -> CliTypedResult<()> { - InitTool { - network: Some(Network::Custom), - rest_url: Some(self.endpoint.clone()), - faucet_url: Some(self.faucet_endpoint.clone()), - rng_args: RngArgs::from_seed([0; 32]), - private_key_options: PrivateKeyInputOptions::from_private_key(private_key)?, - profile_options: Default::default(), - prompt_options: PromptOptions::yes(), - encoding_options: EncodingOptions::default(), - skip_faucet: false, - } - .execute() - .await - } - - pub async fn get_pool_address( - &self, - owner_index: usize, - ) -> CliTypedResult> { - GetStakePool { - owner_address: self.account_id(owner_index), - rest_options: self.rest_options(), - profile_options: Default::default(), - } - .execute() - .await - } - - pub async fn initialize_stake_owner( - &self, - owner_index: usize, - initial_stake_amount: u64, - voter_index: Option, - operator_index: Option, - ) -> CliTypedResult { - InitializeStakeOwner { - txn_options: self.transaction_options(owner_index, None), - initial_stake_amount, - operator_address: operator_index.map(|idx| self.account_id(idx)), - voter_address: voter_index.map(|idx| self.account_id(idx)), - } - .execute() - .await - } - - pub async fn create_stake_pool( - &self, - owner_index: usize, - operator_index: usize, - voter_index: usize, - amount: u64, - commission_percentage: u64, - ) -> CliTypedResult { - RunFunction { - entry_function_args: EntryFunctionArguments { - function_id: MemberId::from_str("0x1::staking_contract::create_staking_contract") - .unwrap(), - arg_vec: ArgWithTypeVec { - args: vec![ - ArgWithType::address(self.account_id(operator_index)), - ArgWithType::address(self.account_id(voter_index)), - ArgWithType::u64(amount), - ArgWithType::u64(commission_percentage), - ArgWithType::bytes(vec![]), - ], - }, - type_args: vec![], - }, - txn_options: self.transaction_options(owner_index, None), - } - .execute() - .await - } - - pub async fn set_operator( - &self, - owner_index: usize, - operator_index: usize, - ) -> CliTypedResult> { - SetOperator { - txn_options: self.transaction_options(owner_index, None), - operator_address: self.account_id(operator_index), - } - .execute() - .await - } - - pub async fn set_delegated_voter( - &self, - owner_index: usize, - voter_index: usize, - ) -> CliTypedResult> { - SetDelegatedVoter { - txn_options: self.transaction_options(owner_index, None), - voter_address: self.account_id(voter_index), - } - .execute() - .await - } - - /// Wait for an account to exist - pub async fn wait_for_account(&self, index: usize) -> CliTypedResult> { - let mut result = self.list_account(index, ListQuery::Balance).await; - let start = Instant::now(); - while start.elapsed() < Duration::from_secs(10) { - match result { - Ok(_) => return result, - _ => { - sleep(Duration::from_millis(500)).await; - result = self.list_account(index, ListQuery::Balance).await; - }, - }; - } - - result - } - - pub async fn account_balance_now(&self, index: usize) -> CliTypedResult { - let result = self.list_account(index, ListQuery::Balance).await?; - Ok(json_account_to_balance(result.first().unwrap())) - } - - pub async fn assert_account_balance_now(&self, index: usize, expected: u64) { - let result = self.list_account(index, ListQuery::Balance).await; - assert!( - result.is_ok(), - "Account {} not yet created, {}, last 10 transactions: {}", - self.account_id(index), - result.unwrap_err(), - self.last_n_transactions_details(10).await - ); - let accounts = result.unwrap(); - let account = accounts.first().unwrap(); - let coin = json_account_to_balance(account); - assert_eq!( - coin, - expected, - "Account {} with state: {:?}, last 10 transactions: {}", - self.account_id(index), - account, - self.last_n_transactions_details(10).await - ); - } - - async fn last_n_transactions_details(&self, count: u16) -> String { - let result = aptos_rest_client::Client::new(self.endpoint.clone()) - .get_transactions(None, Some(count)) - .await; - if let Err(e) = result { - return format!("Err({:?})", e); - } - let lines = result - .unwrap() - .inner() - .iter() - .map(|t| { - if let Transaction::UserTransaction(u) = t { - format!( - " * [{}] {}: sender={}, payload={:?}", - t.version().unwrap_or(0), - t.vm_status(), - u.request.sender, - u.request.payload - ) - } else { - format!( - " * [{}] {}: {}", - t.version().unwrap_or(0), - t.vm_status(), - t.type_str() - ) - } - }) - .collect::>(); - format!("\n{}\n", lines.join("\n")) - } - - pub async fn generate_x25519_key( - &self, - output_file: PathBuf, - seed: [u8; 32], - ) -> CliTypedResult> { - GenerateKey { - key_type: KeyType::X25519, - rng_args: RngArgs::from_seed(seed), - save_params: SaveKey { - file_options: SaveFile { - output_file, - prompt_options: PromptOptions::yes(), - }, - encoding_options: Default::default(), - }, - vanity_prefix: None, - vanity_multisig: false, - } - .execute() - .await - } - - pub async fn extract_peer( - &self, - host: HostAndPort, - private_key_file: PathBuf, - output_file: PathBuf, - ) -> CliTypedResult> { - ExtractPeer { - host, - network_key_input_options: NetworkKeyInputOptions::from_private_key_file( - private_key_file, - ), - output_file_options: SaveFile { - output_file, - prompt_options: PromptOptions::yes(), - }, - encoding_options: Default::default(), - } - .execute() - .await - } - - pub fn init_move_dir(&mut self) { - let move_dir = TempPath::new(); - move_dir - .create_as_dir() - .expect("Expected to be able to create move temp dir"); - self.move_dir = Some(move_dir.path().to_path_buf()); - } - - #[cfg(feature = "cli-framework-test-move")] - pub fn add_move_files(&self) { - let move_dir = self.move_dir(); - let sources_dir = move_dir.join("sources"); - - let hello_blockchain_contents = include_str!( - "../../../../aptos-move/move-examples/hello_blockchain/sources/hello_blockchain.move" - ); - let source_path = sources_dir.join("hello_blockchain.move"); - write_to_file( - source_path.as_path(), - &source_path.as_display().to_string(), - hello_blockchain_contents.as_bytes(), - ) - .unwrap(); - - let hello_blockchain_test_contents = include_str!("../../../../aptos-move/move-examples/hello_blockchain/sources/hello_blockchain_test.move"); - let test_path = sources_dir.join("hello_blockchain_test.move"); - write_to_file( - test_path.as_path(), - &test_path.as_display().to_string(), - hello_blockchain_test_contents.as_bytes(), - ) - .unwrap(); - } - - pub fn move_dir(&self) -> PathBuf { - assert!(self.move_dir.is_some(), "Must have initialized the temp move directory with `CliTestFramework::init_move_dir()` first"); - self.move_dir.as_ref().cloned().unwrap() - } - - pub async fn init_package( - &self, - name: String, - account_strs: BTreeMap<&str, &str>, - framework_dir: Option, - ) -> CliTypedResult<()> { - InitPackage { - name, - package_dir: Some(self.move_dir()), - named_addresses: Self::move_manifest_named_addresses(account_strs), - prompt_options: PromptOptions { - assume_yes: false, - assume_no: true, - }, - framework_package_args: FrameworkPackageArgs { - framework_git_rev: None, - framework_local_dir: framework_dir, - skip_fetch_latest_git_deps: false, - }, - } - .execute() - .await - } - - pub async fn compile_package( - &self, - account_strs: BTreeMap<&str, &str>, - included_artifacts: Option, - ) -> CliTypedResult> { - CompilePackage { - move_options: self.move_options(account_strs), - save_metadata: false, - included_artifacts_args: IncludedArtifactsArgs { - included_artifacts: included_artifacts.unwrap_or(IncludedArtifacts::Sparse), - }, - } - .execute() - .await - } - - pub async fn test_package( - &self, - account_strs: BTreeMap<&str, &str>, - filter: Option<&str>, - ) -> CliTypedResult<&'static str> { - TestPackage { - instruction_execution_bound: 100_000, - move_options: self.move_options(account_strs), - filter: filter.map(|str| str.to_string()), - ignore_compile_warnings: false, - compute_coverage: false, - dump_state: false, - } - .execute() - .await - } - - pub async fn publish_package( - &self, - index: usize, - gas_options: Option, - account_strs: BTreeMap<&str, &str>, - included_artifacts: Option, - ) -> CliTypedResult { - PublishPackage { - move_options: self.move_options(account_strs), - txn_options: self.transaction_options(index, gas_options), - override_size_check: false, - included_artifacts_args: IncludedArtifactsArgs { - included_artifacts: included_artifacts.unwrap_or(IncludedArtifacts::Sparse), - }, - } - .execute() - .await - } - - pub async fn download_package( - &self, - index: usize, - package: String, - output_dir: PathBuf, - ) -> CliTypedResult<&'static str> { - DownloadPackage { - rest_options: self.rest_options(), - profile_options: Default::default(), - account: self.account_id(index), - package, - output_dir: Some(output_dir), - } - .execute() - .await - } - - pub async fn run_function( - &self, - index: usize, - gas_options: Option, - function_id: MemberId, - args: Vec<&str>, - type_args: Vec<&str>, - ) -> CliTypedResult { - let mut parsed_args = Vec::new(); - for arg in args { - parsed_args.push( - ArgWithType::from_str(arg) - .map_err(|err| CliError::UnexpectedError(err.to_string()))?, - ) - } - - let mut parsed_type_args = Vec::new(); - for arg in type_args { - parsed_type_args.push( - MoveType::from_str(arg) - .map_err(|err| CliError::UnexpectedError(err.to_string()))?, - ) - } - - RunFunction { - txn_options: self.transaction_options(index, gas_options), - entry_function_args: EntryFunctionArguments { - function_id, - arg_vec: ArgWithTypeVec { args: parsed_args }, - type_args: parsed_type_args, - }, - } - .execute() - .await - } - - /// Runs the given script contents using the local aptos_framework directory. - pub async fn run_script( - &self, - index: usize, - script_contents: &str, - ) -> CliTypedResult { - self.run_script_with_framework_package(index, script_contents, FrameworkPackageArgs { - framework_git_rev: None, - framework_local_dir: Some(Self::aptos_framework_dir()), - skip_fetch_latest_git_deps: false, - }) - .await - } - - /// Runs the given script contents using the aptos_framework from aptos-core git repository. - pub async fn run_script_with_default_framework( - &self, - index: usize, - script_contents: &str, - ) -> CliTypedResult { - self.run_script_with_framework_package(index, script_contents, FrameworkPackageArgs { - framework_git_rev: None, - framework_local_dir: None, - skip_fetch_latest_git_deps: false, - }) - .await - } - - /// Runs the given script with the provided framework package arguments - pub async fn run_script_with_framework_package( - &self, - index: usize, - script_contents: &str, - framework_package_args: FrameworkPackageArgs, - ) -> CliTypedResult { - // Make a temporary directory for compilation - let temp_dir = TempDir::new().map_err(|err| { - CliError::UnexpectedError(format!("Failed to create temporary directory {}", err)) - })?; - - let source_path = temp_dir.path().join("script.move"); - write_to_file( - source_path.as_path(), - &source_path.as_display().to_string(), - script_contents.as_bytes(), - ) - .unwrap(); - - RunScript { - txn_options: self.transaction_options(index, None), - compile_proposal_args: CompileScriptFunction { - script_path: Some(source_path), - compiled_script_path: None, - framework_package_args, - bytecode_version: None, - }, - arg_vec: ArgWithTypeVec { args: Vec::new() }, - type_args: Vec::new(), - } - .execute() - .await - } - - pub async fn run_script_with_script_path( - &self, - index: usize, - script_path: &str, - args: Vec, - type_args: Vec, - ) -> CliTypedResult { - RunScript { - txn_options: self.transaction_options(index, None), - compile_proposal_args: CompileScriptFunction { - script_path: Some(script_path.parse().unwrap()), - compiled_script_path: None, - framework_package_args: FrameworkPackageArgs { - framework_git_rev: None, - framework_local_dir: Some(Self::aptos_framework_dir()), - skip_fetch_latest_git_deps: false, - }, - bytecode_version: None, - }, - arg_vec: ArgWithTypeVec { args }, - type_args, - } - .execute() - .await - } - - fn aptos_framework_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("aptos-move") - .join("framework") - .join("aptos-framework") - } - - pub fn move_options(&self, account_strs: BTreeMap<&str, &str>) -> MovePackageDir { - MovePackageDir { - package_dir: Some(self.move_dir()), - output_dir: None, - named_addresses: Self::named_addresses(account_strs), - skip_fetch_latest_git_deps: true, - bytecode_version: None, - } - } - - pub fn move_manifest_named_addresses( - account_strs: BTreeMap<&str, &str>, - ) -> BTreeMap { - account_strs - .iter() - .map(|(key, value)| { - ( - key.to_string(), - MoveManifestAccountWrapper::from_str(value).unwrap(), - ) - }) - .collect() - } - - pub fn named_addresses( - account_strs: BTreeMap<&str, &str>, - ) -> BTreeMap { - account_strs - .iter() - .map(|(key, value)| { - ( - key.to_string(), - AccountAddressWrapper::from_str(value).unwrap(), - ) - }) - .collect() - } - - pub fn rest_options(&self) -> RestOptions { - RestOptions::new(Some(self.endpoint.clone()), None) - } - - pub fn faucet_options(&self) -> FaucetOptions { - FaucetOptions::new(Some(self.faucet_endpoint.clone())) - } - - fn transaction_options( - &self, - index: usize, - gas_options: Option, - ) -> TransactionOptions { - TransactionOptions { - private_key_options: PrivateKeyInputOptions::from_private_key(self.private_key(index)) - .unwrap(), - sender_account: Some(self.account_id(index)), - rest_options: self.rest_options(), - gas_options: gas_options.unwrap_or_default(), - prompt_options: PromptOptions::yes(), - ..Default::default() - } - } - - fn operator_args(&self, pool_index: Option) -> OperatorArgs { - OperatorArgs { - pool_address_args: OptionalPoolAddressArgs { - pool_address: pool_index.map(|idx| self.account_id(idx)), - }, - } - } - - pub fn private_key(&self, index: usize) -> &Ed25519PrivateKey { - self.account_keys.get(index).unwrap() - } - - pub fn set_private_key( - &mut self, - index: usize, - new_key: Ed25519PrivateKey, - ) -> Ed25519PrivateKey { - // Insert the new private key into the test framework, returning the old one - mem::replace(&mut self.account_keys[index], new_key) - } - - pub fn account_id(&self, index: usize) -> AccountAddress { - *self.account_addresses.get(index).unwrap() - } - - pub async fn create_proposal( - &mut self, - index: usize, - metadata_url: &str, - script_path: PathBuf, - pool_address: AccountAddress, - is_multi_step: bool, - ) -> CliTypedResult { - SubmitProposal { - metadata_url: Url::parse(metadata_url).unwrap(), - pool_address_args: PoolAddressArgs { pool_address }, - txn_options: self.transaction_options(index, None), - is_multi_step, - compile_proposal_args: CompileScriptFunction { - script_path: Some(script_path), - compiled_script_path: None, - framework_package_args: FrameworkPackageArgs { - framework_git_rev: None, - framework_local_dir: Some(Self::aptos_framework_dir()), - skip_fetch_latest_git_deps: false, - }, - bytecode_version: None, - }, - } - .execute() - .await - } - - pub async fn vote( - &self, - index: usize, - proposal_id: u64, - yes: bool, - no: bool, - pool_addresses: Vec, - ) { - SubmitVote { - proposal_id, - yes, - no, - pool_addresses, - txn_options: self.transaction_options(index, None), - } - .execute() - .await - .expect("Successfully voted."); - } - - pub async fn verify_proposal( - &self, - proposal_id: u64, - script_path: &str, - ) -> CliTypedResult { - VerifyProposal { - proposal_id, - compile_proposal_args: CompileScriptFunction { - script_path: Some(script_path.parse().unwrap()), - compiled_script_path: None, - framework_package_args: FrameworkPackageArgs { - framework_git_rev: None, - framework_local_dir: Some(Self::aptos_framework_dir()), - skip_fetch_latest_git_deps: false, - }, - bytecode_version: None, - }, - rest_options: self.rest_options(), - profile: Default::default(), - prompt_options: PromptOptions::yes(), - } - .execute() - .await - } -} - -// ValidatorConfig/ValidatorSet doesn't match Move ValidatorSet struct, -// and json is serialized with different types from both, so hardcoding deserialization. - -fn json_account_to_balance(value: &Value) -> u64 { - u64::from_str( - value - .as_object() - .unwrap() - .get("coin") - .unwrap() - .as_object() - .unwrap() - .get("value") - .unwrap() - .as_str() - .unwrap(), - ) - .unwrap() -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct IndividualValidatorPerformance { - successful_proposals: String, - failed_proposals: String, -} - -impl IndividualValidatorPerformance { - pub fn successful_proposals(&self) -> u32 { - self.successful_proposals.parse().unwrap() - } - - pub fn failed_proposals(&self) -> u32 { - self.failed_proposals.parse().unwrap() - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ValidatorPerformance { - pub validators: Vec, -} diff --git a/m1/m1-cli/src/test/tests.rs b/m1/m1-cli/src/test/tests.rs deleted file mode 100644 index 62a8d997..00000000 --- a/m1/m1-cli/src/test/tests.rs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - move_tool::{ArgWithType, FunctionArgType}, - CliResult, Tool, -}; -use clap::Parser; -use std::str::FromStr; - -/// In order to ensure that there aren't duplicate input arguments for untested CLI commands, -/// we call help on every command to ensure it at least runs -#[tokio::test] -async fn ensure_every_command_args_work() { - assert_cmd_not_panic(&["movement"]).await; - - assert_cmd_not_panic(&["movement", "account"]).await; - assert_cmd_not_panic(&["movement", "account", "create", "--help"]).await; - assert_cmd_not_panic(&["movement", "account", "create-resource-account", "--help"]).await; - assert_cmd_not_panic(&["movement", "account", "fund-with-faucet", "--help"]).await; - assert_cmd_not_panic(&["movement", "account", "list", "--help"]).await; - assert_cmd_not_panic(&["movement", "account", "lookup-address", "--help"]).await; - assert_cmd_not_panic(&["movement", "account", "rotate-key", "--help"]).await; - assert_cmd_not_panic(&["movement", "account", "transfer", "--help"]).await; - - assert_cmd_not_panic(&["movement", "config"]).await; - assert_cmd_not_panic(&["movement", "config", "generate-shell-completions", "--help"]).await; - assert_cmd_not_panic(&["movement", "config", "init", "--help"]).await; - assert_cmd_not_panic(&["movement", "config", "set-global-config", "--help"]).await; - assert_cmd_not_panic(&["movement", "config", "show-global-config"]).await; - assert_cmd_not_panic(&["movement", "config", "show-profiles"]).await; - - assert_cmd_not_panic(&["movement", "genesis"]).await; - assert_cmd_not_panic(&["movement", "genesis", "generate-genesis", "--help"]).await; - assert_cmd_not_panic(&["movement", "genesis", "generate-keys", "--help"]).await; - assert_cmd_not_panic(&["movement", "genesis", "generate-layout-template", "--help"]).await; - assert_cmd_not_panic(&["movement", "genesis", "set-validator-configuration", "--help"]).await; - assert_cmd_not_panic(&["movement", "genesis", "setup-git", "--help"]).await; - assert_cmd_not_panic(&["movement", "genesis", "generate-admin-write-set", "--help"]).await; - - assert_cmd_not_panic(&["movement", "governance"]).await; - assert_cmd_not_panic(&["movement", "governance", "execute-proposal", "--help"]).await; - assert_cmd_not_panic(&["movement", "governance", "generate-upgrade-proposal", "--help"]).await; - assert_cmd_not_panic(&["movement", "governance", "propose", "--help"]).await; - assert_cmd_not_panic(&["movement", "governance", "vote", "--help"]).await; - - assert_cmd_not_panic(&["movement", "info"]).await; - - assert_cmd_not_panic(&["movement", "init", "--help"]).await; - - assert_cmd_not_panic(&["movement", "key"]).await; - assert_cmd_not_panic(&["movement", "key", "generate", "--help"]).await; - assert_cmd_not_panic(&["movement", "key", "extract-peer", "--help"]).await; - - assert_cmd_not_panic(&["movement", "move"]).await; - assert_cmd_not_panic(&["movement", "move", "clean", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "compile", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "compile-script", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "download", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "init", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "list", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "prove", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "publish", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "run", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "run-script", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "test", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "transactional-test", "--help"]).await; - assert_cmd_not_panic(&["movement", "move", "view", "--help"]).await; - - assert_cmd_not_panic(&["movement", "node"]).await; - assert_cmd_not_panic(&["movement", "node", "check-network-connectivity", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "get-stake-pool", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "analyze-validator-performance", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "bootstrap-db-from-backup", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "initialize-validator", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "join-validator-set", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "leave-validator-set", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "run-local-testnet", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "show-validator-config", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "show-validator-set", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "show-validator-stake", "--help"]).await; - assert_cmd_not_panic(&["movement", "node", "update-consensus-key", "--help"]).await; - assert_cmd_not_panic(&[ - "movement", - "node", - "update-validator-network-addresses", - "--help", - ]) - .await; - - assert_cmd_not_panic(&["movement", "stake"]).await; - assert_cmd_not_panic(&["movement", "stake", "add-stake", "--help"]).await; - assert_cmd_not_panic(&["movement", "stake", "increase-lockup", "--help"]).await; - assert_cmd_not_panic(&["movement", "stake", "initialize-stake-owner", "--help"]).await; - assert_cmd_not_panic(&["movement", "stake", "set-delegated-voter", "--help"]).await; - assert_cmd_not_panic(&["movement", "stake", "set-operator", "--help"]).await; - assert_cmd_not_panic(&["movement", "stake", "unlock-stake", "--help"]).await; - assert_cmd_not_panic(&["movement", "stake", "withdraw-stake", "--help"]).await; -} - -/// Ensure we can parse URLs for args -#[tokio::test] -async fn ensure_can_parse_args_with_urls() { - let result = ArgWithType::from_str("string:https://aptoslabs.com").unwrap(); - matches!(result._ty, FunctionArgType::String); - assert_eq!( - result.arg, - bcs::to_bytes(&"https://aptoslabs.com".to_string()).unwrap() - ); -} - -async fn assert_cmd_not_panic(args: &[&str]) { - // When a command fails, it will have a panic in it due to an improperly setup command - // thread 'main' panicked at 'Command propose: Argument names must be unique, but 'assume-yes' is - // in use by more than one argument or group', ... - - match run_cmd(args).await { - Ok(inner) => assert!( - !inner.contains("panic"), - "Failed to not panic cmd {}: {}", - args.join(" "), - inner - ), - Err(inner) => assert!( - !inner.contains("panic"), - "Failed to not panic cmd {}: {}", - args.join(" "), - inner - ), - } -} - -async fn run_cmd(args: &[&str]) -> CliResult { - let tool: Tool = Tool::try_parse_from(args).map_err(|msg| msg.to_string())?; - tool.execute().await -} diff --git a/m1/m1-cli/src/update/helpers.rs b/m1/m1-cli/src/update/helpers.rs deleted file mode 100644 index b54a0c75..00000000 --- a/m1/m1-cli/src/update/helpers.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::{anyhow, Context, Result}; -use self_update::{backends::github::ReleaseList, cargo_crate_version, version::bump_is_greater}; - -#[derive(Debug)] -pub struct UpdateRequiredInfo { - pub update_required: bool, - pub current_version: String, - pub latest_version: String, - pub latest_version_tag: String, -} - -/// Return information about whether an update is required. -pub fn check_if_update_required(repo_owner: &str, repo_name: &str) -> Result { - // Build a configuration for determining the latest release. - let config = ReleaseList::configure() - .repo_owner(repo_owner) - .repo_name(repo_name) - .build() - .map_err(|e| anyhow!("Failed to build configuration to fetch releases: {:#}", e))?; - - // Get the most recent releases. - let releases = config - .fetch() - .map_err(|e| anyhow!("Failed to fetch releases: {:#}", e))?; - - // Find the latest release of the CLI, in which we filter for the CLI tag. - // If the release isn't in the last 30 items (the default API page size) - // this will fail. See https://github.com/aptos-labs/aptos-core/issues/6411. - let mut releases = releases.into_iter(); - let latest_release = loop { - let release = match releases.next() { - Some(release) => release, - None => return Err(anyhow!("Failed to find latest CLI release")), - }; - if release.version.starts_with("movement-cli-") { - break release; - } - }; - let latest_version_tag = latest_release.version; - let latest_version = latest_version_tag.split("-v").last().unwrap(); - - // Return early if we're up to date already. - let current_version = cargo_crate_version!(); - let update_required = bump_is_greater(current_version, latest_version) - .context("Failed to compare current and latest CLI versions")?; - - Ok(UpdateRequiredInfo { - update_required, - current_version: current_version.to_string(), - latest_version: latest_version.to_string(), - latest_version_tag, - }) -} - -pub enum InstallationMethod { - Source, - Homebrew, - Other, -} - -impl InstallationMethod { - pub fn from_env() -> Result { - // Determine update instructions based on what we detect about the installation. - let exe_path = std::env::current_exe()?; - let installation_method = if exe_path.to_string_lossy().contains("brew") { - InstallationMethod::Homebrew - } else if exe_path.to_string_lossy().contains("target") { - InstallationMethod::Source - } else { - InstallationMethod::Other - }; - Ok(installation_method) - } -} diff --git a/m1/m1-cli/src/update/mod.rs b/m1/m1-cli/src/update/mod.rs deleted file mode 100644 index 17dab6d1..00000000 --- a/m1/m1-cli/src/update/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -mod helpers; -mod tool; - -use helpers::check_if_update_required; -pub use tool::UpdateTool; diff --git a/m1/m1-cli/src/update/tool.rs b/m1/m1-cli/src/update/tool.rs deleted file mode 100644 index 1528ff85..00000000 --- a/m1/m1-cli/src/update/tool.rs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use super::{check_if_update_required, helpers::InstallationMethod}; -use crate::common::{ - types::{CliCommand, CliTypedResult}, - utils::cli_build_information, -}; -use anyhow::{anyhow, Context}; -use aptos_build_info::BUILD_OS; -use async_trait::async_trait; -use clap::Parser; -use self_update::{backends::github::Update, cargo_crate_version, Status}; -use std::process::Command; - -/// Update the CLI itself -/// -/// This can be used to update the CLI to the latest version. This is useful if you -/// installed the CLI via the install script / by downloading the binary directly. -#[derive(Debug, Parser)] -pub struct UpdateTool { - /// The owner of the repo to download the binary from. - #[clap(long, default_value = "movemnt")] - repo_owner: String, - - /// The name of the repo to download the binary from. - #[clap(long, default_value = "subnet")] // TODO: update CI/CD to include this binar release in GitHub workflows - repo_name: String, -} - -impl UpdateTool { - // Out of the box this crate assumes that you have releases named a specific way - // with the crate name, version, and target triple in a specific format. We don't - // do this with our releases, we have other GitHub releases beyond just the CLI, - // and we don't build for all major target triples, so we have to do some of the - // work ourselves first to figure out what the latest version of the CLI is and - // which binary to download based on the current OS. Then we can plug that into - // the library which takes care of the rest. - fn update(&self) -> CliTypedResult { - let installation_method = - InstallationMethod::from_env().context("Failed to determine installation method")?; - match installation_method { - InstallationMethod::Source => { - return Err( - anyhow!("Detected this CLI was built from source, refusing to update").into(), - ); - }, - InstallationMethod::Homebrew => { - return Err(anyhow!( - "Detected this CLI comes from homebrew, use `brew upgrade aptos` instead" - ) - .into()); - }, - InstallationMethod::Other => {}, - } - - let info = check_if_update_required(&self.repo_owner, &self.repo_name)?; - if !info.update_required { - return Ok(format!("CLI already up to date (v{})", info.latest_version)); - } - - // Determine the target we should download. This is necessary because we don't - // name our binary releases using the target triples nor do we build specifically - // for all major triples, so we have to generalize to one of the binaries we do - // happen to build. We figure this out based on what system the CLI was built on. - let build_info = cli_build_information(); - let target = match build_info.get(BUILD_OS).context("Failed to determine build info of current CLI")?.as_str() { - "linux-x86_64" => { - // In the case of Linux, which build to use depends on the OpenSSL - // library on the host machine. So we try to determine that here. - // This code below parses the output of the `openssl version` command, - // where the version string is the 1th (0-indexing) item in the string - // when split by whitespace. - let output = Command::new("openssl") - .args(["version"]) - .output(); - let version = match output { - Ok(output) => { - let stdout = String::from_utf8(output.stdout).unwrap(); - stdout.split_whitespace().collect::>()[1].to_string() - }, - Err(e) => { - println!("Failed to determine OpenSSL version, assuming an older version: {:#}", e); - "1.0.0".to_string() - } - }; - // On Ubuntu < 22.04 the bundled OpenSSL is version 1.x.x, whereas on - // 22.04+ it is 3.x.x. Unfortunately if you build the CLI on a system - // with one major version of OpenSSL, you cannot use it on a system - // with a different version. Accordingly, if the current system uses - // OpenSSL 3.x.x, we use the version of the CLI built on a system with - // OpenSSL 3.x.x, meaning Ubuntu 22.04. Otherwise we use the one built - // on 20.04. - if version.starts_with('3') { - "Ubuntu-22.04-x86_64" - } else { - "Ubuntu-x86_64" - } - }, - "macos-x86_64" => "MacOSX-x86_64", - "windows-x86_64" => "Windows-x86_64", - wildcard => return Err(anyhow!("Self-updating is not supported on your OS right now, please download the binary manually: {}", wildcard).into()), - }; - - // Build a new configuration that will direct the library to download the - // binary with the target version tag and target that we determined above. - let config = Update::configure() - .repo_owner(&self.repo_owner) - .repo_name(&self.repo_name) - .bin_name("movement") - .current_version(cargo_crate_version!()) - .target_version_tag(&info.latest_version_tag) - .target(target) - .build() - .map_err(|e| anyhow!("Failed to build self-update configuration: {:#}", e))?; - - // Update the binary. - let result = config - .update() - .map_err(|e| anyhow!("Failed to update Movement CLI: {:#}", e))?; - - let message = match result { - Status::UpToDate(_) => panic!("We should have caught this already"), - Status::Updated(_) => format!( - "Successfully updated from v{} to v{}", - info.current_version, info.latest_version - ), - }; - - Ok(message) - } -} - -#[async_trait] -impl CliCommand for UpdateTool { - fn command_name(&self) -> &'static str { - "Update" - } - - async fn execute(self) -> CliTypedResult { - tokio::task::spawn_blocking(move || self.update()) - .await - .context("Failed to self-update Movement CLI")? - } -}