Skip to content

Commit 68cac87

Browse files
authored
Fix/update letsbonk fun instructions in the bot (#142)
* feat(core): support multiple initialize instruction variants in LetsBonkEventParser * feat(core): add creator and platform fee vault derivation methods and update account handling in buy/sell instructions * feat(letsbonk): add global_config and platform_config to TokenInfo and update address provider logic
1 parent 7377e17 commit 68cac87

File tree

5 files changed

+212
-29
lines changed

5 files changed

+212
-29
lines changed

src/interfaces/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class TokenInfo:
3838
pool_state: Pubkey | None = None # LetsBonk specific
3939
base_vault: Pubkey | None = None # LetsBonk specific
4040
quote_vault: Pubkey | None = None # LetsBonk specific
41+
global_config: Pubkey | None = None # LetsBonk specific
42+
platform_config: Pubkey | None = None # LetsBonk specific
4143

4244
# Common fields
4345
user: Pubkey | None = None

src/platforms/letsbonk/address_provider.py

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,22 @@ class LetsBonkAddresses:
2323
PROGRAM: Final[Pubkey] = Pubkey.from_string(
2424
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"
2525
)
26+
# NOTE: GLOBAL_CONFIG is NOT constant across all pools!
27+
# Each pool can be initialized with different global_config values. Different global_configs
28+
# may define different program-wide settings, versions, or operational parameters.
29+
# The correct global_config for each pool is extracted during pool initialization
30+
# and stored in TokenInfo.global_config. This value below is a default, used as a fallback.
2631
GLOBAL_CONFIG: Final[Pubkey] = Pubkey.from_string(
2732
"6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX"
2833
)
34+
# NOTE: PLATFORM_CONFIG is NOT constant across all pools!
35+
# Each pool is initialized with a specific platform_config that defines its fee structure,
36+
# launch restrictions, and other settings. Different pools may use different platform_configs
37+
# (e.g., standard launches vs partner launches). The correct platform_config for each pool
38+
# is extracted during pool initialization and stored in TokenInfo.platform_config.
39+
# This value below is the default/most common platform_config, used as a fallback.
2940
PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string(
30-
"FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1"
41+
"5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA"
3142
)
3243

3344

@@ -212,6 +223,53 @@ def derive_event_authority_pda(self) -> Pubkey:
212223
)
213224
return event_authority_pda
214225

226+
def derive_creator_fee_vault(
227+
self, creator: Pubkey, quote_mint: Pubkey | None = None
228+
) -> Pubkey:
229+
"""Derive the creator fee vault PDA.
230+
231+
This vault accumulates creator fees from trades.
232+
233+
Args:
234+
creator: The pool creator's pubkey
235+
quote_mint: The quote token mint (defaults to WSOL)
236+
237+
Returns:
238+
Creator fee vault address
239+
"""
240+
if quote_mint is None:
241+
quote_mint = SystemAddresses.SOL_MINT
242+
243+
creator_fee_vault, _ = Pubkey.find_program_address(
244+
[bytes(creator), bytes(quote_mint)], LetsBonkAddresses.PROGRAM
245+
)
246+
return creator_fee_vault
247+
248+
def derive_platform_fee_vault(
249+
self, platform_config: Pubkey | None = None, quote_mint: Pubkey | None = None
250+
) -> Pubkey:
251+
"""Derive the platform fee vault PDA.
252+
253+
This vault accumulates platform fees from trades.
254+
255+
Args:
256+
platform_config: The platform config account (defaults to LetsBonk config)
257+
quote_mint: The quote token mint (defaults to WSOL)
258+
259+
Returns:
260+
Platform fee vault address
261+
"""
262+
if platform_config is None:
263+
platform_config = LetsBonkAddresses.PLATFORM_CONFIG
264+
265+
if quote_mint is None:
266+
quote_mint = SystemAddresses.SOL_MINT
267+
268+
platform_fee_vault, _ = Pubkey.find_program_address(
269+
[bytes(platform_config), bytes(quote_mint)], LetsBonkAddresses.PROGRAM
270+
)
271+
return platform_fee_vault
272+
215273
def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey:
216274
"""Create a WSOL account address using createAccountWithSeed pattern.
217275
@@ -238,11 +296,25 @@ def get_buy_instruction_accounts(
238296
"""
239297
additional_accounts = self.get_additional_accounts(token_info)
240298

