Skip to content

added keystore based private keys #9

Open
0xparashar wants to merge 1 commit into
mainfrom
feat/keystore-private-keys
Open

added keystore based private keys #9
0xparashar wants to merge 1 commit into
mainfrom
feat/keystore-private-keys

Conversation

@0xparashar
Copy link
Copy Markdown
Contributor

@0xparashar 0xparashar commented Feb 10, 2026

…assword

Summary by CodeRabbit

  • New Features

    • Added encrypted keystore support for secure private key storage with password protection.
    • Implemented interactive private key resolution with keystore loading and wallet generation capabilities.
    • Added keystore management utilities (encrypt, decrypt, save, load, and check existence).
  • API Updates

    • Expanded agent creation to support keystore-driven key resolution with optional fallback to environment variables.
    • Exported keystore utilities and default keystore path as public API.
  • Documentation

    • Updated guides with keystore workflows, examples, and troubleshooting for common errors.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 10, 2026

Walkthrough

This pull request introduces Ethereum keystore support to the Lume library for secure private key management. New keystore utilities are added to lume/utils.py to handle encryption, decryption, saving, and loading of keystores. The create_agent_from_env function in lume/agent.py is extended with new parameters (keystore_path and use_keystore) to support interactive private key resolution through keystores, with up to 3 password attempts and optional wallet generation. Public API exports in lume/__init__.py are updated to expose the new keystore utilities while removing from_atomic. Documentation is updated across multiple skill guides to reflect the new keystore workflows and provide troubleshooting guidance.

Sequence Diagram

