diff --git a/.claude/commands/lint.md b/.claude/commands/lint.md
new file mode 100644
index 0000000000000000000000000000000000000000..dc7ec8202625c688b1813bd01c405bee41495b7c
--- /dev/null
+++ b/.claude/commands/lint.md
@@ -0,0 +1,13 @@
+Run all linting and formatting checks on the codebase.
+
+Execute this command:
+
+```bash
+pre-commit run --all-files
+```
+
+Report the results:
+
+- If all checks pass, confirm success
+- If checks fail, summarize which hooks failed and the key issues found
+- For auto-fixable issues (like formatting), mention if files were modified
diff --git a/.claude/commands/reflection.md b/.claude/commands/reflection.md
new file mode 100644
index 0000000000000000000000000000000000000000..5368d98e52c039fa6a601cfecedf49e2df1f613d
--- /dev/null
+++ b/.claude/commands/reflection.md
@@ -0,0 +1,48 @@
+You are an expert in prompt engineering, specializing in optimizing AI code assistant instructions. Your task is to
+analyze and improve the instructions for Claude Code. Follow these steps carefully:
+
+1. Analysis Phase: Review the chat history in your context window.
+
+Then, examine the current Claude instructions, commands and config /CLAUDE.md /.claude/commands/\*
+\*\*/CLAUDE.md .claude/settings.json .claude/settings.local.json
+
+Analyze the chat history, instructions, commands and config to identify areas that could be improved. Look for:
+
+- Inconsistencies in Claude's responses
+- Misunderstandings of user requests
+- Areas where Claude could provide more detailed or accurate information
+- Opportunities to enhance Claude's ability to handle specific types of queries or tasks
+- New commands or improvements to a commands name, function or response
+- MCPs we've approved locally that we should add to the config, especially if we've added new tools or require them
+ for the command to work
+
+2. Interaction Phase: Present your findings and improvement ideas to the human. For each suggestion: a) Explain the
+ current issue you've identified b) Propose a specific change or addition to the instructions c) Describe how this
+ change would improve Claude's performance
+
+Wait for feedback from the human on each suggestion before proceeding. If the human approves a change, move it to the
+implementation phase. If not, refine your suggestion or move on to the next idea.
+
+3. Implementation Phase: For each approved change: a) Clearly state the section of the instructions you're modifying b)
+ Present the new or modified text for that section c) Explain how this change addresses the issue identified in the
+ analysis phase
+
+4. Output Format: Present your final output in the following structure:
+
+
+[List the issues identified and potential improvements]
+
+
+
+[For each approved improvement:
+1. Section being modified
+2. New or modified instruction text
+3. Explanation of how this addresses the identified issue]
+
+
+ [Present the complete, updated set of instructions for Claude, incorporating all approved changes]
+
+
+Remember, your goal is to enhance Claude's performance and consistency while maintaining the core functionality and
+purpose of the AI assistant. Be thorough in your analysis, clear in your explanations, and precise in your
+implementations.
diff --git a/.claude/commands/smoke.md b/.claude/commands/smoke.md
new file mode 100644
index 0000000000000000000000000000000000000000..10230b31e84ca9917a4fb5fe629e9fe5b025ab97
--- /dev/null
+++ b/.claude/commands/smoke.md
@@ -0,0 +1,9 @@
+Run the smoke test to quickly verify basic CLI functionality.
+
+Execute this command:
+
+```bash
+pytest -n 2 tests/functional/cli/show/test_show_account.py::test_show_account tests/functional/cli/process/test_process_transfer.py::test_process_transfer
+```
+
+Report the results concisely - whether tests passed or failed, and any errors encountered.
diff --git a/.claude/commands/test.md b/.claude/commands/test.md
new file mode 100644
index 0000000000000000000000000000000000000000..0ec397fc322fe41ef2ed883c773e5f06b8b88889
--- /dev/null
+++ b/.claude/commands/test.md
@@ -0,0 +1,28 @@
+Run pytest with the provided arguments.
+
+Arguments: $ARGUMENTS
+
+First, expand any shortcuts in the arguments:
+
+- `unit` → `tests/unit/`
+- `cli` → `tests/functional/cli/`
+- `tui` → `tests/functional/tui/`
+- `functional` → `tests/functional/`
+
+Then execute pytest with the expanded path. Examples:
+
+- `/test unit` runs `pytest tests/unit/`
+- `/test cli` runs `pytest tests/functional/cli/`
+- `/test tests/unit/test_date_utils.py -v` runs as-is (full path provided)
+
+If no arguments provided, run all tests with parallel execution:
+
+```bash
+pytest -n 16
+```
+
+Report results concisely:
+
+- Number of tests passed/failed/skipped
+- For failures, show the test name and a brief summary of the error
+- Suggest next steps if tests fail
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000000000000000000000000000000000000..707610c7924d480a86aacc6ad7b400868c28500d
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,311 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+**Clive** is a CLI and TUI (Terminal User Interface) application for interacting with the Hive blockchain. It's written
+in Python and designed to replace the original Hive CLI for power users and testing. The TUI features mouse-based
+navigation inspired by midnight commander.
+
+- **Main entry point**: `clive` - automatically launches TUI when run without arguments, or CLI mode when arguments
+ are provided like `clive` for TUI and `clive --help` for CLI
+- **Development entry point**: `clive-dev` - includes extra debugging information
+- **Python version**: (see `requires-python` in `pyproject.toml`, restricted due to wax dependency)
+- **Build system**: Poetry
+- **Main branch**: `develop` (use this for PRs, not `main`)
+
+## GitLab Instance
+
+This project uses **gitlab.syncad.com**, NOT gitlab.com.
+
+- Repository: https://gitlab.syncad.com/hive/clive
+- Use `glab api "projects/hive%2Fclive/..."` for API calls
+
+## Claude Code Commands
+
+Available slash commands for development workflows:
+
+| Command | Description |
+| -------------- | ------------------------------------------------------------- |
+| `/smoke` | Run smoke test |
+| `/lint` | Run all pre-commit hooks |
+| `/test ` | Run pytest with shortcuts: `unit`, `cli`, `tui`, `functional` |
+| `/reflection` | Analyze and improve Claude Code configuration |
+
+## Essential Commands
+
+### Installation and Setup
+
+```bash
+# Install dependencies (must be run from repository root)
+poetry install
+
+# When updating dependencies and hive submodule is updated also, test-tools should be forced to uninstall first
+pip uninstall -y test-tools && poetry install
+```
+
+### Running Clive
+
+```bash
+# Launch TUI (default)
+clive
+
+# Use CLI mode
+clive --help
+clive show profile
+clive configure profile create
+
+# Development mode with debug info (useful for presenting full stack-trace instead of pretty errors in CLI)
+clive-dev
+```
+
+### Linting and Formatting
+
+```bash
+# Run all pre-commit hooks (include tools like ruff, mypy and additional hooks)
+pre-commit run --all-files
+
+# Lint with Ruff
+ruff check clive/ tests/
+
+# Format code
+ruff format clive/ tests/
+
+# Type checking
+mypy clive/ tests/
+```
+
+### Testing
+
+Use `/smoke`, `/lint`, and `/test` slash commands for common workflows (see
+[Claude Code Commands](#claude-code-commands)).
+
+```bash
+# Run a specific test (example)
+pytest tests/unit/test_date_utils.py::test_specific_function -v
+
+# Run with timeout (important for tests that may hang)
+pytest --timeout=600
+
+# Run without parallelization (for debugging)
+pytest -n 0
+```
+
+**Note**: Tests require the embedded testnet dependencies. Some tests spawn local Hive nodes using `test-tools`.
+
+## Architecture
+
+### Configuration
+
+**Global settings** can be configured via:
+
+1. **Settings files** (in order of precedence):
+
+ - `~/.clive/settings.toml` (user settings, higher priority)
+ - `{project_root}/settings.toml` (project defaults)
+
+2. **Environment variables** override settings files using the format:
+
+ ```bash
+ CLIVE_{GROUP}__{KEY}=value
+ ```
+
+ Example: `CLIVE_NODE__CHAIN_ID=abc123` overrides `[NODE] CHAIN_ID` in settings.toml
+
+3. **Special environment variable**: `CLIVE_DATA_PATH` controls the Clive data directory (default: `~/.clive`). This
+ also determines where user settings are loaded from (`$CLIVE_DATA_PATH/settings.toml`).
+
+**Per-profile settings** are stored separately for each profile and configured via `clive configure`:
+
+- Tracked accounts (working account + watched accounts)
+- Known accounts (and enable/disable feature)
+- Key aliases
+- Node address
+- Chain ID
+
+See `clive configure --help` for all available options.
+
+### Core Architecture Pattern: Command Pattern
+
+Clive uses a **Command Pattern** for all operations that interact with the blockchain or beekeeper:
+
+- **Commands location**: `clive/__private/core/commands/`
+- **Base classes**: All commands inherit from `Command` (in `abc/command.py`)
+- **Execution**: Commands are async and executed via `await command.execute()`
+- **Command hierarchy**:
+ - `Command` - Base class with `_execute()` method
+ - `CommandWithResult` - Commands that return a value
+ - `CommandRestricted` - Base for commands with execution preconditions
+ - `CommandInUnlocked` - Commands requiring unlocked user wallet
+ - `CommandEncryption` - Commands requiring both unlocked user wallet and encryption wallet
+ - `CommandPasswordSecured` - Commands requiring a password
+ - `CommandDataRetrieval` - Commands that fetch data from the node
+ - `CommandCachedDataRetrieval` - Data retrieval with caching support
+
+### World Object - Application Container
+
+`World` (`clive/__private/core/world.py`) is the top-level container and single source of truth:
+
+- `world.profile` - Current user profile (settings, accounts, keys)
+- `world.node` - Hive node connection for API calls
+- `world.commands` - Access to all command instances
+- `world.beekeeper_manager` - Manages beekeeper (key storage) lifecycle
+- `world.app_state` - Application state (locked/unlocked, etc.)
+
+**Important**: Direct `world.node` API calls should be avoided in CLI/TUI. Use `world.commands` instead, which handles
+errors properly.
+
+### Profile System
+
+Profiles (`clive/__private/core/profile.py`) store user configuration:
+
+- **Working account**: The currently active Hive account
+- **Watched accounts**: Accounts being monitored
+- **Known accounts**: Accounts approved for transactions. CLI requires explicit addition before broadcasting
+ operations (configurable via `enable`/`disable`). TUI automatically adds accounts when operations are added to cart
+ (also configurable). Managed via `clive configure known-account`
+- **Key aliases**: Named public keys
+- **Transaction**: Pending transaction operations
+- **Node address**: Hive node endpoint
+- **Chain ID**: Blockchain identifier (like a mainnet/mirrornet/testnet)
+
+Profiles are persisted to disk via `PersistentStorageService` with encryption support.
+
+**Note**: Tracked accounts is a combination of working account and watched accounts.
+
+### Dual Interface Architecture
+
+**CLI Mode** (`clive/__private/cli/`):
+
+- Built with **Typer** for command-line interface
+- Main command groups: `configure`, `show`, `process`, `beekeeper`, `generate`, `unlock`, `lock`
+- For complete CLI command structure, see `docs/cli_commands_structure.md`
+- CLI implementation in `clive/__private/cli/`
+- Most of the commands —especially those that interact with profile— require Beekeeper (via the
+ `CLIVE_BEEKEEPER__REMOTE_ADDRESS` and `CLIVE_BEEKEEPER__SESSION_TOKEN` environment variables) for profile encryption
+ and decryption.
+- Commands `clive beekeeper spawn` and `clive beekeeper create-session` can be used for preparing the CLI environment.
+
+**TUI Mode** (`clive/__private/ui/`):
+
+- Built with **Textual** (Python TUI framework)
+- Main app: `clive/__private/ui/app.py` (Clive class)
+- Screens in `clive/__private/ui/screens/`
+- Widgets in `clive/__private/ui/widgets/`
+- Styling: TCSS (a Textual variation of CSS) files stored as .scss due to better syntax highlighting
+- TUI can be used in environment where Beekeeper is already running (`CLIVE_BEEKEEPER__REMOTE_ADDRESS` and
+ `CLIVE_BEEKEEPER__SESSION_TOKEN` env vars are set), but without them, beekeeper will be automatically spawned and
+ session will be created when starting TUI.
+
+### Beekeeper Integration
+
+Clive uses **beekeepy** (async Python wrapper) to communicate with Hive's beekeeper for key management:
+
+- **BeekeeperManager**: `clive/__private/core/beekeeper_manager.py`
+- Beekeeper stores keys in encrypted wallets
+- Beekeeper wallets are stored in the `~/.clive/beekeeper` directory (or `$CLIVE_DATA_PATH/beekeeper` if customized)
+- Two wallet types: user wallets (for signing) and encryption wallets (for encrypting profile data)
+- Wallets must be unlocked before use
+- Beekeeper address and session token can be pointed with respective setting in the `settings.toml` file or via env
+ var that would have higher precedence
+
+### Blockchain Communication
+
+- **Node interaction**: `clive/__private/core/node/node.py`
+- **API wrapper**: `clive/__private/core/node/async_hived/` - async wrapper around Hive node APIs
+- **Wax integration**: Uses `hiveio-wax` for transaction building and signing
+- **Operation models**: `clive/__private/models/schemas.py` - Pydantic models for Hive operations
+
+### Storage and Migrations
+
+- **Storage service**: `clive/__private/storage/service/service.py`
+- **Converters**: Runtime models ↔ Storage models
+- **Migrations**: `clive/__private/storage/migrations/` - versioned profile schema migrations
+- Profiles are stored as encrypted files (location can controlled by `CLIVE_DATA_PATH` environment variable, default:
+ `~/.clive/data/`)
+
+## Test Organization
+
+Tests are organized into two main categories:
+
+- **`tests/unit/`** - Unit tests for individual components (keys, storage, commands, etc.)
+- **`tests/functional/`** - Functional tests split by interface:
+ - `functional/cli/` - CLI command tests
+ - `functional/tui/` - TUI interaction tests
+
+**Test fixtures and patterns**:
+
+Common fixtures (`tests/conftest.py`):
+
+- `world` - Async World instance
+- `beekeeper` - Async beekeeper instance from beekeepy (spawned automatically)
+- `node` - Local testnet node (spawned automatically via test-tools)
+
+CLI test patterns (`tests/functional/cli/`):
+
+- Uses `CLITester` from `clive-local-tools` package
+- `cli_tester` fixture - Provides typed CLI testing interface with command invocation and output checking
+
+TUI test patterns (`tests/functional/tui/`):
+
+- Uses `ClivePilot` (Textual's async test driver)
+- `prepared_env` fixture - Returns `(node, wallet, pilot)` tuple with TUI ready on Unlock screen
+- `prepared_tui_on_dashboard` fixture - TUI already authenticated and on Dashboard screen
+- `node_with_wallet` fixture - Test node with initialized wallet
+- Tests interact with TUI via pilot (e.g., `pilot.click()`, `pilot.press()`)
+
+## Development Guidelines
+
+### Code Style
+
+- **Strict mypy**: Type hints are required and strictly enforced
+- **Ruff**: Comprehensive linting with "ALL" rules (see `pyproject.toml` for ignored rules)
+- **Future imports**: All files must have `from __future__ import annotations` (enforced by ruff)
+- **Docstrings**: Google style, checked by pydoclint (no redundant type hints in docstrings)
+- **Line length**: See `line-length` in `pyproject.toml` (currently 120 characters)
+
+### Important Conventions
+
+1. **Private modules**: Implementation details are in `__private/` directories
+2. **No direct initialization**: Some classes (like `Profile`) use factory methods instead of `__init__`
+3. **Command pattern**: Always use commands for blockchain operations, not direct API calls
+4. **Async context managers**: Many resources (World, Node) require async context manager usage
+5. **Settings**: Use `safe_settings` from `clive/__private/settings` for reading configuration
+
+### When Working With Tests
+
+- Pytest tests use `test-tools` from the Hive submodule for spawning local nodes
+- For manual tests, local testnet node can be started manually via `testnet_node.py`
+- Both `testnet_node.py` and `test-tools` based tests require executables from the `hive` submodule pointed by the
+ `HIVE_BUILD_ROOT_PATH` environment variable
+- Beekeeper is spawned automatically by test fixtures when needed
+- Tests modify settings to use test directories, not user's actual Clive data
+- The `clive-local-tools` package provides test helpers and checkers
+
+### Useful Commands
+
+#### GitLab CLI (glab)
+
+```bash
+# Find MR for a branch
+glab mr list --source-branch=
+
+# Add comment to MR
+glab mr note --message "..."
+
+# Get pipeline job details
+glab api "projects/hive%2Fclive/pipelines//jobs"
+
+# Get job logs
+glab api "projects/hive%2Fclive/jobs//trace"
+```
+
+### CI Environment
+
+Tests run in CI with:
+
+- Parallel processes configurable via `PYTEST_NUMBER_OF_PROCESSES` (see `.gitlab-ci.yml`, default: 16)
+- Timeout configurable via `PYTEST_TIMEOUT_MINUTES` (typically 10 minutes for most test suites)
+- Separate jobs for unit tests, CLI tests, and TUI tests
+- Tests run against installed wheel (not editable install)
diff --git a/clive/__private/cli/common/parameters/argument_related_options.py b/clive/__private/cli/common/parameters/argument_related_options.py
index 40537daa3e3f913a5980afe9e4bb22ba0a136cbd..b5e47778b3341881bda9b2a09831301ef2d442a7 100644
--- a/clive/__private/cli/common/parameters/argument_related_options.py
+++ b/clive/__private/cli/common/parameters/argument_related_options.py
@@ -13,6 +13,7 @@ import typer
from clive.__private.cli.common.parameters import options
from clive.__private.cli.common.parameters.modified_param import modified_param
+from clive.__private.cli.common.parsers import account_name as account_name_parser
from clive.__private.cli.common.parsers import public_key
from clive.__private.core.constants.cli import LOOK_INTO_ARGUMENT_OPTION_HELP
@@ -41,7 +42,14 @@ profile_name = _make_argument_related_option(options.profile_name)
new_account_name = _make_argument_related_option(options.new_account_name)
-name = _make_argument_related_option("--name")
+name = _make_argument_related_option(
+ typer.Option(
+ None,
+ "--name",
+ parser=account_name_parser,
+ help=LOOK_INTO_ARGUMENT_OPTION_HELP,
+ )
+)
key = _make_argument_related_option("--key")
diff --git a/clive/__private/cli/common/parameters/arguments.py b/clive/__private/cli/common/parameters/arguments.py
index 32445eb4501033c4307ab3e464d444eb3b71db4f..96f5305dd4b30f77356406a536921d86c2ac146d 100644
--- a/clive/__private/cli/common/parameters/arguments.py
+++ b/clive/__private/cli/common/parameters/arguments.py
@@ -12,11 +12,13 @@ import typer
from clive.__private.cli.common.parameters import modified_param
from clive.__private.cli.common.parameters.styling import stylized_help
+from clive.__private.cli.common.parsers import account_name as account_name_parser
from clive.__private.cli.common.parsers import public_key
from clive.__private.core.constants.cli import PERFORM_WORKING_ACCOUNT_LOAD
working_account_template = typer.Argument(
PERFORM_WORKING_ACCOUNT_LOAD, # we don't know if account_name_option is required until the profile is loaded
+ parser=account_name_parser,
help=stylized_help("The account to use.", is_working_account_default=True, required_as_arg_or_option=True),
show_default=False,
)
diff --git a/clive/__private/cli/common/parameters/options.py b/clive/__private/cli/common/parameters/options.py
index c957e38ed15385c853a498353ef66bad4ab3d69b..fc7838152c80198072f5dd2050394afea75e1bc3 100644
--- a/clive/__private/cli/common/parameters/options.py
+++ b/clive/__private/cli/common/parameters/options.py
@@ -14,6 +14,9 @@ import typer
from clive.__private.cli.common.parameters.modified_param import modified_param
from clive.__private.cli.common.parameters.styling import stylized_help
+from clive.__private.cli.common.parsers import (
+ account_name as account_name_parser,
+)
from clive.__private.cli.common.parsers import (
decimal_percent,
liquid_asset,
@@ -28,12 +31,14 @@ from clive.__private.core.constants.cli import (
working_account_template = typer.Option(
PERFORM_WORKING_ACCOUNT_LOAD, # we don't know if account_name_option is required until the profile is loaded
+ parser=account_name_parser,
help=stylized_help("The account to use.", is_working_account_default=True),
show_default=False,
)
working_account_list_template = typer.Option(
[PERFORM_WORKING_ACCOUNT_LOAD], # we don't know if account_name_option is required until the profile is loaded
+ parser=account_name_parser,
help=stylized_help("List of accounts to use.", is_working_account_default=True),
show_default=False,
)
@@ -49,6 +54,7 @@ account_name = modified_param(working_account_template, param_decls=("--account-
new_account_name = typer.Option(
...,
"--new-account-name",
+ parser=account_name_parser,
help="The name of the new account.",
)
@@ -67,6 +73,7 @@ to_account_name = modified_param(
to_account_name_required = typer.Option(
...,
"--to",
+ parser=account_name_parser,
help='The account to use as "to" argument.',
)
diff --git a/clive/__private/cli/common/parsers.py b/clive/__private/cli/common/parsers.py
index 08ad4a9cad5e3fe75bc84f6ec0f14f4779a79901..48235f8cdbe41820a72fcf2a420d124e30111350 100644
--- a/clive/__private/cli/common/parsers.py
+++ b/clive/__private/cli/common/parsers.py
@@ -143,3 +143,30 @@ def public_key(raw: str) -> PublicKey:
except PublicKeyInvalidFormatError as error:
raise CLIPublicKeyInvalidFormatError(raw) from error
return parsed
+
+
+def account_name(raw: str) -> str:
+ """
+ Parse and validate account name.
+
+ Args:
+ raw: The raw input representing the account name.
+
+ Raises:
+ typer.BadParameter: If the account name is invalid.
+
+ Returns:
+ The validated account name.
+ """
+ from clive.__private.core.constants.cli import PERFORM_WORKING_ACCOUNT_LOAD # noqa: PLC0415
+ from clive.__private.core.formatters.humanize import humanize_validation_result # noqa: PLC0415
+ from clive.__private.validators.account_name_validator import AccountNameValidator # noqa: PLC0415
+
+ # Allow placeholder value to pass through without validation
+ if raw == PERFORM_WORKING_ACCOUNT_LOAD:
+ return raw
+
+ status = AccountNameValidator().validate(raw)
+ if status.is_valid:
+ return raw
+ raise typer.BadParameter(humanize_validation_result(status))
diff --git a/clive/__private/cli/configure/known_account.py b/clive/__private/cli/configure/known_account.py
index 3825bbcb6b2113ead924a63cddcd10133b5fce7f..04b7435762d4fe20881bc66d904a9ba4b33f1eaa 100644
--- a/clive/__private/cli/configure/known_account.py
+++ b/clive/__private/cli/configure/known_account.py
@@ -6,11 +6,14 @@ from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common import argument_related_options, modified_param
from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleAccountNameValue
from clive.__private.cli.common.parameters.styling import stylized_help
+from clive.__private.cli.common.parsers import account_name
known_account = CliveTyper(name="known-account", help="Manage your known account(s).")
_account_name_add_argument = typer.Argument(
- None, help=stylized_help("The name of the known account to add.", required_as_arg_or_option=True)
+ None,
+ parser=account_name,
+ help=stylized_help("The name of the known account to add.", required_as_arg_or_option=True),
)
diff --git a/clive/__private/cli/configure/profile.py b/clive/__private/cli/configure/profile.py
index 692a8e543841e0fb5e0da70f0dd858a290d3725f..bf0652ae994b7e2bc8c942825b841b6798e05333 100644
--- a/clive/__private/cli/configure/profile.py
+++ b/clive/__private/cli/configure/profile.py
@@ -6,6 +6,7 @@ from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common.parameters import argument_related_options, modified_param
from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleProfileNameValue
from clive.__private.cli.common.parameters.styling import stylized_help
+from clive.__private.cli.common.parsers import account_name
profile = CliveTyper(name="profile", help="Manage your Clive profile(s).")
@@ -19,7 +20,7 @@ _profile_name_create_argument = typer.Argument(
async def create_profile(
profile_name: str | None = _profile_name_create_argument,
profile_name_option: str | None = argument_related_options.profile_name,
- working_account_name: str | None = typer.Option(None, help="The name of the working account."),
+ working_account_name: str | None = typer.Option(None, parser=account_name, help="The name of the working account."),
) -> None:
"""
Create a new profile. Password for new profile is provided by stdin.
diff --git a/clive/__private/cli/configure/tracked_account.py b/clive/__private/cli/configure/tracked_account.py
index 6c84c00c3f64c9b6211e8312f6f9305bb7a59a5d..0e90bc569806eb8ecb6c77ff96fc7e121f9e2a48 100644
--- a/clive/__private/cli/configure/tracked_account.py
+++ b/clive/__private/cli/configure/tracked_account.py
@@ -6,11 +6,14 @@ from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common import argument_related_options, modified_param
from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleAccountNameValue
from clive.__private.cli.common.parameters.styling import stylized_help
+from clive.__private.cli.common.parsers import account_name
tracked_account = CliveTyper(name="tracked-account", help="Manage your tracked account(s).")
_account_name_add_argument = typer.Argument(
- None, help=stylized_help("The name of the tracked account to add.", required_as_arg_or_option=True)
+ None,
+ parser=account_name,
+ help=stylized_help("The name of the tracked account to add.", required_as_arg_or_option=True),
)
diff --git a/clive/__private/cli/configure/working_account.py b/clive/__private/cli/configure/working_account.py
index fcc648acb751ff504330e2b54a46279863e01676..67054af505fb62b913f70b1e6ba16e7aacec71d5 100644
--- a/clive/__private/cli/configure/working_account.py
+++ b/clive/__private/cli/configure/working_account.py
@@ -6,11 +6,13 @@ from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common.parameters import argument_related_options
from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleAccountNameValue
from clive.__private.cli.common.parameters.styling import stylized_help
+from clive.__private.cli.common.parsers import account_name
working_account = CliveTyper(name="working-account", help="Manage your working account.")
_account_name_switch_argument = typer.Argument(
None,
+ parser=account_name,
help=stylized_help("The name of the account to switch to.", required_as_arg_or_option=True),
)
diff --git a/clive/__private/cli/generate/main.py b/clive/__private/cli/generate/main.py
index e0cfbfc78ead92f02f070235ed099f5969e69fa1..d033315317fa73366e65244a2b5c98bebd309ada 100644
--- a/clive/__private/cli/generate/main.py
+++ b/clive/__private/cli/generate/main.py
@@ -9,6 +9,7 @@ from clive.__private.cli.common.parameters.argument_related_options import (
)
from clive.__private.cli.common.parameters.ensure_single_value import EnsureSingleValue
from clive.__private.cli.common.parameters.styling import stylized_help
+from clive.__private.cli.common.parsers import account_name
from clive.__private.core.types import AuthorityLevel
generate = CliveTyper(name="generate", help="Commands for generating things (e.g. keys).")
@@ -16,6 +17,7 @@ generate = CliveTyper(name="generate", help="Commands for generating things (e.g
_account_name_argument = typer.Argument(
None,
+ parser=account_name,
help=stylized_help(
"Account for which key is derived, this is not working account of profile.", required_as_arg_or_option=True
),
diff --git a/clive/__private/cli/process/custom_operations/custom_json.py b/clive/__private/cli/process/custom_operations/custom_json.py
index 16e4fcad09d9ba546424ef01116481979538423d..317f597d86ad82dbdec5b7cef058c8bb8a436c19 100644
--- a/clive/__private/cli/process/custom_operations/custom_json.py
+++ b/clive/__private/cli/process/custom_operations/custom_json.py
@@ -4,6 +4,7 @@ import typer
from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common import modified_param, options
+from clive.__private.cli.common.parsers import account_name
custom_json = CliveTyper(name="custom-json", help="Send raw custom json operation.")
@@ -18,6 +19,7 @@ async def process_custom_json( # noqa: PLR0913
),
authorize_by_active: list[str] = typer.Option(
[],
+ parser=account_name,
help="Active authorities. Option can be added multiple times. If neither authorize nor authorize-by-active is"
" used, then posting authority of working account is used for authorization.",
),
diff --git a/clive/__private/cli/process/hive_power/delegations.py b/clive/__private/cli/process/hive_power/delegations.py
index e32b235f29cfec7033a907c1097330eee6ad28a8..10ff290be5d1187d6aade0e6222a10442b685935 100644
--- a/clive/__private/cli/process/hive_power/delegations.py
+++ b/clive/__private/cli/process/hive_power/delegations.py
@@ -6,6 +6,7 @@ import typer
from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common import options
+from clive.__private.cli.common.parsers import account_name
if TYPE_CHECKING:
from clive.__private.models.asset import Asset
@@ -15,6 +16,7 @@ delegations = CliveTyper(name="delegations", help="Set or remove vesting delegat
_delegatee_account_name = typer.Option(
...,
"--delegatee",
+ parser=account_name,
help='The account to use as "delegatee" argument.',
)
diff --git a/clive/__private/cli/process/main.py b/clive/__private/cli/process/main.py
index 4a54635bca01060cdc9572b71849db7d50baaacb..9b8cb089a9182526ecc5dda682544cddb42f7054 100644
--- a/clive/__private/cli/process/main.py
+++ b/clive/__private/cli/process/main.py
@@ -13,7 +13,7 @@ from clive.__private.cli.common.parameters.ensure_single_value import (
EnsureSingleValue,
)
from clive.__private.cli.common.parameters.styling import stylized_help
-from clive.__private.cli.common.parsers import decimal_percent, hbd_asset, hive_asset, public_key
+from clive.__private.cli.common.parsers import account_name, decimal_percent, hbd_asset, hive_asset, public_key
from clive.__private.cli.process.claim import claim
from clive.__private.cli.process.custom_operations.custom_json import custom_json
from clive.__private.cli.process.hive_power.delegations import delegations
@@ -56,7 +56,7 @@ process.add_typer(voting_rights)
@process.command(name="transfer")
async def transfer( # noqa: PLR0913
from_account: str = options.from_account_name,
- to: str = typer.Option(..., help="The account to transfer to."),
+ to: str = typer.Option(..., parser=account_name, help="The account to transfer to."),
amount: str = options.liquid_amount,
memo: str = options.memo_text,
sign_with: str | None = options.sign_with,
@@ -145,6 +145,7 @@ async def process_update_memo_key( # noqa: PLR0913
_new_account_name_argument = typer.Argument(
None,
+ parser=account_name,
help=stylized_help("The name of the new account.", required_as_arg_or_option=True),
)
@@ -303,6 +304,7 @@ async def process_change_recovery_account( # noqa: PLR0913
),
new_recovery_account: str = typer.Option(
...,
+ parser=account_name,
help="This is your trusted account. In case of compromise, only this account can create a recovery request.",
),
sign_with: str | None = options.sign_with,
diff --git a/clive/__private/cli/process/proxy.py b/clive/__private/cli/process/proxy.py
index ffa184b6c1bf2b1a55528c8d8a7d3df828bfe334..c6129106c42a1b92807a40c43c1e9c13c56aa312 100644
--- a/clive/__private/cli/process/proxy.py
+++ b/clive/__private/cli/process/proxy.py
@@ -4,6 +4,7 @@ import typer
from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common import options
+from clive.__private.cli.common.parsers import account_name
proxy = CliveTyper(name="proxy", help="Set, change or remove a proxy.")
@@ -11,7 +12,7 @@ proxy = CliveTyper(name="proxy", help="Set, change or remove a proxy.")
@proxy.command(name="set")
async def process_proxy_set( # noqa: PLR0913
account_name: str = options.account_name,
- proxy: str = typer.Option(..., help="Name of new proxy account."),
+ proxy: str = typer.Option(..., parser=account_name, help="Name of new proxy account."),
sign_with: str | None = options.sign_with,
autosign: bool | None = options.autosign, # noqa: FBT001
broadcast: bool | None = options.broadcast, # noqa: FBT001
diff --git a/clive/__private/cli/process/update_authority.py b/clive/__private/cli/process/update_authority.py
index 4c107c9557b92431b5e832d5507a446beeb911d7..03c333371aa4aaadbfc1a23277569f5d24d52213 100644
--- a/clive/__private/cli/process/update_authority.py
+++ b/clive/__private/cli/process/update_authority.py
@@ -9,6 +9,7 @@ from click import Context, pass_context
from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common import options
from clive.__private.cli.common.parameters import modified_param
+from clive.__private.cli.common.parsers import account_name
from clive.__private.core._async import asyncio_run
if TYPE_CHECKING:
@@ -20,6 +21,7 @@ if TYPE_CHECKING:
_authority_account_name = typer.Option(
...,
"--account",
+ parser=account_name,
help="The account to add/remove/modify (account must exist).",
)
_authority_key = typer.Option(
diff --git a/clive/__private/cli/process/vote_witness.py b/clive/__private/cli/process/vote_witness.py
index fa5fc9dce2f4840594a3819cde1a92f8704e522a..1c29ca4f76ef6ddbeab21fc6c766dc6fb43e22da 100644
--- a/clive/__private/cli/process/vote_witness.py
+++ b/clive/__private/cli/process/vote_witness.py
@@ -4,6 +4,7 @@ import typer
from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.common import options
+from clive.__private.cli.common.parsers import account_name
vote_witness = CliveTyper(name="vote-witness", help="Vote/unvote for a witness.")
@@ -11,7 +12,7 @@ vote_witness = CliveTyper(name="vote-witness", help="Vote/unvote for a witness."
@vote_witness.command(name="add")
async def process_vote_witness_add( # noqa: PLR0913
account_name: str = options.account_name,
- witness_name: str = typer.Option(..., help="Witness name to vote."),
+ witness_name: str = typer.Option(..., parser=account_name, help="Witness name to vote."),
sign_with: str | None = options.sign_with,
autosign: bool | None = options.autosign, # noqa: FBT001
broadcast: bool | None = options.broadcast, # noqa: FBT001
@@ -34,7 +35,7 @@ async def process_vote_witness_add( # noqa: PLR0913
@vote_witness.command(name="remove")
async def process_vote_witness_remove( # noqa: PLR0913
account_name: str = options.account_name,
- witness_name: str = typer.Option(..., help="Witness name to unvote."),
+ witness_name: str = typer.Option(..., parser=account_name, help="Witness name to unvote."),
sign_with: str | None = options.sign_with,
autosign: bool | None = options.autosign, # noqa: FBT001
broadcast: bool | None = options.broadcast, # noqa: FBT001
diff --git a/clive/__private/cli/show/main.py b/clive/__private/cli/show/main.py
index 9e4fc1c8e885415105fbe6dc36ab8822a7075241..9808d8ab5e4b45d0a398a7e412c6a669edcefa71 100644
--- a/clive/__private/cli/show/main.py
+++ b/clive/__private/cli/show/main.py
@@ -10,6 +10,7 @@ from clive.__private.cli.common.parameters.ensure_single_value import (
)
from clive.__private.cli.common.parameters.modified_param import modified_param
from clive.__private.cli.common.parameters.styling import stylized_help
+from clive.__private.cli.common.parsers import account_name
from clive.__private.cli.show.pending import pending
from clive.__private.core.constants.data_retrieval import (
ORDER_DIRECTION_DEFAULT,
@@ -130,7 +131,9 @@ async def show_witnesses(
).run()
-_witness_name_argument = typer.Argument(None, help=stylized_help("Witness name.", required_as_arg_or_option=True))
+_witness_name_argument = typer.Argument(
+ None, parser=account_name, help=stylized_help("Witness name.", required_as_arg_or_option=True)
+)
@show.command(name="witness")
diff --git a/clive/__private/settings/_settings.py b/clive/__private/settings/_settings.py
index 553652c6a817c3213f434d453514e06339178274..63a4239ce2781fb657e26548d6eb6d5194e9f49b 100644
--- a/clive/__private/settings/_settings.py
+++ b/clive/__private/settings/_settings.py
@@ -50,7 +50,7 @@ class Settings:
The environment variable
```bash
- CLIVE__FIRST_GROUP__NESTED_GROUP__SOME_KEY=124
+ CLIVE_FIRST_GROUP__NESTED_GROUP__SOME_KEY=124
```
would override the setting in the file.
diff --git a/tests/unit/cli/__init__.py b/tests/unit/cli/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/unit/cli/test_parsers.py b/tests/unit/cli/test_parsers.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0e71bb7e845b58017e84d89823e58b4a57b6de1
--- /dev/null
+++ b/tests/unit/cli/test_parsers.py
@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+import pytest
+import typer
+
+from clive.__private.cli.common.parsers import account_name
+from clive.__private.core.constants.cli import PERFORM_WORKING_ACCOUNT_LOAD
+
+
+def test_account_name_valid_simple() -> None:
+ # ARRANGE
+ raw = "alice"
+
+ # ACT
+ result = account_name(raw)
+
+ # ASSERT
+ assert result == "alice"
+
+
+def test_account_name_valid_with_numbers() -> None:
+ # ARRANGE
+ raw = "alice123"
+
+ # ACT
+ result = account_name(raw)
+
+ # ASSERT
+ assert result == "alice123"
+
+
+def test_account_name_valid_with_dots() -> None:
+ # ARRANGE
+ raw = "alice.bob"
+
+ # ACT
+ result = account_name(raw)
+
+ # ASSERT
+ assert result == "alice.bob"
+
+
+def test_account_name_valid_with_hyphens() -> None:
+ # ARRANGE
+ raw = "alice-bob"
+
+ # ACT
+ result = account_name(raw)
+
+ # ASSERT
+ assert result == "alice-bob"
+
+
+def test_account_name_allows_perform_working_account_load_placeholder() -> None:
+ # ARRANGE
+ raw = PERFORM_WORKING_ACCOUNT_LOAD
+
+ # ACT
+ result = account_name(raw)
+
+ # ASSERT
+ assert result == PERFORM_WORKING_ACCOUNT_LOAD
+
+
+def test_account_name_invalid_too_short() -> None:
+ # ARRANGE
+ raw = "ab"
+
+ # ACT & ASSERT
+ with pytest.raises(typer.BadParameter):
+ account_name(raw)
+
+
+def test_account_name_invalid_too_long() -> None:
+ # ARRANGE
+ raw = "a" * 17
+
+ # ACT & ASSERT
+ with pytest.raises(typer.BadParameter):
+ account_name(raw)
+
+
+def test_account_name_invalid_uppercase() -> None:
+ # ARRANGE
+ raw = "Alice"
+
+ # ACT & ASSERT
+ with pytest.raises(typer.BadParameter):
+ account_name(raw)
+
+
+def test_account_name_invalid_starting_with_number() -> None:
+ # ARRANGE
+ raw = "1alice"
+
+ # ACT & ASSERT
+ with pytest.raises(typer.BadParameter):
+ account_name(raw)
+
+
+def test_account_name_invalid_special_chars() -> None:
+ # ARRANGE
+ raw = "alice@bob"
+
+ # ACT & ASSERT
+ with pytest.raises(typer.BadParameter):
+ account_name(raw)