fix(examples): align listeners and decoders with refreshed pump.fun IDL

Refreshes the read-only learning examples (Phase A3) against the post
Feb–Apr 2026 IDL changes:
  - CreateEvent now has 15 fields (timestamp, 4×u64 reserves, token_program,
    is_mayhem_mode, is_cashback_enabled). Listener parsers were truncating
    after `creator`; the v2 ones were also reading is_mayhem_mode at the
    wrong offset.
  - BondingCurve account is now 83 bytes (added is_cashback_coin).
  - PumpSwap Pool account is now 245 bytes (added is_cashback_coin) — the
    listen_programsubscribe dataSize filter was matching nothing.
  - IDL instruction `createV2` was renamed to `create_v2`; the legacy
    `create` instruction added a `creator: pubkey` arg in March 2026.
  - decode_from_blockSubscribe.py decoder now handles bool, i64, u32, u16,
    u8, and the OptionBool defined type (1-byte wire format).

Files touched (all under learning-examples/):
  bonding-curve-progress/get_bonding_curve_status.py  — V4 struct (cashback)
  bonding-curve-progress/poll_bonding_curve_progress.py
  decode_from_blockSubscribe.py
  decode_from_getAccountInfo.py
  decode_from_getTransaction.py
  listen-migrations/compare_migration_listeners.py
  listen-migrations/listen_programsubscribe.py
  listen-new-tokens/compare_listeners.py
  listen-new-tokens/listen_blocksubscribe.py
  listen-new-tokens/listen_geyser.py
  listen-new-tokens/listen_logsubscribe.py
  listen-new-tokens/listen_logsubscribe_abc.py

