Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/interfaces/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class TokenInfo:
pool_state: Pubkey | None = None # LetsBonk specific
base_vault: Pubkey | None = None # LetsBonk specific
quote_vault: Pubkey | None = None # LetsBonk specific
global_config: Pubkey | None = None # LetsBonk specific
platform_config: Pubkey | None = None # LetsBonk specific

# Common fields
user: Pubkey | None = None
Expand Down
120 changes: 113 additions & 7 deletions src/platforms/letsbonk/address_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,22 @@ class LetsBonkAddresses:
PROGRAM: Final[Pubkey] = Pubkey.from_string(
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"
)
# NOTE: GLOBAL_CONFIG is NOT constant across all pools!
# Each pool can be initialized with different global_config values. Different global_configs
# may define different program-wide settings, versions, or operational parameters.
# The correct global_config for each pool is extracted during pool initialization
# and stored in TokenInfo.global_config. This value below is a default, used as a fallback.
GLOBAL_CONFIG: Final[Pubkey] = Pubkey.from_string(
"6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX"
)
# NOTE: PLATFORM_CONFIG is NOT constant across all pools!
# Each pool is initialized with a specific platform_config that defines its fee structure,
# launch restrictions, and other settings. Different pools may use different platform_configs
# (e.g., standard launches vs partner launches). The correct platform_config for each pool
# is extracted during pool initialization and stored in TokenInfo.platform_config.
# This value below is the default/most common platform_config, used as a fallback.
PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string(
"FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1"
"5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA"
)


Expand Down Expand Up @@ -212,6 +223,53 @@ def derive_event_authority_pda(self) -> Pubkey:
)
return event_authority_pda

def derive_creator_fee_vault(
self, creator: Pubkey, quote_mint: Pubkey | None = None
) -> Pubkey:
"""Derive the creator fee vault PDA.

This vault accumulates creator fees from trades.

Args:
creator: The pool creator's pubkey
quote_mint: The quote token mint (defaults to WSOL)

Returns:
Creator fee vault address
"""
if quote_mint is None:
quote_mint = SystemAddresses.SOL_MINT

creator_fee_vault, _ = Pubkey.find_program_address(
[bytes(creator), bytes(quote_mint)], LetsBonkAddresses.PROGRAM
)
return creator_fee_vault

def derive_platform_fee_vault(
self, platform_config: Pubkey | None = None, quote_mint: Pubkey | None = None
) -> Pubkey:
"""Derive the platform fee vault PDA.

This vault accumulates platform fees from trades.

Args:
platform_config: The platform config account (defaults to LetsBonk config)
quote_mint: The quote token mint (defaults to WSOL)

Returns:
Platform fee vault address
"""
if platform_config is None:
platform_config = LetsBonkAddresses.PLATFORM_CONFIG

if quote_mint is None:
quote_mint = SystemAddresses.SOL_MINT

platform_fee_vault, _ = Pubkey.find_program_address(
[bytes(platform_config), bytes(quote_mint)], LetsBonkAddresses.PROGRAM
)
return platform_fee_vault

def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey:
"""Create a WSOL account address using createAccountWithSeed pattern.

Expand All @@ -238,11 +296,25 @@ def get_buy_instruction_accounts(
"""
additional_accounts = self.get_additional_accounts(token_info)