sequenceDiagram
    actor User
    participant Agent as create_agent_from_env
    participant Utils as Keystore Utils
    participant FS as File System
    participant Account as eth_account.Account

    User->>Agent: Call with use_keystore=True
    Agent->>Agent: Check LUME_PRIVATE_KEY/PRIVATE_KEY env var
    alt Env var set
        Agent->>Agent: Use env var key
    else Env var not set and use_keystore enabled
        Agent->>Utils: keystore_exists(keystore_path)
        alt Keystore exists
            Agent->>Utils: load_keystore(keystore_path)
            Utils->>FS: Read keystore.json
            FS-->>Utils: Keystore dict
            Agent->>User: Prompt for password
            User-->>Agent: Password
            Agent->>Utils: decrypt_private_key(keystore, password)
            Utils->>Account: Decrypt using eth_account
            alt Decryption succeeds
                Account-->>Utils: Private key
                Utils-->>Agent: Private key (hex, no 0x)
            else Decryption fails (retry up to 3 times)
                Agent->>User: Retry password prompt
                User-->>Agent: New password
                Agent->>Utils: decrypt_private_key (retry)
            end
        else Keystore not found
            Agent->>User: Prompt to generate new wallet
            User-->>Agent: Confirm generation
            Agent->>Account: Generate new wallet
            Account-->>Agent: Private key, address
            Agent->>User: Request password for encryption
            User-->>Agent: Password
            Agent->>Utils: encrypt_private_key(private_key, password)
            Utils->>Account: Encrypt to keystore v3
            Account-->>Utils: Keystore dict
            Agent->>Utils: save_keystore(keystore, path)
            Utils->>FS: Save with 700 dir/600 file perms
            FS-->>Utils: Absolute path
        end
    else Env var not set and use_keystore disabled
        Agent->>Agent: Raise ValueError
    end
    Agent->>Agent: Create AgentClient with resolved key
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 With keystores now encrypted tight,
Private keys are locked just right,
Passwords prompt with three good tries,
Wallets born before our eyes,
Security hops through Lume's new skies! 🔐✨

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@lume/agent.py`:
- Around line 388-442: When resolving the keystore path in create_agent_from_env
before calling _resolve_private_key_interactive, expand user (~) and environment
variables so paths like LUME_KEYSTORE_PATH="~/.lume/keystore.json" work;
normalize the chosen resolved_path (keystore_path or
os.getenv("LUME_KEYSTORE_PATH") or DEFAULT_KEYSTORE_PATH) with
os.path.expanduser and os.path.expandvars (and optionally os.path.abspath) and
pass that expanded path into _resolve_private_key_interactive.
- Around line 341-385: The keystore loading path in
_resolve_private_key_interactive currently lets JSONDecodeError/PermissionError
bubble up; wrap the call to load_keystore(keystore_path) in a try/except that
catches JSONDecodeError, PermissionError, OSError (or Exception) and re-raise a
ValueError with a clear message (e.g., "Failed to load keystore: <detail>") so
callers always see ValueError as documented; keep the rest of the existing
decryption/password retry flow unchanged and include the original exception text
in the new ValueError for debugging.

In `@lume/utils.py`:
- Around line 97-115: In save_keystore, handle the case where
os.path.dirname(path) is empty (skip os.makedirs) and avoid a brief window of
permissive permissions by creating/truncating the file with mode 0o600 up-front
instead of relying on chmod afterwards: if directory := os.path.dirname(path) is
non-empty, call os.makedirs(directory, mode=0o700, exist_ok=True); then open the
file using a low-level open that accepts a mode (e.g., os.open with
os.O_WRONLY|os.O_CREAT|os.O_TRUNC and mode=0o600) and wrap the resulting fd with
os.fdopen to json.dump the keystore; keep returning os.path.abspath(path).
Ensure references to DEFAULT_KEYSTORE_PATH and the function save_keystore remain
intact.

In `@skills/lume-trading/SKILL.md`:
- Around line 69-130: The snippet claims to honor LUME_KEYSTORE_PATH but calls
keystore_exists() and load_keystore() without a path; update the flow to first
resolve keystore_path = os.getenv("LUME_KEYSTORE_PATH") (fallback to default),
then pass keystore_path into keystore_exists(keystore_path),
load_keystore(keystore_path), and save_keystore(keystore_path) so the env var is
actually used; do the same for decrypt_private_key/encrypt_private_key usage and
keep AskUserQuestion, generate_wallet, and AgentClient usage unchanged.
🧹 Nitpick comments (1)
skills/lume-trading/references/EXAMPLES.md (1)

110-142: Prefer getpass in copy‑paste examples to avoid hard‑coded secrets.

✏️ Suggested doc tweak
-from lume import generate_wallet, encrypt_private_key, save_keystore
+import getpass
+from lume import generate_wallet, encrypt_private_key, save_keystore
@@
-password = "my-secure-password"  # In practice, use getpass.getpass()
+password = getpass.getpass("Keystore password: ")

-from lume import load_keystore, decrypt_private_key, keystore_exists
+import getpass
+from lume import load_keystore, decrypt_private_key, keystore_exists
@@
-    password = "my-secure-password"  # In practice, use getpass.getpass()
+    password = getpass.getpass("Keystore password: ")

Comment thread lume/agent.py
Comment on lines +341 to +385
def _resolve_private_key_interactive(keystore_path: str) -> str:
"""Resolve a private key interactively via keystore.

If a keystore file exists at *keystore_path*, the user is prompted for the
password (up to 3 attempts). Otherwise a new wallet is generated, the user
is asked to create a password (with confirmation), and the encrypted
keystore is persisted.

Returns:
Hex private key **without** the ``0x`` prefix.

Raises:
ValueError: After 3 failed password attempts or mismatched confirmation.
"""

if keystore_exists(keystore_path):
keystore = load_keystore(keystore_path)
for attempt in range(1, 4):
password = getpass.getpass(
f"Enter keystore password ({attempt}/3): "
)
try:
private_key = decrypt_private_key(keystore, password)
address = Account.from_key(private_key).address
print(f"Wallet loaded: {address}")
print(f"Keystore: {keystore_path}")
return private_key
except ValueError:
if attempt < 3:
print("Incorrect password, try again.")
raise ValueError("Failed to decrypt keystore after 3 attempts")

# No existing keystore — generate a new wallet
private_key, address = generate_wallet()

password = getpass.getpass("Create keystore password: ")
password_confirm = getpass.getpass("Confirm keystore password: ")
if password != password_confirm:
raise ValueError("Passwords do not match")

keystore = encrypt_private_key(private_key, password)
saved_path = save_keystore(keystore, keystore_path)
print(f"New wallet generated: {address}")
print(f"Keystore saved: {saved_path}")
return private_key
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize keystore load failures to a consistent ValueError.

Right now, corrupt JSON or permission issues bubble as JSONDecodeError/PermissionError, while docs mention ValueError. Consider wrapping keystore load failures to produce a consistent error.

🛠️ Suggested adjustment
-    if keystore_exists(keystore_path):
-        keystore = load_keystore(keystore_path)
+    if keystore_exists(keystore_path):
+        try:
+            keystore = load_keystore(keystore_path)
+            if not keystore:
+                raise ValueError("Keystore file is empty or unreadable")
+        except Exception as exc:
+            raise ValueError(
+                f"Unable to load keystore at {keystore_path}"
+            ) from exc
         for attempt in range(1, 4):
             password = getpass.getpass(
                 f"Enter keystore password ({attempt}/3): "
             )
🤖 Prompt for AI Agents
In `@lume/agent.py` around lines 341 - 385, The keystore loading path in
_resolve_private_key_interactive currently lets JSONDecodeError/PermissionError
bubble up; wrap the call to load_keystore(keystore_path) in a try/except that
catches JSONDecodeError, PermissionError, OSError (or Exception) and re-raise a
ValueError with a clear message (e.g., "Failed to load keystore: <detail>") so
callers always see ValueError as documented; keep the rest of the existing
decryption/password retry flow unchanged and include the original exception text
in the new ValueError for debugging.

Comment thread lume/agent.py
Comment on lines 388 to +442
def create_agent_from_env(
agent_id: Optional[str] = None,
display_name: Optional[str] = None,
auto_register: bool = True,
keystore_path: Optional[str] = None,
use_keystore: bool = True,
) -> AgentClient:
"""Create an AgentClient from environment variables.

Reads configuration from environment:
- LUME_PRIVATE_KEY or PRIVATE_KEY: Required wallet private key
- LUME_PRIVATE_KEY or PRIVATE_KEY: Wallet private key
- LUME_API_URL: Optional API URL override
- LUME_AGENT_ID: Optional agent ID (overridden by agent_id param)
- LUME_AGENT_NAME: Optional display name (overridden by display_name param)
- LUME_KEYSTORE_PATH: Optional custom keystore file path

**Priority chain** for resolving the private key:

1. ``LUME_PRIVATE_KEY`` / ``PRIVATE_KEY`` environment variable
2. Encrypted keystore file (if *use_keystore* is ``True``)
3. Generate a new wallet and save to keystore

Args:
agent_id: Agent identifier (defaults to LUME_AGENT_ID env var)
display_name: Display name (defaults to LUME_AGENT_NAME env var)
auto_register: Whether to register automatically
keystore_path: Path to keystore JSON file (defaults to
``LUME_KEYSTORE_PATH`` env var or ``~/.lume/keystore.json``)
use_keystore: If ``True`` (default) and no env-var key is present,
interactively resolve the key via keystore. If ``False``,
a missing env var raises ``ValueError``.

Returns:
Configured AgentClient instance

Raises:
ValueError: If PRIVATE_KEY is not set
ValueError: If no private key can be resolved
AgentRegistrationError: If auto_register is True and registration fails
"""
import os

private_key = os.getenv("LUME_PRIVATE_KEY") or os.getenv("PRIVATE_KEY")

if not private_key:
raise ValueError(
"LUME_PRIVATE_KEY or PRIVATE_KEY environment variable is required"
)
if use_keystore:
resolved_path = (
keystore_path
or os.getenv("LUME_KEYSTORE_PATH")
or DEFAULT_KEYSTORE_PATH
)
private_key = _resolve_private_key_interactive(resolved_path)
else:
raise ValueError(
"LUME_PRIVATE_KEY or PRIVATE_KEY environment variable is required"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Expand ~ and env vars in the keystore path.

LUME_KEYSTORE_PATH commonly uses ~, but os.path.isfile won’t expand it. Normalize the path before passing it into _resolve_private_key_interactive.

🛠️ Suggested adjustment
         if use_keystore:
             resolved_path = (
                 keystore_path
                 or os.getenv("LUME_KEYSTORE_PATH")
                 or DEFAULT_KEYSTORE_PATH
             )
+            resolved_path = os.path.expanduser(os.path.expandvars(resolved_path))
             private_key = _resolve_private_key_interactive(resolved_path)
🤖 Prompt for AI Agents
In `@lume/agent.py` around lines 388 - 442, When resolving the keystore path in
create_agent_from_env before calling _resolve_private_key_interactive, expand
user (~) and environment variables so paths like
LUME_KEYSTORE_PATH="~/.lume/keystore.json" work; normalize the chosen
resolved_path (keystore_path or os.getenv("LUME_KEYSTORE_PATH") or
DEFAULT_KEYSTORE_PATH) with os.path.expanduser and os.path.expandvars (and
optionally os.path.abspath) and pass that expanded path into
_resolve_private_key_interactive.

Comment thread lume/utils.py
Comment on lines +97 to +115
def save_keystore(keystore: dict, path: str = DEFAULT_KEYSTORE_PATH) -> str:
"""Persist a keystore dict to disk with restrictive permissions.

Creates the parent directory (mode 0o700) if it does not exist and
writes the file with mode 0o600 so only the current user can read it.

Args:
keystore: Keystore v3 dict.
path: Destination file path.

Returns:
The absolute path where the keystore was saved.
"""
directory = os.path.dirname(path)
os.makedirs(directory, mode=0o700, exist_ok=True)
with open(path, "w") as f:
json.dump(keystore, f, indent=2)
os.chmod(path, 0o600)
return os.path.abspath(path)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden keystore saving for empty directories and restrictive perms at creation.

If path is just a filename (no directory), os.path.dirname(path) returns "" and os.makedirs("", ...) fails. Also, the file is created with default umask before chmod, briefly widening permissions. Consider skipping directory creation when empty and creating the file with mode 0o600 up-front.

🛠️ Proposed fix
 def save_keystore(keystore: dict, path: str = DEFAULT_KEYSTORE_PATH) -> str:
@@
-    directory = os.path.dirname(path)
-    os.makedirs(directory, mode=0o700, exist_ok=True)
-    with open(path, "w") as f:
-        json.dump(keystore, f, indent=2)
-    os.chmod(path, 0o600)
+    directory = os.path.dirname(path)
+    if directory:
+        os.makedirs(directory, mode=0o700, exist_ok=True)
+    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+    with os.fdopen(fd, "w") as f:
+        json.dump(keystore, f, indent=2)
     return os.path.abspath(path)
🤖 Prompt for AI Agents
In `@lume/utils.py` around lines 97 - 115, In save_keystore, handle the case where
os.path.dirname(path) is empty (skip os.makedirs) and avoid a brief window of
permissive permissions by creating/truncating the file with mode 0o600 up-front
instead of relying on chmod afterwards: if directory := os.path.dirname(path) is
non-empty, call os.makedirs(directory, mode=0o700, exist_ok=True); then open the
file using a low-level open that accepts a mode (e.g., os.open with
os.O_WRONLY|os.O_CREAT|os.O_TRUNC and mode=0o600) and wrap the resulting fd with
os.fdopen to json.dump the keystore; keep returning os.path.abspath(path).
Ensure references to DEFAULT_KEYSTORE_PATH and the function save_keystore remain
intact.

Comment on lines +69 to +130
### Initialize the agent — IMPORTANT: Follow this procedure

For autonomous agents, use `AgentClient` (extends `LumeClient` with
registration and authentication):
Before ANY trading operation, you MUST resolve the agent's private key. Follow
this priority chain **in order** — stop at the first one that succeeds:

```python
import os
from lume import AgentClient
#### Priority 1: Encrypted keystore (highest)

