Skip to content

Commit 4a48f54

Browse files
smypmsaclaude
andauthored
feat(pumpfun): update IDLs and add bonding_curve_v2 + cashback support (#159)
Update all 3 pump.fun IDL files (pump_fun, pump_swap, pump_fees) with new cashback rewards and fee-sharing features. Fix IDL parser to handle tuple struct types (OptionBool). Add bonding_curve_v2 remaining account to all buy/sell instructions as required by the pump.fun program upgrade. Key changes: - Fix IDL parser crash on tuple struct fields (string vs dict) - Add bonding_curve_v2 PDA derivation and append as remaining account - Add is_cashback_coin field to TokenInfo and BondingCurve decoding - Propagate is_cashback_enabled from CreateEvent/create_v2 to TokenInfo - Conditionally include user_volume_accumulator for cashback sell txs Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0b6779c commit 4a48f54

9 files changed

Lines changed: 4717 additions & 471 deletions

File tree

idl/pump_fees.json

Lines changed: 2107 additions & 138 deletions
Large diffs are not rendered by default.

idl/pump_fun_idl.json

Lines changed: 1479 additions & 217 deletions
Large diffs are not rendered by default.

idl/pump_swap_idl.json

Lines changed: 1019 additions & 94 deletions
Large diffs are not rendered by default.

src/interfaces/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class TokenInfo:
4747
creator_vault: Pubkey | None = None
4848
token_program_id: Pubkey | None = None # Token or Token2022 program
4949
is_mayhem_mode: bool = False # pump.fun mayhem mode flag
50+
is_cashback_coin: bool = False # pump.fun cashback coin flag
5051

5152
# Metadata
5253
creation_timestamp: float | None = None

src/platforms/pumpfun/address_provider.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
7575
)
7676
return derived_address
7777

78+
@staticmethod
79+
def find_bonding_curve_v2(mint: Pubkey) -> Pubkey:
80+
"""Derive the bonding curve v2 PDA for a token mint.
81+
82+
Args:
83+
mint: Token mint address
84+
85+
Returns:
86+
Pubkey of the derived bonding curve v2 account
87+
"""
88+
derived_address, _ = Pubkey.find_program_address(
89+
[b"bonding-curve-v2", bytes(mint)],
90+
PumpFunAddresses.PROGRAM,
91+
)
92+
return derived_address
93+
7894
@staticmethod
7995
def find_fee_config() -> Pubkey:
8096
"""
@@ -256,6 +272,17 @@ def derive_user_volume_accumulator(self, user: Pubkey) -> Pubkey:
256272
"""
257273
return PumpFunAddresses.find_user_volume_accumulator(user)
258274

