Skip to content

fix: validate config keys against known set in config set#12

Open
P-r-e-m-i-u-m wants to merge 4 commits intochainstacklabs:mainfrom
P-r-e-m-i-u-m:fix/validate-config-keys
Open

fix: validate config keys against known set in config set#12
P-r-e-m-i-u-m wants to merge 4 commits intochainstacklabs:mainfrom
P-r-e-m-i-u-m:fix/validate-config-keys

Conversation

@P-r-e-m-i-u-m
Copy link

@P-r-e-m-i-u-m P-r-e-m-i-u-m commented Mar 18, 2026

Closes #6

Added validation in config_set() to reject unknown config keys before saving.

Changes

src/pumpfun_cli/commands/config.py

  • Added KNOWN_KEYS = {"rpc", "keyfile", "priority_fee", "compute_units"}
  • Added key validation in config_set() — rejects unknown keys with a clear error and hint showing valid keys

tests/test_commands/test_config_cmd.py

  • Added test_config_set_unknown_key — asserts that typos like rpcc are rejected with exit code != 0 and error message containing "unknown config key" and "valid keys"

Risk Assessment: Low Risk — Configuration Validation Only

This PR adds strict key validation to the CLI config setter and a corresponding test. It does not modify any code that constructs or sends Solana transactions.

What changed

  • src/pumpfun_cli/core/config.py
    • Added KNOWN_KEYS: frozenset[str] = frozenset(ENV_MAP).
  • src/pumpfun_cli/commands/config.py
    • config_set() now rejects keys not in KNOWN_KEYS by calling error(...) with a hint listing valid keys and exits before calling save_config_value.
  • tests/test_commands/test_config_cmd.py
    • Added test_config_set_unknown_key to assert unknown keys (e.g., "rpcc") are rejected (non-zero exit), error output contains "unknown config key" and "valid keys", and the invalid key is not persisted.

Security / funds impact

  • This change reduces risk of silent misconfiguration of critical settings that affect funds/transactions:
    • "rpc" controls which Solana endpoint transactions are sent to.
    • "keyfile" points to the signing key used for transactions.
    • "priority_fee" and "compute_units" affect transaction execution parameters.
  • By rejecting unknown keys, the PR prevents accidental persistence of typos (e.g., rpcc) that could cause routing to wrong RPC endpoints or failure to locate the keyfile.
  • The code path that saves config is not executed for invalid keys (error() triggers exit), so no invalid state is written.

Architectural considerations

  • Validation is implemented at the CLI/config boundary before persistence — appropriate placement; no architectural layer violations.
  • No production code that constructs or sends Solana transactions was changed.

Tests

  • Adds a unit test ensuring unknown keys are rejected and not persisted.
  • Existing config set/get/list/delete behaviors and JSON output remain unchanged.

Overall: defensive input validation that safely reduces misconfiguration risk without touching transaction-sending or wallet-handling implementation.

Signed-off-by: 🄂ʏᴇᴅ 🄰ʙᴅᴜʟ 🄰ᴍᴀ🄝 ✧ <amanbaba9404522@gmail.com>
@P-r-e-m-i-u-m P-r-e-m-i-u-m requested a review from smypmsa as a code owner March 18, 2026 10:13
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

Validate keys in the config set CLI against a new KNOWN_KEYS set; unknown keys now produce an error and are not saved. Several tests were added for unknown-key rejection and extensive auto-routing/pumpswap fallback behavior.

Changes

Cohort / File(s) Summary
Config CLI & constants
src/pumpfun_cli/commands/config.py, src/pumpfun_cli/core/config.py
Added KNOWN_KEYS (frozenset) and validation in config_set() to reject unknown config keys with an error and a "Valid keys" hint; save_config_value is not called for unknown keys.
Config CLI tests
tests/test_commands/test_config_cmd.py
Added test_config_set_unknown_key asserting unknown key causes non-zero exit, prints unknown-key error with valid-keys hint, and does not persist the invalid key.
Auto-routing & trade tests
tests/test_commands/test_trade_cmd.py, tests/test_core/test_auto_routing.py, tests/test_surfpool/test_auto_routing.py
Added many tests and helpers for graduated-token auto-routing to PumpSwap, verifying fallback behavior, parameter passthrough (--slippage, --priority-fee, --compute-units, --dry-run, --confirm), error propagation, and CLI-layer integration flows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

core