Verified live against mainnet — listeners decode tokens including
mayhem-mode and cashback-enabled curves correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anton Sauchyk
2026-04-27 16:42:11 +02:00
parent 156cf508a0
commit 9b4841890c
12 changed files with 246 additions and 263 deletions
@@ -65,6 +65,19 @@ class BondingCurveState:
"is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte
)
# V4: V3 + is_cashback_coin (83 bytes total: 8 discriminator + 75 data) — added in the late-Feb 2026 cashback upgrade
_STRUCT_V4 = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag,
"creator" / Bytes(32),
"is_mayhem_mode" / Flag,
"is_cashback_coin" / Flag,
)
def __init__(self, data: bytes) -> None:
"""Parse bonding curve data."""
if data[:8] != EXPECTED_DISCRIMINATOR:
@@ -75,14 +88,19 @@ class BondingCurveState:
if total_length == 81: # V2: Creator only
parsed = self._STRUCT_V2.parse(data[8:])
self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field
self.creator = Pubkey.from_bytes(self.creator)
self.is_mayhem_mode = False
self.is_cashback_coin = False
elif total_length >= 82: # V3: Creator + mayhem mode
elif total_length == 82: # V3: Creator + mayhem
parsed = self._STRUCT_V3.parse(data[8:])
self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field
self.creator = Pubkey.from_bytes(self.creator)
self.is_cashback_coin = False
elif total_length >= 83: # V4: Creator + mayhem + cashback
parsed = self._STRUCT_V4.parse(data[8:])
self.__dict__.update(parsed)
self.creator = Pubkey.from_bytes(self.creator)
else:
@@ -162,6 +180,9 @@ async def check_token_status(mint_address: str) -> None:
print(
f"Mayhem Mode: {'✅ Enabled' if curve_state.is_mayhem_mode else '❌ Disabled'}"
)
print(
f"Cashback Coin: {'✅ Enabled' if curve_state.is_cashback_coin else '❌ Disabled'}"
)
print(
f"Completed: {'✅ Migrated' if curve_state.complete else '❌ Bonding curve'}"
)
@@ -104,6 +104,12 @@ def parse_curve_state(data: bytes) -> dict:
else:
result["is_mayhem_mode"] = False
# Parse is_cashback_coin if present (added in late-Feb 2026 cashback upgrade)
if data_length >= 75:
result["is_cashback_coin"] = bool(data[82])
else:
result["is_cashback_coin"] = False
return result
@@ -19,23 +19,52 @@ def load_transaction(file_path):
def decode_instruction(ix_data, ix_def):
"""Decode an instruction's args based on its IDL definition.
Supports the primitive scalar types and the `OptionBool` defined type
(added in the late-Feb 2026 cashback upgrade — `OptionBool` is a struct
wrapping a single bool, so it serializes as 1 byte on the wire).
"""
args = {}
offset = 8 # Skip 8-byte discriminator
for arg in ix_def["args"]:
if arg["type"] == "u64":
t = arg["type"]
if t == "u64":
value = struct.unpack_from("<Q", ix_data, offset)[0]
offset += 8
elif arg["type"] == "pubkey":
elif t == "i64":
value = struct.unpack_from("<q", ix_data, offset)[0]
offset += 8
elif t == "u32":
value = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
elif t == "u16":
value = struct.unpack_from("<H", ix_data, offset)[0]
offset += 2
elif t == "u8":
value = ix_data[offset]
offset += 1
elif t == "bool":
value = bool(ix_data[offset])
offset += 1
elif t == "pubkey":
value = ix_data[offset : offset + 32].hex()
offset += 32
elif arg["type"] == "string":
elif t == "string":
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif isinstance(t, dict) and "defined" in t:
defined_name = t["defined"]["name"] if isinstance(t["defined"], dict) else t["defined"]
if defined_name == "OptionBool":
value = bool(ix_data[offset])
offset += 1
else:
raise ValueError(f"Unsupported defined type: {defined_name}")
else:
raise ValueError(f"Unsupported type: {arg['type']}")
raise ValueError(f"Unsupported type: {t}")
args[arg["name"]] = value
@@ -51,11 +51,19 @@ class BondingCurveState:
if len(data) > offset:
self.is_mayhem_mode = bool(data[offset])
offset += 1
else:
self.is_mayhem_mode = None
if len(data) > offset:
self.is_cashback_coin = bool(data[offset])
else:
self.is_cashback_coin = None
else:
self.creator = None
self.is_mayhem_mode = None
self.is_cashback_coin = None
def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float:
+21 -10
View File
@@ -27,8 +27,10 @@ print(json.dumps(tx_data, indent=2))
def decode_create_instruction(data):
"""Decode legacy Create instruction (Metaplex tokens)."""
# The Create instruction has 3 string arguments: name, symbol, uri
"""Decode legacy Create instruction (Metaplex tokens).
IDL args (March 7, 2026 program upgrade): name, symbol, uri, creator (pubkey).
"""
offset = 8 # Skip the 8-byte discriminator
results = []
for _ in range(3):
@@ -37,18 +39,25 @@ def decode_create_instruction(data):
string_data = data[offset : offset + length].decode("utf-8")
results.append(string_data)
offset += length
creator = base58.b58encode(data[offset : offset + 32]).decode("utf-8") if offset + 32 <= len(data) else None
return {
"name": results[0],
"symbol": results[1],
"uri": results[2],
"creator": creator,
"token_standard": "legacy",
"is_mayhem_mode": False,
}
def decode_create_v2_instruction(data):
"""Decode CreateV2 instruction (Token2022 tokens)."""
# The CreateV2 instruction has 3 string arguments: name, symbol, uri + is_mayhem_mode
"""Decode CreateV2 instruction (Token2022 tokens).
IDL args: name, symbol, uri, creator (pubkey), is_mayhem_mode (bool),
is_cashback_enabled (OptionBool = 1 byte).
"""
offset = 8 # Skip the 8-byte discriminator
results = []
for _ in range(3):
@@ -58,20 +67,22 @@ def decode_create_v2_instruction(data):
results.append(string_data)
offset += length
# Skip creator pubkey (32 bytes)
creator = base58.b58encode(data[offset : offset + 32]).decode("utf-8") if offset + 32 <= len(data) else None
offset += 32
# Parse is_mayhem_mode (OptionBool at the end)
is_mayhem_mode = False
if offset < len(data):
is_mayhem_mode = bool(data[offset])
is_mayhem_mode = bool(data[offset]) if offset < len(data) else False
offset += 1
is_cashback_enabled = bool(data[offset]) if offset < len(data) else False
return {
"name": results[0],
"symbol": results[1],
"uri": results[2],
"creator": creator,
"token_standard": "token2022",
"is_mayhem_mode": is_mayhem_mode,
"is_cashback_enabled": is_cashback_enabled,
}
@@ -84,7 +95,7 @@ def decode_buy_instruction(data):
def decode_instruction_data(instruction, accounts, data):
if instruction["name"] == "create":
return decode_create_instruction(data)
elif instruction["name"] == "createV2":
elif instruction["name"] == "create_v2":
return decode_create_v2_instruction(data)
elif instruction["name"] == "buy":
return decode_buy_instruction(data)
@@ -41,7 +41,7 @@ QUOTE_MINT_SOL = base58.b58encode(
).decode()
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # Pool account with is_mayhem_mode = 244 bytes
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1 # Pool with is_mayhem_mode + is_cashback_coin = 245 bytes
class DetectionTracker:
@@ -333,6 +333,7 @@ def parse_market_account_data(data):
("lp_supply", "u64"),
("coin_creator", "pubkey"),
("is_mayhem_mode", "bool"),
("is_cashback_coin", "bool"),
]
try:
@@ -24,7 +24,7 @@ WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # discriminator + pool_bump + index + 6 pubkeys + lp_supply + coin_creator + is_mayhem_mode = 244 bytes
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1 # disc + pool_bump + index + 6 pubkeys + lp_supply + coin_creator + is_mayhem_mode + is_cashback_coin = 245 bytes
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
QUOTE_MINT_SOL = base58.b58encode(
bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))
@@ -77,6 +77,7 @@ def parse_market_account_data(data):
("lp_supply", "u64"),
("coin_creator", "pubkey"),
("is_mayhem_mode", "bool"),
("is_cashback_coin", "bool"),
]
try:
@@ -334,11 +334,16 @@ def decode_create_v2_instruction(ix_data, account_keys):
parsed_data["uri"] = read_string()
parsed_data["creator"] = read_pubkey()
# Parse is_mayhem_mode (OptionBool at the end)
# CreateV2 trailing args: is_mayhem_mode (bool, 1B), is_cashback_enabled (OptionBool, 1B)
if offset < len(ix_data):
parsed_data["is_mayhem_mode"] = bool(ix_data[offset])
offset += 1
else:
parsed_data["is_mayhem_mode"] = False
if offset < len(ix_data):
parsed_data["is_cashback_enabled"] = bool(ix_data[offset])
else:
parsed_data["is_cashback_enabled"] = False
# Extract accounts from account_keys array
if len(account_keys) >= 6:
@@ -355,27 +360,33 @@ def decode_create_v2_instruction(ix_data, account_keys):
return None
def parse_create_event(data):
"""Parse legacy Create event from logs (event data includes all fields)."""
_CREATE_EVENT_FIELDS = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
("timestamp", "i64"),
("virtual_token_reserves", "u64"),
("virtual_sol_reserves", "u64"),
("real_token_reserves", "u64"),
("token_total_supply", "u64"),
("token_program", "publicKey"),
("is_mayhem_mode", "bool"),
("is_cashback_enabled", "bool"),
]
def _parse_create_event(data, token_standard_hint):
"""Parse a CreateEvent payload (same on-chain layout for create and create_v2)."""
if len(data) < 8:
return None
offset = 8 # Skip discriminator
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
for field_name, field_type in _CREATE_EVENT_FIELDS:
if field_type == "string":
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
@@ -390,67 +401,31 @@ def parse_create_event(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
elif field_type == "u64":
value = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
elif field_type == "i64":
value = struct.unpack("<q", data[offset : offset + 8])[0]
offset += 8
elif field_type == "bool":
value = bool(data[offset]) if offset < len(data) else False
offset += 1
parsed_data[field_name] = value
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = token_standard_hint
return parsed_data
except Exception as e:
print(f"[ERROR] Failed to parse create event: {e}")
return None
def parse_create_event(data):
"""Parse CreateEvent emitted by legacy Create instruction."""
return _parse_create_event(data, token_standard_hint="legacy")
def parse_create_v2_event(data):
"""Parse CreateV2 event from logs (event data includes all fields)."""
if len(data) < 8:
return None
offset = 8 # Skip discriminator
parsed_data = {}
# Parse fields based on CreateV2Event structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
# Parse is_mayhem_mode (OptionBool at the end)
if offset < len(data):
is_mayhem_mode = bool(data[offset])
parsed_data["is_mayhem_mode"] = is_mayhem_mode
else:
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = "token2022"
return parsed_data
except Exception as e:
print(f"[ERROR] Failed to parse create v2 event: {e}")
return None
"""Parse CreateEvent emitted by CreateV2 instruction (Token2022 tokens)."""
return _parse_create_event(data, token_standard_hint="token2022")
def is_transaction_successful(logs):
@@ -197,29 +197,34 @@ def decode_create_v2_instruction(ix_data, ix_def, accounts):
args = {}
offset = 8 # Skip 8-byte discriminator
# Parse instruction arguments according to IDL definition
# Parse instruction arguments according to IDL definition.
# CreateV2 args: name, symbol, uri, creator (pubkey), is_mayhem_mode (bool),
# is_cashback_enabled (OptionBool, serialized as 1 byte).
for arg in ix_def["args"]:
if arg["type"] == "string":
# String format: 4-byte length prefix + UTF-8 encoded string
t = arg["type"]
if t == "string":
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif arg["type"] == "pubkey":
# Pubkey is 32 bytes, encoded as base58
elif t == "pubkey":
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
elif t == "bool":
value = bool(ix_data[offset]) if offset < len(ix_data) else False
offset += 1
elif isinstance(t, dict) and "defined" in t:
defined_name = t["defined"]["name"] if isinstance(t["defined"], dict) else t["defined"]
if defined_name == "OptionBool":
value = bool(ix_data[offset]) if offset < len(ix_data) else False
offset += 1
else:
raise ValueError(f"Unsupported defined type: {defined_name}")
else:
raise ValueError(f"Unsupported type: {arg['type']}")
raise ValueError(f"Unsupported type: {t}")
args[arg["name"]] = value
# Parse is_mayhem_mode (OptionBool at the end)
# Format: 1 byte (0 = false/None, 1 = true)
is_mayhem_mode = False
if offset < len(ix_data):
is_mayhem_mode = bool(ix_data[offset])
# Extract account addresses from the accounts array
# Account layout for CreateV2 instruction:
# 0: mint, 1: metadata, 2: bondingCurve, 3: associatedBondingCurve,
@@ -229,7 +234,6 @@ def decode_create_v2_instruction(ix_data, ix_def, accounts):
args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[5])
args["token_standard"] = "token2022"
args["is_mayhem_mode"] = is_mayhem_mode
return args
@@ -338,7 +342,7 @@ async def listen_and_decode_create():
# CreateV2 instruction (Token2022 tokens)
create_v2_ix = next(
(instr for instr in idl["instructions"]
if instr["name"] == "createV2"),
if instr["name"] == "create_v2"),
next(instr for instr in idl["instructions"]
if instr["name"] == "create")
)
@@ -194,10 +194,14 @@ def decode_create_v2_instruction(ix_data: bytes, keys, accounts) -> dict:
uri = read_string()
creator = read_pubkey()
# Parse is_mayhem_mode (OptionBool at the end)
# CreateV2 trailing args: is_mayhem_mode (bool, 1B), is_cashback_enabled (OptionBool, 1B)
is_mayhem_mode = False
is_cashback_enabled = False
if offset < len(ix_data):
is_mayhem_mode = bool(ix_data[offset])
offset += 1
if offset < len(ix_data):
is_cashback_enabled = bool(ix_data[offset])
token_info = {
"name": name,
@@ -210,6 +214,7 @@ def decode_create_v2_instruction(ix_data: bytes, keys, accounts) -> dict:
"user": get_account_key(5),
"token_standard": "token2022",
"is_mayhem_mode": is_mayhem_mode,
"is_cashback_enabled": is_cashback_enabled,
}
return token_info
@@ -92,27 +92,38 @@ def parse_create_instruction(data):
Returns:
Dictionary containing decoded token information, or None if parsing fails
"""
return _parse_create_event(data, token_standard_hint="legacy")
_CREATE_EVENT_FIELDS = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
("timestamp", "i64"),
("virtual_token_reserves", "u64"),
("virtual_sol_reserves", "u64"),
("real_token_reserves", "u64"),
("token_total_supply", "u64"),
("token_program", "publicKey"),
("is_mayhem_mode", "bool"),
("is_cashback_enabled", "bool"),
]
def _parse_create_event(data, token_standard_hint):
"""Parse a CreateEvent payload (same on-chain layout for both create and create_v2)."""
if len(data) < 8:
print(f"⚠️ Data too short for Create event: {len(data)} bytes")
return None
offset = 8 # Skip event discriminator
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
for field_name, field_type in _CREATE_EVENT_FIELDS:
if field_type == "string":
# String format: 4-byte length prefix + UTF-8 encoded string
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
@@ -122,93 +133,33 @@ def parse_create_instruction(data):
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
# Pubkey is 32 bytes, encoded as base58
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
elif field_type == "u64":
value = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
elif field_type == "i64":
value = struct.unpack("<q", data[offset : offset + 8])[0]
offset += 8
elif field_type == "bool":
value = bool(data[offset]) if offset < len(data) else False
offset += 1
parsed_data[field_name] = value
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = token_standard_hint
return parsed_data
except Exception as e:
print(f"❌ Parse Create error: {e}")
print(f"❌ Parse Create event error: {e}")
print(f" Data length: {len(data)} bytes, offset: {offset}")
print(f" Data hex: {data.hex()[:200]}...")
return None
def parse_create_v2_instruction(data):
"""
Parse CreateEvent data from CreateV2 instruction (Token2022 tokens).
CreateV2 uses Token-2022 standard with additional features. The event format
is identical to Create, with an additional optional is_mayhem_mode flag at the end.
Token-2022 Reference:
https://spl.solana.com/token-2022
Args:
data: Raw event data bytes from program logs
Returns:
Dictionary containing decoded token information, or None if parsing fails
"""
if len(data) < 8:
print(f"⚠️ Data too short for CreateV2 event: {len(data)} bytes")
return None
offset = 8 # Skip event discriminator
parsed_data = {}
# Parse fields based on CreateV2Event structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
# String format: 4-byte length prefix + UTF-8 encoded string
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
# Pubkey is 32 bytes, encoded as base58
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
# Parse is_mayhem_mode (OptionBool at the end)
# Format: 1 byte (0 = false/None, 1 = true)
if offset < len(data):
is_mayhem_mode = bool(data[offset])
parsed_data["is_mayhem_mode"] = is_mayhem_mode
else:
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = "token2022"
return parsed_data
except Exception as e:
print(f"❌ Parse CreateV2 error: {e}")
print(f" Data length: {len(data)} bytes, offset: {offset}")
print(f" Data hex: {data.hex()[:200]}...")
return None
"""Parse CreateEvent emitted by CreateV2 instruction (Token2022 tokens)."""
return _parse_create_event(data, token_standard_hint="token2022")
async def listen_for_new_tokens():
@@ -109,27 +109,34 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey
return derived_address
def parse_create_instruction(data):
"""Parse legacy Create instruction (Metaplex tokens)."""
_CREATE_EVENT_FIELDS = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
("timestamp", "i64"),
("virtual_token_reserves", "u64"),
("virtual_sol_reserves", "u64"),
("real_token_reserves", "u64"),
("token_total_supply", "u64"),
("token_program", "publicKey"),
("is_mayhem_mode", "bool"),
("is_cashback_enabled", "bool"),
]
def _parse_create_event(data, token_standard_hint):
"""Parse a CreateEvent payload (same on-chain layout for both create and create_v2)."""
if len(data) < 8:
print(f"⚠️ Data too short for Create instruction: {len(data)} bytes")
print(f"⚠️ Data too short for Create event: {len(data)} bytes")
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
for field_name, field_type in _CREATE_EVENT_FIELDS:
if field_type == "string":
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
@@ -144,69 +151,33 @@ def parse_create_instruction(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
elif field_type == "u64":
value = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
elif field_type == "i64":
value = struct.unpack("<q", data[offset : offset + 8])[0]
offset += 8
elif field_type == "bool":
value = bool(data[offset]) if offset < len(data) else False
offset += 1
parsed_data[field_name] = value
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = token_standard_hint
return parsed_data
except Exception as e:
print(f"❌ Parse Create error: {e}")
print(f"❌ Parse Create event error: {e}")
print(f" Data length: {len(data)} bytes, offset: {offset}")
return None
def parse_create_instruction(data):
"""Parse CreateEvent emitted by legacy Create instruction (Metaplex tokens)."""
return _parse_create_event(data, token_standard_hint="legacy")
def parse_create_v2_instruction(data):
"""Parse CreateV2 instruction (Token2022 tokens)."""
if len(data) < 8:
print(f"⚠️ Data too short for CreateV2 instruction: {len(data)} bytes")
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateV2Event structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
# Parse is_mayhem_mode (OptionBool at the end)
if offset < len(data):
is_mayhem_mode = bool(data[offset])
parsed_data["is_mayhem_mode"] = is_mayhem_mode
else:
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = "token2022"
return parsed_data
except Exception as e:
print(f"❌ Parse CreateV2 error: {e}")
print(f" Data length: {len(data)} bytes, offset: {offset}")
return None
"""Parse CreateEvent emitted by CreateV2 instruction (Token2022 tokens)."""
return _parse_create_event(data, token_standard_hint="token2022")
async def listen_for_new_tokens():