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:
+1973
-4
File diff suppressed because it is too large
Load Diff
+1323
-61
File diff suppressed because it is too large
Load Diff
+926
-1
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user