🚥 Pre-merge checks | ✅ 5 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Raw summary indicates changes to test_trade_cmd.py, test_auto_routing.py, and test_surfpool/test_auto_routing.py that appear unrelated to config key validation scope in #6. Clarify whether test_trade_cmd.py, test_core/test_auto_routing.py, and test_surfpool/test_auto_routing.py changes are in scope for this PR or should be in a separate PR addressing graduated token auto-routing.
Rpcclient Cleanup ❓ Inconclusive Unable to execute shell commands or access file system. Cannot verify test file contents or RpcClient usage patterns. Provide the actual file contents from tests/test_surfpool/test_auto_routing.py, tests/test_commands/test_trade_cmd.py, and src/pumpfun_cli/core/trade.py for manual verification.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format (fix:) and accurately describes the main change: validating config keys against a known set.
Description check ✅ Passed Description clearly explains the change with sections for summary and detailed changes. Links to issue #6, identifies affected layers (commands/ and core/), and documents test coverage.
Linked Issues check ✅ Passed All core requirements from #6 are fully met: KNOWN_KEYS constant added [#6], key validation in config_set() rejects unknown keys [#6], error message with valid keys hint [#6], and test coverage for unknown key rejection [#6].
No Print() Calls ✅ Passed The two changed Python files in src/ contain no bare print() calls. Output uses error() from pumpfun_cli.output and render() or typer.echo().
Layer Separation ✅ Passed Comprehensive search of all Python files in src/pumpfun_cli/commands/ confirms zero direct imports from pumpfun_cli.protocol. All imports from pumpfun_cli package are from core submodules or utility modules.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Tip

CodeRabbit can use your project's `ruff` configuration to improve the quality of Python code reviews.

Add a Ruff configuration file to your project to customize how CodeRabbit runs ruff.

smypmsa
smypmsa previously approved these changes Mar 19, 2026
Copy link

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pumpfun_cli/commands/config.py`:
- Around line 21-31: The config key list is duplicated here (KNOWN_KEYS) causing
drift; remove KNOWN_KEYS from config_set and delegate validation to the core
config module (pumpfun_cli.core.config) by importing its authoritative map or
validator (e.g., ENV_MAP or a provided is_valid_key/validate_config_key
function) and call that from config_set, then call error(...) only when the core
validation fails; keep config_set as Typer wiring (parse args, call core
validator, then error()) and do not reimplement the key list in this file.

In `@tests/test_commands/test_config_cmd.py`:
- Around line 85-91: The test test_config_set_unknown_key should also assert the
failed key wasn't persisted: after invoking config set with the invalid key
(rpcc), call the CLI to list or dump config (e.g., runner.invoke(app, ["config",
"list", "--json"]) or another existing list/get command) and assert the output
does not contain "rpcc" (or that a JSON payload lacks that key); update
test_config_set_unknown_key to run that follow-up runner.invoke and include an
assertion that the invalid key is absent from the configuration output to catch
regressions that write invalid keys.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6cabd160-4a73-4e49-acbc-272c70ea4a54

📥 Commits

Reviewing files that changed from the base of the PR and between e665c38 and 754c6b3.

📒 Files selected for processing (2)
  • src/pumpfun_cli/commands/config.py
  • tests/test_commands/test_config_cmd.py

smypmsa and others added 2 commits March 21, 2026 12:46
…hainstacklabs#17)

- 11 core-layer unit tests verifying graduated→pumpswap routing sequence (mocked RPC)
- 10 command-layer unit tests verifying parameter forwarding (slippage, priority-fee,
  dry-run, confirm) through the fallback path and edge cases (not_found does not
  trigger fallback, pool-not-found after graduation surfaces correctly)
- 3 surfpool integration tests verifying full auto-routing end-to-end against a
  forked mainnet node

No production code changed. Test delta: +21 unit tests (339→360), +3 surfpool tests.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ted in test

Signed-off-by: 🄂ʏᴇᴅ 🄰ʙᴅᴜʟ 🄰ᴍᴀ🄝 ✧ <amanbaba9404522@gmail.com>
@P-r-e-m-i-u-m
Copy link
Author

"Moved KNOWN_KEYS to core/config.py derived from ENV_MAP to avoid drift, updated import in commands/config.py, and added assertion that invalid key is not persisted in the test. Ready for re-review @smypmsa 👍"

@P-r-e-m-i-u-m P-r-e-m-i-u-m requested a review from smypmsa March 21, 2026 07:16
@coderabbitai coderabbitai bot added core and removed cli tests labels Mar 21, 2026
Copy link

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_core/test_auto_routing.py`:
- Around line 176-230: Update the two tests
(test_buy_auto_route_forwards_slippage and
test_sell_auto_route_forwards_slippage) so they actually verify the passed
slippage is used: call buy_pumpswap/sell_pumpswap in a mode or with arguments
that return the dry_run details (or otherwise expose the computed minimum-out),
then assert that the dry_run/minimum-out field reflects slippage=5 (e.g.
min_base_amount_out equals ~95% of the estimated amount). Concretely, modify the
test to capture the returned dry_run object or computed min value from
buy_pumpswap/sell_pumpswap and add an assertion comparing it to the expected 95%
value rather than only asserting action/venue.