241-
return {
299+
# Use global_config from TokenInfo if available, otherwise use default
300+
global_config = (
301+
token_info.global_config
302+
if token_info.global_config
303+
else LetsBonkAddresses.GLOBAL_CONFIG
304+
)
305+
306+
# Use platform_config from TokenInfo if available, otherwise use default
307+
platform_config = (
308+
token_info.platform_config
309+
if token_info.platform_config
310+
else LetsBonkAddresses.PLATFORM_CONFIG
311+
)
312+
313+
accounts = {
242314
"payer": user,
243315
"authority": additional_accounts["authority"],
244-
"global_config": LetsBonkAddresses.GLOBAL_CONFIG,
245-
"platform_config": LetsBonkAddresses.PLATFORM_CONFIG,
316+
"global_config": global_config,
317+
"platform_config": platform_config,
246318
"pool_state": additional_accounts["pool_state"],
247319
"user_base_token": self.derive_user_token_account(user, token_info.mint),
248320
"base_vault": additional_accounts["base_vault"],
@@ -253,8 +325,18 @@ def get_buy_instruction_accounts(
253325
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
254326
"event_authority": additional_accounts["event_authority"],
255327
"program": LetsBonkAddresses.PROGRAM,
328+
"system_program": SystemAddresses.SYSTEM_PROGRAM,
329+
"platform_fee_vault": self.derive_platform_fee_vault(platform_config),
256330
}
257331

332+
# Add creator fee vault if creator is known
333+
if token_info.creator:
334+
accounts["creator_fee_vault"] = self.derive_creator_fee_vault(
335+
token_info.creator
336+
)
337+
338+
return accounts
339+
258340
def get_sell_instruction_accounts(
259341
self, token_info: TokenInfo, user: Pubkey
260342
) -> dict[str, Pubkey]:
@@ -269,11 +351,25 @@ def get_sell_instruction_accounts(
269351
"""
270352
additional_accounts = self.get_additional_accounts(token_info)
271353

272-
return {
354+
# Use global_config from TokenInfo if available, otherwise use default
355+
global_config = (
356+
token_info.global_config
357+
if token_info.global_config
358+
else LetsBonkAddresses.GLOBAL_CONFIG
359+
)
360+
361+
# Use platform_config from TokenInfo if available, otherwise use default
362+
platform_config = (
363+
token_info.platform_config
364+
if token_info.platform_config
365+
else LetsBonkAddresses.PLATFORM_CONFIG
366+
)
367+
368+
accounts = {
273369
"payer": user,
274370
"authority": additional_accounts["authority"],
275-
"global_config": LetsBonkAddresses.GLOBAL_CONFIG,
276-
"platform_config": LetsBonkAddresses.PLATFORM_CONFIG,
371+
"global_config": global_config,
372+
"platform_config": platform_config,
277373
"pool_state": additional_accounts["pool_state"],
278374
"user_base_token": self.derive_user_token_account(user, token_info.mint),
279375
"base_vault": additional_accounts["base_vault"],
@@ -284,8 +380,18 @@ def get_sell_instruction_accounts(
284380
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
285381
"event_authority": additional_accounts["event_authority"],
286382
"program": LetsBonkAddresses.PROGRAM,
383+
"system_program": SystemAddresses.SYSTEM_PROGRAM,
384+
"platform_fee_vault": self.derive_platform_fee_vault(platform_config),
287385
}
288386

387+
# Add creator fee vault if creator is known
388+
if token_info.creator:
389+
accounts["creator_fee_vault"] = self.derive_creator_fee_vault(
390+
token_info.creator
391+
)
392+
393+
return accounts
394+
289395
def get_wsol_account_creation_accounts(
290396
self, user: Pubkey, wsol_account: Pubkey
291397
) -> dict[str, Pubkey]:

src/platforms/letsbonk/curve_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]:
186186
"real_quote": decoded_pool_state.get("real_quote", 0),
187187
"status": decoded_pool_state.get("status", 0),
188188
"supply": decoded_pool_state.get("supply", 0),
189+
"creator": decoded_pool_state.get("creator"), # Creator pubkey (as base58 string)
190+
"base_vault": decoded_pool_state.get("base_vault"), # Base vault pubkey
191+
"quote_vault": decoded_pool_state.get("quote_vault"), # Quote vault pubkey
189192
}
190193

191194
# Calculate additional metrics

src/platforms/letsbonk/event_parser.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,23 @@ def __init__(self, idl_parser: IDLParser):
3333
self.address_provider = LetsBonkAddressProvider()
3434
self._idl_parser = idl_parser
3535

36-
# Get discriminators from injected IDL parser
36+
# Get all initialize instruction discriminators from injected IDL parser
37+
# LetsBonk has multiple initialize variants: initialize, initialize_v2, initialize_with_token_2022
3738
discriminators = self._idl_parser.get_instruction_discriminators()
38-
self._initialize_discriminator_bytes = discriminators["initialize"]
39-
self._initialize_discriminator = struct.unpack(
40-
"<Q", self._initialize_discriminator_bytes
41-
)[0]
42-
43-
logger.info("LetsBonk event parser initialized with injected IDL parser")
39+
self._initialize_discriminator_bytes_list = [
40+
discriminators["initialize"],
41+
discriminators["initialize_v2"],
42+
discriminators["initialize_with_token_2022"],
43+
]
44+
self._initialize_discriminators = {
45+
struct.unpack("<Q", disc_bytes)[0]
46+
for disc_bytes in self._initialize_discriminator_bytes_list
47+
}
48+
49+
logger.info(
50+
f"LetsBonk event parser initialized with {len(self._initialize_discriminators)} "
51+
f"initialize instruction variants"
52+
)
4453

4554
@property
4655
def platform(self) -> Platform:
@@ -76,7 +85,11 @@ def parse_token_creation_from_instruction(
7685
Returns:
7786
TokenInfo if token creation found, None otherwise
7887
"""
79-
if not instruction_data.startswith(self._initialize_discriminator_bytes):
88+
# Check if instruction starts with any of the initialize discriminators
89+
if not any(
90+
instruction_data.startswith(disc_bytes)
91+
for disc_bytes in self._initialize_discriminator_bytes_list
92+
):
8093
return None
8194

8295
try:
@@ -93,7 +106,12 @@ def get_account_key(index):
93106
decoded = self._idl_parser.decode_instruction(
94107
instruction_data, account_keys, accounts
95108
)
96-
if not decoded or decoded["instruction_name"] != "initialize":
109+
# Accept any of the initialize instruction variants
110+
if not decoded or decoded["instruction_name"] not in {
111+
"initialize",
112+
"initialize_v2",
113+
"initialize_with_token_2022",
114+
}:
97115
return None
98116

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

120138
creator = get_account_key(0) # First signer account (creator)
139+
global_config = get_account_key(2) # global_config account
140+
platform_config = get_account_key(3) # platform_config account
121141
pool_state = get_account_key(5) # pool_state account
122142
base_mint = get_account_key(6) # base_mint account
123143
base_vault = get_account_key(8) # base_vault account
124144
quote_vault = get_account_key(9) # quote_vault account
125145

126-
if not all([creator, pool_state, base_mint, base_vault, quote_vault]):
146+
if not all([creator, global_config, platform_config, pool_state, base_mint, base_vault, quote_vault]):
127147
logger.debug(
128-
f"Missing required accounts: creator={creator}, pool_state={pool_state}, "
129-
f"base_mint={base_mint}, base_vault={base_vault}, quote_vault={quote_vault}"
148+
f"Missing required accounts: creator={creator}, global_config={global_config}, "
149+
f"platform_config={platform_config}, pool_state={pool_state}, base_mint={base_mint}, "
150+
f"base_vault={base_vault}, quote_vault={quote_vault}"
130151
)
131152
return None
132153

@@ -139,6 +160,8 @@ def get_account_key(index):
139160
pool_state=pool_state,
140161
base_vault=base_vault,
141162
quote_vault=quote_vault,
163+
global_config=global_config,
164+
platform_config=platform_config,
142165
user=creator,
143166
creator=creator,
144167
creation_timestamp=monotonic(),
@@ -219,9 +242,9 @@ def get_instruction_discriminators(self) -> list[bytes]:
219242
"""Get instruction discriminators for token creation.
220243
221244
Returns:
222-
List of discriminator bytes to match
245+
List of discriminator bytes to match (all initialize variants)
223246
"""
224-
return [self._initialize_discriminator_bytes]
247+
return self._initialize_discriminator_bytes_list
225248

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

260283
ix_data = bytes(ix.data)
261284

262-
# Check for initialize discriminator
285+
# Check for any initialize discriminator variant
263286
if len(ix_data) >= 8:
264287
discriminator = struct.unpack("<Q", ix_data[:8])[0]
265288

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

321-
if discriminator == self._initialize_discriminator:
344+
if discriminator in self._initialize_discriminators:
322345
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
323346
continue
324347

0 commit comments

Comments
 (0)