added keystore based private keys #9
Conversation
WalkthroughThis pull request introduces Ethereum keystore support to the Lume library for secure private key management. New keystore utilities are added to Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Comment |
There was a problem hiding this comment.
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: Prefergetpassin 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: ")
| 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 |
There was a problem hiding this comment.
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.
| 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" | ||
| ) |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| ### 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. |
There was a problem hiding this comment.
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.
| ### 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.
…assword
Summary by CodeRabbit
New Features
API Updates
Documentation