In `@tests/test_surfpool/test_auto_routing.py`:
- Around line 108-154: The test test_buy_auto_route_full_command_layer mutates
process-wide os.environ and the shared keystore; change it to avoid global state
by creating a private config dir under tmp_path (e.g., tmp_path /
"pumpfun-cli"), copy test_keystore to that dir as wallet.enc, construct an env
dict with PUMPFUN_RPC, PUMPFUN_PASSWORD and XDG_CONFIG_HOME=str(tmp_path) (do
not write to os.environ), and pass that env into runner.invoke(..., env=env) so
the CLI sees the isolated config; remove the os.environ writes and the
finally-pop pops so no global env is modified. Ensure you reference the same
symbols used in the test (test_keystore, tmp_path, runner.invoke, app,
graduated_mint) when applying the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e0f19a1-a4fd-4bfb-9440-eb85d1e3bfec

📥 Commits

Reviewing files that changed from the base of the PR and between 754c6b3 and 70dbc95.

📒 Files selected for processing (6)
  • src/pumpfun_cli/commands/config.py
  • src/pumpfun_cli/core/config.py
  • tests/test_commands/test_config_cmd.py
  • tests/test_commands/test_trade_cmd.py
  • tests/test_core/test_auto_routing.py
  • tests/test_surfpool/test_auto_routing.py

Comment on lines +176 to +230
@pytest.mark.asyncio
async def test_buy_auto_route_forwards_slippage(tmp_keystore):
"""slippage=5 is forwarded to buy_pumpswap."""
pool_data = build_pool_data()
with patch("pumpfun_cli.core.pumpswap.RpcClient") as MockClient:
client = AsyncMock()
MockClient.return_value = client
client.get_account_info.side_effect = [
_mock_pool_resp(pool_data),
_mock_token_program_resp(),
_mock_global_config_resp(),
_mock_vol_accumulator_resp(),
]
client.get_token_account_balance.side_effect = [1_000_000_000, 30_000_000_000]
client.get_balance.return_value = 10_000_000_000
client.send_tx.return_value = "buysig"
client.close = AsyncMock()

result = await buy_pumpswap(
"http://rpc", tmp_keystore, "testpass", _VALID_MINT, 0.01, slippage=5
)

assert result["action"] == "buy"
assert result["venue"] == "pumpswap"
# With slippage=5, min_base_amount_out should be 95% of estimated
# (verified by the fact that the trade succeeded with slippage=5)


@pytest.mark.asyncio
async def test_sell_auto_route_forwards_slippage(tmp_keystore):
"""slippage=5 is forwarded to sell_pumpswap."""
pool_data = build_pool_data()
with patch("pumpfun_cli.core.pumpswap.RpcClient") as MockClient:
client = AsyncMock()
MockClient.return_value = client
client.get_account_info.side_effect = [
_mock_pool_resp(pool_data),
_mock_token_program_resp(),
_mock_global_config_resp(),
]
client.get_token_account_balance.side_effect = [
1_000_000,
1_000_000_000,
30_000_000_000,
]
client.send_tx.return_value = "sellsig"
client.close = AsyncMock()

result = await sell_pumpswap(
"http://rpc", tmp_keystore, "testpass", _VALID_MINT, "all", slippage=5
)

assert result["action"] == "sell"
assert result["venue"] == "pumpswap"

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

These slippage tests don't currently prove slippage handling.

