mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
fix(monitoring,core): keep detecting tokens after the Solana transaction v1 cutover (#212)
* fix(monitoring,core): keep detecting tokens after the Solana transaction v1 cutover Solana transaction v1 (SIMD-0296/0385) went live at epoch 1035 on 2026-09-15. `maxSupportedTransactionVersion` turns out to be a whole-frame setting, not a per-transaction filter: asking blockSubscribe for 0 does not skip the v1 transactions in a block, it nulls out `value.block` for the entire notification, which reads as a skipped slot. Measured against mainnet 2026-09-16, 60s per run on the pump program: 0 delivered 1 block and 177 nulls, 1 delivered 78 blocks and no nulls. solders 0.26 cannot deserialize a v1 envelope either, but it does not need to. The RPC has already decoded it by the time `meta.logMessages` is emitted, and the platform parsers prefer the CreateEvent in those logs anyway, for the canonical creator. What broke was the dispatch: _process_block_transactions decoded the bytes first only to learn which program a transaction touched, and swallowed the failure in a bare `except Exception: pass`. It now tries _parse_from_logs on every transaction first and keeps the byte decode as a fallback, so v1 coins arrive with state_from_event=True and extreme_fast_mode keeps its zero-RPC contract. Also raises every other reader to v1, fixes the null-block crash in listen_blocksubscribe.py, and records that PumpPortal does not push coins created in a v1 transaction — 0 of 6 measured, against near-complete v0 coverage. That one is upstream and not fixable here. Closes #197 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98 * fix(examples): survive a v1 envelope and a null block per notification Raising maxSupportedTransactionVersion to 1 means these examples now receive the v1 transactions the RPC used to withhold, and solders 0.26 cannot decode one. manual_buy.py had no handler at all around from_bytes, so the first v1 transaction in a block would have ended listen_for_create_transaction outright — a regression introduced by the version bump itself. Guards the null `value.block` in the same three places while here: the key is present and the value is not for a skipped slot, so the membership test raises TypeError and costs the whole notification. Same root cause as #196. Found by CodeRabbit on #212. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98 * fix(examples): raise the shared confirm helper to v1 and decode create_v2's OptionU64 Two gaps the first pass missed, both found by running the changed code against mainnet instead of only exercising it offline. tx_status.py confirmed through solana-py's snake_case `max_supported_transaction_version=0`, a spelling the source scan in verify_transaction_v1.py did not know about — so the sweep reported clean while the helper every example confirms through still rejected v1. Verified live: at 0 it raises UnsupportedTransactionVersionMessage on a v1 signature, at 1 it reads meta.err. The scan now covers both spellings, and setting the threshold above 1 makes it name all seven call sites, so the check is not vacuous. listen_blocksubscribe.py then turned out to have a third cause behind #197 that no offline check could reach, because until this branch the example never received a block to decode: its local create_v2 decoder raises on OptionU64, the arg added by the 2026-09-15 program upgrade. Live before the fix, 90s: 0 creates decoded, 8 tracebacks. After: 58 creates, 0 tracebacks. Trailing option args are reported unset rather than as a fabricated default, matching utils/idl_parser.py and the three wire forms that are live on chain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98 * fix(examples): decode create_v2's trailing optional args in manual_buy Found by running the script against mainnet with real funds. It crashed before it could buy: File "learning-examples/manual_buy.py", line 316, in decode_create_instruction value = bool(ix_data[offset]) IndexError: index out of range decode_create_instruction treated every `defined` arg as a 1-byte OptionBool and read it unconditionally. create_v2's trailing args are positional and legally absent from the wire, and the 2026-09-15 upgrade added creator_fee_bps as an 8-byte OptionU64, so the fixed read walks off the end on the shorter forms — which is most coins. Same defect class as the OptionU64 crash in listen_blocksubscribe.py, and invisible until this branch let either script receive a block at all. Absent trailing options are now reported as None, meaning unset, matching utils/idl_parser.py. Verified live: the script decodes a create with creator_fee_bps and is_holder_reward both null, then buys 69,387 tokens for 0.003 SOL, confirmed on-chain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98 * refactor(examples): drop one v1 fixture and trim the other Two fixtures was one too many. The v0 one was captured to prove the byte-decode path still works, but it never did: the listener routes on logs first, so the fixture was caught there and the check passed without exercising what its name claimed. Feeding it through with the logs stripped shows the byte fallback does not parse a v0 create_v2 either — the parser's own logs-first branch was always doing the detection. So the check now reuses the create fixture already committed for verify_create_v2_optional_args. The v1 fixture stays, because a v1 transaction cannot be synthesized locally — solders 0.26 cannot build one — and every verifier here runs offline. Trimmed from 30KB to 13KB by dropping what nothing reads: innerInstructions, token balances and pre/post balances. What is left is the envelope, the version and the logs the listener parses. Adds the check that was missing: strip the logs and the v1 create must vanish. That is what makes the log route load-bearing rather than decorative, and it will start failing the day solders learns to decode a v1 envelope, which is exactly when the routing could be simplified again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e1c09f6605
commit
13b9ec2baf
@@ -163,6 +163,55 @@ trailing option-typed args as unset — `verify_create_v2_optional_args.py`
|
||||
machine-checks that, and that mandatory args still fail the decode. The
|
||||
log/event path stays preferred for the canonical-creator reason above.
|
||||
|
||||
### Verifying transaction v1 handling (Solana cutover 2026-09-15)
|
||||
|
||||
```bash
|
||||
# Offline: every reader accepts v1, and the blocks listener detects a v1
|
||||
# create_v2 without decoding its envelope
|
||||
uv run learning-examples/verify_transaction_v1.py
|
||||
```
|
||||
|
||||
Solana **transaction v1** (SIMD-0296 size, SIMD-0385 format) went live at epoch
|
||||
1035, ~01:04 UTC 2026-09-15. 4096-byte envelope, first byte `129`, signatures at
|
||||
the tail, **no address lookup tables** (every account inlined, 64 max), and the
|
||||
resource limits — priority fee in **total lamports**, CU limit,
|
||||
loaded-accounts-data-size, heap — moved out of ComputeBudget instructions into a
|
||||
fixed-offset config. ComputeBudget instructions in a v1 transaction are ignored
|
||||
for configuration and merely burn CU. Legacy and v0 both stay valid with no
|
||||
announced sunset, and the bot still **sends** legacy, so nothing on the trade
|
||||
path changed. Everything below is about reading other people's transactions.
|
||||
|
||||
**`maxSupportedTransactionVersion` is a whole-frame setting, not a
|
||||
per-transaction filter.** Asking `blockSubscribe` for `0` does not skip the v1
|
||||
transactions in a block — the RPC nulls out `value.block` for the entire
|
||||
notification, which is indistinguishable from a skipped slot. Measured against
|
||||
mainnet 2026-09-16, 60s per run on the pump program: `0` delivered 1 block and
|
||||
177 nulls, `1` delivered 78 blocks and no nulls. `getBlock` is louder about the
|
||||
same thing, answering `-32015`. So `0` is not a conservative default any more,
|
||||
it is a near-total outage of the blocks listener. Every call site in `src/` and
|
||||
`learning-examples/` now sends `1`, and the verifier fails the build if one
|
||||
regresses.
|
||||
|
||||
**solders 0.26 cannot deserialize a v1 transaction** —
|
||||
`VersionedTransaction.from_bytes` raises `ValueError: io error: unexpected end
|
||||
of file`. It does not need to: the RPC has already decoded the envelope by the
|
||||
time it emits `meta.logMessages`, and the platform parsers prefer the CreateEvent
|
||||
in those logs anyway, for the canonical creator. What broke was the *dispatch* —
|
||||
`UniversalBlockListener._process_block_transactions` decoded the bytes first only
|
||||
to learn which program the transaction touched, and swallowed the failure in a
|
||||
bare `except Exception: pass`. It now tries `_parse_from_logs` on every
|
||||
transaction first and keeps the byte decode as a fallback, so v1 coins arrive with
|
||||
`state_from_event=True` and `extreme_fast_mode` keeps its zero-RPC contract.
|
||||
Verified on two live v1 `create_v2` coins. A later bump to solders ≥0.28 (which
|
||||
adds `MessageV1`/`TransactionConfig`) would restore the byte path too, but it is
|
||||
gated behind `solana==0.36.6`, which pins `solders<0.27` — **not** required for
|
||||
detection, so don't reach for it to fix a listener.
|
||||
|
||||
Measured the same day: pump.fun 21/100 and letsbonk 16/100 recent program
|
||||
transactions were v1, 10 of 10 sampled blocks contained at least one, and 2 of 13
|
||||
successful `create_v2` coins in one window were minted in v1 transactions. This
|
||||
is not a future problem.
|
||||
|
||||
### Verifying transaction-status handling
|
||||
|
||||
```bash
|
||||
@@ -308,6 +357,11 @@ offline and only visible after a couple of minutes against mainnet.
|
||||
reconnect handler with its `sleep` is unreachable in that shape. A narrow
|
||||
`except TimeoutError` or `except json.JSONDecodeError` is fine to swallow —
|
||||
those are per-message, not per-connection.
|
||||
- **Never gate a listener's dispatch on decoding the transaction envelope.**
|
||||
The envelope is the one part of a transaction whose format changes under you —
|
||||
solders cannot read a v1 one at all. `meta.logMessages` is already decoded by
|
||||
the RPC and is version-agnostic, so route on it and keep the byte decode as a
|
||||
fallback. See "Verifying transaction v1 handling" above.
|
||||
- **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
|
||||
@@ -550,6 +604,12 @@ interface pump.fun maintains.
|
||||
checks this against committed fixtures, and `--live` re-checks it against the
|
||||
real feed. The bonk **trade** path past detection is still unverified — see
|
||||
issue #201.
|
||||
- **PumpPortal does not report coins created in a transaction v1.** Measured
|
||||
2026-09-16 across two windows: 0 of 6 v1 creates pushed, against near-complete
|
||||
coverage of v0 creates (104 of 105 in one 240s window). It is a third-party
|
||||
feed, so no local change recovers them — the coin is never sent. This compounds
|
||||
the thin-bonk-payload gap below, and it means `pumpportal` is a sampling feed
|
||||
now, not a complete one. `bots/bot-sniper-4-pp.yaml` carries the same warning.
|
||||
- `config_loader.py` validates the platform/listener pairing before startup:
|
||||
pump.fun supports `logs`, `blocks`, `geyser`, `pumpportal`; letsbonk.fun
|
||||
supports `blocks`, `geyser`, `pumpportal` — **not `logs`**. Adding a listener
|
||||
|
||||
@@ -71,10 +71,12 @@ Each YAML file in `bots/` is one bot instance. They ship with commented defaults
|
||||
| `bot-sniper-1-geyser.yaml` | `geyser` — fastest, needs a Geyser endpoint | `pump_fun` |
|
||||
| `bot-sniper-2-logs.yaml` | `logs` — `logsSubscribe`, supported everywhere | `pump_fun` |
|
||||
| `bot-sniper-3-blocks.yaml` | `blocks` — `blockSubscribe`, not supported by every provider | `pump_fun` |
|
||||
| `bot-sniper-4-pp.yaml` | `pumpportal` — third-party aggregator | `lets_bonk` |
|
||||
| `bot-sniper-4-pp.yaml` | `pumpportal` — third-party aggregator, misses some coins | `lets_bonk` |
|
||||
|
||||
Set `platform: "pump_fun"` or `platform: "lets_bonk"`. pump.fun supports all four listeners; letsbonk.fun supports `blocks`, `geyser`, and `pumpportal` but **not** `logs`. The bot validates the pairing at startup and refuses to run an invalid one.
|
||||
|
||||
`pumpportal` is a third-party feed and only reports what it indexes. As of 2026-09-16 it does not push coins whose creation landed in a Solana transaction v1 (a format live since 2026-09-15), so it sees a sample rather than everything. `geyser` and `logs` read the chain directly and are unaffected; `blocks` needs `maxSupportedTransactionVersion: 1`, which it now sends.
|
||||
|
||||
Set `enabled: false` to keep a config around without running it. Every bot with `enabled: true` starts when you run the bot.
|
||||
|
||||
### 5. Run
|
||||
|
||||
@@ -90,6 +90,12 @@ compute_units:
|
||||
filters:
|
||||
match_string: null # Only process tokens with this string in name/symbol
|
||||
bro_address: null # Only trade tokens created by this user address
|
||||
# PumpPortal is a third-party feed, so it only reports what it indexes.
|
||||
# Measured 2026-09-16: it reported 0 of 6 coins whose create landed in a
|
||||
# Solana transaction v1 (live since 2026-09-15), against near-complete
|
||||
# coverage of v0 creates. Nothing in this repo can recover those — the
|
||||
# coin is simply never pushed. Use "geyser" or "logs" if you need every
|
||||
# coin; see learning-examples/verify_transaction_v1.py.
|
||||
listener_type: "pumpportal" # Method for detecting new tokens: "logs", "blocks", "geyser", or "pumpportal"
|
||||
max_token_age: 0.001 # Maximum token age in seconds for processing
|
||||
marry_mode: false # Only buy tokens, skip selling
|
||||
|
||||
@@ -65,7 +65,7 @@ async def listen_for_transactions():
|
||||
"encoding": "base64",
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
"maxSupportedTransactionVersion": 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -83,7 +83,8 @@ async def listen_for_transactions():
|
||||
block_data = data["params"]["result"]
|
||||
if "value" in block_data and "block" in block_data["value"]:
|
||||
block = block_data["value"]["block"]
|
||||
if "transactions" in block:
|
||||
# `block` is null for a skipped or unavailable slot.
|
||||
if block and "transactions" in block:
|
||||
transactions = block["transactions"]
|
||||
for tx in transactions:
|
||||
tx_signature = extract_signature(tx)
|
||||
|
||||
@@ -519,7 +519,7 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
|
||||
"encoding": "base64",
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
"maxSupportedTransactionVersion": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -544,8 +544,11 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
|
||||
):
|
||||
continue
|
||||
|
||||
# `block` is null for a skipped or unavailable slot,
|
||||
# which would make the membership test raise TypeError
|
||||
# and cost this lane the whole notification.
|
||||
block = block_data["value"]["block"]
|
||||
if "transactions" not in block:
|
||||
if not block or "transactions" not in block:
|
||||
continue
|
||||
|
||||
for tx in block["transactions"]:
|
||||
@@ -620,6 +623,12 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
|
||||
f"[ERROR] Failed to process block instruction: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
# A v1 transaction (Solana, live 2026-09-15)
|
||||
# lands here: solders cannot deserialize the
|
||||
# envelope, so this lane under-counts creates
|
||||
# by the v1 share while logs and geyser, which
|
||||
# read meta.logMessages, count them all. Do not
|
||||
# read that gap as a speed difference.
|
||||
print(f"[ERROR] Failed to process transaction: {e}")
|
||||
|
||||
except websockets.ConnectionClosed:
|
||||
|
||||
@@ -204,7 +204,18 @@ def decode_create_v2_instruction(ix_data, ix_def, accounts):
|
||||
|
||||
# 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).
|
||||
# is_cashback_enabled (OptionBool), creator_fee_bps (OptionU64),
|
||||
# is_holder_reward (OptionBool). The last two arrived with the 2026-09-15
|
||||
# program upgrade.
|
||||
#
|
||||
# OptionBool and OptionU64 are single-field Anchor structs with no presence
|
||||
# tag: each serializes as its bare inner value, 1 and 8 bytes. They are also
|
||||
# positional rather than independently optional, and the trailing ones are
|
||||
# legally absent from the wire — three lengths are live on chain (no
|
||||
# trailing args, is_cashback_enabled only, is_cashback_enabled plus
|
||||
# creator_fee_bps). An absent one is reported as None, meaning unset, rather
|
||||
# than as a fabricated default. Same rule as utils/idl_parser.py (issue
|
||||
# #184); reading a fixed number of trailing bytes raises IndexError instead.
|
||||
for arg in ix_def["args"]:
|
||||
t = arg["type"]
|
||||
if t == "string":
|
||||
@@ -221,8 +232,17 @@ def decode_create_v2_instruction(ix_data, ix_def, accounts):
|
||||
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
|
||||
if offset >= len(ix_data):
|
||||
value = None
|
||||
else:
|
||||
value = bool(ix_data[offset])
|
||||
offset += 1
|
||||
elif defined_name == "OptionU64":
|
||||
if offset + 8 > len(ix_data):
|
||||
value = None
|
||||
else:
|
||||
value = struct.unpack_from("<Q", ix_data, offset)[0]
|
||||
offset += 8
|
||||
else:
|
||||
raise ValueError(f"Unsupported defined type: {defined_name}")
|
||||
else:
|
||||
@@ -270,7 +290,7 @@ async def listen_and_decode_create():
|
||||
"encoding": "base64",
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
"maxSupportedTransactionVersion": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -288,15 +308,39 @@ async def listen_and_decode_create():
|
||||
block_data = data["params"]["result"]
|
||||
if "value" in block_data and "block" in block_data["value"]:
|
||||
block = block_data["value"]["block"]
|
||||
if "transactions" in block:
|
||||
# `block` is null for a skipped or unavailable slot:
|
||||
# the key is present, the value is not. Without this
|
||||
# check the membership test below raises TypeError.
|
||||
if block and "transactions" in block:
|
||||
for tx in block["transactions"]:
|
||||
if isinstance(tx, dict) and "transaction" in tx:
|
||||
tx_data_decoded = base64.b64decode(
|
||||
tx["transaction"][0]
|
||||
)
|
||||
transaction = VersionedTransaction.from_bytes(
|
||||
tx_data_decoded
|
||||
)
|
||||
try:
|
||||
transaction = (
|
||||
VersionedTransaction.from_bytes(
|
||||
tx_data_decoded
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
# Solana transaction v1 (live since
|
||||
# 2026-09-15) starts with byte 129 and
|
||||
# the installed solders cannot
|
||||
# deserialize it. Report it instead of
|
||||
# letting one transaction end the block
|
||||
# — this example decodes the
|
||||
# instruction from the envelope, so it
|
||||
# has nothing else to fall back on.
|
||||
# The bot's own blocks listener routes
|
||||
# through meta.logMessages, which works
|
||||
# for every version.
|
||||
if tx_data_decoded[:1] == b"\x81":
|
||||
print(
|
||||
"⚠️ Skipping a v1 transaction: "
|
||||
"solders cannot decode it yet"
|
||||
)
|
||||
continue
|
||||
|
||||
# Extract loaded addresses from transaction metadata
|
||||
loaded_addresses = None
|
||||
|
||||
@@ -312,9 +312,26 @@ def decode_create_instruction(ix_data, ix_def, accounts):
|
||||
value = bool(ix_data[offset])
|
||||
offset += 1
|
||||
elif isinstance(t, dict) and "defined" in t:
|
||||
# OptionBool = struct { bool } = 1 byte
|
||||
value = bool(ix_data[offset])
|
||||
offset += 1
|
||||
# OptionBool and OptionU64 are single-field Anchor structs with no
|
||||
# presence tag: each serializes as its bare inner value, 1 and 8
|
||||
# bytes. They are positional, and the trailing ones are legally
|
||||
# absent from the wire — reading them unconditionally raises
|
||||
# IndexError on the shorter forms, which is every coin whose
|
||||
# create_v2 stops early. An absent one is reported as None, meaning
|
||||
# unset, matching utils/idl_parser.py.
|
||||
defined = t["defined"]
|
||||
name = defined["name"] if isinstance(defined, dict) else defined
|
||||
if name == "OptionU64":
|
||||
if offset + 8 > len(ix_data):
|
||||
value = None
|
||||
else:
|
||||
value = struct.unpack_from("<Q", ix_data, offset)[0]
|
||||
offset += 8
|
||||
elif offset >= len(ix_data):
|
||||
value = None
|
||||
else:
|
||||
value = bool(ix_data[offset])
|
||||
offset += 1
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {t}")
|
||||
|
||||
@@ -350,7 +367,7 @@ async def listen_for_create_transaction():
|
||||
"encoding": "base64",
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
"maxSupportedTransactionVersion": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -367,15 +384,28 @@ async def listen_for_create_transaction():
|
||||
block_data = data["params"]["result"]
|
||||
if "value" in block_data and "block" in block_data["value"]:
|
||||
block = block_data["value"]["block"]
|
||||
if "transactions" in block:
|
||||
# `block` is null for a skipped or unavailable slot:
|
||||
# the key is present, the value is not.
|
||||
if block and "transactions" in block:
|
||||
for tx in block["transactions"]:
|
||||
if isinstance(tx, dict) and "transaction" in tx:
|
||||
tx_data_decoded = base64.b64decode(
|
||||
tx["transaction"][0]
|
||||
)
|
||||
transaction = VersionedTransaction.from_bytes(
|
||||
tx_data_decoded
|
||||
)
|
||||
try:
|
||||
transaction = (
|
||||
VersionedTransaction.from_bytes(
|
||||
tx_data_decoded
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
# A Solana v1 transaction (first byte
|
||||
# 129, live since 2026-09-15) that the
|
||||
# installed solders cannot decode. Skip
|
||||
# it rather than ending the listener —
|
||||
# the create we are waiting for may be
|
||||
# later in the same block.
|
||||
continue
|
||||
|
||||
for ix in transaction.message.instructions:
|
||||
if str(
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"transaction": [
|
||||
"gQIAEAwAAADIMuQrOCu5+ZbvKoknqFBpiIXxd/asz7yeJUboQ01Q3gMl/7V2hHCyPEYsYI9Mg0kK3vB1/Sj82VggoCRXq/AR3ZtF3KeJrTWespItdWZBTZZCiM9NOHw1kYmRupY3y44yTgUq5den2qckpuqwpylUkYVa1KBnFmBnTE4DRVmAPWWjEwClcG2YJJ9NCjAIBF7vxYTMGNrR65i2CJLIMdEL3WQgF1mMQwH21/W4L/MZX/2ap7gNw9EH4IY4apauSdm5riEIW0VFvuErY/n6aMWKlo3zOc6o98iQpOMFF5u3z301LUhbU7y64r7fi2/sYzOZhzXIJzx0ZPHjpj4fxzLOqMJKwvjQ3Vy8l+MonBl8tQYqVPPZVrnOblEV+WVnqlyz5lPGGcLPdiAOEoMfmKgAGMN8nYQAssOzRJwu4vvIoAsWXRhGc1ZLrQu7O6bfSIIiYDJeJBpD2OXCcL7NEHRZgERfp8M1uWEUofaYuIXuFFTqeNC2wpxIv67bydcWOhjLHGSy2XbmblDj2xF++iCCgun9GiMAqdk4+5xExeVjHD+bcd6O47XIEgaTmt5iVIdLckvgDtu4Xc/UzMrOtG5SMm+A5EjYb7n9YVo/YlGSdY4pFV/9/Iu5L2yxM/tNt1Wdq44E8he9SlSrfcOsWDFi33/10HSNmGHFvUJ93ws4FE79ootf0mq0eaapzGy/awsj62GIWjceASCsqRO+7z0Tini9Az1j17BV9vHBIeZwaSWu7qpTjuI5VY3BvaNjZjOq/std2FL7GfnBvcWcj0emS4aampvj1TBcY9Z7CCUV2sUd5qfiIGi7iGQKpX+QkwjGH+9xGgFj9adVwHC8hg0fY2fwpkBbTzdXHR7KBPjBLLogbguRcgu1w+Lz5DKc5Fd65PeP5MY9g4BwiKZgH4knUZwNwkuz4jyrzhRKBfXnC7UmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAra6ifbPG89BOXPCiAp8D4zmPCFOV7+/vFx/GSvgXQQFW4PaTZlrPRNsVaL8XW6pRicuX9dL/O2VdK7b9bRiwBTtOMqYCGZJkwXNLaN+OHygnYRB6uvdpZtQ2r5iGst4Gp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAbFwc5jjSVn0mRosF65UdGijcxuEjSCtcZ1FJdw5ivyBt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/wMNf+pBVqOVo2o97wHVhUnTPHJLKQfQACcUWqkFMJ8cDY8cKUFuzj5P9y31HJ3tGv+zAExz9w2PYfJwIR57VKfOoZeae4PVIDKvPZjV+TcLxjVjUXB6nSJ+zcj2Xk8cqZY6n2uAJq/JZn175WcnSk5FDty0P/UR+xgddr5f50QIm+atKTxlY3AqclMP7csB5lYQ+2kheOiTxDGk5n4GZQPjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+Fms8TbrAfwcTog9I8i1hEq1mjf2at1XxemsO1PgWdNcZPoJEaVIY0EtYx9OB4cDKWwDXw0TM6DZyIONc7cQ/m4t/lY4XyWMKHqpgKb2Jo6dHBWjA6uB3t1k9rdpbYFouYTAJwkAAAAABBcUdQAhBwEAJB4cAAEaBAgeABUbIQIWDwUOIhcYERsf1pBM7F+LMbQGAAAASmFydmlzBgAAAEphcnZpcyoAAABodHRwczovL21ldGFkYXRhLmo3dHJhY2tlci5pby9tL0JZb0xKSHRRQ2L/tXaEcLI8Rixgj0yDSQre8HX9KPzZWCCgJFer8BHdmwAAZAAAAAAAAAABABMAARUbGQEAHgEYGxshBwkSCgQIEQATFAwNHSMDBiAcFSIXCxB6HF6TtC3wYQEAAAAAAAAAAAAAAAAAAAAAAAAAgw1+BpmHqoMAhTGjvLFjV4MEbTxtP1SEpYWQovFRMhtxmBhG26VBISQeS8QmpL0SBh9o1YjjF53Ue2krshB+CBysZg8Aec3Sg21IuNC7s+rf2Ta0B2i01/jTbG8kvjq4jS5tIOC3J98yueJnUACfpvzSJ4p1DULLhQqDH5mcSg8=",
|
||||
"base64"
|
||||
],
|
||||
"version": 1,
|
||||
"slot": 447574345,
|
||||
"meta": {
|
||||
"err": null,
|
||||
"status": {
|
||||
"Ok": null
|
||||
},
|
||||
"fee": 10000,
|
||||
"logMessages": [
|
||||
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [1]",
|
||||
"Program log: Instruction: CreateV2",
|
||||
"Program 11111111111111111111111111111111 invoke [2]",
|
||||
"Program 11111111111111111111111111111111 success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: MetadataPointerInstruction::Initialize",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 925 of 586803 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: Instruction: InitializeMint2",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1772 of 584215 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program 11111111111111111111111111111111 invoke [2]",
|
||||
"Program 11111111111111111111111111111111 success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]",
|
||||
"Program log: Create",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
|
||||
"Program log: Instruction: GetAccountDataSize",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1447 of 562888 compute units",
|
||||
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb qgAAAAAAAAA=",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program 11111111111111111111111111111111 invoke [3]",
|
||||
"Program 11111111111111111111111111111111 success",
|
||||
"Program log: Initialize the associated token account",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
|
||||
"Program log: Instruction: InitializeImmutableOwner",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 556619 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
|
||||
"Program log: Instruction: InitializeAccount3",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1969 of 553547 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 18447 of 569721 compute units",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]",
|
||||
"Program log: Create",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
|
||||
"Program log: Instruction: GetAccountDataSize",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2116 of 512762 compute units",
|
||||
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb swAAAAAAAAA=",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program 11111111111111111111111111111111 invoke [3]",
|
||||
"Program 11111111111111111111111111111111 success",
|
||||
"Program log: Initialize the associated token account",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
|
||||
"Program log: Instruction: InitializeImmutableOwner",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 505824 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
|
||||
"Program log: Instruction: InitializeAccount3",
|
||||
"Program log: Warning: Mint has a permanent delegate, so tokens in this account may be seized at any time",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3682 of 502750 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 19333 of 518097 compute units",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
|
||||
"Program 11111111111111111111111111111111 invoke [2]",
|
||||
"Program 11111111111111111111111111111111 success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: TokenMetadataInstruction: Initialize",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3460 of 492408 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: TokenMetadataInstruction: UpdateAuthority",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3769 of 486698 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: Instruction: MintTo",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1714 of 480737 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: Instruction: SetAuthority",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1044 of 477087 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program data: G3KpTd7rY3YGAAAASmFydmlzBgAAAEphcnZpcyoAAABodHRwczovL21ldGFkYXRhLmo3dHJhY2tlci5pby9tL0JZb0xKSHRRQ2JF3KeJrTWespItdWZBTZZCiM9NOHw1kYmRupY3y44yTiAXWYxDAfbX9bgv8xlf/ZqnuA3D0Qfghjhqlq5J2bmu/7V2hHCyPEYsYI9Mg0kK3vB1/Sj82VggoCRXq/AR3ZubRqQN1bxu00er2xC9tTJZfM+sOuVrjqCMbqBkCZOYuVzLqmoAAAAAABDYR+PPAwBbiiIBAAAAAAB4xftR0QIAAIDGpH6NAwAG3fbh7nWP3hhCXbzkbM3athr8TYO5DSf+vfko2KGL/AAABTtOMqYCGZJkwXNLaN+OHygnYRB6uvdpZtQ2r5iGst5biiIBAAAAAGQAAAAAAAAAAQ==",
|
||||
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]",
|
||||
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 2143 of 469958 compute units",
|
||||
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
|
||||
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P consumed 133537 of 600000 compute units",
|
||||
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]",
|
||||
"Program log: CreateIdempotent",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: Instruction: GetAccountDataSize",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1523 of 459556 compute units",
|
||||
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb qgAAAAAAAAA=",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program 11111111111111111111111111111111 invoke [2]",
|
||||
"Program 11111111111111111111111111111111 success",
|
||||
"Program log: Initialize the associated token account",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: Instruction: InitializeImmutableOwner",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 453211 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]",
|
||||
"Program log: Instruction: InitializeAccount3",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2153 of 450142 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 18791 of 466463 compute units",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
|
||||
"Program J7pourVwqP1VtRNBiFNqbdGxwkx7ta2LHAURURRRJqmd invoke [1]",
|
||||
"Program log: Instruction: BuyPumpDelta",
|
||||
"Program 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P invoke [2]",
|
||||
"Program log: Instruction: BuyExactQuoteInV2",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [3]",
|
||||
"Program log: CreateIdempotent",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
|
||||
"Program log: Instruction: GetAccountDataSize",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2116 of 370557 compute units",
|
||||
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb swAAAAAAAAA=",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program 11111111111111111111111111111111 invoke [4]",
|
||||
"Program 11111111111111111111111111111111 success",
|
||||
"Program log: Initialize the associated token account",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
|
||||
"Program log: Instruction: InitializeImmutableOwner",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 363619 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
|
||||
"Program log: Instruction: InitializeAccount3",
|
||||
"Program log: Warning: Mint has a permanent delegate, so tokens in this account may be seized at any time",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3682 of 360545 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 19341 of 375900 compute units",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [3]",
|
||||
"Program log: CreateIdempotent",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
|
||||
"Program log: Instruction: GetAccountDataSize",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2116 of 344779 compute units",
|
||||
"Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb swAAAAAAAAA=",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program 11111111111111111111111111111111 invoke [4]",
|
||||
"Program 11111111111111111111111111111111 success",
|
||||
"Program log: Initialize the associated token account",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
|
||||
"Program log: Instruction: InitializeImmutableOwner",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 736 of 337841 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [4]",
|
||||
"Program log: Instruction: InitializeAccount3",
|
||||
"Program log: Warning: Mint has a permanent delegate, so tokens in this account may be seized at any time",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3682 of 334767 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 19341 of 350122 compute units",
|
||||
"Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success",
|
||||
"Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ invoke [3]",
|
||||
"Program log: Instruction: GetFeesWithQuoteMint",
|
||||
"Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ consumed 3402 of 325547 compute units",
|
||||
"Program return: pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ AAAAAAAAAABfAAAAAAAAAB4AAAAAAAAA",
|
||||
"Program pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ success",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [3]",
|
||||
"Program log: Instruction: TransferChecked",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 2562 of 317318 compute units",
|
||||
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
|
||||
"Log truncated"
|
||||
],
|
||||
"computeUnitsConsumed": 327006
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ async def assert_transaction_succeeded(
|
||||
RuntimeError: If it cannot be found or carries no execution metadata
|
||||
"""
|
||||
result = await client.get_transaction(
|
||||
signature, commitment=commitment, max_supported_transaction_version=0
|
||||
signature, commitment=commitment, max_supported_transaction_version=1
|
||||
)
|
||||
value = result.value
|
||||
if value is None:
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
"""Verify the bot still detects coins after the Solana transaction v1 cutover.
|
||||
|
||||
Transaction v1 (SIMD-0296 size, SIMD-0385 format) activated at epoch 1035 on
|
||||
2026-09-15. It is not an opt-in: other people's creates and trades arrive in
|
||||
whatever version they chose, and both pump.fun and letsbonk.fun already carry
|
||||
v1 traffic. Two things break a reader that ignores it.
|
||||
|
||||
`maxSupportedTransactionVersion` is a whole-frame setting, not a per-transaction
|
||||
one. Asking `blockSubscribe` for 0 does not skip the v1 transactions in a block,
|
||||
it nulls out `value.block` for the entire notification. Measured against mainnet
|
||||
on 2026-09-16, 60s per run: `0` delivered 1 block and 177 nulls, `1` delivered
|
||||
78 blocks and no nulls — the blocks listener was roughly 99% blind, silently,
|
||||
because a null frame looks exactly like a skipped slot.
|
||||
|
||||
The installed solders cannot deserialize a v1 transaction (they begin with byte
|
||||
129 and put the signatures at the tail). That is survivable, because the RPC has
|
||||
already decoded the envelope by the time it emits `meta.logMessages`, and the
|
||||
platform parsers prefer the CreateEvent in those logs anyway — for the canonical
|
||||
creator. It is only fatal if the listener insists on decoding the bytes *before*
|
||||
it will hand a transaction to a parser, which is what
|
||||
`_process_block_transactions` used to do.
|
||||
|
||||
Offline machine checks, no network and no funds moved:
|
||||
|
||||
1. Every blockSubscribe/getBlock/getTransaction call site in `src/` and
|
||||
`learning-examples/` asks for maxSupportedTransactionVersion >= 1 —
|
||||
both the raw JSON key and solana-py's snake_case keyword argument.
|
||||
2. The committed fixture really is a v1 transaction (version 1, byte 129)
|
||||
carrying a successful create_v2.
|
||||
3. The pump.fun event parser reads it into a TokenInfo — mint, creator and
|
||||
state_from_event, so extreme_fast_mode keeps its zero-RPC contract.
|
||||
4. The block listener returns that TokenInfo for the fixture, which is the
|
||||
regression that matters: it must not depend on the envelope decoding.
|
||||
5. The listener still reads a pre-v1 create, so nothing regressed for the
|
||||
coins that already worked.
|
||||
6. With the logs removed, the v1 create is NOT detected — which is what makes
|
||||
the log route load-bearing rather than decorative. If this check starts
|
||||
failing, solders has learned to decode a v1 envelope and the routing could
|
||||
be simplified.
|
||||
|
||||
Usage:
|
||||
uv run learning-examples/verify_transaction_v1.py
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||
|
||||
from core.client import SolanaClient # noqa: E402
|
||||
from interfaces.core import Platform # noqa: E402
|
||||
from monitoring.universal_block_listener import UniversalBlockListener # noqa: E402
|
||||
from platforms import get_platform_implementations # noqa: E402
|
||||
|
||||
V1_FIXTURE = (
|
||||
PROJECT_ROOT / "learning-examples" / "raw_create_v2_v1_from_gettransaction.json"
|
||||
)
|
||||
# A pre-v1 create, already committed for the optional-args verifier. Reused
|
||||
# rather than capturing another one: it only has to be a create the listener
|
||||
# still detects.
|
||||
V0_FIXTURE = (
|
||||
PROJECT_ROOT
|
||||
/ "learning-examples"
|
||||
/ "raw_create_v2_with_fee_bps_from_gettransaction.json"
|
||||
)
|
||||
|
||||
# Solana tags a v1 transaction with this first byte (SIMD-0385 VersionByte).
|
||||
V1_VERSION_BYTE = 129
|
||||
|
||||
MIN_SUPPORTED_VERSION = 1
|
||||
|
||||
SCAN_ROOTS = ("src", "learning-examples")
|
||||
# Two spellings reach the same RPC field: the raw JSON key used by hand-built
|
||||
# request bodies, and solana-py's snake_case keyword argument. A scan that knows
|
||||
# only the first one misses tx_status.py, which every example confirms through.
|
||||
MSV_PATTERN = re.compile(
|
||||
r"[\"']maxSupportedTransactionVersion[\"']\s*:\s*(\d+)"
|
||||
r"|max_supported_transaction_version\s*=\s*(\d+)"
|
||||
)
|
||||
|
||||
|
||||
class _OfflineClient(SolanaClient):
|
||||
"""A SolanaClient that never opens a connection.
|
||||
|
||||
The parsers only need address derivation here, and SolanaClient.__init__
|
||||
starts a blockhash updater task.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.rpc_endpoint = "http://offline.invalid"
|
||||
self._client = None
|
||||
self._cached_blockhash = None
|
||||
self._blockhash_lock = None
|
||||
self._blockhash_updater_task = None
|
||||
|
||||
|
||||
def _load(path: Path) -> dict:
|
||||
with path.open() as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def _as_block_transaction(result: dict) -> dict:
|
||||
"""Shape a getTransaction result like one blockSubscribe transaction."""
|
||||
return {"transaction": result["transaction"], "meta": result["meta"]}
|
||||
|
||||
|
||||
def check_every_call_site_accepts_v1() -> bool:
|
||||
"""No reader may ask for a version below v1.
|
||||
|
||||
A call site left at 0 does not degrade gracefully — it loses whole blocks.
|
||||
"""
|
||||
stale: list[str] = []
|
||||
for root in SCAN_ROOTS:
|
||||
for path in sorted((PROJECT_ROOT / root).rglob("*.py")):
|
||||
if "generated" in path.parts:
|
||||
continue
|
||||
text = path.read_text()
|
||||
for match in MSV_PATTERN.finditer(text):
|
||||
value = match.group(1) or match.group(2)
|
||||
if int(value) < MIN_SUPPORTED_VERSION:
|
||||
line = text[: match.start()].count("\n") + 1
|
||||
stale.append(f"{path.relative_to(PROJECT_ROOT)}:{line}")
|
||||
if stale:
|
||||
print(" call sites still below v1: " + ", ".join(stale))
|
||||
return not stale
|
||||
|
||||
|
||||
def check_fixture_is_a_v1_create() -> bool:
|
||||
"""The fixture has to be the real thing or the rest proves nothing."""
|
||||
result = _load(V1_FIXTURE)
|
||||
if result.get("version") != 1:
|
||||
print(f" fixture version is {result.get('version')!r}, expected 1")
|
||||
return False
|
||||
if result["meta"].get("err") is not None:
|
||||
print(" fixture create failed on chain; a failed create has no mint")
|
||||
return False
|
||||
|
||||
raw = base64.b64decode(result["transaction"][0])
|
||||
if raw[0] != V1_VERSION_BYTE:
|
||||
print(f" first byte is {raw[0]}, expected {V1_VERSION_BYTE}")
|
||||
return False
|
||||
|
||||
logs = result["meta"].get("logMessages") or []
|
||||
if not any("Instruction: CreateV2" in line for line in logs):
|
||||
print(" fixture carries no CreateV2 instruction")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_parser_reads_the_v1_create() -> bool:
|
||||
"""The platform parser works off logs, so the version is irrelevant to it."""
|
||||
parser = get_platform_implementations(
|
||||
Platform.PUMP_FUN, _OfflineClient()
|
||||
).event_parser
|
||||
token_info = parser.parse_token_creation_from_block(
|
||||
{"transactions": [_as_block_transaction(_load(V1_FIXTURE))]}
|
||||
)
|
||||
if token_info is None:
|
||||
print(" parser returned None for a v1 create_v2")
|
||||
return False
|
||||
if not token_info.state_from_event:
|
||||
print(" state_from_event is False — extreme_fast_mode would refresh")
|
||||
return False
|
||||
if token_info.mint is None or token_info.creator is None:
|
||||
print(" parsed TokenInfo is missing mint or creator")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_listener_detects_the_v1_create() -> bool:
|
||||
"""The regression under test: dispatch must not require a byte decode.
|
||||
|
||||
solders cannot deserialize a v1 envelope, so a listener that decodes first
|
||||
and dispatches second drops the coin without raising anything.
|
||||
"""
|
||||
listener = UniversalBlockListener("wss://offline.invalid", [Platform.PUMP_FUN])
|
||||
result = _load(V1_FIXTURE)
|
||||
token_info = listener._process_block_transactions( # noqa: SLF001
|
||||
[_as_block_transaction(result)]
|
||||
)
|
||||
if token_info is None:
|
||||
print(" block listener returned None for a v1 create_v2")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_log_route_is_what_detects_v1() -> bool:
|
||||
"""Strip the logs and the v1 create must vanish.
|
||||
|
||||
Proves the log route is doing the work, rather than passing because some
|
||||
other path happens to cope. solders cannot decode a v1 envelope, so with
|
||||
no logs there is nothing left to parse.
|
||||
"""
|
||||
listener = UniversalBlockListener("wss://offline.invalid", [Platform.PUMP_FUN])
|
||||
result = _load(V1_FIXTURE)
|
||||
tx = _as_block_transaction(result)
|
||||
tx["meta"] = {**tx["meta"], "logMessages": []}
|
||||
token_info = listener._process_block_transactions([tx]) # noqa: SLF001
|
||||
if token_info is not None:
|
||||
print(" a v1 create parsed without its logs — routing may have changed")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_listener_still_detects_a_pre_v1_create() -> bool:
|
||||
"""The log route is an addition, not a replacement."""
|
||||
listener = UniversalBlockListener("wss://offline.invalid", [Platform.PUMP_FUN])
|
||||
result = _load(V0_FIXTURE)
|
||||
if result.get("version") == 1:
|
||||
print(" the legacy/v0 fixture is itself v1; the check proves nothing")
|
||||
return False
|
||||
token_info = listener._process_block_transactions( # noqa: SLF001
|
||||
[_as_block_transaction(result)]
|
||||
)
|
||||
if token_info is None:
|
||||
print(" block listener returned None for a pre-v1 create")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
checks = [
|
||||
("every call site accepts v1", check_every_call_site_accepts_v1),
|
||||
("fixture is a successful v1 create_v2", check_fixture_is_a_v1_create),
|
||||
("event parser reads the v1 create", check_parser_reads_the_v1_create),
|
||||
("block listener detects the v1 create", check_listener_detects_the_v1_create),
|
||||
(
|
||||
"block listener still detects a pre-v1 create",
|
||||
check_listener_still_detects_a_pre_v1_create,
|
||||
),
|
||||
("the log route is what detects v1", check_log_route_is_what_detects_v1),
|
||||
]
|
||||
failed = 0
|
||||
for label, check in checks:
|
||||
try:
|
||||
ok = check()
|
||||
except Exception as error: # noqa: BLE001 - report and continue
|
||||
print(f"FAIL {label}: {type(error).__name__}: {error}")
|
||||
failed += 1
|
||||
continue
|
||||
print(f"{'PASS' if ok else 'FAIL'} {label}")
|
||||
failed += 0 if ok else 1
|
||||
print(f"\n{len(checks) - failed}/{len(checks)} checks passed")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -369,7 +369,7 @@ async def check_rpc_timeouts_are_retried_not_raised() -> None:
|
||||
async def check_versioned_transactions_are_requested() -> None:
|
||||
"""getTransaction must opt in to versioned transactions.
|
||||
|
||||
Without `maxSupportedTransactionVersion` the RPC answers -32015 for every v0
|
||||
Without `maxSupportedTransactionVersion` the RPC answers -32015 for every v0/v1
|
||||
transaction, so `meta.err` is unreadable and a successful trade reads back as
|
||||
unconfirmed. The bot currently sends legacy transactions, which is the only
|
||||
reason this was survivable.
|
||||
@@ -386,7 +386,7 @@ async def check_versioned_transactions_are_requested() -> None:
|
||||
|
||||
assert bodies, "no RPC issued"
|
||||
config = bodies[0]["params"][1]
|
||||
assert config.get("maxSupportedTransactionVersion") == 0, (
|
||||
assert config.get("maxSupportedTransactionVersion") == 1, (
|
||||
f"getTransaction omits maxSupportedTransactionVersion: {config}"
|
||||
)
|
||||
|
||||
|
||||
+6
-3
@@ -787,10 +787,13 @@ class SolanaClient:
|
||||
{
|
||||
"encoding": "jsonParsed",
|
||||
"commitment": "confirmed",
|
||||
# Without this the RPC rejects every versioned (v0)
|
||||
# Without this the RPC rejects every versioned
|
||||
# transaction with -32015, so meta.err cannot be read and a
|
||||
# perfectly good trade reads back as unconfirmed.
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
# perfectly good trade reads back as unconfirmed. It has to
|
||||
# cover v1 (live since 2026-09-15), not just v0: the bot
|
||||
# sends legacy transactions, but this method is also used to
|
||||
# read back signatures it did not send.
|
||||
"maxSupportedTransactionVersion": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -179,7 +179,14 @@ class UniversalBlockListener(BaseTokenListener):
|
||||
"encoding": "base64",
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
# Solana transaction v1 (SIMD-0296/0385) has been
|
||||
# live since epoch 1035, 2026-09-15. A version the
|
||||
# subscription does not accept does not just skip
|
||||
# that transaction: the RPC nulls out `value.block`
|
||||
# for the whole frame. Measured against mainnet on
|
||||
# 2026-09-16, 60s each: `0` delivered 1 block and
|
||||
# 177 nulls, `1` delivered 78 blocks and no nulls.
|
||||
"maxSupportedTransactionVersion": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -302,6 +309,16 @@ class UniversalBlockListener(BaseTokenListener):
|
||||
if isinstance(meta, dict) and meta.get("err") is not None:
|
||||
continue
|
||||
|
||||
# Route on the logs before touching the transaction itself. The
|
||||
# decoding below exists only to learn which program the transaction
|
||||
# touched, and solders cannot deserialize a v1 envelope at all — so
|
||||
# gating the dispatch on it drops every v1 coin without raising
|
||||
# anything the caller sees. The parsers prefer meta.log_messages
|
||||
# anyway, for the canonical creator; this just lets them be reached.
|
||||
token_info = self._parse_from_logs(tx)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
tx_data = tx["transaction"]
|
||||
|
||||
# Handle base64 encoded transaction data
|
||||
@@ -357,8 +374,42 @@ class UniversalBlockListener(BaseTokenListener):
|
||||
continue
|
||||
|
||||
except Exception:
|
||||
# Failed to decode transaction - skip it
|
||||
pass
|
||||
# A transaction the installed solders cannot deserialize — a v1
|
||||
# one, for instance. Logged rather than swallowed: silence here is
|
||||
# what made the v1 cutover look like a quiet drop in detections.
|
||||
logger.debug("Could not decode a block transaction envelope")
|
||||
|
||||
return None
|
||||
|
||||
def _parse_from_logs(self, tx: dict) -> TokenInfo | None:
|
||||
"""Dispatch one transaction to its platform parser using its logs.
|
||||
|
||||
Works for any transaction version, because the RPC has already decoded
|
||||
the envelope by the time the logs are emitted.
|
||||
|
||||
Args:
|
||||
tx: Transaction wrapper from block, including its `meta`
|
||||
|
||||
Returns:
|
||||
TokenInfo if a token creation is found, None otherwise
|
||||
"""
|
||||
meta = tx.get("meta")
|
||||
if not isinstance(meta, dict):
|
||||
return None
|
||||
|
||||
logs = meta.get("logMessages") or meta.get("log_messages")
|
||||
if not logs:
|
||||
return None
|
||||
|
||||
for program_id, (_platform, parser) in self.program_id_to_parser.items():
|
||||
if not any(program_id in line for line in logs):
|
||||
continue
|
||||
# parse_token_creation_from_block contains its own failures and
|
||||
# answers None, so a parser that cannot read this transaction just
|
||||
# hands the next one its turn.
|
||||
token_info = parser.parse_token_creation_from_block({"transactions": [tx]})
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user