Skip to content

Conversation

@smypmsa
Copy link
Member

@smypmsa smypmsa commented Nov 15, 2025

Implements updates recently introduced into IDL of PF bonding curve and AMM.

Summary by CodeRabbit

  • New Features

    • Mayhem Mode: enable/disable special token behavior and alternate fee routing.
    • Token2022 support: buy/sell and discovery now handle Token2022 tokens.
    • Creator tracking: creator shown for created tokens.
    • Volume tracking: trade volume is recorded and reported.
    • Dynamic fee recipients: fee destination selected based on token/mode.
  • Bug Fixes / UX

    • Improved token discovery and clearer creation/sale output.

@smypmsa smypmsa self-assigned this Nov 15, 2025
@smypmsa smypmsa added the enhancement New feature or request label Nov 15, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 15, 2025

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main changes: adding mayhem mode support and Token2022 integration, which are the primary objectives reflected throughout the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mayhem-update

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.

@smypmsa smypmsa linked an issue Nov 15, 2025 that may be closed by this pull request
@smypmsa smypmsa marked this pull request as ready for review November 18, 2025 11:19
Copy link
Contributor

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
learning-examples/listen-migrations/listen_logsubscribe.py (4)

97-102: Add type hints and complete docstring to is_transaction_successful.

Function lacks type hints and Google-style docstring documentation.

-def is_transaction_successful(logs):
+def is_transaction_successful(logs: list[str]) -> bool:
+    """Check if a transaction succeeded by examining log messages.
+
+    Args:
+        logs: List of log messages from the transaction.
+
+    Returns:
+        True if transaction succeeded, False if errors found in logs.
+    """
     for log in logs:
         if "AnchorError thrown" in log or "Error" in log:
-            print(f"[ERROR] Transaction failed: {log}")
+            logger.error(f"Transaction failed: {log}")
             return False
     return True

105-123: Add type hints and complete docstring to print_transaction_details; use logger.

Function lacks type hints, incomplete docstring, and uses print() instead of logger.

-def print_transaction_details(log_data):
+def print_transaction_details(log_data: dict) -> None:
+    """Parse and log migration transaction details.
+
+    Args:
+        log_data: Transaction log data containing "logs" key with log messages.
+    """
     logs = log_data.get("logs", [])
     parsed_data = {}
 
     for log in logs:
         if log.startswith("Program data:"):
             try:
                 data = base64.b64decode(log.split(": ")[1])
                 parsed_data = parse_migrate_instruction(data)
                 if parsed_data:
-                    print("[INFO] Parsed from Program data:")
+                    logger.info("Parsed from Program data:")
                     for key, value in parsed_data.items():
-                        print(f"  {key}: {value}")
+                        logger.info(f"  {key}: {value}")
             except Exception as e:
-                print(f"[ERROR] Failed to decode Program data: {e}")
+                logger.exception("Failed to decode Program data")
 
     if not parsed_data:
-        print("[ERROR] Failed to parse migration data: parsed data is empty")
+        logger.error("Failed to parse migration data: parsed data is empty")

125-194: Add type hints to listen_for_migrations and replace all print() with logger.

The main async function lacks return type hint and systematically uses print() instead of logging.

-async def listen_for_migrations():
+async def listen_for_migrations() -> None:
     while True:
         try:
-            print("\n[INFO] Connecting to WebSocket ...")
+            logger.info("Connecting to WebSocket ...")
             async with websockets.connect(WSS_ENDPOINT) as websocket:
                 subscription_message = json.dumps(
                     {
                         "jsonrpc": "2.0",
                         "id": 1,
                         "method": "logsSubscribe",
                         "params": [
                             {"mentions": [str(MIGRATION_PROGRAM_ID)]},
                             {"commitment": "processed"},
                         ],
                     }
                 )
                 await websocket.send(subscription_message)
-                print(
-                    f"[INFO] Listening for migration instructions from program: {MIGRATION_PROGRAM_ID}"
-                )
+                logger.info(
+                    f"Listening for migration instructions from program: {MIGRATION_PROGRAM_ID}"
+                )
 
                 response = await websocket.recv()
-                print(f"[INFO] Subscription response: {response}")
+                logger.info(f"Subscription response: {response}")
 
                 while True:
                     try:
                         response = await asyncio.wait_for(websocket.recv(), timeout=60)
                         data = json.loads(response)
 
                         if "method" in data and data["method"] == "logsNotification":
                             log_data = data["params"]["result"]["value"]
                             logs = log_data.get("logs", [])
 
                             signature = log_data.get("signature", "N/A")
-                            print(f"\n[INFO] Transaction signature: {signature}")
+                            logger.info(f"Transaction signature: {signature}")
 
                             if is_transaction_successful(logs):
                                 if not any(
                                     "Program log: Instruction: Migrate" in log
                                     for log in logs
                                 ):
-                                    print("[INFO] Skipping: no migrate instruction")
+                                    logger.info("Skipping: no migrate instruction")
                                     continue
 
                                 if any(
                                     "Program log: Bonding curve already migrated" in log
                                     for log in logs
                                 ):
