-
Notifications
You must be signed in to change notification settings - Fork 311
Feat/letsbonk examples #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThis PR updates the Raydium LaunchLab IDL to version 0.2.0, introducing renamed instructions, expanded type definitions, and new PDA structures. It adds an IDL parser utility for decoding Solana Anchor instructions and account data, and provides four complete trading examples demonstrating buy/sell operations on Raydium LaunchLab with manual account derivation and transaction construction. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Script as Trading Script
participant IDL as IDL Parser
participant RPC as Solana RPC
participant Program as Raydium Program
User->>Script: Invoke with token/amount
Script->>Script: Derive PDAs & vaults
Script->>RPC: Fetch pool state
RPC-->>Script: Raw account data
Script->>IDL: Decode pool state
IDL-->>Script: Decoded pool struct
Script->>Script: Calculate min/max output with slippage
Script->>Script: Build tx: compute budget + ATA + WSOL + instruction
Script->>RPC: Simulate transaction
alt Simulation Success
Script->>RPC: Send transaction
RPC-->>Program: Execute instruction
Program-->>RPC: Confirm
RPC-->>Script: Tx signature
Script-->>User: Success + Solscan link
else Simulation Fails
Script-->>User: Error message
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes This change spans heterogeneous areas: (1) significant IDL schema restructuring with renamed entities and new types requiring semantic verification; (2) a new utility module with multiple public APIs and dense type-handling logic; (3) four similar but distinct trading scripts with repetitive patterns yet significant logic density around PDA derivation, account sequencing, and transaction assembly. The review requires careful attention to IDL correctness, IDL parser robustness across type variants, and logic validation in each trading example's transaction construction and slippage calculations. Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
🧹 Nitpick comments (22)
idl/raydium_launchlab_idl.json (1)
200-373: Trade instruction account lists still omit fee-vaults; rely on remaining accounts.buy_exact_out keeps 15 main accounts and does not list creator/platform fee vaults, which the program now expects via remaining accounts. The examples add these (plus System Program), resolving the "NotEnoughRemainingAccounts" failure. Consider documenting these three remaining accounts in the docs array to avoid confusion.
learning-examples/letsbonk-buy-sell/idl_parser.py (3)
24-41: Annotate mutable class attribute with ClassVar and refine typing.Add ClassVar to satisfy RUF012 and strengthen types for the primitive map.
Apply this diff:-from typing import Any +from typing import Any, ClassVar, Tuple @@ - _PRIMITIVE_TYPE_INFO = { + _PRIMITIVE_TYPE_INFO: ClassVar[dict[str, Tuple[str | None, int]]] = {
42-59: Use centralized logger instead of print for diagnostics.Replace print-based verbose paths with get_logger(name) to follow repo logging guidance.
Apply this diff:+from src.utils.logger import get_logger @@ - self.verbose = verbose + self.verbose = verbose + self._logger = get_logger(__name__) @@ - print( + self._logger.warning( f"⚠️ Instruction data for '{instruction_name}' is shorter than the expected minimum " f"({actual_size}/{expected_min_size} bytes)." ) @@ - print(f"❌ Decode error in argument '{arg['name']}': {e}") + self._logger.exception("Decode error in argument '%s'", arg["name"]) @@ - print(f"Account type '{account_type_name}' not found in IDL") + self._logger.error("Account type '%s' not found in IDL", account_type_name) @@ - print( + self._logger.error( f"Account data too short to contain a discriminator: {len(account_data)} bytes" ) @@ - print(f"Error decoding account data for {account_type_name}: {e}") + self._logger.exception("Error decoding account data for %s", account_type_name) @@ - print(f"📏 Initialize instruction min size: {min_size} bytes") + self._logger.debug("Initialize instruction min size: %d bytes", min_size) @@ - print(f"⚠️ Could not calculate size for {instruction['name']}: {e}") + self._logger.warning("Could not calculate size for %s", instruction["name"], exc_info=True)
118-123: Avoid blindexcept Exception; either narrow or re-raise after logging.At least catch (ValueError, KeyError, struct.error) where applicable; keep verbose path logging.
learning-examples/letsbonk-buy-sell/manual_buy_exact_out.py (5)
549-556: Avoid hardcoded rent; fetch minimum balance for rent exemption.Token account rent is network-dependent. Query via RPC and add to the deposit to prevent “insufficient funds” or close failures.
Example change inside buy_exact_out():- account_creation_lamports = 2_039_280 # Standard account creation cost + rent_resp = await client.get_minimum_balance_for_rent_exemption(165) + account_creation_lamports = rent_resp.value
375-377: Call SyncNative after initializing WSOL.After funding and initializing the native mint account, add SyncNative (instruction 17) so token amount matches lamports.
Apply this diff:+def create_sync_native_instruction(account: Pubkey) -> Instruction: + # SPL Token: SyncNative (instruction 17) + return Instruction( + program_id=TOKEN_PROGRAM_ID, + data=bytes([17]), + accounts=[AccountMeta(pubkey=account, is_signer=False, is_writable=True)], + ) @@ instructions = [ set_compute_unit_price(COMPUTE_UNIT_PRICE), set_compute_unit_limit(COMPUTE_UNIT_LIMIT), create_ata_ix, create_wsol_ix, init_wsol_ix, + create_sync_native_instruction(user_quote_token), buy_exact_out_ix, close_wsol_ix, ]Also applies to: 640-654
713-719: Add wallet balance check versus total_lamports before sending.Prevent runtime failures by ensuring balance covers WSOL funding + fees.
22-35: Import ordering and linting.Per guidelines, place standard lib, then third-party, then local imports. Move
from idl_parser import load_idl_parserbelow third-party imports; also enforce 88-char lines.
686-696: Retries/backoff on send_transaction (examples).Optional: add simple exponential backoff retry on transient RPC errors to improve reliability.
learning-examples/letsbonk-buy-sell/manual_buy_exact_in.py (4)
424-441: Min-out math is conservative; ok. Consider fees.Using virtual reserves with a slippage buffer works, but does not account for protocol/platform/creator fees. If available, incorporate fee rates from Global/PlatformConfig for tighter bounds.
538-545: Use dynamic rent instead of hardcoded constant for WSOL account.Same rationale as buy_exact_out; query get_minimum_balance_for_rent_exemption(165).
373-376: Add SyncNative after initializing WSOL.Mirror the change suggested in buy_exact_out to keep WSOL amount in sync.
Apply the same helper and insertion before the swap instruction.Also applies to: 629-643
22-35: Import ordering and 88-char wrapping.Move local
idl_parserimport after third-party imports and wrap long lines to ≤88 chars.learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py (5)
668-679: Add retries with exponential backoff and handle common transient RPC errorsSingle send + confirm is fragile. Implement minimal retries for BlockhashNotFound, AccountInUse, NodeBehind, etc., and refresh blockhash when needed.
- print("Sending transaction...") - result = await client.send_transaction( - transaction, - opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed), - ) + print("Sending transaction...") + result = None + for attempt in range(3): + try: + result = await client.send_transaction( + transaction, + opts=TxOpts(skip_preflight=False, preflight_commitment=Confirmed), + ) + break + except Exception as send_err: + # Refresh blockhash once and retry with backoff + if attempt == 1: + bh = await client.get_latest_blockhash() + recent_blockhash = bh.value.blockhash + message = Message.new_with_blockhash( + instructions, PAYER.pubkey(), recent_blockhash + ) + transaction = VersionedTransaction(message, [PAYER]) + await asyncio.sleep(0.5 * (2**attempt)) + if result is None: + print("Failed to send transaction after retries") + return NoneAs per coding guidelines on retries and blockhash caching.
87-89: Tune priority fee; make it configurable and adaptive1_000 micro‑lamports/CU is often too low. Make it env-configurable and consider bumping on simulation “insufficient priority fee”.
-COMPUTE_UNIT_LIMIT = 150_000 -COMPUTE_UNIT_PRICE = 1_000 +COMPUTE_UNIT_LIMIT = int(os.environ.get("COMPUTE_UNIT_LIMIT", "200000")) +COMPUTE_UNIT_PRICE = int(os.environ.get("COMPUTE_UNIT_PRICE", "30000")) # µ-lamports
682-687: Replace prints + broad except with structured logging and narrower exceptionsUse centralized logger and log exceptions with context; avoid bare
except Exception. This also satisfies Ruff BLE001/TRY300.+from src.utils.logger import get_logger +logger = get_logger(__name__) @@ - print(f"Error decoding pool state: {e}") - import traceback - traceback.print_exc() + logger.exception("Error decoding pool state") @@ - except Exception as e: - print(f"Error getting pool state data: {e}") + except ValueError as e: + logger.error("Pool state account not found: %s", e) + return None + except Exception: + logger.exception("Error getting pool state data") return None @@ - except Exception as e: - print(f"Error executing sell_exact_in: {e}") - import traceback - traceback.print_exc() + except Exception: + logger.exception("Error executing sell_exact_in") return None @@ - except ValueError as e: - print(f"Invalid token mint address: {e}") + except ValueError as e: + logger.error("Invalid token mint address: %s", e) sys.exit(1) - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() + except Exception: + logger.exception("Unhandled error in main") sys.exit(1)As per coding guidelines (logger, BLE/TRY).
Also applies to: 200-206, 227-230, 451-454, 742-751
181-199: Type hints: prefer precise dict[str, Any] over bare dictImprove signatures for static analysis and readability.
-def decode_pool_state(account_data: bytes) -> dict | None: +from typing import Any +def decode_pool_state(account_data: bytes) -> dict[str, Any] | None:Repeat similarly where
dictis used in this file.
441-447: Line length > 88 chars; wrap long f-stringsSeveral print/log lines exceed 88 chars. Please wrap to comply with Ruff and project style.
Example:
- print(f"Minimum with {slippage_tolerance * 100}% slippage: {minimum_with_slippage:,} lamports ({minimum_with_slippage / LAMPORTS_PER_SOL:.6f} SOL)") + print( + f"Minimum with {slippage_tolerance * 100}% slippage: " + f"{minimum_with_slippage:,} lamports " + f"({minimum_with_slippage / LAMPORTS_PER_SOL:.6f} SOL)" + )As per coding guidelines.
learning-examples/letsbonk-buy-sell/manual_sell_exact_out.py (4)
669-680: Add retry/backoff and blockhash refresh on send/confirm pathMirror the retry approach suggested in the other file; reduce flakes and align with guidelines.
Same diff as proposed for manual_sell_exact_in.py send/confirm block.
87-89: Priority fee defaults are low; make configurable and bumpUse env-driven defaults and consider adaptive bumps when simulation suggests low priority.
-COMPUTE_UNIT_LIMIT = 150_000 -COMPUTE_UNIT_PRICE = 1_000 +COMPUTE_UNIT_LIMIT = int(os.environ.get("COMPUTE_UNIT_LIMIT", "200000")) +COMPUTE_UNIT_PRICE = int(os.environ.get("COMPUTE_UNIT_PRICE", "30000"))
684-690: Use centralized logger and avoid broad exceptSwitch prints to logger and narrow exceptions; satisfies Ruff BLE001/TRY300.
+from src.utils.logger import get_logger +logger = get_logger(__name__) @@ - except Exception as e: - print(f"Error executing sell_exact_out: {e}") - import traceback - traceback.print_exc() + except Exception: + logger.exception("Error executing sell_exact_out") return None @@ - except ValueError as e: - print(f"Invalid token mint address: {e}") + except ValueError as e: + logger.error("Invalid token mint address: %s", e) sys.exit(1) - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() + except Exception: + logger.exception("Unhandled error in main") sys.exit(1)As per coding guidelines.
Also applies to: 200-206, 227-230, 454-456, 743-751
181-199: Tighten type hints to dict[str, Any]Improve static typing on decode_pool_state and other dict-returning functions.
-from typing import Any -def decode_pool_state(account_data: bytes) -> dict | None: +from typing import Any +def decode_pool_state(account_data: bytes) -> dict[str, Any] | None:Repeat where applicable.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
idl/raydium_launchlab_idl.json(29 hunks)learning-examples/letsbonk-buy-sell/idl_parser.py(1 hunks)learning-examples/letsbonk-buy-sell/manual_buy_exact_in.py(1 hunks)learning-examples/letsbonk-buy-sell/manual_buy_exact_out.py(1 hunks)learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py(1 hunks)learning-examples/letsbonk-buy-sell/manual_sell_exact_out.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.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:
learning-examples/letsbonk-buy-sell/idl_parser.pylearning-examples/letsbonk-buy-sell/manual_buy_exact_in.pylearning-examples/letsbonk-buy-sell/manual_buy_exact_out.pylearning-examples/letsbonk-buy-sell/manual_sell_exact_in.pylearning-examples/letsbonk-buy-sell/manual_sell_exact_out.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 developersAlways test changes using scripts in learning-examples/ before modifying the main bot
Files:
learning-examples/letsbonk-buy-sell/idl_parser.pylearning-examples/letsbonk-buy-sell/manual_buy_exact_in.pylearning-examples/letsbonk-buy-sell/manual_buy_exact_out.pylearning-examples/letsbonk-buy-sell/manual_sell_exact_in.pylearning-examples/letsbonk-buy-sell/manual_sell_exact_out.py
learning-examples/**
📄 CodeRabbit inference engine (.cursor/rules/architecture.mdc)
Place integration tests and validation scenarios under learning-examples/
Files:
learning-examples/letsbonk-buy-sell/idl_parser.pylearning-examples/letsbonk-buy-sell/manual_buy_exact_in.pylearning-examples/letsbonk-buy-sell/manual_buy_exact_out.pylearning-examples/letsbonk-buy-sell/manual_sell_exact_in.pylearning-examples/letsbonk-buy-sell/manual_sell_exact_out.py
🧠 Learnings (1)
📚 Learning: 2025-10-04T12:42:50.785Z
Learnt from: CR
PR: chainstacklabs/pumpfun-bonkfun-bot#0
File: AGENTS.md:0-0
Timestamp: 2025-10-04T12:42:50.785Z
Learning: Applies to learning-examples/manual_{buy,sell}.py : Test changes using manual_buy.py and manual_sell.py with minimal amounts before production use
Applied to files:
learning-examples/letsbonk-buy-sell/manual_buy_exact_in.pylearning-examples/letsbonk-buy-sell/manual_sell_exact_in.py
🧬 Code graph analysis (4)
learning-examples/letsbonk-buy-sell/manual_buy_exact_in.py (1)
learning-examples/letsbonk-buy-sell/idl_parser.py (2)
load_idl_parser(380-391)decode_account_data(144-185)
learning-examples/letsbonk-buy-sell/manual_buy_exact_out.py (2)
learning-examples/letsbonk-buy-sell/idl_parser.py (2)
load_idl_parser(380-391)decode_account_data(144-185)src/core/wallet.py (2)
keypair(29-31)pubkey(24-26)
learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py (2)
learning-examples/letsbonk-buy-sell/idl_parser.py (2)
load_idl_parser(380-391)decode_account_data(144-185)src/core/client.py (4)
get_account_info(96-114)get_latest_blockhash(131-139)confirm_transaction(209-229)get_token_account_balance(116-129)
learning-examples/letsbonk-buy-sell/manual_sell_exact_out.py (3)
learning-examples/letsbonk-buy-sell/idl_parser.py (2)
load_idl_parser(380-391)decode_account_data(144-185)src/core/wallet.py (2)
keypair(29-31)pubkey(24-26)src/core/client.py (4)
get_account_info(96-114)get_latest_blockhash(131-139)confirm_transaction(209-229)get_token_account_balance(116-129)
🪛 Ruff (0.14.1)
learning-examples/letsbonk-buy-sell/idl_parser.py
24-40: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
119-119: Do not catch blind exception: Exception
(BLE001)
180-180: Consider moving this statement to an else block
(TRY300)
182-182: Do not catch blind exception: Exception
(BLE001)
213-213: Do not catch blind exception: Exception
(BLE001)
232-234: Avoid specifying long messages outside the exception class
(TRY003)
253-253: Avoid specifying long messages outside the exception class
(TRY003)
276-278: Avoid specifying long messages outside the exception class
(TRY003)
294-294: Avoid specifying long messages outside the exception class
(TRY003)
312-312: Avoid specifying long messages outside the exception class
(TRY003)
335-335: Avoid specifying long messages outside the exception class
(TRY003)
352-354: Avoid specifying long messages outside the exception class
(TRY003)
377-377: Avoid specifying long messages outside the exception class
(TRY003)
learning-examples/letsbonk-buy-sell/manual_buy_exact_in.py
196-196: Consider moving this statement to an else block
(TRY300)
198-198: Do not catch blind exception: Exception
(BLE001)
225-225: Do not catch blind exception: Exception
(BLE001)
442-442: Consider moving this statement to an else block
(TRY300)
444-444: Do not catch blind exception: Exception
(BLE001)
678-678: Consider moving this statement to an else block
(TRY300)
680-680: Do not catch blind exception: Exception
(BLE001)
725-725: Do not catch blind exception: Exception
(BLE001)
learning-examples/letsbonk-buy-sell/manual_buy_exact_out.py
198-198: Consider moving this statement to an else block
(TRY300)
200-200: Do not catch blind exception: Exception
(BLE001)
227-227: Do not catch blind exception: Exception
(BLE001)
432-434: Abstract raise to an inner function
(TRY301)
432-434: Avoid specifying long messages outside the exception class
(TRY003)
452-452: Consider moving this statement to an else block
(TRY300)
454-454: Do not catch blind exception: Exception
(BLE001)
689-689: Consider moving this statement to an else block
(TRY300)
691-691: Do not catch blind exception: Exception
(BLE001)
730-730: Do not catch blind exception: Exception
(BLE001)
learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py
198-198: Consider moving this statement to an else block
(TRY300)
200-200: Do not catch blind exception: Exception
(BLE001)
227-227: Do not catch blind exception: Exception
(BLE001)
449-449: Consider moving this statement to an else block
(TRY300)
451-451: Do not catch blind exception: Exception
(BLE001)
680-680: Consider moving this statement to an else block
(TRY300)
682-682: Do not catch blind exception: Exception
(BLE001)
728-728: Do not catch blind exception: Exception
(BLE001)
745-745: Do not catch blind exception: Exception
(BLE001)
learning-examples/letsbonk-buy-sell/manual_sell_exact_out.py
198-198: Consider moving this statement to an else block
(TRY300)
200-200: Do not catch blind exception: Exception
(BLE001)
227-227: Do not catch blind exception: Exception
(BLE001)
452-452: Consider moving this statement to an else block
(TRY300)
454-454: Do not catch blind exception: Exception
(BLE001)
682-682: Consider moving this statement to an else block
(TRY300)
684-684: Do not catch blind exception: Exception
(BLE001)
729-729: Do not catch blind exception: Exception
(BLE001)
746-746: Do not catch blind exception: Exception
(BLE001)
🔇 Additional comments (18)
idl/raydium_launchlab_idl.json (11)
5-5: Version bump looks correct and aligns with new features.IDL metadata version set to "0.2.0" matches added instructions/types and error codes used by the new examples.
390-559: Creator fee vault PDA seeds match example derivation.claim_creator_fee shows creator_fee_vault seeds [creator, quote_mint] and an authority seed, which aligns with the examples’ derive_creator_fee_vault(creator, quote_mint).
Please confirm that all LaunchLab pools use the same quote mint (WSOL in Letsbonk) so the examples’ PDA derivations are universally correct.
563-726: Platform fee collection path consistent with examples.claim_platform_fee indicates platform_fee_wallet-gated collection and uses recipient_token_account via ATA seeds; examples derive the fee vault via [platform_config, quote_mint], matching claim_platform_fee_from_vault.
913-1108: Vesting additions are coherent; no conflicts with trading examples.Vesting accounts/events don’t impact buy/sell flows directly; schema looks self-consistent.
1206-1324: Protocol/migrate fee collection reflects new owners; good separation.collect_fee and collect_migrate_fee clearly separate protocol and migrate fee owners; no action for examples.
1439-1511: create_platform_config includes cpswap_config and transfer-fee authority.New accounts support post-migration parameters; consistent with later migration instructions.
1653-1930: initialize: PDAs and programs align; token program addresses pinned.Seeds for pool_state/base_vault/quote_vault match examples’ derivations.
1932-2235: initialize_v2 adds AmmCreatorFeeOn; ensure caller tooling supports it.Examples don’t initialize pools, but downstream tools should be aware of the new enum.
2237-2527: Token-2022 init path is explicit and uses Token-2022 program id.Clear separation reduces ambiguity in examples (which use classic Token program).
3554-3623: Platform curve param remove/update flows added; good admin surface.No direct impact to examples.
4334-4438: New error 6018 NotEnoughRemainingAccounts directly addresses #139.This clarifies the failure observed in examples and justifies adding remaining accounts in scripts.
learning-examples/letsbonk-buy-sell/idl_parser.py (1)
250-279: Enum size computation looks correct; keep comment.Largest-variant sizing plus 1-byte discriminator aligns with Anchor layout.
learning-examples/letsbonk-buy-sell/manual_buy_exact_out.py (2)
609-620: Fix for “NotEnoughRemainingAccounts” is correct.Adding System Program, platform_fee_vault, and creator_fee_vault as remaining accounts resolves the program’s new requirement (IDL error 6018).
Run a dry-run against a known pool to confirm no “Not enough remaining accounts” error persists.
141-179: PDA seeds align with IDL; good.creator_fee_vault seeds [creator, quote_mint] and platform_fee_vault seeds [platform_config, quote_mint] match the IDL.
learning-examples/letsbonk-buy-sell/manual_buy_exact_in.py (2)
598-609: Fix for “NotEnoughRemainingAccounts” is correct here too.System Program + platform_fee_vault + creator_fee_vault appended; aligns with IDL and resolves #139.
690-721: Nice: balance check present.This prevents needless simulation/sends. Keep it.
learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py (1)
602-613: Good: remaining accounts appended — likely fixes “Not enough remaining accounts”Adding System Program, platform fee vault, and creator fee vault as remaining accounts aligns with how LaunchLab collects fees. This should resolve issue #139. Please confirm via simulation on a small trade.
Would you run the script with a tiny amount and share the simulation/confirmation logs to confirm the error is gone?
learning-examples/letsbonk-buy-sell/manual_sell_exact_out.py (1)
604-615: Good: remaining accounts added — addresses the linked errorAppending System Program + platform/creator fee vaults should satisfy the program’s remaining accounts requirement. Likely resolves “Not enough remaining accounts”.
Please confirm by running a small sell_exact_out and sharing the simulation success.
| if virtual_base <= amount_out: | ||
| raise ValueError( | ||
| f"Amount out ({amount_out}) cannot be >= virtual base reserves ({virtual_base})" | ||
| ) | ||
|
|
||
| # Calculate required input using rearranged constant product formula | ||
| numerator = amount_out * virtual_quote | ||
| denominator = virtual_base - amount_out | ||
| expected_input = numerator // denominator | ||
|
|
||
| # Apply slippage tolerance (add buffer for price movement) | ||
| maximum_with_slippage = int(expected_input * (1 + slippage_tolerance)) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use ceil for required input to avoid underfunding.
Floor division can underestimate amount_in; use integer ceil to ensure enough input before slippage.
Apply this diff:
- expected_input = numerator // denominator
+ # ceil(numerator / denominator)
+ expected_input = (numerator + denominator - 1) // denominator📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if virtual_base <= amount_out: | |
| raise ValueError( | |
| f"Amount out ({amount_out}) cannot be >= virtual base reserves ({virtual_base})" | |
| ) | |
| # Calculate required input using rearranged constant product formula | |
| numerator = amount_out * virtual_quote | |
| denominator = virtual_base - amount_out | |
| expected_input = numerator // denominator | |
| # Apply slippage tolerance (add buffer for price movement) | |
| maximum_with_slippage = int(expected_input * (1 + slippage_tolerance)) | |
| if virtual_base <= amount_out: | |
| raise ValueError( | |
| f"Amount out ({amount_out}) cannot be >= virtual base reserves ({virtual_base})" | |
| ) | |
| # Calculate required input using rearranged constant product formula | |
| numerator = amount_out * virtual_quote | |
| denominator = virtual_base - amount_out | |
| # ceil(numerator / denominator) | |
| expected_input = (numerator + denominator - 1) // denominator | |
| # Apply slippage tolerance (add buffer for price movement) | |
| maximum_with_slippage = int(expected_input * (1 + slippage_tolerance)) |
🧰 Tools
🪛 Ruff (0.14.1)
432-434: Abstract raise to an inner function
(TRY301)
432-434: Avoid specifying long messages outside the exception class
(TRY003)
🤖 Prompt for AI Agents
In learning-examples/letsbonk-buy-sell/manual_buy_exact_out.py around lines 431
to 443, the code uses floor division for expected_input which can under-estimate
required amount_in; replace the integer floor division with an integer ceiling
division (e.g. expected_input = (numerator + denominator - 1) // denominator or
use math.ceil(numerator / denominator)), and compute maximum_with_slippage using
a ceiling as well (e.g. maximum_with_slippage = math.ceil(expected_input * (1 +
slippage_tolerance))); add the needed import for math if using math.ceil.
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
| # Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging | ||
| IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Broken module import path for idl_parser; make path deterministic
Import will fail because sys.path is bumped two levels up while idl_parser.py lives alongside this script. Also hardcoded relative IDL path is brittle. Fix by appending this file’s directory to sys.path and resolving the IDL path from file.
Apply:
+import pathlib
@@
-sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging
-IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True)
+BASE_DIR = pathlib.Path(__file__).resolve().parent
+if str(BASE_DIR) not in sys.path:
+ sys.path.append(str(BASE_DIR))
+# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging
+IDL_PATH = BASE_DIR / "idl" / "raydium_launchlab_idl.json"
+IDL_PARSER = load_idl_parser(str(IDL_PATH), verbose=True)And keep from idl_parser import load_idl_parser as-is so it resolves from BASE_DIR. As per coding guidelines.
Also applies to: 24-25
🤖 Prompt for AI Agents
learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py around lines 36 to
39 (also applies to lines 24-25): the current sys.path modification jumps two
directories up and the IDL path is a hardcoded relative path which breaks module
resolution; instead append this file's directory (os.path.dirname(__file__)) to
sys.path so idl_parser.py resolves reliably, and build the IDL JSON path using
os.path.join(os.path.dirname(__file__), "idl", "raydium_launchlab_idl.json")
(keep the existing from idl_parser import load_idl_parser import as-is).
| RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") | ||
| PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) | ||
| PAYER = Keypair.from_bytes(PRIVATE_KEY) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate env vars before use to avoid crashes and improve UX
Decoding a missing SOLANA_PRIVATE_KEY or using a None RPC endpoint will throw. Validate early with clear errors.
-RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
-PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
-PAYER = Keypair.from_bytes(PRIVATE_KEY)
+RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
+PRIVATE_KEY_B58 = os.environ.get("SOLANA_PRIVATE_KEY")
+if not RPC_ENDPOINT or not PRIVATE_KEY_B58:
+ raise EnvironmentError(
+ "Set SOLANA_NODE_RPC_ENDPOINT and SOLANA_PRIVATE_KEY environment variables."
+ )
+PRIVATE_KEY = base58.b58decode(PRIVATE_KEY_B58)
+PAYER = Keypair.from_bytes(PRIVATE_KEY)Based on coding guidelines.
🤖 Prompt for AI Agents
In learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py around lines 47
to 50, the code reads SOLANA_NODE_RPC_ENDPOINT and SOLANA_PRIVATE_KEY from the
environment and immediately decodes/uses them which will crash if either is
missing or malformed; add explicit validation: verify SOLANA_NODE_RPC_ENDPOINT
is present and non-empty and raise/exit with a clear error if not, read
SOLANA_PRIVATE_KEY and check it is present before attempting base58 decode, wrap
the base58.b58decode call in try/except to catch decoding errors and raise a
clear message, and only call Keypair.from_bytes after successful decode; ensure
error messages state which env var is missing or invalid so the user can fix it.
| try: | ||
| account_info = await client.get_account_info(pool_state) | ||
| if not account_info.value: | ||
| print("Pool state account not found") | ||
| return None | ||
|
|
||
| return decode_pool_state(account_info.value.data) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decode account data bytes correctly; request base64 and decode
decode_pool_state expects bytes, but get_account_info returns base64-encoded data. Ask RPC for base64 and decode before passing to decoder.
- account_info = await client.get_account_info(pool_state)
+ import base64
+ account_info = await client.get_account_info(pool_state, encoding="base64")
@@
- return decode_pool_state(account_info.value.data)
+ data_b64, _encoding = account_info.value.data
+ return decode_pool_state(base64.b64decode(data_b64))As per relevant snippets in src/core/client.py.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py around lines
219-226, the code passes account_info.value.data (which is base64 text) directly
to decode_pool_state which expects raw bytes; update the RPC call to request
base64-encoded data (e.g., pass encoding="base64" or the equivalent parameter
used by our client), then base64-decode account_info.value.data into bytes and
pass those bytes into decode_pool_state, preserving the existing presence check
and return path.
| # Calculate required lamports (minimal amount for account creation) | ||
| account_creation_lamports = 2_039_280 # Standard account creation cost | ||
|
|
||
| user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed( | ||
| PAYER.pubkey(), wsol_seed, account_creation_lamports | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Avoid hardcoding rent-exempt lamports; query RPC
Rent exemption changes over time. Use RPC to fetch the exact minimum for 165-byte token accounts.
- # Calculate required lamports (minimal amount for account creation)
- account_creation_lamports = 2_039_280 # Standard account creation cost
+ # Calculate required lamports (rent-exempt) for a 165-byte token account
+ account_creation_lamports = await client.get_minimum_balance_for_rent_exemption(
+ 165
+ )
+ if hasattr(account_creation_lamports, "value"):
+ account_creation_lamports = account_creation_lamports.value🤖 Prompt for AI Agents
In learning-examples/letsbonk-buy-sell/manual_sell_exact_in.py around lines 543
to 548, the lamports for rent-exemption are hardcoded; replace that with an RPC
query to fetch the precise minimum for a 165-byte token account using your
Solana connection (call get_minimum_balance_for_rent_exemption(165) or the async
equivalent), assign the returned value to account_creation_lamports, and use
that variable when creating the WSOL account; if the connection call can fail,
add a short fallback or raise a clear error so the code does not silently
continue with an incorrect hardcoded value.
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
| # Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging | ||
| IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Same import-path fragility for idl_parser and IDL file path
Mirror the import/path fix from the other script: append this dir to sys.path and resolve IDL JSON from file.
+import pathlib
@@
-sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging
-IDL_PARSER = load_idl_parser("idl/raydium_launchlab_idl.json", verbose=True)
+BASE_DIR = pathlib.Path(__file__).resolve().parent
+if str(BASE_DIR) not in sys.path:
+ sys.path.append(str(BASE_DIR))
+# Initialize IDL parser for Raydium LaunchLab with verbose mode for debugging
+IDL_PATH = BASE_DIR / "idl" / "raydium_launchlab_idl.json"
+IDL_PARSER = load_idl_parser(str(IDL_PATH), verbose=True)Also applies to: 24-25
🤖 Prompt for AI Agents
In learning-examples/letsbonk-buy-sell/manual_sell_exact_out.py around lines
36-39 (also applies to lines 24-25), the current sys.path manipulation and IDL
JSON reference are fragile; change it to append the script's directory
(os.path.dirname(__file__)) to sys.path and resolve the IDL file path relative
to __file__ (e.g. build the absolute path with
os.path.join(os.path.dirname(__file__), 'idl', 'raydium_launchlab_idl.json'))
before calling load_idl_parser so the import and JSON file load work reliably
regardless of CWD.
| RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") | ||
| PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) | ||
| PAYER = Keypair.from_bytes(PRIVATE_KEY) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard env reads (RPC endpoint, private key) with explicit validation
Avoid TypeError on base58 decode and give clear guidance when env vars are missing.
-RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
-PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
-PAYER = Keypair.from_bytes(PRIVATE_KEY)
+RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
+PRIVATE_KEY_B58 = os.environ.get("SOLANA_PRIVATE_KEY")
+if not RPC_ENDPOINT or not PRIVATE_KEY_B58:
+ raise EnvironmentError(
+ "Set SOLANA_NODE_RPC_ENDPOINT and SOLANA_PRIVATE_KEY environment variables."
+ )
+PRIVATE_KEY = base58.b58decode(PRIVATE_KEY_B58)
+PAYER = Keypair.from_bytes(PRIVATE_KEY)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") | |
| PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) | |
| PAYER = Keypair.from_bytes(PRIVATE_KEY) | |
| RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") | |
| PRIVATE_KEY_B58 = os.environ.get("SOLANA_PRIVATE_KEY") | |
| if not RPC_ENDPOINT or not PRIVATE_KEY_B58: | |
| raise EnvironmentError( | |
| "Set SOLANA_NODE_RPC_ENDPOINT and SOLANA_PRIVATE_KEY environment variables." | |
| ) | |
| PRIVATE_KEY = base58.b58decode(PRIVATE_KEY_B58) | |
| PAYER = Keypair.from_bytes(PRIVATE_KEY) |
| try: | ||
| account_info = await client.get_account_info(pool_state) | ||
| if not account_info.value: | ||
| print("Pool state account not found") | ||
| return None | ||
|
|
||
| return decode_pool_state(account_info.value.data) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decode account_info data to bytes before IDL parsing
Same as the exact-in script: request base64 encoding and decode.
- account_info = await client.get_account_info(pool_state)
+ import base64
+ account_info = await client.get_account_info(pool_state, encoding="base64")
@@
- return decode_pool_state(account_info.value.data)
+ data_b64, _encoding = account_info.value.data
+ return decode_pool_state(base64.b64decode(data_b64))Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In learning-examples/letsbonk-buy-sell/manual_sell_exact_out.py around lines
219-226, account_info.value.data is being passed directly to decode_pool_state
but must be decoded from base64 bytes first; update the get_account_info call to
request base64 encoding (e.g., encoding='base64') if not already, then when
account_info.value exists decode account_info.value.data from base64 into bytes
and pass those bytes into decode_pool_state (keep the existing missing-account
check and return logic).
| # Calculate required lamports (minimal amount for account creation) | ||
| account_creation_lamports = 2_039_280 # Standard account creation cost | ||
|
|
||
| user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed( | ||
| PAYER.pubkey(), wsol_seed, account_creation_lamports | ||
| ) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Compute rent-exempt lamports dynamically
Use RPC query instead of a magic number for token account size 165.
- account_creation_lamports = 2_039_280 # Standard account creation cost
+ account_creation_lamports = await client.get_minimum_balance_for_rent_exemption(
+ 165
+ )
+ if hasattr(account_creation_lamports, "value"):
+ account_creation_lamports = account_creation_lamports.value📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Calculate required lamports (minimal amount for account creation) | |
| account_creation_lamports = 2_039_280 # Standard account creation cost | |
| user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed( | |
| PAYER.pubkey(), wsol_seed, account_creation_lamports | |
| ) | |
| # Calculate required lamports (minimal amount for account creation) | |
| account_creation_lamports = await client.get_minimum_balance_for_rent_exemption( | |
| 165 | |
| ) | |
| if hasattr(account_creation_lamports, "value"): | |
| account_creation_lamports = account_creation_lamports.value | |
| user_quote_token, create_wsol_ix, init_wsol_ix = create_wsol_account_with_seed( | |
| PAYER.pubkey(), wsol_seed, account_creation_lamports | |
| ) |
🤖 Prompt for AI Agents
In learning-examples/letsbonk-buy-sell/manual_sell_exact_out.py around lines 545
to 551, replace the hard-coded account_creation_lamports = 2_039_280 with a
dynamic RPC call to fetch the rent-exempt balance for a token account of size
165 (e.g. await connection.get_minimum_balance_for_rent_exemption(165) or
equivalent on the RPC client used), assign that returned value to
account_creation_lamports, and use that variable in
create_wsol_account_with_seed so the lamports are computed at runtime rather
than using a magic number.
Summary by CodeRabbit