diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml index 24e2b8a..c2e48e3 100644 --- a/.github/workflows/example.yml +++ b/.github/workflows/example.yml @@ -56,13 +56,13 @@ jobs: - name: Create and fund an account run: | - go run ./cmd/golembase account create - go run ./cmd/golembase account fund + printf "password" | go run ./cmd/golembase account create + printf "password" | go run ./cmd/golembase account fund working-directory: ./gb-op-geth - name: Run the example SDK app run: > - nix develop --no-write-lock-file --reference-lock-file ../flake.lock --command + printf "password" | nix develop --no-write-lock-file --reference-lock-file ../flake.lock --command ./result/bin/main --instance local working-directory: ./example diff --git a/README.md b/README.md index 1ff4c67..305edd5 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The repo also contains an example application to showcase how you can use this S (Note: As an alternative to installing the demo CLI, you can build the [actual CLI](https://github.com/Golem-Base/golembase-op-geth/blob/main/cmd/golembase/README.md) as it's included in the golembase-op-geth repo.) -When you create a user, it will generate a private key file called `private.key` and store it in: +When you create a user, it will generate a private key file called `wallet.json` and store it in: * `~/.config/golembase/` on **Linux** * `~/Library/Application Support/golembase/` on **macOS** diff --git a/example/golem_base_sdk_example/__init__.py b/example/golem_base_sdk_example/__init__.py index 29473d0..624b965 100755 --- a/example/golem_base_sdk_example/__init__.py +++ b/example/golem_base_sdk_example/__init__.py @@ -5,7 +5,6 @@ import logging import logging.config -import anyio from golem_base_sdk import ( Annotation, GolemBaseClient, @@ -13,8 +12,9 @@ GolemBaseDelete, GolemBaseExtend, GolemBaseUpdate, + WalletError, + decrypt_wallet, ) -from xdg import BaseDirectory logging.config.dictConfig( { @@ -59,11 +59,12 @@ async def run_example(instance: str) -> None: # noqa: PLR0915 """Run the example.""" - async with await anyio.open_file( - BaseDirectory.xdg_config_home + "/golembase/private.key", - "rb", - ) as private_key_file: - key_bytes = await private_key_file.read(32) + try: + key_bytes = await decrypt_wallet() + except WalletError as e: + print(f"Error: {e}") + except KeyboardInterrupt: + print("\nOperation cancelled by user.") client = await GolemBaseClient.create( rpc_url=INSTANCE_URLS[instance]["rpc"], diff --git a/golem_base_sdk/__init__.py b/golem_base_sdk/__init__.py index 8ca5270..a8e76d2 100755 --- a/golem_base_sdk/__init__.py +++ b/golem_base_sdk/__init__.py @@ -51,6 +51,10 @@ WatchLogsHandle, ) from .utils import rlp_encode_transaction +from .wallet import ( + WalletError, + decrypt_wallet, +) __all__: Sequence[str] = [ # Exports from .types @@ -73,6 +77,9 @@ # Exports from .constants "GOLEM_BASE_ABI", "STORAGE_ADDRESS", + # Exports from .wallet + "decrypt_wallet", + "WalletError", # Exports from this file "GolemBaseClient", # Re-exports diff --git a/golem_base_sdk/wallet.py b/golem_base_sdk/wallet.py new file mode 100644 index 0000000..ce4351f --- /dev/null +++ b/golem_base_sdk/wallet.py @@ -0,0 +1,44 @@ +"""Wallet module for creating and decrypting Ethereum wallets.""" + +import getpass +import json +import sys +from pathlib import Path +from typing import cast + +import anyio +from eth_account import Account +from xdg import BaseDirectory + +WALLET_PATH = Path(BaseDirectory.xdg_config_home) / "golembase" / "wallet.json" + +class WalletError(Exception): + """Base class for wallet-related errors.""" + + pass + +async def decrypt_wallet() -> bytes: + """Decrypts the wallet and returns the private key bytes.""" + if not WALLET_PATH.exists(): + raise WalletError(f"Expected wallet file to exist at '{WALLET_PATH}'") + + async with await anyio.open_file( + WALLET_PATH, + "r", + ) as f: + keyfile_json = json.loads(await f.read()) + + if not sys.stdin.isatty(): + password = sys.stdin.read().rstrip() + else: + password = getpass.getpass("Enter password to decrypt wallet: ") + + try: + print(f"Attempting to decrypt wallet at '{WALLET_PATH}'") + private_key = Account.decrypt(keyfile_json, password) + print("Successfully decrypted wallet") + except ValueError as e: + raise WalletError("Incorrect password or corrupted wallet file.") from e + + return cast(bytes, private_key) + diff --git a/nix/devshell.nix b/nix/devshell.nix index c83b69b..9723cbc 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -12,6 +12,7 @@ perSystem.devshell.mkShell { packages = [ virtualenvDev pkgs.uv + pkgs.ruff ]; env = [ diff --git a/pyproject.toml b/pyproject.toml index ed6cff5..87e3997 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,8 @@ requires-python = ">=3.12" dynamic = ["description"] dependencies = [ + "anyio>=4.10.0", + "pyxdg>=0.28", "rlp>=1.2.0", "web3>=4.7.2", ] diff --git a/uv.lock b/uv.lock index 96432b5..811f5a0 100644 --- a/uv.lock +++ b/uv.lock @@ -83,6 +83,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + [[package]] name = "attrs" version = "25.3.0" @@ -433,6 +447,8 @@ name = "golem-base-sdk" version = "0.0.7" source = { editable = "." } dependencies = [ + { name = "anyio" }, + { name = "pyxdg" }, { name = "rlp" }, { name = "web3" }, ] @@ -446,6 +462,8 @@ dev = [ [package.metadata] requires-dist = [ + { name = "anyio", specifier = ">=4.10.0" }, + { name = "pyxdg", specifier = ">=0.28" }, { name = "rlp", specifier = ">=1.2.0" }, { name = "web3", specifier = ">=4.7.2" }, ] @@ -916,6 +934,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, ] +[[package]] +name = "pyxdg" +version = "0.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/25/7998cd2dec731acbd438fbf91bc619603fc5188de0a9a17699a781840452/pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4", size = 77776, upload-time = "2022-06-05T11:35:01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/8d/cf41b66a8110670e3ad03dab9b759704eeed07fa96e90fdc0357b2ba70e2/pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab", size = 49520, upload-time = "2022-06-05T11:34:58.832Z" }, +] + [[package]] name = "regex" version = "2024.11.6" @@ -981,6 +1008,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973, upload-time = "2025-02-04T22:05:57.05Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "toolz" version = "1.0.0"