275+
def derive_bonding_curve_v2(self, mint: Pubkey) -> Pubkey:
276+
"""Derive the bonding curve v2 PDA for a token mint.
277+
278+
Args:
279+
mint: Token mint address
280+
281+
Returns:
282+
Bonding curve v2 address
283+
"""
284+
return PumpFunAddresses.find_bonding_curve_v2(mint)
285+
259286
def derive_fee_config(self) -> Pubkey:
260287
"""Derive the fee config PDA.
261288
@@ -326,6 +353,7 @@ def get_buy_instruction_accounts(
326353
"user_volume_accumulator": self.derive_user_volume_accumulator(user),
327354
"fee_config": self.derive_fee_config(),
328355
"fee_program": PumpFunAddresses.FEE_PROGRAM,
356+
"bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint),
329357
}
330358

331359
def get_sell_instruction_accounts(
@@ -375,4 +403,6 @@ def get_sell_instruction_accounts(
375403
"program": PumpFunAddresses.PROGRAM,
376404
"fee_config": self.derive_fee_config(),
377405
"fee_program": PumpFunAddresses.FEE_PROGRAM,
406+
"bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint),
407+
"user_volume_accumulator": self.derive_user_volume_accumulator(user),
378408
}

src/platforms/pumpfun/curve_manager.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def _decode_curve_state_with_idl(self, data: bytes) -> dict[str, Any]:
191191
"complete": decoded_curve_state.get("complete", False),
192192
"creator": decoded_curve_state.get("creator", ""),
193193
"is_mayhem_mode": decoded_curve_state.get("is_mayhem_mode", False),
194+
"is_cashback_coin": decoded_curve_state.get("is_cashback_coin", False),
194195
}
195196

196197
# Calculate additional metrics
@@ -205,10 +206,7 @@ def _decode_curve_state_with_idl(self, data: bytes) -> dict[str, Any]:
205206
)
206207

207208
curve_data["price_per_token"] = (
208-
(
209-
curve_data["virtual_sol_reserves"]
210-
/ curve_data["virtual_token_reserves"]
211-
)
209+
(curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"])
212210
* (10**TOKEN_DECIMALS)
213211
/ LAMPORTS_PER_SOL
214212
)

src/platforms/pumpfun/event_parser.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def __init__(self, idl_parser: IDLParser):
4848
)[0]
4949

5050
# Support for token2022 (create_v2 instruction)
51-
self._create_v2_instruction_discriminator_bytes = instruction_discriminators.get(
52-
"create_v2"
51+
self._create_v2_instruction_discriminator_bytes = (
52+
instruction_discriminators.get("create_v2")
5353
)
5454
self._create_v2_instruction_discriminator = (
5555
struct.unpack("<Q", self._create_v2_instruction_discriminator_bytes)[0]
@@ -110,10 +110,15 @@ def parse_token_creation_from_logs(
110110

111111
# First, collect all Program data entries and note when Create instruction happens
112112
for i, log in enumerate(logs):
113-
if "Program log: Instruction: Create" in log or "Program log: Instruction: Create_v2" in log:
113+
if (
114+
"Program log: Instruction: Create" in log
115+
or "Program log: Instruction: Create_v2" in log
116+
):
114117
create_instruction_found = True
115118
instruction_type = "Create_v2" if "Create_v2" in log else "Create"
116-
logger.info(f"📝 Found {instruction_type} instruction at log index {i}")
119+
logger.info(
120+
f"📝 Found {instruction_type} instruction at log index {i}"
121+
)
117122
elif "Program data:" in log:
118123
# Extract base64 encoded event data
119124
encoded_data = log.split("Program data: ")[1].strip()
@@ -269,6 +274,7 @@ def parse_token_creation_from_logs(
269274
creator=creator,
270275
creator_vault=creator_vault,
271276
token_program_id=token_program_id,
277+
is_cashback_coin=fields.get("is_cashback_enabled", False),
272278
creation_timestamp=monotonic(),
273279
)
274280

@@ -304,7 +310,9 @@ def parse_token_creation_from_instruction(
304310
is_create_v2 = False
305311
elif (
306312
self._create_v2_instruction_discriminator_bytes
307-
and instruction_data.startswith(self._create_v2_instruction_discriminator_bytes)
313+
and instruction_data.startswith(
314+
self._create_v2_instruction_discriminator_bytes
315+
)
308316
):
309317
is_create_v2 = True
310318
else:
@@ -354,6 +362,16 @@ def get_account_key(index):
354362
else SystemAddresses.TOKEN_PROGRAM
355363
)
356364

365+
# Extract cashback flag from OptionBool struct (decoded as {"field_0": bool})
366+
is_cashback_raw = args.get("is_cashback_enabled")
367+
is_cashback = (
368+
is_cashback_raw.get("field_0", False)
369+
if isinstance(is_cashback_raw, dict)
370+
else bool(is_cashback_raw)
371+
if is_cashback_raw is not None
372+
else False
373+
)
374+
357375
return TokenInfo(
358376
name=args.get("name", ""),
359377
symbol=args.get("symbol", ""),
@@ -366,6 +384,7 @@ def get_account_key(index):
366384
creator=creator,
367385
creator_vault=creator_vault,
368386
token_program_id=token_program_id,
387+
is_cashback_coin=is_cashback,
369388
creation_timestamp=monotonic(),
370389
)
371390

@@ -480,11 +499,13 @@ def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
480499
discriminator = struct.unpack("<Q", ix_data[:8])[0]
481500

482501
is_create = (
483-
discriminator == self._create_instruction_discriminator
502+
discriminator
503+
== self._create_instruction_discriminator
484504
)
485505
is_create_v2 = (
486506
self._create_v2_instruction_discriminator
487-
and discriminator == self._create_v2_instruction_discriminator
507+
and discriminator
508+
== self._create_v2_instruction_discriminator
488509
)
489510

490511
if is_create or is_create_v2:
@@ -543,11 +564,13 @@ def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
543564
discriminator = struct.unpack("<Q", ix_data[:8])[0]
544565

545566
is_create = (
546-
discriminator == self._create_instruction_discriminator
567+
discriminator
568+
== self._create_instruction_discriminator
547569
)
548570
is_create_v2 = (
549571
self._create_v2_instruction_discriminator
550-
and discriminator == self._create_v2_instruction_discriminator
572+
and discriminator
573+
== self._create_v2_instruction_discriminator
551574
)
552575

553576
if is_create or is_create_v2:
@@ -594,7 +617,10 @@ def _derive_creator_vault(self, creator: Pubkey) -> Pubkey:
594617
return derived_address
595618

596619
def _derive_associated_bonding_curve(
597-
self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
620+
self,
621+
mint: Pubkey,
622+
bonding_curve: Pubkey,
623+
token_program_id: Pubkey | None = None,
598624
) -> Pubkey:
599625
"""Derive the associated bonding curve (ATA of bonding curve for the token).
600626

