fix(platforms,tools,cookbook): match the create marker whole, race pump.fun coins only (#231)

The logs create gate tested the marker as a substring. Anchor writes
`Instruction: <Name>` for every program, so it also accepted
CreateTokenAccount, CreatePool and the rest from whichever programs shared the
transaction. The CreateEvent check downstream rejected them, but a
CreateTokenAccount veto had been added to suppress them and, being a
whole-transaction test, it also dropped genuine creates whose transaction
opened a token account. Matching the marker lines whole retires the veto.

compare_listeners' PumpPortal lane ignored the payload's `pool` field, so
letsbonk creates raced as pump.fun coins against lanes that subscribe to the
pump.fun program alone. The lane now reports `pool: "pump"` only.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anton Sauchyk
2026-09-25 22:07:26 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 33b456901c
commit ca1f8919e6
10 changed files with 541 additions and 53 deletions
+6 -4
View File
@@ -192,10 +192,12 @@ in the verifier named in [docs/regression-tests.md](docs/regression-tests.md).
`geyser`, `shreds`, `pumpportal`; letsbonk.fun supports `blocks`, `geyser`,
`pumpportal` — not `logs`, and not `shreds` (issue #201). Adding a listener
means updating `PLATFORM_LISTENER_COMPATIBILITY` there too.
- **PumpPortal is a sampling feed, not a complete one.** It never sends a coin
created in a transaction v1, and a bonk `create` carries no `name`, `symbol` or
`uri` — so only `mint` and `traderPublicKey` are required, and
`filters.match_string` can never match a bonk token. The bonk trade path past
- **PumpPortal is a sampling feed, not a complete one, and not one launchpad.**
It never sends a coin created in a transaction v1, and a bonk `create` carries
no `name`, `symbol` or `uri` — so only `mint` and `traderPublicKey` are
required, and `filters.match_string` can never match a bonk token. Each
payload names its launchpad in `pool`; a consumer that ignores it counts bonk
coins as pump.fun ones. The bonk trade path past
detection is unverified (issue #201).
- Bots with `separate_process: true` run in their own process, one log file each.
- `pump_bot` runs **every** `bots/*.yaml`, and three of the four committed configs
@@ -83,6 +83,12 @@ def print_token_info(token_data, signature=None):
# base64 "Program data:" log line, which the RPC has already decoded for us.
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
# The two log lines the pump.fun program writes when it creates a coin.
CREATE_INSTRUCTION_LOGS = (
"Program log: Instruction: Create",
"Program log: Instruction: CreateV2",
)
# The CreateEvent layout, in order. Trailing fields were appended by later
# upgrades, so a shorter payload stops early rather than failing.
_CREATE_EVENT_FIELDS = [
@@ -157,7 +163,10 @@ def find_create_event(logs):
Returns:
The decoded event, or None if the transaction created no coin
"""
if not any("Program log: Instruction: Create" in log for log in logs):
# Matched whole: Anchor writes `Instruction: <Name>` for every program, so
# a substring test also accepts CreateTokenAccount, CreatePool and the rest
# from whichever programs share the transaction.
if not any(log in CREATE_INSTRUCTION_LOGS for log in logs):
return None
for log in logs:
if "Program data:" not in log:
@@ -54,6 +54,10 @@ ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
# The two log lines the pump.fun program writes when it creates a coin.
CREATE_LOG = "Program log: Instruction: Create"
CREATE_V2_LOG = "Program log: Instruction: CreateV2"
def print_token_info(
token_data, signature=None, associated_bonding_curve: str | None = None
@@ -246,15 +250,14 @@ async def listen_for_new_tokens():
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
# Detect both Create and CreateV2 instructions
is_create = any(
"Program log: Instruction: Create" in log
for log in logs
)
is_create_v2 = any(
"Program log: Instruction: CreateV2" in log
for log in logs
)
# Detect both Create and CreateV2 instructions.
# Matched whole: Anchor writes
# `Instruction: <Name>` for every program, so a
# substring test also accepts CreateTokenAccount,
# CreatePool and the rest from whichever programs
# share the transaction.
is_create = CREATE_LOG in logs
is_create_v2 = CREATE_V2_LOG in logs
if is_create or is_create_v2:
for log in logs:
@@ -298,6 +298,12 @@ async def buy_token(
# works for every transaction version.
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
# The two log lines the pump.fun program writes when it creates a coin.
CREATE_INSTRUCTION_LOGS = (
"Program log: Instruction: Create",
"Program log: Instruction: CreateV2",
)
_CREATE_EVENT_FIELDS = [
("name", "string"),
("symbol", "string"),
@@ -360,7 +366,10 @@ def token_info_from_logs(logs):
Returns:
The token fields the buy path needs, or None if no coin was created
"""
if not any("Program log: Instruction: Create" in log for log in logs):
# Matched whole: Anchor writes `Instruction: <Name>` for every program, so
# a substring test also accepts CreateTokenAccount, CreatePool and the rest
# from whichever programs share the transaction.
if not any(log in CREATE_INSTRUCTION_LOGS for log in logs):
return None
for log in logs:
if "Program data:" not in log:
+15
View File
@@ -75,6 +75,21 @@ against mainnet.
the installed solders is only ever one version behind. Route on
`meta.logMessages`, which the RPC has already decoded and which is
version-agnostic; keep the byte decode as a fallback.
- **Match a create marker as a whole log line, not a substring.** Anchor writes
`Program log: Instruction: <Name>` for every program, so
`"Program log: Instruction: Create" in log` also accepts CreateTokenAccount,
CreateFeeSharingConfig, CreatePool and CreateConsume from whichever programs
share the transaction. pump.fun writes exactly
`Program log: Instruction: Create` or `... CreateV2`. (The associated
token account program writes `Program log: Create`, with no `Instruction: `
prefix, so it never matched either way.)
- **PumpPortal's `subscribeNewToken` is not one launchpad.** Each payload names
its launchpad in `pool` (`pump`, `bonk`), and a consumer that ignores the
field treats letsbonk creates as pump.fun coins, each then reading as a
simultaneous miss by every on-chain lane. The `mint` field is also not always
the coin: on a bonk pool quoted in something other than SOL it carries the
quote mint instead, which resolves to a token with its own trading history
rather than a fresh one.
- **Resolve v0 lookup-table accounts before indexing them.** An instruction's
account indices can point past `message.account_keys` into the address lookup
table, which geyser reports in `meta.loaded_writable_addresses` then
+2
View File
@@ -19,6 +19,8 @@ individual scripts for a subset.
| `verify_ping_loop_close_is_quiet.py` | a WebSocket closing normally ends the ping loop quietly in every listener, while an unexpected failure is still logged |
| `verify_listener_cancellation.py` | a cancelled WebSocket listener stops, even when `websockets` reports cancellation as `AssertionError` |
| `verify_pumpportal_buy_path.py` | curve derived from the mint, unreadable curve skips the buy, curve+mint read in one slot-consistent batch |
| `verify_create_log_gate.py` | the create marker is matched as a whole log line, so a foreign `Create*` instruction is not a coin and a real create sharing its transaction with one is still detected |
| `verify_pumpportal_pool_filter.py` | `compare_listeners`' PumpPortal lane reports `pool: "pump"` coins only, still counts every frame, and tallies the launchpads it ignored |
| `verify_pumpportal_bonk_fields.py` | bonk payloads (no name/symbol/uri) still produce a `TokenInfo`; `--live` re-checks the real feed |
| `verify_extreme_fast_zero_rpc.py` | zero RPC calls between detection and submission for CreateEvent-sourced tokens |
| `verify_cleanup_survives_shutdown.py` | shutdown cleanup runs to completion through the cancellation that triggered it, bounded, and a cleanup that hangs or raises still lets the process exit |
+16 -16
View File
@@ -36,6 +36,16 @@ _CREATE_V2_USER_ACCOUNT_INDEX = 5
# Length of a Solana public key in bytes.
PUBKEY_BYTE_LENGTH = 32
# The two log lines the pump.fun program writes when it creates a coin. They are
# matched whole: Anchor writes `Instruction: <Name>` for every program, so a
# substring test also accepts CreateTokenAccount, CreatePool,
# CreateFeeSharingConfig and any other instruction whose name starts with
# "Create", from any program sharing the transaction.
CREATE_INSTRUCTION_LOGS = (
"Program log: Instruction: Create",
"Program log: Instruction: CreateV2",
)
def _option_bool(value: object) -> bool:
"""Read an Anchor OptionBool argument.
@@ -146,16 +156,9 @@ class PumpFunEventParser(EventParser):
Returns:
TokenInfo if token creation found, None otherwise
"""
# Check if this is a token creation transaction (create or create_v2 for token2022)
if not any(
"Program log: Instruction: Create" in log
or "Program log: Instruction: Create_v2" in log
for log in logs
):
return None
# Skip swaps as the first condition may pass them
if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
# Check if this is a token creation transaction (create, or create_v2
# for token2022)
if not any(log in CREATE_INSTRUCTION_LOGS for log in logs):
return None
logger.info(f"🔍 Parsing token creation from logs for signature: {signature}")
@@ -168,12 +171,9 @@ 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 log in CREATE_INSTRUCTION_LOGS:
create_instruction_found = True
instruction_type = "Create_v2" if "Create_v2" in log else "Create"
instruction_type = log.rsplit(": ", 1)[1]
logger.info(
f"📝 Found {instruction_type} instruction at log index {i}"
)
@@ -186,7 +186,7 @@ class PumpFunEventParser(EventParser):
)
if not create_instruction_found:
logger.info("❌ No Create or Create_v2 instruction found in logs")
logger.info("❌ No Create or CreateV2 instruction found in logs")
return None
if not program_data_entries:
+203
View File
@@ -0,0 +1,203 @@
"""Verify the logs create gate matches pump.fun's own marker and nothing else.
`parse_token_creation_from_logs` opened with a substring test:
if not any("Program log: Instruction: Create" in log for log in logs):
Anchor writes `Instruction: <Name>` for every program, so that line accepted any
instruction whose name merely starts with "Create" — CreateTokenAccount,
CreateFeeSharingConfig, CreatePool, CreateConsume — from any program sharing the
transaction. (The associated token account program is not among them: it writes
`Program log: Create`, without Anchor's `Instruction: ` prefix.)
No false coin reached the bot, because the CreateEvent discriminator check
downstream rejects every one of them. The cost was the opposite: a
`CreateTokenAccount` veto had been added on top to suppress the loudest of those
false hits, and being a whole-transaction test it also dropped any genuine
create whose transaction opened a token account — a real coin, silently not
detected. Matching the two marker lines whole removes the need for the veto.
Offline machine checks, no network and no funds moved:
1. Every create fixture is still detected from its logs alone.
2. A genuine create whose transaction also carries `CreateTokenAccount` is
detected. This is the check that fails before the fix: the veto returned
None for it.
3. Log sets carrying only a foreign `Create*` instruction are rejected.
4. No listener, tool or cookbook script tests the marker as a substring.
Usage:
uv run tests/regression/verify_create_log_gate.py
"""
import json
import re
import sys
from collections.abc import Iterator
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
from interfaces.core import Platform # noqa: E402
from platforms.pumpfun.event_parser import PumpFunEventParser # noqa: E402
from utils.idl_manager import get_idl_manager # noqa: E402
DECODE_DIR = PROJECT_ROOT / "cookbook" / "pumpfun" / "decode"
# Instruction markers the substring test accepted and the whole-line test
# does not. None of them creates a coin.
FOREIGN_CREATE_MARKERS = (
"Program log: Instruction: CreateTokenAccount",
"Program log: Instruction: CreateFeeSharingConfig",
"Program log: Instruction: CreatePool",
"Program log: Instruction: CreateConsume",
"Program log: Instruction: CreateIdempotent",
)
# Every site that decides "this transaction created a coin" from the logs.
GATE_SITES = (
Path("src/platforms/pumpfun/event_parser.py"),
Path("tools/compare_listeners.py"),
Path("cookbook/pumpfun/listen/pumpfun_listen_tokens_logsubscribe.py"),
Path("cookbook/pumpfun/listen/pumpfun_listen_tokens_blocksubscribe.py"),
Path("cookbook/pumpfun/trade/pumpfun_snipe_token_blocksubscribe.py"),
)
# A substring test against the marker, in either argument order.
SUBSTRING_GATE = re.compile(
r'"Program log: Instruction: Create\w*"\s+in\s+log\b'
r'|\bin\s+"Program log: Instruction: Create\w*"'
)
def _parser() -> PumpFunEventParser:
"""Real pump.fun event parser with the vendored IDL, no RPC client needed."""
return PumpFunEventParser(
idl_parser=get_idl_manager().get_parser(Platform.PUMP_FUN)
)
def _logs_from_fixture(path: Path) -> list[str] | None:
"""Pull `meta.logMessages` out of a captured transaction fixture.
Returns:
The log lines, or None if the fixture holds no transaction meta.
"""
def walk(node: object) -> Iterator[list[str]]:
if isinstance(node, dict):
for key, value in node.items():
if key in ("logMessages", "log_messages") and value:
yield value
else:
yield from walk(value)
elif isinstance(node, list):
for value in node:
yield from walk(value)
return next(walk(json.loads(path.read_text())), None)
def _create_fixtures() -> list[tuple[Path, list[str]]]:
"""Every decode fixture whose logs carry a create marker."""
found = []
for path in sorted(DECODE_DIR.glob("raw_create*.json")):
logs = _logs_from_fixture(path)
if logs and any("Instruction: Create" in line for line in logs):
found.append((path, logs))
return found
def _check(label: str, passed: bool, detail: str) -> bool: # noqa: FBT001
print(f" [{'PASS' if passed else 'FAIL'}] {label}: {detail}")
return passed
def check_real_creates_are_still_detected() -> bool:
print("\n1. Every create fixture is detected from its logs alone")
fixtures = _create_fixtures()
if not fixtures:
return _check(
"fixtures found", passed=False, detail="none under cookbook/pumpfun/decode"
)
parser = _parser()
ok = True
for path, logs in fixtures:
token_info = parser.parse_token_creation_from_logs(logs, path.stem)
ok &= _check(
path.name,
token_info is not None,
f"{token_info.symbol}" if token_info else "not detected",
)
return ok
def check_create_alongside_token_account_is_detected() -> bool:
print("\n2. A create sharing its transaction with CreateTokenAccount is detected")
fixtures = _create_fixtures()
parser = _parser()
ok = True
for path, logs in fixtures:
polluted = [*logs, "Program log: Instruction: CreateTokenAccount"]
token_info = parser.parse_token_creation_from_logs(polluted, path.stem)
ok &= _check(
path.name,
token_info is not None,
f"{token_info.symbol}" if token_info else "dropped by the veto",
)
return ok
def check_foreign_create_markers_are_rejected() -> bool:
print("\n3. A foreign Create* instruction alone is not a coin creation")
parser = _parser()
# The event blob is a real one from a fixture, so the rejection is the
# gate's doing and not a decode failure further down.
_, sample = _create_fixtures()[0]
program_data = [line for line in sample if "Program data:" in line]
ok = True
for marker in FOREIGN_CREATE_MARKERS:
logs = [
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]",
marker,
*program_data,
]
token_info = parser.parse_token_creation_from_logs(logs, marker)
ok &= _check(
marker.split(": ")[-1],
token_info is None,
"rejected" if token_info is None else f"accepted as {token_info.symbol}",
)
return ok
def check_no_site_matches_the_marker_as_a_substring() -> bool:
print("\n4. No site tests the create marker as a substring")
ok = True
for relative in GATE_SITES:
path = PROJECT_ROOT / relative
hits = [
f"{n}: {line.strip()}"
for n, line in enumerate(path.read_text().splitlines(), 1)
if SUBSTRING_GATE.search(line)
]
ok &= _check(str(relative), not hits, "clean" if not hits else "; ".join(hits))
return ok
def main() -> None:
"""Run every check and exit non-zero if any failed."""
print("Verifying the logs create gate")
results = [
check_real_creates_are_still_detected(),
check_create_alongside_token_account_is_detected(),
check_foreign_create_markers_are_rejected(),
check_no_site_matches_the_marker_as_a_substring(),
]
print(f"\n{sum(results)}/{len(results)} checks passed")
sys.exit(0 if all(results) else 1)
if __name__ == "__main__":
main()
@@ -0,0 +1,211 @@
"""Verify the compare_listeners PumpPortal lane reports pump.fun coins only.
`tools/compare_listeners.py` races five lanes for coverage. Four of them
subscribe to the pump.fun program; the fifth reads PumpPortal's
`subscribeNewToken`, which aggregates launchpads and names each coin's one in
the payload's `pool` field. The lane ignored that field, so every letsbonk
create entered the race as though it were a pump.fun coin — detected by
PumpPortal and by nobody else, because nobody else was listening for it, and so
scored as a simultaneous miss by all four on-chain lanes.
Offline machine checks, no network and no funds moved. A stub websocket replays
scripted payloads into the real `listen_pumpportal`:
1. A `pool: "pump"` payload is reported.
2. A `pool: "bonk"` payload is not. This is the check that fails before the
fix.
3. A payload naming no pool is not reported either — an unknown launchpad is
not assumed to be pump.fun.
4. Every payload still counts towards the lane's frame total, so filtering
them out does not read as a starved lane.
5. The ignored launchpads are tallied on the way out, so a run says what it
dropped rather than dropping it silently.
Usage:
uv run tests/regression/verify_pumpportal_pool_filter.py
"""
import asyncio
import contextlib
import importlib.util
import io
import json
import sys
from collections.abc import Iterable
from pathlib import Path
from types import ModuleType
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
PUMP_PAYLOAD = {
"signature": "1" * 88,
"mint": "PumpCoin1111111111111111111111111111111111",
"traderPublicKey": "Trader11111111111111111111111111111111111",
"txType": "create",
"name": "Pump Coin",
"symbol": "PUMP",
"pool": "pump",
}
BONK_PAYLOAD = {
"signature": "2" * 88,
"mint": "BonkCoin1111111111111111111111111111111111",
"traderPublicKey": "Trader11111111111111111111111111111111111",
"txType": "create",
"pool": "bonk",
}
# A bonk create carries no name, symbol or uri; a payload naming no pool at all
# is the shape a launchpad added to the feed later would arrive in.
POOLLESS_PAYLOAD = {
"signature": "3" * 88,
"mint": "Unknown11111111111111111111111111111111111",
"traderPublicKey": "Trader11111111111111111111111111111111111",
"txType": "create",
}
PAYLOADS = (PUMP_PAYLOAD, BONK_PAYLOAD, POOLLESS_PAYLOAD)
def _load_compare_listeners() -> ModuleType:
"""Import tools/compare_listeners.py by path; it is not an installed module."""
path = PROJECT_ROOT / "tools" / "compare_listeners.py"
spec = importlib.util.spec_from_file_location("compare_listeners", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class StubWebSocket:
"""Serves the scripted payloads, then cancels to end the lane."""
def __init__(self, payloads: Iterable[dict]) -> None:
self.payloads = list(payloads)
self.sent: list[dict] = []
async def send(self, message: str) -> None:
self.sent.append(json.loads(message))
async def recv(self) -> str:
if not self.payloads:
raise asyncio.CancelledError
return json.dumps(self.payloads.pop(0))
async def __aenter__(self) -> "StubWebSocket":
return self
async def __aexit__(self, *_exc: object) -> bool:
return False
class RecordingTracker:
"""Stands in for QueueTracker, keeping what the lane reported."""
def __init__(self) -> None:
self.tokens: list[tuple] = []
self.frames = 0
def add_token(
self,
mint: str,
name: str,
symbol: str,
lane: str,
timestamp: float,
) -> None:
self.tokens.append((mint, name, symbol, lane, timestamp))
def increment_messages(self, _lane: str) -> None:
self.frames += 1
def _run_lane() -> tuple[RecordingTracker, str]:
"""Drive the real lane over the scripted payloads.
Returns:
Tuple of (tracker holding what was reported, the lane's stdout)
"""
module = _load_compare_listeners()
tracker = RecordingTracker()
module.websockets.connect = lambda *_a, **_kw: StubWebSocket(PAYLOADS)
output = io.StringIO()
async def drive() -> None:
with contextlib.suppress(asyncio.CancelledError):
await module.listen_pumpportal("pumpportal", tracker, set())
with contextlib.redirect_stdout(output):
asyncio.run(drive())
return tracker, output.getvalue()
def _check(label: str, passed: bool, detail: str) -> bool: # noqa: FBT001
print(f" [{'PASS' if passed else 'FAIL'}] {label}: {detail}")
return passed
def main() -> None:
"""Run every check and exit non-zero if any failed."""
print("Verifying the PumpPortal lane's launchpad filter")
tracker, output = _run_lane()
reported = {token[0] for token in tracker.tokens}
print("\n1. A pump.fun coin is reported")
results = [
_check(
PUMP_PAYLOAD["mint"],
PUMP_PAYLOAD["mint"] in reported,
"reported" if PUMP_PAYLOAD["mint"] in reported else "missing",
)
]
print("\n2. A letsbonk coin is not")
results.append(
_check(
BONK_PAYLOAD["mint"],
BONK_PAYLOAD["mint"] not in reported,
"ignored"
if BONK_PAYLOAD["mint"] not in reported
else "counted against the on-chain lanes",
)
)
print("\n3. A payload naming no pool is not reported")
results.append(
_check(
POOLLESS_PAYLOAD["mint"],
POOLLESS_PAYLOAD["mint"] not in reported,
"ignored" if POOLLESS_PAYLOAD["mint"] not in reported else "reported",
)
)
print("\n4. Every payload still counts as a frame")
results.append(
_check(
"frames",
tracker.frames == len(PAYLOADS),
f"{tracker.frames} of {len(PAYLOADS)}",
)
)
print("\n5. The ignored launchpads are tallied")
tallied = "ignored coins from other launchpads" in output and "bonk=1" in output
results.append(
_check(
"shutdown tally",
tallied,
next(
(line for line in output.splitlines() if "launchpads" in line),
"not printed",
),
)
)
print(f"\n{sum(results)}/{len(results)} checks passed")
sys.exit(0 if all(results) else 1)
if __name__ == "__main__":
main()
+56 -22
View File
@@ -15,7 +15,9 @@ with per-method latency, message counts and coverage:
This races on *coverage*: which lane saw a given mint, and how much later than
the winner. A lane that never reports a mint is the finding, not a rounding
error — `shreds` misses router creates and PumpPortal samples its feed.
error — `shreds` misses router creates and PumpPortal samples its feed. Every
lane is restricted to pump.fun coins, PumpPortal included: it aggregates other
launchpads, and their coins would read as a miss by all four on-chain lanes.
`tools/compare_deshred_latency.py` answers the other question, pairing deshred
against executed per signature to measure the lead itself.
@@ -40,6 +42,7 @@ import os
import struct
import sys
import time
from collections import Counter
from pathlib import Path
from queue import Empty
@@ -95,6 +98,15 @@ LANE_SHUTDOWN_GRACE = 10
# PumpPortal WebSocket endpoint (third-party service)
PUMPPORTAL_WS_URL = "wss://pumpportal.fun/api/data"
# PumpPortal aggregates several launchpads and names each coin's one in `pool`.
# The four on-chain lanes subscribe to the pump.fun program alone, so anything
# else is a coin they are not watching for, not one they missed.
PUMPPORTAL_POOL = "pump"
# The two log lines the pump.fun program writes when it creates a coin.
CREATE_LOG = "Program log: Instruction: Create"
CREATE_V2_LOG = "Program log: Instruction: CreateV2"
# Sampling window in seconds, overridable with --duration. Short runs see few
# coins, and a lane's coverage gap only shows up over enough of them.
DEFAULT_DURATION = 60
@@ -559,10 +571,7 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
# difference against logs and geyser.
meta = tx.get("meta") or {}
logs = meta.get("logMessages") or []
if not any(
"Program log: Instruction: Create" in log
for log in logs
):
if not (CREATE_LOG in logs or CREATE_V2_LOG in logs):
continue
decoded = None
@@ -585,10 +594,7 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
if not mint or mint in known_tokens:
continue
is_v2 = any(
"Program log: Instruction: CreateV2" in log
for log in logs
)
is_v2 = CREATE_V2_LOG in logs
kind = (
"CreateV2 (Token2022)" if is_v2 else "Create (Legacy)"
)
@@ -668,13 +674,14 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
# Detect both Create and CreateV2 instructions
is_create = any(
"Program log: Instruction: Create" in log for log in logs
)
is_create_v2 = any(
"Program log: Instruction: CreateV2" in log for log in logs
)
# Detect both Create and CreateV2 instructions. The
# lines are matched whole: Anchor writes
# `Instruction: <Name>` for every program, so a
# substring test also accepts CreateTokenAccount,
# CreatePool and the rest from whichever programs share
# the transaction.
is_create = CREATE_LOG in logs
is_create_v2 = CREATE_V2_LOG in logs
if not (is_create or is_create_v2):
continue
@@ -997,12 +1004,36 @@ async def listen_deshred_grpc(
async def listen_pumpportal(provider_name, tracker, known_tokens=None):
"""Listen for new tokens via PumpPortal WebSocket"""
"""Listen for new tokens via PumpPortal WebSocket.
Only pump.fun coins are reported. `subscribeNewToken` also carries
letsbonk creates, and counting those against lanes that never subscribed to
that program turns a launchpad this race does not cover into a hole in
every on-chain lane at once.
"""
if known_tokens is None:
known_tokens = set()
lane = f"{provider_name}_pumpportal"
skipped_by_pool = Counter()
try:
await _pumpportal_loop(lane, tracker, known_tokens, skipped_by_pool)
finally:
if skipped_by_pool:
# A payload naming no pool sorts and prints as "unknown" rather than
# being left out: an unrecognised launchpad is the case worth seeing.
tally = ", ".join(
f"{pool or 'unknown'}={count}"
for pool, count in sorted(
skipped_by_pool.items(), key=lambda item: str(item[0])
)
)
print(f"[INFO] {lane} ignored coins from other launchpads: {tally}")
async def _pumpportal_loop(lane, tracker, known_tokens, skipped_by_pool):
"""Drain the PumpPortal feed until cancelled, reconnecting as needed."""
while True:
try:
print("[INFO] Connecting to PumpPortal WebSocket...")
@@ -1011,7 +1042,7 @@ async def listen_pumpportal(provider_name, tracker, known_tokens=None):
await websocket.send(
json.dumps({"method": "subscribeNewToken", "params": []})
)
print(f"[INFO] PumpPortal listener active for {provider_name}")
print(f"[INFO] PumpPortal listener active for {lane}")
while True:
try:
@@ -1037,6 +1068,11 @@ async def listen_pumpportal(provider_name, tracker, known_tokens=None):
if not mint:
continue
pool = token_info.get("pool")
if pool != PUMPPORTAL_POOL:
skipped_by_pool[pool] += 1
continue
if mint in known_tokens:
continue
@@ -1046,13 +1082,11 @@ async def listen_pumpportal(provider_name, tracker, known_tokens=None):
known_tokens.add(mint)
except Exception as e:
print(f"[ERROR] PumpPortal listener for {provider_name}: {e}")
print(f"[ERROR] PumpPortal listener for {lane}: {e}")
break
except Exception as e:
print(
f"[ERROR] Connection error in PumpPortal listener for {provider_name}: {e}"
)
print(f"[ERROR] Connection error in PumpPortal listener for {lane}: {e}")
print("[INFO] Reconnecting in 5 seconds...")
await asyncio.sleep(5)