-                                    print(
+                                    logger.info(
                                         "[INFO] Skipping: bonding curve already migrated"
                                     )
                                     continue
 
-                                print("[INFO] Processing migration instruction...")
+                                logger.info("Processing migration instruction...")
                                 print_transaction_details(log_data)
                             else:
-                                print("[INFO] Skipping failed transaction.")
+                                logger.info("Skipping failed transaction.")
                     except TimeoutError:
-                        print(
+                        logger.warning(
                             "[INFO] Timeout waiting for WebSocket message, retrying..."
                         )
                     except Exception as e:
-                        print(f"[ERROR] An error occurred: {e}")
+                        logger.exception("An error occurred")
                         break
 
         except Exception as e:
-            print(f"[ERROR] Connection error: {e}")
-            print("[INFO] Reconnecting in 5 seconds...")
+            logger.exception("Connection error")
+            logger.info("Reconnecting in 5 seconds...")
             await asyncio.sleep(5)

Note: When using logger.exception(), do not include the exception in the message—it's automatically appended. Per guidelines: "Log exceptions with context using logging.exception()".


1-50: Add type hints to all public functions, use centralized logger, and complete Google-style docstrings.

This learning example must follow the same coding standards as production code per guidelines. Verification confirms systematic violations across all 4 public functions:

  1. Missing type hints on parse_migrate_instruction, is_transaction_successful, print_transaction_details, and listen_for_migrations
  2. 19 print() calls instead of centralized logger—replace with module-level logger via from src.utils.logger import get_logger
  3. Incomplete docstrings—add Args: and Returns: sections in Google style

Apply this diff:

+import logging
+
 import asyncio
 import base64
 import json
 import os
 import struct
 
 import base58
 import websockets
 from dotenv import load_dotenv
 from solders.pubkey import Pubkey
 
+from src.utils.logger import get_logger
+
 load_dotenv()
 
+logger = get_logger(__name__)
+
 WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
 MIGRATION_PROGRAM_ID = Pubkey.from_string(
     "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
 )
 
 
-def parse_migrate_instruction(data):
+def parse_migrate_instruction(data: bytes) -> dict | None:
     """Parse migration event from the migration wrapper program.
 
+    Args:
+        data: Binary event data from the migration wrapper program.
+
+    Returns:
+        Parsed event data as a dictionary, or None if parsing fails.
+
     Note: This parses the event emitted by the migration wrapper program
     (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different
     structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json.
     """
     if len(data) < 8:
-        print(f"[ERROR] Data length too short: {len(data)} bytes")
+        logger.error(f"Data length too short: {len(data)} bytes")
         return None

Then replace all 19 print() calls with appropriate logger levels:

  • print(f"[ERROR]...")logger.error(...)
  • print(f"[INFO]...")logger.info(...)

Add type hints to remaining functions: is_transaction_successful(logs: list) -> bool, print_transaction_details(log_data: dict) -> None, and listen_for_migrations() -> None.

learning-examples/listen-new-tokens/listen_pumpportal.py (1)

99-102: Use proper logging instead of print statements for errors.

The error handling is a good addition, but should use the centralized logger instead of print statements for consistency with project standards.

First, add the import at the top of the file:

from src.utils.logger import get_logger

logger = get_logger(__name__)

Then apply this diff to use proper logging:

             except json.JSONDecodeError:
-                print(f"\nReceived non-JSON message: {message}")
+                logger.warning("Received non-JSON message: %s", message)
             except Exception as e:
-                print(f"\nAn error occurred: {e}")
+                logger.exception("Error processing WebSocket message")

Similarly, update the error handling in the main() function (lines 109-112) and connection closed message (line 97) to use logging.

As per coding guidelines.

src/platforms/pumpfun/curve_manager.py (1)

304-342: validate_curve_state_structure incorrectly calls async get_pool_state without awaiting

At Line 316, pool_state = self.get_pool_state(pool_address) assigns the coroutine object, not the decoded dict, because get_pool_state is async. The subsequent checks (if field not in pool_state, isinstance(pool_state[field], int)) will fail at runtime.

Consider updating this method to be async and await the call, or otherwise running the coroutine in a controlled way consistent with how CurveManager is defined:

-    def validate_curve_state_structure(self, pool_address: Pubkey) -> bool:
+    async def validate_curve_state_structure(self, pool_address: Pubkey) -> bool:
@@
-            pool_state = self.get_pool_state(pool_address)
+            pool_state = await self.get_pool_state(pool_address)

If CurveManager.validate_curve_state_structure is currently synchronous in the interface, you’ll need to adjust the interface and call sites accordingly or explicitly execute the coroutine here using your event-loop strategy instead of calling it directly.

learning-examples/copytrading/listen_wallet_transactions.py (1)

124-258: Progressive TradeEvent parsing looks good; harden ix_name decoding and tidy long lines

The progressive parsing and grouping of extended fields looks consistent with the documented layout: core fields are length‑guarded, extended blocks are only parsed when enough bytes remain, and sensible defaults maintain backward compatibility. That part looks solid.

One robustness gap: decoding ix_name can raise UnicodeDecodeError if the data are malformed or not valid UTF‑8. Because the base64/log payload is fully external, this would currently bubble up and kill the listener instead of just omitting the name. Consider catching this locally and falling back to an empty string:

-    # Parse string field (ix_name): 4 bytes for length + string data
-    if len(data) >= offset + 4:
-        string_length = struct.unpack("<I", data[offset : offset + 4])[0]
-        offset += 4
-        if len(data) >= offset + string_length:
-            ix_name = data[offset : offset + string_length].decode("utf-8")
-        else:
-            ix_name = ""
-    else:
-        ix_name = ""
+    # Parse string field (ix_name): 4 bytes for length + string data
+    if len(data) >= offset + 4:
+        string_length = struct.unpack("<I", data[offset : offset + 4])[0]
+        offset += 4
+        if len(data) >= offset + string_length:
+            try:
+                ix_name = data[offset : offset + string_length].decode("utf-8")
+            except UnicodeDecodeError:
+                ix_name = ""
+        else:
+            ix_name = ""
+    else:
+        ix_name = ""

Also, a couple of the new dict entries are well over the 88‑char limit and use single quotes. You can keep things Ruff‑friendly and consistent with the rest of the file by switching to double quotes and wrapping with parentheses, e.g.:

-        "fee_recipient": base58.b58encode(fee_recipient).decode() if fee_recipient != b'\x00' * 32 else None,
+        "fee_recipient": (
+            base58.b58encode(fee_recipient).decode()
+            if fee_recipient != b"\x00" * 32
+            else None
+        ),
@@
-        "creator": base58.b58encode(creator).decode() if creator != b'\x00' * 32 else None,
+        "creator": (
+            base58.b58encode(creator).decode()
+            if creator != b"\x00" * 32
+            else None
+        ),
learning-examples/manual_buy_geyser.py (1)

341-373: Update track_volume encoding in manual_buy_geyser.py and manual_buy_cu_optimized.py to use OptionBool format

Verification confirms the encoding mismatch. The current manual_buy_geyser.py (line 431) and manual_buy_cu_optimized.py (line 331) encode track_volume as a single byte using struct.pack("<B", 1), producing 0x01. However, manual_buy.py, mint_and_buy.py, mint_and_buy_v2.py, and manual_sell.py all encode it as OptionBool (two bytes [1, 1]), with explicit documentation: [0] = None, [1, 0] = Some(false), [1, 1] = Some(true).

Since all files target the same PUMP_PROGRAM and share the same buy instruction discriminator, this encoding mismatch will cause instruction data to be mis-parsed, leading to transaction rejections on-chain.

Fix:

  • Replace struct.pack("<B", 1) with bytes([1, 1]) in both files
  • Add the OptionBool documentation comment for clarity
🧹 Nitpick comments (56)
learning-examples/listen-migrations/listen_programsubscribe.py (1)

60-111: Consider adding type hints and structured logging as per coding guidelines.

The parsing logic for the new is_mayhem_mode field is correct. However, the function could be improved to align with the project's coding guidelines.

As per coding guidelines, consider these improvements:

+from typing import Any
+from src.utils.logger import get_logger
+
+logger = get_logger(__name__)
+
-def parse_market_account_data(data):
-    """Parse Pool account data according to pump_swap_idl.json structure.
+def parse_market_account_data(data: bytes) -> dict[str, Any]:
+    """Parse Pool account data according to pump_swap_idl.json structure.
+
+    Args:
+        data: Raw binary account data from Solana RPC.
 
-    Total 11 fields including the new is_mayhem_mode field added with mayhem update.
-    """
+    Returns:
+        Dictionary containing parsed pool account fields including:
+        pool_bump, index, creator, base_mint, quote_mint, lp_mint,
+        pool_base_token_account, pool_quote_token_account, lp_supply,
+        coin_creator, and is_mayhem_mode.
+    """
     parsed_data = {}
     offset = 8  # Discriminator
     
     # ... fields definition ...
     
     try:
         # ... parsing logic ...
     except Exception as e:
-        print(f"[ERROR] Failed to parse market data: {e}")
+        logger.exception("Failed to parse market data")
     
     return parsed_data

Note: The import path src.utils.logger should be adjusted if this learning example has a different module structure.

learning-examples/listen-migrations/compare_migration_listeners.py (1)

315-367: Consider extracting shared parsing logic to reduce duplication.

The parse_market_account_data function is duplicated from listen_programsubscribe.py. While this is acceptable for learning examples, consider extracting it to a shared utility module for easier maintenance.

Additionally, apply the same type hints and logging improvements suggested for listen_programsubscribe.py:

+from typing import Any
+from src.utils.logger import get_logger
+
+logger = get_logger(__name__)
+
-def parse_market_account_data(data):
+def parse_market_account_data(data: bytes) -> dict[str, Any]:
     """
     Parse binary Pool account data according to pump_swap_idl.json structure
+    
+    Args:
+        data: Raw binary account data from Solana RPC.
+    
+    Returns:
+        Dictionary containing parsed pool account fields.
+    """
learning-examples/copytrading/listen_wallet_transactions.py (1)

276-305: Extended display logic is correct and improves event context; minor style cleanups

The new display logic correctly uses ix_name, fee/creator fields, and real vs virtual reserves from decode_trade_event, which gives much richer context for each trade and aligns well with the “log significant trading events with context” guidance. Based on learnings.

Minor polish to consider:

  • Use double quotes consistently and avoid nested f‑strings to keep things readable and formatter‑friendly.
  • Split the long print lines to stay within 88 chars.

For example:

-        ix_name = trade_data.get('ix_name', '')
-        trade_type = 'BUY' if trade_data['is_buy'] else 'SELL'
-        print(f"  Type: {trade_type}{f' ({ix_name})' if ix_name else ''}")
+        ix_name = trade_data.get("ix_name", "")
+        trade_type = "BUY" if trade_data["is_buy"] else "SELL"
+        ix_suffix = f" ({ix_name})" if ix_name else ""
+        print(f"  Type: {trade_type}{ix_suffix}")
@@
-        if trade_data['fee'] > 0 or trade_data['fee_basis_points'] > 0:
-            print(f"  Fee: {trade_data['fee'] / 1_000_000_000:.6f} SOL ({trade_data['fee_basis_points']} bps)")
+        if trade_data["fee"] > 0 or trade_data["fee_basis_points"] > 0:
+            fee_sol = trade_data["fee"] / 1_000_000_000
+            print(
+                f"  Fee: {fee_sol:.6f} SOL "
+                f"({trade_data['fee_basis_points']} bps)"
+            )
@@
-        print(f"  Virtual Reserves: {trade_data['virtual_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['virtual_token_reserves']:,} tokens")
+        virt_sol = trade_data["virtual_sol_reserves"] / 1_000_000_000
+        virt_tokens = trade_data["virtual_token_reserves"]
+        print(f"  Virtual Reserves: {virt_sol:.6f} SOL / {virt_tokens:,} tokens")

These are non‑blocking, but will make the example script better aligned with the repo’s Python style.

learning-examples/listen-new-tokens/compare_listeners.py (3)

261-305: Create/CreateV2 instruction decoders look correct; Ruff warnings are mostly noise here

The new decode_create_instruction correctly:

  • Skips the 8‑byte discriminator.
  • Reads name, symbol, uri as length‑prefixed UTF‑8 strings and a 32‑byte creator pubkey.
  • Maps account indices (mint, bondingCurve, user) consistently with the IDL comments used elsewhere.
  • Tags token_standard="legacy" and is_mayhem_mode=False, which the static analysis “password” warning (S105) is incorrectly flagging.

For this diagnostic script, the broad except Exception with a simple error print is a reasonable trade‑off, even though Ruff (BLE001/TRY300) prefers more specific exceptions. If you want to satisfy Ruff, you could narrow the except to (struct.error, UnicodeDecodeError, IndexError, ValueError) and keep the behavior the same.


307-355: CreateV2 decoder matches the Token2022 + mayhem layout

decode_create_v2_instruction mirrors the legacy decoder, with the additions you need:

  • Reads the same string triplet, plus a 32‑byte creator pubkey.
  • Parses is_mayhem_mode from the trailing flag byte.
  • Uses account indices [0]=mint, [2]=bondingCurve, [5]=user consistent with other pump.fun decoders.
  • Tags token_standard="token2022".

Same note as above: the broad except Exception is acceptable for a learning tool, but if you want Ruff‑clean code you can tighten the exception list.


777-858: Geyser gRPC listener’s dual‑path decoding is consistent with the other listeners

In the Geyser path:

  • is_create / is_create_v2 checks mirror the discriminator logic used in the block listener.
  • Account keys are extracted via msg.account_keys and base58‑encoded, with bounds checks.
  • The same decoders are used (decode_create_instruction / decode_create_v2_instruction), and the token is de‑duplicated via known_tokens.

You might optionally consider using decoded.get("mint") instead of assuming account_keys[0] is always the mint, but given the pump.fun IDL and the other code in this repo, the current assumption is consistent.

src/platforms/pumpfun/pumpportal_processor.py (1)

8-11: PumpPortal processor now correctly tags Token2022 program usage

This update ties the PumpPortal path into the new token‑program‑aware model:

  • Using SystemAddresses.TOKEN_2022_PROGRAM as token_program_id matches pump.fun’s CreateV2 default and the rest of the codebase.
  • Passing token_program_id into derive_associated_bonding_curve ensures the ATA derivation matches the actual token program.
  • Returning token_program_id in TokenInfo lets downstream trading/cleanup flows use the correct SPL program.

Given PumpPortal doesn’t expose token standard yet, the “assume Token2022” comment is a reasonable documented trade‑off. If PumpPortal later adds a token‑standard field, it would be straightforward to branch here to support legacy tokens explicitly.

Also applies to: 84-90, 91-110

learning-examples/decode_from_getTransaction.py (1)

29-46: Create/CreateV2 decoding and mayhem metadata are wired correctly for this example

Unpacking three strings, skipping the 8‑byte discriminator and 32‑byte creator, and then reading a trailing byte as is_mayhem_mode is consistent between legacy and CreateV2 paths, and the added token_standard/is_mayhem_mode keys make downstream inspection much clearer.

Since this script is decoding externally sourced transaction data, consider adding minimal length checks (or try/except around struct.unpack_from) to avoid uncaught struct.error on truncated payloads, even in a learning‑example context.

Also applies to: 49-75

src/trading/universal_trader.py (1)

194-201: Token program tracking for cleanup is correct; wrap long lines and handle None safely

Using self.traded_token_programs: dict[str, Pubkey] keyed by str(mint) and then building mints_list plus a parallel token_program_ids list in _cleanup_resources() is a reasonable way to feed handle_cleanup_post_session() with per‑mint program context.

Two small follow‑ups:

  1. Line length/style – The self.traded_token_programs declaration and the list comprehension for token_program_ids are long enough that they likely exceed the 88‑character limit. You can keep them functionally identical while satisfying style by splitting, e.g.:
-        self.traded_token_programs: dict[str, Pubkey] = {}  # Maps mint (as string) to token_program_id
+        # Maps mint (as string) to token_program_id
+        self.traded_token_programs: dict[str, Pubkey] = {}
...
-                token_program_ids = [
-                    self.traded_token_programs.get(str(mint))
-                    for mint in mints_list
-                ]
+                token_program_ids = [
+                    self.traded_token_programs.get(str(mint))
+                    for mint in mints_list
+                ]
  1. None token_program_id – If any TokenInfo arrives without token_program_id set, self.traded_token_programs.get(str(mint)) will produce None. Please confirm that AccountCleanupManager.cleanup_ata() (and the post‑session cleanup mode you use) can safely handle a None program ID or apply a sensible default (e.g., legacy TOKEN vs TOKEN2022).

Also applies to: 324-344

src/platforms/pumpfun/instruction_builder.py (1)

154-162: Consider de-duplicating track_volume_bytes constant

The track_volume_bytes = bytes([1, 1]) literal is duplicated in both buy and sell builders. It’s small, but factoring it into a module-level constant would make the OptionBool encoding explicit and reduce drift if the layout ever changes.

For example:

+TRACK_VOLUME_SOME_TRUE = bytes([1, 1])
+
 ...
-        track_volume_bytes = bytes([1, 1])
+        track_volume_bytes = TRACK_VOLUME_SOME_TRUE
 ...
-        track_volume_bytes = bytes([1, 1])
+        track_volume_bytes = TRACK_VOLUME_SOME_TRUE

Pure readability/maintainability improvement; behavior stays the same.

Also applies to: 256-264

learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py (2)

20-21: Hardcoded TOKEN_MINT is fine for example usage, but consider documenting it

Using a concrete mint here makes the script runnable out of the box, which is nice. If you expect people to point this at arbitrary tokens, consider either:

  • Reading TOKEN_MINT from env/CLI, or
  • Explicitly mentioning in the module docstring that this is an example mint and should be replaced.

No functional issue as-is.


84-107: Optional: avoid magic offsets when parsing creator / mayhem fields

The parsing logic is correct given the current layout:

  • Base struct: <QQQQQ?> = 41 bytes after the 8-byte discriminator.
  • Creator occupies bytes [49:81] (8 + 41 .. +32).
  • is_mayhem_mode lives at index 81 when present.

The explicit constants (49, 81, 73, 74) are a bit magical, though. For readability and future-proofing, consider deriving them from struct.calcsize and the running offset instead of hard-coding:

base_size = struct.calcsize("<QQQQQ?")
base_offset = 8 + base_size  # 49
...
if len(data) >= 8 + base_size + 32:
    creator_bytes = data[base_offset : base_offset + 32]
    ...
if len(data) >= 8 + base_size + 32 + 1:
    result["is_mayhem_mode"] = bool(data[base_offset + 32])

This keeps the implementation self-documenting if the base struct ever changes.

src/cleanup/modes.py (2)

19-35: Verify token_program_id propagation and list alignment for post-session cleanup

Plumbing token_program_id into all three cleanup helpers and down into AccountCleanupManager.cleanup_ata is the right move for TOKEN vs TOKEN_2022 awareness.

Two things to double‑check:

  1. Call sites: Ensure all existing callers of these helpers are updated to pass the new token_program_id (or list thereof), otherwise this will be a breaking change.
  2. Length mismatches in handle_cleanup_post_session: Using zip(mints, token_program_ids) will silently ignore extra items if the lists differ in length. If that’s undesirable, consider either asserting equality or logging a warning when they don’t match.

Example:

if len(mints) != len(token_program_ids):
    logger.warning(
        "Mismatched mints (%d) and token_program_ids (%d); extra entries will be ignored",
        len(mints),
        len(token_program_ids),
    )
for mint, token_program_id in zip(mints, token_program_ids):
    await manager.cleanup_ata(mint, token_program_id)

Behavior is otherwise sound.

Also applies to: 37-53, 55-71


19-28: Consider adding type hints and docstrings to cleanup entrypoints

These async helpers are part of the public cleanup flow and now gained new parameters (token_program_id, token_program_ids). To stay aligned with the project-wide guidelines (type hints + Google-style docstrings), it would be good to:

  • Add parameter/return type annotations, and
  • Document the semantics of cleanup_mode, cleanup_with_prior_fee, force_burn, and the meaning of token_program_id.

This will make future changes around TOKEN vs TOKEN_2022 migrations less error-prone.

Also applies to: 37-46, 55-64

learning-examples/decode_from_getAccountInfo.py (1)

14-23: _STRUCT is now unused – clarify intent or remove

The _STRUCT definition with construct.Struct is no longer used for parsing since __init__ switched to manual byte offsets. If it’s intended purely as documentation of the layout, consider:

  • Adding a comment to that effect, or
  • Removing _STRUCT and the construct imports to avoid an unnecessary runtime dependency.

This is a small clean-up, but it’ll reduce confusion for readers wondering why construct is imported here.

learning-examples/listen-new-tokens/listen_logsubscribe.py (3)

34-71: Event discriminator and print helper integration look good

Defining CREATE_EVENT_DISCRIMINATOR and adding print_token_info gives this listener the same “new token detected” UX as the blockSubscribe variant and ensures you don’t accidentally treat non-Create events as tokens.

If you find yourself tweaking the output format often, consider centralizing print_token_info in a shared helper module to avoid divergence between listeners, but it’s fine as-is for learning examples.


74-141: Create / CreateV2 parsers are robust; could be DRY’d slightly

Both parsers:

  • Validate minimum length,
  • Parse length-prefixed UTF‑8 strings with explicit boundary checks,
  • Parse 32‑byte pubkeys safely, and
  • Provide helpful context (offset, data length, hex preview) on error.

The logic is sound and matches the listen_logsubscribe_abc reference. If you want to trim duplication, you could extract the common loop into a small helper:

def _parse_event_fields(data, offset, fields):
    parsed = {}
    for field_name, field_type in fields:
        ...
    return parsed, offset

Then both parse_create_instruction and parse_create_v2_instruction can reuse it, differing only in the extra is_mayhem_mode handling and token_standard value.

Not a blocker; the current version is already quite defensive.

Also applies to: 143-211


247-299: Core logs loop logic is solid; small Ruff cleanup opportunity

The main logs loop correctly:

  • Detects both Create and CreateV2 instructions,
  • Filters program logs to Program data: entries,
  • Guards on CREATE_EVENT_DISCRIMINATOR to avoid mis-parsing other events, and
  • Delegates to the appropriate parser before printing via print_token_info.

Two minor polish points:

  1. Ruff F541: This line doesn’t need an f-string:
-                                            print(f"⚠️  Parsing failed for CreateEvent")
+                                            print("⚠️  Parsing failed for CreateEvent")
  1. Exception granularity (BLE001): You’re catching bare Exception in a couple of places. For a learning/integration script that must never crash, this is understandable, but if you want to appease Ruff you could narrow it to the expected failure types (ValueError, KeyError, base64.binascii.Error, json.JSONDecodeError, etc.) in the inner loops and leave a broad catch only at the outermost level.

Functionality-wise, this block looks good; I’d just fix the F541 warning.

src/platforms/pumpfun/address_provider.py (1)

34-39: MAYHEM_FEE constant looks good; consider simplifying the container class

The hardcoded MAYHEM_FEE with an inline note about offset 483 on the Global account is clear and matches the manual parsing approach used elsewhere. Given Ruff’s RUF009 about function calls in dataclass defaults and the fact that PumpFunAddresses is essentially a namespace of constants, you might consider converting it to a plain class (or module-level constants) instead of a @dataclass to avoid the dataclass-default semantics. This is optional and mostly about style/tooling noise.

learning-examples/listen-new-tokens/listen_logsubscribe_abc.py (1)

112-156: Robust Create/CreateV2 parsing with good diagnostics; minor nits

The updated parse_create_instruction / parse_create_v2_instruction add useful safety checks (length guards for strings and pubkeys) and detailed diagnostics on failure, which is exactly what you want in a debugging/learning script.

Two small optional tweaks:

  • The bare except Exception blocks are fine for an example script, but if you ever promote this logic into production code, narrowing to ValueError (and maybe UnicodeDecodeError) would avoid swallowing unexpected issues.
  • print(f"⚠️ Parsing failed for CreateEvent") doesn’t interpolate anything; you can drop the f prefix to satisfy Ruff’s F541 without changing behavior.

Otherwise, the mayhem flag handling and token_standard defaults look consistent with the rest of the repo.

Also applies to: 159-209

learning-examples/listen-new-tokens/listen_blocksubscribe.py (2)

128-176: Create/CreateV2 decoders match IDL layout and handle mayhem mode cleanly

Both decoders:

  • Use the IDL’s args to parse string and pubkey fields, matching the patterns in other modules.
  • Map account indices to mint, bondingCurve, associatedBondingCurve, and user in line with the documented layouts.
  • Set token_standard and is_mayhem_mode consistently with the rest of the codebase (legacy = no mayhem; V2 reads the optional trailing byte).

Optional robustness tweak: if you want to hard-fail on future IDL changes, you could assert the expected number of args or account indices; but for a learning script this is fine.

Also applies to: 178-234


237-247: Block listener enhancements for CreateV2 and ALTs are well-structured; consider printing signatures

The listen_and_decode_create loop now:

  • Extracts loadedAddresses where available and passes them into get_account_keys.
  • Uses discriminators to branch between legacy Create and CreateV2.
  • Picks createV2 from the IDL with a sane fallback to create if needed.
  • Skips transactions with unresolved accounts and logs ALT usage counts.

This is a solid and readable flow. One improvement to consider, to stay consistent with the log/geyser listeners, is to extract the transaction signature and pass it into print_token_info so users can quickly click through to the transaction when comparing listener behavior.

You might also add a quick if len(ix_data) < 8: continue guard before struct.unpack("<Q", ix_data[:8]) as cheap protection against any unexpected short data.

Also applies to: 290-368

learning-examples/fetch_price.py (1)

6-7: Versioned BondingCurveState parsing is well thought out

The split into _STRUCT_V1/_STRUCT_V2/_STRUCT_V3 with version auto-detection via len(data) - 8 matches the expected layout:

  • V1: base reserves + supply + complete.
  • V2: adds 32-byte creator.
  • V3: adds is_mayhem_mode.

Setting creator = None and is_mayhem_mode = False for V1 preserves backward compatibility, while the bytes→Pubkey conversion for V2/V3 gives a nicer API to callers. Pulling RPC_ENDPOINT from the environment fits the example’s configuration story.

If you ever expect a V4 layout, you might want to explicitly assert data_length in {41, 73, 74} and raise on anything else, so parse failures surface as a clear “unsupported version” instead of a generic construct error. For now, this is perfectly reasonable for a learning script.

Also applies to: 12-18, 20-73

learning-examples/bonding-curve-progress/get_bonding_curve_status.py (2)

42-66: Versioned BondingCurveState parsing looks sound; consider aligning with progressive parser.

The V2/V3 layouts, size comments, and discriminator checks look correct, and the is_mayhem_mode defaulting to False for V2 keeps older accounts safe. The explicit length checks and descriptive ValueError for unexpected sizes are also good.

Given you already have a progressive parser pattern in learning-examples/manual_buy.py that uses a base struct plus “bytes remaining” logic, it might be worth unifying on that approach here to avoid future drift if the on-chain BondingCurve struct gains more fields. That would also remove the need for hard-coded total lengths.

Also applies to: 73-90


92-103: PDA helper is correct; consider centralizing the bonding-curve derivation.

get_bonding_curve_address correctly returns (Pubkey, bump) and matches the other helpers shown in the relevant snippets. At this point you have several variants of this helper across learning examples; factoring it into a shared utility (or at least keeping signature/return-type consistent everywhere) would reduce duplication and potential mismatches between scripts.

learning-examples/manual_sell.py (5)

46-61: Progressive BondingCurveState parser matches other scripts; consider de-duplicating.

The base-struct + offset-based parsing for creator and is_mayhem_mode is a nice, forward-compatible improvement over fixed-size layouts and matches the pattern used in learning-examples/manual_buy.py.

Given this logic now exists in multiple learning examples, consider extracting a shared BondingCurveState helper (or a small parsing function) to avoid future divergence between buy/sell scripts when the on-chain layout changes.

Also applies to: 63-99


120-131: Associated bonding-curve ATA derivation correctly parameterizes token program.

Adding token_program_id into find_associated_bonding_curve and using it in the PDA seeds matches the Token2022-aware ATA layout and aligns with the other snippets you provided.

Just make sure all callers (e.g., other learning scripts like mint_and_buy.py) are updated to supply the correct token program so you don’t accidentally derive the old ATA for Token2022 mints.


150-195: Mayhem-aware fee recipient logic is reasonable but relies on a fragile fixed offset.

get_fee_recipient correctly falls back to PUMP_FEE when mayhem mode is off or when the Global account can’t be fetched/parsed, which is a good safety net.

The hard-coded RESERVED_FEE_RECIPIENT_OFFSET = 483, though well-documented, is inherently brittle: any change to the Global layout in the pump.fun IDL will silently break this parser. If possible, consider one of:

  • Deriving the offset from an IDL-driven decoder (similar to how you’re handling BondingCurve), or
  • Centralizing this offset in a single module shared by all scripts that parse Global, so it only needs updating in one place.

Given this is a learning example, it’s acceptable, but worth tightening over time.


213-229: Token program detection is straightforward; consider handling RPC failures explicitly.

get_token_program_id correctly inspects the mint’s owner and distinguishes between the legacy token program and Token2022, raising a clear ValueError for unknown owners.

To make failures easier to debug, you might want to:

  • Wrap the get_account_info call in a small try/except that logs network/RPC errors separately from “unknown owner” cases, and
  • Potentially surface a more actionable message if the account exists but is owned by an unexpected program.

Not strictly required for a learning script but would improve diagnosability.


359-367: Main flow correctly resolves token program and threads it through the sell path.

Resolving token_program_id once, deriving the associated bonding-curve ATA with it, and passing it through to sell_token is clean and avoids duplicated RPC calls.

As a final safety check, you may want to log/print which token program was selected (standard vs Token2022) before executing the sale so that misconfigurations are obvious at run time.

Also applies to: 379-385

src/platforms/pumpfun/event_parser.py (2)

50-58: create_v2 discriminator wiring is good; consider exposing it via get_instruction_discriminators.

The optional initialization of _create_v2_instruction_discriminator_bytes and _create_v2_instruction_discriminator is clean and guarded for older IDLs.

However, get_instruction_discriminators() still returns only [self._create_instruction_discriminator_bytes]. If anything relies on that list to pre-filter instructions (e.g., Geyser/block listeners), it will ignore pure create_v2 instructions. It would be safer to include the v2 discriminator when available:

-    def get_instruction_discriminators(self) -> list[bytes]:
+    def get_instruction_discriminators(self) -> list[bytes]:
         """Get instruction discriminators for token creation.
@@
-        return [self._create_instruction_discriminator_bytes]
+        discriminators = [self._create_instruction_discriminator_bytes]
+        if self._create_v2_instruction_discriminator_bytes:
+            discriminators.append(self._create_v2_instruction_discriminator_bytes)
+        return discriminators

Also applies to: 69-72


595-610: New helpers are fine placeholders; tighten unused args and exception handling.

  • _derive_associated_bonding_curve’s optional token_program_id defaulting to SystemAddresses.TOKEN_PROGRAM keeps call sites simple while supporting Token2022.
  • _parse_bonding_curve_state and _get_is_mayhem_mode_from_curve are intentionally conservative, but:
    • _parse_bonding_curve_state catches a bare Exception; consider narrowing this or at least logging at info or warning when verbose mode is enabled so decoding issues are more visible.
    • _get_is_mayhem_mode_from_curve doesn’t use bonding_curve_address; renaming the parameter to _bonding_curve_address would make the “unused by design” intent clear and satisfy linters.

These are minor polish items and don’t affect current behavior.

Also applies to: 621-639, 641-658

learning-examples/manual_buy_cu_optimized.py (3)

72-88: Progressive BondingCurveState parsing is consistent; consider sharing implementation.

This BondingCurveState is the same progressive parser pattern as in other scripts (base struct + optional creator + optional is_mayhem_mode), which is good for forward compatibility and for powering mayhem-aware features.

Given the same logic now exists in multiple files (manual_buy.py, manual_sell.py, this script), a shared helper (even under learning-examples/) would reduce duplication and ensure any future layout change is reflected everywhere at once.

Also applies to: 89-126


183-228: Mayhem-mode fee recipient logic matches manual_sell; same brittleness caveat on the offset.

get_fee_recipient here mirrors the implementation in manual_sell.py, including the 483-byte offset into the Global account. The behavior and fallbacks are sensible, but the same concern applies: this offset is tightly coupled to the on-chain struct layout.

If you find yourself updating this offset in more than one place, consider centralizing it or deriving it via an IDL-based parser so you don’t have to maintain a magic constant in multiple scripts.


413-419: create vs create_v2 detection and token_program propagation are well-structured; watch for new arg types.

Computing both create_discriminator and create_v2_discriminator, mapping them to instruction_name ("create"/"create_v2") and to the appropriate token program, and then exporting token_program and is_token_2022 through decoded_args is a clean way to keep the rest of the script agnostic.

One thing to keep an eye on: if create_v2 ever introduces argument types beyond "string" and "pubkey", decode_create_instruction will raise ValueError("Unsupported type: ..."). If the IDL evolves, you may need to extend decode_create_instruction’s type handling to cover the new types.

Also applies to: 471-504

idl/pump_swap_idl.json (2)

1752-2090: is_mayhem_mode is threaded consistently through create_pool, BondingCurve, Pool, and events.

The addition of is_mayhem_mode: bool to:

  • create_pool args,
  • BondingCurve and Pool account types, and
  • CreatePoolEvent,

is internally consistent and matches the mayhem-mode feature described in the PR summary.

Because these fields change the serialized layout of the affected accounts, make sure all decoders (including any hand-written parsers or offset-based readers outside the IDL-driven paths) have been updated accordingly. The progressive parsers you added in the Python code are a good hedge here.

Also applies to: 4162-4193, 4462-4550, 4916-4965


3011-3096: New mayhem-related admin instructions, events, and errors align; watch for offset consumers.

The new pieces:

  • set_reserved_fee_recipients and toggle_mayhem_mode instructions,
  • ReservedFeeRecipientsEvent (both in the events list and the type definitions),
  • Additional GlobalConfig fields (whitelist_pda, reserved_fee_recipient, mayhem_mode_enabled, reserved_fee_recipients), and
  • Error codes MayhemModeDisabled, OnlyPumpPoolsMayhemMode, MayhemModeInDesiredState, NotEnoughRemainingAccounts,

all fit together coherently in the IDL.

Given GlobalConfig’s layout change, double-check any external code that reads it via hard-coded offsets (e.g., to fetch reserved fee recipients) to ensure those offsets were recomputed from this updated IDL, and consider centralizing such offsets to reduce the risk of them drifting from the IDL.

Also applies to: 3225-3288, 3799-3811, 4969-4992, 4071-4086, 4819-4838

learning-examples/manual_buy_geyser.py (4)

64-117: BondingCurveState progressive parser matches other scripts; consider adding type hints

The progressive parsing (base struct, optional creator, optional mayhem flag) looks consistent with the patterns in learning-examples/manual_buy.py and learning-examples/manual_sell.py and should safely handle future schema extensions.

To align with repo guidelines for public APIs in Python, it would be good to:

  • Add type hints to BondingCurveState.__init__(self, data: bytes) -> None.
  • Optionally pre-check len(data) >= 8 + self._BASE_STRUCT.sizeof() and raise a clearer ValueError before construct parsing, so malformed accounts yield explicit errors instead of low-level parse exceptions.

175-219: Mayhem fee recipient parsing is consistent but duplicated; consider centralizing

The mayhem fee logic (get_fee_recipient) mirrors manual_buy.py / manual_sell.py and parses reserved_fee_recipient at offset 483, falling back to PUMP_FEE on short or missing data. That matches the IDL comments and should behave correctly when is_mayhem_mode is set.

However, this offset and parsing logic is now duplicated across several files. To reduce drift risk when the IDL changes, consider extracting a shared helper (e.g., in a small utility module) that returns the mayhem fee recipient from PUMP_GLOBAL, and reuse it here and in the other scripts.

You may want to double-check the offset 483 against the current on-chain Global layout in the IDL to ensure it hasn’t shifted.


315-332: Discriminator-based token_program selection: prefer derived discriminator to hard-coded bytes

The discriminator-based selection of token_program (create vs create_v2) is correct in spirit and matches the flow in manual_buy.py, but PUMP_CREATE_V2_DISCRIMINATOR is hard-coded as raw bytes here while manual_buy.py derives the create_v2 discriminator from the IDL.

To avoid future drift if the IDL changes, consider:

  • Reusing the same calculate_discriminator("global:create_v2") approach as manual_buy.py, or
  • At least adding a short comment referencing the IDL version these bytes are derived from.

It’s worth re-confirming that PUMP_CREATE_V2_DISCRIMINATOR matches the discriminator computed from the latest pump_fun_idl.json.


510-518: Line length likely exceeds 88 characters

The print line that shows token program and type is long enough that it almost certainly exceeds the 88‑character limit enforced by Ruff:

print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")

Consider splitting it across lines (e.g., with a temporary token_type variable or multiline f-string) to keep within the configured limit.

learning-examples/manual_buy.py (3)

51-105: BondingCurveState progressive parsing is aligned; minor robustness improvements optional

This BondingCurveState implementation mirrors the progressive parser in learning-examples/manual_sell.py (base struct, optional creator, optional is_mayhem_mode) and should handle older/newer layouts correctly.

Two optional robustness tweaks:

  • Add type hints on __init__ (data: bytes -> None) to align with repo guidelines.
  • Before parsing, check that len(data) >= 8 + self._BASE_STRUCT.sizeof() and raise a clear ValueError if not, rather than relying on construct exceptions.

162-207: Mayhem fee recipient helper duplicates logic; consider a shared utility

get_fee_recipient correctly:

  • Returns PUMP_FEE when is_mayhem_mode is false.
  • Fetches PUMP_GLOBAL, parses reserved_fee_recipient at offset 483, and falls back to PUMP_FEE if the account is missing/too short.

Since identical logic now exists in several scripts (and offset 483 is non-obvious), centralizing this in a small shared helper (used by manual buy/sell and mint/buy flows) would reduce the chance of a future IDL change breaking one call site but not others.

It’s worth re-confirming that offset 483 still matches the latest Global struct in pump_fun_idl.json.


485-498: Long print call likely exceeds 88-char limit

Similar to the Geyser script, the print that shows token program and whether it’s Token2022 vs standard is likely over the 88-character line length:

print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")

Splitting this into multiple lines or extracting token_type into a variable would keep it Ruff-compliant.

learning-examples/mint_and_buy_v2.py (3)

326-357: Mayhem fee recipient helper is correct but duplicates offset logic

get_fee_recipient_for_mayhem correctly falls back to PUMP_FEE when not in mayhem mode or when the Global account is missing/too short, and uses offset 483 for reserved_fee_recipient, matching the other scripts.

Given this same offset and parsing logic appear in multiple files, extracting a common helper (or at least a shared RESERVED_FEE_RECIPIENT_OFFSET constant) would reduce maintenance burden.

Re-confirm offset 483 against the current Global struct definition in pump_fun_idl.json and consider centralizing it.


359-408: Main flow is coherent; one unused variable and possible retry enhancement

The main orchestration flow (derive PDAs, compute initial price and expected tokens, build instructions in the right order) looks correct and consistent with the existing mint_and_buy.py.

Two minor points:

  • initial_real_token_reserves is computed but never used; you can safely remove it to appease Ruff (F841).
  • Transaction submission is a single-shot send_transaction inside a broad except Exception. For a more robust integration (and per repo guidelines), consider:
    • Adding a simple retry with exponential backoff, and/or
    • Narrowing the exception type and logging additional context.

414-488: Transaction assembly and priority fees are aligned with guidelines

The transaction build:

  • Prepends set_compute_unit_limit and set_compute_unit_price to the instruction list, satisfying the “priority fees + CU limit” guidance.
  • Includes create_v2, extend_account, user ATA creation, and buy instructions in a sensible order.
  • Uses TxOpts(skip_preflight=True, preflight_commitment=Confirmed), which is reasonable for this manual script.

The bare except Exception with print + raise is acceptable for an integration script, but if you later move this logic into production code, switching to the centralized logger and more specific exception handling would be preferable.

learning-examples/pumpswap/manual_sell_pumpswap.py (4)

105-125: get_market_address_by_base_mint docstring clarifies behavior; consider handling empty results

The improved docstring and Memcmp filter usage in get_market_address_by_base_mint clearly describe how the pool is discovered.

One small robustness improvement: if response.value is empty, response.value[0] will raise IndexError. For a demo script that’s acceptable, but if you expect to use it more broadly, consider raising a ValueError with a clear message when no pools are found.


228-291: Mayhem fee recipients for pumpswap are well-structured; shared logic with buy script

get_reserved_fee_recipient_pumpswap and get_pumpswap_fee_recipients:

  • Enforce that the GlobalConfig and pool accounts exist and have data.
  • Check the mayhem flag via POOL_MAYHEM_MODE_MIN_SIZE and POOL_MAYHEM_MODE_OFFSET.
  • Return both the chosen fee recipient and its WSOL ATA, using the standard token program for WSOL.

Since this logic is shared conceptually with manual_buy_pumpswap.py, keeping the offsets and seeds synchronized between the two files is important; a future refactor could move these helpers into a shared module.

Please confirm the mayhem flag offset and reserved fee offset against the current AMM/GlobalConfig IDL to ensure they remain valid.


332-348: get_token_program_id performs the right checks; Ruff TRY003 is just style

get_token_program_id correctly:

  • Fetches mint account info.
  • Raises if the account is missing.
  • Branches on owner to distinguish TokenProgram vs Token2022, raising for unknown owners.

Ruff’s TRY003 complaint about the long f-string message is stylistic; if you want to appease it, you can shorten the string or move detailed context into a custom exception, but functionally this is fine.

Because this depends on the RPC client’s response shape and program IDs, re-check against the Solana RPC/SDK docs if you upgrade dependencies.


390-499: sell_pump_swap correctly wires mayhem fees and dynamic token program; consider narrower exception handling

The updated sell_pump_swap:

  • Accepts market and token_program_id explicitly.
  • Calculates min SOL output with slippage, logs token balance and price.
  • Uses get_pumpswap_fee_recipients so mayhem-mode pools route fees to the correct recipient and WSOL ATA.
  • Injects token_program_id into the account metas, while still including SYSTEM_TOKEN_PROGRAM for WSOL, which matches the expected AMM layout.

The main functional concern is around error handling:

  • The try/except Exception block prints Error: {e!s} and returns None, which hides the original stack trace and can make debugging harder.
  • For a more robust integration, consider catching narrower exceptions (e.g., RPC-related) and at least logging full details via the centralized logger, or re-raising after logging.

If this flow moves beyond manual experimentation, you may also want to add a basic retry with backoff around send_transaction, mirroring patterns in other scripts.

learning-examples/pumpswap/manual_buy_pumpswap.py (5)

117-193: Market discovery and pool parsing match manual_sell_pumpswap

get_market_address_by_base_mint and get_market_data replicate the same structure and documentation as in the sell script, ensuring both buy and sell paths agree on how pools are located and deserialized.

As with the sell script, you might eventually want to handle the case where get_program_accounts returns 0 items more explicitly, but for a manual script this is acceptable.


275-337: Mayhem fee helpers mirror sell script; shared utility could avoid duplication

get_reserved_fee_recipient_pumpswap and get_pumpswap_fee_recipients are a direct counterpart to those in the sell script:

  • They parse the mayhem flag from the pool account and select the reserved vs standard fee recipient accordingly.
  • They derive the fee recipient’s WSOL ATA via the standard token program.

Given the overlap, consider moving these into a shared helper (or at least centralizing the offsets) to keep buy and sell flows from diverging if the IDL changes.

As in the sell script, please re-check POOL_MAYHEM_MODE_OFFSET, POOL_MAYHEM_MODE_MIN_SIZE, and GLOBALCONFIG_RESERVED_FEE_OFFSET against the current IDL.


379-395: get_token_program_id correctly distinguishes Token vs Token2022

Same comments as for the sell script:

  • Logic is correct (owner-based).
  • Raises clear errors for missing/unknown owners.
  • Ruff TRY003 warnings about long messages are stylistic; you can shorten or restructure messages if you want lint cleanliness.

398-503: buy_pump_swap wiring is solid: mayhem fees, dynamic token program, WSOL handling, and volume tracking

The buy_pump_swap function:

  • Determines token_program_id dynamically via get_token_program_id, which is important for Token2022 support.
  • Computes expected token amount and max_sol_input with slippage, logging both.
  • Derives global/user volume accumulator PDAs and includes them in the account metas.
  • Uses get_pumpswap_fee_recipients to honor mayhem-mode fee routing, including the WSOL ATA for the fee recipient.
  • Builds the account list with token_program_id and SYSTEM_TOKEN_PROGRAM in the expected positions, and sets global_volume_accumulator as read-only and user accumulator as writable.
  • Encodes data as [BUY_DISCRIMINATOR, amount_out, max_in, track_volume_byte], which matches the documented layout for this AMM buy instruction.

The only substantive concern is error handling:

  • Broad except Exception as e with print and return None hides stack traces and may complicate debugging.
  • For anything beyond quick manual experiments, consider narrower exceptions and logging via your central logger.

If you later use this path in production, adding a simple retry/backoff around send_transaction (as in some other scripts) would also be beneficial.


515-599: WSOL wrapping and ATA creation flow look correct; simulation-before-send is a good safety net

Within buy_pump_swap:

  • Compute budget instructions are added first, honoring the priority-fee guidelines.
  • The WSOL ATA is created idempotently with the standard Token program, which is correct.
  • wrap_amount includes PROTOCOL_FEE_BUFFER, reducing the risk of underfunding due to protocol fees.
  • transfer + sync_native correctly wrap SOL into WSOL.
  • A separate ATA is created for the purchased base token using token_program_id to respect Token2022 mints.
  • The transaction is simulated before sending, and preflight is skipped only after a successful simulation, which is a sensible pattern in competitive environments.

The structure here is solid; only the broad Exception catch at the end is a (minor) deviation from Ruff’s BLE rules.

@smypmsa smypmsa changed the title Feat/mayhem update Add mayhem mode support and Token2022 integration Nov 18, 2025
Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
learning-examples/cleanup_accounts.py (1)

79-91: Avoid UnboundLocalError for client in finally

The updated ATA derivation on Line 85 correctly passes the token program into get_associated_token_address and aligns with the new token‑program‑aware wallet API.

However, in this same block there is a latent bug: if an exception is raised before client is assigned (e.g., bad RPC_ENDPOINT or Wallet(PRIVATE_KEY) failure), the finally block will raise UnboundLocalError when it tries to await client.close().

Consider initializing client to None and guarding the close:

-async def main():
-    try:
-        client = SolanaClient(RPC_ENDPOINT)
+async def main():
+    client: SolanaClient | None = None
+    try:
+        client = SolanaClient(RPC_ENDPOINT)
         wallet = Wallet(PRIVATE_KEY)
@@
-    finally:
-        await client.close()
+    finally:
+        if client is not None:
+            await client.close()

This preserves the new behavior while making the cleanup path safe.

🧹 Nitpick comments (7)
src/platforms/letsbonk/curve_manager.py (1)

197-211: Reserve validation and price computation look correct; consider tightening error style

The positive-reserve checks and derived price_per_token are consistent with the PumpFun implementation and prevent silent zero/NaN prices, which aligns well with the “Normal Mode with full validation” requirement. Based on learnings.

To keep Ruff (TRY003) quiet and reduce repetition, you might slightly shorten or standardize the error messages, e.g.:

-        if pool_data["virtual_base"] <= 0:
-            raise ValueError(
-                f"Invalid virtual_base: {pool_data['virtual_base']} - cannot calculate price"
-            )
+        if pool_data["virtual_base"] <= 0:
+            raise ValueError(f"Invalid virtual_base {pool_data['virtual_base']}")

Same idea for virtual_quote.

src/platforms/pumpfun/curve_manager.py (2)

193-214: Stricter reserve checks and price_per_token calculation are sound

Validating both virtual_token_reserves and virtual_sol_reserves before computing price_per_token is the right behavior; it avoids silent zero prices and matches how the traders now rely on price_per_token from get_pool_state.

As in the LetsBonk manager, the very long ValueError messages may trip Ruff (TRY003). If needed, consider trimming them or using a shared helper to build the message.

-        if curve_data["virtual_token_reserves"] <= 0:
-            raise ValueError(
-                f"Invalid virtual_token_reserves: {curve_data['virtual_token_reserves']} - cannot calculate price"
-            )
+        if curve_data["virtual_token_reserves"] <= 0:
+            raise ValueError(
+                f"Invalid virtual_token_reserves {curve_data['virtual_token_reserves']}"
+            )

Same pattern for virtual_sol_reserves.


64-85: Align calculate_price behavior with new decode-based price_per_token semantics

calculate_price() still returns 0.0 when virtual_token_reserves <= 0, while _decode_curve_state_with_idl now raises on invalid reserves and always computes a positive price_per_token.

If anything still calls calculate_price() directly, it will see different behavior (silent 0.0 vs. hard failure) from callers that go via get_pool_state()["price_per_token"].

Consider either:

  • Re-implementing calculate_price() in terms of the decoded state:
 async def calculate_price(self, pool_address: Pubkey) -> float:
-    pool_state = await self.get_pool_state(pool_address)
-
-    # Use virtual reserves for price calculation
-    virtual_token_reserves = pool_state["virtual_token_reserves"]
-    virtual_sol_reserves = pool_state["virtual_sol_reserves"]
-
-    if virtual_token_reserves <= 0:
-        return 0.0
-
-    # Price = sol_reserves / token_reserves
-    price_lamports = virtual_sol_reserves / virtual_token_reserves
-    return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
+    pool_state = await self.get_pool_state(pool_address)
+    return float(pool_state["price_per_token"])
  • Or deprecating calculate_price() if it is no longer used.

I’d recommend verifying actual usage to avoid subtle behavioral drift between call sites.

Also applies to: 193-214

src/trading/platform_aware.py (1)

68-82: Buy/sell now fail fast on invalid prices, restoring slippage protection

Switching both buy and sell paths to:

  • Fetch pool_state via get_pool_state,
  • Read price_per_token from the decoded state, and
  • Raise when the price is missing or non-positive,

fixes the previous issue where a zero price effectively disabled min-out slippage guards. This is exactly the safer behavior the earlier review was aiming for and aligns with the “Normal Mode with full validation” requirement. Based on learnings.

Two small polish suggestions:

  • Ensure is_mayhem_mode is always a bool in TokenInfo:
-                token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
+                token_info.is_mayhem_mode = bool(
+                    pool_state.get("is_mayhem_mode", False)
+                )

Apply similarly in the seller.

  • If Ruff’s TRY rules are enforced, you could factor the long ValueError construction into a tiny helper (shared between buy and sell) to avoid TRY003/TRY301 noise, but that’s optional.

Also applies to: 236-251

src/platforms/pumpfun/event_parser.py (3)

91-97: Create/Create_v2 log handling is solid; consider using the actual instruction type for token_program_id

The logs parser now:

  • Recognizes both "Instruction: Create" and "Instruction: Create_v2", and
  • Correctly decodes the shared CreateEvent from program data.

Right now, the derived token_program_id is always set to SystemAddresses.TOKEN_2022_PROGRAM, based on recent pump.fun behavior.

To keep this robust if pump.fun ever emits legacy Create again, you could leverage the already-computed instruction_type (or a boolean) to choose the program:

-            create_instruction_found = False
+            create_instruction_found = False
+            saw_create_v2 = False
@@
-                if "Program log: Instruction: Create" in log or "Program log: Instruction: Create_v2" in log:
+                if (
+                    "Program log: Instruction: Create" in log
+                    or "Program log: Instruction: Create_v2" in log
+                ):
                     create_instruction_found = True
-                    instruction_type = "Create_v2" if "Create_v2" in log else "Create"
+                    if "Create_v2" in log:
+                        saw_create_v2 = True
@@
-                    token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
+                    token_program_id = (
+                        SystemAddresses.TOKEN_2022_PROGRAM
+                        if saw_create_v2
+                        else SystemAddresses.TOKEN_PROGRAM
+                    )

If you’re confident all new tokens are Create_v2-only, this can be deferred, but it future-proofs the logs path for mixed historical data.

Also applies to: 112-117, 246-253, 260-273


50-59: Expose create_v2 discriminator from get_instruction_discriminators for full Token2022 coverage

You correctly:

  • Initialize _create_v2_instruction_discriminator_bytes (when present),
  • Handle create vs create_v2 in parse_token_creation_from_instruction, and
  • Detect both in parse_token_creation_from_block.

However, get_instruction_discriminators() still returns only the legacy create discriminator:

def get_instruction_discriminators(self) -> list[bytes]:
    return [self._create_instruction_discriminator_bytes]

If any upstream listener uses this list to pre-filter instructions, it will miss create_v2 traffic.

Consider:

 def get_instruction_discriminators(self) -> list[bytes]:
@@
-        return [self._create_instruction_discriminator_bytes]
+        discriminators = [self._create_instruction_discriminator_bytes]
+        if self._create_v2_instruction_discriminator_bytes:
+            discriminators.append(self._create_v2_instruction_discriminator_bytes)
+        return discriminators

This keeps backward compatibility while ensuring Token2022 create_v2 gets picked up everywhere that relies on this API.

Also applies to: 69-72, 426-433, 478-491, 545-553


622-640: New helpers are fine as placeholders; minor cleanup for Ruff warnings

_parse_bonding_curve_state and _get_is_mayhem_mode_from_curve are sensible hooks for future mayhem-mode support without an RPC client in the parser.

To reduce Ruff noise:

  • _get_is_mayhem_mode_from_curve currently ignores bonding_curve_address. Either mark the parameter as unused or reference it:
-    def _get_is_mayhem_mode_from_curve(self, bonding_curve_address: Pubkey) -> bool:
+    def _get_is_mayhem_mode_from_curve(self, bonding_curve_address: Pubkey) -> bool:
@@
-        # Since event parser doesn't have RPC client access, we cannot fetch
-        # and parse bonding curve state here. Mayhem mode will be set later
-        # when traders fetch the bonding curve state.
-        return False
+        # Parameter kept for future implementation.
+        _ = bonding_curve_address
+        return False
  • If BLE001 (catching Exception) in _parse_bonding_curve_state becomes an issue, you can drop the try/except and let the underlying decode_account_data handle errors, given it already catches internally.

These are small hygiene tweaks; the functional behavior is already consistent with the comments.

Also applies to: 642-659

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4840db and 97fd3f7.

📒 Files selected for processing (7)
  • learning-examples/cleanup_accounts.py (4 hunks)
  • src/platforms/letsbonk/address_provider.py (1 hunks)
  • src/platforms/letsbonk/curve_manager.py (1 hunks)
  • src/platforms/pumpfun/curve_manager.py (1 hunks)
  • src/platforms/pumpfun/event_parser.py (14 hunks)
  • src/platforms/pumpfun/instruction_builder.py (5 hunks)
  • src/trading/platform_aware.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/platforms/pumpfun/instruction_builder.py
🧰 Additional context used
📓 Path-based instructions (7)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Enforce max line length of 88 characters in Python (Ruff config)
Use 4 spaces for indentation in Python files
Target Python version 3.11+ features and syntax
Prefer double quotes for strings in Python code
Enable and honor import sorting in Python modules
Apply Ruff rule sets: Security (S), Annotations (ANN), Exceptions (BLE, TRY), Complexity (C90), Pylint conventions (PL), and no commented-out code (ERA)
Order imports as: standard library, third-party, then local imports
Use Google-style docstrings for functions and classes
Provide type hints for all public functions
Use a module-level logger via get_logger(name)
Wrap risky operations in try/except with proper logging
Implement rate limiting and retry mechanisms where external calls are made
Perform comprehensive input validation for externally sourced data

**/*.py: Limit lines to 88 characters
Use 4 spaces for indentation (never tabs)
Use double quotes for strings consistently
Target Python 3.11+ features and syntax
Enable automatic import sorting and organization
Order imports: standard library first, third-party second, local last
Add type hints to all public functions and methods
Use modern typing syntax (e.g., X | Y unions)
Include explicit return type annotations
Use typing.Any for complex/unknown types (from typing import Any)
Use Google-style docstrings for all functions and classes
Wrap external operations (RPC calls, file I/O) in try/except blocks
Log exceptions with context using logging.exception()
Provide meaningful error messages when handling exceptions
Do not suppress exceptions without good reason
Use get_logger(name) with logger from utils.logger
Use appropriate log levels (DEBUG, INFO, WARNING, ERROR) with contextual messages
Never hardcode secrets (private keys, API tokens)
Use environment variables for sensitive configuration
Do not log sensitive information
Validate all external inputs
Comply with Ruff security rules (S)
Comply with Ruff type annotation rules (ANN)
Comply...

Files:

  • src/trading/platform_aware.py
  • learning-examples/cleanup_accounts.py
  • src/platforms/pumpfun/event_parser.py
  • src/platforms/pumpfun/curve_manager.py
  • src/platforms/letsbonk/curve_manager.py
  • src/platforms/letsbonk/address_provider.py
src/{client,trading,monitoring,platforms}/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Separate concerns into client, trading, monitoring, and platforms packages under src/

Files:

  • src/trading/platform_aware.py
  • src/platforms/pumpfun/event_parser.py
  • src/platforms/pumpfun/curve_manager.py
  • src/platforms/letsbonk/curve_manager.py
  • src/platforms/letsbonk/address_provider.py
src/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/architecture.mdc)

src/**/*.py: Use snake_case for all Python file names
Avoid circular imports between packages
Use asyncio.create_task() for concurrent operations
Implement proper cleanup on shutdown for async tasks/resources

Files:

  • src/trading/platform_aware.py
  • src/platforms/pumpfun/event_parser.py
  • src/platforms/pumpfun/curve_manager.py
  • src/platforms/letsbonk/curve_manager.py
  • src/platforms/letsbonk/address_provider.py
src/trading/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/architecture.mdc)

trading/ may depend on core/, platforms/, and interfaces/

Files:

  • src/trading/platform_aware.py
learning-examples/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Treat learning-examples Python scripts as integration tests for manual runs

learning-examples/**/*.py: Use learning examples to test new features before integration
Use learning examples to validate platform-specific functionality
Use learning examples for performance benchmarking
Use learning examples for educational purposes for new developers

Always test changes using scripts in learning-examples/ before modifying the main bot

Files:

  • learning-examples/cleanup_accounts.py
learning-examples/**

📄 CodeRabbit inference engine (.cursor/rules/architecture.mdc)

Place integration tests and validation scenarios under learning-examples/

Files:

  • learning-examples/cleanup_accounts.py
src/platforms/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Implement platform-specific logic under src/platforms/

src/platforms/**/*.py: Platform-specific modules may only import from their own package and core/interfaces
platforms/ may depend on core/ and interfaces/ only
Implement platform-specific functionality via factory pattern using interfaces

Update platform-specific implementations under src/platforms/ when adding features

Files:

  • src/platforms/pumpfun/event_parser.py
  • src/platforms/pumpfun/curve_manager.py
  • src/platforms/letsbonk/curve_manager.py
  • src/platforms/letsbonk/address_provider.py
🧠 Learnings (4)
📚 Learning: 2025-08-21T12:08:15.475Z
Learnt from: CR
Repo: chainstacklabs/pumpfun-bonkfun-bot PR: 0
File: .cursor/rules/trading-bot.mdc:0-0
Timestamp: 2025-08-21T12:08:15.475Z
Learning: Applies to **/*.py : Implement wallet balance checks before trading

Applied to files:

  • src/trading/platform_aware.py
📚 Learning: 2025-08-21T12:08:15.475Z
Learnt from: CR
Repo: chainstacklabs/pumpfun-bonkfun-bot PR: 0
File: .cursor/rules/trading-bot.mdc:0-0
Timestamp: 2025-08-21T12:08:15.475Z
Learning: Applies to **/*.py : Validate token data (e.g., age, metadata) before trading

Applied to files:

  • src/trading/platform_aware.py
📚 Learning: 2025-08-21T12:08:15.475Z
Learnt from: CR
Repo: chainstacklabs/pumpfun-bonkfun-bot PR: 0
File: .cursor/rules/trading-bot.mdc:0-0
Timestamp: 2025-08-21T12:08:15.475Z
Learning: Applies to **/*.py : Provide a Normal Mode with full validation and price checks

Applied to files:

  • src/platforms/letsbonk/curve_manager.py
📚 Learning: 2025-08-21T12:08:15.475Z
Learnt from: CR
Repo: chainstacklabs/pumpfun-bonkfun-bot PR: 0
File: .cursor/rules/trading-bot.mdc:0-0
Timestamp: 2025-08-21T12:08:15.475Z
Learning: Applies to **/*.py : Implement platform-specific address providers behind an abstraction

Applied to files:

  • src/platforms/letsbonk/address_provider.py
🧬 Code graph analysis (4)
src/trading/platform_aware.py (3)
src/platforms/letsbonk/curve_manager.py (1)
  • get_pool_state (43-64)
src/platforms/pumpfun/curve_manager.py (1)
  • get_pool_state (41-62)
src/interfaces/core.py (1)
  • get_pool_state (243-252)
learning-examples/cleanup_accounts.py (1)
src/core/pubkeys.py (1)
  • SystemAddresses (42-67)
src/platforms/pumpfun/event_parser.py (2)
src/core/pubkeys.py (1)
  • SystemAddresses (42-67)
src/utils/idl_parser.py (1)
  • decode_account_data (290-331)
src/platforms/letsbonk/address_provider.py (1)
src/core/pubkeys.py (1)
  • SystemAddresses (42-67)
🪛 Ruff (0.14.5)
src/trading/platform_aware.py

75-78: Abstract raise to an inner function

(TRY301)


75-78: Avoid specifying long messages outside the exception class

(TRY003)


244-247: Abstract raise to an inner function

(TRY301)


244-247: Avoid specifying long messages outside the exception class

(TRY003)

src/platforms/pumpfun/event_parser.py

637-637: Consider moving this statement to an else block

(TRY300)


638-638: Do not catch blind exception: Exception

(BLE001)


642-642: Unused method argument: bonding_curve_address

(ARG002)

src/platforms/pumpfun/curve_manager.py

199-201: Avoid specifying long messages outside the exception class

(TRY003)


203-205: Avoid specifying long messages outside the exception class

(TRY003)

src/platforms/letsbonk/curve_manager.py

199-201: Avoid specifying long messages outside the exception class

(TRY003)


203-205: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (1)
learning-examples/cleanup_accounts.py (1)

42-51: Consistent use of TOKEN_PROGRAM for burn and close instructions looks good

Using program_id=TOKEN_PROGRAM in both the burn and close_account flows keeps all token operations aligned with the configured token program, matching how the ATA is derived. As long as TOKEN_PROGRAM is set correctly for the target mint (legacy vs Token‑2022), this wiring is correct and clear.

Also applies to: 57-62

@smypmsa smypmsa merged commit 03a4e7b into main Nov 18, 2025
@smypmsa smypmsa deleted the feat/mayhem-update branch November 18, 2025 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mayhem mode update

2 participants