src/platforms/pumpfun/instruction_builder.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from solders.pubkey import Pubkey
1212
from spl.token.instructions import create_idempotent_associated_token_account
1313

14-
from core.pubkeys import TOKEN_DECIMALS, SystemAddresses
14+
from core.pubkeys import TOKEN_DECIMALS
1515
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
1616
from utils.idl_parser import IDLParser
1717
from utils.logger import get_logger
@@ -143,6 +143,12 @@ async def build_buy_instruction(
143143
is_signer=False,
144144
is_writable=False,
145145
),
146+
# Remaining account: bonding_curve_v2 (readonly, required for all coins)
147+
AccountMeta(
148+
pubkey=accounts_info["bonding_curve_v2"],
149+
is_signer=False,
150+
is_writable=False,
151+
),
146152
]
147153

148154
# Build instruction data: discriminator + token_amount + max_sol_cost + track_volume
@@ -247,6 +253,25 @@ async def build_sell_instruction(
247253
),
248254
]
249255

256+
# Remaining accounts (after fee_program) for cashback + bonding_curve_v2
257+
if token_info.is_cashback_coin:
258+
# Cashback sell: user_volume_accumulator (mutable) + bonding_curve_v2 (readonly)
259+
sell_accounts.append(
260+
AccountMeta(
261+
pubkey=accounts_info["user_volume_accumulator"],
262+
is_signer=False,
263+
is_writable=True,
264+
)
265+
)
266+
# bonding_curve_v2 is required for ALL coins (cashback and non-cashback)
267+
sell_accounts.append(
268+
AccountMeta(
269+
pubkey=accounts_info["bonding_curve_v2"],
270+
is_signer=False,
271+
is_writable=False,
272+
)
273+
)
274+
250275
# Build instruction data: discriminator + token_amount + min_sol_output + track_volume
251276
# Encode OptionBool for track_volume: [1, 1] = Some(true)
252277
track_volume_bytes = bytes([1, 1])
@@ -293,6 +318,7 @@ def get_required_accounts_for_buy(
293318
accounts_info["program"],
294319
accounts_info["fee_config"],
295320
accounts_info["fee_program"],
321+
accounts_info["bonding_curve_v2"],
296322
]
297323

298324
def get_required_accounts_for_sell(
@@ -320,6 +346,7 @@ def get_required_accounts_for_sell(
320346
accounts_info["program"],
321347
accounts_info["fee_config"],
322348
accounts_info["fee_program"],
349+
accounts_info["bonding_curve_v2"],
323350
]
324351

325352
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:

src/utils/idl_parser.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,12 @@ def _calculate_defined_type_min_size(self, type_name: str) -> int:
412412
type_def = self.types[type_name]["type"]
413413

414414
if type_def["kind"] == "struct":
415-
return sum(
416-
self._calculate_type_min_size(field["type"])
417-
for field in type_def["fields"]
418-
)
415+
total = 0
416+
for field in type_def["fields"]:
417+
# Handle both named fields (dicts) and tuple fields (strings/dicts)
418+
field_type = field["type"] if isinstance(field, dict) else field
419+
total += self._calculate_type_min_size(field_type)
420+
return total
419421

420422
if type_def["kind"] == "enum":
421423
# The size of an enum is its discriminator plus the size of its LARGEST variant,
@@ -495,9 +497,15 @@ def _decode_defined_type(
495497

496498
if type_def["kind"] == "struct":
497499
struct_data = {}
498-
for field in type_def["fields"]:
499-
value, offset = self._decode_type(data, offset, field["type"])
500-
struct_data[field["name"]] = value
500+
for i, field in enumerate(type_def["fields"]):
501+
if isinstance(field, dict):
502+
# Named field: {"name": "x", "type": "bool"}
503+
value, offset = self._decode_type(data, offset, field["type"])
504+
struct_data[field["name"]] = value
505+
else:
506+
# Tuple struct field: just a type string like "bool"
507+
value, offset = self._decode_type(data, offset, field)
508+
struct_data[f"field_{i}"] = value
501509
return struct_data, offset
502510

503511
if type_def["kind"] == "enum":

0 commit comments

Comments
 (0)