return {
# Use global_config from TokenInfo if available, otherwise use default
global_config = (
token_info.global_config
if token_info.global_config
else LetsBonkAddresses.GLOBAL_CONFIG
)

# Use platform_config from TokenInfo if available, otherwise use default
platform_config = (
token_info.platform_config
if token_info.platform_config
else LetsBonkAddresses.PLATFORM_CONFIG
)

accounts = {
"payer": user,
"authority": additional_accounts["authority"],
"global_config": LetsBonkAddresses.GLOBAL_CONFIG,
"platform_config": LetsBonkAddresses.PLATFORM_CONFIG,
"global_config": global_config,
"platform_config": platform_config,
"pool_state": additional_accounts["pool_state"],
"user_base_token": self.derive_user_token_account(user, token_info.mint),
"base_vault": additional_accounts["base_vault"],
Expand All @@ -253,8 +325,18 @@ def get_buy_instruction_accounts(
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": additional_accounts["event_authority"],
"program": LetsBonkAddresses.PROGRAM,
"system_program": SystemAddresses.SYSTEM_PROGRAM,
"platform_fee_vault": self.derive_platform_fee_vault(platform_config),
}

# Add creator fee vault if creator is known
if token_info.creator:
accounts["creator_fee_vault"] = self.derive_creator_fee_vault(
token_info.creator
)

return accounts

def get_sell_instruction_accounts(
self, token_info: TokenInfo, user: Pubkey
) -> dict[str, Pubkey]:
Expand All @@ -269,11 +351,25 @@ def get_sell_instruction_accounts(
"""
additional_accounts = self.get_additional_accounts(token_info)

return {
# Use global_config from TokenInfo if available, otherwise use default
global_config = (
token_info.global_config
if token_info.global_config
else LetsBonkAddresses.GLOBAL_CONFIG
)

# Use platform_config from TokenInfo if available, otherwise use default
platform_config = (
token_info.platform_config
if token_info.platform_config
else LetsBonkAddresses.PLATFORM_CONFIG
)

accounts = {
"payer": user,
"authority": additional_accounts["authority"],
"global_config": LetsBonkAddresses.GLOBAL_CONFIG,
"platform_config": LetsBonkAddresses.PLATFORM_CONFIG,
"global_config": global_config,
"platform_config": platform_config,
"pool_state": additional_accounts["pool_state"],
"user_base_token": self.derive_user_token_account(user, token_info.mint),
"base_vault": additional_accounts["base_vault"],
Expand All @@ -284,8 +380,18 @@ def get_sell_instruction_accounts(
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": additional_accounts["event_authority"],
"program": LetsBonkAddresses.PROGRAM,
"system_program": SystemAddresses.SYSTEM_PROGRAM,
"platform_fee_vault": self.derive_platform_fee_vault(platform_config),
}

# Add creator fee vault if creator is known
if token_info.creator:
accounts["creator_fee_vault"] = self.derive_creator_fee_vault(
token_info.creator
)

return accounts

def get_wsol_account_creation_accounts(
self, user: Pubkey, wsol_account: Pubkey
) -> dict[str, Pubkey]:
Expand Down
3 changes: 3 additions & 0 deletions src/platforms/letsbonk/curve_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]:
"real_quote": decoded_pool_state.get("real_quote", 0),
"status": decoded_pool_state.get("status", 0),
"supply": decoded_pool_state.get("supply", 0),
"creator": decoded_pool_state.get("creator"), # Creator pubkey (as base58 string)
"base_vault": decoded_pool_state.get("base_vault"), # Base vault pubkey
"quote_vault": decoded_pool_state.get("quote_vault"), # Quote vault pubkey
}

# Calculate additional metrics
Expand Down
57 changes: 40 additions & 17 deletions src/platforms/letsbonk/event_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,23 @@ def __init__(self, idl_parser: IDLParser):
self.address_provider = LetsBonkAddressProvider()
self._idl_parser = idl_parser

# Get discriminators from injected IDL parser
# Get all initialize instruction discriminators from injected IDL parser
# LetsBonk has multiple initialize variants: initialize, initialize_v2, initialize_with_token_2022
discriminators = self._idl_parser.get_instruction_discriminators()
self._initialize_discriminator_bytes = discriminators["initialize"]
self._initialize_discriminator = struct.unpack(
"<Q", self._initialize_discriminator_bytes
)[0]

logger.info("LetsBonk event parser initialized with injected IDL parser")
self._initialize_discriminator_bytes_list = [
discriminators["initialize"],
discriminators["initialize_v2"],
discriminators["initialize_with_token_2022"],
]
self._initialize_discriminators = {
struct.unpack("<Q", disc_bytes)[0]
for disc_bytes in self._initialize_discriminator_bytes_list
}

logger.info(
f"LetsBonk event parser initialized with {len(self._initialize_discriminators)} "
f"initialize instruction variants"
)

Comment on lines +36 to 53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Good: supporting multiple initialize variants. Add a Token‑2022 hint for downstream.

Set a flag that indicates when the initialize variant is initialize_with_token_2022 so builders/address providers can choose the correct token program.

Apply:

@@ def parse_token_creation_from_instruction(...):
-            # Accept any of the initialize instruction variants
+            # Accept any of the initialize instruction variants
             if not decoded or decoded["instruction_name"] not in {
                 "initialize",
                 "initialize_v2",
                 "initialize_with_token_2022",
             }:
                 return None
@@
-            return TokenInfo(
+            is_token_2022 = decoded["instruction_name"] == "initialize_with_token_2022"
+            return TokenInfo(
                 name=base_mint_param.get("name", ""),
                 symbol=base_mint_param.get("symbol", ""),
                 uri=base_mint_param.get("uri", ""),
                 mint=base_mint,
                 platform=Platform.LETS_BONK,
                 pool_state=pool_state,
                 base_vault=base_vault,
                 quote_vault=quote_vault,
                 global_config=global_config,
                 platform_config=platform_config,
                 user=creator,
                 creator=creator,
                 creation_timestamp=monotonic(),
+                additional_data={"is_token_2022": is_token_2022},
             )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/platforms/letsbonk/event_parser.py around lines 36 to 53, add a boolean
flag on the parser that records whether the initialize variant include the
initialize_with_token_2022 discriminator so downstream builders/address
providers can pick the correct token program; after you build
self._initialize_discriminator_bytes_list or self._initialize_discriminators set
self._initialize_with_token_2022 = True if
discriminators["initialize_with_token_2022"] (or its unpacked integer) is
present in the list/set, otherwise set it to False, and ensure the logger still
reports the initialized variants (you can include the new flag in the state but
do not change logging semantics unless you want to mention the flag in the
message).

@property
def platform(self) -> Platform:
Expand Down Expand Up @@ -76,7 +85,11 @@ def parse_token_creation_from_instruction(
Returns:
TokenInfo if token creation found, None otherwise
"""
if not instruction_data.startswith(self._initialize_discriminator_bytes):
# Check if instruction starts with any of the initialize discriminators
if not any(
instruction_data.startswith(disc_bytes)
for disc_bytes in self._initialize_discriminator_bytes_list
):
return None

try:
Expand All @@ -93,7 +106,12 @@ def get_account_key(index):
decoded = self._idl_parser.decode_instruction(
instruction_data, account_keys, accounts
)
if not decoded or decoded["instruction_name"] != "initialize":
# Accept any of the initialize instruction variants
if not decoded or decoded["instruction_name"] not in {
"initialize",
"initialize_v2",
"initialize_with_token_2022",
}:
return None

args = decoded.get("args", {})
Expand All @@ -118,15 +136,18 @@ def get_account_key(index):
# ... other accounts

creator = get_account_key(0) # First signer account (creator)
global_config = get_account_key(2) # global_config account
platform_config = get_account_key(3) # platform_config account
pool_state = get_account_key(5) # pool_state account
base_mint = get_account_key(6) # base_mint account
base_vault = get_account_key(8) # base_vault account
quote_vault = get_account_key(9) # quote_vault account

if not all([creator, pool_state, base_mint, base_vault, quote_vault]):
if not all([creator, global_config, platform_config, pool_state, base_mint, base_vault, quote_vault]):
logger.debug(
f"Missing required accounts: creator={creator}, pool_state={pool_state}, "
f"base_mint={base_mint}, base_vault={base_vault}, quote_vault={quote_vault}"
f"Missing required accounts: creator={creator}, global_config={global_config}, "
f"platform_config={platform_config}, pool_state={pool_state}, base_mint={base_mint}, "
f"base_vault={base_vault}, quote_vault={quote_vault}"
)
return None

Expand All @@ -139,6 +160,8 @@ def get_account_key(index):
pool_state=pool_state,
base_vault=base_vault,
quote_vault=quote_vault,
global_config=global_config,
platform_config=platform_config,
user=creator,
creator=creator,
creation_timestamp=monotonic(),
Expand Down Expand Up @@ -219,9 +242,9 @@ def get_instruction_discriminators(self) -> list[bytes]:
"""Get instruction discriminators for token creation.

Returns:
List of discriminator bytes to match
List of discriminator bytes to match (all initialize variants)
"""
return [self._initialize_discriminator_bytes]
return self._initialize_discriminator_bytes_list

def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
"""Parse token creation from block data (for block listener).
Expand Down Expand Up @@ -259,11 +282,11 @@ def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:

ix_data = bytes(ix.data)

# Check for initialize discriminator
# Check for any initialize discriminator variant
if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0]

if discriminator == self._initialize_discriminator:
if discriminator in self._initialize_discriminators:
# Token creation should have substantial data and many accounts
if len(ix_data) <= 8 or len(ix.accounts) < 10:
continue
Expand Down Expand Up @@ -318,7 +341,7 @@ def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0]

if discriminator == self._initialize_discriminator:
if discriminator in self._initialize_discriminators:
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
continue

Expand Down
Loading