Skip to content

Commit 8103367

Browse files
0xmortuexclaude
andcommitted
feat: validate slippage range (0-100) in buy and sell commands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8934055 commit 8103367

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

src/pumpfun_cli/commands/trade.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ def _validate_mint(mint: str):
4343
error("Invalid mint address.", hint="Provide a valid base58 Solana address.")
4444

4545

46+
def _validate_slippage(slippage: int):
47+
"""Validate slippage is between 0 and 100, or exit with error."""
48+
if slippage < 0 or slippage > 100:
49+
error("Slippage must be between 0 and 100.", hint=f"Got: {slippage}")
50+
51+
4652
def _require_rpc_and_wallet(ctx: typer.Context) -> tuple:
4753
"""Return (rpc, keyfile, password) or exit with error."""
4854
state = ctx.obj
@@ -76,6 +82,7 @@ def buy(
7682
):
7783
"""Buy tokens with SOL."""
7884
_validate_mint(mint)
85+
_validate_slippage(slippage)
7986
rpc, keyfile, password = _require_rpc_and_wallet(ctx)
8087
overrides = _get_overrides(ctx)
8188
try:
@@ -164,6 +171,7 @@ def sell(
164171
):
165172
"""Sell tokens for SOL."""
166173
_validate_mint(mint)
174+
_validate_slippage(slippage)
167175
rpc, keyfile, password = _require_rpc_and_wallet(ctx)
168176
overrides = _get_overrides(ctx)
169177
try:

tests/test_commands/test_trade_cmd.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,98 @@ def test_sell_insufficient_balance_json(tmp_path, monkeypatch):
451451
assert "Insufficient" in result.output
452452

453453

454+
def test_buy_slippage_negative():
455+
"""Buy with negative slippage exits with error."""
456+
result = runner.invoke(app, ["--rpc", "https://fake.rpc", "buy", "--slippage", "-5", _FAKE_MINT, "0.01"])
457+
assert result.exit_code != 0
458+
assert "Slippage must be between 0 and 100" in result.output
459+
460+
461+
def test_buy_slippage_above_100():
462+
"""Buy with slippage above 100 exits with error."""
463+
result = runner.invoke(app, ["--rpc", "https://fake.rpc", "buy", "--slippage", "999", _FAKE_MINT, "0.01"])
464+
assert result.exit_code != 0
465+
assert "Slippage must be between 0 and 100" in result.output
466+
467+
468+
def test_sell_slippage_negative():
469+
"""Sell with negative slippage exits with error."""
470+
result = runner.invoke(app, ["--rpc", "https://fake.rpc", "sell", "--slippage", "-5", _FAKE_MINT, "all"])
471+
assert result.exit_code != 0
472+
assert "Slippage must be between 0 and 100" in result.output
473+
474+
475+
def test_sell_slippage_above_100():
476+
"""Sell with slippage above 100 exits with error."""
477+
result = runner.invoke(app, ["--rpc", "https://fake.rpc", "sell", "--slippage", "999", _FAKE_MINT, "all"])
478+
assert result.exit_code != 0
479+
assert "Slippage must be between 0 and 100" in result.output
480+
481+
482+
def test_buy_slippage_zero(tmp_path, monkeypatch):
483+
"""Buy with slippage=0 is valid (boundary)."""
484+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
485+
monkeypatch.setenv("PUMPFUN_PASSWORD", "testpass")
486+
487+
from solders.keypair import Keypair
488+
489+
from pumpfun_cli.crypto import encrypt_keypair
490+
491+
config_dir = tmp_path / "pumpfun-cli"
492+
config_dir.mkdir()
493+
encrypt_keypair(Keypair(), "testpass", config_dir / "wallet.enc")
494+
495+
with patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy:
496+
mock_buy.return_value = {
497+
"action": "buy",
498+
"mint": _FAKE_MINT,
499+
"sol_spent": 0.01,
500+
"tokens_received": 100.0,
501+
"signature": "sig",
502+
"explorer": "https://solscan.io/tx/sig",
503+
}
504+
505+
result = runner.invoke(
506+
app,
507+
["--json", "--rpc", "http://rpc", "buy", "--slippage", "0", _FAKE_MINT, "0.01"],
508+
)
509+
510+
assert result.exit_code == 0
511+
assert "Slippage must be between" not in result.output
512+
513+
514+
def test_buy_slippage_100(tmp_path, monkeypatch):
515+
"""Buy with slippage=100 is valid (boundary)."""
516+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
517+
monkeypatch.setenv("PUMPFUN_PASSWORD", "testpass")
518+
519+
from solders.keypair import Keypair
520+
521+
from pumpfun_cli.crypto import encrypt_keypair
522+
523+
config_dir = tmp_path / "pumpfun-cli"
524+
config_dir.mkdir()
525+
encrypt_keypair(Keypair(), "testpass", config_dir / "wallet.enc")
526+
527+
with patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy:
528+
mock_buy.return_value = {
529+
"action": "buy",
530+
"mint": _FAKE_MINT,
531+
"sol_spent": 0.01,
532+
"tokens_received": 100.0,
533+
"signature": "sig",
534+
"explorer": "https://solscan.io/tx/sig",
535+
}
536+
537+
result = runner.invoke(
538+
app,
539+
["--json", "--rpc", "http://rpc", "buy", "--slippage", "100", _FAKE_MINT, "0.01"],
540+
)
541+
542+
assert result.exit_code == 0
543+
assert "Slippage must be between" not in result.output
544+
545+
454546
def test_buy_json_output_has_expected_keys(tmp_path, monkeypatch):
455547
"""Verify JSON buy output has all expected keys."""
456548
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))

0 commit comments

Comments
 (0)