agent = AgentClient(private_key=os.environ["PRIVATE_KEY"])
info = agent.register()
Check if `~/.lume/keystore.json` exists (or `LUME_KEYSTORE_PATH` env var).
Since `getpass` does not work in non-interactive shells, you MUST ask the user
for their password using `AskUserQuestion`, then decrypt programmatically:

print(f"EOA Address: {agent.eoa_address}")
print(f"Proxy Wallet: {agent.proxy_wallet}")
print(f"Registered: {info.is_registered}")
```python
from lume import keystore_exists, load_keystore, decrypt_private_key

if keystore_exists():
# keystore found — ASK the user for their password via AskUserQuestion,
# then decrypt:
keystore = load_keystore()
private_key = decrypt_private_key(keystore, password) # password from user
```

Or use the environment helper:
If decryption fails (`ValueError`), tell the user the password was wrong and
ask again (up to 3 attempts).

```python
from lume import create_agent_from_env
#### Priority 2: Environment variable

If no keystore exists, check `LUME_PRIVATE_KEY` or `PRIVATE_KEY`:

# Reads LUME_PRIVATE_KEY (or PRIVATE_KEY), LUME_API_URL, LUME_AGENT_ID, LUME_AGENT_NAME
agent = create_agent_from_env(agent_id="my-bot", display_name="My Trading Bot")
```python
import os
private_key = os.getenv("LUME_PRIVATE_KEY") or os.getenv("PRIVATE_KEY")
```

You can also use `LumeClient` directly if you don't need agent registration:
#### Priority 3: Generate new wallet

If neither keystore nor env var is available, generate a new wallet, ask the
user to create a password (with confirmation), encrypt, and save:

```python
from lume import LumeClient
from lume import generate_wallet, encrypt_private_key, save_keystore

