Skip to content

Latest commit

Β 

History

History
154 lines (110 loc) Β· 6.41 KB

File metadata and controls

154 lines (110 loc) Β· 6.41 KB

CLAUDE.md β€” pumpfun-cli

CLI for trading, launching, and browsing tokens on pump.fun (Solana). Python 3.12+, uv, Typer, solana-py/solders.

Commands

# Install / sync
uv sync

# Run CLI (dev)
uv run pumpfun <command> [options]
uv run pumpfun --json <command>        # force JSON output

# Unit tests (always safe, no network)
uv run pytest tests/ -q
uv run pytest tests/ -v --cov          # with coverage
uv run pytest tests/test_core -v       # single module
uv run pytest tests/ -k test_name      # filter

# Surfpool integration (needs running surfpool daemon)
./scripts/surfpool-autodiscover.sh

# E2e mainnet (COSTS REAL SOL β€” always confirm with user first)
./scripts/mainnet-test.sh
./scripts/mainnet-test.sh --skip-trading

pytest is configured to auto-ignore tests/test_surfpool/ by default. Surfpool and mainnet tests require explicit invocation.

Architecture

Three-layer separation β€” never bypass layers:

commands/  β†’  core/  β†’  protocol/
  (thin)      (logic)    (Solana)
  • commands/ β€” Typer CLI wiring only. Parse args, call core, call render() or error(). All commands are async, bridged with asyncio.run().
  • core/ β€” Framework-free business logic. Accept primitives (rpc_url: str, password: str, amounts). Return dict with results or "error" key for expected failures.
  • protocol/ β€” Pure Solana/pump.fun code. PDAs, RPC client, instruction builders, curve math, pool parsing. Zero business logic.

Entry point: src/pumpfun_cli/cli.py β†’ pyproject.toml [project.scripts] pumpfun = "pumpfun_cli.cli:app"

Project Structure

src/pumpfun_cli/
β”œβ”€β”€ cli.py              # Root Typer app, GlobalState, callback
β”œβ”€β”€ crypto.py           # AES-256-GCM wallet encryption (scrypt KDF)
β”œβ”€β”€ output.py           # render() + error() β€” TTY-aware output
β”œβ”€β”€ commands/           # Thin CLI layer (config, info, launch, tokens, trade, tx_status, wallet)
β”œβ”€β”€ core/               # Business logic (config, info, launch, pumpswap, tokens, trade, tx_status, wallet)
└── protocol/           # Solana primitives (address, client, contracts, curve, idl_parser, instructions, pumpswap)

tests/
β”œβ”€β”€ test_commands/      # CLI smoke tests
β”œβ”€β”€ test_core/          # Mocked business logic tests
β”œβ”€β”€ test_protocol/      # Unit tests for protocol math/parsing
└── test_surfpool/      # Integration tests (ignored by default)

Code Conventions

Output: Use render(data, json_mode) for all output β€” auto-detects TTY (Rich table) vs pipe (JSON). Use error(msg, hint, exit_code) for failures β€” prints to stderr and raises SystemExit. Never use print().

Error handling: error() raises SystemExit β€” code after it is unreachable. Core functions return dict with "error" key for expected failures (graduated tokens, not found, slippage exceeded, insufficient_balance). Catch ValueError for wrong wallet password in every command that decrypts. Buy/sell functions perform pre-trade balance validation β€” SOL balance for buys (including fees + ATA rent), token balance for sells with specific amounts.

Imports: stdlib β†’ third-party β†’ local. Example:

import asyncio
from pathlib import Path

from solders.pubkey import Pubkey

from pumpfun_cli.core.config import resolve_value
from pumpfun_cli.output import render, error

Naming: snake_case functions/variables/files. UPPER_CASE constants. Prefix private helpers with _.

Type hints: Full type hints on all public function signatures.

Async: All I/O functions are async def. Commands bridge with asyncio.run(). RpcClient is stateless β€” always call .close() in finally.

Config resolution: resolve_value(key, env_var, flag) β€” flag > env var > config file > default.

Adding a New Command

  1. Add core/my_feature.py β€” async def my_operation(rpc_url, keystore_path, password, ...) -> dict
  2. Add commands/my_feature.py β€” Typer wrapper that calls core and renders output
  3. Register in cli.py with app.command("my-command")(my_feature_cmd)
  4. Add tests in tests/test_core/test_my_feature.py with mocked RPC/HTTP

Working with Solana Accounts

# Derive PDA
from pumpfun_cli.protocol.address import derive_bonding_curve

# Fetch and decode
data = await client.get_account_data(address)
state = idl.decode_account_data(data, "BondingCurve", skip_discriminator=True)

# Build instructions
from pumpfun_cli.protocol.instructions import build_buy_ix
ixs = build_buy_ix(...)
sig = await client.send_tx(ixs, [keypair], compute_units=..., confirm=True)

Environment

PUMPFUN_RPC=https://...     # Solana RPC endpoint (required for trading)
PUMPFUN_PASSWORD=...        # Wallet password (for non-interactive use)

Config file: ~/.config/pumpfun-cli/config.toml Wallet keystore: ~/.config/pumpfun-cli/wallet.enc

Test Fixtures

  • Test wallet pubkey: 2kPYzWkeJCiUEpo7yBNX7jYdwmyqXGrKsjetNJdHPfYz (password: testpass123)
  • Test token mint: 72xpy6cejkorDh8gx328CAW3Fq7uCQdCyXkSLAE5to5p (CLITEST, on bonding curve)

Hooks

Three hooks run automatically during Claude Code sessions (see .claude/settings.json):

  • guard.py (PreToolUse β†’ Write|Edit): Blocks edits to .env, wallet.enc, idl/, and credential files (.pem, .key, .secret). Edit these files manually.
  • lint.py (PostToolUse β†’ Write|Edit): Runs ruff format + ruff check --fix on every edited .py file. If unfixable errors remain, you will see them as errors β€” fix them before moving on.
  • bash-guard.py (PreToolUse β†’ Bash): Advisory only β€” prints warnings (never blocks) when commands send Solana transactions, run mainnet tests, or delete sensitive files. The CLI must work unimpeded when run via assistants or automated workflows.

Boundaries

Always do:

  • Run uv run pytest tests/ -q after code changes
  • Follow the three-layer separation
  • Use render() and error() for all CLI output
  • Catch ValueError on wallet decryption
  • Close RpcClient in finally blocks

Ask first:

  • Running mainnet e2e tests (costs real SOL)
  • Modifying protocol/contracts.py constants (program IDs, discriminators)
  • Changing wallet encryption format in crypto.py

Never do:

  • Bypass layers (commands must not import from protocol directly)
  • Use print() instead of render()/error()
  • Commit .env or wallet files
  • Run mainnet tests without explicit user confirmation
  • Add dependencies without discussing first