diff --git a/src/aleph_client/__main__.py b/src/aleph_client/__main__.py index b46ff3ce..26c6e89e 100644 --- a/src/aleph_client/__main__.py +++ b/src/aleph_client/__main__.py @@ -2,6 +2,7 @@ Aleph Client command-line interface. """ +import os from typing import Optional from pathlib import Path @@ -10,7 +11,7 @@ from aleph_client.types import AccountFromPrivateKey from aleph_client.account import _load_account from aleph_client.conf import settings -from .commands import files, message, program, help_strings, aggregate +from .commands import files, message, program, help_strings, aggregate, account app = typer.Typer() @@ -30,6 +31,9 @@ aggregate.app, name="aggregate", help="Manage aggregate messages on Aleph.im" ) +app.add_typer( + account.app, name="account", help="Manage account" +) @app.command() def whoami( @@ -44,6 +48,11 @@ def whoami( Display your public address. """ + if private_key is not None: + private_key_file = None + elif private_key_file and not os.path.exists(private_key_file): + exit(0) + account: AccountFromPrivateKey = _load_account(private_key, private_key_file) typer.echo(account.get_address()) diff --git a/src/aleph_client/chains/common.py b/src/aleph_client/chains/common.py index c4092060..86008264 100644 --- a/src/aleph_client/chains/common.py +++ b/src/aleph_client/chains/common.py @@ -77,8 +77,10 @@ def get_fallback_private_key() -> bytes: private_key = prvfile.read() except OSError: private_key = generate_key() + os.makedirs(os.path.dirname(settings.PRIVATE_KEY_FILE), exist_ok=True) with open(settings.PRIVATE_KEY_FILE, "wb") as prvfile: prvfile.write(private_key) + os.symlink(settings.PRIVATE_KEY_FILE, os.path.join(os.path.dirname(settings.PRIVATE_KEY_FILE), "default.key")) return private_key @@ -86,5 +88,6 @@ def get_fallback_private_key() -> bytes: def delete_private_key_file(): try: os.remove(settings.PRIVATE_KEY_FILE) + os.unlink(os.path.join(os.path.dirname(settings.PRIVATE_KEY_FILE), "default.key")) except FileNotFoundError: pass diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py new file mode 100644 index 00000000..79816f62 --- /dev/null +++ b/src/aleph_client/commands/account.py @@ -0,0 +1,50 @@ +import os +import typer +import logging +from typing import Optional +from aleph_client.types import AccountFromPrivateKey +from aleph_client.chains.common import generate_key +from aleph_client.account import _load_account +from aleph_client.conf import settings + +from aleph_client.commands import help_strings +from aleph_client.commands.utils import setup_logging + + +logger = logging.getLogger(__name__) +app = typer.Typer() + + +@app.command() +def create( + from_private_key: Optional[str] = typer.Option( + None, help=help_strings.PRIVATE_KEY + ), + debug: bool = False, +): + """Create or import a private key.""" + + setup_logging(debug) + + typer.echo("Generating private key file.") + private_key_file = typer.prompt("Enter file in which to save the key", settings.PRIVATE_KEY_FILE) + + if os.path.exists(private_key_file): + typer.echo(f"Error: key already exists: '{private_key_file}'") + exit(1) + + private_key = None + if from_private_key is not None: + account: AccountFromPrivateKey = _load_account(private_key_str=from_private_key) + private_key = from_private_key.encode() + else: + private_key = generate_key() + + if private_key is None: + typer.echo("An unexpected error occurred!") + exit(1) + + os.makedirs(os.path.dirname(private_key_file), exist_ok=True) + with open(private_key_file, "wb") as prvfile: + prvfile.write(private_key) + typer.echo(f"Private key created => {private_key_file}") diff --git a/src/aleph_client/conf.py b/src/aleph_client/conf.py index d3f5f12c..d7390c60 100644 --- a/src/aleph_client/conf.py +++ b/src/aleph_client/conf.py @@ -1,18 +1,23 @@ +import pathlib from pathlib import Path from shutil import which from typing import Optional from pydantic import BaseSettings, Field +import os +import sys class Settings(BaseSettings): + CONFIG_HOME: Optional[str] = None + # In case the user does not want to bother with handling private keys himself, # do an ugly and insecure write and read from disk to this file. PRIVATE_KEY_FILE: Path = Field( - default=Path("device.key"), + default=Path("ethereum.key"), description="Path to the private key used to sign messages", ) - + PRIVATE_KEY_STRING: Optional[str] = None API_HOST: str = "https://api2.aleph.im" MAX_INLINE_SIZE: int = 50000 @@ -42,3 +47,17 @@ class Config: # Settings singleton settings = Settings() + +if settings.CONFIG_HOME is None: + xdg_data_home = os.environ.get("XDG_DATA_HOME") + if xdg_data_home is not None: + os.environ["ALEPH_CONFIG_HOME"] = str(Path(xdg_data_home, ".aleph-im")) + else: + home = os.path.expanduser("~") + os.environ["ALEPH_CONFIG_HOME"] = str(Path(home, ".aleph-im")) + + settings = Settings() + +assert settings.CONFIG_HOME +if str(settings.PRIVATE_KEY_FILE) == "ethereum.key": + settings.PRIVATE_KEY_FILE = Path(settings.CONFIG_HOME, "private-keys", "ethereum.key") diff --git a/tests/unit/test_asynchronous.py b/tests/unit/test_asynchronous.py index 279063d3..5caa81da 100644 --- a/tests/unit/test_asynchronous.py +++ b/tests/unit/test_asynchronous.py @@ -18,7 +18,7 @@ create_program, forget, ) -from aleph_client.chains.common import get_fallback_private_key +from aleph_client.chains.common import get_fallback_private_key, delete_private_key_file from aleph_client.chains.ethereum import ETHAccount from aleph_client.conf import settings from aleph_client.types import StorageEnum, MessageStatus @@ -45,7 +45,7 @@ async def test_create_post(): _get_fallback_session.cache_clear() if os.path.exists(settings.PRIVATE_KEY_FILE): - os.remove(settings.PRIVATE_KEY_FILE) + delete_private_key_file() private_key = get_fallback_private_key() account: ETHAccount = ETHAccount(private_key=private_key) @@ -74,7 +74,7 @@ async def test_create_aggregate(): _get_fallback_session.cache_clear() if os.path.exists(settings.PRIVATE_KEY_FILE): - os.remove(settings.PRIVATE_KEY_FILE) + delete_private_key_file() private_key = get_fallback_private_key() account: ETHAccount = ETHAccount(private_key=private_key) @@ -109,7 +109,7 @@ async def test_create_store(): _get_fallback_session.cache_clear() if os.path.exists(settings.PRIVATE_KEY_FILE): - os.remove(settings.PRIVATE_KEY_FILE) + delete_private_key_file() private_key = get_fallback_private_key() account: ETHAccount = ETHAccount(private_key=private_key) @@ -162,7 +162,7 @@ async def test_create_program(): _get_fallback_session.cache_clear() if os.path.exists(settings.PRIVATE_KEY_FILE): - os.remove(settings.PRIVATE_KEY_FILE) + delete_private_key_file() private_key = get_fallback_private_key() account: ETHAccount = ETHAccount(private_key=private_key) @@ -188,7 +188,7 @@ async def test_forget(): _get_fallback_session.cache_clear() if os.path.exists(settings.PRIVATE_KEY_FILE): - os.remove(settings.PRIVATE_KEY_FILE) + delete_private_key_file() private_key = get_fallback_private_key() account: ETHAccount = ETHAccount(private_key=private_key)