Both tests call buy_pumpswap / sell_pumpswap directly and only assert success, so they still pass if the implementation ignores the passed slippage and uses the default. Assert on a slippage-derived field in dry_run output, or inspect the computed minimum-out value instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_core/test_auto_routing.py` around lines 176 - 230, Update the two
tests (test_buy_auto_route_forwards_slippage and
test_sell_auto_route_forwards_slippage) so they actually verify the passed
slippage is used: call buy_pumpswap/sell_pumpswap in a mode or with arguments
that return the dry_run details (or otherwise expose the computed minimum-out),
then assert that the dry_run/minimum-out field reflects slippage=5 (e.g.
min_base_amount_out equals ~95% of the estimated amount). Concretely, modify the
test to capture the returned dry_run object or computed min value from
buy_pumpswap/sell_pumpswap and add an assertion comparing it to the expected 95%
value rather than only asserting action/venue.

Comment on lines +108 to +154
def test_buy_auto_route_full_command_layer(
surfpool_rpc, funded_keypair, test_keystore, test_password, graduated_mint, tmp_path
):
"""CliRunner buy without --force-amm shows venue=pumpswap."""
import json
import os

# Set up env for CLI
env = {
"PUMPFUN_RPC": surfpool_rpc,
"PUMPFUN_PASSWORD": test_password,
"XDG_CONFIG_HOME": str(test_keystore.parent.parent),
}
for key, val in env.items():
os.environ[key] = val

try:
# Rename the keystore dir to match what the CLI expects
import shutil

cli_config_dir = test_keystore.parent.parent / "pumpfun-cli"
if not cli_config_dir.exists():
cli_config_dir.mkdir()
cli_wallet = cli_config_dir / "wallet.enc"
if not cli_wallet.exists():
shutil.copy2(str(test_keystore), str(cli_wallet))

result = runner.invoke(
app,
[
"--json",
"--rpc",
surfpool_rpc,
"buy",
graduated_mint,
"0.001",
"--slippage",
"50",
],
)

assert result.exit_code == 0, f"CLI failed: {result.output}"
data = json.loads(result.output)
assert data["venue"] == "pumpswap"
finally:
for key in env:
os.environ.pop(key, None)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Isolate this CLI integration test from process-wide environment state.

This block writes directly to os.environ, copies the wallet into a shared session directory, and then blindly deletes the keys in finally. That can clobber pre-existing values and make later surfpool tests order-dependent. Use runner.invoke(..., env=env) and point XDG_CONFIG_HOME at tmp_path so the copied wallet is private to this test.

♻️ Suggested isolation pattern
 def test_buy_auto_route_full_command_layer(
     surfpool_rpc, funded_keypair, test_keystore, test_password, graduated_mint, tmp_path
 ):
     """CliRunner buy without --force-amm shows venue=pumpswap."""
     import json
-    import os
+    import shutil
 
-    # Set up env for CLI
+    cli_config_dir = tmp_path / "pumpfun-cli"
+    cli_config_dir.mkdir()
+    shutil.copy2(test_keystore, cli_config_dir / "wallet.enc")
+
     env = {
         "PUMPFUN_RPC": surfpool_rpc,
         "PUMPFUN_PASSWORD": test_password,
-        "XDG_CONFIG_HOME": str(test_keystore.parent.parent),
+        "XDG_CONFIG_HOME": str(tmp_path),
     }
-    for key, val in env.items():
-        os.environ[key] = val
-
-    try:
-        # Rename the keystore dir to match what the CLI expects
-        import shutil
-
-        cli_config_dir = test_keystore.parent.parent / "pumpfun-cli"
-        if not cli_config_dir.exists():
-            cli_config_dir.mkdir()
-        cli_wallet = cli_config_dir / "wallet.enc"
-        if not cli_wallet.exists():
-            shutil.copy2(str(test_keystore), str(cli_wallet))
-
-        result = runner.invoke(
-            app,
-            [
-                "--json",
-                "--rpc",
-                surfpool_rpc,
-                "buy",
-                graduated_mint,
-                "0.001",
-                "--slippage",
-                "50",
-            ],
-        )
-
-        assert result.exit_code == 0, f"CLI failed: {result.output}"
-        data = json.loads(result.output)
-        assert data["venue"] == "pumpswap"
-    finally:
-        for key in env:
-            os.environ.pop(key, None)
+    result = runner.invoke(
+        app,
+        [
+            "--json",
+            "--rpc",
+            surfpool_rpc,
+            "buy",
+            graduated_mint,
+            "0.001",
+            "--slippage",
+            "50",
+        ],
+        env=env,
+    )
+
+    assert result.exit_code == 0, f"CLI failed: {result.output}"
+    data = json.loads(result.output)
+    assert data["venue"] == "pumpswap"
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 109-109: Unused function argument: funded_keypair

(ARG001)


[warning] 109-109: Unused function argument: tmp_path

(ARG001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_surfpool/test_auto_routing.py` around lines 108 - 154, The test
test_buy_auto_route_full_command_layer mutates process-wide os.environ and the
shared keystore; change it to avoid global state by creating a private config
dir under tmp_path (e.g., tmp_path / "pumpfun-cli"), copy test_keystore to that
dir as wallet.enc, construct an env dict with PUMPFUN_RPC, PUMPFUN_PASSWORD and
XDG_CONFIG_HOME=str(tmp_path) (do not write to os.environ), and pass that env
into runner.invoke(..., env=env) so the CLI sees the isolated config; remove the
os.environ writes and the finally-pop pops so no global env is modified. Ensure
you reference the same symbols used in the test (test_keystore, tmp_path,
runner.invoke, app, graduated_mint) when applying the change.

@P-r-e-m-i-u-m
Copy link
Author

"The CodeRabbit comments are on test_auto_routing.py and test_surfpool/test_auto_routing.py which were added by @smypmsa — not part of our changes. Our core/config.py and commands/config.py changes are clean. @smypmsa 👍"

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Validate config keys against known set in config set

2 participants