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>
This commit is contained in:
Anton Sauchyk
2026-02-27 23:39:52 +01:00
committed by GitHub
parent 0b6779ca34
commit 4a48f545c1
9 changed files with 4334 additions and 88 deletions
+1973 -4
View File
File diff suppressed because it is too large Load Diff
+1323 -61
View File
File diff suppressed because it is too large Load Diff
+926 -1
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -47,6 +47,7 @@ class TokenInfo:
creator_vault: Pubkey | None = None
token_program_id: Pubkey | None = None # Token or Token2022 program
is_mayhem_mode: bool = False # pump.fun mayhem mode flag
is_cashback_coin: bool = False # pump.fun cashback coin flag
# Metadata
creation_timestamp: float | None = None
+30
View File
@@ -75,6 +75,22 @@ class PumpFunAddresses:
)
return derived_address
@staticmethod
def find_bonding_curve_v2(mint: Pubkey) -> Pubkey:
"""Derive the bonding curve v2 PDA for a token mint.
Args:
mint: Token mint address
Returns:
Pubkey of the derived bonding curve v2 account
"""
derived_address, _ = Pubkey.find_program_address(
[b"bonding-curve-v2", bytes(mint)],
PumpFunAddresses.PROGRAM,
)
return derived_address
@staticmethod
def find_fee_config() -> Pubkey:
"""
@@ -256,6 +272,17 @@ class PumpFunAddressProvider(AddressProvider):
"""
return PumpFunAddresses.find_user_volume_accumulator(user)
def derive_bonding_curve_v2(self, mint: Pubkey) -> Pubkey:
"""Derive the bonding curve v2 PDA for a token mint.
Args:
mint: Token mint address
Returns:
Bonding curve v2 address
"""
return PumpFunAddresses.find_bonding_curve_v2(mint)
def derive_fee_config(self) -> Pubkey:
"""Derive the fee config PDA.
@@ -326,6 +353,7 @@ class PumpFunAddressProvider(AddressProvider):
"user_volume_accumulator": self.derive_user_volume_accumulator(user),
"fee_config": self.derive_fee_config(),
"fee_program": PumpFunAddresses.FEE_PROGRAM,
"bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint),
}
def get_sell_instruction_accounts(
@@ -375,4 +403,6 @@ class PumpFunAddressProvider(AddressProvider):
"program": PumpFunAddresses.PROGRAM,
"fee_config": self.derive_fee_config(),
"fee_program": PumpFunAddresses.FEE_PROGRAM,
"bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint),
"user_volume_accumulator": self.derive_user_volume_accumulator(user),
}
+2 -4
View File
@@ -191,6 +191,7 @@ class PumpFunCurveManager(CurveManager):
"complete": decoded_curve_state.get("complete", False),
"creator": decoded_curve_state.get("creator", ""),
"is_mayhem_mode": decoded_curve_state.get("is_mayhem_mode", False),
"is_cashback_coin": decoded_curve_state.get("is_cashback_coin", False),
}
# Calculate additional metrics
@@ -205,10 +206,7 @@ class PumpFunCurveManager(CurveManager):
)
curve_data["price_per_token"] = (
(
curve_data["virtual_sol_reserves"]
/ curve_data["virtual_token_reserves"]
)
(curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"])
* (10**TOKEN_DECIMALS)
/ LAMPORTS_PER_SOL
)
+36 -10
View File
@@ -48,8 +48,8 @@ class PumpFunEventParser(EventParser):
)[0]
# Support for token2022 (create_v2 instruction)
self._create_v2_instruction_discriminator_bytes = instruction_discriminators.get(
"create_v2"
self._create_v2_instruction_discriminator_bytes = (
instruction_discriminators.get("create_v2")
)
self._create_v2_instruction_discriminator = (
struct.unpack("<Q", self._create_v2_instruction_discriminator_bytes)[0]
@@ -110,10 +110,15 @@ class PumpFunEventParser(EventParser):
# First, collect all Program data entries and note when Create instruction happens
for i, log in enumerate(logs):
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"
logger.info(f"📝 Found {instruction_type} instruction at log index {i}")
logger.info(
f"📝 Found {instruction_type} instruction at log index {i}"
)
elif "Program data:" in log:
# Extract base64 encoded event data
encoded_data = log.split("Program data: ")[1].strip()
@@ -269,6 +274,7 @@ class PumpFunEventParser(EventParser):
creator=creator,
creator_vault=creator_vault,
token_program_id=token_program_id,
is_cashback_coin=fields.get("is_cashback_enabled", False),
creation_timestamp=monotonic(),
)
@@ -304,7 +310,9 @@ class PumpFunEventParser(EventParser):
is_create_v2 = False
elif (
self._create_v2_instruction_discriminator_bytes
and instruction_data.startswith(self._create_v2_instruction_discriminator_bytes)
and instruction_data.startswith(
self._create_v2_instruction_discriminator_bytes
)
):
is_create_v2 = True
else:
@@ -354,6 +362,16 @@ class PumpFunEventParser(EventParser):
else SystemAddresses.TOKEN_PROGRAM
)
# Extract cashback flag from OptionBool struct (decoded as {"field_0": bool})
is_cashback_raw = args.get("is_cashback_enabled")
is_cashback = (
is_cashback_raw.get("field_0", False)
if isinstance(is_cashback_raw, dict)
else bool(is_cashback_raw)
if is_cashback_raw is not None
else False
)
return TokenInfo(
name=args.get("name", ""),
symbol=args.get("symbol", ""),
@@ -366,6 +384,7 @@ class PumpFunEventParser(EventParser):
creator=creator,
creator_vault=creator_vault,
token_program_id=token_program_id,
is_cashback_coin=is_cashback,
creation_timestamp=monotonic(),
)
@@ -480,11 +499,13 @@ class PumpFunEventParser(EventParser):
discriminator = struct.unpack("<Q", ix_data[:8])[0]
is_create = (
discriminator == self._create_instruction_discriminator
discriminator
== self._create_instruction_discriminator
)
is_create_v2 = (
self._create_v2_instruction_discriminator
and discriminator == self._create_v2_instruction_discriminator
and discriminator
== self._create_v2_instruction_discriminator
)
if is_create or is_create_v2:
@@ -543,11 +564,13 @@ class PumpFunEventParser(EventParser):
discriminator = struct.unpack("<Q", ix_data[:8])[0]
is_create = (
discriminator == self._create_instruction_discriminator
discriminator
== self._create_instruction_discriminator
)
is_create_v2 = (
self._create_v2_instruction_discriminator
and discriminator == self._create_v2_instruction_discriminator
and discriminator
== self._create_v2_instruction_discriminator
)
if is_create or is_create_v2:
@@ -594,7 +617,10 @@ class PumpFunEventParser(EventParser):
return derived_address
def _derive_associated_bonding_curve(
self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
self,
mint: Pubkey,
bonding_curve: Pubkey,
token_program_id: Pubkey | None = None,
) -> Pubkey:
"""Derive the associated bonding curve (ATA of bonding curve for the token).
+28 -1
View File
@@ -11,7 +11,7 @@ from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey
from spl.token.instructions import create_idempotent_associated_token_account
from core.pubkeys import TOKEN_DECIMALS, SystemAddresses
from core.pubkeys import TOKEN_DECIMALS
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
from utils.idl_parser import IDLParser
from utils.logger import get_logger
@@ -143,6 +143,12 @@ class PumpFunInstructionBuilder(InstructionBuilder):
is_signer=False,
is_writable=False,
),
# Remaining account: bonding_curve_v2 (readonly, required for all coins)
AccountMeta(
pubkey=accounts_info["bonding_curve_v2"],
is_signer=False,
is_writable=False,
),
]
# Build instruction data: discriminator + token_amount + max_sol_cost + track_volume
@@ -247,6 +253,25 @@ class PumpFunInstructionBuilder(InstructionBuilder):
),
]
# Remaining accounts (after fee_program) for cashback + bonding_curve_v2
if token_info.is_cashback_coin:
# Cashback sell: user_volume_accumulator (mutable) + bonding_curve_v2 (readonly)
sell_accounts.append(
AccountMeta(
pubkey=accounts_info["user_volume_accumulator"],
is_signer=False,
is_writable=True,
)
)
# bonding_curve_v2 is required for ALL coins (cashback and non-cashback)
sell_accounts.append(
AccountMeta(
pubkey=accounts_info["bonding_curve_v2"],
is_signer=False,
is_writable=False,
)
)
# Build instruction data: discriminator + token_amount + min_sol_output + track_volume
# Encode OptionBool for track_volume: [1, 1] = Some(true)
track_volume_bytes = bytes([1, 1])
@@ -293,6 +318,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
accounts_info["program"],
accounts_info["fee_config"],
accounts_info["fee_program"],
accounts_info["bonding_curve_v2"],
]
def get_required_accounts_for_sell(
@@ -320,6 +346,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
accounts_info["program"],
accounts_info["fee_config"],
accounts_info["fee_program"],
accounts_info["bonding_curve_v2"],
]
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
+15 -7
View File
@@ -412,10 +412,12 @@ class IDLParser:
type_def = self.types[type_name]["type"]
if type_def["kind"] == "struct":
return sum(
self._calculate_type_min_size(field["type"])
for field in type_def["fields"]
)
total = 0
for field in type_def["fields"]:
# Handle both named fields (dicts) and tuple fields (strings/dicts)
field_type = field["type"] if isinstance(field, dict) else field
total += self._calculate_type_min_size(field_type)
return total
if type_def["kind"] == "enum":
# The size of an enum is its discriminator plus the size of its LARGEST variant,
@@ -495,9 +497,15 @@ class IDLParser:
if type_def["kind"] == "struct":
struct_data = {}
for field in type_def["fields"]:
value, offset = self._decode_type(data, offset, field["type"])
struct_data[field["name"]] = value
for i, field in enumerate(type_def["fields"]):
if isinstance(field, dict):
# Named field: {"name": "x", "type": "bool"}
value, offset = self._decode_type(data, offset, field["type"])
struct_data[field["name"]] = value
else:
# Tuple struct field: just a type string like "bool"
value, offset = self._decode_type(data, offset, field)
struct_data[f"field_{i}"] = value
return struct_data, offset
if type_def["kind"] == "enum":