Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions fireblocks_cli/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0
# Author: Shohei KAMON <cameong@stir.network>
26 changes: 26 additions & 0 deletions fireblocks_cli/auth/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0
# Author: Shohei KAMON <cameong@stir.network>

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
26 changes: 26 additions & 0 deletions fireblocks_cli/auth/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0
# Author: Shohei KAMON <cameong@stir.network>

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}")
43 changes: 43 additions & 0 deletions fireblocks_cli/auth/file_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0
# Author: Shohei KAMON <cameong@stir.network>

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")
23 changes: 23 additions & 0 deletions fireblocks_cli/auth/vault_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0
# Author: Shohei KAMON <cameong@stir.network>

# 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
41 changes: 13 additions & 28 deletions fireblocks_cli/commands/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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", "<missing>")
secret_type = values.get("api_secret_key", {}).get("type", "<unknown>")
typer.echo(
Expand Down
42 changes: 42 additions & 0 deletions fireblocks_cli/commands/profile_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0
# Author: Shohei KAMON <cameong@stir.network>

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)
29 changes: 0 additions & 29 deletions fireblocks_cli/config.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,4 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0

# Author: Shohei KAMON <cameong@stir.network>
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",
},
}
}
2 changes: 1 addition & 1 deletion fireblocks_cli/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
15 changes: 13 additions & 2 deletions fireblocks_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -27,7 +30,13 @@ def main(
if v
else None
),
)
),
profile: str = typer.Option(
"default",
"--profile",
"-p",
help="Specify profile to use.",
),
):
pass

Expand All @@ -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()
4 changes: 4 additions & 0 deletions fireblocks_cli/types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0
# Author: Shohei KAMON <cameong@stir.network>
20 changes: 20 additions & 0 deletions fireblocks_cli/types/profile_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0
# Author: Shohei KAMON <cameong@stir.network>

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
Loading