Files
pumpfun-bonkfun-bot_github/tests/regression/verify_cookbook_arguments.py
T
1e2de43292 feat(geyser): upgrade to yellowstone proto 13.0.0-rc4 and read the transaction v1 config (#219)
The vendored geyser protos predated transaction v1 (SIMD-0385, live since
2026-09-15). Message.config -- the inline compute budget that v1 moved off the
ComputeBudget instructions -- is field 7, which the old stubs had no definition
for. Protobuf skips an unknown field rather than failing, so the gap was
silent: frames still decoded and only the budget went missing.

Pin to yellowstone v16.0.0-rc10+solana.4.3.0 (commit 301e9cf, proto
13.0.0-rc4), the build the endpoint itself reports from GetVersion. Not master:
master has diverged and is missing block_footer, bank_id and VATDebit.
Regenerate the stubs (gencode 6.31.1 -> 7.35.1) and rewrite the emitted
relative imports to the absolute form this repo requires.

Fields adopted: Message.config and TransactionStatusMeta.cost_units. Over
geyser there is no version number on the wire and `versioned` is true for v0
and v1 alike, so config presence is the version test; the listener and the
geyser example both report it. CommitmentLevel lost members 3-6 to a new
SlotStatus enum -- the one breaking change, and nothing here sent anything but
PROCESSED.

Adds a cookbook example for SubscribeDeshred, the pre-execution stream, and
tools/compare_deshred_latency.py to measure it. Deshred is deliberately not
wired into the bot: it carries no TransactionStatusMeta, so no CreateEvent and
no state_from_event, and a router-created coin is structurally invisible on it
because the create is an inner instruction and inner instructions are produced
by execution. Measured 4 of 118 creates missed in a 300s window, against a
6.4ms median lead on the creates it does see.

verify_transaction_v1.py gains four geyser checks on a committed v1
SubscribeUpdate, pinning the log route and the instruction fallback separately,
as the block listener already does.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 15:26:41 +02:00

163 lines
6.0 KiB
Python

"""Verify every cookbook script takes its input on the command line.
The cookbook is a reference collection: someone reads a script, then runs it
against their own coin. That only works if the coin is an argument. Scripts used
to carry it as a module-level constant instead:
TOKEN_MINT = Pubkey.from_string(sys.argv[1] if len(sys.argv) > 1 else "...")
which reads as "optional", but `Pubkey.from_string("...")` raises at import, so
running the script the way its own usage line describes it — with no argument —
died on `ValueError: Invalid Base58 string` before printing anything. Nine
scripts did this. Others took their amount and slippage from environment
variables, which no usage line mentioned at all.
The rule is that a value the caller would vary per run is an argument. A default
is fine, and encouraged; a default that is not a real value is not a default.
Offline machine checks, no network and no funds moved:
A. No runnable script reads `sys.argv` at module level.
B. No script contains a placeholder standing in for a real address.
C. Every script that takes input builds an `ArgumentParser`, so `--help` works
and a bad address is rejected with a usage message rather than a traceback.
D. Every `ArgumentParser` default is a usable value, not a placeholder.
Usage:
uv run tests/regression/verify_cookbook_arguments.py
"""
import ast
import re
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
COOKBOOK = PROJECT_ROOT / "cookbook"
# A value that looks like configuration but cannot be used as one.
PLACEHOLDER = re.compile(r'"(\.\.\.|YOUR_[A-Z_]+|<[A-Z_]+>)"')
# Scripts that genuinely take no input: they subscribe to a program and print
# what arrives. Nothing to parameterise, so nothing to get wrong.
NO_INPUT = {
"pumpfun_listen_tokens_logsubscribe.py",
"pumpfun_listen_tokens_blocksubscribe.py",
"pumpfun_listen_tokens_geyser.py",
"pumpfun_listen_tokens_deshred.py",
"pumpfun_listen_tokens_pumpportal.py",
"pumpfun_listen_migrations_logsubscribe.py",
"pumpfun_listen_migrations_programsubscribe.py",
"pumpfun_capture_transactions_blocksubscribe.py",
}
def runnable_scripts() -> list[Path]:
"""Every cookbook script with a `__main__` guard.
Returns:
The runnable scripts, sorted. Import-only helpers are excluded.
"""
return sorted(
p for p in COOKBOOK.rglob("*.py") if '__name__ == "__main__"' in p.read_text()
)
def check_no_module_level_argv() -> None:
"""Input must be read inside a function, not while the module imports."""
offenders = []
for path in runnable_scripts():
tree = ast.parse(path.read_text())
for node in tree.body:
for sub in ast.walk(node):
if isinstance(sub, ast.Attribute) and sub.attr == "argv":
offenders.append(f"{path.relative_to(PROJECT_ROOT)}:{sub.lineno}")
assert not offenders, (
"sys.argv read at module level, so the script fails before main() runs:\n "
+ "\n ".join(sorted(set(offenders)))
)
def check_no_placeholders() -> None:
"""A placeholder is not a default; it is a crash with extra steps."""
offenders = []
for path in runnable_scripts():
for number, line in enumerate(path.read_text().splitlines(), 1):
if PLACEHOLDER.search(line) and not line.lstrip().startswith("#"):
offenders.append(
f"{path.relative_to(PROJECT_ROOT)}:{number}: {line.strip()[:70]}"
)
assert not offenders, "placeholder values standing in for real input:\n " + (
"\n ".join(offenders)
)
def check_scripts_use_argparse() -> None:
"""Anything taking input parses it, so --help and errors work."""
offenders = [
str(path.relative_to(PROJECT_ROOT))
for path in runnable_scripts()
if path.name not in NO_INPUT and "ArgumentParser" not in path.read_text()
]
assert not offenders, "scripts taking input without argparse:\n " + "\n ".join(
offenders
)
def check_defaults_are_usable() -> None:
"""Every argparse default must be a value the script can actually run with."""
offenders = []
for path in runnable_scripts():
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
if not (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "add_argument"
):
continue
for keyword in node.keywords:
if keyword.arg != "default":
continue
if isinstance(keyword.value, ast.Constant) and isinstance(
keyword.value.value, str
):
if PLACEHOLDER.search(f'"{keyword.value.value}"'):
offenders.append(
f"{path.relative_to(PROJECT_ROOT)}:{node.lineno}: "
f"default={keyword.value.value!r}"
)
assert not offenders, "argparse defaults that are not usable values:\n " + (
"\n ".join(offenders)
)
def main() -> None:
"""Run every check and report."""
print("=" * 72)
print("Verifying cookbook scripts take their input as arguments")
print("=" * 72)
scripts = runnable_scripts()
print(f"{len(scripts)} runnable scripts, {len(NO_INPUT)} of them taking no input\n")
for label, check in [
("no module-level sys.argv", check_no_module_level_argv),
("no placeholder values", check_no_placeholders),
("input is parsed with argparse", check_scripts_use_argparse),
("every default is usable", check_defaults_are_usable),
]:
check()
print(f"{label} -> OK")
missing = sorted(NO_INPUT - {p.name for p in scripts})
if missing:
print(f"\nNote: exempt scripts no longer present: {', '.join(missing)}")
sys.exit(1)
print("\nALL CHECKS PASSED")
if __name__ == "__main__":
main()