diff --git a/fireblocks_cli/auth/__init__.py b/fireblocks_cli/auth/__init__.py new file mode 100644 index 0000000..44a9b22 --- /dev/null +++ b/fireblocks_cli/auth/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON diff --git a/fireblocks_cli/auth/base.py b/fireblocks_cli/auth/base.py new file mode 100644 index 0000000..3a8caec --- /dev/null +++ b/fireblocks_cli/auth/base.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +from abc import ABC, abstractmethod +from typing import Dict + + +class BaseAuthProvider(ABC): + @abstractmethod + def get_jwt(self) -> str: + """Get JWT token""" + pass + + @abstractmethod + def get_api_id(self) -> str: + """Return the API ID""" + pass + + @abstractmethod + def get_secret_key(self) -> str: + """ + Return secret info api_secret_key value (raw string ) + """ + pass diff --git a/fireblocks_cli/auth/factory.py b/fireblocks_cli/auth/factory.py new file mode 100644 index 0000000..9032a29 --- /dev/null +++ b/fireblocks_cli/auth/factory.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +import toml +from fireblocks_cli.auth.file_provider import FileAuthProvider +from fireblocks_cli.types.profile_config import ApiProfile, SecretKeyConfig +from fireblocks_cli.utils.profile import load_profile + + +def get_auth_provider(profile_name: str = "default"): + config = load_profile(profile_name) + provider_type = config["api_secret_key"]["type"] + + if provider_type == "file": + profile = ApiProfile( + profile_name=profile_name, + api_id=config["api_id"], + api_secret_key=SecretKeyConfig(**config["api_secret_key"]), + ) + return FileAuthProvider(profile) + elif provider_type == "vault": + raise NotImplementedError("Vault provider is not yet implemented") + else: + raise ValueError(f"Unknown provider type: {provider_type}") diff --git a/fireblocks_cli/auth/file_provider.py b/fireblocks_cli/auth/file_provider.py new file mode 100644 index 0000000..341011f --- /dev/null +++ b/fireblocks_cli/auth/file_provider.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +from fireblocks_cli.auth.base import BaseAuthProvider +from fireblocks_cli.types.profile_config import ApiProfile, SecretKeyConfig +from pathlib import Path + + +class FileAuthProvider(BaseAuthProvider): + def __init__(self, profile: ApiProfile): + self.profile = profile + + def get_api_id(self) -> str: + return self.profile.api_id + + def get_secret_key(self) -> str: + secret_path = Path(self.profile.api_secret_key.value).expanduser() + with open(secret_path, "r") as f: + secret_key = f.read() + return secret_key + + def get_api_secret_info(self) -> dict[str, str]: + return { + "profile": self.profile.profile_name, + "api_id": self.profile.api_id, + "api_secret_key": self.get_secret_key(), + } + + def get_jwt(self) -> str: + print(f"Using profile: {self.profile.profile_name}") + + import time, jwt + + payload = { + "uri": "/v1/*", + "nonce": int(time.time() * 1000), + "iat": int(time.time()), + "exp": int(time.time()) + 300, + "sub": self.get_api_id(), + } + return jwt.encode(payload, self.get_api_id(), algorithm="HS256") diff --git a/fireblocks_cli/auth/vault_provider.py b/fireblocks_cli/auth/vault_provider.py new file mode 100644 index 0000000..187d529 --- /dev/null +++ b/fireblocks_cli/auth/vault_provider.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +# fireblocks_cli/auth/vault_provider.py + +from fireblocks_cli.auth.base import BaseAuthProvider + + +class VaultAuthProvider(BaseAuthProvider): + def __init__(self, vault_path: str): + # Planning: HashiCorp Vault, AWS Secrets Manager + self.vault_path = vault_path + + def get_api_key(self) -> str: + raise NotImplementedError + + def get_secret_key(self) -> str: + raise NotImplementedError + + def get_jwt(self) -> str: + raise NotImplementedError diff --git a/fireblocks_cli/commands/configure.py b/fireblocks_cli/commands/configure.py index ca49ce0..80c1262 100644 --- a/fireblocks_cli/commands/configure.py +++ b/fireblocks_cli/commands/configure.py @@ -7,12 +7,14 @@ import typer from pathlib import Path from fireblocks_cli.crypto import generate_key_and_csr -from fireblocks_cli.config import ( +from fireblocks_cli.utils.profile import ( get_config_dir, get_config_file, get_api_key_dir, get_credentials_file, DEFAULT_CONFIG, + get_profiles, + ProfileLoadError, ) from fireblocks_cli.utils.toml import save_toml from tomlkit import document, table, inline_table, dumps @@ -80,7 +82,7 @@ def validate(): """ Validate the format of config.toml and credentials files. """ - from fireblocks_cli.config import get_config_file, get_credentials_file + from fireblocks_cli.utils.profile import get_config_file, get_credentials_file import toml from pathlib import Path @@ -148,7 +150,7 @@ def edit(): """ import os import subprocess - from fireblocks_cli.config import get_config_file + from fireblocks_cli.utils.profile import get_config_file config_path = get_config_file() @@ -194,38 +196,21 @@ def list_profiles(): List available profiles from config.toml and credentials (if present). Profiles in credentials override those in config.toml. """ - import toml - from fireblocks_cli.config import get_config_file, get_credentials_file - - config_path = get_config_file() - credentials_path = get_credentials_file() - combined_data = {} - - # Step 1: load config.toml - if config_path.exists(): - try: - config_data = toml.load(config_path) - combined_data.update(config_data) - except Exception as e: - typer.secho(f"❌ Failed to parse config.toml: {e}", fg=typer.colors.RED) - raise typer.Exit(code=1) + profiles = {} - # Step 2: override with credentials if it exists - if credentials_path.exists(): - try: - credentials_data = toml.load(credentials_path) - combined_data.update(credentials_data) # override same keys - except Exception as e: - typer.secho(f" Failed to parse credentials: {e}", fg=typer.colors.RED) - raise typer.Exit(code=1) + try: + profiles = get_profiles() + except ProfileLoadError as e: + typer.secho(f"❌ {e}", fg=typer.colors.RED) + raise typer.Exit(code=1) - if not combined_data: + if not profiles: typer.echo("⚠️ No profiles found in config.toml or credentials.") return typer.echo("📜 Available Profiles:\n") - for name, values in combined_data.items(): + for name, values in profiles.items(): api_id = values.get("api_id", "") secret_type = values.get("api_secret_key", {}).get("type", "") typer.echo( diff --git a/fireblocks_cli/commands/profile_debug.py b/fireblocks_cli/commands/profile_debug.py new file mode 100644 index 0000000..5faec2e --- /dev/null +++ b/fireblocks_cli/commands/profile_debug.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +import typer +from fireblocks_cli.utils.profile import load_profile +from fireblocks_sdk import FireblocksSDK +from fireblocks_cli.auth.file_provider import FileAuthProvider +from fireblocks_cli.types.profile_config import ApiProfile, SecretKeyConfig + + +app = typer.Typer() + + +@app.command("debug") +def profile_debug(profile_name: str = typer.Option("default", "--profile", "-p")): + """Check if the selected profile works with Fireblocks SDK.""" + + raw_profile = load_profile(profile_name) + profile_obj = ApiProfile( + profile_name=profile_name, + api_id=raw_profile["api_id"], + api_secret_key=SecretKeyConfig(**raw_profile["api_secret_key"]), + ) + provider = FileAuthProvider(profile_obj) + + api_id = provider.get_api_id() + secret_key = provider.get_secret_key() + + fireblocks = FireblocksSDK(secret_key, api_id) + + try: + accounts = fireblocks.get_vault_account(vault_account_id=1) + + typer.secho( + f"✅ Successfully accessed Fireblocks API with profile '{profile_name}'", + fg=typer.colors.GREEN, + ) + typer.echo(f"Vault info: {accounts}") + except Exception as e: + typer.secho(f"❌ Error accessing Fireblocks API: {e}", fg=typer.colors.RED) diff --git a/fireblocks_cli/config.py b/fireblocks_cli/config.py index e6b916d..44a9b22 100644 --- a/fireblocks_cli/config.py +++ b/fireblocks_cli/config.py @@ -1,33 +1,4 @@ # SPDX-FileCopyrightText: 2025 Ethersecurity Inc. # # SPDX-License-Identifier: MPL-2.0 - # Author: Shohei KAMON -from pathlib import Path - - -def get_config_dir() -> Path: - return Path.home() / ".config" / "fireblocks-cli" - - -def get_config_file() -> Path: - return get_config_dir() / "config.toml" - - -def get_api_key_dir() -> Path: - return get_config_dir() / "keys" - - -def get_credentials_file() -> Path: - return get_config_dir() / "credentials" - - -DEFAULT_CONFIG = { - "default": { - "api_id": "get-api_id-from-fireblocks-dashboard", - "api_secret_key": { - "type": "file", - "value": "~/.config/fireblocks-cli/keys/abcd.key", - }, - } -} diff --git a/fireblocks_cli/crypto.py b/fireblocks_cli/crypto.py index 3fb91fb..470249f 100644 --- a/fireblocks_cli/crypto.py +++ b/fireblocks_cli/crypto.py @@ -9,7 +9,7 @@ from pathlib import Path import subprocess import typer -from fireblocks_cli.config import ( +from fireblocks_cli.utils.profile import ( get_api_key_dir, ) diff --git a/fireblocks_cli/main.py b/fireblocks_cli/main.py index 4dbfe25..eb4bf02 100644 --- a/fireblocks_cli/main.py +++ b/fireblocks_cli/main.py @@ -9,9 +9,12 @@ from fireblocks_cli.commands.configure import configure_app import typer from fireblocks_cli import __version__ +from fireblocks_cli.commands import profile_debug + +app = typer.Typer(help="Unofficial CLI for Fireblocks") -app = typer.Typer() app.add_typer(configure_app, name="configure") +app.add_typer(profile_debug.app, name="profile") @app.callback() @@ -27,7 +30,13 @@ def main( if v else None ), - ) + ), + profile: str = typer.Option( + "default", + "--profile", + "-p", + help="Specify profile to use.", + ), ): pass @@ -42,5 +51,7 @@ def version(): typer.echo(f"fireblocks-cli version {__version__}") +app.add_typer(profile_debug.app, name="profile") + if __name__ == "__main__": app() diff --git a/fireblocks_cli/types/__init__.py b/fireblocks_cli/types/__init__.py new file mode 100644 index 0000000..44a9b22 --- /dev/null +++ b/fireblocks_cli/types/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON diff --git a/fireblocks_cli/types/profile_config.py b/fireblocks_cli/types/profile_config.py new file mode 100644 index 0000000..c2ddc44 --- /dev/null +++ b/fireblocks_cli/types/profile_config.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +from dataclasses import dataclass +from typing import Literal, Dict + + +@dataclass +class SecretKeyConfig: + type: Literal["file", "env", "vault"] + value: str + + +@dataclass +class ApiProfile: + profile_name: str + api_id: str + api_secret_key: SecretKeyConfig diff --git a/fireblocks_cli/utils/profile.py b/fireblocks_cli/utils/profile.py new file mode 100644 index 0000000..bf378a9 --- /dev/null +++ b/fireblocks_cli/utils/profile.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +from typing import List, Dict +import toml +from fireblocks_cli.types.profile_config import ApiProfile, SecretKeyConfig + + +DEFAULT_PROFILE = "default" + + +from pathlib import Path + +DEFAULT_PROFILE = "default" +DEFAULT_CONFIG = { + "default": { + "api_id": "get-api_id-from-fireblocks-dashboard", + "api_secret_key": { + "type": "file", + "value": "~/.config/fireblocks-cli/keys/abcd.key", + }, + } +} + + +def get_config_dir() -> Path: + return Path.home() / ".config" / "fireblocks-cli" + + +def get_config_file() -> Path: + return get_config_dir() / "config.toml" + + +def get_api_key_dir() -> Path: + return get_config_dir() / "keys" + + +def get_credentials_file() -> Path: + return get_config_dir() / "credentials" + + +class ProfileLoadError(Exception): + pass + + +def get_profiles() -> dict: + """ + Load and merge profile configurations from config.toml and credentials. + + Returns: + dict: A dictionary where keys are profile names and values are their respective configurations. + + Raises: + ProfileLoadError: If either config.toml or credentials.toml fails to parse. + + Notes: + - config.toml is loaded first. + - credentials (if present) will override any conflicting keys from config.toml. + """ + config_path = get_config_file() + credentials_path = get_credentials_file() + combined_data = {} + + # Step 1: load config.toml + if config_path.exists(): + try: + config_data = toml.load(config_path) + combined_data.update(config_data) + except Exception as e: + raise ProfileLoadError(f"Failed to parse config.toml: {e}") + + # Step 2: override with credentials if it exists + if credentials_path.exists(): + try: + credentials_data = toml.load(credentials_path) + combined_data.update(credentials_data) # override same keys + except Exception as e: + raise ProfileLoadError(f"Failed to parse credentials: {e}") + return combined_data + + +def load_profile(profile: str = DEFAULT_PROFILE) -> dict: + profiles = get_profiles() + if profile not in profiles: + raise ValueError(f"No such profile :'{profile}' is defined.") + return profiles[profile] diff --git a/poetry.lock b/poetry.lock index c036bdc..ad341a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -72,7 +72,7 @@ version = "25.1.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, @@ -159,7 +159,7 @@ version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, @@ -171,8 +171,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" +groups = ["main", "dev"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -242,6 +241,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {main = "platform_python_implementation != \"PyPy\"", dev = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" @@ -276,7 +276,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -406,8 +406,7 @@ version = "44.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +groups = ["main", "dev"] files = [ {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, @@ -445,6 +444,7 @@ files = [ {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, ] +markers = {dev = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\""} [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} @@ -500,6 +500,23 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3) testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] +[[package]] +name = "fireblocks-sdk" +version = "2.16.1" +description = "Fireblocks python SDK" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "fireblocks_sdk-2.16.1-py3-none-any.whl", hash = "sha256:7015040d2063874522ccff7682a292a94664d1f84c758c19143dfa88aba0ba22"}, + {file = "fireblocks_sdk-2.16.1.tar.gz", hash = "sha256:d4f336483f2125d8f3c1e5c1601186b2c9462f160997f5af1b7d1c061515992c"}, +] + +[package.dependencies] +cryptography = ">=2.7" +PyJWT = ">=2.8.0" +requests = ">=2.22.0" + [[package]] name = "id" version = "1.5.0" @@ -541,7 +558,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -932,7 +949,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -990,7 +1007,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1002,7 +1019,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1027,7 +1044,7 @@ version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, @@ -1079,12 +1096,12 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["dev"] -markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" +groups = ["main", "dev"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "platform_python_implementation != \"PyPy\"", dev = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\""} [[package]] name = "pygments" @@ -1152,6 +1169,24 @@ files = [ packaging = ">=22.0" setuptools = ">=42.0.0" +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pyproject-hooks" version = "1.2.0" @@ -1305,7 +1340,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1540,7 +1575,7 @@ version = "2.4.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, @@ -1597,4 +1632,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.14" -content-hash = "4d64f5580254d70c4089155b6da6a8af45b8a03d85f1dad177cb9027d67d9e0c" +content-hash = "ac063cd02c5c38e344c3d13101dc4b134da75b4f7a8df53dcbc54b73e4fba671" diff --git a/poetry.lock.license b/poetry.lock.license index f3fafd4..95faf7c 100644 --- a/poetry.lock.license +++ b/poetry.lock.license @@ -2,4 +2,3 @@ SPDX-FileCopyrightText: 2025 2025 Ethersecurity Inc. SPDX-FileCopyrightText: 2025 Ethersecurity Inc. SPDX-License-Identifier: MPL-2.0 -# Author: Shohei KAMON diff --git a/pyproject.toml b/pyproject.toml index b07e9f6..3c74232 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,9 @@ requires-python = ">=3.11,<3.14" dependencies = [ "typer[all]>0.15.0", "toml>0.10.0", - "tomlkit (>=0.13.2,<0.14.0)" + "tomlkit (>=0.13.2,<0.14.0)", + "fireblocks-sdk (>=2.16.1,<3.0.0)", + "black (>=25.1.0,<26.0.0)" ] [project.scripts] diff --git a/scripts/get_changed_files.sh b/scripts/get_changed_files.sh index 19050ba..c46aeef 100644 --- a/scripts/get_changed_files.sh +++ b/scripts/get_changed_files.sh @@ -7,4 +7,4 @@ # Author: Shohei KAMON # exclude removed file: grep -v "^D" -git status --porcelain | grep -v "^??" | grep -v "^D " | cut -c4- | grep -v "\.txt$" +git status --porcelain | grep -v "^??" | grep -v "^D " | cut -c4- | grep -v "\.txt$" | grep -v "poetry.lock.license" diff --git a/tests/auth/test_auth_factory.py b/tests/auth/test_auth_factory.py new file mode 100644 index 0000000..7a5f28a --- /dev/null +++ b/tests/auth/test_auth_factory.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +import pytest +from fireblocks_cli.auth.factory import get_auth_provider, FileAuthProvider +from fireblocks_cli.types.profile_config import ApiProfile, SecretKeyConfig + + +def test_get_auth_provider_returns_file_provider(monkeypatch, tmp_path): + # 仮の秘密鍵ファイルを作成 + secret_file = tmp_path / "mock_secret.key" + secret_file.write_text("mock-super-secret") + + # monkeypatch: load_profile() をモックして返す値を差し替える + monkeypatch.setattr( + "fireblocks_cli.auth.factory.load_profile", + lambda profile_name: { + "api_id": "mock-api-id", + "api_secret_key": { + "type": "file", + "value": str(secret_file), + }, + }, + ) + + # 呼び出し + provider = get_auth_provider("mock") + + # 検証 + assert isinstance(provider, FileAuthProvider) + assert provider.get_api_id() == "mock-api-id" + assert provider.get_secret_key() == "mock-super-secret" diff --git a/tests/auth/test_file_provider.py b/tests/auth/test_file_provider.py new file mode 100644 index 0000000..2465dbd --- /dev/null +++ b/tests/auth/test_file_provider.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +import pytest +from fireblocks_cli.auth.file_provider import FileAuthProvider +from fireblocks_cli.types.profile_config import ApiProfile, SecretKeyConfig + + +def test_get_api_id(): + profile = ApiProfile( + profile_name="test", + api_id="test-api-id", + api_secret_key=SecretKeyConfig( + type="file", value="/dummy/path" + ), # 使わないのでダミーでOK + ) + provider = FileAuthProvider(profile) + assert provider.get_api_id() == "test-api-id" + + +def test_get_secret_key(tmp_path): + # 仮の秘密鍵ファイルを作成 + secret_file = tmp_path / "test_secret.key" + secret_file.write_text("my-secret-key\n") + + profile = ApiProfile( + profile_name="test", + api_id="dummy", + api_secret_key=SecretKeyConfig(type="file", value=str(secret_file)), + ) + provider = FileAuthProvider(profile) + secret = provider.get_secret_key() + + assert secret.strip() == "my-secret-key" diff --git a/tests/test_configure_edit.py b/tests/test_configure_edit.py index 6d4d9ad..b7767e6 100644 --- a/tests/test_configure_edit.py +++ b/tests/test_configure_edit.py @@ -7,7 +7,7 @@ import pytest from typer.testing import CliRunner from fireblocks_cli.main import app -from fireblocks_cli.config import get_config_file +from fireblocks_cli.utils.profile import get_config_file from pathlib import Path runner = CliRunner()