client = LumeClient(private_key=os.environ["PRIVATE_KEY"])
private_key, address = generate_wallet()
# ASK user to provide a password via AskUserQuestion
keystore = encrypt_private_key(private_key, password)
save_keystore(keystore)
```

### Generate a new wallet (if needed)
#### Create the AgentClient

Once you have the `private_key`, create the agent directly:

```python
from lume import generate_wallet
from lume import AgentClient

private_key, address = generate_wallet()
print(f"Address: {address}")
# Save private_key securely
agent = AgentClient(private_key=private_key, auto_register=True)
print(f"EOA Address: {agent.eoa_address}")
print(f"Proxy Wallet: {agent.proxy_wallet}")
```

> **Note:** Do NOT use `create_agent_from_env()` from within this skill because
> its `getpass` prompts fail in non-interactive shells. Always resolve the key
> yourself using the procedure above, then pass it to `AgentClient` directly.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align the snippet with LUME_KEYSTORE_PATH support.

Line 76 says LUME_KEYSTORE_PATH is honored, but the snippet uses keystore_exists() / load_keystore() without passing the resolved path, so the env var won’t be used. Consider wiring the path into the snippet.

✏️ Suggested doc fix
-from lume import keystore_exists, load_keystore, decrypt_private_key
+import os
+from lume import (
+    DEFAULT_KEYSTORE_PATH,
+    keystore_exists,
+    load_keystore,
+    decrypt_private_key,
+)
+
+keystore_path = os.getenv("LUME_KEYSTORE_PATH") or DEFAULT_KEYSTORE_PATH

-if keystore_exists():
+if keystore_exists(keystore_path):
     # keystore found — ASK the user for their password via AskUserQuestion,
     # then decrypt:
-    keystore = load_keystore()
+    keystore = load_keystore(keystore_path)
     private_key = decrypt_private_key(keystore, password)  # password from user
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Initialize the agent — IMPORTANT: Follow this procedure
For autonomous agents, use `AgentClient` (extends `LumeClient` with
registration and authentication):
Before ANY trading operation, you MUST resolve the agent's private key. Follow
this priority chain **in order** — stop at the first one that succeeds:
```python
import os
from lume import AgentClient
#### Priority 1: Encrypted keystore (highest)
agent = AgentClient(private_key=os.environ["PRIVATE_KEY"])
info = agent.register()
Check if `~/.lume/keystore.json` exists (or `LUME_KEYSTORE_PATH` env var).
Since `getpass` does not work in non-interactive shells, you MUST ask the user
for their password using `AskUserQuestion`, then decrypt programmatically:
print(f"EOA Address: {agent.eoa_address}")
print(f"Proxy Wallet: {agent.proxy_wallet}")
print(f"Registered: {info.is_registered}")
```python
from lume import keystore_exists, load_keystore, decrypt_private_key
if keystore_exists():
# keystore found — ASK the user for their password via AskUserQuestion,
# then decrypt:
keystore = load_keystore()
private_key = decrypt_private_key(keystore, password) # password from user
```
Or use the environment helper:
If decryption fails (`ValueError`), tell the user the password was wrong and
ask again (up to 3 attempts).
```python
from lume import create_agent_from_env
#### Priority 2: Environment variable
If no keystore exists, check `LUME_PRIVATE_KEY` or `PRIVATE_KEY`:
# Reads LUME_PRIVATE_KEY (or PRIVATE_KEY), LUME_API_URL, LUME_AGENT_ID, LUME_AGENT_NAME
agent = create_agent_from_env(agent_id="my-bot", display_name="My Trading Bot")
```python
import os
private_key = os.getenv("LUME_PRIVATE_KEY") or os.getenv("PRIVATE_KEY")
```
You can also use `LumeClient` directly if you don't need agent registration:
#### Priority 3: Generate new wallet
If neither keystore nor env var is available, generate a new wallet, ask the
user to create a password (with confirmation), encrypt, and save:
```python
from lume import LumeClient
from lume import generate_wallet, encrypt_private_key, save_keystore
client = LumeClient(private_key=os.environ["PRIVATE_KEY"])
private_key, address = generate_wallet()
# ASK user to provide a password via AskUserQuestion
keystore = encrypt_private_key(private_key, password)
save_keystore(keystore)
```
### Generate a new wallet (if needed)
#### Create the AgentClient
Once you have the `private_key`, create the agent directly:
```python
from lume import generate_wallet
from lume import AgentClient
private_key, address = generate_wallet()
print(f"Address: {address}")
# Save private_key securely
agent = AgentClient(private_key=private_key, auto_register=True)
print(f"EOA Address: {agent.eoa_address}")
print(f"Proxy Wallet: {agent.proxy_wallet}")
```
> **Note:** Do NOT use `create_agent_from_env()` from within this skill because
> its `getpass` prompts fail in non-interactive shells. Always resolve the key
> yourself using the procedure above, then pass it to `AgentClient` directly.
import os
from lume import (
DEFAULT_KEYSTORE_PATH,
keystore_exists,
load_keystore,
decrypt_private_key,
)
keystore_path = os.getenv("LUME_KEYSTORE_PATH") or DEFAULT_KEYSTORE_PATH
if keystore_exists(keystore_path):
# keystore found — ASK the user for their password via AskUserQuestion,
# then decrypt:
keystore = load_keystore(keystore_path)
private_key = decrypt_private_key(keystore, password) # password from user
🤖 Prompt for AI Agents
In `@skills/lume-trading/SKILL.md` around lines 69 - 130, The snippet claims to
honor LUME_KEYSTORE_PATH but calls keystore_exists() and load_keystore() without
a path; update the flow to first resolve keystore_path =
os.getenv("LUME_KEYSTORE_PATH") (fallback to default), then pass keystore_path
into keystore_exists(keystore_path), load_keystore(keystore_path), and
save_keystore(keystore_path) so the env var is actually used; do the same for
decrypt_private_key/encrypt_private_key usage and keep AskUserQuestion,
generate_wallet, and AgentClient usage unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant