mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
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>
163 lines
6.0 KiB
Python
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()
|