Skip to content

Commit 540c1cf

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 540c1cf

2 files changed

Lines changed: 172 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) -> None:
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: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,170 @@ 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(
457+
app, ["--rpc", "https://fake.rpc", "buy", "--slippage", "-5", _FAKE_MINT, "0.01"]
458+
)
459+
assert result.exit_code != 0
460+
assert "Slippage must be between 0 and 100" in result.output
461+
462+
463+
def test_buy_slippage_above_100():
464+
"""Buy with slippage above 100 exits with error."""
465+
result = runner.invoke(
466+
app, ["--rpc", "https://fake.rpc", "buy", "--slippage", "999", _FAKE_MINT, "0.01"]
467+
)
468+
assert result.exit_code != 0
469+
assert "Slippage must be between 0 and 100" in result.output
470+
471+
472+
def test_sell_slippage_negative():
473+
"""Sell with negative slippage exits with error."""
474+
result = runner.invoke(
475+
app, ["--rpc", "https://fake.rpc", "sell", "--slippage", "-5", _FAKE_MINT, "all"]
476+
)
477+
assert result.exit_code != 0
478+
assert "Slippage must be between 0 and 100" in result.output
479+
480+
481+
def test_sell_slippage_above_100():
482+
"""Sell with slippage above 100 exits with error."""
483+
result = runner.invoke(
484+
app, ["--rpc", "https://fake.rpc", "sell", "--slippage", "999", _FAKE_MINT, "all"]
485+
)
486+
assert result.exit_code != 0
487+
assert "Slippage must be between 0 and 100" in result.output
488+
489+
490+
def test_buy_slippage_zero(tmp_path, monkeypatch):
491+
"""Buy with slippage=0 is valid (boundary)."""
492+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
493+
monkeypatch.setenv("PUMPFUN_PASSWORD", "testpass")
494+
495+
from solders.keypair import Keypair
496+
497+
from pumpfun_cli.crypto import encrypt_keypair
498+
499+
config_dir = tmp_path / "pumpfun-cli"
500+
config_dir.mkdir()
501+
encrypt_keypair(Keypair(), "testpass", config_dir / "wallet.enc")
502+
503+
with patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy:
504+
mock_buy.return_value = {
505+
"action": "buy",
506+
"mint": _FAKE_MINT,
507+
"sol_spent": 0.01,
508+
"tokens_received": 100.0,
509+
"signature": "sig",
510+
"explorer": "https://solscan.io/tx/sig",
511+
}
512+
513+
result = runner.invoke(
514+
app,
515+
["--json", "--rpc", "http://rpc", "buy", "--slippage", "0", _FAKE_MINT, "0.01"],
516+
)
517+
518+
assert result.exit_code == 0
519+
assert "Slippage must be between" not in result.output
520+
521+
522+
def test_buy_slippage_100(tmp_path, monkeypatch):
523+
"""Buy with slippage=100 is valid (boundary)."""
524+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
525+
monkeypatch.setenv("PUMPFUN_PASSWORD", "testpass")
526+
527+
from solders.keypair import Keypair
528+
529+
from pumpfun_cli.crypto import encrypt_keypair
530+
531+
config_dir = tmp_path / "pumpfun-cli"
532+
config_dir.mkdir()
533+
encrypt_keypair(Keypair(), "testpass", config_dir / "wallet.enc")
534+
535+
with patch("pumpfun_cli.commands.trade.buy_token", new_callable=AsyncMock) as mock_buy:
536+
mock_buy.return_value = {
537+
"action": "buy",
538+
"mint": _FAKE_MINT,
539+
"sol_spent": 0.01,
540+
"tokens_received": 100.0,
541+
"signature": "sig",
542+
"explorer": "https://solscan.io/tx/sig",
543+
}
544+
545+
result = runner.invoke(
546+
app,
547+
["--json", "--rpc", "http://rpc", "buy", "--slippage", "100", _FAKE_MINT, "0.01"],
548+
)
549+
550+
assert result.exit_code == 0
551+
assert "Slippage must be between" not in result.output
552+
553+
554+
def test_sell_slippage_zero(tmp_path, monkeypatch):
555+
"""Sell with slippage=0 is valid (boundary)."""
556+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
557+
monkeypatch.setenv("PUMPFUN_PASSWORD", "testpass")
558+
559+
from solders.keypair import Keypair
560+
561+
from pumpfun_cli.crypto import encrypt_keypair
562+
563+
config_dir = tmp_path / "pumpfun-cli"
564+
config_dir.mkdir()
565+
encrypt_keypair(Keypair(), "testpass", config_dir / "wallet.enc")
566+
567+
with patch("pumpfun_cli.commands.trade.sell_token", new_callable=AsyncMock) as mock_sell:
568+
mock_sell.return_value = {
569+
"action": "sell",
570+
"mint": _FAKE_MINT,
571+
"sol_received": 0.01,
572+
"tokens_sold": 100.0,
573+
"signature": "sig",
574+
"explorer": "https://solscan.io/tx/sig",
575+
}
576+
577+
result = runner.invoke(
578+
app,
579+
["--json", "--rpc", "http://rpc", "sell", "--slippage", "0", _FAKE_MINT, "all"],
580+
)
581+
582+
assert result.exit_code == 0
583+
assert "Slippage must be between" not in result.output
584+
585+
586+
def test_sell_slippage_100(tmp_path, monkeypatch):
587+
"""Sell with slippage=100 is valid (boundary)."""
588+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
589+
monkeypatch.setenv("PUMPFUN_PASSWORD", "testpass")
590+
591+
from solders.keypair import Keypair
592+
593+
from pumpfun_cli.crypto import encrypt_keypair
594+
595+
config_dir = tmp_path / "pumpfun-cli"
596+
config_dir.mkdir()
597+
encrypt_keypair(Keypair(), "testpass", config_dir / "wallet.enc")
598+
599+
with patch("pumpfun_cli.commands.trade.sell_token", new_callable=AsyncMock) as mock_sell:
600+
mock_sell.return_value = {
601+
"action": "sell",
602+
"mint": _FAKE_MINT,
603+
"sol_received": 0.01,
604+
"tokens_sold": 100.0,
605+
"signature": "sig",
606+
"explorer": "https://solscan.io/tx/sig",
607+
}
608+
609+
result = runner.invoke(
610+
app,
611+
["--json", "--rpc", "http://rpc", "sell", "--slippage", "100", _FAKE_MINT, "all"],
612+
)
613+
614+
assert result.exit_code == 0
615+
assert "Slippage must be between" not in result.output
616+
617+
454618
def test_buy_json_output_has_expected_keys(tmp_path, monkeypatch):
455619
"""Verify JSON buy output has all expected keys."""
456620
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))

0 commit comments

Comments
 (0)