From 9464eb089a210d6cdaf6478e273f2011ab854745 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Mon, 11 Aug 2025 13:09:35 -0700 Subject: [PATCH 1/7] Add wallet.py --- README.md | 2 +- example/golem_base_sdk_example/__init__.py | 22 ++++++++--- golem_base_sdk/__init__.py | 5 +++ golem_base_sdk/wallet.py | 46 ++++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 11 ++++++ 6 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 golem_base_sdk/wallet.py 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..7998705 100755 --- a/example/golem_base_sdk_example/__init__.py +++ b/example/golem_base_sdk_example/__init__.py @@ -13,6 +13,9 @@ GolemBaseDelete, GolemBaseExtend, GolemBaseUpdate, + create_wallet, + decrypt_wallet, + WalletError, ) from xdg import BaseDirectory @@ -59,11 +62,20 @@ 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: + create_wallet() + except WalletError as e: + print(f"Error: {e}") + except KeyboardInterrupt: + print("\nOperation cancelled by user.") + + try: + key_bytes = 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..3aed4b5 100755 --- a/golem_base_sdk/__init__.py +++ b/golem_base_sdk/__init__.py @@ -51,6 +51,7 @@ WatchLogsHandle, ) from .utils import rlp_encode_transaction +from .wallet import (create_wallet, decrypt_wallet, WalletError) __all__: Sequence[str] = [ # Exports from .types @@ -73,6 +74,10 @@ # Exports from .constants "GOLEM_BASE_ABI", "STORAGE_ADDRESS", + # Exports from .wallet + "create_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..9de11ad --- /dev/null +++ b/golem_base_sdk/wallet.py @@ -0,0 +1,46 @@ +import getpass +import json +from pathlib import Path +from xdg import BaseDirectory +from eth_account import Account + +WALLET_PATH = Path(BaseDirectory.xdg_config_home) / "golembase" / "wallet.json" + +class WalletError(Exception): + """Base class for wallet-related errors.""" + pass + +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}'") + + with WALLET_PATH.open("r") as f: + keyfile_json = json.load(f) + try: + print(f"Attempting to decrypt wallet at '{WALLET_PATH}'") + private_key = Account.decrypt(keyfile_json, getpass.getpass("Enter wallet password: ")) + except ValueError as e: + raise WalletError("Incorrect password or corrupted wallet file.") from e + + return private_key + +def create_wallet() -> None: + """Creates a new wallet if one does not already exist.""" + if WALLET_PATH.exists(): + print(f"WARNING: A wallet already exists at '{WALLET_PATH}'") + return None + + password = getpass.getpass("Enter wallet password: ") + confirm = getpass.getpass("Confirm wallet password: ") + if password != confirm: + raise WalletError("Passwords do not match.") + + account = Account.create() + encrypted = account.encrypt(password) + + WALLET_PATH.parent.mkdir(parents=True, exist_ok=True) + with WALLET_PATH.open("w") as f: + json.dump(encrypted, f) + + print(f"Wallet created at '{WALLET_PATH}'") diff --git a/pyproject.toml b/pyproject.toml index ed6cff5..b794eef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ requires-python = ">=3.12" dynamic = ["description"] dependencies = [ + "pyxdg>=0.28", "rlp>=1.2.0", "web3>=4.7.2", ] diff --git a/uv.lock b/uv.lock index 96432b5..67efe3a 100644 --- a/uv.lock +++ b/uv.lock @@ -433,6 +433,7 @@ name = "golem-base-sdk" version = "0.0.7" source = { editable = "." } dependencies = [ + { name = "pyxdg" }, { name = "rlp" }, { name = "web3" }, ] @@ -446,6 +447,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "pyxdg", specifier = ">=0.28" }, { name = "rlp", specifier = ">=1.2.0" }, { name = "web3", specifier = ">=4.7.2" }, ] @@ -916,6 +918,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" From e2486f4fdce17da2d9cd93731ac5011362fb40f5 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Mon, 11 Aug 2025 13:32:12 -0700 Subject: [PATCH 2/7] fix lints --- golem_base_sdk/__init__.py | 6 +++++- golem_base_sdk/wallet.py | 16 ++++++++++++---- nix/devshell.nix | 1 + 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/golem_base_sdk/__init__.py b/golem_base_sdk/__init__.py index 3aed4b5..dadddd4 100755 --- a/golem_base_sdk/__init__.py +++ b/golem_base_sdk/__init__.py @@ -51,7 +51,11 @@ WatchLogsHandle, ) from .utils import rlp_encode_transaction -from .wallet import (create_wallet, decrypt_wallet, WalletError) +from .wallet import ( + WalletError, + create_wallet, + decrypt_wallet, +) __all__: Sequence[str] = [ # Exports from .types diff --git a/golem_base_sdk/wallet.py b/golem_base_sdk/wallet.py index 9de11ad..cd86e53 100644 --- a/golem_base_sdk/wallet.py +++ b/golem_base_sdk/wallet.py @@ -1,13 +1,18 @@ +"""Wallet module for creating and decrypting Ethereum wallets.""" + import getpass import json from pathlib import Path -from xdg import BaseDirectory +from typing import cast + 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 def decrypt_wallet() -> bytes: @@ -19,14 +24,17 @@ def decrypt_wallet() -> bytes: keyfile_json = json.load(f) try: print(f"Attempting to decrypt wallet at '{WALLET_PATH}'") - private_key = Account.decrypt(keyfile_json, getpass.getpass("Enter wallet password: ")) + private_key = Account.decrypt( + keyfile_json, + getpass.getpass("Enter wallet password: ") + ) except ValueError as e: raise WalletError("Incorrect password or corrupted wallet file.") from e - return private_key + return cast(bytes, private_key) def create_wallet() -> None: - """Creates a new wallet if one does not already exist.""" + """Create a new wallet if one does not already exist.""" if WALLET_PATH.exists(): print(f"WARNING: A wallet already exists at '{WALLET_PATH}'") return None 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 = [ From 34d3fb649cac45d0d3a51cb533e2e714300b8bb5 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Mon, 11 Aug 2025 13:35:41 -0700 Subject: [PATCH 3/7] fix example lint --- example/golem_base_sdk_example/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example/golem_base_sdk_example/__init__.py b/example/golem_base_sdk_example/__init__.py index 7998705..14676d2 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,11 +12,10 @@ GolemBaseDelete, GolemBaseExtend, GolemBaseUpdate, + WalletError, create_wallet, decrypt_wallet, - WalletError, ) -from xdg import BaseDirectory logging.config.dictConfig( { From f255ab06818a489ee33092592edddda4ff69ae3e Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Tue, 12 Aug 2025 09:56:15 -0700 Subject: [PATCH 4/7] Add passphrase to stdin for account create and fund --- .github/workflows/example.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml index 24e2b8a..967c8f9 100644 --- a/.github/workflows/example.yml +++ b/.github/workflows/example.yml @@ -56,8 +56,8 @@ jobs: - name: Create and fund an account run: | - go run ./cmd/golembase account create - go run ./cmd/golembase account fund + printf "passphrase" | go run ./cmd/golembase account create + printf "passphrase" | go run ./cmd/golembase account fund working-directory: ./gb-op-geth - name: Run the example SDK app From 2b2732a7e04d5600f0f084f440c90c839c313c18 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sun, 17 Aug 2025 21:51:18 -0700 Subject: [PATCH 5/7] add password to stdin for CI demo, add stdin as an option to the decrypt function --- .github/workflows/example.yml | 6 +++--- example/golem_base_sdk_example/__init__.py | 7 ------- golem_base_sdk/wallet.py | 13 +++++++++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml index 967c8f9..c2e48e3 100644 --- a/.github/workflows/example.yml +++ b/.github/workflows/example.yml @@ -56,13 +56,13 @@ jobs: - name: Create and fund an account run: | - printf "passphrase" | go run ./cmd/golembase account create - printf "passphrase" | 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/example/golem_base_sdk_example/__init__.py b/example/golem_base_sdk_example/__init__.py index 14676d2..dbdff25 100755 --- a/example/golem_base_sdk_example/__init__.py +++ b/example/golem_base_sdk_example/__init__.py @@ -60,13 +60,6 @@ async def run_example(instance: str) -> None: # noqa: PLR0915 """Run the example.""" - try: - create_wallet() - except WalletError as e: - print(f"Error: {e}") - except KeyboardInterrupt: - print("\nOperation cancelled by user.") - try: key_bytes = decrypt_wallet() except WalletError as e: diff --git a/golem_base_sdk/wallet.py b/golem_base_sdk/wallet.py index cd86e53..fef3c74 100644 --- a/golem_base_sdk/wallet.py +++ b/golem_base_sdk/wallet.py @@ -2,6 +2,7 @@ import getpass import json +import sys from pathlib import Path from typing import cast @@ -22,12 +23,16 @@ def decrypt_wallet() -> bytes: with WALLET_PATH.open("r") as f: keyfile_json = json.load(f) + + if not sys.stdin.isatty(): + password = sys.stdin.read().rstrip("\n") + else: + password = getpass.getpass("Enter wallet password: ") + try: print(f"Attempting to decrypt wallet at '{WALLET_PATH}'") - private_key = Account.decrypt( - keyfile_json, - getpass.getpass("Enter wallet password: ") - ) + 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 From d746e66bb34456f91330d86f9d8e2c46fe2f9fed Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sun, 17 Aug 2025 22:25:07 -0700 Subject: [PATCH 6/7] remove create wallet function --- example/golem_base_sdk_example/__init__.py | 2 -- golem_base_sdk/__init__.py | 2 -- golem_base_sdk/wallet.py | 23 ++-------------------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/example/golem_base_sdk_example/__init__.py b/example/golem_base_sdk_example/__init__.py index dbdff25..c3ece67 100755 --- a/example/golem_base_sdk_example/__init__.py +++ b/example/golem_base_sdk_example/__init__.py @@ -13,7 +13,6 @@ GolemBaseExtend, GolemBaseUpdate, WalletError, - create_wallet, decrypt_wallet, ) @@ -67,7 +66,6 @@ async def run_example(instance: str) -> None: # noqa: PLR0915 except KeyboardInterrupt: print("\nOperation cancelled by user.") - client = await GolemBaseClient.create( rpc_url=INSTANCE_URLS[instance]["rpc"], ws_url=INSTANCE_URLS[instance]["ws"], diff --git a/golem_base_sdk/__init__.py b/golem_base_sdk/__init__.py index dadddd4..a8e76d2 100755 --- a/golem_base_sdk/__init__.py +++ b/golem_base_sdk/__init__.py @@ -53,7 +53,6 @@ from .utils import rlp_encode_transaction from .wallet import ( WalletError, - create_wallet, decrypt_wallet, ) @@ -79,7 +78,6 @@ "GOLEM_BASE_ABI", "STORAGE_ADDRESS", # Exports from .wallet - "create_wallet", "decrypt_wallet", "WalletError", # Exports from this file diff --git a/golem_base_sdk/wallet.py b/golem_base_sdk/wallet.py index fef3c74..3dabb57 100644 --- a/golem_base_sdk/wallet.py +++ b/golem_base_sdk/wallet.py @@ -25,9 +25,9 @@ def decrypt_wallet() -> bytes: keyfile_json = json.load(f) if not sys.stdin.isatty(): - password = sys.stdin.read().rstrip("\n") + password = sys.stdin.read().rstrip() else: - password = getpass.getpass("Enter wallet password: ") + password = getpass.getpass("Enter password to decrypt wallet: ") try: print(f"Attempting to decrypt wallet at '{WALLET_PATH}'") @@ -38,22 +38,3 @@ def decrypt_wallet() -> bytes: return cast(bytes, private_key) -def create_wallet() -> None: - """Create a new wallet if one does not already exist.""" - if WALLET_PATH.exists(): - print(f"WARNING: A wallet already exists at '{WALLET_PATH}'") - return None - - password = getpass.getpass("Enter wallet password: ") - confirm = getpass.getpass("Confirm wallet password: ") - if password != confirm: - raise WalletError("Passwords do not match.") - - account = Account.create() - encrypted = account.encrypt(password) - - WALLET_PATH.parent.mkdir(parents=True, exist_ok=True) - with WALLET_PATH.open("w") as f: - json.dump(encrypted, f) - - print(f"Wallet created at '{WALLET_PATH}'") From d1ca24c495228dc97afd313094a3adeed476a033 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Tue, 19 Aug 2025 16:14:24 -0700 Subject: [PATCH 7/7] Add anyio to uv.lock, use async file object for reading wallet --- example/golem_base_sdk_example/__init__.py | 2 +- golem_base_sdk/wallet.py | 10 ++++++--- pyproject.toml | 1 + uv.lock | 25 ++++++++++++++++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/example/golem_base_sdk_example/__init__.py b/example/golem_base_sdk_example/__init__.py index c3ece67..624b965 100755 --- a/example/golem_base_sdk_example/__init__.py +++ b/example/golem_base_sdk_example/__init__.py @@ -60,7 +60,7 @@ async def run_example(instance: str) -> None: # noqa: PLR0915 """Run the example.""" try: - key_bytes = decrypt_wallet() + key_bytes = await decrypt_wallet() except WalletError as e: print(f"Error: {e}") except KeyboardInterrupt: diff --git a/golem_base_sdk/wallet.py b/golem_base_sdk/wallet.py index 3dabb57..ce4351f 100644 --- a/golem_base_sdk/wallet.py +++ b/golem_base_sdk/wallet.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import cast +import anyio from eth_account import Account from xdg import BaseDirectory @@ -16,13 +17,16 @@ class WalletError(Exception): pass -def decrypt_wallet() -> bytes: +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}'") - with WALLET_PATH.open("r") as f: - keyfile_json = json.load(f) + 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() diff --git a/pyproject.toml b/pyproject.toml index b794eef..87e3997 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ 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 67efe3a..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,7 @@ name = "golem-base-sdk" version = "0.0.7" source = { editable = "." } dependencies = [ + { name = "anyio" }, { name = "pyxdg" }, { name = "rlp" }, { name = "web3" }, @@ -447,6 +462,7 @@ 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" }, @@ -992,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"